前言:
移动互联网的火爆改变了人们一系列的生活方式,从社交、购物、教育等方方面面渗透进大众的生活,移动端的迅速崛起和高速发展离不开移动端背后的技术演进和迭代,产品更新迭代过程中如何优化它的性能?实际实践中遇到的坑又该如何处理?本文是根据七牛架构师实践日上海站,英语流利说Android 端开发人员王聪武的演讲内容进行整理,介绍了英语流利说移动应用在Android音视频处理相关实践。
英语流利说 APP 有一个配音功能,功能简而言之就是将一系列录音片段和视频背景声做混合,最后合成输出视频。本次分享『音视频相关的实践与优化』主要讲得就是对功能实现与优化的一个过程,就此分享出来希望对大家有所帮助。
一、方案的考量指标
实现功能需要选择方案,选择方案需要确立对应的指标。就此归纳了如下四个指标。
内存占用:
Android App 的内存占用一般分两种 Java Heap 和 Native Heap,可以采用 Android Monitor 和 GT 来看,也可以自己动手通过 Android Api 周期性采集数据。
执行耗时:
方法耗时的统计通用的做法就是打 log 记录耗时。倘若想要整体了解一系列耗时,其中 Java 层可以用 Device Monitor 提供的 method profiling,native 可以采用 valgrind。
代码大小:
一般自己实现的代码量有限,这里指特指方案依赖 Library 的大小。library 的种类也一般分两种. jar 与 .so。jar 的大小会影响 dex method count,method count 过大不仅 Apk 会变大,也会影响运行时性能。so 的问题则是,需要多种 cpu 架构会让 so 的大小成倍放大,最终导致 Apk 急剧膨胀。
复杂度:
包含两个考量点,一是开发语言,java 与 c/c++ 的选择,一般情况下后者会更加复杂。 二是实现的代码量,实现所需的代码量一般越短越易于理解与维护。
二、方案优化的方法
代码指令上面的优化:一般可以选取更合适的算法或者内存占用更少的数据结构,如果是 native 层可以试试 neon 指令有没有帮助。
预处理:把原先需要客户端及时处理生成的,交给服务单预先生成好,提前下发客户端避免客户端处理耗时,空间换时间。
后台处理:也是预处理一种,区别在于这个预处理是客户端将任务放在后台预处理,当需要用到相关数据的时候,可以 join 这个后台任务
不完美的替代方案:曲线优化选用一个性能更好的近似方案来替代。
三、案例分析
案例一
配音第一步,需要将多段人声与背景音做混合。
第一个方案需要引入 AacEncoder Mp3Decoder FlacDecode,虽然有现成的实现,但是三者 api 接口不一致,有一定使用成本,并且在 java 层来做 mix 效率也不行,所以不考虑。第二个方案采用 ffmpeg命令行的方式调用,会因为命令的不灵活无法一条命令完成,需要产生一些中间文件,导致产生很多多余步骤。第三个方案采用 Sox/libSox 来,Sox 被称为音频处理的瑞士军刀,专注于音频领域,整个项目代码量不大能够通读了解以便定制开发。针对这个功能参考 Sox 中 mix 的实现调用 libSox 来实现 mix 即可。
libSox 中定义支持编解码的支持很简单,只需要在 soxconfig.h 开启配置,并且将相对应的 编解码库挂载编译即可,而后 libSox 则会通过统一的封装自动处理编码解码。
libSox 将音频处理抽象成添加 effect,将多个 effect 添加到 chain 上处理即可。其中 input_combiner_effect_fn 会提供读取输入文件进行 mix的 effect,而 output_aac_effect_fn 则会提供输出到 aac 文件的 effect。( aac 在 libSox 里面的实现是依赖 ffmpeg 的,所以需要手工引入编码器,自定义 effect 来做输出处理)
effect_hanlder 将音频处理抽象出 start drain stop 这几个阶段,libSox 会将数据流依次流经这几个 callback 进行处理。
这里 mix 操作是将音频 sample 进行加和进行处理。在 combinder_drain 中将 ibuf 中的音频进行加和后输出到 obuf 即可。
针对于 mix 这个场景主要耗时分布在 IO 读写文件、编解码文件、mix 操作。先针对 mix 操作进行优化,会发现 在 combinder_drain 中我们对音频进行加和,将 sample 转成 double 后进行处理的方式是有优化空间的。可以采用 Neon 指令中 VQADDS32 指令,该指令会自动对溢出进行处理,避免原先需要转换成浮点型进行计算,并且在计算后还需要比较是否有溢出。用 neon 实现替换后,会发现对应的汇编指令少了不少,并且整体 mix 的操作耗时减少了 10%-20%左右。
案例二
配音第二步骤,需要将处理完的音频 mux 到视频上。mux 也有两个方案,一是 Java 的 Mp4parser 二是 ffmpeg。
首先比较一下Mp4Parser和FFmpeg的代码库大小,前者大小会小很多。当然此处 ffmpeg 这个尺寸大小是因为没有经过细致裁剪导致的,如果支持 mux 可能只需要 几百kb,但是因为 so 的大小和支持的 cpu 架构有关,所以整体上的大小肯定是 ffmpeg 大的。
然后再对比一下性能。发现首次调用的时候两者耗时差距比较大,在多次调用后差距逐渐缩小。就此倘若解决这个首次调用的耗时问题,选用 mp4parse 也未尝不可。
使用 Method Profiling 对 mp4parse 的调用进行分析,会发现首次运行主要耗时在 getResourceAsStream 这个方法,而 getResourceAsStream 这个方法在 Android 上的实现非常慢。所以解决这个问题也很简单,参照 jodatime-android 的实现,用 android get resource 的方式即可。
案例三
Mix+Mux 的耗时是受到视频长度,音频长度影响的,所以总耗时无法控制,需要用进度动画来提示用户,但是这在一定程度上是打断用户操作。
就此曲线优化,在预览的时候并不合成,而是定制播放器来实现实时的预览。通过定制 exoPlayer 支持播放时指定外部音轨和 注入 MixAudioProcessor 来实现实时混合。
案例四
配音作品中加上了添加版权信息。
采用 ffmpeg 的实现,取视频最后一帧作为背景,并且动态生成版权消息绘制到图片上,最后将图片生成1.5s的视频,将这视频拼接到作品上即可。其中最后一帧图片的获取转为服务端生成下发,可以避免30%的耗时。
虽然以上优化减少了一定耗时,但是视频编码操作仍然非常耗时,所以采用 rxJava 提供的 BehaviorSubject 将生成操作提交交给后台进行预处理,而后将依赖于他的合成操作用 flatMap 串联即可。