媒体来源附加信息

François Beaufort
François Beaufort
Joe Medley
Joe Medley

Media Source Extensions (MSE) 是一个 JavaScript API,可让您构建用于从音频或视频片段播放的串流。虽然本文未介绍,但如果您想在自己的网站中嵌入视频,以执行以下操作,则需要了解 MSE:

  • 自适应流式传输,换句话说就是根据设备功能和网络状况进行调整
  • 自适应拼接,例如广告插入
  • 时移
  • 控制性能和下载大小
基本 MSE 数据流
图 1:基本 MSE 数据流

您几乎可以将 MSE 想象成一个链。如图所示,下载的文件和媒体元素之间有多个层。

  • 用于播放媒体的 <audio><video> 元素。
  • 包含 SourceBufferMediaSource 实例,用于馈送媒体元素。
  • 用于检索 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.readyStateMediaSource.readyState。在关联的事件传送之前,这些值可能会发生变化。
  • 请确保在更新 SourceBuffermodetimestampOffsetappendWindowStartappendWindowEnd 之前检查 SourceBuffer.updating 布尔值,或者对 SourceBuffer 调用 appendBuffer()remove(),以确保之前的 appendBuffer()remove() 调用仍在进行中。
  • 对于添加到 MediaSource 的所有 SourceBuffer 实例,请确保在调用 MediaSource.endOfStream() 或更新 MediaSource.duration 之前,其 updating 值均不为 true。
  • 如果 MediaSource.readyState 值为 ended,则 appendBuffer()remove() 等调用或设置 SourceBuffer.modeSourceBuffer.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.');
}
将来源属性作为 blob 进行处理
图 1:blob 形式的来源属性

能否将 MediaSource 对象传递给 src 属性似乎有点奇怪。它们通常是字符串,但也可以是 blob。 如果您检查包含嵌入式媒体的网页并查看其媒体元素,就会明白我的意思。

MediaSource 实例是否已准备就绪?

URL.createObjectURL() 本身是同步的;不过,它会异步处理附件。这会导致您暂时无法对 MediaSource 实例执行任何操作。幸运的是,我们可以通过一些方法来测试这一点。最简单的方法是使用名为 readyStateMediaSource 属性。readyState 属性描述了 MediaSource 实例与媒体元素之间的关系。它可以是以下值之一:

  • closed - MediaSource 实例未附加到媒体元素。
  • open - MediaSource 实例已附加到媒体元素,并且已准备好接收数据或正在接收数据。
  • ended - MediaSource 实例附加到媒体元素,其所有数据都已传递到该元素。

直接查询这些选项可能会对性能产生不利影响。幸运的是,MediaSourcereadyState 发生变化时也会触发事件,具体而言是 sourceopensourceclosedsourceended。在我所构建的示例中,我将使用 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);
    });
}

反馈