FFmpeg入门 - Android移植

news/2024/7/10 20:57:33 标签: ffmpeg, android, 音视频

1、FFmpeg编译与集成


FFmpeg的安卓交叉编译网上有很多的资料,基本上都是些编译配置而已。可以直接将我的脚本放到ffmpeg源码根目录,修改下NDK的路径和想要编译的ABI之后直接执行。然后就能在android目录里面得到编译好的so和.h

如果的确编译出现问题,也可以直接用我编出来的库。

将库放到AndroidStudio工程的jniLibs目录,将include目录放到app/src/main/cpp下,然后修改CMakeLists.txt添加ffmpeg头文件路径、库路径、链接配置等:

cmake_minimum_required(VERSION 3.18.1)

project("ffmpegdemo")

add_library(ffmpegdemo SHARED ffmpeg_demo.cpp video_sender.cpp opengl_display.cpp egl_helper.cpp video_decoder.cpp)

find_library(log-lib log)

# 头文件路径

include_directories(${CMAKE_SOURCE_DIR}/include)

# ffmpeg库依赖

add_library(avcodec SHARED IMPORTED)

set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libavcodec.so)

add_library(avfilter SHARED IMPORTED)

set_target_properties(avfilter PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libavfilter.so)

add_library(avformat SHARED IMPORTED)

set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libavformat.so)

add_library(avutil SHARED IMPORTED)

set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libavutil.so)

add_library(swresample SHARED IMPORTED)

set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libswresample.so)

add_library(swscale SHARED IMPORTED)

set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libswscale.so)

target_link_libraries(

ffmpegdemo

# log

${log-lib}

EGL

GLESv2

android

# FFmpeg libs

avcodec

avfilter

avformat

avutil

swresample

swscale

)

这样一套下来其实ffmpeg的安卓环境就整好了,我们把之前的video_sender.cpp和video_sender.h拷贝过来添加个jni的接口验证下推流:

// java

Filefile=newFile(getFilesDir(), "video.flv");

try {

InputStreamis=getAssets().open("video.flv");

OutputStreamos=newFileOutputStream(file);

FileUtils.copy(is, os);

} catch (Exceptione) {

Log.d("FFmpegDemo", "err", e);

}

newThread(newRunnable() {

@Override

publicvoidrun() {

send(file.getAbsolutePath(), "rtmp://"+SERVER_IP+"/live/livestream");

}

}).start();

//jni

extern"C"JNIEXPORTvoidJNICALL

Java_me_linjw_demo_ffmpeg_MainActivity_send(

JNIEnv*env,

jobject/* this */,

jstringsrcFile,

jstringdestUrl) {

constchar*src=env->GetStringUTFChars(srcFile, NULL);

constchar*dest=env->GetStringUTFChars(destUrl, NULL);

LOGD("send: %s -> %s", src, dest);

VideoSender::Send(src, dest);

}

然后就可以用安卓去推流,在pc上用之前的demo进行播放验证。

2、OpenGLES播放FFmpeg


之前的demo使用SDL2播放视频,但是安卓上更常规的做法是通过OpenGLES去播放。其实之前在做摄像教程的时候已经有介绍过OpenGLES的使用了:

安卓特效相机(二) EGL基础

安卓特效相机(三) OpenGL ES 特效渲染

这篇我们就只补充下之前没有提到的部分。

3、YUV


首先有个很重要的知识点在于我们的视频很多情况下解码出来都是YUV格式的画面而不是安卓应用开发常见的RGB格式。

YUV是编译true-color颜色空间(color space)的种类,Y'UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。“Y”表示明亮度(Luminance、Luma),“U”和“V”则是色度、浓度(Chrominance、Chroma),也就是说通过UV可以选择到一种颜色:

1.png

然后再加上这种颜色的亮度就能代表我们实际看到的颜色。

