Media Source 系列 - 使用 Media Source Extensions 播放视频

终于有时间写关于 Media Source Extensions (后面简称 MSE)Media Source Extensions 是在 2016年成为推荐的的 Html5 API。

This specification extends HTMLMediaElement [HTML51] to allow JavaScript to generate media streams for playback. Allowing JavaScript to generate streams facilitates a variety of use cases like adaptive streaming and time shifting live streams.

草案简单明了的指出这个 API 设计的目的:

允许 JavaScript 来生成看到播放的流媒体扩展了 HTMLMediaElement 对象。允许 JavaScript 来生成流促进了很多用途,如可自 适应的流和可进行时间变换的直播流

W3C 草图案例

透过概念,实际上我们就可以理解它所适应的场景,比如播放流文件(m3u8) 或者 我们现在比较火的直播领域都能够使用它。

MSE 在使用的时候我们还需要关心它浏览器支持情况,很不幸,在 IE 上,我们依旧要考虑兼容性的问题。

初步使用

图 Google Developer -
Media Source Extensions

MSE 处理的三要素大概如上图所示:

  • Video / Audio 这些媒体元素
  • MediaSource 的实例对象
  • 需要请求下来的 MediaSource.SourceBuffer 资源

我们看下基本的代码:

var videoMp4 = document.querySelector('.js-player-mp4');
  if (window.MediaSource) {
    var mediaSource = new MediaSource();
    videoMp4.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener('sourceopen', sourceOpen);
  } else {
  console.log("The Media Source Extensions API is not supported.")
  }

  function sourceOpen(e) {
    URL.revokeObjectURL(videoMp4.src);
    // 设置 媒体的编码类型
    var mime = 'video/webm; codecs="vorbis, vp8"';
    var mediaSource = e.target;
    var sourceBuffer = mediaSource.addSourceBuffer(mime);
    var videoUrl = './video/avegers3.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();
          videoMp4.play().then(function() {
          }).catch(function(err) {
            log('.js-log-mp4', err)
          });
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
      });
  }

这是 Google Developers 里的一个实现。大致代码细节就是:

var videoMp4 = document.querySelector('.js-player-mp4');
  if (window.MediaSource) {
    var mediaSource = new MediaSource();
    videoMp4.src = URL.createObjectURL(mediaSource);
    // mediaSource.addEventListener('sourceopen', sourceOpen);
  } else {
  console.log("The Media Source Extensions API is not supported.")
  }

我们前面提到过,我们在使用 MSE 的时候需要三个要素,其中,我们需要实例化 MediaSource 并且使用 URL.createObjectURL 会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的 URL 对象表示指定的 File 对象或 Blob 对象或者 Media Stream;这个时候 媒体元素已经了,Media Source 对象也已经有了,而且它们之间已经关联起来了。

注意,这个时候实际上我们还是不能播放的,很明显,我们没有具体的媒体资源,即我们播出视频的对象文件。但是在引入开始之前,我们还需要了解 MSE 本身实例化后的状态,我们可以通过属性 readyState 获取:

  • open MSE 实例,已经绑定到了媒体元素上,等待接受数据或者正在接受数据
  • closed MSE 实例未绑定到了媒体元素上
  • ended MSE 实例,已经绑定到了媒体元素上, 并且所有数据都已经接受到了

直接使用属性访问状态,对性能不是很好,MSE 还支持了具体的事件,

  • sourceopen 绑定到媒体元素后开始触发
  • sourceclosed 未绑定到媒体元素后开始触发
  • sourceended 所有数据接收完成后触发

创建 SourceBuffer

前面提到了三要素的前面两点,但是具体的数据需要我们接下来的代码来实现了;

function sourceOpen(e) {
  URL.revokeObjectURL(videoMp4.src);
  var mime = 'video/webm; codecs="opus, vp9"';
  // 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.
}

在 MSE 中 SourceBuffer 在 MSE 实例和媒体元素之前穿梭,SourceBuffer 必须指定你需要加载资源的媒体 MIME 类型;

我们通过 addSourceBuffer() 方法来进行添加,代码中,我们在指定 MIME 类型的时候还包含了,两种 [codec](https://en.wikipedia.org/wiki/Video_codec) ,它表示音频和视频的编码格式;MSE 版本1 的草案中没有强制要求同时含有 mine 类型和 编码格式;一些浏览器可能不用那么完整,但是 Chrome 需要,而且必须和视频格式匹配,如果不匹配你可能会遇见这样的错误:

Failed to load because no supported source was found

你可以借助一些软件来查看视频的这些信息,比如MediaInfo

请求媒体资源

前面的代码我们并没有看到具体需要播放什么内容,我们需要借助 XHR 来请求我们要播放的视频。

var videoUrl = './video/avegers3.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();
          videoMp4.play().then(function() {
          }).catch(function(err) {
            log('.js-log-mp4', err)
          });
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
      });
  }

我们可以留意到,在请求完毕后我们返回的是response.arrayBuffer() 在 MSE 中我们返回的对象数据必须是 buffer 对象才能够使用,要不然媒体是不能正常播放的。

这段代码虽然短,但是在实际的案列场景中我们可以看到这一块逻辑确实最为复杂的,它需要处理对 流文件,不同的清晰度的文件的请求判断和拉取,这一块关于请求 HLS 或者 DASH后面的文章会写到,这里不展开;

调用endOfStream()

在数据请求完成后,我们需要调用 endOfStream()。它会改变 MediaSource.readyStateended 并且触发 sourceended 事件。

 if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
      mediaSource.endOfStream();
         
}

小结

文章简单介绍了 MSE 的基本使用和一些基本的 API ,后面会连着写几篇关于 Media Source 在实际应用场景中的使用。文章代码:

Gtihub Code

演示地址

扩展阅读

Media Source Extensions 中文文档1

HTTP Live Streaming

关于音视频的一些知识(demux、filter等)