MyException - 我的异常网
当前位置:我的异常网» Android » 谈一下关于Android视频编码的那些坑

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

www.MyException.Cn  网友分享于:2013-11-22  浏览:0次
谈谈关于Android视频编码的那些坑
本文讲的是谈论关于Android视频编码的那些坑,Android的视频相关的开发,大概一直是整个Android生态,以及Android API中,最为分裂以及兼容性问题最为突出的一部分。摄像头,以及视频编码相关的API,谷歌一直对这方面的控制力非常差,导致不同厂商对这两个API的实现有不少差异,而且从API的设计来看,一直以来优化也相当有限,甚至有人认为这是“安卓上最难用的API之一”

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

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

大体上就是从摄像头输出的YUV帧经过预处理之后,送入编码器,获得编码好的H264视频流。

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

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

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

视频编码器的选择

对于录制视频的需求,不少应用都需要对每一帧数据进行单独处理,因此很少会直接用到MediaRecorder来直接录制视频,一般来说,会有这么两个选择

  • 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来实现,这部分比较简单,在这里就不再阐述了。

 

 

 

阅读原文

文章评论

科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
总结2014中国互联网十大段子
总结2014中国互联网十大段子
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
我是如何打败拖延症的
我是如何打败拖延症的
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
10个调试和排错的小建议
10个调试和排错的小建议
程序员和编码员之间的区别
程序员和编码员之间的区别
那些争议最大的编程观点
那些争议最大的编程观点
程序员都该阅读的书
程序员都该阅读的书
每天工作4小时的程序员
每天工作4小时的程序员
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
旅行,写作,编程
旅行,写作,编程
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
漫画:程序员的工作
漫画:程序员的工作
 程序员的样子
程序员的样子
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
我的丈夫是个程序员
我的丈夫是个程序员
Java程序员必看电影
Java程序员必看电影
老程序员的下场
老程序员的下场
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
一个程序员的时间管理
一个程序员的时间管理
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
为什么程序员都是夜猫子
为什么程序员都是夜猫子
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
程序员必看的十大电影
程序员必看的十大电影
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
程序员应该关注的一些事儿
程序员应该关注的一些事儿
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
代码女神横空出世
代码女神横空出世
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
如何成为一名黑客
如何成为一名黑客
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
中美印日四国程序员比较
中美印日四国程序员比较
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有