YUV的发明是由于彩色电视与黑白电视的过渡时期,黑白电视只有亮度的值(Y)到了彩色电视的时代为了兼容之前的黑白电视,于是在亮度值后面加上了UV值指定颜色,如果忽略了UV那么剩下的Y,就和黑白电视的信号保持一致。

这种情况下数据是以 平面格式(planar formats) 去保存的,类似YYYYUUUUVVVV,YUV三者分开存放。 另外也有和常见的RGB存放方式类似的 紧缩格式(packed formats) ,类似YUVYUVYUV,每个像素点的YUV数据连续存放。

由于人的肉眼对亮度敏感对颜色相对不敏感,所以我们可以相邻的几个像素共用用UV信息,减少数据带宽。

这里的共用UV信息并没有对多个像素点做UV数据的均值,而是简单的跳过一些像素点不去读取他们的UV数据。

3.1 YUV444

每个像素都有自己的YUV数据,每个像素占用Y + U + V = 8 + 8 + 8 = 24 bits

YUV444.png

444的含义是同一行相邻的4个像素,分别采样4个Y,4个U,4个V

3.2 YUV422

每两个像素共用一对UV分量,每像素平均占用Y + U + V = 8 + 4 + 4 = 16 bits

YUV422.png

422的含义是同一行相邻的4个像素,分别采样4个Y,2个U,2个V

3.3 YUV420

每四个像素共用一对UV分量,每像素平均占用Y + U + V = 8 + 2 + 2 = 12 bits

YUV420.png

YUV420在YUV422的基础上再隔行扫描UV信息,一行只采集U,下一行只采集V

420的含义是同一行相邻的4个像素,分别采样4个Y,2个U,0个V,或者4个Y,0个U,2个V

4、OpenGLES显示YUV图像


由于OpenGLES使用RGB色彩,所以我们需要在fragmentShader里面将YUV转成RGB,转换公式如下:

R = Y + 1.4075 * V; G = Y - 0.3455 * U - 0.7169*V; B = Y + 1.779 * U;

由于解码之后的数据使用平面格式(planar formats)保存,所以我们可以创建三张灰度图图片分别存储YUV的分量,另外由于OpenGLES里面色彩的值范围是0~1.0,而UV分量的取值范围是-0.5~0.5所以我们UV分量统一减去0.5做偏移.于是fragmentShader代码如下:

staticconststringFRAGMENT_SHADER="#extension GL_OES_EGL_image_external : require\n"

"precision highp float;\n"

"varying vec2 vCoord;\n"

"uniform sampler2D texY;\n"

"uniform sampler2D texU;\n"

"uniform sampler2D texV;\n"

"varying vec4 vColor;\n"

"void main() {\n"

" float y = texture2D(texY, vCoord).x;\n"

" float u = texture2D(texU, vCoord).x - 0.5;\n"

" float v = texture2D(texV, vCoord).x - 0.5;\n"

" float r = y + 1.4075 * v;\n"

" float g = y - 0.3455 * u - 0.7169 * v;\n"

" float b = y + 1.779 * u;\n"

" gl_FragColor = vec4(r, g, b, 1);\n"

"}";

接着由于OpenGLES里面纹理坐标原点是左下角,而解码的画面原点是左上角,所以纹理坐标需要上下调换一下:

staticconstfloatVERTICES[] = {

-1.0f, 1.0f,

-1.0f, -1.0f,

1.0f, -1.0f,

1.0f, 1.0f

};

// 由于OpenGLES里面纹理坐标原点是左下角,而解码的画面原点是左上角,所以纹理坐标需要上下调换一下

staticconstfloatTEXTURE_COORDS[] = {

0.0f, 0.0f,

0.0f, 1.0f,

1.0f, 1.0f,

1.0f, 0.0f

};

staticconstshortORDERS[] = {

0, 1, 2, // 左下角三角形

2, 3, 0 // 右上角三角形

};

最后就只要将每帧解析出来的图像交给OpenGLES去渲染就好:

AVFrame*frame;

