原文连接:https://zhuanlan.zhihu.com/p/82130600java
Android的视频相关的开发,大概一直是整个Android生态,以及Android API中,最为分裂以及兼容性问题最为突出的一部分。摄像头,以及视频编码相关的API,Google一直对这方面的控制力很是差,致使不一样厂商对这两个API的实现有很多差别,并且从API的设计来看,一直以来优化也至关有限,甚至有人认为这是“Android上最难用的API之一”web
以微信为例,咱们录制一个540p的mp4文件,对于Android来讲,大致上是遵循这么一个流程:数组
大致上就是从摄像头输出的YUV帧通过预处理以后,送入编码器,得到编码好的h264视频流。微信
上面只是针对视频流的编码,另外还须要对音频流单独录制,最后再将视频流和音频流进行合成出最终视频。多线程
这篇文章主要将会对视频流的编码中两个常见问题进行分析:app
1.视频编码器的选择(硬编 or 软编)?
2.如何对摄像头输出的YUV帧进行快速预处理(镜像,缩放,旋转)?异步
对于录制视频的需求,很多app都须要对每一帧数据进行单独处理,所以不多会直接用到MediaRecorder来直接录取视频,通常来讲,会有这么两个选择:
1.MediaCodec
2.FFMpeg+x264/openh264ide
咱们来逐个解析一下优化
MediaCodec是API 16以后Google推出的用于音视频编解码的一套偏底层的API,能够直接利用硬件加速进行视频的编解码。调用的时候须要先初始化MediaCodec做为视频的编码器,而后只须要不停传入原始的YUV数据进入编码器就能够直接输出编码好的h264流,整个API设计模型来看,就是同时包含了输入端和输出端的两条队列:编码
所以,做为编码器,输入端队列存放的就是原始YUV数据,输出端队列输出的就是编码好的h264流,做为解码器则对应相反。在调用的时候,MediaCodec提供了同步和异步两种调用方式,可是异步使用Callback的方式是在API 21以后才加入的,以同步调用为例,通常来讲调用方式大概是这样(摘自官方例子):
MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); MediaFormat outputFormat = codec.getOutputFormat(); // option B codec.start(); for (;;) { int inputBufferId = codec.dequeueInputBuffer(timeoutUs); if (inputBufferId >= 0) { ByteBuffer inputBuffer = codec.getInputBuffer(…); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } int outputBufferId = codec.dequeueOutputBuffer(…); if (outputBufferId >= 0) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is identical to outputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) outputFormat = codec.getOutputFormat(); // option B } } codec.stop(); codec.release();
简单解释一下,经过getInputBuffers
获取输入队列,而后调用dequeueInputBuffer
获取输入队列空闲数组下标,注意dequeueOutputBuffer
会有几个特殊的返回值表示当前编解码状态的变化,而后再经过queueInputBuffer
把原始YUV数据送入编码器,而在输出队列端一样经过getOutputBuffers
和dequeueOutputBuffer
获取输出的h264流,处理完输出数据以后,须要经过releaseOutputBuffer
把输出buffer还给系统,从新放到输出队列中。
从上面例子来看的确是很是原始的API,因为MediaCodec底层是直接调用了手机平台硬件的编解码能力,因此速度很是快,可是由于Google对整个Android硬件生态的掌控力很是弱,因此这个API有不少问题:
MediaCodec在初始化的时候,在configure
的时候,须要传入一个MediaFormat对象,看成为编码器使用的时候,咱们通常须要在MediaFormat中指定视频的宽高,帧率,码率,I帧间隔等基本信息,除此以外,还有一个重要的信息就是,指定编码器接受的YUV帧的颜色格式。这个是由于因为YUV根据其采样比例,UV份量的排列顺序有不少种不一样的颜色格式,而对于Android的摄像头在onPreviewFrame
输出的YUV帧格式,若是没有配置任何参数的状况下,基本上都是NV21格式,但Google对MediaCodec的API在设计和规范的时候,显得很不厚道,过于贴近Android的HAL层了,致使了NV21格式并非全部机器的MediaCodec都支持这种格式做为编码器的输入格式! 所以,在初始化MediaCodec的时候,咱们须要经过codecInfo.getCapabilitiesForType
来查询机器上的MediaCodec实现具体支持哪些YUV格式做为输入格式,通常来讲,起码在4.4+的系统上,这两种格式在大部分机器都有支持:
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
两种格式分别是YUV420P和NV21,若是机器上只支持YUV420P格式的状况下,则须要先将摄像头输出的NV21格式先转换成YUV420P,才能送入编码器进行编码,不然最终出来的视频就会花屏,或者颜色出现错乱
这个算是一个不大不小的坑,基本上用上了MediaCodec进行视频编码都会赶上这个问题
若是使用MediaCodec来编码H264视频流,对于H264格式来讲,会有一些针对压缩率以及码率相关的视频质量设置,典型的诸如Profile(baseline, main, high),Profile Level, Bitrate mode(CBR, CQ, VBR),合理配置这些参数可让咱们在同等的码率下,得到更高的压缩率,从而提高视频的质量,Android也提供了对应的API进行设置,能够设置到MediaFormat中这些设置项:
MediaFormat.KEY_BITRATE_MODE MediaFormat.KEY_PROFILE MediaFormat.KEY_LEVEL
但问题是,对于Profile,Level, Bitrate mode这些设置,在大部分手机上都是不支持的,即便是设置了最终也不会生效,例如设置了Profile为high,最后出来的视频依然还会是Baseline,Shit....
这个问题,在7.0如下的机器几乎是必现的,其中一个可能的缘由是,Android在源码层级hardcode了profile的的设置:
// XXX if (h264type.eProfile != OMX_VIDEO_AVCProfileBaseline) { ALOGW("Use baseline profile instead of %d for AVC recording", h264type.eProfile); h264type.eProfile = OMX_VIDEO_AVCProfileBaseline; }
Android直到7.0以后才取消了这段地方的Hardcode
if (h264type.eProfile == OMX_VIDEO_AVCProfileBaseline) { .... } else if (h264type.eProfile == OMX_VIDEO_AVCProfileMain || h264type.eProfile == OMX_VIDEO_AVCProfileHigh) { ..... }
这个问题能够说间接致使了MediaCodec编码出来的视频质量偏低,同等码率下,难以得到跟软编码甚至iOS那样的视频质量。
前面说到,MediaCodec这个API在设计的时候,过于贴近HAL层,这在不少Soc的实现上,是直接把传入MediaCodec的buffer,在不通过任何前置处理的状况下就直接送入了Soc中。而在编码h264视频流的时候,因为h264的编码块大小通常是16x16,因而乎在一开始设置视频的宽高的时候,若是设置了一个没有对齐16的大小,例如960x540,在某些cpu上,最终编码出来的视频就会直接花屏!
很明显这仍是由于厂商在实现这个API的时候,对传入的数据缺乏校验以及前置处理致使的,目前来看,华为,三星的Soc出现这个问题会比较频繁,其余厂商的一些早期Soc也有这种问题,通常来讲解决方法仍是在设置视频宽高的时候,统一设置成对齐16位以后的大小就行了。
除了使用MediaCodec进行编码以外,另一种比较流行的方案就是使用ffmpeg+x264/openh264进行软编码,ffmpeg是用于一些视频帧的预处理。这里主要是使用x264/openh264做为视频的编码器。
x264基本上被认为是当今市面上最快的商用视频编码器,并且基本上全部h264的特性都支持,经过合理配置各类参数仍是可以获得较好的压缩率和编码速度的,限于篇幅,这里再也不阐述h264的参数配置
openh264则是由思科开源的另一个h264编码器,项目在2013年开源,对比起x264来讲略显年轻,不过因为思科支付满了h264的年度专利费,因此对于外部用户来讲,至关于能够直接无偿使用了,另外,firefox直接内置了openh264,做为其在webRTC中的视频的编解码器使用。
但对比起x264,openh264在h264高级特性的支持比较差:
从编码效率上来看,openh264的速度也并不会比x264快,不过其最大的好处,仍是可以直接无偿使用吧。
从上面的分析来看,
1.硬编的好处主要在于速度快,并且系统自带不须要引入外部的库,可是特性支持有限,并且硬编的压缩率通常偏低,2.而对于软编码来讲,虽然速度较慢,可是压缩率比较高,并且支持的H264特性也会比硬编码多不少,相对来讲比较可控。就可用性而言,3.在4.4+的系统上,MediaCodec的可用性是可以基本保证的,可是不一样等级的机器的编码器能力会有很多差异,建议能够根据机器的配置,选择不一样的编码器配置。视频流合流而后包装到mp4文件,这部分咱们能够经过