极致首帧播放方案-零首帧解决方案

技术运营 @ 北京字节跳动科技有限公司

作者:琨君

背景介绍

首帧时间,是指用户从点击开始播放到视频首帧画面展现出来的时间。「零首帧」并不是真的0毫秒启播,而是用户几乎感知不到有首帧时间的存在,在我们的播放质量埋点中对应小于100ms以内的首帧时间。 在我们的播放器中,在各环节提供了极致的首帧优化方法,在条件允许符合时,可以将首帧时间压缩到100ms以下,用户感知到的就是完全平滑播放,没有首屏的顿感。 当然在现实业务中,有些场景是无法使用所有的优化条件的,比如在随机播放的场景不能进行预加载、某些场景不适合使用播放器复用技术等。结合实际业务场景,尽量多的使用我们提供的优化能力,就可以使大部分用户体验为零首帧情况。

首帧的构成

首帧的概念,首帧对于视频播放体验的影响

首帧时间,是视频类应用的一个重要核心指标,也是影响用户观看体验的核心因素之一。举个例子,如果一个视频需要花好几秒时间才加载出首帧,大部分用户都等不到首帧加载出来就放弃播放了。因此,优化视频播放的首帧时间是极其重要的。 上面这幅图是一个视频点击播放的整个流程,可以看出首帧时间主要包含这么几个部分: 获取视频播放链接,网络建连,下载视频头部数据,音视频解码和渲染。 本文将从视频播放的整个过程出发,介绍首帧优化的一些通用方案。同时在本文最后,会以长视频场景播放和带历史进度的起播为例,介绍面向场景的首帧优化。

通用的首帧优化方法

获取播放地址

播放地址随feed下发

视频播放的第一步就是获取视频资源的播放链接,通常而言,视频资源会有唯一标识video id, 在点播的服务端会有一个根据video id信息获取播放链接的服务,如果app server端能够调用vod服务生成播放地址,然后将播放地址随feed流一起下发,则省去了客户端的一次网络请求耗时。

网络建连

DNS预解析,预连接、连接复用,https TLS 1.3 false start, session复用 0RTT

在拿到播放地址之后,播放器会去与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以上的耗时。如果能够将视频的首帧提前渲染好而不播放,将会缩减掉这部分的耗时。而预渲染则是提前将视频的首帧渲染出来的技术。具体来说,预渲染会提前解码出视频首帧,并且将首帧渲染出来,但是这个过程中音频不会播放出来。 举个例子,在小视频场景上,当滑动视频卡片时,就已经开始启动预渲染,在卡片滑动过程中,视频的首帧很可能就已经通过预渲染加载出来,这样当卡片滑到中央时,则直接启动播放,这时候用户基本上感受不到视频的加载。

结合使用场景的优化

前面提到各种首帧优化的手段都是比较通用的策略,而面向场景的优化也是极为重要的,在本文接下来的部分会介绍两种场景下的起播优化: 1.长视频场景的播放优化 2.带历史进度的起播优化。

长视频场景优化

mp4格式介绍,moov大小与时长关系

短视频通常采用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的数据。如果我们限制只在关键帧位置起播,则可以避免这些额外数据的下载,从而显著缩短首帧的耗时。

总结

本文主要按照首帧的各个阶段分别介绍了对应的优化方案,也简单介绍了预加载和预渲染这两个优化首帧的利器,在文章最后针对长视频以及历史进度起播这两种场景,介绍了对应的优化手段。

文章分类
前端
文章标签