乐趣区

海外高性能视频你得这么玩

作者:忘语 转载自:公众号 方凳雅集

一、流媒体与 RangeRequest

1. 流媒体

对于一般的数据请求,我们要等整个请求数据完全返回才能做后续的处理,但是对于视频这种大文件,等整个响应结束再处理显然不合理。流媒体就是对文件按时间纬度进行切片并顺序存储,网络传输时,客户端可以接收一个切片展示一个切片。

视频的流媒体编码格式有很多,其中浏览器支持最好的是 mp4,它得到了 PC / 无线端所有主流浏览器的支持:

一般的视频文件,为了画面连续,一秒会采集 30 帧的画面;在这么短的时间内,每帧的画面变化其实很少。mp4 文件针对视频的这种特性,会将画面中连续差别不大的帧分为一组,分组开始的帧称为关键帧(I 帧),对关键帧只做常规的图像压缩,保存完整的图像信息;然后在分组中的其它帧里取几个断点作为 P 帧,P 帧不保存完整的图像信息,只保留当前帧跟关键帧(或者上一个 P 帧)之间的差异信息;分组内其它帧(B 帧)在关键帧和 P 帧之间,保存当前帧跟前后两帧之间的差异信息;这样可以在不影响画面质量的情况下,得到非常高的压缩率,这也是 mp4 文件能够这么普及的主要原因。

mp4 很好,但是这个协议并不开源,商业使用需要专利授权;为此 apple 开发了 mov 格式,chrome 则直接推出了开源的 webm 格式,两个格式都在 mp4 的压缩原理基础上做了进一步的优化,有更好的清晰度和压缩率。

2. RangeRequests

对于视频播放来说,还有一个很重要的就是要支持用户调整播放进度,支持让用户选择直接跳到中间部分开始播放。这就需要 HTTP Range Requests 协议了,这个协议很简单,支持客户端在 http 请求时通过 Range 请求头指定要请求文件的起始和结束位置。

curl http://test.video -H "Range: bytes=0-1023"

如果服务器支持 Range Requests 协议,会读取 test.video 文件,并将他的第 0~1023 字节提取出来,并以状态码 206 响应请求;
如果服务器不支持,直接忽略 Range 头,读取整个文件内容,以状态码 200 响应即可。
当我们在 html 中放一个 video 标签,浏览器会直接发起一个 Range: bytes=0- 的请求,向服务器请求从开始到结尾的完整文件,如果服务器不支持 Range Requests,响应码为 200,浏览器会正常按流式加载整个视频文件;
如果服务器支持 Range Requests,响应码为 206,则浏览器会在接收到足够字节(一般是比当前播放进度往后推 20s)时结束掉请求,以节省网络流量;当播放进度继续往前,缓存不够时,浏览器会发起一个新的 Range Requests 请求,请求的 Range 直接从缓存结尾的字节开始,只加载剩余的部分文件;

类似的,在我们调整播放进度,跳过一部分内容时,浏览器会把当前的 Range Requests 结束,从选择的播放进度开始重新发起新的 Range Requests,接收到分段内容后直接从新的进度开始播放。

二、视频按流量计费与防盗流

1. 视频按流量计费

我们前面提到,如果服务器支持 Range Requests,浏览器会在缓存足够时提前结束请求而不是加载整个视频文件。这主要是因为一般视频服务器都是按流量计费的,流量套餐跟我们手机的网络流量套餐类似,你可以选择一个保底的月流量总量,超过总量的,每 G 加多少钱,下面是我们 icbu 跟 akamai 签订的流量套餐,大家可以感受一下:

2. 防盗链

既然视频流量这么金贵,我们必须限制视频只在我们自己的网站上播放。防盗链也比较简单,视频的 cdn 地址里会有一段定期失效的加密串,加密串校验失败时,cdn 不返回内容。我们在网站上不直接使用视频 cdn 地址,而是一个带视频 id 的鉴权链接,鉴权成功后 302 到实时生成的带加密串的 cdn 地址。

三、视频性能指标

1. 有效播放率

前面我们已经介绍了视频相关的一些基础知识,在开始进行视频性能优化之前,先要确定好能反应视频性能的几个关键指标。
首先,我们看的最根本的指标是视频的有效播放率,icbu 这边定义视频正常播放 3s 为一个有效播放(这个还有宽窄口径之分,窄口径下在可视区域的播放才算),同个视频 有效播放的 uv / 开始播放 uv 称为视频的有效播放率

2. 首帧播放时间

有效播放率是一个偏业务的指标,很大程度上受视频质量的影响,首帧播放时间则是实打实的纯性能指标了。这个计算比较简单,视频在播放中或者进度信息调整时会触发 timeupdate 事件,当视频在播放状态时,第一个 timeupdate 事件跟上一个播放事件的时间差就是首帧播放时间了。
对于自动播放的视频,可以将视频节点插入 dom 的时间作为起始时间,这样可以避免视频预加载的影响,也可以把鉴权,302 跳转等时间计算在一起,衡量整个链路的加载时间。

3. 卡顿率

除了首帧的加载速度,还有一个非常影响体验的是视频的流畅度。卡顿率的计算稍微复杂一些,不能简单计算视频的卡顿次数,因为一个小时的视频卡顿 10 次跟 10s 的视频卡顿 10 次体验是完全不一样的。一般的计算方法是:

