所有在线内容播放,都面临一些共同的问题:启播慢,seek慢,码率切换慢。而随着互联网的发展,用户对这些方面的要求越来越高,对慢的容忍度越来越差,如何解决这些问题,就成了MXPlayer 在线业务初期优化的重点。
本次分享将分为四个部分:第一部分介绍MXPlayer 如何解决起播慢,seek慢;第二部分介绍如何解决码率切换慢的问题;第三部分介绍缓存面临的问题,以及缓存是如何为前两部分服务的;最后一个部分介绍在复杂业务下,多播放器实例是如何管理的,通过以上几个部分为大家介绍MXPlayer 对在线内容播放优化的经验。
文/赵琳琳
整理/LiveVideoStack
大家好,我是赵琳琳,来自MX Player。我的分享主题是:MX Player在线播放优化。
首先简单介绍一下业务背景:MX Player起初是一款比较纯粹的本地播放器,其体积小,但功能强大,在全球各个地区都有不少的活跃用户。随着时间推移,公司在产品策略上也进行升级,引入了在线化的内容如影视剧、短视频、直播等。
虽然全球都有MX Player的用户,但其中主要用户来自于印度地区。印度地区的用户比较有特色,第一是语言(有十几种主流语言);第二是网络(4G网络占多数,家庭有线宽带较少);第三是设备(偏低端机型,存储空间小)。
随着在线内容的引入,也出现了一些问题。例如启播慢、seek慢、码率切换慢、缓存命中率低、代码复杂度高等。
今天的分享内容主要围绕以上的问题展开,首先介绍秒开、快速seek是如何实现的;之后是平滑码率切换的技术细节;然后是缓存面临的挑战;最后是多播放器事例是如何管理的。
-01-
秒开
第一部分,秒开。如何让用户尽可能快地看到视频内容,是所有在线播放面临的难题。有数据表明,等待时间越长,用户流失越严重。
根据统计,热度视频平均启播时长在2.5秒左右。
经过进一步的分析,发现其中绝大多数时间用于下载数据。
那么如何减少用户的等待时间呢?我们分析了几个优化方向,首先解码占用时间是较少的,优化空间有限;启播缓冲区大概需要2秒等待时间,我们认为这个时间已经很短了,所以不选择减小以优化;最终我们经过分析对比选择了预加载方案。
预加载的流程是:首先服务端根据一些策略判定哪些视频需要预加载,并且把信息告诉客户端。客户端收到请求后,会启动预加载程序。预加载程序会在视频没有播放的情况下启动预加载,把需要加载的视频头2秒放到缓存内。
策略上线后,热度视频的平均启播时长由2.5秒降到1.8秒。如果预加载的缓存命中,启播时长可以达到0.5秒,收益非常可观。
那么如何决策哪些视频需要预加载?服务端会根据视频的标签(新剧/推广/热度)等进行打分,最终做出决策并告知客户端,启动预加载程序。
-02-
快速seek
接下来介绍快速seek。和启播一样,等待时间越长,用户流失越严重。
我们先看一下优化之前的seek过程。假设用户要seek到A点,播放器首先会进入暂停状态,并缓冲接下来6秒的数据,完成之后开始播放。
经过分析,发现用户seek平均等待时间是3.4秒
那如何减少seek时间呢?
前文有提到预加载方案,是否适用于seek呢?答案是No,因为并不知道用户要seek到什么位置,除非缓存整个视频,显然这是不合适的。
另外一个方案是减小缓冲区,例如从6秒调整到4秒或更短。但这种方法比较简单粗暴,无法预知其是否有负面的用户体验影响,所以这个方案也被否定。
那么如何解决呢?经过分析和观察,我们发现当播放器当前的下载速度大于播放速度,不论缓冲区有多少数据,播放都是流畅的。
根据以上结论,我们得出一个快速seek方案:用户seek到A点,播放器暂停并缓冲1秒,得出当前下载速度D和常量播放速度P,如果D>=P,则直接开始播放;如果D<P,就会执行缓冲6秒数据再开始播放。
策略上线后,数据统计分析显示平均等待时长由原先的3.4秒降为2.5秒。
-03-
平滑码率切换
接下来讲平滑码率切换,首先看一下优化前的流程。
箭头所指为当前播放器进度,蓝色为已缓冲内容。如果开始切换,播放器会暂停,并丢弃当前的缓冲区,接着缓冲对应码率的6秒数据,再开始播放。
经过数据统计,码率切换平均等待时间为2.8秒。
如何减小切换等待时间呢?同样的,减小缓冲区的方案比较简单粗暴,直接pass。
可以注意到,图中红色的丢弃缓冲区,原本是可以正常播放的,只是因为用户需要切换码率,所以丢弃。那么是否可以重用该缓冲区呢?答案是肯定的。
如图是优化后的平滑码率切换方案:当播放器进行码率切换时,并不会进入暂停,而是继续播放当前码率上已经缓冲好的数据,并开辟一个新的缓冲区来缓冲对应码流的6秒数据,同时当旧缓冲区数据播放完,进行缓冲区切换。
业务上线后经数据统计,优化后的码率切换平均等待时间由原来的2.8秒降为0.4秒。
-04-
缓存面临的挑战
接下来介绍缓存面临的挑战以及对应的解决方法。
前述提到MX Player在线用户主要分布在印度,他们的设备偏低端,内存空间普遍都非常小。在业务上线之初,我们给缓存空间定了一个上限为2G。上线后,后台收到了大量的用户反馈,说App占用空间过大。经过我们的讨论和决策,把缓存下调到500M,这带来了一个直接的问题:命中率降低。
如图是优化前的缓存布局,在容量不变的情况下,分片越小,个数越多,命中率就会越高。
基于上述结论,我们得出一个方案:两段缓存。
我们把一个完整的分片拆为头尾两部分,头部优先级高,尾部优先级低,并放入缓存空间。当空间满了之后,会先清理掉所有尾部数据。
这是优化后的缓存布局。左图为装满6个分为头尾的分片数据,当用户继续使用一段时间后,缓存空间布局就会变成右图的样子,只留下了各个分片的头部数据。
这时如果播放器需要播放内容,到缓冲区查找数据,就会发现右图的命中率一定是比左图布局要高的。
这时回顾一下预加载和快速seek:预加载方案是缓存视频头2秒,和两段缓存中的头部分片数据是吻合的。在快速seek中,如果在1秒缓冲内可以命中缓存的头部数据,并且下载速度大于播放速度,就可以直接启播。
两段缓存方案在后台默默地为其他功能进行支持,这些方案是相辅相成的关系。
-05-
多播放器如何管理
接下来介绍多播放器如何管理。
随着业务发展,播放场景也越来越多。这些播放场景之间并不是彼此孤立的,如PIP播放和音乐播放就是互斥关系。当PIP正在播放,用户需要切换音乐播放,就需要暂停PIP,反之亦然。
另外,feed流播放需要和PIP播放以及音乐播放共存。因为feed流播放是无声和自动播放,我们并不希望这种播放打扰用户行为。
随着在线业务高速发展,随之而来的是一系列问题。
经过讨论决定,我们选择重构部分代码以解决问题,同时定下几条重构原则:
第一,业务代码彼此独立:业务之间无需互相知道;增加新业务无需修改其他业务;启动播放无需判断是否有其他播放。
第二,环境变化自我感知:当App失去声音焦点,自动暂停;程序进入后台,播放器按需暂停;进入音乐播放后,自动暂停PIP。
基于以上原则,我们重构了代码,结构如图所示。Player代表底层播放器,并抽象出来PlayerContext的概念。每一个Player必须和Context进行attach,才能播放。Context有一个属性为ready,只有在ready情况下,才能够让播放器启动,否则会暂停。
Context有几个具体的实例,首先是Activity,它是一个安卓标准组件,只有在unReceived的时候才是ready状态;Fragment和Activity类似,只有unReceived才是ready状态;Screen在锁屏时为非ready状态,其他时候都为ready;Global永远都是ready状态。
Activity和Fragment主要用于详情页播放和feed流播放;Screen用于PIP播放;Global用于后台播放。
Player和PlayerContext之间不直接交互,而是通过PlayerManager进行管理。它会监听和维护所有PlayerContext的状态,并根据其状态改变来控制播放器的生命周期变化。同时,Manager会维护Player和Context之间的映射关系。
经过这样的结构重构,我们的业务代码基本实现了解耦,新增业务不需要改动已有业务,并且样本代码不需要出现在具体业务中,PlayerManager可以感知并通知Player进行操作。
以上就是我的全部分享,谢谢大家!
▲扫描图中二维码或点击“阅读原文” ▲
查看更多LiveVideoStackCon 2023上海站精彩话题