因为HTML5 和 标签的限制
- 不支持流
- 不支持 DRM 和加密
- 很难自定义控制, 以及保持跨浏览器的一致性
- 编解码和封装在不同浏览器支持不同
浏览器出了 MSE 解决 HTML5 的流问题。
Media Source Extensions(MSE)是 Chrome、Safari、Edge 等主流浏览器支持的一个新的Web API。MSE 是一个 W3C 标准,允许 JavaScript 动态构建 和 的媒体流。它定义了对象,允许 JavaScript 传输媒体流片段到一个 HTMLMediaElement。
MediaSource
媒体源扩展 API(MSE) 提供了实现无插件且基于 Web 的流媒体的功能。使用 MSE,媒体串流能够通过 JavaScript 创建,并且能通过使用 和 元素进行播放。 该规范允许通过JavaScript为 和 动态构造媒体源,它定义了 MediaSource 对象,作为 HTML 5 中 HTMLMediaElement 的媒体数据源。MediaSource 对象可以有一个或多个 SourceBuffer 对象。应用程序可以向 SourceBuffer 对象动态添加数据片段,并可以根据系统性能及其他因素自适应调整所添加媒体数据的数据质量。来自 SourceBuffer 对象的数据可以解码为音频、视频或文本数据,并由浏览器或播放器处理。与媒体源扩展一同使用的,还是包括媒体原扩展字节流格式注册表及一组预定义的字节流格式规范。
要注意 MediaSource 浏览器的兼容性。具体的API可以查看官网 MediaSource
MediaSource 的简单使用
var supportMediaSource = 'MediaSource' in window // 判断是否支持 MediaSource
if (supportMediaSource) {
// 新建一个 MediaSource 对象,并且把 mediaSource 作为 objectURL 附加到 video 标签上
var mediaSource = new MediaSource()
var video = document.querySelector('video')
video.src = URL.createObjectURL(mediaSource)
// 监听 mediaSource 上的 sourceOpen 事件
mediaSource.addEventListener('sourceopen', sourceOpen);
function sourceOpen {
// todo...
}
}
SourceBuffer
SourceBuffer 对象提供了一系列接口,可以动态地向 MediaSource 中添加视频/音频片段(对于一个 MediaSource,可以同时存在多个 SourceBuffer)。
function sourceOpen () {
var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
// 新建一个 sourceBuffer
var sourceBuffer = mediaSource.addSourceBuffer(mime);
// 加载一段 chunk,然后 append 到 sourceBuffer 中
fetchBuffer('/xxxx.mp4', buffer => {
sourceBuffer.appendBuffer(buffer)
})
}
// 以二进制格式请求某个url
function fetchBuffer (url, callback) {
var xhr = new XMLHttpRequest;
xhr.open('get', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
callback(xhr.response);
};
xhr.send();
}
拉流
video元素本身支持h264编码的mp4、vp8编码的webm、theora编码的ogg视频格式。浏览器内部处理拉流逻辑。
对于像flv格式的视频流数据,我们需要自行拉取数据。
浏览器依赖 HTTP FLV 或者 WebSocket 中的一种协议来传输FLV。其中HTTP FLV需通过流式IO去拉取数据,支持流式IO的有 fetch 或者 stream。
Fetch API
Fetch API 会在发起请求后得到的 Promise 对象中返回一个 Response 对象,而 Response 对象除了提供 headers、redirect() 等参数和方法外,还实现了 Body 这个 mixin 类,而在 Body 上我们才看到我们常用的那些 res.json()、res.text()、res.arrayBuffer() 等方法。在 Body 上还有一个 body 参数,这个 body 参数就是一个 ReadableStream。
fetch(this.url, {
method: "GET"
}).then(resp => {
const { status, statusText } = resp;
resp.body.getReader().then(result => {
let { value, done } = result;
value = new Uint8Array(value ? value : 0);
this.data = concat(this.data, value);
if (done) {
this.done = true;
}
});
});
Streams API
this.xhr = new XMLHttpRequest();
this.xhr.open("GET", this.url);
this.xhr.responseType = "arraybuffer";
this.xhr.setRequestHeader("Range", `bytes=${startIndex}-${endIndex}`);
this.xhr.onload = () => {
if (this.xhr.readyState == 4) {
if (this.xhr.status >= 200 && this.xhr.status 小于等于 299 ) {
if (!this.emitted) {
this.emitted = true;
}
this.startIndex = endIndex + 1;
resolve(new Uint8Array(this.xhr.response));
}
}
};
this.xhr.send();
流操作基础
拿到了流以后,就需要对流进行操作。前端对于ArrayBuffer操作有两种方式,TypeArray 和 DataView。当然对于各个字节序的读取没有Node原生的Buffer对象方便。
常用的操作有:
- 对bit单元的操作
- 对字节/ 字节序的操作
对bit的操作
// 获取一个字节的前4个bit
readUInt8(buffer, 0) & 240) >> 4;
// 获取一个字节的后4个bit
readUInt8(buffer, 0) & 15;
对字节/ 字节序的操作
读取字节、字节序
/**
* 读取32位大端字节序
*/
function readUInt32BE(buffer: ArrayBuffer, offset: number) {
return new DataView(buffer, offset).getInt32(0, false);
}
/**
* 读取24位大端字节序
*/
function readUInt24BE(buffer: ArrayBuffer, offset: number) {
const arr = new Uint8Array(buffer);
return (arr[offset] << 16) | (arr[offset + 1] << 8) | arr[offset + 2];
}
/**
* 读取16位大端字节序
*/
function readUInt16BE(buffer: ArrayBuffer, offset: number) {
return new DataView(buffer, offset).getInt16(0, false);
}
/**
* 读取8位大端字节序
*/
function readUInt8(buffer: ArrayBuffer, offset: number) {
const arr = new Uint8Array(buffer);
return arr[offset];
}
写入字节流
// 申请一个byteLength长度arraybuffer空间
const typeArray = new Uint8Array(byteLength);
// 将newBuffer插入到arraybuffer空间中的offset位置
typeArray.set(new Uint8Array(newBuffer), offset);
复制字节流
let a = new Uint8Array([1,2,3,4,5,6]);
let c = a.slice(0, 5);
共享字节流
let a = new Uint8Array([1,2,3,4,5,6]);
let b = a.subarray(0, 5);