点击上方👆蓝字关注我们!
背景介绍
首帧时间,是指用户从点击开始播放到视频首帧画面展现出来的时间。 「零首帧」并不是真的 0 毫秒启播,而 是用户几乎感知不到有首帧时间的存在 ,在我们的播放质量埋点中对应小于 100ms 以内的首 帧时间。
在我们的播放器中,在各环节提供了极致的首帧优化方法,在条件允许符合时,可以将首帧时间压缩到 100ms 以下,用户感知到的就是完全平滑播放,没有首屏的顿感。 当然在现实业务中,有些场景是无法使用所有的优化条件的,比如在随机播放的场景不能进行预加载、某些场景不适合使用播放器复用技术等。 结合实际业务场景,尽量多的使用我们提供的优化能力,就可以使大部分用户体验为零首帧情况。
首帧的构成
首帧时间,是视频类应用的一个重要核心指标,也是影响用户观看体验的核心因素之一。举个例子,如果一个视频需要花好几秒时间才加载出首帧,大部分用户都等不到首帧加载出来就放弃播放了。因此,优化视频播放的首帧时间是极其重要的。
上面这幅图是一个视频点击播放的整个流程,可以看出首帧时间主要包含这么几个部分: 获取视频播放链接,网络建连,下载视频头部数据,音视频解码和渲染。本文将从视频播放的整个过程出发,介绍首帧优化的一些通用方案。同时在本文最后,会以长视频场景播放和带历史进度的起播为例,介绍面向场景的首帧优化。
通用的首帧优化方法
获取播放地址
视频播放的第一步就是获取视频资源的播放链接,通常而言,视频资源会有唯一标识 video id , 在点播的服务端会有一个根据 video id 信息获取播放链接的服务,如果 app server 端能够调用 vod 服务生成播放地址,然后将播放地址随 feed 流一起下发,则省去了客户端的一次网络请求耗时。
网络建连
在拿到播放地址之后,播放器会去与 CDN 建连,首先会进行 DNS 解析。而为了防止 DNS 劫持,大部分客户端会采取 HTTPDNS 的方式做 DNS 解析,这又会涉及到一个网络请求的耗时。我们可以采取 DNS 预解析的策略,比如 app 启动时,服务端就可以提前下发 app 可能用到的域名,客户端则可以对这几个域名提前做 DNS 解析,并做缓存。
在 HTTP 1.1 中支持连接复用,因此我们可以预先创建几个与 CDN 的 socket 连接,然后在播放时直接复用连接即可。另外,为了应对内容劫持,在部分地区播放开启了 https,而 https 相比于 http 多了 TLS 握手的过程,这个握手过程会给视频首帧多引入 2 个 RTT 。通过 TLS False Start 加上 session 复用,可以做到 0RTT 握手。
音视频首包
减少 probe、moov 位置:在播放器与 CDN 完成建连后,播放器就开始下载视频文件, 首先播放器会尝试探测视频文件的格式、编码等信息。如果视频源经过服务端统一转码,那么就可以省去这个探测的过程。同时,值得一提的是,常见的 mp4 视频文件,有一个 moov box,这里面会存储音视频流 track 信息比如解码信息、以及音视频帧与文件对应的关系(用于 seek ),因此通常播放器都会先下载 moov 的数据。而 moov 的位置则会对起播造成一定的影响。举个例子,如果 moov 在文件尾部,当下载了视频前面部分数据,发现 moov 没找到,就去尾部查找 moov,这样就又多了两次网络请求。对于这个问题,我们可以通过转码将 moov 挪到视频文件的头部,从而缩短首帧耗时。
音视频解码
解码器异步初始化、解码器复用:通常情况下,在播放器读取到视频数据,拿到视频的解码信息后就可以开始创建解码器解码了。不过解码器创建这个过程,尤其是在 Android 平台上 MediaCodec 的创建是一个比较耗时的操作。这里我主要介绍两个优化: 解码器异步初始化和复用。如果 app server 提前把视频的解码信息传递给播放器,那么播放器就可以在建连的同时去异步初始化解码器,这样就可以减小硬解创建耗时的影响。而解码器复用则可以完全消除这个耗时,顺着这个思路,我们可以做播放器线程的复用甚至整个播放器的复用,这些方法都可以大幅优化首帧耗时。
起播水位
理论上,要做到极致首帧,可以当视频首帧解码完成就直接播放。但是实践发现,这样做会导致视频播放的卡顿增多,尤其是视频起播后 1~3s 的卡顿增加。经过大量的实验,我们发现,如果对视频起播做一定限制,比如让起播之前缓存一定的数据,这样可以大幅减少卡顿,同时对首帧的影响不大,并且可以显著提升用户的观看时长和观看 vv。
预加载
预加载是一种常见的首帧优化措施,我们可以提前下载视频数据的一部分以达到快速起播的目的。但是什么时候去预加载、预加载多少、并行预加载数量等都是实际需要考虑的问题。
首先是预加载时机的问题,对于 15s 的短视频而言,完全可以等到当前视频加载完成之后再启动预加载,这样预加载就不会和当前视频的播放抢占带宽。但是如果一个视频时长超过 1min,我们就得重新考虑预加载时机的问题了。具体来说,我们需要考虑当前播放视频的可用缓存、当前网络的下载速度、当前视频的码率以及即将预加载视频的码率、并行预加载数量,通过这些数据我们能够构建一个模型去预测接下来视频播放的卡顿状况,如果大概率是不会发生卡顿,则可以开启预加载,反之则不启用或者暂停预加载。
另外一个问题,预加载多少,直观认识,至少得保证首帧能加载出来。一个粗略的估算方法是 moov 大小加上视频的平均码率 * 预加载时长,这样就可以通过服务端下发 moov 头大小及视频的平均码率,然后在 app 端上通过实验去调整预加载时长参数,进而调整预加载大小。
预渲染
预加载只能够将网络请求的耗时消除掉,但播放器还是需要经历解复用、解码、渲染的步骤,在中低端机器有 200ms 以上的耗时。如果能够将视频的首帧提前渲染好而不播放,将会缩减掉这部分的耗时。而预渲染则是提前将视频的首帧渲染出来的技术。
具体来说,预渲染会提前解码出视频首帧,并且将首帧渲染出来,但是这个过程中音频不会播放出来。举个例子,在小视频场景上,当滑动视频卡片时,就已经开始启动预渲染,在卡片滑动过程中,视频的首帧很可能就已经通过预渲染加载出来,这样当卡片滑到中央时,则直接启动播放,这时候用户基本上感受不到视频的加载。
结合使用场景的优化
前面提到各种首帧优化的手段都是比较通用的策略,而面向场景的优化也是极为重要的,在本文接下来的部分会介绍两种场景下的起播优化。
长视频场景的播放优化
短视频通常采用 mp4 这种视频格式,前面也提到过 moov 的下载是 mp4 视频起播的重要条件,而 moov 的大小则与视频时长正相关,粗略统计 moov 的大小约为40KB/min。这样1h的长视频 moov 头就有2.4MB,如果平均网速1MB/s,则需要2.4秒的加载时间,这对于弱网用户而言是极差的体验。而 fmp4这种视频格式则能很好解决这个问题,fmp4 将一个完整的视频拆分成若干个小的片段,而每个片段的索引则存在于 sidx box 中,这样起播所需要的数据量就大幅下降,从而缩短了首帧耗时。另外,长视频往往有前贴广告,我们也可以在前贴广告播放器期间,结合预渲染提前加载正片首帧。
带历史进度的起播优化
在中长视频中有一个功能是记住历史进度播放,通常的实现方式是 seek 到历史进度前面最近的一个关键帧,然后把视频帧塞给解码器,在解码器中做丢帧处理,直到 pts 到了指定的历史进度。假设这个视频的码率是 4Mbps,视频的 GOP 大小为5s,那么这种场景的起播最坏情况需要额外下载 4 * 5=20Mb 的数据。如果我们限制只在关键帧位置起播,则可以避免这些额外数据的下载,从而显著缩短首帧的耗时。
总结
本文主要按照首帧的各个阶段分别介绍了对应的优化方案,也简单介绍了预加载和预渲染这两个优化首帧的利器,在文章最后针对长视频以及历史进度起播这两种场景,介绍了对应的优化手段。
活动预告
6 月 26 日,火山引擎开发者社区将举办第三期 Meetup,我们邀请到了字节跳动的 4 位技术大咖,同大家一起聊聊亿级 DAU 背后的音视频、直播、点播、RTC 等最佳实践,探讨如何打造极致的音视频用户体验。扫描下方二维码或点击【
阅读原文
】立即报名!