媒体来源附加信息

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

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 实例已附加到某个媒体元素,且其所有数据均已传递给该元素。

直接查询这些选项可能会对性能产生负面影响。幸运的是,MediaSource 也会在 readyState 发生更改(具体而言,即 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);
    });
}

反馈