while ((frame=decoder.NextFrame()) !=NULL) {

eglHelper.MakeCurrent();

display.Render(frame->data, frame->linesize);

eglHelper.SwapBuffers();

}

5、linesize


接着我们就需要根据这些YUV数据创建三个灰度图分别存储各个分量的数据。这里有个知识点,解码得到的YUV数据,高是对应分量的高,但是宽却不一定是对应分量的宽.

这是因为在做视频解码的时候会对宽进行对齐,让宽是16或者32的整数倍,具体是16还是32由cpu决定.例如我们的video.flv视频,原始画面尺寸是289*160,如果按32去对齐的话,他的Y分量的宽则是320.

对齐之后的宽在ffmpeg里面称为linesize,而由于我们这个demo只支持YUV420的格式,它的Y分量的高度为原始图像的高度,UV分量的高度由于是隔行扫描,所以是原生图像高度的一半:

voidOpenGlDisplay::Render(uint8_t*yuv420Data[3], intlineSize[3]) {

// 解码得到的YUV数据,高是对应分量的高,但是宽却不一定是对应分量的宽

// 这是因为在做视频解码的时候会对宽进行对齐,让宽是16或者32的整数倍,具体是16还是32由cpu决定

// 例如我们的video.flv视频,原始画面尺寸是689x405,如果按32去对齐的话,他的Y分量的宽则是720

// 对齐之后的宽在ffmpeg里面称为linesize

// 而对于YUV420来说Y分量的高度为原始图像的高度,UV分量的高度由于是隔行扫描,所以是原生图像高度的一半

setTexture(0, "texY", yuv420Data[0], lineSize[0], mVideoHeight);

setTexture(1, "texU", yuv420Data[1], lineSize[1], mVideoHeight/2);

setTexture(2, "texV", yuv420Data[2], lineSize[2], mVideoHeight/2);

// 由于对齐之后创建的纹理宽度大于原始画面的宽度,所以如果直接显示,视频的右侧会出现异常

// 所以我们将纹理坐标进行缩放,忽略掉右边对齐多出来的部分

GLintscaleX=glGetAttribLocation(mProgram, "aCoordScaleX");

glVertexAttrib1f(scaleX, mVideoWidth*1.0f/lineSize[0]);

glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);

glDrawElements(GL_TRIANGLES, sizeof(ORDERS) /sizeof(short), GL_UNSIGNED_SHORT, ORDERS);

}

另外由于对齐之后创建的纹理宽度大于原始画面的宽度,所以如果直接显示,视频的右侧会出现异常:

2.png

所以我们将纹理坐标进行缩放,忽略掉右边对齐多出来的部分:

// VERTICES_SHADER

vCoord=vec2(aCoord.x*aCoordScaleX, aCoord.y);

6、保持视频长宽比


虽然视频能正常播放了,但是可以看到整个视频是铺满屏幕的。所以我们需要对视频进行缩放让他保持长宽比然后屏幕居中:

voidOpenGlDisplay::SetVideoSize(intvideoWidth, intvideoHeight) {

mVideoWidth=videoWidth;

mVideoHeight=videoHeight;

// 如果不做处理(-1.0f, 1.0f),(-1.0f, -1.0f),(1.0f, -1.0f),(1.0f, 1.0f)这个矩形会铺满整个屏幕导致图像拉伸

// 由于坐标的原点在屏幕中央,所以只需要判断是横屏还是竖屏然后对x轴或者y轴做缩放就能让图像屏幕居中,然后恢复原始视频的长宽比

if (mWindowHeight>mWindowWidth) {

// 如果是竖屏的话,图像的宽不需要缩放,图像的高缩小使其竖直居中

GLintscaleX=glGetAttribLocation(mProgram, "aPosScaleX");

glVertexAttrib1f(scaleX, 1.0f);

// y坐标 * mWindowWidth / mWindowHeight 得到屏幕居中的正方形

// 然后再 * videoHeight / videoWidth 就能恢复原始视频的长宽比

floatr=1.0f*mWindowWidth/mWindowHeight*videoHeight/videoWidth;

GLintscaleY=glGetAttribLocation(mProgram, "aPosScaleY");

glVertexAttrib1f(scaleY, r);

} else {

// 如果是横屏的话,图像的高不需要缩放,图像的宽缩小使其水平居中

GLintscaleY=glGetAttribLocation(mProgram, "aPosScaleY");

glVertexAttrib1f(scaleY, 1.0f);

// x坐标 * mWindowHeight / mWindowWidth 得到屏幕居中的正方形

// 然后再 * videoWidth / videoHeight 就能恢复原始视频的长宽比

floatr=1.0f*mWindowHeight/mWindowWidth*videoWidth/videoHeight;

GLintscaleX=glGetAttribLocation(mProgram, "aPosScaleX");

glVertexAttrib1f(scaleX, r);

}

}

