洞察 video 超能力系列——玩转 flv

avatar
@字节跳动


从2016年10月(Chrome 54)开始,Chrome不再内置flash,而是改为用户第一次访问flash资源时提示安装。从Chrome62开始,不再提供“click to play的选项”,改为点击视频box后,左上方弹出

这意味着,flash作为过时的标准将被新技术所取代。


前言

从我们可以在网站上播放视频开始,到h5播放器们如火如荼地发展之前,使用flash一直都是web播放视频的不二之选。甚至于说得更加广泛一些,在html5成为主流之前,网站的多媒体能力,包括动画、游戏和视频一直都是adobe生态在掌控。那么随着html5标准的推出,这一切都成为了过去式。

flv与直播方案

flv的全程是 Flash Video,顾名思义,就是专门给flash播放器提供的播放格式,这种格式具有结构简单、清晰的优点,最早出现是为了解决flash导出的swf文件体积过大,不适合在web中播放的问题。随着flash的逐渐淘汰,新的video标签自然是不支持flv格式的,人们开始把web视频的重点放在mp4或者hls上,但是随着直播这种视频形式的火热兴起,flv迎来了新一轮的生机。

我们来横向对比一下可用的直播方案:

flv播放实现·跳转·清晰度切换

可以看到,flv由于其编码格式的特点,只需要一个MetaData 以及音视频Track各自的Header就可以在任意的时间点播放,极大地符合实时直播的需求,在GOP足够小的情况下甚至可以达到0延迟,可以说在现有的技术方案里,http-flv是最理想的一个,正是因此,flv依然是web播放器不可或缺的一个格式支持。

首先我们通过一张图了解一下前端播放flv的过程

从上图可以看到,flv播放的过程其实是从flvt中提取元信息、音视频header以及数据,然后转码成fmp4盒子结构的过程,再通过MSE交给video的过程。因为flv的结构本身就是流式的,也就是说,它的数据被拆分到了很多个小的tag中,所以我们可以很方便地做数据的封装,也就是说将一段flv tag数据封装成一个独立的moof_mdat盒子对,然后就可以直接交给MSE处理,非常的方便。这里我们讨论flv是如何处理加载、跳转播放、重放及清晰度切换问题的

数据加载 数据加载直播和点播两种模式,首先来聊一下点播:针对点播的数据获取,我们采用Range这个参数作为分段加载的控制参数

如上图,Range这个参数向服务器描述了希望加载一个flv文件的 100000 到 3987705 个字节 这段数据,相应的,服务器也需要能够根据Range参数正确返回这段数据。这里展示一个极简的服务端代码:

从上述代码可以看到,服务端是先是解析range,根据range切出文件分段,然后返回一个readableStream交给前端。

再看一下直播:直播跟点播是非常不一样的数据流动结构,我们看一下简单的直播流程

可以看到,直播的流程是一个 推流->服务端格式编码-> 终端播放 的一个流程。那么这里就带来了一个问题,我们不可以像点播时那样去加载数据了。因为服务端实际上保存的是一个文件流,不断有数据推到服务端,播放器也不存在缓存一段数据这样的情况,而是不断向服务器请求,只要有新的流到达服务端,播放器就要拿到这一段进行解码播放,达到推流和拉流同步进行的一个直播效果。为了实现这种效果,在播放器内,我们使用 fetch+ streamReader,简单的实现如下:

从上述代码我们可以看到,我们通过一个reader递归地从流里面读取数据,再将数据交给解码器进行处理。关于直播的数据加载还有更加先进的websocket方式,这里不再深入探讨,有兴趣的同学可以自行查阅相关资料。

播放时跳转 播放时点击进度条跳转(下称seek)是一个非常高频的操作,尤其是在长视频中。用户遇到不想看的部分,或者想重看的片段,都会点击进度条触发seek。举个例子,一段视频,可能用户实际观看的部分只占不到50%, 如果我们将整段视频加载,那剩下加载的50%就浪费掉了。所以我们要做的事情就是精确地加载用户希望播放的部分,节约流量。解决方法如下:

  1. 获取用户将要跳转到的时间点,下称seekTime根据seekTime计算出离该时间点最近的一帧的位置,称为 startPos

  2. 以预加载时间为30s 为例, 计算出seekTime + 30s 这个时间点最近的帧位置 称为 endPos

  3. 我们以 Range: startPos-endPos 为请求参数,向服务端请求这一段数据

  4. 解码播放

这里说起来简单,但是涉及到了一个跟flv格式紧密相关的问题,那就是flv的onMetaData信息里是否具有keyframes这个属性。我们上述所提到的,基于时间点计算某一帧位置的算法,是完全依赖keyframes这个属性的,它记录了flv中所有关键帧的时间点和文件偏移量,keyframes大概长这个样子:

times中每一个时间点都对应着同位置的fileposition的偏移量,正是基于这一点,我们可以计算需要加载的数据Range。值得注意的一点是,并不是所有flv文件都携带了keyframes头,flv文件缺失部分onMetaData属性是很常见的事情。缺失了keyframes信息,我们就没办法做跳转了,因此我们需要借助额外的工具帮我们补全flv的onMetaData。这里推荐使用yamdi,一个轻量级的工具,有兴趣的同学可以看一下它的使用。

清晰度切换 清晰度切换是一个非常重要的功能,为什么重要呢,因为用户会根据网速的快慢,去选择更清晰或者更流畅的视频。假设没有这个选项,用户看视频卡顿了没法切换清晰度,就只能愤愤地关掉窗口了。那么多个清晰度的视频源我们如何做到无缝的切换呢?我们分情况来讨论一下:

点播中的清晰度切换方案 点播的切换清晰度是比较容易实现的,流程如下

如图,简而言之,就是尝试加载视频B,通过从视频B的onMetaData中提取关键帧信息,推算出当前应该加载哪一个关键帧,然后加载这个位置之后的数据,直到数据加载到之后,将视频B的数据交给MSE,同时,清除掉之前视频A在buffer中的缓存。

直播中的清晰度切换方案 直播清晰度切换目前还没有发现无缝的方案,因此建议在切换时,直接重建解码器以及MSE,关于这方面的问题我们还在探究。更多内容请关注我们的开源播放器,也欢迎来我们github提issue。