前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微信团队分享:微信Android版小视频编码填过的那些坑

微信团队分享:微信Android版小视频编码填过的那些坑

作者头像
JackJiang
发布2018-08-29 17:15:33
2.6K1
发布2018-08-29 17:15:33
举报
文章被收录于专栏:即时通讯技术

1、前言

Android端的视频相关的开发,大概一直是整个Android生态,以及Android API中,最为分裂以及兼容性问题最为突出的一部分。摄像头,以及视频编码相关的API,Google一直对这方面的控制力非常差,导致不同厂商对这两个API的实现有不少差异,而且从API的设计来看,一直以来优化也相当有限,甚至有人认为这是“Android上最难用的API之一”

以微信的小视频为例,我们录制一个540p的mp4文件,对于Android来说,大体上是遵循这么一个流程:

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

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

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

1)视频编码器的选择:硬编、软编; 2)如何对摄像头输出的YUV帧进行快速预处理:镜像、缩放、旋转。

(本文同步发布于:http://www.52im.net/thread-1173-1-1.html

2、视频编码器的选择

对于录制视频的需求,不少app都需要对每一帧数据进行单独处理,因此很少会直接用到MediaRecorder来直接录取视频。

一般来说,会有这么两个选择:

1)MediaCodec; 2)FFMpeg+x264/openh264。

我们来逐个解析一下。

3、MediaCodec

3.1 基本介绍

MediaCodec是API 16之后Google推出的用于音视频编解码的一套偏底层的API,可以直接利用硬件加速进行视频的编解码。调用的时候需要先初始化MediaCodec作为视频的编码器,然后只需要不停传入原始的YUV数据进入编码器就可以直接输出编码好的h264流。

整个API设计模型来看,就是同时包含了输入端和输出端的两条队列:

因此,作为编码器,输入端队列存放的就是原始YUV数据,输出端队列输出的就是编码好的h264流,作为解码器则对应相反。在调用的时候,MediaCodec提供了同步和异步两种调用方式,但是异步使用Callback的方式是在API 21之后才加入的。

以同步调用为例,一般来说调用方式大概是这样(摘自官方例子):

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

关于MediaCodec更复杂的使用例子,可以参照下CTS测试里面的使用方式:

EncodeDecodeTest.java

从上面例子来看的确是非常原始的API,由于MediaCodec底层是直接调用了手机平台硬件的编解码能力,所以速度非常快,但是因为Google对整个Android硬件生态的掌控力非常弱,所以这个API有很多问题。

3.2 颜色格式问题

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+的系统上,这两种格式在大部分机器都有支持:

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

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

3.3 编码器支持特性相当有限

如果使用MediaCodec来编码H264视频流,对于H264格式来说,会有一些针对压缩率以及码率相关的视频质量设置,典型的诸如Profile(baseline, main, high),Profile Level, Bitrate mode(CBR, CQ, VBR),合理配置这些参数可以让我们在同等的码率下,获得更高的压缩率,从而提升视频的质量。

Android也提供了对应的API进行设置,可以设置到MediaFormat中这些设置项:

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

这个问题,在7.0以下的机器几乎是必现的,其中一个可能的原因是,Android在源码层级hardcode了profile的的设置:

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

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

3.4 16位对齐要求

前面说到,MediaCodec这个API在设计的时候,过于贴近HAL层,这在很多Soc的实现上,是直接把传入MediaCodec的buffer,在不经过任何前置处理的情况下就直接送入了Soc中。而在编码h264视频流的时候,由于h264的编码块大小一般是16x16,于是乎在一开始设置视频的宽高的时候,如果设置了一个没有对齐16的大小,例如960x540,在某些cpu上,最终编码出来的视频就会直接花屏!

很明显这还是因为厂商在实现这个API的时候,对传入的数据缺少校验以及前置处理导致的,目前来看,华为,三星的Soc出现这个问题会比较频繁,其他厂商的一些早期Soc也有这种问题,一般来说解决方法还是在设置视频宽高的时候,统一设置成对齐16位之后的大小就好了。

4、FFMpeg+x264/openh264

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

x264基本上被认为是当今市面上最快的商用视频编码器,而且基本上所有h264的特性都支持,通过合理配置各种参数还是能够得到较好的压缩率和编码速度的。

限于篇幅,这里不再阐述h264的参数配置,有兴趣可以看下这两篇文章对x264编码参数的调优:

https://www.nmm-hd.org/d/index.php?title=X264%E4%BD%BF%E7%94%A8%E4%BB%8B%E7%BB%8D&variant=zh-cn

http://www.cnblogs.com/wainiwann/p/5647521.html

openh264(https://github.com/cisco/openh264)则是由思科开源的另外一个h264编码器,项目在2013年开源,对比起x264来说略显年轻,不过由于思科支付满了h264的年度专利费,所以对于外部用户来说,相当于可以直接免费使用了,另外,firefox直接内置了openh264,作为其在webRTC中的视频的编解码器使用。

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

1)Profile只支持到baseline, level 5.2; 2)多线程编码只支持slice based,不支持frame based的多线程编码; 3)从编码效率上来看,openh264的速度也并不会比x264快,不过其最大的好处,还是能够直接免费使用吧。

5、软硬编对比

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

6、YUV帧的预处理

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

6.1 缩放

如果设置了camera的预览大小为1080p的情况下,在onPreviewFrame中输出的YUV帧直接就是1920x1080的大小,如果需要编码跟这个大小不一样的视频,我们就需要在录制的过程中,实时的对YUV帧进行缩放。

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

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

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

很明显,直接使用ffmpeg进行缩放是在是太慢了,不得不说swsscale简直就是ffmpeg里面的渣渣。

在对比了几种业界常用的算之后,我们最后考虑实现使用这种快速缩放的算法:

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

上面使用的Neon指令每次只能读取和存储8或者16位的数据,对于多出来的数据,只需要用同样的算法改成用C语言实现即可。

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

6.2 旋转

在android机器上,由于摄像头安装角度不同,onPreviewFrame出来的YUV帧一般都是旋转了90或者270度,如果最终视频是要竖拍的,那一般来说需要把YUV帧进行旋转。

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

在这里我们换个思路,能不能不对YUV帧进行旋转?

事实上在mp4文件格式的头部,我们可以指定一个旋转矩阵,具体来说是在moov.trak.tkhd box里面指定,视频播放器在播放视频的时候,会在读取这里矩阵信息,从而决定视频本身的旋转角度,位移,缩放等,具体可以参考下苹果的文档:https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-18737

通过ffmpeg,我们可以很轻松的给合成之后的mp4文件打上这个旋转角度:

于是可以在录制的时候省下一大笔旋转的开销了,excited!

6.3 镜像

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

但由于摄像头安装角度一般是90或者270度,所以实际上原生的YUV帧是水平翻转过来的,因此做镜像翻转的时候,只需要刚好以中间为中轴,分别上下交换每行数据即可,注意Y跟UV要分开处理。

这种算法用Neon实现相当简单:

同样,剩余的数据用纯C代码实现就好了, 在nexus6p上,这种镜像翻转一帧1080x1920 YUV数据大概只要不到5ms。在编码好h264视频流之后,最终处理就是把音频流跟视频流合流然后包装到mp4文件,这部分我们可以通过系统的MediaMuxer, mp4v2, 或者ffmpeg来实现,这部分比较简单,在这里就不再阐述了。

7、参考资料

1)雷霄骅(leixiaohua1020)的专栏:

http://blog.csdn.net/leixiaohua1020

大名鼎鼎雷神的博客,里面有非常多关于音视频编码/ffmpeg相关的学习资料,入门必备。也祝愿他能够在天堂安息吧。

2)Android MediaCodec stuff:

http://bigflake.com/mediacodec/

包含了一些MediaCodec使用的示例代码,初次使用可以参考下这里。

3)Coding for NEON:

https://community.arm.com/processors/b/blog/posts/coding-for-neon---part-1-load-and-stores

一个系列教程,讲述了一些常用Neon指令使用方法。上面在介绍缩放的时候使用到了Neon,事实上大部分音视频处理过程都会使用到,以YUV帧处理为例,缩放,旋转,镜像翻转都可以使用neon来做优化。

4)libyuv:

https://chromium.googlesource.com/libyuv/libyuv/

Google开源的一个YUV处理库,上面只针对1080p->540p视频帧缩放的算法,而对于通用的压缩处理,可以直接使用这里的实现,对比起ffmpeg的速度快上不少。

