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
。在传送关联事件之前,这些值可以更改。 - 确保之前的
appendBuffer()
和remove()
调用仍在进行中,方法是先检查SourceBuffer.updating
布尔值,然后再更新SourceBuffer
的mode
、timestampOffset
、appendWindowStart
或appendWindowEnd
,或者对SourceBuffer
调用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
的实例。它会转换为网址
并传递给媒体元素的来源属性
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);
});
}