メディアソース拡張機能

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

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

  • アダプティブ ストリーミング。これは、デバイスの機能やネットワークの状態への適応を意味します。
  • アダプティブ スプライシング(広告の挿入など)
  • タイムシフト
  • パフォーマンスとダウンロード サイズの制御
基本的な MSE データフロー
図 1: 基本的な 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.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 タイプは不要でも、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>;
}

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

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

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

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

レスポンス オブジェクトから MediaSource インスタンスにデータを渡す一般的な方法は、レスポンス オブジェクトから ArrayBuffer を取得して SourceBuffer に渡すことです。まず response.arrayBuffer() を呼び出して、バッファに 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);
    });
}

最終バージョン

コード例の全体は次のとおりです。メディアソース拡張機能について理解を深めていただければ幸いです。

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

フィードバック