一. 前言
一些涉及的基本概念:
- 转码:一般指多媒体文件格式的转换,比如分辨率、码率、封装格式等;
- 解复用(demux):从某种封装中分离出视频track和音频track,然后交给后续模块进行处理;
- 复用(mux):将视频压缩数据(例如H.264)和音频压缩数据(例如AAC)合并到某种封装格式的文件中去。常提到的MP4即是一种封装;
- 编码(encode):通过专门的算法(例如H.264或AAC)来对原始音视频数据进行压缩;
- 解码(decode):对压缩后的数据进行解压缩。
短视频APP中录制完成后,为什么要做转码:
- 原始视频文件码率较大,上传下载都需要很长时间,不利于传播;
- 编辑时增加特效、转场效果后,只是在预览中有效,原始文件并未改变,需要进行一次转码来把这些效果合成进最终的文件;
- 多段视频进行编辑前转码拼接为一个文件,方便后续的编辑;
- 目标格式和源文件格式不一致,比如需要从mp4转成gif。
为什么不在服务端做转码呢?
- 短视频需要加入滤镜等效果,在移动端转码可以充分利用手机的GPU等资源,实现实时添加滤镜实时看到效果;
- 原始视频码率较大,上传下载都需要很长时间。
转码的主要流程如下:
其中Audio Filter和Video Filter分别是指音频和视频的预处理。
- 短视频转码的时机:
- 多段视频的导入;
- 转场完的合成;
- 编辑完的合成。
二. Demuxer方案的选择
Demuxer模块的实现,主要有以下三种方案:
-
方案一,使用播放器
播放器的主要功能是播放,也就是从原始文件/流中提取出音视频,按照pts完成音视频的渲染。转码并不需要渲染,要求在保持音视频同步的情况下,尽快把解码数据重新按要求编码成新的音视频包,重新复用成文件。我们也曾经为了实现尽快这个要求,把播放器强行改造成快速播放的模式,但后来遇到了很多问题:- 音视频同步时机的问题,视频的解码是慢于音频的解码,必然需要实现同步逻辑。player中如果改成快速播放模式,player内部加上音视频同步的逻辑,改动非常大。如果player不管同步,解码数据直接上抛给调用层,则需要在短视频上层做音视频同步,引入了额外的工作量;
- 使用硬解码时,从
SurfaceTexture
中获取的timestamp不准。因此最后放弃了这个方案。
-
方案二,使用MediaExtractor
MediaExtractor是Android系统封装好的用来分离容器中的视频track和音频track的Java类。优点是使用简单,缺点是支持的格式有限。 -
方案三,使用FFmpeg
使用FFmpeg的av_read_frame
API来做解复用,即实现简易版的播放器逻辑。- 优点:FFmpeg中对视频格式有大量兼容的逻辑,相比MediaExtractor兼容性好,增加新的输入格式的支持会更容易,同时音视频同步逻辑的控制更简单;
- 缺点: 需要引用FFmpeg,相对来说SDK体积较大。
方案二的兼容性不如方案三。相比方案一,方案三把音视频的解复用和解码都放到了同一个线程,av_read_frame
能输出同步交织的音视频packet,上层逻辑调用更清晰。
同时短视频其他功能模块已经引入了FFmpeg,转码模块引入FFmpeg并不增加包大小,所以选择了FFmpeg方案。
三. 转码的数据传递
金山云多媒体SDK实践中,Demuxer实际上是在C层做的,但是接口的封装是在Java层。解码结构也是一样。Demuxer和Decoder之间如何高效地在Java和C层之间传递待解码的音视频包?
3.1 AVPacket的传递
FFmpeg的demuxer模块解复用出来的为音频或视频的AVPacket。最开始的时候我们并没有在Java层对整个AVPacket的地址指针进行封装,而是把数据封装在ByteBuffer
和其他的参数中。这样遇到了很多因为AVPacket中的参数没有传递到解码模块导致的问题。
最终我们通过intptr_t
在C层保存AVPacket的指针,同时在Java层以long
类型来保存和传递这个指针,解决了这个问题。
3.2 AVFormatContext/AVCodecParams的传递
为了实现模块的复用,我们把Demuxer和Decoder分成了两个模块。使用FFmpeg来实现时,Decoder模块可以和Demuxer模块共用AVFormatContext
,通过AVFormatContext
来创建AVCodecContext
。
但是这样会有一个问题,Demuxer的工作速度会快于Decoder,此时AVFormatContext
是由Demuxer来创建的,Demuxer停止的时候会释放AVFormatContext
。如果交给Decoder模块来释放,不利于模块的复用和解耦。最终我们发现在FFmpeg 3.3的版本中,AVCodecParams
结构图中有Decoder所需要的全部信息,可以通过传递AVCodecParams
来构造AVCodecContext
。
四. 转码提速
转码的速度是客户非常关心的一个点,转码时间太长,用户体验会非常差。我们花了非常多的精力来对短视频的转码时间进行提速。经验主要有以下这些点:
4.1 调整视频软编编码参数
转码的时间大部分都被视频的编码占用了,我们把x264编码做了调整,在保证画质影响较小的前提下,节省了30%以上的编码时间。
4.2 优化GPU数据读取
使用视频软编时,如何从GPU中把数据“下载”到CPU上,我们尝试了很多中方案,具体的我们会在另一篇文章中详细解释。之前的方案是使用ImageReader
读取RGBA数据。优化为用OpenGL ES将RGBA转换为YUVA。读取数据后从YUVA再转为I420,下载和格式转化总耗时,提速了大约40%。
4.3 开启硬编
硬编的缺点: 在Android平台上,硬编的兼容性较差,同时视频硬编的压缩比差于软编。
硬编的优点是显而易见的,编码器速度快,占用的资源也相对较少。
4.4 开启硬解
经过大量的测试,硬解的兼容性相较于硬编会好很多,使用硬解码,直接使用MediaCodec渲染到texture上,省去手动上传YUV的步骤,也节省了软解码的时间开销。
4.4.1 硬编解遇到的坑
关于Android的硬编解网上已经有很多例子,官方文档也比较完善。不过在实现过程中还是会遇到一些意想不到的问题。
- 图像质量的问题
在硬编上线后,我们对比画质发现转码图像质量较差。原因是使用MediaCodec API时,选择的是MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR
,CBR的好处是码率比较稳定,但是会牺牲画质,移动直播中选用CBR更合理。短视频转码场景硬编时推荐使用MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
,VBR会获得更好的图像质量。对于软编时,我们也尝试过ABR(也就是VBR),但实际测试下来效果并不能保证。
- 硬解不兼容AVCC/HVCC 码流格式
H.264码流主要分Annex-B和AVCC两种格式,H.265码流主要分为Annex-B和HVCC格式。AnnexB与AVCC/HVCC的区别在于参数集与帧格式,AnnexB的参数集sps、pps以NAL的形式存在码流中(带内传输),以startcode分割NAL。
而AVCC/HVCC 的参数集存储在extradata中(带外传输),使用NALU长度(固定字节,通常为4字节,从extradata中解析)分隔NAL,通常MP4、MKV使用AVCC格式来存储。
Android的硬解只接受Annex-B格式的码流,所以在解码MP4 Demux出的视频流时,需要解析extradata,取出sps、pps,通过CSD(Codec-Specific Data
)来初始化解码器;并且将AVCC码流转换为Annex-B,在ffmpeg中使用h264_mp4toannexb_filter
或hevc_mp4toannexb
做转换。
- 硬解时间戳不准确的问题
硬解码器解码视频到Surface,此时通过SurfaceTexture.getTimestamp()
获得时间戳并不准确,某些机型会出现异常。所以还是要使用解码输入的时间戳,可将解码过程由异步转为同步,或者将pts存储到队列中来实现。
- 音频硬编硬解解的速度
MediaCodec的音频编解码具体实现和机型有关,许多机型的MediaCodec音频编解码工作仍然是软件方案。经过测试MediaCodec音频硬编码较软编码有6%左右的提速,但MediaCodec音频硬解反而比软解的的速度慢,具体原因有待进一步调查。不过这只是部分机型的测试结果,更多机型的比较大家可以使用我们demo的转码/合成功能进行测试。
4.5 转码提速对比
下面以三星S8为例,短视频SDK在转码速度上的进步,更多机型的对比数据,请移步github wiki查看。
将1分钟1080p 18Mbps视频,转码成540p 1.2Mbps,不同版本时间开销大致如下:
机型 | 版本 | 编码方式 | 第一次合成时长 | 第二次合成时长 | 第三次合成时长 | 平均值 |
---|---|---|---|---|---|---|
三星S8 | V1.0.4 | 软编 | 52s | 54s | 58s | 54.7s |
V1.1.2 | 软编 | 49s | 50s | 50s | 49.7s | |
V1.1.2 | 硬编 | 35s | 36s | 38s | 36.3s | |
V1.4.7 | 硬编 | 21.5s | 21.9s | 22.5s | 22.0s |
可以看到,使用了硬编、硬解等提速手段后,合成速度由54秒优化到22秒。
五. 模块化的思考
金山云短视频SDK的基础模块是基于直播SDK,整体来说,是一套push模式的流水线。
流水线中的每个模块都很好地实现了解耦,单独模块完成单一的功能,模块的复用也非常方便。前置模块在产生新的音视频帧后,会立即push给后续模块,后续模块需要尽快把前置模块产生的音视频帧消化掉,最大程度上保证实时性。为了保证音视频同步等逻辑,引入了大量同步锁。在短视频的开发中,遇到了不少的死锁和不方便。对于短视频这种非实时的场景,更多的时候,需要由后续模块(而非前置模块)来控制整个流程的进度。
当前处理过程中需要实现暂停,需要在前置模块加锁来实现。为了能方便以后的开发,我们会在接下来重新梳理这种push流水线的方式, 实现模块化的同时,尽量减少同步锁的使用。
六. 总结
转码对于普通用户来说不可见的,但却是短视频SDK的一个重要过程。怎么样让转码过程耗时更短,转码图像质量更高,特效添加更灵活,减少我们团队自身的开发和维护成本,同时也为开发者提供最方便易用的API,一直是金山云多媒体SDK团队的目标。
团队在很用心的开发短视频SDK,欢迎试用!