// VERTICES_SHADER

gl_Position=vec4(aPosition.x*aPosScaleX, aPosition.y*aPosScaleY, 0, 1);

3.jpeg

7、Demo工程


完整的代码已经上传到Github

原文链接:https://www.jianshu.com/p/754ce4ac4

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓


http://www.niftyadmin.cn/n/118804.html

相关文章

简单介绍编程进制

十进制 十进制的位权为 10,比如十进制的 123,123 1 * 10 ^ 2 2 * 10 ^ 1 3 * 10 ^ 0。 二进制 二进制的位权为 2,比如十进制的 4,二进制为 100,4 1 * 2 ^ 2 0 * 2 ^ 1 0 *2 ^ 0。 Java7 之前,不支…

【独家】华为OD机试提供C语言题解 - 数字加减游戏

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 最近更新的博客使用说明数字…

17、MySQL高可用和读写分离架构

MySQL高可用和读写分离架构 1、MHA1.2 架构和相关组件1.3 工作流程1.4 MHA高可用架构部署1.4.1 环境准备1.4.2 软件安装1.4.3 创建软链接1.4.4 配置各节点互信1.4.5 节点免密验证1.4.6 mha管理用户1.4.7 配置文件1.4.8 状态检查1.4.9 开启MHA1.5 主库宕机故障模拟及处理1.6.MHA…

关于slice扩容性能损耗的探究

背景 ​ 如果让我评选最伟大的数据结构,在我心中答案只有两个,数组和哈希表,这两个是我的程序的重要组成部分,同时也是我饭碗的重要组成部分。slice和map简洁明了的API很容易让我们有一种他们提供了无限大的空间,可以…

绝对零基础的C语言科班作业(期末模拟考试)(十道编程题)

编程题(共10题; 共100.0分)(给猛男妙妙屋更一篇模拟考试)模拟1(输出m到n的素数)从键盘输入两个整数[m,n], 输出m和n之间的所有素数。 输入样例:3,20输出样例:…

Linux命令行安装Oracle19c教程和踩坑经验

安装 下载 从 Oracle官方下载地址 需要的版本,本次安装是在Linux上使用yum安装,因此下载的是RPM。另外,需要说明的是,Oracle加了锁的下载需要登录用户才能安装,而用户是可以免费注册的,这里不做过多说明。 …

W800系列|ST-LINK|STM32最小版|HEX文件|CKLINK|DebugServer|学习(3-1):自制cklink调试工具

硬件准备 1.stm32最小系统板:stm32c8t6 2.stlink下载器:stlink v2版本 软件安装: 1、STM32 ST-LINK Utility(官网下载) 2、csky IDE: 剑池CDK集成开发环境V2.18.2 https://occ.t-head.cn/community/download?id…

ubuntu 将jupyter-lab保存为桌面快捷方式和favourites

ubuntu: 将jupyter-lab保存为桌面快捷方式和favourites desktop shortcut 建立一个新的desktop文件 cd ~/Desktop touch Jupyter-lab.desktop将文件修改成如下: [Desktop Entry] Version1.0 NameJupyterlab CommentBack up your data with one click Exec/home/cjb/…