一些涉及的基本概念:git
短视频APP中录制完成后,为何要作转码:github
为何不在服务端作转码呢?算法
转码的主要流程以下:异步
其中Audio Filter和Video Filter分别是指音频和视频的预处理。ide
Demuxer模块的实现,主要有如下三种方案:模块化
方案一,使用播放器
播放器的主要功能是播放,也就是从原始文件/流中提取出音视频,按照pts完成音视频的渲染。转码并不须要渲染,要求在保持音视频同步的状况下,尽快把解码数据从新按要求编码成新的音视频包,从新复用成文件。咱们也曾经为了实现尽快这个要求,把播放器强行改形成快速播放的模式,但后来遇到了不少问题:测试
SurfaceTexture
中获取的timestamp不许。所以最后放弃了这个方案。方案二,使用MediaExtractor
MediaExtractor是Android系统封装好的用来分离容器中的视频track和音频track的Java类。优势是使用简单,缺点是支持的格式有限。优化
方案三,使用FFmpeg
使用FFmpeg的av_read_frame
API来作解复用,即实现简易版的播放器逻辑。编码
方案二的兼容性不如方案三。相比方案一,方案三把音视频的解复用和解码都放到了同一个线程,av_read_frame
能输出同步交织的音视频packet,上层逻辑调用更清晰。
同时短视频其余功能模块已经引入了FFmpeg,转码模块引入FFmpeg并不增长包大小,因此选择了FFmpeg方案。线程
金山云多媒体SDK实践中,Demuxer其实是在C层作的,可是接口的封装是在Java层。解码结构也是同样。Demuxer和Decoder之间如何高效地在Java和C层之间传递待解码的音视频包?
FFmpeg的demuxer模块解复用出来的为音频或视频的AVPacket。最开始的时候咱们并无在Java层对整个AVPacket的地址指针进行封装,而是把数据封装在ByteBuffer
和其余的参数中。这样遇到了不少由于AVPacket中的参数没有传递到解码模块致使的问题。
最终咱们经过intptr_t
在C层保存AVPacket的指针,同时在Java层以long
类型来保存和传递这个指针,解决了这个问题。
为了实现模块的复用,咱们把Demuxer和Decoder分红了两个模块。使用FFmpeg来实现时,Decoder模块能够和Demuxer模块共用AVFormatContext
,经过AVFormatContext
来建立AVCodecContext
。
可是这样会有一个问题,Demuxer的工做速度会快于Decoder,此时AVFormatContext
是由Demuxer来建立的,Demuxer中止的时候会释放AVFormatContext
。若是交给Decoder模块来释放,不利于模块的复用和解耦。最终咱们发如今FFmpeg 3.3的版本中,AVCodecParams
结构图中有Decoder所须要的所有信息,能够经过传递AVCodecParams
来构造AVCodecContext
。
转码的速度是客户很是关心的一个点,转码时间太长,用户体验会很是差。咱们花了很是多的精力来对短视频的转码时间进行提速。经验主要有如下这些点:
转码的时间大部分都被视频的编码占用了,咱们把x264编码作了调整,在保证画质影响较小的前提下,节省了30%以上的编码时间。
使用视频软编时,如何从GPU中把数据“下载”到CPU上,咱们尝试了不少中方案,具体的咱们会在另外一篇文章中详细解释。以前的方案是使用ImageReader
读取RGBA数据。优化为用OpenGL ES将RGBA转换为YUVA。读取数据后从YUVA再转为I420,下载和格式转化总耗时,提速了大约40%。
硬编的缺点: 在Android平台上,硬编的兼容性较差,同时视频硬编的压缩比差于软编。
硬编的优势是显而易见的,编码器速度快,占用的资源也相对较少。
通过大量的测试,硬解的兼容性相较于硬编会好不少,使用硬解码,直接使用MediaCodec渲染到texture上,省去手动上传YUV的步骤,也节省了软解码的时间开销。
关于Android的硬编解网上已经有不少例子,官方文档也比较完善。不过在实现过程当中仍是会遇到一些意想不到的问题。
在硬编上线后,咱们对比画质发现转码图像质量较差。缘由是使用MediaCodec API时,选择的是MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR
,CBR的好处是码率比较稳定,可是会牺牲画质,移动直播中选用CBR更合理。短视频转码场景硬编时推荐使用MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
,VBR会得到更好的图像质量。对于软编时,咱们也尝试过ABR(也就是VBR),但实际测试下来效果并不能保证。
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的转码/合成功能进行测试。
下面以三星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,欢迎试用!