前言
提到网页播放视频,大部分前端首先想到的肯定是:
<video width="600" controls>
<source src="demo.mp4" type="video/mp4">
<source src="demo.ogg" type="video/ogg">
<source src="demo.webm" type="video/webm">
您的浏览器不支持 video 标签。
</video>
的确,一个简单的video
标签就可以轻松实现视频播放功能
但是,当视频的文件很大时,使用video
的播放效果就不是很理想:
- 播放不流畅(尤其在:首次初始化视频 场景时卡顿非常明显)
- 浪费带宽,如果用户仅仅观看了一个视频的前几秒,可能已经被提前下载了几十兆流量了。即浪费了用户的流量,也浪费了服务器的昂贵带宽
理想状态下,我们希望的播放效果是:
- 边播放,边下载(渐进式下载),无需一次性下载视频(流媒体)
- 视频码率的无缝切换(DASH)
- 隐藏真实的视频访问地址,防止盗链和下载(Object URL)
在这种情况下,普通的video
标签就无法满足需求了
206 状态码
<video width="600" controls>
<source src="demo.mp4" type="video/mp4">
</video>
我们播放demo.mp4
视频时,浏览器其实已经做过了部分优化,并不会等待视频全部下载完成后才开始播放,而是先请求部分数据
我们在请求头添加
Range: bytes=3145728-4194303
表示需要文件的第3145728
字节到第4194303
字节区间的数据
后端响应头返回
Content-Length: 1048576
Content-Range: bytes 3145728-4194303/25641810
Content-Range
表示返回了文件的第3145728
字节到第4194303
字节区间的数据,请求文件的总大小是25641810
字节Content-Length
表示这次请求返回了1048576
字节(4194303 - 3145728 + 1)
断点续传和本文接下来将要介绍的视频分段下载,就需要使用这个状态码
Object URL
我们先来看看市面上各大视频网站是如何播放视频?
哔哩哔哩:
腾讯视频:
爱奇艺:
可以看到,上述网站的video
标签指向的都是一个以blob
开头的地址: blob:https://www.bilibili.com/0159a831-92c9-43d1-8979-fe42b40b0735
,该地址有几个特点:
- 格式固定:
blob:当前网站域名/一串字符
- 无法直接在浏览器地址栏访问
- 即使是同一个视频,每次新打开页面,生成的地址都不同
其实,这个地址是通过URL.createObjectURL生成的Object URL
const obj = {name: 'deepred'};
const blob = new Blob([JSON.stringify(obj)], {type : 'application/json'});
const objectURL = URL.createObjectURL(blob);
console.log(objectURL); // blob:https://anata.me/06624c66-be01-4ec5-a351-84d716eca7c0
createObjectURL
接受一个File
,Blob
或者MediaSource
对象作为参数,返回的ObjectURL
就是这个对象的引用
Blob
Blob是一个由不可改变的原始数据组成的类似文件的对象;它们可以作为文本或二进制数据来读取,或者转换成一个ReadableStream以便用来用来处理数据
我们常用的File
对象就是继承并拓展了Blob
对象的能力
<input id="upload" type="file" />
const upload = document.querySelector("#upload");
const file = upload.files[0];
file instanceof File; // true
file instanceof Blob; // true
File.prototype instanceof Blob; // true
我们也可以创建一个自定义的blob
对象
const obj = {hello: 'world'};
const blob = new Blob([JSON.stringify(obj, null, 2)], {type : 'application/json'});
blob.size; // 属性
blob.text().then(res => console.log(res)) // 方法
Object URL的应用
<input id="upload" type="file" />
<img id="preview" alt="预览" />
const upload = document.getElementById('upload');
const preview = document.getElementById("preview");
upload.addEventListener('change', () => {
const file = upload.files[0];
const src = URL.createObjectURL(file);
preview.src = src;
});
createObjectURL
返回的Object URL
直接通过img
进行加载,即可实现前端的图片预览功能
同理,如果我们用video
加载Object URL
,是不是就能播放视频了?
index.html
<video controls width="800"></video>
demo.js
function fetchVideo(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'blob'; // 文件类型设置成blob
xhr.onload = function() {
resolve(xhr.response);
};
xhr.onerror = function () {
reject(xhr);
};
xhr.send();
})
}
async function init() {
const res = await fetchVideo('./demo.mp4');
const url = URL.createObjectURL(res);
document.querySelector('video').src = url;
}
init();
文件目录如下:
├── demo.mp4
├── index.html
├── demo.js
使用http-server
简单启动一个静态服务器
npm i http-server -g
http-server -p 4444 -c-1
访问http://127.0.0.1:4444/
,video
标签的确能够正常播放视频,但我们使用ajax
异步请求了全部的视频数据,这和直接使用video
加载原始视频相比,并无优势
Media Source Extensions
结合前面介绍的206
状态码,我们能不能通过ajax
请求部分的视频片段(segments),先缓冲到video
标签里,然后当视频即将播放结束前,继续下载部分视频,实现分段播放呢?
答案当然是肯定的,但是我们不能直接使用video
加载原始分片数据,而是要通过 MediaSource API
需要注意的是,普通的mp4格式文件,是无法通过MediaSource
进行加载的,需要我们使用一些转码工具,将普通的mp4转换成fmp4(Fragmented MP4)。为了简单演示,我们这里不使用实时转码,而是直接通过MP4Box工具,直接将一个完整的mp4转换成fmp4
#### 每4s分割1段
mp4box -dash 4000 demo.mp4
运行命令,会生成一个demo_dashinit.mp4
视频文件和一个demo_dash.mpd
配置文件。其中demo_dashinit.mp4
就是被转码后的文件,这次我们可以使用MediaSource
进行加载了
文件目录如下:
├── demo.mp4
├── demo_dashinit.mp4
├── demo_dash.mpd
├── index.html
├── demo.js
index.html
<video width="600" controls></video>
demo.js
class Demo {
constructor() {
this.video = document.querySelector('video');
this.baseUrl = '/demo_dashinit.mp4';
this.mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
this.mediaSource = null;
this.sourceBuffer = null;
this.init();
}
init = () => {
if ('MediaSource' in window && MediaSource.isTypeSupported(this.mimeCodec)) {
const mediaSource = new MediaSource();
this.video.src = URL.createObjectURL(mediaSource); // 返回object url
this.mediaSource = mediaSource;
mediaSource.addEventListener('sourceopen', this.sourceOpen); // 监听sourceopen事件
} else {
console.error('不支持MediaSource');
}
}
sourceOpen = async () => {
const sourceBuffer = this.mediaSource.addSourceBuffer(this.mimeCodec); // 返回sourceBuffer
this.sourceBuffer = sourceBuffer;
const start = 0;
const end = 1024 * 1024 * 5 - 1; // 加载视频开头的5M数据。如果你的视频文件很大,5M也许无法启动视频,可以适当改大点
const range = `${start}-${end}`;
const initData = await this.fetchVideo(range);
this.sourceBuffer.appendBuffer(initData);
this.sourceBuffer.addEventListener('updateend', this.updateFunct, false);
}
updateFunct = () => {
}
fetchVideo = (range) => {
const url = this.baseUrl;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.setRequestHeader("Range", "bytes=" + range); // 添加Range头
xhr.responseType = 'arraybuffer';
xhr.onload = function (e) {
if (xhr.status >= 200 && xhr.status < 300) {
return resolve(xhr.response);
}
return reject(xhr);
};
xhr.onerror = function () {
reject(xhr);
};
xhr.send();
})
}
}
const demo = new Demo()
实现原理:
- 通过请求头
Range
拉取数据 - 将数据喂给
sourceBuffer
,MediaSource
对数据进行解码处理 - 通过
video
进行播放
我们这次只请求了视频的前5M数据,可以看到,视频能够成功播放几秒,然后画面就卡住了。
接下来我们要做的就是,监听视频的播放时间,如果缓冲数据即将不够时,就继续下载下一个5M数据
const isTimeEnough = () => {
// 当前缓冲数据是否足够播放
for (let i = 0; i < this.video.buffered.length; i++) {
const bufferend = this.video.buffered.end(i);
if (this.video.currentTime < bufferend && bufferend - this.video.currentTime >= 3) // 提前3s下载视频
return true
}
return false
}
当然我们还有很多问题需要考虑,例如:
- 每次请求分段数据时,如何更新
Range
的请求范围 - 初次请求数据时,如何确保
video
有足够的数据能够播放视频 - 兼容性问题
- 更多细节。。。。
详细分段下载过程,见完整代码
流媒体协议
视频服务一般分为:
- 点播
- 直播
不同的服务,选择的流媒体协议也各不相同。主流的协议有: RTMP、HTTP-FLV、HLS、DASH、webRTC等等,详见《流媒体协议的认识》
我们之前的示例,其实就是使用的DASH协议进行的点播服务。还记得当初使用mp4box
生成的demo_dash.mpd
文件吗?mpd
(Media Presentation Description)文件就存储了fmp4文件的各种信息,包括视频大小,分辨率,分段视频的码率。。。
b站的点播就是采用的DASH协议
HLS协议的m3u8
索引文件就类似DASH的mpd
描述文件
协议 | 索引文件 | 传输格式 |
---|---|---|
DASH | mpd | m4s |
HLS | m3u8 | ts |
开源库
我们之前使用原生Media Source
手写的加载过程,其实市面上已经有了成熟的开源库可以拿来即用,例如:http-streaming,hls.js,flv.js。同时搭配一些解码转码库,也可以很方便的在浏览器端进行文件的实时转码,例如mp4box.js,ffmpeg.js
总结
本文简单介绍了 Media Source Extensions
实现视频渐进式播放的原理,涉及到基础的点播直播相关知识。由于音视频技术涉及的内容很多,加上本人水平的限制,所以只能帮助大家初步入个门而已
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。