谈谈关于Android视频编码的那些坑

本文讲的是谈论关于Android视频编码的那些坑,Android的视频相关的开发,大概一直是整个Android生态,以及Android API中,最为分裂以及兼容性问题最为突出的一部分。摄像头,以及视频编码相关的API,谷歌一直对这方面的控制力很是差,致使不一样厂商对这两个API的实现有很多差别,并且从API的设计来看,一直以来优化也至关有限,甚至有人认为这是“安卓上最难用的API之一”java

以微信为例,咱们录制一个540P的MP4文件,对于安卓来讲,大致上是遵循这么一个流程:android

谈谈关于Android的视频编码的那些坑

大致上就是从摄像头输出的YUV帧通过预处理以后,送入编码器,得到编码好的H264视频流。算法

上面只是针对视频流的编码,另外还须要对音频流单独录制,最后再将视频流和音频流进行合成出最终视频。微信

这篇文章主要将会对视频流的编码中两个常见问题进行分析:多线程

  • 视频编码器的选择(硬编或软编)?
  • 如何对摄像头输出的YUV帧进行快速预处理(镜像,缩放,旋转)?

视频编码器的选择异步

对于录制视频的需求,很多应用都须要对每一帧数据进行单独处理,所以不多会直接用到MediaRecorder来直接录制视频,通常来讲,会有这么两个选择ide

  • MediaCodec
  • FFmpeg的+ X264 / openh264

咱们来逐个解析一下函数

MediaCodec性能

MediaCodec是API 16以后Google推出的用于音视频编解码的一套偏底层的API,能够直接利用硬件加速进行视频的编解码。调用的时候须要先初始MediaCodec做为视频的编码器,而后只须要不停传入原始的YUV数据进入编码器就能够直接输出编码好的H264流,整个API设计模型来看,就是同时包含了输入端和输出端的两条队列:测试

谈谈关于Android的视频编码的那些坑

所以,做为编码器,输入端队列存放的就是原始YUV数据,输出端队列输出的就是编码好的H264流,做为解码器则对应相反。在调用的时候,MediaCodec提供了同步和异步两种调用方式,可是异步使用Callback的方式是在API 21以后才加入的,以同步调用为例,通常来讲调用方式大概是这样(摘自官方例子):

 
  1. MediaCodec codec = MediaCodec.createByCodecName(name ); 
  2. codec.configure(format,...); 
  3. MediaFormat outputFormat = codec.getOutputFormat(); //  选项 B 
  4. codec.start(); 
  5. for  (;;){ 
  6.   int  inputBufferId = codec.dequeueInputBuffer(timeoutUs); 
  7.   if(inputBufferId> = 0){ 
  8.     ByteBuffer inputBuffer = codec.getInputBuffer(...); 
  9.     // 用 有效的数据 填充inputBuffer 
  10.     ... 
  11.     codec.queueInputBuffer(inputBufferId,...); 
  12.   } 
  13.   int  outputBufferId = codec.dequeueOutputBuffer(...); 
  14.   if(outputBufferId> = 0){ 
  15.     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); 
  16.     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); //  选项 A 
  17.     // bufferFormat  是 相同  于 OUTPUTFORMAT 
  18.     // OutputBuffer中  是 准备  要 被处理  或 渲染。 
  19.     ... 
  20.     codec.releaseOutputBuffer(outputBufferId,...); 
  21.   }  else  if(outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){ 
  22.     //随后的数据将符合  到 新的格式。 
  23.     //能够  忽略 使用getOutputFormat(outputBufferId) 
  24.     outputFormat = codec.getOutputFormat(); //  选项 B 
  25.   } 
  26. codec.stop(); 
  27. codec.release(); 

简单解释一下,经过getInputBuffers获取输入队列,而后调用dequeueInputBuffer获取输入队列空闲数据下标,注意dequeueOutputBuffer会有几个特殊的返回值表示当前编码状态的变化,而后再经过queueInputBuffer把原始数据送入编码器,而在输出队列端一样经过getOutputBuffers和dequeueOutputBuffer获取输出的h264流,处理完输出数据以后,须要经过releaseOutputBuffer把输出缓冲器还给系统,从新放到输出队列中。

关于MediaCodec更复杂的使用例子,能够参考下CTS测试里面的使用方式:EncodeDecodeTest.java

从上面例子来看的确是很是原始的API,因为MediaCodec底层是直接调用了手机平台硬件的编解码能力,因此速度很是快,可是由于谷歌对整个Android的硬件生态的掌控力很是弱,因此这个API有不少问题:

