前端同学要使用 HTML5 播放器视频,必然会使用 video 标签,不过大多数同学只是使用了较简单的功能,其实它本身拥有不凡之力有待我们发现。
首先,我们先来看下 video 最基础的用法:
-
使用 src 属性
<video src="http://v2v.cc/~j/theora_testsuite/320x240.ogg" controls> 你的浏览器不支持 <code>video</code> 标签。 </video>
-
使用 source 标签
<video controls> <source src="foo.ogg" type="video/ogg"> <source src="foo.mp4" type="video/mp4"> Your browser does not support the <code>video</code> element. </video>
这是 MDN 关于 video 给出的基本用例。在这里我们简单介绍下两种方法的不同,src 只能赋予 video 一个播放地址,当浏览器不支持这种视频格式的解码时就会出现错误,导致视频播放失败。为了解决这个问题才有了 source 标签,利用多个 source 标签引入不同格式的视频,从上到下解析直到遇到看上述代码当浏览器不支持 ogg 格式时,浏览器会自动播放 foo.mp4。
实用技巧
我们会发现使用 video:src 属性播放视频的时候会经常出现播放失败,我们该怎么做能提升视频播放的质量?
这种情况一般都使用 cdn,为了更保险一般还会将不同的 cdn 厂商分为主用 cdn 和备用 cdn。那么问题来了,我们怎么利用 video 本身的特性并结合 cdn 保证我们的视频播放质量?
<video controls>
<source src="//a.com/main.mp4" type="video/mp4">
<source src="//b.com/backup.mp4" type="video/mp4">
Your browser does not support the <code>video</code> element.
</video>
不难看出,source 标签不仅支持不同视频格式的自动切换,也适用于相同视频格式的失败切换,即 main.mp4 因网络问题无法获取时浏览器会自动切换到 backup.mp4 。
高阶技能
如果大家浏览视频的时候,可以发现很多网站的 video 是这个样子的:
<video src="blob:http://abc.com/d0823f0f-2b2a-4fd6-a93a-e4c82173c107">
</video>
如果直接访问 blob 这个地址发现并不存在。
这个地址就是映射 Blob 对象的 DOMString,其实 video 属性 src 支持 Blob 的,不过新的标准是使用 srcObject 属性来替代这一个功能,目前代码可以这样写:
const mediaSource = new MediaSource();
const video = document.createElement('video');
try {
video.srcObject = mediaSource;
} catch (error) {
video.src = URL.createObjectURL(mediaSource);
}
在这段代码中除了 Blob 对象,还有 MediaSource 对象,让 video 拥有不凡之力的主要因素是浏览器对 MediaSource 对象的支持,才让 JavaScript 对 video 有了更大的操纵空间。关于如何使用 MediaSource 不在本文讲述,大家可以自行查阅,我们要说的是 video 结合 MediaSource 可以做哪些事情?
清晰度无缝切换
点播领域里 mp4 是最普遍、兼容性最好的视频容器,不过 mp4 也有它的局限性,比如常见的清晰度切换,我们是无法像youtube那样做到无缝切换的。我们可以看下普通的mp4播放的网络请求和youtube视频播放的网络请求的区别。
图1.1 普通mp4的下载请求过程
图1.2 Youtube视频下载请求过程
这两张图不难看出,在默认情况下 mp4 使用一次 http 请求所有的视频数据,Youtube 则分次请求。当然这个描述很不专业,但确实形象。造成这种差异的是 video 不支持流式的视频数据,Youtube 采用的是流式的视频容器 webm,而 mp4 是非流式的。那如何解释清楚流式的视频数据呢,从专业的角度三言两语很难说清楚,但用大白话翻译过来就是流式的视频数据支持分段独立播放,非流式的不可以。换句话说一个10M的视频文件,流式的视频可以把0~1M的数据请求回来单独播放,但是非流式的不可以。
上面我们描述了视频格式的不同,接下来我们要说的是第一张图中的视频加载是浏览器来控制的,通过给 video 的 src 属性配置视频地址,触发播放之后浏览器就会开始下载了,JS干涉不了。而 Youtube 的视频加载是通过JS来控制的,各位可以再次看下第二张图的网络请求类型:xhr,足以证明这一点。
上面两点搞清楚之后我们就该说下清晰度切换的事情了。这个需求大家都不陌生,但是直接使用 mp4 格式做无缝清晰度切换,难度还挺大的。先解释下“无缝清晰度切换”的概念:从播放一个分辨率的视频到另一个分辨率且保证画面、声音不停顿的平滑切换过程。了解了这个概念,大家应该知道了用 video 无缝切换 mp4 有多难。一方面,video 是不支持流式的视频格式的,一方面,video 的加载是不受JS控制的。通过切换 video 的 src 属性,必然会导致画面中断、重新请求视频数据等。有的同学想到说利用两个 video 再结合 z-index 来搞,但是当你生成另一个video去加载视频的时候,无法保证两个画面是严格一致的,即使将原来的画面暂停到一个时刻,用另一个视频通过 currentTime 属性与之同步,切换仍然看到画面闪烁,基本无法和 Youtube 无缝切换的体验匹敌。而且还会造成更多流量的浪费,背后的原因大家可以研究下 mp4 容器和 webm 容器的异同,也可以看下视频解码相关的文章。
还有一种方法就是将 mp4 格式统统转码到流式的视频格式比如 hls、webm 等。不过这种看上去可行的方式实际上会带来很大的成本开销,如将大量视频做转码会消耗高昂的机器资源、双倍存储的费用、CDN的双倍费用等等。其实我们也是在这种背景下研究出来新的技术问题解决清晰度无缝切换的。
首先,我们改变对 mp4 视频的播放流程,不再直接使用 video 的 src 来播放,因为我们没有任何可以操作的空间。video不仅支持 src 属性还支持 Blob 对象,我们就是利用后者。播放的流程如下:
图1.3 mp4 视频新播放流程
- 来请求 mp4 视频数据,这样可以结合视频 Range 服务,做到精确加载。
- 编写解析器将加载回来的部分 mp4 视频数据进行解复用
- 将解复用的视频数据转成 fmp4 格式并传递给 MediaSource
- 使用 video 进行解码完成播放
然后在做清晰度切换的时候流程如下:
图1.4 mp4视频清晰度切换原理示意图
- 来请求 mp4 视频数据,这样可以结合视频 Range 服务,做到精确加载。
- 编写解析器将加载回来的部分 mp4 视频数据进行解复用
- 将解复用的视频数据转成 fmp4 格式并传递给 MediaSource
-
使用 video 进行解码完成播放
然后在做清晰度切换的时候流程如下:
图1.5 mp4视频清晰度切换流程示意图这个过程看上去比较繁琐,但是所有的操作都是在浏览器端完成,也就是说都是JS来实现的。这样之前说的所有成本问题都不存在,还能做到youtube相同体验的无缝切换。如果大家也想使用这个功能不需要自己再去实现一遍上述流程,可以使用如下代码:
import Player from 'xgplayer'; import 'xgplayer-mp4'; let player = new Player({ el:document.querySelector('#mse'), url: [{src:'/mp4/',type:"video/mp4"},{src:'/mp5/',type:'video/mp4'}] }); player.emit('resourceReady', [{name: '高清', url: '/mp4/',cname:'高清'}, {name: '超清', url: '/mp5/',cname:'超清'}]);
如果对这段代码有什么疑惑或者想深入了解下它背后是如何实现的可以参考 文档 或者 Github。
节省视频流量
我们平时直接使用video加载视频,大概是这样的:
图2.1 video默认下载截图
我随便找了个视频,大家看下视频总长度是 02:08,在播放到 00:05 的时候,浏览器已经下载到 01:30 了,如果用户终止观看,下载的视频就这样被浪费掉了。当然,如果不断的 seek 也会造成较多的流量浪费。按照我们之前的统计在短视频领域,用户 seek 的频率在 80%,所以这部分流量是可以节省掉的。具体原理如下:
图2.2 播放器加载视频原理
- 设置每次加载的数据包大小
- 设置预加载时长
- 开启加载队列,完成第一次数据包下载,判断缓冲时间和预加载时长是否满足,不满足请求下一个数据包
具体实现代码如下:
import Player from 'xgplayer';
import 'xgplayer-mp4';
const player = new Player({
id:'vs',
url:'//abc.com/a/mp4',
preloadTime:10
});
这样就实现了视频在播放过程中永远只预加载10秒的数据,进而保证节省流量。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。