企鹅FM(Android) 播放成功率从 2 个 9 到 3 个 9 的蜕变

2021-08-03 14:56:11 浏览数 (1)

作者:张陈博男

业务层播放器架构演变

企鹅FM android端的播放器架构经历过两次较大的调整

第一次是2.1版本,首次引入了以FFmpeg为基础的腾讯视频SDK,替换了之前一直使用的系统播放器,结束了不同机型上表现不一和调用其api在不同版本上出现莫名其妙崩溃的历史,点播成功率最终优化到99.7%左右,HSL直播的成功率优化到97%附近。

第二次是3.7版本,使用了Google的开源播放器内核ExoPlayer替换了腾讯视频SDK,到目前最新的3.8版本,点播成功率已经优化到99.9%,HLS直播成功率优化到99.2%

最近半年包括更换ExoPlayer的诸多努力,都是朝着99.9%这个方向去的。这个优化过程中,最艰辛的是具体问题case by case的解决,不过回过头从架构上看,也是可以提炼出一些原则,来甄别到底什么样才是一个好的业务播放器。

  1. 一套统一的代码,这也是作为一个优秀的业务播放器的必备条件,否则假如建立在系统播放器基础上(各个厂商都会修改系统播放器代码),同样的实现在不同的机型上几乎无法做到表现一致,更遑论成功率了(常常是改动了调用方式后,在一个机型ok另一个机型不行,不同系统版本间也有此类问题),所以实现一个好的播放器,第一步就是先统一播放器内核。
  2. 完善的错误信息统计,播放是一个复杂的行为,牵扯到数据的预加载,加载,解码和最终给到系统AudioTrack播放,当支持了分片加载和缓存后这个模型就变得更加复杂,于是错误是不能避免的,但最重要的是,如何通过错误的统计上报,能够直指问题的根本,在此基础上,才有空间进一步优化成功率。
  3. 播放器内核和业务层足够解耦,只有设计上的解耦,才能给更换更好的播放器内核打下基础,否则如果每次切换都会带来巨大的业务逻辑调整,本身就会引入很多和播放器无关的问题,对成功率优化会适得其反。
  4. 和播放器内核对接的功能模块尽可能结构简单,这符合KIS原则,要在可扩展性和模块的结构简单易维护上作出协调,当代码足够简单直白,问题往往会更容易暴露和得到解决。

横向对比3中播放器内核:

播放器

代码统一 错误统计

接入层复杂度

系统MediaPlayer

不完善,播放错误码分散而且很多错误错误码相同

腾讯视频SDK

不完善,过滤日志 播放错误 转化为业务层错误码

谷歌ExoPlayer

除MediaCodec以外是

完善,所有错误都通过java层异常抛出,直接转化成对应处理逻辑或者业务层错误码

注:这里的接入层指的是为了实现完整的业务逻辑,在播放器内核外围的逻辑层

换ExoPlayer与奥卡姆剃刀

常做优化的同学肯定很清楚,越是小数点后面的9,越来之不易,90%到99%再到99.9%,这其中的困难可以说是指数上升的。那么是什么东西去鼓动我们换掉已经维护的很成熟的腾讯视频SDK而换用谷歌的ExoPlayer呢,动力来自于寻求到3个9的突破,而思想来自于奥卡姆剃刀原则——如无必要,勿增实体。

腾讯视频很完善,具备一切我们需要的功能,但是太过于庞杂:最下层是FFmpeg,然后是C 实现的播放器逻辑,上层一个java接口层和部分逻辑。因为发起请求的逻辑封装在播放器底层,所以为了实现分片下载和缓存的策略,增加了一层本地的Http代理。

这个架构可以完整的实现所有我们要的播放功能,可以处理播放请求,也可以分片下载和缓存,也可以添加音效和改变播放速度,但是问题也有不少:

  1. C/C 层的逻辑过多,首先、这部分逻辑不易维护,产生的逻辑问题,主要通过日志来排查。其次、C/C 层的逻辑一旦出异常,堆栈极其难以定位到原因,而且就算定位到了,FFmpeg带来的问题也比较棘手。再次、处理数据就必须经过多次jni传递,这降低了效率。
  2. 本地代理带来的结构上的冗余。引入本地代理是因为腾讯视频SDK的请求部分是写死的,无法在其中再加入我们自身的比如文件头zip压缩和分片下载缓存的逻辑。但是本地代理本身把一个请求的链路拖长了,而且本地tcp socket同样有这各种各样的断开问题和连接超时问题,实质上增加了整个系统出错的概率。

于是当发现ExoPlayer能够很完善解决这两个问题的时候,我们就进行了替换

  1. 得益于ExoPlayer高度可扩展的特性,我们去除了本地代理模块,将分片加载和缓存以及音频的变速和特效处理模块直接集成进来,砍掉了很多冗余的通信
  2. 而且采用了ExoPlayer以后去除了大部分的C/C 层逻辑,剩下的jni通信基本都属于系统组件,譬如MediaCodec和AudioTrack,对于实现者来说可以当做透明,目前日登陆百万的用户量来看,MediaCodec在各个机型上兼容性较好,投诉较少(目前仅收到两例初始化MediaCodec失败的投诉)
  3. ExoPlayer纯Java的实现,也帮助了我们尽可能收归各种错误信息,转换成业务错误码

总体来讲,剔除掉了不必要的逻辑后,代码更加的简洁,而且数据的路径也更加简短,这提升了可维护度和降低了出错概率。

其他的补充手段

仅仅靠换播放器内核和重新设计业务逻辑接入是没法做到极致的,这里还针对点播和广播(HLS)做了一些额外的优化

  1. ExoPlayer是通过抛异常来上报各种播放错误的,起初我们把全部的异常都算在播放错误中,导致播放错误偏高,后来发现这里面很多异常其实是自己代码实现的逻辑问题,需要解决,所以播放错误仅仅应当统计播放下载过程中无法解决的问题,而不应该包括代码的逻辑缺陷,后者应当继续抛出crash,由bugly上报解决。
  2. MediaExtractorPeriod和HlsMediaChunk的cancelLoadable()方法都没有调用dataSource的close()方法,这里我们加上了这个调用,原因在于快速切换节目时,如果不关闭前一个正在进行的连接有可能导致大量连接堆积会耗尽socket或者是Http连接池中的资源
  3. 针对播放HLS中的BehindLiveWindow异常进行一定次数重试,该问题通常是资源问题或者连接太慢导致,可以通过重试恢复
  4. 针对免流带来的连接超时问题,3.8版本加入了针对王卡优先直连(联通王卡类支持腾讯IP免流)的策略,也进一步提升了成功率

总结

只要保持代码架构的简洁和解耦,有着良好的错误信息反馈机制,加上长期的问题跟进,打造99.9%的成功率也不是难事,对吗。

0 人点赞