1,颜色格式问题

MediaCodec在初始化的时候,在配置的时候,须要传入一个MediaFormat对象,看成为编码器使用的时候,咱们通常须要在MediaFormat中指定视频的宽高,帧率,码率,I帧间隔等基本信息,除此以外,还有一个重要的信息就是,指定编码器接受的YUV帧的颜色格式。这个是由于因为YUV根据其采样比例,UV份量的排列顺序有不少种不一样的颜色格式,而对于Android的摄像头在onPreviewFrame输出的YUV帧格式,若是没有配置任何参数的状况下,基本都是NV21格式,但Google对MediaCodec的API在设计和规范的时候,显得很不厚道,过于贴近Android的HAL层了,致使了NV21格式并非全部机器的MediaCodec都支持这种格式做为编码器的输入格式!所以,在初始化MediaCodec的时候,咱们须要经过codecInfo.getCapabilitiesForType来实现具体支持哪些媒体代码实现具体支持哪些YUV格式做为输入格式,通常来讲,起码在4.4+的系统上,这两种格式在大部分机器都有支持:

 
  1. MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar 
  2. MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar 

两种格式分别是YUV420P和NV21,若是机器上只支持YUV420P格式的状况下,则须要先将摄像头输出的NV21格式先转换成YUV420P,才能送入编码器进行编码,不然最终出来的视频就会花屏,或者颜色出现错乱

这个算是一个不大不小的坑,基本上用上了MediaCodec进行视频编码都会赶上这个问题

2,编码器支持特性至关有限

若是使用MediaCodec来编码H264视频流,对于H264格式来讲,会有一些针对压缩率以及码率相关的视频质量设置,典型的如Profile(baseline,main,high),Profile Level,Bitrate mode(CBR, CQ,VBR),合理配置这些参数可让咱们在同等的码率下,得到更高的压缩率,从而提高视频的质量,Android也提供了对应的API进行设置,能够设置到MediaFormat中这些设置项:

 
  1. MediaFormat.KEY_BITRATE_MODE 
  2. MediaFormat.KEY_PROFILE 
  3. MediaFormat.KEY_LEVEL 

但问题是,对于Profile,Level,Bitrate mode这些设置,在大部分手机上都是不支持的,即便是设置了最终也不会生效,例如设置了Profile为high,最后出来的视频依然还会是基线,妈....

