Android短视频SDK转码实践

一. 前言

一些涉及的基本概念:

  • 转码:一般指多媒体文件格式的转换,比如分辨率、码率、封装格式等;
  • 解复用(demux):从某种封装中分离出视频track和音频track,然后交给后续模块进行处理;
  • 复用(mux):将视频压缩数据(例如H.264)和音频压缩数据(例如AAC)合并到某种封装格式的文件中去。常提到的MP4即是一种封装;
  • 编码(encode):通过专门的算法(例如H.264AAC)来对原始音视频数据进行压缩;
  • 解码(decode):对压缩后的数据进行解压缩。

短视频APP中录制完成后,为什么要做转码:

  1. 原始视频文件码率较大,上传下载都需要很长时间,不利于传播;
  2. 编辑时增加特效、转场效果后,只是在预览中有效,原始文件并未改变,需要进行一次转码来把这些效果合成进最终的文件;
  3. 多段视频进行编辑前转码拼接为一个文件,方便后续的编辑;
  4. 目标格式和源文件格式不一致,比如需要从mp4转成gif

为什么不在服务端做转码呢?

  1. 短视频需要加入滤镜等效果,在移动端转码可以充分利用手机的GPU等资源,实现实时添加滤镜实时看到效果;
  2. 原始视频码率较大,上传下载都需要很长时间。

转码的主要流程如下:

 
Android短视频SDK转码实践
图1. 转码基本流程

其中Audio FilterVideo Filter分别是指音频和视频的预处理。

  • 短视频转码的时机:
  1. 多段视频的导入;
  2. 转场完的合成;
  3. 编辑完的合成。

二. Demuxer方案的选择

Demuxer模块的实现,主要有以下三种方案:

  1. 方案一,使用播放器

    播放器的主要功能是播放,也就是从原始文件/流中提取出音视频,按照pts完成音视频的渲染。转码并不需要渲染,要求在保持音视频同步的情况下,尽快把解码数据重新按要求编码成新的音视频包,重新复用成文件。我们也曾经为了实现尽快这个要求,把播放器强行改造成快速播放的模式,但后来遇到了很多问题:

    • 音视频同步时机的问题,视频的解码是慢于音频的解码,必然需要实现同步逻辑。player中如果改成快速播放模式,player内部加上音视频同步的逻辑,改动非常大。如果player不管同步,解码数据直接上抛给调用层,则需要在短视频上层做音视频同步,引入了额外的工作量;
    • 使用硬解码时,从SurfaceTexture中获取的timestamp不准。因此最后放弃了这个方案。
  2. 方案二,使用MediaExtractor

    MediaExtractorAndroid系统封装好的用来分离容器中的视频track和音频trackJava类。优点是使用简单,缺点是支持的格式有限。

  3. 方案三,使用FFmpeg

    使用FFmpegav_read_frame API来做解复用,即实现简易版的播放器逻辑。

    • 优点:FFmpeg中对视频格式有大量兼容的逻辑,相比MediaExtractor兼容性好,增加新的输入格式的支持会更容易,同时音视频同步逻辑的控制更简单;
    • 缺点: 需要引用FFmpeg,相对来说SDK体积较大。

方案二的兼容性不如方案三。相比方案一,方案三把音视频的解复用和解码都放到了同一个线程,av_read_frame能输出同步交织的音视频packet,上层逻辑调用更清晰。

同时短视频其他功能模块已经引入了FFmpeg,转码模块引入FFmpeg并不增加包大小,所以选择了FFmpeg方案。

三. 转码的数据传递

金山云多媒体SDK实践中,Demuxer实际上是在C层做的,但是接口的封装是在Java层。解码结构也是一样。DemuxerDecoder之间如何高效地在JavaC层之间传递待解码的音视频包?

3.1 AVPacket的传递

FFmpegdemuxer模块解复用出来的为音频或视频的AVPacket。最开始的时候我们并没有在Java层对整个AVPacket的地址指针进行封装,而是把数据封装在ByteBuffer和其他的参数中。这样遇到了很多因为AVPacket中的参数没有传递到解码模块导致的问题。

最终我们通过intptr_tC层保存AVPacket的指针,同时在Java层以long类型来保存和传递这个指针,解决了这个问题。

3.2 AVFormatContext/AVCodecParams的传递

为了实现模块的复用,我们把DemuxerDecoder分成了两个模块。使用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 ESRGBA转换为YUVA。读取数据后从YUVA再转为I420,下载和格式转化总耗时,提速了大约40%。

4.3 开启硬编

硬编的缺点: 在Android平台上,硬编的兼容性较差,同时视频硬编的压缩比差于软编。

硬编的优点是显而易见的,编码器速度快,占用的资源也相对较少。

4.4 开启硬解

经过大量的测试,硬解的兼容性相较于硬编会好很多,使用硬解码,直接使用MediaCodec渲染到texture上,省去手动上传YUV的步骤,也节省了软解码的时间开销。

4.4.1 硬编解遇到的坑

关于Android的硬编解网上已经有很多例子,官方文档也比较完善。不过在实现过程中还是会遇到一些意想不到的问题。

  • 图像质量的问题

在硬编上线后,我们对比画质发现转码图像质量较差。原因是使用MediaCodec API时,选择的是MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBRCBR的好处是码率比较稳定,但是会牺牲画质,移动直播中选用CBR更合理。短视频转码场景硬编时推荐使用MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBRVBR会获得更好的图像质量。对于软编时,我们也尝试过ABR(也就是VBR),但实际测试下来效果并不能保证。

  • 硬解不兼容AVCC/HVCC 码流格式

H.264码流主要分Annex-BAVCC两种格式,H.265码流主要分为Annex-BHVCC格式。AnnexBAVCC/HVCC的区别在于参数集与帧格式,AnnexB的参数集spsppsNAL的形式存在码流中(带内传输),以startcode分割NAL

AVCC/HVCC 的参数集存储在extradata中(带外传输),使用NALU长度(固定字节,通常为4字节,从extradata中解析)分隔NAL,通常MP4MKV使用AVCC格式来存储。

Android的硬解只接受Annex-B格式的码流,所以在解码MP4 Demux出的视频流时,需要解析extradata,取出sps、pps,通过CSD(Codec-Specific Data)来初始化解码器;并且将AVCC码流转换为Annex-B,在ffmpeg中使用h264_mp4toannexb_filterhevc_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,欢迎试用!

上一篇:Java程序员面试题集(71-85)(转)


下一篇:JBox - 模态窗口,工具提示和消息 jQuery 插件