Media Source Extensions(MSE)は、音声または動画のセグメントから再生するためのストリームを構築できる JavaScript API です。この記事では説明していませんが、次のようなことをする動画をサイトに埋め込む場合は、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.readyStateとMediaSource.readyStateを確認します。これらの値は、関連するイベントが配信される前に変更される可能性があります。 SourceBufferのmode、timestampOffset、appendWindowStart、appendWindowEndを更新する前、またはSourceBufferでappendBuffer()または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.');
}
MediaSource オブジェクトを src 属性に渡せるのは少し奇妙に思えるかもしれません。通常は文字列ですが、BLOB にすることもできます。埋め込みメディアを含むページを検査してメディア要素を調べると、その意味がわかります。
MediaSource インスタンスの準備はできていますか?
URL.createObjectURL() 自体は同期ですが、アタッチメントは非同期で処理されます。これにより、MediaSource インスタンスで何かを行うまでに少し時間がかかります。幸い、これをテストする方法があります。最も簡単な方法は、readyState という MediaSource プロパティを使用する方法です。readyState プロパティは、MediaSource インスタンスとメディア要素の関係を表します。次のいずれかの値です。
closed-MediaSourceインスタンスがメディア要素にアタッチされていません。open-MediaSourceインスタンスがメディア要素に接続されており、データの受信準備ができているか、データの受信中です。ended-MediaSourceインスタンスがメディア要素に接続され、そのデータがすべてその要素に渡されています。
これらのオプションを直接クエリすると、パフォーマンスに悪影響が出る可能性があります。幸い、MediaSource は readyState が変更された場合(特に sourceopen、sourceclosed、sourceended)にもイベントを発生させます。作成する例では、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>;
}
本番環境品質のプレーヤーには、さまざまなブラウザに対応するために同じファイルの複数のバージョンが用意されています。音声と動画に別々のファイルを使用して、言語設定に基づいて音声を選択できるようにすることもできます。
実際のコードでは、さまざまなデバイスの機能やネットワークの状態に適応できるように、さまざまな解像度のメディア ファイルの複数のコピーも用意されます。このようなアプリケーションは、範囲リクエストまたはセグメントを使用して、動画をチャンクで読み込んで再生できます。これにより、メディアの再生中にネットワークの状態に適応できます。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.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);
});
}