Media Source Extensions(MSE)は、音声または動画のセグメントから再生するストリームを構築できる JavaScript API です。この記事では取り上げませんが、次のような動画をサイトに埋め込む場合は、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.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>;
}
製品版品質プレーヤーでは、異なるブラウザに対応するために、複数のバージョンで同じファイルを使用します。音声と動画用に別々のファイルを使用し、言語設定に基づいて音声を選択できるようにします。
また、実際のコードには、さまざまなデバイスの機能やネットワーク状態に対応できるように、解像度の異なるメディア ファイルのコピーが複数含まれます。このようなアプリケーションでは、範囲リクエストまたはセグメントを使用して、動画をチャンクで読み込んで再生できます。これにより、メディアの再生中もネットワーク状態に適応できます。これを行う 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.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);
});
}
最終バージョン
コードサンプルの全文を以下に示します。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);
});
}