这个问题,在7.0如下的机器几乎是必须的,其中一个可能的缘由是,Android在源码层级hardcode了profile的的设置:

 
  1. // XXX 
  2. if(h264type.eProfile!= OMX_VIDEO_AVCProfileBaseline){ 
  3.     ALOGW(“使用基线配置文件代替AVC录制的%d” , 
  4.             h264type.eProfile); 
  5.     h264type.eProfile = OMX_VIDEO_AVCProfileBaseline; 

Android直到7.0以后才取消了这段地方的Hardcode

 
  1. if(h264type.eProfile == OMX_VIDEO_AVCProfileBaseline){ 
  2.     .... 
  3. }  不然 若是(h264type.eProfile == OMX_VIDEO_AVCProfileMain || 
  4.             h264type.eProfile == OMX_VIDEO_AVCProfileHigh){ 
  5.     ..... 

这个问题能够说间接致使了MediaCodec编码出来的视频质量偏低,同等码率下,难以得到跟软编码甚至iOS版那样的视频质量。

3,16位对齐要求

前面说到,MediaCodec这个API在设计的时候,过于贴近HAL层,这在不少志的实现上,是直接把传入MediaCodec的缓冲液中,在不通过任何前置处理的状况下就直接送入了志中。而在编码H264视频流的时候,因为H264的编码块大小通常是16×16,因而乎在一开始设置视频的宽高的时候,若是设置了一个没有对齐16的大小,例如960×540,在某些cpu上,最终编码出来的视频就会直接花屏!

很明显这仍是由于厂商在实现这个API的时候,对传入的数据缺乏校验以及前置处理致使的,目前来看,华为,三星的志出现这个问题会比较频繁,其余厂商的一些早期志也有这种问题,通常来讲解决方法仍是在设置视频宽高的时候,统一设置成对齐16位以后的大小就行了。

FFmpeg的+ X264 / openh264

除了使用MediaCodec进行编码以外,另一种比较流行的方案就是使用的ffmpeg + X264 / openh264进行软编码,FFMPEG是用于一些视频帧的预处理。这里主要是使用X264 / openh264做为视频的编码器。

X264基本上被认为是当今市面上最快的商用视频编码器,并且基本上全部的H264的特性都支持,经过合理配置各类参数仍是可以获得较好的压缩率和编码速度的,限于篇幅,这里再也不阐述h264的参数配置,有兴趣能够看下这里和这里对x264编码参数的调优。

openh264则是由思科开源的另外一个h264编码器,项目在2013年开源,对比起x264来讲略显年轻,不过因为思科支付满了,因此对于外部用户来讲,至关于能够直接无偿使用了,另外,火狐直接内置了openh264,做为其在的WebRTC中的视频的编解码器使用。

但对比起X264,openh264在H264高级特性的支持比较差:

  • 简介只支持到基准,等级5.2
  • 多线程编码只支持片基,不支持基于帧的多线程编码

从编码效率上来看,openh264的速度也并不会比X264快,不过其最大的好处,仍是可以直接无偿使用吧。

软硬编对比

从上面的分析来看,硬编的好处主要在于速度快,并且系统自带不须要引入外部的库,可是特性支持有限,并且硬编的压缩率通常偏低,而对于软编码来讲,虽然速度较慢,但​​是压缩率比较高,并且支持的H264特性也会比硬编码多不少,相对来讲比较可控。就可用性而言,在4.4+的系统上,MediaCodec的可用性是可以基本保证的,可是不一样等级的机器的编码器能力会有很多差异,建议能够根据机器的配置,选择不一样的编码器配置。

YUV帧的预处理

根据最开始给出的流程,在送入编码器以前,咱们须要先对摄像头输出的YUV帧进行一些前置处理

1.缩放

若是设置了相机的预览大小为1080p的状况下,在onPreviewFrame中输出的YUV帧直接就是1920x1080的大小,若是须要编码跟这个大小不同的视频,咱们就须要在录制的过程当中,实时的对YUV帧进行缩放。

以微信为例,摄像头预览1080的数据,须要编码960×540大小的视频。

最为常见的作法是使用ffmpeg的这种的sws_scale函数进行直接缩放,效果/性能比较好的通常是选择SWS_FAST_BILINEAR算法:

 
  1. mScaleYuvCtxPtr = sws_getContext( 
  2.                    srcWidth, 
  3.                    srcHeight, 
  4.                    AV_PIX_FMT_NV21, 
  5.                    dstWidth, 
  6.                    dstHeight, 
  7.                    AV_PIX_FMT_NV21, 
  8.                    SWS_FAST_BILINEAR,  NULL ,  NULL ,  NULL ); 
  9. sws_scale(mScaleYuvCtxPtr, 
  10.                     (const uint8_t * const *)srcAvPicture-> data, 
  11.                     srcAvPicture-> linesize,0,srcHeight, 
  12.                     dstAvPicture-> data,dstAvPicture-> linesize); 

在nexus 6p上,直接使用ffmpeg来进行缩放的时间基本上都须要40ms +,对于咱们须要记录30fps的来讲,每帧处理时间最多就30ms左右,若是光是缩放就消耗了如此多的时间,上录制出来的视频只能在15fps的上下了。

很明显,直接使用的ffmpeg进行缩放是在是太慢了,不得不说swsscale简直就是ffmpeg的里面的渣渣,在对比了几种业界经常使用的算以后,咱们最后考虑实现使用这种快速缩放的算法:

谈谈关于Android的视频编码的那些坑

咱们选择一种叫作的局部均值算法,先后两行四个临近点算出最终图片的四个像素点,对于源图片的每行像素,咱们可使用Neon直接实现,以缩放Y份量为例:

 
  1. const uint8 * src_next = src_ptr + src_stride; 
  2.   asm挥发性( 
  3.     “1:\ n”     
  4.       “vld4.8 {d0,d1,d2,d3},[%0]!\ n” 
  5.       “vld4.8 {d4,d5,d6,d7},[%1]!\ n” 
  6.       “subs%3,%3,#16 \ n”   //每一个循环处理16次 
  7.  
  8.       “vrhadd.u8 d0,d0,d1 \ n” 
  9.       “vrhadd.u8 d4,d4,d5 \ n” 
  10.       “vrhadd.u8 d0,d0,d4 \ n” 
  11.  
  12.       “vrhadd.u8 d2,d2,d3 \ n” 
  13.       “vrhadd.u8 d6,d6,d7 \ n” 
  14.       “vrhadd.u8 d2,d2,d6 \ n” 
  15.  
  16.       “vst2.8 {d0,d2},[%2]!\ n”   //存储奇数像素 
  17.  
  18.       “bgt 1b \ n” 
  19.     :  “+ r” (src_ptr),//%0 
  20.       “+ r” (src_next),//%1 
  21.       “+ r” (dst),//%2 
  22.       “+ r” (dst_width)//%3 
  23.     : 
  24.     :  “q0” ,  “q1” ,  “q2” ,  “q3”               // Clobber List 
  25.   ); 

上面使用的霓虹灯指令每次只能读取和存储8或者16位的数据,对于多出来的数据,只须要用一样的算法改为用Ç语言实现便可。

在使用上述的算法优化以后,进行每帧缩放,在Nexus 6p上,只须要不到5ms就能完成了,而对于缩小质量来讲,ffmpeg的SWS_FAST_BILINEAR算法和上述算法缩放出来的图片进行对比,峰值信噪比(psnr)在大部分场景下大概在38-40左右,质量也足够好了。

2.旋转

在android机器上,因为摄像头安装角度不一样,onPreviewFrame出来的YUV帧通常都是旋转了90或者270度,若是最终视频是要竖拍的,那通常来讲须要把YUV帧进行旋转。

对于旋转的算法,若是是纯C实现的代码,通常来讲是个O(n ^ 2)复杂度的算法,若是是旋转960x540的yuv帧数据,在nexus 6p上,每帧旋转也须要30ms +,这显然也是不能接受的。

在这里咱们换个思路,能不能不对YUV帧进行旋转?(固然是能够的6666)

事实上在mp4文件格式的头部,咱们能够指定一个旋转矩阵,具体来讲是在moov.trak.tkhd盒里面指定,视频播放器在播放视频的时候,会在读取这里矩阵信息,从而决定视频自己的旋转角度,位移,缩放等,具体可参考下苹果的文档

经过ffmpeg的,咱们能够很轻松的给合成以后的MP4文件打上这个旋转角度:

 
  1. char  rotateStr [1024]; 
  2. sprintf(rotateStr,  “%d” ,rotate); 
  3. av_dict_set(&out_stream-> metadata,  “rotate” ,rotateStr,0); 

因而能够在录制的时候省下一大笔旋转的开销了,兴奋!

3.镜像

在使用前置摄像头拍摄的时候,若是不对YUV帧进行处理,那么直接拍出来的视频是会镜像翻转的,这里原理就跟照镜子同样,从前置摄像头方向拿出来的YUV帧恰好是反的,但有些时候拍出来的镜像视频可能不合咱们的需求,所以这个时候咱们就须要对YUV帧进行镜像翻转。

但因为摄像头安装角度通常是90或者270度,因此实际上原生的YUV帧是水平翻转过来的,所以作镜像翻转的时候,只须要恰好以中间为中轴,分别上下交换每行数据便可,注意Ÿ跟UV要分开处理,这种算法用霓虹灯实现至关简单:

 
  1. asm挥发性( 
  2.       “1:\ n” 
  3.         “vld4.8 {d0,d1,d2,d3},[%2]!\ n”   //  从 src  载入 32 
  4.         “vld4.8 {d4,d5,d6,d7},[%3]!\ n”   //  从 dst  载入 32 
  5.         “subs%4,%4,#32 \ n”   // 32每一个循环处理 
  6.         “vst4.8 {d0,d1,d2,d3},[%1]!\ n”   //存储32  到 dst 
  7.         “vst4.8 {d4,d5,d6,d7},[%0]!\ n”   //存储32  到 src 
  8.         “bgt 1b \ n” 
  9.       :  “+ r” (src),//%0 
  10.         “+ r” (dst),//%1 
  11.         “+ r” (srcdata),//%2 
  12.         “+ r” (dstdata),//%3 
  13.         “+ r” (count )//%4 //  输出 寄存器 
  14.       ://输入寄存器 
  15.       :  “cc” ,  “memory” ,  “q0” ,  “q1” ,  “q2” ,  “q3”   // Clobber List 
  16.     ); 

一样,剩余的数据用纯C代码实现就行了,在nexus6p上,这种镜像翻转一帧1080x1920 YUV数据大概只要不到5ms

在编码好h264视频流以后,最终处理就是把音频流跟视频流合然而后包装到mp4文件,这部分咱们能够经过系统的MediaMuxer,mp4v2,或者ffmpeg来实现,这部分比较简单,在这里就再也不阐述了。

 

 

 

阅读原文

相关文章
相关标签/搜索