Media Source Extensions (MSE) 是一个 JavaScript API,可让您构建用于从音频或视频片段播放的串流。虽然本文未介绍,但如果您想在自己的网站中嵌入视频,以执行以下操作,则需要了解 MSE:
- 自适应流式传输,换句话说就是根据设备功能和网络状况进行调整
- 自适应拼接,例如广告插入
- 时移
- 控制性能和下载大小
您几乎可以将 MSE 想象成一个链。如图所示,下载的文件和媒体元素之间有多个层。
- 用于播放媒体的
<audio>
或<video>
元素。 - 包含
SourceBuffer
的MediaSource
实例,用于馈送媒体元素。 - 用于检索
Response
对象中的媒体数据的fetch()
或 XHR 调用。 - 调用
Response.arrayBuffer()
以馈送MediaSource.SourceBuffer
。
实际上,该链如下所示:
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.log('The Media Source Extensions API is not supported.');
}
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function (response) {
return response.arrayBuffer();
})
.then(function (arrayBuffer) {
sourceBuffer.addEventListener('updateend', function (e) {
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
});
sourceBuffer.appendBuffer(arrayBuffer);
});
}
如果您能从上述说明中理清问题,可以随时停止阅读。如需更详细的说明,请继续阅读。 我将通过构建一个基本的 MSE 示例来介绍此链条。每个构建步骤都会向上一步添加代码。
关于清晰度的注意事项
本文是否介绍了您在网页上播放媒体时需要了解的所有信息?不会,它只是为了帮助您理解您在其他地方可能遇到的更复杂的代码。为清楚起见,本文档简化了许多内容,并排除了许多内容。我们认为可以这样做,因为我们还建议使用 Google 的 Shaka Player 等库。在整个过程中,我会指出我有意简化的地方。
不涵盖的几项内容
以下是我不会介绍的一些内容(排名不分先后)。
- 播放控件。我们可以使用 HTML5
<audio>
和<video>
元素免费获得这些功能。 - 错误处理。
用于生产环境
在正式使用 MSE 相关 API 时,我建议注意以下几点:
- 在对这些 API 进行调用之前,请处理所有错误事件或 API 异常,并检查
HTMLMediaElement.readyState
和MediaSource.readyState
。在关联的事件传送之前,这些值可能会发生变化。 - 请确保在更新
SourceBuffer
的mode
、timestampOffset
、appendWindowStart
、appendWindowEnd
之前检查SourceBuffer.updating
布尔值,或者对SourceBuffer
调用appendBuffer()
或remove()
,以确保之前的appendBuffer()
和remove()
调用仍在进行中。 - 对于添加到
MediaSource
的所有SourceBuffer
实例,请确保在调用MediaSource.endOfStream()
或更新MediaSource.duration
之前,其updating
值均不为 true。 - 如果
MediaSource.readyState
值为ended
,则appendBuffer()
和remove()
等调用或设置SourceBuffer.mode
或SourceBuffer.timestampOffset
会导致此值转换为open
。这意味着,您应做好处理多个sourceopen
事件的准备。 - 处理
HTMLMediaElement error
事件时,MediaError.message
的内容有助于确定失败的根本原因,尤其是对于难以在测试环境中重现的错误而言。
将 MediaSource 实例附加到媒体元素
与当今 Web 开发中的许多方面一样,您需要先从特征检测开始。接下来,获取媒体元素,即 <audio>
或 <video>
元素。最后,创建 MediaSource
的实例。它会转换为网址,并传递给媒体元素的 source 属性。
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
// Is the MediaSource instance ready?
} else {
console.log('The Media Source Extensions API is not supported.');
}
能否将 MediaSource
对象传递给 src
属性似乎有点奇怪。它们通常是字符串,但也可以是 blob。
如果您检查包含嵌入式媒体的网页并查看其媒体元素,就会明白我的意思。
MediaSource 实例是否已准备就绪?
URL.createObjectURL()
本身是同步的;不过,它会异步处理附件。这会导致您暂时无法对 MediaSource
实例执行任何操作。幸运的是,我们可以通过一些方法来测试这一点。最简单的方法是使用名为 readyState
的 MediaSource
属性。readyState
属性描述了 MediaSource
实例与媒体元素之间的关系。它可以是以下值之一:
closed
-MediaSource
实例未附加到媒体元素。open
-MediaSource
实例已附加到媒体元素,并且已准备好接收数据或正在接收数据。ended
-MediaSource
实例附加到媒体元素,其所有数据都已传递到该元素。
直接查询这些选项可能会对性能产生不利影响。幸运的是,MediaSource
在 readyState
发生变化时也会触发事件,具体而言是 sourceopen
、sourceclosed
和 sourceended
。在我所构建的示例中,我将使用 sourceopen
事件来告知我何时提取和缓冲视频。
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
<strong>mediaSource.addEventListener('sourceopen', sourceOpen);</strong>
} else {
console.log("The Media Source Extensions API is not supported.")
}
<strong>function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
// Create a SourceBuffer and get the media file.
}</strong>
请注意,我还调用了 revokeObjectURL()
。我知道这似乎为时过早,但在媒体元素的 src
属性连接到 MediaSource
实例后,我可以随时执行此操作。调用此方法不会销毁任何对象。它确实允许平台在适当的时间处理垃圾回收,这就是我立即调用它的原因。
创建 SourceBuffer
现在,我们需要创建 SourceBuffer
,它是真正负责在媒体源和媒体元素之间传送数据的对象。SourceBuffer
必须特定于您要加载的媒体文件的类型。
在实践中,您可以通过使用适当的值调用 addSourceBuffer()
来实现此目的。请注意,在以下示例中,MIME 类型字符串包含一个 MIME 类型和 两个编解码器。这是视频文件的 MIME 字符串,但它为文件的视频和音频部分使用了不同的编解码器。
MSE 规范版本 1 允许用户代理在是否同时需要 MIME 类型和编解码器方面有所不同。有些用户代理不需要,但允许仅使用 MIME 类型。某些用户代理(例如 Chrome)会要求对不能自行描述其编解码器的 MIME 类型使用编解码器。与其一一列出,不如同时包含这两项。
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.log('The Media Source Extensions API is not supported.');
}
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
<strong>
var mime = 'video/webm; codecs="opus, vp09.00.10.08"'; // e.target refers to
the mediaSource instance. // Store it in a variable so it can be used in a
closure. var mediaSource = e.target; var sourceBuffer =
mediaSource.addSourceBuffer(mime); // Fetch and process the video.
</strong>;
}
获取媒体文件
如果您在互联网上搜索 MSE 示例,会发现很多使用 XHR 检索媒体文件的示例。为了更贴近前沿技术,我将使用 Fetch API 及其返回的 Promise。如果您尝试在 Safari 中执行此操作,则必须使用 fetch()
polyfill,否则无法正常运行。
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
<strong>
fetch(videoUrl) .then(function(response){' '}
{
// Process the response object.
}
);
</strong>;
}
生产级画质播放器会在多个版本中保存同一个文件,以支持不同的浏览器。它可以为音频和视频使用单独的文件,以便根据语言设置选择音频。
现实世界的代码还会包含不同分辨率的媒体文件的多个副本,以便适应不同的设备功能和网络条件。此类应用能够使用范围请求或片段以分块加载和播放视频。这样,系统就可以在播放媒体时适应网络状况。您可能听说过 DASH 或 HLS 这两个术语,它们是实现此目的的两种方法。对此主题的完整讨论超出了本简介的范围。
处理响应对象
代码看起来已完成,但媒体无法播放。我们需要将媒体数据从 Response
对象获取到 SourceBuffer
。
将数据从响应对象传递到 MediaSource
实例的典型方法是从响应对象获取 ArrayBuffer
并将其传递给 SourceBuffer
。首先调用 response.arrayBuffer()
,它会将 Promise 返回给缓冲区。我已将该 promise 传递给第二个 then()
子句,并在该子句中将其附加到 SourceBuffer
。
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function(response) {
<strong>return response.arrayBuffer();</strong>
})
<strong>.then(function(arrayBuffer) {
sourceBuffer.appendBuffer(arrayBuffer);
});</strong>
}
调用 endOfStream()
在所有 ArrayBuffers
都附加完毕且预计不再需要媒体数据后,请调用 MediaSource.endOfStream()
。这会将 MediaSource.readyState
更改为 ended
并触发 sourceended
事件。
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function(response) {
return response.arrayBuffer();
})
.then(function(arrayBuffer) {
<strong>sourceBuffer.addEventListener('updateend', function(e) {
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
});</strong>
sourceBuffer.appendBuffer(arrayBuffer);
});
}
最终版本
以下是完整的代码示例。希望您对媒体源扩展有了一些了解。
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.log('The Media Source Extensions API is not supported.');
}
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function (response) {
return response.arrayBuffer();
})
.then(function (arrayBuffer) {
sourceBuffer.addEventListener('updateend', function (e) {
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
});
sourceBuffer.appendBuffer(arrayBuffer);
});
}