メディアソース拡張機能

フランソワ ボーフォール
François Beaufort
ジョー・メドレー
Joe Medley

Media Source Extensions(MSE)は、音声または動画のセグメントから再生するストリームを構築できる JavaScript API です。この記事では取り上げませんが、次のような動画をサイトに埋め込む場合は、MSE を理解しておく必要があります。

  • アダプティブ ストリーミング。デバイスの機能やネットワーク状態に適応する
  • アダプティブ スプライシング(広告挿入など)
  • タイムシフト
  • パフォーマンスとダウンロード サイズの管理
基本的な MSE データフロー
図 1: 基本的な MSE のデータフロー

MSE は連鎖のようなものです。図に示すように、ダウンロードしたファイルとメディア要素の間には複数のレイヤがあります。

  • メディアを再生するための <audio> 要素または <video> 要素。
  • メディア要素にフィードする SourceBuffer を含む MediaSource インスタンス。
  • fetch() または XHR 呼び出し。Response オブジェクト内のメディアデータを取得します。
  • MediaSource.SourceBuffer にフィードする Response.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);
    });
}

ここまでの説明から読み終えられた場合は、読み飛ばしてもかまいません。詳しい説明をお求めの場合は、このまま読み進めてください。 このチェーンについて、基本的な MSE の例を作成してみましょう。各ビルドステップで、前のステップにコードが追加されます。

明確さに関する注意事項

ウェブページでメディアを再生する場合に知っておくべきことがすべて説明されていますか?いいえ。本ガイドは、他の場所に存在するような、より複雑なコードを理解するためだけのものです。わかりやすくするため、このドキュメントでは多くの項目を簡素化および省略しています。Google の Shaka Player などのライブラリを使用することをおすすめします。あえて単純化している部分については指摘します

対象外となる事項

以下の項目は順不同です。ここでは取り上げません。

  • 再生コントロール。これらは、HTML5 の <audio> 要素と <video> 要素を使用することで、無料で入手できます。
  • エラー処理。

本番環境での使用

MSE 関連 API を本番環境で使用する場合は、次のことをおすすめします。

  • これらの API を呼び出す前に、エラーイベントまたは API 例外を処理し、HTMLMediaElement.readyStateMediaSource.readyState を確認してください。これらの値は、関連するイベントが配信される前に変更される可能性があります。
  • SourceBuffermodetimestampOffsetappendWindowStartappendWindowEnd を更新するか、SourceBufferappendBuffer() または remove() を呼び出す前に、SourceBuffer.updating のブール値を確認して、以前の 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 インスタンスをメディア要素にアタッチする

最近のウェブ開発の多くと同様に、まずは特徴検出から始めます。次に、メディア要素(<audio> 要素または <video> 要素)を取得します。最後に、MediaSource のインスタンスを作成します。URL に変換され、メディア要素のソース属性に渡されます。

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 インスタンスに対してなんらかの操作が実行できるようになるまでにわずかな遅延が発生します。幸い、これをテストする方法があります。最も簡単な方法は、readyState という MediaSource プロパティを使用する方法です。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 タイプと 2 つのコーデックが含まれています。これは動画ファイルの 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() ポリフィルがないと機能しません。

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>;
}

製品版品質プレーヤーでは、異なるブラウザに対応するために、複数のバージョンで同じファイルを使用します。音声と動画用に別々のファイルを使用し、言語設定に基づいて音声を選択できるようにします。

また、実際のコードには、さまざまなデバイスの機能やネットワーク状態に対応できるように、解像度の異なるメディア ファイルのコピーが複数含まれます。このようなアプリケーションでは、範囲リクエストまたはセグメントを使用して、動画をチャンクで読み込んで再生できます。これにより、メディアの再生中もネットワーク状態に適応できます。これを行う 2 つの方法である DASH と HLS という用語を耳にしたことがあるかもしれません。このトピックの詳細な説明は、この概要の範囲外です。

レスポンス オブジェクトを処理する

コードはほぼ完成しましたが、メディアは再生されません。Response オブジェクトから SourceBuffer にメディアデータを取得する必要があります。

レスポンス オブジェクトから MediaSource インスタンスにデータを渡す一般的な方法は、レスポンス オブジェクトから ArrayBuffer を取得して SourceBuffer に渡すことです。まず response.arrayBuffer() を呼び出します。これにより、バッファに Promise が返されます。私のコードでは、この Promise を 2 番目の 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.readyStateended に変更され、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);
    });
}

最終バージョン

コードサンプルの全文を以下に示します。Media Source Extensions について ご理解いただけたでしょうか。

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);
    });
}

フィードバック