8月6日,暴风CEO冯鑫(微博)参加了2017极客公园奇点创新者峰会,会后冯鑫在接受腾讯科技等媒体采访时说,暴风从去年开始在主要平台上全力拥抱AI,在暴风TV上,团队已经基于信息流进行了大改版。冯鑫说,信息流可以提升用户观看电视的效率,今年5月暴风发布的人工智能电视65 X5 ECHO已经具备了初步的信息流展示功能,目前正在优化体验,将在今年9月发布ECHO 2.0版本。
作者简介本篇来自 剑西 的投稿,主要介绍了FFmpeg如何进行Android视频的录制和压缩,由于部分代码偏长,所以我就做了省略标记,感兴趣的朋友可以点击最后 阅读原文 查看,希望大家会喜欢。
剑西 的博客地址:
预热http://my.csdn.net/mabeijianxi
时光荏苒,光阴如梭,离上一次吹牛逼已经过去了两三个月,身边很多人的女票已经分了又合,合了又分,本屌依旧骄傲单身。上一次啊我们大致说了一些简单的 FFmpeg 命令以及 Java 层简单的调用方式,然后有很多朋友在 github 或者 csdn 上给我留言,很多时候我都选择避而不答,原因是本库以前用的 so 包是不开源的,我根本改不了里面东西。但是这一次啊我们玩点大的,我重新编译了 FFmpeg 且重写 JNI 的接口函数,这次将从 C 到 Java 全面开源,2.0项目花了本尊两个多月的业余时间,今天终于完工,非常鸡冻,且本博客将抒发出作者的全部心声,有没有很鸡冻,有没有。鸡冻之余,我也想吐槽下其软编的效率,确实不是很高,3.0的时候将会试试硬编码,或则在2.0迭代的时候会采用H265编码,这都是后话了,不过看微信把小视频换成大视频的节奏,地址如下:
http://lib.csdn.net/base/wechat
应该可以搞。
本文涉及的知识点
Andorid 视频和音频采集
YUV 视频处理(手动剪切、旋转、镜像等) PCM 音频处理
利用 FFmpeg API ,YUV 编码为H264、PCM 编码为 AAC
FFmpeg 编码器的配置
JNI 在工程中的实际运用
Android 下 FFmpeg 命令工具的制作与应用
Android Studio 插件 cMake 在工程中的应用
充能
至少需要知道 YUV、PCM、MP4 是什么,视音频编解码技术零基础学习方法,地址如下:
http://blog.csdn.net/mabeijianxi/article/details/72910638
最好能先阅读编译 Android 下可用的 FFmpeg(包含libx264与libfdk-aac),地址如下:
http://blog.csdn.net/mabeijianxi/article/details/72888067
编译 Android 下可执行命令的 FFmpeg,地址如下:
http://blog.csdn.net/mabeijianxi/article/details/72904694
Android 下玩 JNI 的新老三种姿势,地址如下:
http://blog.csdn.net/mabeijianxi/article/details/68525164
为了不太啰嗦,这些文章中分享过的大多数知识将不再重复。
对C/C++基本语法有基本的了解。
本人环境与工具
系统: macOS-10.12.5
编译器: Android Studio-2.3.2
ndk: r14
FFmpeg: 3.2.5
效果图
项目地址没变:
https://github.com/mabeijianxi/small-video-record
这里复用了1.0版本的 gif 图,因为界面一点没变,功能的话暂时没封装那么多,没关系后期会补上。
整体流程
工程目录浏览
新建项目我们新建一个项目,也许与以往不同,需要勾选上 C++ 支持与 C++ standard选项时选择 C++ 11,如下图:
C++ 支持是必须的,至于选用 C++ 11也是有原因的,后面我们会用的里面的一些API。然后我们把在编译 Android 下可用的 FFmpeg(包含libx264与libfdk-aac) 中编译好的六个动态库,地址如下:
http://blog.csdn.net/mabeijianxi/article/details/72888067
头文件还有 cmdutils.c cmdutils.h cmdutils_common_opts.h config.h ffmpeg.c ffmpeg.h ffmpeg_filter.c ffmpeg_opt.c copy 到我们工程的 cpp 目录下,完成后你 cpp目录应该如下
也许你会比我多一个自动生成的 native-lib.cpp ,这个文件暂时保留它。
编写 JNI 接口我新建了一个接口类 FFmpegBridge.java,且根据我的需求暂时定义了如下方法:
你新建这些方法的时候由于 native 没有定义,这时候它们都会爆红,不要担心不要纠结,光标放到对应的方法上,轻轻按下 Atl + Enter 你就会出现如图的效果了:
再次确定之后这个接口就会在 native 添加。我不太喜欢叫 native-lib.cpp ,于是我改成了jx_ffmpeg_jni.cpp,其内容暂时如下:
编写 Native 代码我用c/c++用的不多,Java又用习惯了,所以在命名上有时候很纠结,看不惯亲的怎么办?那就些许的忍一忍吧~~
准备 log 函数
不管玩什么语言,没日志玩毛线啊,所以这是第一步。新建 jx_log.cpp 与 jx_log.h。
jx_log.h:
jx_log.cpp:
当然我们也定义了一个是否开启 debug 的标志 JNI_DEBUG。
准备好可执行命令的FFmepg接口
这里假设你已经看完了编译 Android 下可执行命令的 FFmpeg,地址如下:
http://blog.csdn.net/mabeijianxi/article/details/72904694
因为我们要对之前 copy 进来的源码做些修改,不然没法用的。我们新建两个文件来对接FFmpeg,文件中一个函数给 Java 层调用,一个给 Native 调用,还有一个是初始化 debug 控制日志用的,可以先不管。
jx_ffmpeg_cmd_run.h:
jx_ffmpeg_cmd_run.c:
一口气写到这里,必定会四处爆红,惨不忍睹,各种找不到文件,找不到方法,那是因为你添加了这么多文件,cMake 工具不知道,正确的做法是每添加一个 C/C++ 文件然后就去 CMakeLists.txt 里面告诉人家一声,完了还别忘了点击 Sync 同步下子。
CMakeLists.txt 的编写
先强上一个脚本:
代码省略
当然这个脚本是整个完整工程的,有些文件我们到后面才会建出来,现在就忍耐一下,如果你不想被爆红那么就需要每添加一个文件然后就在第一个 add_library 里面也添加一下,再点击 Android Studio 的同步按钮。 里面其他 library 都是我们事先编译好 copy 进来的,所以采用预构建的方式添加,这里都是相对路径,所以你不需要修改什么。
include_directories 里面写上你已经编译过的源码的路径,很关键。这里面的头文件才是全的~。
准备一个安全的队列
我们在采集音视频数据后会发送给 FFmpeg 做一系列的处理,由于是软编码所以编码快慢和 CPU 有很大的关系,就现在的x264的算法,结合当今的 CPU 是跟不上咋们采集每秒20帧+的速度的,直接采集一帧就编码一帧的话肯定会丢帧的,所以我决定把它放入一个队里里面,由于存在多线程编程,我们的队列需要 safety,就跟几个男的抢一个妹子一样,妹子自然需要我这样的人保护她咯。这个队列的代码是我网上 copy 的,没啥说的~~
threadsafe_queue.cpp
这里面用的几个 lib 就是 C++ 11标准里面的啦~
准备一个存储配置信息的结构体
其实这玩意和 JavaBean 差不多嘛,直接搞代码,代码中的 JXJNIHandler 字段姑且当做没看到。
jx_user_arguments.h:
这个结构体在整个过程中都会用到。
编写一个 base.sh
其实啊,当时写这个头文件是不想老去 include 同样的东西,我们视频编码与音频编码都需要要 include 的头文件放在了这里,并且定义了一些规则性的宏。
base_include.h:
FFmpeg 源码 C 的,include 时 extern "C" 很关键
编写视频(YUV)编码代码
这小节是本文的核心之一,简化后的思路是这样的:
有的兄弟可能会问为什么不编码一帧合成一帧,因为啊我测试了下合成时间,基本都是毫秒级别的,还有就是嫌麻烦,我这样做的话直接用我们制作的 FFmpeg 命令工具然后几行代码就搞定了,先上代码。
jx_yuv_encode_h264.h:
代码省略
jx_yuv_encode_h264.cpp:
代码省略
代码贴完了,现在来听本屌说说它的前世今生,很关键~。
视频编码器参数配置
这里稍微说几个重要的,一会没吐槽到的参数可以再开这里再仔细看看,ffmpeg 编码器AVCodecContext 的配置参数。地址如下:
http://blog.csdn.net/mabeijianxi/article/details/72910941
通过上面代码我们 copy 了下视频输出地址,我们视频输出地址是以 .h264 结尾的很关键,因为下面的 avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file)函数会检查其合法性,并且根据你的后缀格式对应为 pFormatCtx 赋值。
pCodecCtx->codec_id = AV_CODEC_ID_H264 这里指定编码器id,是H264无疑;
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;指定编码的数据格式;
pCodecCtx->bit_rate = arguments->video_bit_rate,指定视频比特率,这个参数相当重要,很大程度上决定你视频质量与大小,但是根据这个也跟码率模式有关在VBR模式下,其将会有一定的波动。
pCodecCtx->thread_count = 16 线程条数,我这里写死了,不太好,道上的朋友称1.5陪核数就好。
pCodecCtx->time_base.num = 1; pCodecCtx->time_base.den = arguments->frame_rate 这两个是控制帧率的,num是分母,den是分子,相除既得到帧率。你必须和你采集到的帧率一样,你这里很关键,不然可能会导致视音不同步,踩坑的路过~,你给你相机设置的帧数不一定就是实际保存的帧数,这个时候也会造成视音不同步,这个后面与Java层对接的时候再道来。
av_opt_set(pCodecCtx->priv_data, "preset", "superfast", 0) 这里是指定一个编码速度的预设值,我暂时写死为最快。
pCodecCtx->qmin pCodecCtx->qmax 这是量化范围设定,其值范围为0~51,越小质量越高,需要的比特率越大,0为无损编码。关于编码过程及原理可阅读视频压缩编码和音频压缩编码的基本原理,地址如下:
http://blog.csdn.net/mabeijianxi/article/details/72933910
pCodecCtx->max_b_frames = 3 最大b帧是3,可以设置为0这样编码时会快一些,因为运动估计和运动补偿编码时分 i、b、p帧,借鉴一句雷神的话:I帧只使用本帧内的数据进行编码,在编码过程中它不需要进行运动估计和运动补偿。显然,由于I帧没有消除时间方向的相关性,所以压缩比相对不高。P帧在编码过程中使用一个前面的I帧或P帧作为参考图像进行运动补偿,实际上是对当前图像与参考图像的差值进行编码。B帧的编码方式与P帧相似,惟一不同的地方是在编码过程中它要使用一个前面的I帧或P帧和一个后面的I帧或P帧进行预测。由此可见,每一个P帧的编码需要利用一帧图像作为参考图像,而B帧则需要两帧图像作为参考。相比之下,B帧比P帧拥有更高的压缩比,所以b帧多会有一定延迟。
av_dict_set(¶m, "profile", "baseline", 0) 它可以将你的输出限制到一个特定的 H.264 profile,所有profile 包括:baseline,main.high,high10,high422,high444 ,注意使用--profile选项和无损编码是不兼容的。
Android摄像头所采集的 YUV 数据结构
先简要说说 YUV 格式,与 RGB 类似 YUV 也是一种颜色编码方法,Y:表示明亮度(Luminance或Luma),也就是灰度值;而 U 和 V :表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。如果只有Y那么就是黑白音像。根据采样方式不同主要有 YUV4:4:4,YUV4:2:2,YUV4:2:0。其 YUV 4:4:4采样,每一个 Y 对应一组 UV分量。 YUV 4:2:2采样,每两个Y共用一组UV分量。YUV 4:2:0采样,每四个 Y 共用一组 UV分量 。举个例子,屏幕上有八个像素点,YUV4:4:4会有8个 Y,8个U,8个V。YUV4:2:2 会有 8个Y,4个U,4个V。YUV4:2:0会有8个Y,2个U,2个V。
我们要对咋们采集的数据做处理,我们必须知道其数据类型和数据结构,在老版本的android sdk中其只能采集两种模式的数据,YV12 与 NV12,他们都是属于 YUV420,只是其排列结构不同。我们看看下面的图,当然下面第一张图我 P 过,因为原图有错,但是人老了手斗没 P 完美,就将就看了。
可以看到Y1, Y2, Y7, Y8这些物理上相近的4个像素公用了同样的 U1 和 V1,相似的Y3,Y4,Y9,Y10 用的就是 U2 和 V2。这里不同的颜色把这个特性刻画的非常形象,一 目了然。格子数目就是这一帧图像的 byte 数组的大小,其数组元素排放顺序就是后面那一长条的样子。
NV12 如下:
可以发现它们只是 UV 的排放位置不同而已。
YV12数据处理
用 YV12 于 NV12 都是可以的,我在配置相机参数的时候选择了 YV12,接下我们写几个简单的算法实现视频的剪切旋转,非常的简单,我当时估摸着是这个样子就写出来了。
我们这里假设我们采集的视频宽是640,高是480,我们要剪切成宽是400,高是300的视频。根据上面的知识我们能指定640*480的一帧 byte 数组里面将会有640*480个 Y,且排在最前面,然后有(1/4)*640*480个 V,然后有(1/4)*640*480个 U ,我们要剪切成400*300,自然是保留一部分数据即可。我们先对 Y 建立一个模型,既然是640*480,我们可以把它当成一行有640个 Y,一共有480行,如下图所示红色标注内表示640*480个 Y ,而黄色区域内则是我们剪切完成的 Y 的所有值。
需要注意图像方向哈。有了这个模型我们就可以写代码操作数组了。下面搞段代码:
剪切Y:
假设in_buf是一帧 YV12 视频数据的话,执行完这个循环我们就得到剪切好的 Y 值了,接下来我们解析剪切 UV数据,UV 的模型和 Y 有点不同。之所以叫 YUV4:2:0,不是因为没有 V ,它其实是在纵向上 UV 交换扫描的,比如第一行扫描U第二行就扫描 V,第三行再扫描 U 。在横向上是隔一个扫描,比如第一列扫描了,第二列就不扫描,然后扫描第三列。所以 U 在横向和纵向上的数据都是其 Y 的1/2,总数量是其1/4,V 也是一样的。知道了这些我们就可以轻易的建立模型。
320*240的区域就是我们就是我们U值或者V值的区域,200*150的区域就是我们剪切后的 U 值或者 V 值的目标区域。代码如下:
剪切UV:
经过上面的操作我们已经完成了最基本的剪切,摄像头采集的数据是横屏的,如果我们竖屏录制且我们不做任何操作的话这时候我们录制的视频是逆时针旋转了90°的,tnd 你逆时针那哥就顺时针给你转90°,这样应该就正了。
思路有了,就是如上图所示,我们 for 循环不变,因为需要剪切的位置不变,我们只改变输出数组的排放位置,原来第一排的放到最后一列,第二排放到倒数第二列,以此内推。下面也用代码演示下:
Y剪切并顺时针旋转90°:
Y 弄好了 UV 就特别简单,因为我们已经掌握了规律,UV 在横向和纵向上的值都是 Y 的一半。
剪切UV:
因为前置摄像头的原因,会导致镜像,所以在用前置摄像头录制的时候还需要处理镜像,更多详情查阅源码即可,除了这些我们可以做好多有趣的操作,比如当 UV 值都赋予128的时候就成了黑吧影像,你还可以调节亮度色调等等。
处理完数据后调用FFmpeg编码的API即可。
音频编码
从上面流程图看到其步骤也和视频差不多的,而且数据量比较小,用 libfdk-aac 编的话基本能追上采集速度了,先上菜,再聊天:
jx_pcm_encode_aac.h:
代码省略
jx_pcm_encode_aac.cpp:
代码省略
音频我研究不是那么多,下面只简单介绍下参数,更多可访问视音频数据处理入门:PCM音频采样数据处理,地址如下:
http://blog.csdn.net/leixiaohua1020/article/details/50534316
编码参数:
pCodecCtx->sample_fmt = AV_SAMPLE_FMT_S16 设定其采样格式,我们的为16位无符号整数,这里需要和 Java 音频采集的时候设置的参数对应。
pCodecCtx->sample_rate = arguments->audio_sample_rate 采样率,音频不是我们最重要的,这里我写死了主流的44100,这里也需要和Java音频采集的时候设置的参数对应。
pCodecCtx->channel_layout = AV_CH_LAYOUT_MONO; pCodecCtx->channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout) 这是设置通道数,由于对音频要求不高我采用了单通道,这里也需要和 Java 音频采集的时候设置的参数对应。还有很多选择如 AV_CH_LAYOUT_STEREO 是立体声双通道,AV_CH_LAYOUT_4POINT0 是4通道。
pCodecCtx->bit_rate = arguments->audio_bit_rate 音频比特率。
配置完参数其他就交给 FFmpeg 了。
编写视频合成类
在音频和视频都编码完成后,我们需要将其合成 mp4,现在就可以用上我们做好的 FFmpeg 命令工具了,我们只需把地址丢给它即可,这个合成过程也耗时很少。
jx_media_muxer.h:
jx_media_muxer.cpp:
我靠,写到这提示太长叫别篇写~~我嘞个去,好吧,更多内容在下一篇利用 FFmpeg玩转Android视频录制与压缩(三),地址如下:
更多http://blog.csdn.net/mabeijianxi/article/details/73011313
每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。
如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。
欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: