作者:VE视频引擎
来源:https://blog.csdn.net/weixin_41191739/article/details/101210752
概述
在安卓平台为了实现h264视频编码,我们通常可以使用libx264, ffmpeg等第三方视频编码库,但是如果对编码的速度有一定的要求,要实现实时甚至超实时的高速视频编码,我们并没有太多选项,只能使用Android提供的MediaCodec硬编码模块。
MediaCodec模块在实际使用中会遇到很多问题,本文主要讨论使用MediaCodec来对OpenGL渲染的画面进行编码视频时,如何达到速度快和画面清晰的均衡。
注意,本文将默认你已经熟悉使用MediaCodec,配合SurfaceTexture进行OpenGL画面编码的基本流程
分析影响编码速度的因素
除去设备硬件的因素,影响MediaCodec对视频画面进行编码的速度的其他因素并不多,我们实践探索下来主要发现以下几点:
画面尺寸就是要编码的视频画面的宽高了,它直接定义了每一帧要编码的画面数据的大小,所以它也主要决定了Mediacodec在进行每帧编码时的任务量。这个很容易理解。
帧速率指的是每一秒时长的视频所包含的静止画面的帧数。由于Mediacodec在编码时是以帧为单位,对每一个静止帧画面进行编码,所以,帧速率直接决定了MediaCodec在编码固定时长的视频时需要编码的画面数量。
比如,同样编码一段10秒钟的视频,在不考虑画面内容和其他因素的影响下,如果帧速率是30帧每秒,那么MediaCodec的编码工作量大致就是帧速率15帧每秒的情况的2倍,所以其耗时也大致是其2倍。
MediaCodec有两种工作模式,同步模式和异步模式,其中异步模式只有在Android 5.0以上的系统才支持。如果要兼容5.0以下的系统,那么就必须用到同步模式,而网上流传的众多对MediaCodec的编码流程讲解(比如经典的bigflake)也都是基于同步模式的。
那么在这些文档和案例指导下,大家在使用MediaCodec进行编码时,也都使用着大致相同的设置。
其中,同步模式下,在编码流程中,我们需要调用MediaCodec的dequeueOutputBuffer接口来从队列里获取编码输出缓冲区的内容。dequeueOutputBuffer接口的第二个参数,timeoutUs是一个以微妙为单位的时间值,它定义了从MediaCodec获取输出缓冲区内容时的超时期限。
目前流传的文档教程和案例中,大多将该值设置为10000微秒,也就是10毫秒。但是当我们把这个值降低到1毫秒甚至更低时,会发现,MediaCodec整个编码流程的速度会得到大幅度的提升,而基于我们的测试,编码结果也并未因此受到太多可视的影响。
当然,Android官方文档中也并未对timeoutUs的合适取值范围给出具体的建议,我这里得出的结论是在我们项目实际开发中测试得出,并且其结果满足我们的需求。
如果你的项目不需要保持对5.0以下系统的支持,我还是建议你使用异步模式来进行编码,这样可以直接规避这种同步等待的问题。
Profile是对视频压缩特性的描述,它主要是定义了编码工具的集合。Profile越高,就说明其采用了越高级的压缩特性。而通常,更高级的压缩方式通常需要耗费更多的压缩时间,也就是说,当profile设置的越高时,其编码耗费的时间往往更多。
例如,当我们在使用ffmpeg进行编码时,如果我们把编码配置设置为ultra-fast时,profile会被强制设置为baseline。
影响画面清晰度的因素
Profile是对视频压缩特性的描述,它主要是定义了编码工具的集合。Profile越高,就说明其采用了越高级的压缩特性,相应的,同样配置下所编码得到的视频文件的清晰度就越高,并且码流越小。
安卓系统支持的H264编码profile一共有3种
- Baseline Profile
- Main Profile
- High Profile
其中Baseline是从Android 3.0之后开始支持,据我们测试,Main Profile及更高的Profile的自定义设置时是从Android 7.0之后开始支持,之前版本Android系统调用相应接口对其设置均不起作用。而就算Android7.0以后的系统,也存在部分机型(比如部分OPPO和华为机型)不支持的情况。
所以如果你的产品需要保持对主流机型的支持,Profile的可靠选择也只有Baseline了。
Biterate, 即视频码率,是视频数据(视频色彩量、亮度量、像素量)每秒输出的位数。一般用的单位是kbps。
通常情况下,码率越高则视频清晰度越高。但是由于人肉眼分辨的范围有限,码率设置过高所带来的画面清晰度替身也很难被人眼辨别,所以码率设置过高也没有意义。
通常情况下,我推荐一个公式来计算如何对编码器设置对应的码率:
Biterate = Width * Height * FrameRate * Factor
其中,Width、Height和FrameRate分别代表视频的宽度,高度和帧速率,而Factor则是一个系数,用来控制码率。
通常情况下,在网络流媒体使用场景中,可以将Factor设置为0.1~0.2,这样能在保证画面损失不严重的情况下生成出来的视频文件大小较小;
在普通本地浏览的使用场景中,可以将Factor设置为0.25~0.5,这样可以保证画面清晰度不会因为编码而造成过多肉眼可见的损失,这样生成出来的视频文件也相对较大;
在高清视频处理的使用场景中,可以将Factor设置为0.5以上。
不过,当视频画面颜色越丰富、画面变化越快时,视频编码需要的码率就更高,如果遇到这种视频画面场景,需要适当提高码率来保证清晰度。
需要注意的是,大多数安卓机型在对Bitrate的支持都有一个上限,如果设置的Bitrate值超过了上限,可能导致编码器抛出异常,进而编码流程失败。
所以在设计Bitrate的值时,需要通过MediaCodecInfo.CodecCapabilities提供的相关接口来检查系统支持的Bitrate上限。
虽然我们可以通过接口为编码器设置相关的码率,但是编码器在编码过程中如果处理我们设置的码率也是有几种不同的模式的。
- BITRATE_MODE_CQ
忽略用户设置的码率,由编码器自己控制码率,并尽可能保证画面清晰度和码率的均衡。
- BITRATE_MODE_CBR
无论视频的画面内容如果,尽可能遵守用户设置的码率
- BITRATE_MODE_VBR
尽可能遵守用户设置的码率,但是会根据帧画面之间运动矢量(通俗理解就是帧与帧之间的画面变化程度)来动态调整码率,如果运动矢量较大,则在该时间段将码率调高,如果画面变换很小,则码率降低。
所以,我们在设置码率的同时,也要注意对Bitrate Mode的设置。不同的设置对于生成出来的视频文件的大小和清晰度的影响都是不同的。
- 时间戳和帧速率
我们知道,在使用MediaCodec对OpenGL渲染内容进行编码的过程中,可以通过设置MediaFormat的MediaFormat.KEY_FRAME_RATE字段来设置编码器的帧速率;也可以通过设置MediaCodec.BufferInfo对象的presentationTimeUs值来设置每一帧画面的时间戳。
但是通常会忽略,我们在运行完OpenGL相关绘制命令,在调用swapbuffer之前需要调用eglPresentationTimeANDROID接口来设置当前帧的时间戳。
如果没有调用eglPresentationTimeANDROID来设置当前帧的时间戳,只设置了MediaCodec.BufferInfo对象的presentationTimeUs,编码产生的视频在播放时不会有任何时间上的异常,所以很多开发者往往忽略了eglPresentationTimeANDROID接口的调用。
但实际上,如果不调用eglPresentationTimeANDROID,编码出的视频在清晰度上会有额外的损失。
由于MediaCodec的设计是面向实时视频画面流编码的使用场景,所以MediaCodec会根据用户向其输入画面的速度来对编码的速度进行调节。如果我们不通过eglPresentationTimeANDROID来在编码之前对画面的时间戳进行设置,那么MediaCodec往往会将我们向其输入画面的速度默认为实时速度,来对编码速度进行调节。
这种调节会造成码率降低,视频画面清晰度降低。
当我们通过eglPresentationTimeANDROID在编码之前对画面的时间戳进行了正确的设置,MediaCodec会以实际每帧的时间戳为依据来对编码素材进行调节。
这样可以保证其不会对码率进行额外的降低,保证画面清晰度设置生效。
解决方案
结合以上的分析,为了当我们在使用MediaCodec进行视频编码时,为了达到编码速度和画面清晰度的平衡,我们需要在通过几个方面进行综合的优化。
综合上文的介绍,为了保证app在各个Android机型上的完美适配,其实我们在Profile方面能做的选择不多,最安全的情况是将Profile设置为Baseline。
Bitrate的设置我推荐使用Biterate = Width * Height * FrameRate * Factor的公式结合产品的使用场景进行设置。
Biterate Mode的默认设置是BITRATE_MODE_VBR,我推荐在系统和机型支持的情况下尽量将Biterate Mode设置为BITRATE_MODE_CQ。
在BITRATE_MODE_CQ情况下,编码器自身对码率和编码速度的调节往往能达到理想的效果,生成的视频文件不至于过多,但是画面清晰度优秀。
当然也要注意做好系统和机型的适配,进行异常处理,因为在某些机型上可能出现不支持BITRATE_MODE_CQ而导致MediaCodec在configure方法时失败,那么此时,我们需要回退到系统默认的Biterate Mode了。
我们在完成每帧OpenGL画面渲染,调用swapbuffer前一定要先调用eglPresentationTimeANDROID方法来正确设置该帧的时间戳。
eglPresentationTimeANDROID默认并不直接暴露,我们在包含相应头文件前先定义拓展宏来载入该函数。下面是载入eglPresentationTimeANDROID函数的相关代码:
#define EGL_EGLEXT_PROTOTYPES
#include
eglPresentationTimeANDROID(mDisplay, mSurface, (EGLnsecsANDROID) time); // 在调用eglSwapBuffers先正确设置当前时间戳
eglSwapBuffers(mDisplay, mSurface);
技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。
推荐阅读:
音视频面试基础题
OpenGL ES 学习资源分享
开通专辑 | 细数那些年写过的技术文章专辑
NDK 学习进阶免费视频来了
推荐几个堪称教科书级别的 Android 音视频入门项目
觉得不错,点个在看呗~