编者按:本文来自微信公众号“InfoQ(ID:infoqchina)”;
注:本文整理自腾讯研发总监王辉在 QCon 北京 2017 上的演讲,原题为:《十亿级视频播放技术优化揭秘》。
QQ 空间在 2016 年日均视频播放量由年初的千万级迅速突破到十亿级,过程中也对整个视频播放技术的可靠性、性能、操作体验等方面提出严峻的考验,相关质量急需提升。经过多个迭代持续和各项优化,外网整体质量已经达标:在保证播放成功率提升到 99.92% 的基础上,首次缓冲耗时降到 0.70s,二次缓冲概率降到 0.48%,做到稳中有升。我们将从视频组件的整体架构,优化效果衡量,首次缓冲耗时优化,播放成功率优化,二次缓冲优化,总结六个方面介绍视频点播在整个优化过程中的心路历程。
写在前面
我今天的话题是“十亿级视频播放技术优化揭密”。介绍我们团队在去年短视频风口上,视频播放量从 5000 万到十亿级过程中的一些技术实践,希望我的分享能给大家带来一些借鉴和参考。
自我介绍一下,我叫王辉,来自腾讯,从 2009 年开始从事 QQ 空间技术研发,近期主要关注手机短视频、视频直播、AI 智能硬件。我个人喜欢跑步,觉得跑步是解决程序员亚健康的一个比较好的方式。
众所周知,短视频去年是一个风口,起因是来自 Facebook 2015 年 Q3 的财报,财报表明在 Facebook 平台上每天有 80 亿次短视频播放,给 Facebook 带来了强劲的广告收入,正是这个数据给国内核心大公司和创业公司带来的一些新的突破口。
其实短视频已经不是一个新的概念,从 2006 年开始国内就有很多公司在做短视频。随着 Facebook 吹起短视频风,去年在短视频行业有近百款应用出现,中国网民里面每 5 个里面就有 1 个是短视频的用户,短视频成为互联网的流量入口。
QQ 空间也在这个风口中,从 2015 年 11 月份的每天 5000 万视频播放量,经过一年的耕耘细作,徒增到 2016 年 12 月份的 10 亿量级,现在还在不断增长。
我的分享主要是按照我们产品迭代的几个关键步骤展开:
1、首先是快速上线,2015 年我也是跟随着大家的体验快速上线了新短视频的体验;
2、其次面临的是成本问题,在做的过程中做了一些成本的优化工作;
3、然后是体验优化。在解决成本问题之后,短视频的观看体验要做到极致。比如说视频的秒开、不产生缓冲、不卡、成功率的提升。
快速上线
首先看快速上线,在开始之前,我先介绍一下我们的团队职责,我们团队负责手机 QQ 和手机 QQ 空间两个 APP,每个 APP 有 iOS 和 Android 两个团队,一共四个团队,四个团队负责两个 APP。在这个项目中,我们四个团队要针对两个平台实现四套逻辑,这里的效率是存在一定的问题。
关于短视频体验,在这之前,我们也只是做到能播放而已,没有做很精细的工作,并且这里的产品观感体验也不是很一致,也不是很好。
技术上,之前也只是做很基础的架构,直接由播放器连接服务器下载数据,达到能播放就可以。之前我们没有积极去做这个事情,导致播放成功率低、失败原因未知、不支持边下边播、缓冲时间比较长等问题,流量浪费也比较严重。
在产品上要解决的,是在整个 APP 里面把所有产品的体验做到一致,比如说每个功能的观看体验,视频浮层的体验,统一观看体验也为我们项目清除了很多障碍。
而这里的技术上的要点是非常关键的,第一个是边下边播,这是基础的要求,是为了加快视频播放速度。第二个是流量的控制,这么大的平台,之前只是做 5000 万的播放量,如果没有流量控制的云策略,可能到后面流量是无法把控的。第三,刚才讲到团队的现状,团队要负责两个 APP,这里要做代码复用,不可能再像之前一样四个团队维护四套代码,第四,我们支持第三方的视频源。第五,需要完善监控上报,对业务知根知底。
可以看到,完成核心技术要点最核心的一点是如何控制视频的下载,传统的方式是播放器直接塞播放地址给播放器,它就可以直接播放,这其实是一个黑盒。我们在中间加了一个本地代理,播放器与服务器的数据请求,我们完全可以把控。在这个过程中,比如说播放器要数据时,可以给它更多的数据,这样能解决它缓冲的问题。有了这层代理之后,架构也更清晰一点。
基于 MVC 架构,在 MODEL 一层做一些业务的逻辑,在 VideoController 这一层做控制视频的播放和下载。有了下载代理之后,就可以通过代理管理下载,在 APP 里面有很多的视频请求,VideoProxy 可以管理这些请求,做流量控制,做预加载,还可以做优先级调度和做监控上报,下载逻辑层则主要关注怎么优化服务器,对接缓存管理层,同时我们抽象出了一个数据层,我的数据源可以是 HTTPDataSource,也可以读本地,也可以是来来自腾讯视频的数据源,也可以是第三方 APP 的数据源,协议层主要是 HTTP、HTTPS、HTTP2 的解决。
在 VideoController 的逻辑里,其实都可以放到 C 层来实现,这样安卓和 iOS 完全可以通用,这一层的逻辑可以在 QQ 和 QQ 空间两个 APP 里面使用,相当于是我们一套逻辑可以完全复用,不用再开发四套逻辑。
我们团队的职能也做了相应调整,之前可能是按团队划分,四个团队负责四个终端,现在可能是按 FT 的方式划分做视频的团队,iOS 做视频的团队可能负责 QQ 和 QQ 空间里的业务,安卓也是如此。直播的 FT 也可以这样划分,iOS 的负责 iOS 的两个 APP,安卓的负责安卓的两个 APP,这样代码复用更清晰一点,我的团队更专注一点。视频的团队专注视频的研发。
监控上报,肯定是不可缺少的,这是一个成熟的项目必备的要素:
1、问题定位,老板跟用户反馈说我这个视频播不了,要有一套成熟的问题定位的方式;
2、耗时统计,用户播放这个视频花多长时间播出来,这也是要了解到的;
3、成功率统计,外网用户播放视频的成功率是多少?还要通过实时报警,才能及时知道外网发生一些故障。
传统的捞 Log 方式大家都有,但是这种方式效率太低,需要等用户上线之后才能捞到 Log,Log 捞到之后还得花时间去分析。我们做法的是在关键问题上做一些插装,把每一类错误和每一个具体的子错误都能定义出来,这样一看错误码就知道播放错误是由什么原因导致的。
还可以把每次播放视频的链路所有关键流水上报到统计系统里来,每一次播放都是一组流水,每一条流水里面就包含了例如首次缓冲发生的 Seek,或下载的链接是多少,下载的时间是多少,有了这些流水之后,用户反馈播放失败,我首先可以用流水看发生了什么错误?错误在哪一步?每一步信息是什么?几秒钟就可以定位到问题。
有了这个数据上报之后,还可以做一些报表。比如说可以做错误码的报表,有了报表之后就可以跟进哪个错误是在 TOP 的,负责人是谁,原因是什么,都可以看到。
我们也有自己实时的曲线,可以看到各项数据的情况。
在告警方面,基于成功率和失败率的统计,进行实时告警。一出现错误码,微信立即可以收到提醒,提醒说是什么原因导致这次告警,完全全自动。
成本优化
上线一个月之后,一个坏消息一个好消息。好消息是播放量涨了 4 倍,坏消息是带宽涨了 6 倍,带宽优化是每个做视频的人必须要面临的问题!
我们也分析这个过程中的原因,发现因为改为边下边播之后用户观看视频的意愿比较强,用户有挑选心理,不是每个视频都去看,看了一下之后不喜欢就划走了,之前下载的那部分其实是浪费的。
如果之前不做限速的话,一点开视频就疯狂地下数据,带宽有多大就下多少的数据,这样浪费很严重。我们采取的第一个策略是进行流量控制。在高峰期播放到第 10 秒时,预下载 N 秒数据,下载到 N 秒就停下来。然后,可以做多级限速。一开始不限速,下载到合适时机做 1 倍码率限速。高峰期时预加载的数据会少一些,防止高峰期时带宽占用明显,这是初级的策略。最终我们也有码率切换的策略。这对用户的观看体验影响比较大,这也是之前必备的一个策略。
上线这个策略之后,对带宽的优化还是比较明显的。在高峰期时从 18:00 到凌晨 1 点带宽下降 25.4%,这个是我们不断灰度最终确定的值。这个值会影响播放缓冲,因为数据少的话必定会卡顿,在卡顿之间和流量之间取了一个最优值,最终是 25.4%。
但这样肯定是不够的,因为流量涨的还是很明显的,我们想到 H.265,压缩率相对于 H.264 提升了 30%-50%,但它的复杂度也是呈指数级上升。复杂度导致它的编解码耗时更长,占用资源也更长。如果把 H.265 用在客户端上的话,可能要评估一些点,比如说在编码上面,现在手机上没有做 H.265 硬件支持的,相对于 H.264 的耗时 3-7 倍,之前耗时可能是 10 分钟,而现在可能需要到 70 分钟左右。
解码的硬件支持 H.265 的也很少,耗时差不多是一样的。解码是可行的,你可以采用软解的方式,这个带来的问题是 CPU 占用非常高,可能之前 H.264 占 20% 的 CPU,H.265 占 70%、80% 左右,带来的问题是发热和耗电。
结论,解码是可行的,但是编码不用考虑,在移动客户端不可行的情况下,那编码就要放在后台来做了。
为了解决如何在我们手机上能够解码的问题,对手机的解码能力做一次评估。我在合适的时机做一次大规模的浮点数运算,将数据上传到后台服务器进行云适配。如果当前的指数满足 H.265 条件的话,可以给你下载 H.265 视频给你播放。从而保证软件解码柔性可用,针对视频源规格按机型适配降级,保证用户视频播放体验。
经过评估之后,判断当前机型更适合 360P 还是 480P 等等,保证如果手机不适合 H.265 就不会给你的手机下发 H.265 视频的。
经过我们的统计,外网上有 94% 的手机还是支持 H.265 解码的。支持 1080P 手机的解码占 46%。
编码只能在后台做,如果在视频后台进行全面编码的话,是不现实的。因为编码复杂度呈指数级上升,拿后台服务器进行编码也是不可行的。我们的做法是只用热点视频进行后台转码,不是所有视频都去编码,对观看量在 TOP N 的视频进行编码,只需要编码少量的视频就可以带来流量优化效果,因为 TOP N 就占了全网 80-90% 的流量,只对少量的视频进行解码,让 90% 的视频享受转码的优势。
因为热点转瞬即逝,可能前一分钟是热点,后一分钟就不是热点,社交网络的传播非常快,我们给后台的要求是转码速度一定要快。在之前没有优化时,转一个 10 分钟的视频要半个小时左右。后来做了分布式处理之后,转 10 分钟的视频只用 2-3 分钟。一些短视频最长 5 分钟左右,只要监测到视频很热的话,1 分钟之内就能转出来,就是 H.265 了。
同样,在 H.265 编码器上做了一些优化,比如编码速度和码率的节省都会有提升。
经过 H.265 尝试之后,带宽进一步下降,节省了 31% 左右。
带宽问题解决之后,面临的下一个问题是体验优化。用户最想要的是视频能立马播出来 。
我们定了一个秒开技术指标,只要这个视频从到我的视野范围,到视频播出来之间的耗时在一秒以内。这也是对标 Facebook 的体验,Facebook 一打开动态,视频是能立即播出来的,不需要等待就能播,这个体验其实很顺畅。
核心的流程主要是三个步骤:
1、客户端初始化播放器;
2、下载数据;
3、等待播放。
这里主要有两个大的耗时点,第一下载视频数据耗时;第二个是客户端的耗时,下载视频数据耗时的话,主要是下载数据量和下载的速度。
这里有一个很直接的问题,播放器需要下载多少数据才能播放?
我们可以看一下 MP4 格式,MP4 其实是一个比较灵活的容器格式,每个东西都是用 Box 表达的,每个 Box 又可以嵌入到另外一个 Box。MP4 主要由 MOOV 和 Mdata 组成,MOOV 是囊括了所有的视频关键信息,肯定是先把 MOOV 下载完之后才能找视频数据才能播起来。不巧的是,在我们外网会发现有 5% 左右用户上传的视频,它的 MOOV 是在尾部的。后来也发现,有很多安卓手机比如说山寨机,一些摄像头处理的厂商可能比较偷懒,因为他们只有在你采集完信息之后才能知道他所有的信息,他可能把所有的信息放在尾部。
对于 iOS 来说,一开始把头部下载了,找不到 MOOV,就猜测 MOOV 在尾部,多一次 Range 请求去探测 MOOV 到底在哪?基本做法是去尾部探测, 如果 MOOV 在其他地方的话,这次播放肯定是失败的。安卓某些手机如果没有经过处理,MOOV 在尾部的情况下需要下载整个视频才能开始播放。
我们的处理方式,用户在后台统一做一次转码修复,客户端采集后做一次转码修复。
看一下 Mdata,视频的原数据。目前大部分是 H.264 编码,H.264 通过帧预测的方式进行视频编码。这里有一个 GOP 概念,也是在直播里面经常谈的。一般的播放器需要下载完整的 GOP 数据才可以播。
需要下载多少数据才能播呢?每个播放器的行为也不一样。iOS 要下载一个完整的 GOP 才可以播。像 FFmpeg Based Player 的话只需要关键帧就可以播出来。安卓是比较尴尬的一个系统,在 6.0 及以下,需要下载 5 秒视频数据才可以播起来。
如果需要下载 5 秒数据才可以播的话,肯定是非常慢的。我们这里的策略会采用 FFmpeg Based Player 自己来做解码,要关注兼容性和耗电的问题。解决了 Mdata 之后,如果 MOOV 数据在头部,拿关键信息进行播放的话,其实需要的开始播放数据量是非常小的。
对于下载优化,会有一个防盗链的请求,通过 HTTP 拿到真实的播放 URL 才可以下载数据。
但是在手机上执行 HTTP 请求非常耗时,这里走私有长连接通道做这个事情。
关于优化下载链路,这里也是谈的比较多的,一般也是直接输出 IP 地址,利用 IP 地址做跑马的策略,兼顾性能的效率,这个是用的比较多的方式。
进一步思考,按照普遍 600K 码率的话,我们统计到现在 APP 上面下载的平均速度是 400K 左右,这样计算的话,可能在安卓上面播放一个视频的话,需要将近 0.9 秒左右才可以下载到你需要的数据。
如果码率再进一步提升的话,可能会更大,这其实我们也做了一些场景分析,会发现我们是社交网站,它有好友动态,视频在好友动态里播放,或者是在视频浮层里播放,我们的选择是预加载的策略,这也是常见的策略。
我们会在当前看这条动态时,预加载后面视频的关键信息,比如会加载头部信息和需要播放的数据。在播放当前视频时,加载一定数据之后会加载下一个视频的数据,这些都可以做到的。预加载有一个问题,我们之前踩了一个坑,可能预加载视频时还是要优先图片的。视频当然重要,但是社交网络的图片更重要,可能在预加载视频时会考虑到更高优先级的一些任务。
优化效果也是比较明显,经过刚才几个策略,一个是我们对头和播放器的处理,我们对防盗链的处理,还有对下载链路的处理和预加载,这样我们的耗时大幅度减少了,之前是 1.8 秒降到 0.6 秒左右。
客户端的性能也是让人容易忽视的问题,发现有些用户虽然有视频的缓存,但是播起来还是很慢,这其实是客户端性能的影响。
因为视频涉及到的流程比较多,在这个过程中还要更关注客户端的影响,要分析下客户端哪些在抢占视频播放资源,我们之前犯过一些错误,md5 会卡住一些流程,或者是 HttpParser 会阻止你的任务,会导致视频播放更慢。
在优化视频播放过程中,我们在 4 月份也做直播。直播这里面插入个事情,我们要播放直播的视频流,是 HLS 的视频,在好友动态里面可以观看直播的内容。HLS 在安卓上面体验非常差,因为安卓 3.0 之后对 HLS 基本没有做的优化工作,这里每次安卓上播放 HLS 需要等待 6-9 秒。
分析发现它的处理也不是很得当,因为安卓系统请求链路较长,串行下载,需要下载 3-4 片 TS 才能启动播放,下载 3 个分片的话,耗时就会很久。之前提到我们这里有代理,有了代理之后做事情方便很多了,通过里获取 M3U8,解析 M3U8 里面有哪些文件,可以做并行下载,只让他下载一次 M3U8,这样下载速度大幅度提升。回到刚才架构上,有了下载代理层的话,你可以做 HLS 的加速和管理,可以加入 HLS 的视频源。
回到刚才架构上,有了下载代理层的话,可以做 HLS 的加速和下载管理,可以加入 HLS 的视频源。
HLS 优化效果也是很明显的,之前需要 6 秒左右,现在 1 秒左右就可以播起来。整体从之前的 2 秒左右,现在优化到 700m 秒,0-1 秒的占比目前在 80% 左右,80% 用户都可以在 1 秒内播视频。
体验优化
还有一个是用户比较关注的问题,观看视频时卡,观看一会卡了一下,loading 数据,loading 完以后又卡,这个体验非常差,我们希望所有的视频都不卡。
其实这有两个播放场景,一个是正常场景,边下边看,数据在下载。对于正常场景下载时会做一些带宽调整,在低速时会做切换 IP 的处理,比如说当前连通 IP 的耗时比较久的话,会做一些处理,也会对网络进行速度限制。
针对 Seek 场景,用户拖动,如果文件缓存系统是顺序存储系统的话,必然会造成拖到这里时,后面的缓存数据没有办法下载到系统里面来。只要用户每次拖动,拖动后的下载数据没法存到硬盘上来。
我们就对存储做了一次重构,支持文件空洞。会按照一兆的方式进行文件碎片划分,这就是视频的数据,每一兆就是个文件分片,通过它的 Key 和文件进行存储,这样好处是可以分段存储,可以允许逻辑空洞,拖动的话也可以在后面存储,也不依赖数据库,通过文件名可以知道是从哪个位置到哪个位置的存储。这样淘汰缓存高效一点,可以制定更灵活的缓存策略。可以淘汰更低粒度的文件,比如 seek 之后的文件可以保留,还可以对文件加密。
产生卡顿的用户里面,90% 是因为进行拖动,拖动之后又没有缓存数据,所以这里有可能导致缓存。统计效果也是比较明显的,上了分片缓存之后,之前的缓存概率是 4.6% 左右,最后下降到 0.48%,基本上看不到发生缓冲的场景。
成功率优化,也是比较关键的指标。成功率优化没有捷径,可能是 Case by Case 各个击破。针对错误进行编码,有几百个错误码,每一个错误码捞 log 去看,错误码原因进行上报,每次进行循环一个个错误码进行解决。
DNS 劫持是比较多的,会劫持你的请求。这个是在国内比较常见的劫持,有的小运营商按 URL 劫持视频内容,可能直接污染 DNS 让你查找不到 CDN,这是比较多的,还有一些网络不稳定的影响导致。更高级的直接污染视频内容,让视频内容是错误的。
播放比较多的可能是一些编码的原因,手机采集出来的视频在低端手机上播不出来,我们会对这些视频进行修复。
逻辑上的问题,因为播放器是有状态机的,开发人员比较多,每个人过来加一个逻辑的话,会导致播放状态出现问题。
我们解决播放器错误的方法:HOOK 播放器接口与回调,实现播放器状态机,监控插放器 API 的调用是否合法,不合法直接告警或 Crash。帮助开发快速定位问题,同时减轻测试同事的负担,封装成 UI 组件,使其它开发不必理解播放器。
最终优化的成果是这样的,下载成功率优化前是 97.1%,优化后是 99.9%。播放成功率优化前是 97.0%,优化后是 99.9%。首次缓冲耗时优化前是 1.95s,优化后是 0.7s。二次缓冲概率优化前是 4.63%,优化后是 0.48%。数据还是很可观的。