(总卡顿时长 / 总播放时长)*(卡顿次数 * 权重)

打点的时候注意要分段打点,每次卡顿都要记录卡顿前后的播放片段时长和卡顿时长。最终数据统计的时候再按视频 id 和 session 进行加合。

4. 错误率

这个不用多解释,我们要尽量将视频的加载错误率降到最低。视频出错一般有几种情况:客户网络问题、视频源问题和 cdn 回源超时。
视频源问题,一般是视频源被用户删除,导致视频不可用。这一类问题服务器一般会返回 404 等错误;注意我们在 html 中插入 video 时最好使用 <source> 标签指定视频地址并监听 <source> 元素本身的错误事件,直接通过 <video> 的 src 属性指定视频地址时,对于资源 404 的错误,<video> 元素无法捕获到。

四、视频性能优化

1. 流媒体 CDN 支持

经过以上的分析,我们可以开始进行视频的性能优化了。与其它前端资源一样,第一步要通过 cdn 提升资源的加载速度。不同于普通的静态资源 cdn,流媒体需要专门的 cdn 支持,他与普通的 cdn 主要有两方面的差别:
1)流媒体 cdn 回源超时时间更长

 由于流媒体文件普遍很大,回源时间会很长 

2)流媒体 cdn 支持分段 cache 和回源

 比如客户端请求一个视频文件的 0~1024 字节,而 cdn 节点上已经有完整文件的时候,节点直接从缓存文件中取对应字节返回而不会回源到源站;另一方面,如果节点上只有 0~512 字节的缓存,节点会只向节点请求 512~1024 分段的内容,而不会整体回源;

2. 鉴权服务和源站本地化部署

为了防盗链,用户不能直接访问 cdn 链接,而是要经过一层鉴权服务;这层鉴权服务必须本地化部署,否则首帧播放时间会慢很多。另一方面,视频文件的缓存命中率很低(ICBU 这边的视频缓存命中率只有 65.5%),会有大量的客户请求回源,我们必须把视频源站也进行本地化部署。

19 年初,ICBU 这边在淘宝视频同学的大力支持下,将所有视频源文件进行了中美 OSS 同步,将鉴权服务部署到了美国机房,采购了海外的 akamai cdn,整个视频播放流程改造如下:

美国部署改造后整个海外视频的首帧播放时间提升了 1s+

3. 视频内容压缩

3.1 使用压缩率更高的文件格式

mov 和 webm 格式压缩率更好,而且 ios 和 android 端分别支持两种格式,对于移动端场景可以尝试使用。不过格式不统一会让 cdn 命中率进一步降低,具体效果我们也还在测试验证中。

3.2 视频分辨率

视频分辨率指视频的宽高尺寸,类似图片的尺寸,1920×1080 代表视频宽 1920 像素,高 1080 像素;常见的几种视频分辨率有:1280×720(别名 720P)、1920×1080(别名 1080P)、2048×1080(别名 2K)、4096×2160(别名 4K)
如果我们的视频播放窗口只有 350 像素,而卖家上传的视频是 1080P 的,这无疑会浪费大量的带宽,视频性能也会很差。目前淘宝视频会在视频上传完成后进行简单的处理,生成低清、标清和高清三种分辨率的视频,分别对应 720、540 和 360 三个尺寸断点。例如一个 1920×1080 的视频,转出高清视频时会限制视频宽或者高为 720 像素,这样转换得到的高清视频分辨率为 1280×720;类似的转出标清视频为 960×540,低清视频为 640×360。这样我们根据不同的播放需要选择加载不同清晰度的视频即可。

3.3 视频帧率

我们知道视频是利用人眼的视觉暂留现象,通过一张张图片来实现动态效果的:

每秒展示多少张图片(帧),叫做视频的帧率(FPS),fps 越高,视频看起来就越顺畅:

对于一般的视频,24~30 的帧率已经足够,人眼基本看不出卡顿,事实上 24FPS 已经是 80 多年的电影帧率标准。目前淘视频还没有专门处理视频帧率,线上也有一些卖家上传的高帧率视频文件。

3.4 音、视频内容分离

另一个能有效压缩视频体积的办法是将音、视频文件分离,因为我们有很多场景是把视频作为底图或者 gif 来用的,加载音频文件非常浪费

4. 分段请求优化

浏览器内置的视频播放器只能满足基本的视频播放需求,我们无法通过 js 来控制分段请求的大小或者操作视频缓存。比如我们拖放视频进度到第 3s 开始播放,浏览器加载到第 5s 时,我们又将进度跳回第 1s 开始播放,这时第 3~5s 的内容已经缓存完毕,但是浏览器内置播放器发起的分段请求是请求从第 1s 直到文件结束的所有数据信息。
通过 H5 的 MediaSource API,可以很方便地通过 js 来控制视频的加载与播放:

// 1. video 的 src 不是某个特定的 url,而是 mediaSource 对象的内存地址
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);

// 2. 通过 mediaSource 创建一个 mp4 编码的视频缓存区
const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');

// 3. 请求所需的视频分段,将数据放入缓存中
fetch('/video', {
    headers: {'Range': 'bytes=1024-2048',}
})
.then(response => response.blob())
.then(buffer => sourceBuffer.appendBuffer(buffer));

// 视频有足够的缓冲后即可以开始播放视频了
video.play();
退出移动版