很久没写文章,已经有点生疏了。但最近我在开发前端功能的时候,无意间发下一个奇妙的问题,在网上一搜,才发现遇到类似问题的人其实很多。趁着周末,坐下来研究了下问题的原因以及寻找了一些解法,在这里记录我分析解决问题的过程。
首先,问题是这样的。我最近在开发一个需要在网页上播放视频的需求,我们知道眼下基于H5做视频播放,咱们可以使用<video>
标签,使用非常简单,而且经过各方浏览器的底层实现,效果已然很不错,乍一看好像并没有什么值得一提的地方。但是,问题就在于视频的来源出了点问题:服务端由于种种限制和历史包袱,无法直接提供视频文件的存储链接,而只能通过服务器http请求返回视频流(类似:http://127.0.0.1:8181/file?name=movie.mp4
),而前端在<video>
标签中使用这个链接的时候,在Chrome浏览器上就出现了无法控制视频播放进度的问题。从代码的角度也无法通过JS操控<video>
标签的currentTime
属性,很显然作为一个视频播放功能,不能快进/快退,这是使用者无法接受的体验。
虽然我对Chrome青睐有加,并且我的需求也只需要满足Chrome上的播放体验就好。但是出于好奇,我还是在Firefox上面验证了一番,事实证明在Firefox浏览器中并没有出现相同的问题,这么看来应该是Chrome浏览器上的一个兼容性问题。
由于此前并没有遇到过类似的问题,刚开始有些摸不着头脑,我还是先在网上狂搜了一番,找到几篇类似的文章均表明<video>
控件的资源需要是http/https
的,好像跟我遇到的根本不是一个前提,因为我的视频链接本身就是一个http的远程地址(并且是同源的,这个不重要),一番尝试,并没有找到问题的答案。
经过一番摸索,我从一些可以正常操控的在线演示示例中找来一个远程视频文件地址(https://www.runoob.com/try/demo_source/movie.mp4
),直接放到自己的<video>
标签中,发现也能正常使用,于是我确定是我们服务端的代理请求哪里出了问题。为了找到问题原因,我在本地编写了一个类似的返回视频流的http服务。
route.get('/file', ctx => {
const { name = '', type = 'all' } = ctx.request.query || {};
ctx.set({
'Content-Type': mimes[type],
'Content-Disposition': `attachment; filename=${name}`
});
ctx.body = fs.createReadStream(`./${name}`);
});
通过该测试,我完全复原了我之前使用的视频请求的各种条件(Headers),没出意外,在Chrome中使用该链接确实也复现出了前面的问题。
起初我以为是响应头中的Content-Type: application/octet-stream
未指定为具体的视频格式导致,于是将Content-Type
调整为Content-Type: video/mp4
,然而问题并没有得到解决。我只能继续与前面可正常使用的视频请求做对比:
通过对比,很明显的发现表现异常的请求响应头部中少了很多数据。于是我大胆猜测问题是由于缺少响应头配置导致的,排除掉一些明显无关的信息后,我在本地请求的头部进行了数据补全。重新运行,播放视频,拖动进度条,此时已经能正常的进行操控了。我继续对响应头进行反复筛验,最终发现是accept-ranges
和content-length
的缺失(验证发现缺一不可)导致的进度控制失效。
Accept-Ranges响应的 HTTP 标头是由服务器使用以通告其支持部分请求的标识。此字段的值表示可用于定义范围的单位。 如果存在Accept-Ranges标头,浏览器可能会尝试恢复中断的下载,而不是从头再次开始。
测试发现,当视频加载完成前,我们改变视频播放进度的时候浏览器会发出新的请求来加载新的视频片段。所以,我大胆猜测,由于视频可能会是很大的数据包,Chrome浏览器会分段加载,而在单个片段请求中未包含视频的完整长度数据,以至于<video>
控件无法准确计算当前位置在整个视频中的进度百分比,导致控制进度异常。而当我们在响应头中明确告知内容长度之后,该问题得以解决。
现在看来,要解决需求开发上的问题已经不是问题了。但好奇心泛滥,迫使我继续寻找是否还有其他的解法。经过多番查找,还真让我找到了另一种解法,在服务端不做更改的情况下,前端也有对应的解决方法,不过这种解法应该只适用于短、小视频,针对大文件应该就不行了。
const v = document.querySelector('#fetch_v');
fetch('http://127.0.0.1:8181/file?name=movie.mp4').then(res => res.blob()).then(blob => {
const vurl = URL.createObjectURL(blob);
v.src = vurl;
// 此处设置 v.currentTime = 2; 也是完全OK的
});
优点:
- 一次性加载,本地播放,不卡顿
- 无需服务端支持
缺点:
- 只适用于小文件,大文件加载太耗时,体验不佳
- 不支持分段加载
最终效果对比:
如图,第一个视频点击进度条的时候没有任何变化,其余的视频点击的时候画面都会跟随切换,区别在于最后一个视频切换进度的时候并不会伴随新的http请求。
总结,网页视频播放属于比较小众的场景,主要是视频文件一般体积较大,背后需要一些存储服务器,资源CDN等硬件条件,成本相对比较高。当我们在使用<video>
控件遇到问题的时候,如上其实并不是什么好高深的问题,但可能由于网络上相对可查的资源比较少,需要自己想一些办法去找答案。
最后附上测试代码:
route.get(['/', '/index'], ctx => {
ctx.type = 'text/html; charset=utf-8';
ctx.body = fs.readFileSync('./index.html').toString('utf-8');
}).get('/file', ctx => {
const { name = '', type } = ctx.request.query || {};
const fstat = fs.statSync(`./${name}`);
const hs = {
'Content-Type': 'video/mp4',
'Content-Disposition': `attachment; filename=${name}`
};
if (type === 'ok') {
hs['accept-ranges'] = 'bytes';
hs['content-length'] = fstat.size;
}
ctx.set(hs);
ctx.body = fs.createReadStream(`./${name}`);
}).get('/movie.mp4', ctx => {
const fstat = fs.statSync('./movie.mp4');
ctx.set({
'Content-Type': 'video/mp4',
'accept-ranges': 'bytes',
'content-length': fstat.size
});
ctx.body = fs.createReadStream('./movie.mp4');
});