(原文链接:https://www.qcloud.com/community/article/893795

附录1:音视频基础资料汇总

即时通讯音视频开发(一):视频编解码之理论概述

即时通讯音视频开发(二):视频编解码之数字视频介绍

即时通讯音视频开发(三):视频编解码之编码基础

即时通讯音视频开发(四):视频编解码之预测技术介绍

即时通讯音视频开发(五):认识主流视频编码技术H.264

即时通讯音视频开发(六):如何开始音频编解码技术的学习

即时通讯音视频开发(七):音频基础及编码原理入门

即时通讯音视频开发(八):常见的实时语音通讯编码标准

即时通讯音视频开发(九):实时语音通讯的回音及回音消除�概述

即时通讯音视频开发(十):实时语音通讯的回音消除�技术详解

即时通讯音视频开发(十一):实时语音通讯丢包补偿技术详解

即时通讯音视频开发(十二):多人实时音视频聊天架构探讨

即时通讯音视频开发(十三):实时视频编码H.264的特点与优势

即时通讯音视频开发(十四):实时音视频数据传输协议介绍

即时通讯音视频开发(十五):聊聊P2P与实时音视频的应用情况

即时通讯音视频开发(十六):移动端实时音视频开发的几个建议

即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生

附录2:有关微信、QQ的文章汇总

[1] 有关QQ、微信的技术文章:

微信团队分享:微信Android版小视频编码填过的那些坑》 《微信手机端的本地数据全文检索优化之路》 《企业微信客户端中组织架构数据的同步更新方案优化实战》 《微信团队披露:微信界面卡死超级bug“15。。。。”的来龙去脉》 《QQ 18年:解密8亿月活的QQ后台服务接口隔离技术》 《月活8.89亿的超级IM微信是如何进行Android端兼容测试的》 《以手机QQ为例探讨移动端IM中的“轻应用”》 《一篇文章get微信开源移动端数据库组件WCDB的一切!》 《微信客户端团队负责人技术访谈:如何着手客户端性能监控和优化》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信团队原创分享:Android版微信的臃肿之困与模块化实践之路》 《微信后台团队:微信后台异步消息队列的优化升级实践分享》 《微信团队原创分享:微信客户端SQLite数据库损坏修复实践》 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)》 《微信Mars:微信内部正在使用的网络层封装库,即将开源》 《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》 《开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石 [源码下载]》 《微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解》 《微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)》 《微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)》 《Android版微信从300KB到30MB的技术演进(PPT讲稿) [附件下载]》 《微信团队原创分享:Android版微信从300KB到30MB的技术演进》 《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《微信技术总监谈架构:微信之道——大道至简(PPT讲稿) [附件下载]》 《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《微信海量用户背后的后台系统存储架构(视频+PPT) [附件下载]》 《微信异步化改造实践:8亿月活、单机千万连接背后的后台解决方案》 《微信朋友圈海量技术之道PPT [附件下载]》 《微信对网络影响的技术试验及分析(论文全文)》 《一份微信后台技术架构的总结性笔记》 《架构之道:3个程序员成就微信朋友圈日均10亿发布量[有视频]》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《快速裂变:见证微信强大后台架构从0到1的演进历程(二)》 《微信团队原创分享:Android内存泄漏监控和优化技巧总结》 《全面总结iOS版微信升级iOS9遇到的各种“坑”》 《微信团队原创资源混淆工具:让你的APK立减1M》 《微信团队原创Android资源混淆工具:AndResGuard [有源码]》 《Android版微信安装包“减肥”实战记录》 《iOS版微信安装包“减肥”实战记录》 《移动端IM实践:iOS版微信界面卡顿监测方案》 《微信“红包照片”背后的技术难题》 《移动端IM实践:iOS版微信小视频功能技术方案实录》 《移动端IM实践:Android版微信如何大幅提升交互性能(一)》 《移动端IM实践:Android版微信如何大幅提升交互性能(二)》 《移动端IM实践:实现Android版微信的智能心跳机制》 《移动端IM实践:WhatsApp、Line、微信的心跳策略分析》 《移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)》 《移动端IM实践:iOS版微信的多设备字体适配方案探讨》 《信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑》 《腾讯信鸽技术分享:百亿级实时消息推送的实战经验》 >>更多同类文章 ……

[2] 有关QQ、微信的技术故事:

腾讯开发微信花了多少钱?技术难度真这么大?难在哪?》 《技术往事:创业初期的腾讯——16年前的冬天,谁动了马化腾的代码》 《技术往事:史上最全QQ图标变迁过程,追寻IM巨人的演进历史》 《技术往事:“QQ群”和“微信红包”是怎么来的?》 《开发往事:深度讲述2010到2015,微信一路风雨的背后》 《开发往事:微信千年不变的那张闪屏图片的由来》 《开发往事:记录微信3.0版背后的故事(距微信1.0发布9个月时)》 《一个微信实习生自述:我眼中的微信开发团队》 《首次揭秘:QQ实时视频聊天背后的神秘组织》 >>更多同类文章 ……

(本文同步发布于:http://www.52im.net/thread-1173-1-1.html

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017.10.31 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、前言
  • 2、视频编码器的选择
  • 3、MediaCodec
  • 4、FFMpeg+x264/openh264
  • 5、软硬编对比
  • 6、YUV帧的预处理
  • 7、参考资料
  • 附录1:音视频基础资料汇总
  • 附录2:有关微信、QQ的文章汇总
相关产品与服务
即时通信 IM
即时通信 IM(Instant Messaging)基于腾讯二十余年的 IM 技术积累,支持 Android、iOS、Mac、Windows、Web、H5、小程序平台且跨终端互通,低代码 UI 组件助您30分钟集成单聊、群聊、好友与资料、消息漫游、群组管理、会话管理、直播弹幕、内容审核和推送等能力。适用于直播互动、电商带货、客服咨询、社交沟通、企业办公、互动游戏、医疗健康等场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档