V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Recommended Services
Amazon Web Services
LeanCloud
New Relic
ClearDB
KSClive
V2EX  ›  云计算

Android 短视频 SDK 转码实践

  •  
  •   KSClive · 2017-12-18 16:18:39 +08:00 · 2925 次点击
    这是一个创建于 2532 天前的主题,其中的信息可能已经有所发展或是发生改变。

    短视频 APP 中录制完成后,为什么要做转码,为什么不在服务端做转码呢?怎么样让转码过程耗时更短,转码图像质量更高,特效添加更灵活。

    KSClive
        1
    KSClive  
    OP
       2017-12-18 16:23:57 +08:00
    一. 前言

    一些涉及的基本概念:

    转码:一般指多媒体文件格式的转换,比如分辨率、码率、封装格式等;
    解复用(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,欢迎试用!

    转载请注明:
    作者金山视频云,首发简书 Jianshu.com

    Android 短视频 SDK: https://github.com/ksvc/KSYMediaEditorKit_Android
    有关音视频的更多精彩内容,请参考 https://github.com/ksvc

    金山云 SDK 相关的 QQ 交流群:

    视频云技术交流群:574179720
    视频云 Android 技术交流:620036233

    作者:金山视频云
    链接: http://www.jianshu.com/p/bfb9ee91572a
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2572 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 132ms · UTC 10:42 · PVG 18:42 · LAX 02:42 · JFK 05:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.