メディア要素での収録
MediaStream 収録 API の使用の記事では、 MediaRecorder インターフェイスを使用して navigator.mediaDevices.getUserMedia() からたハードウェアデバイスによって生成された MediaStream をキャプチャする方法について説明しましたが、記録する MediaStream のソースとして HTML メディア要素(<audio> または <video>)も使用できます。 この記事では、それを実現する例を見ていきます。
メディア要素の収録の例
>HTML
まずは HTML の要点を見てみましょう。 これ以上のものはありませんが、アプリのコア操作の一部ではなく、単なる情報提供です。
<div class="left">
<div id="startButton" class="button">録画を開始</div>
<h2>Preview</h2>
<video id="preview" width="160" height="120" autoplay muted></video>
</div>
2 つの欄で主要なインターフェイスを提示します。 左欄には、Start(開始)ボタンと動画プレビューを表示する <video> 要素があります。 これは、ユーザーのカメラが見ている動画です。 autoplay 属性は、カメラからストリームが到着したらすぐに表示するために使用し、muted 属性は、ユーザーのマイクからの音声をスピーカーに出力しないように使用していることに注意してください。 出力すると醜いフィードバックループ(ハウリング)を引き起こします。
<div class="right">
<div id="stopButton" class="button">録画を停止</div>
<h2>Recording</h2>
<video id="recording" width="160" height="120" controls></video>
<a id="downloadButton" class="button">ダウンロード</a>
</div>
右欄には、Stop(停止)ボタンと収録された動画の再生に使用する <video> 要素があります。 再生パネルには autoplay を設定せずに(メディアが到着しても再生が開始されない)、controls を設定して、再生や一時停止などのユーザーコントロールを表示するように指示しています。
再生要素の下には、収録した動画をダウンロードするためのボタンがあります。
それでは、JavaScript コードを見てみましょう。 結局のところ、これがアクションの大部分が起こるところです!
グローバル変数の設定
必要なグローバル変数をいくつか設定することから始めます。
let preview = document.getElementById("preview");
let recording = document.getElementById("recording");
let startButton = document.getElementById("startButton");
let stopButton = document.getElementById("stopButton");
let downloadButton = document.getElementById("downloadButton");
let logElement = document.getElementById("log");
let recordingTimeMS = 5000;
これらのほとんどは、私たちが取り組む必要がある要素への参照です。 最後の recordingTimeMS は 5000 ミリ秒(5 秒)に設定されています。 これは、収録する動画の長さを指定します。
ユーティリティ関数
次に、後で使用するユーティリティ関数をいくつか作成します。
function log(msg) {
logElement.innerText += `${msg}\n`;
}
log() 関数は、ユーザーと情報を共有できるように、テキスト文字列を <div> に出力するために使用します。 それほどきれいではありませんが、この仕事は私たちの目的のために行われます。
function wait(delayInMS) {
return new Promise((resolve) => setTimeout(resolve, delayInMS));
}
wait() 関数は、指定したミリ秒数が経過すると解決する新しい Promise を返します。 タイムアウトハンドラー関数としてプロミスの解決ハンドラーを指定して、setTimeout() を呼び出すアロー関数を使用して動作します。 これにより、タイムアウトを使用するときにプロミス構文を使用できます。 これは、後で説明するように、プロミスを連鎖させるときに非常に便利です。
メディア収録の開始
startRecording() 関数は収録プロセスの開始を処理します。
function startRecording(stream, lengthInMS) {
let recorder = new MediaRecorder(stream);
let data = [];
recorder.ondataavailable = (event) => data.push(event.data);
recorder.start();
log(`${recorder.state} for ${lengthInMS / 1000} seconds…`);
let stopped = new Promise((resolve, reject) => {
recorder.onstop = resolve;
recorder.onerror = (event) => reject(event.name);
});
let recorded = wait(lengthInMS).then(() => {
if (recorder.state === "recording") {
recorder.stop();
}
});
return Promise.all([stopped, recorded]).then(() => data);
}
startRecording() は 2 つの入力引数を取ります。 収録元の MediaStream と記録するミリ秒単位の長さです。 指定されたミリ秒数以下のメディアを常に収録しますが、その時間に達する前にメディアが停止すると、MediaRecorder も自動的に収録を停止します。
- まず、入力ストリーム (
stream) の収録を処理するMediaRecorderを作成します。 - 初期状態で空の配列
dataを作成します。 これは、ondataavailableイベントハンドラーに提供されたメディアデータのBlobを保持するために使用します。 ondataavailableに代入することで、dataavailableイベントのハンドラーを設定します。 受信したイベントのdataプロパティはメディアデータを含むBlobです。 イベントハンドラーは単純にBlobをdata配列にプッシュ(末尾に追加)します。recorder.start()を呼び出して収録処理を開始し、recorderの更新された状態と収録される秒数とともにメッセージをログに出力します。stoppedという名前の新しいPromiseを作成します。 これは、MediaRecorderのonstopイベントハンドラーが呼び出されると解決し、onerrorイベントハンドラーが呼び出されると拒否します。 拒否ハンドラーは、発生したエラーの名前を入力として受け取ります。recordedという名前のもう一つのPromiseを作成します。 これは、指定されたミリ秒数が経過すると解決します。 解決すると、MediaRecorderが収録中の場合は停止します。- 最後に、
Promise.allを使用してPromise(stoppedとrecorded)の両方が解決したときに満たされる新しいPromiseを作成します。 それが解決すると、配列データはstartRecording()によってその呼び出し元に返されます。
入力ストリームの停止
stop() 関数は単に入力メディアを停止します。
function stop(stream) {
stream.getTracks().forEach((track) => track.stop());
}
これは MediaStream.getTracks() を呼び出し、forEach() を使用してストリーム内の各トラックの MediaStreamTrack.stop() を呼び出すことによって機能します。
入力ストリームを取得してレコーダーを設定
それでは、この例で最も複雑なコードを見てみましょう。 開始ボタンをクリックしたときのイベントハンドラーです。
startButton.addEventListener(
"click",
() => {
navigator.mediaDevices
.getUserMedia({
video: true,
audio: true,
})
.then((stream) => {
preview.srcObject = stream;
downloadButton.href = stream;
preview.captureStream =
preview.captureStream || preview.mozCaptureStream;
return new Promise((resolve) => (preview.onplaying = resolve));
})
.then(() => startRecording(preview.captureStream(), recordingTimeMS))
.then((recordedChunks) => {
let recordedBlob = new Blob(recordedChunks, { type: "video/webm" });
recording.src = URL.createObjectURL(recordedBlob);
downloadButton.href = recording.src;
downloadButton.download = "RecordedVideo.webm";
log(
`Successfully recorded ${recordedBlob.size} bytes of ${recordedBlob.type} media.`,
);
})
.catch((error) => {
if (error.name === "NotFoundError") {
log("Camera or microphone not found. Can't record.");
} else {
log(error);
}
});
},
false,
);
click イベントが発生すると、次のようになります。
-
MediaDevices.getUserMediaは、動画トラックと音声トラックの両方を持つ新しいMediaStreamを要求するために呼び出します。 これが収録するストリームです。 -
getUserMedia()から返されたプロミスが解決すると、プレビューの<video>要素のsrcObjectプロパティを入力ストリームに設定し、ユーザーのカメラでキャプチャしている動画をプレビューボックスに表示します。<video>要素はミュートしているので、音声は再生しません。 "Download"(ダウンロード)ボタンのリンクも、ストリームを参照するように設定します。 次に、preview.captureStream()でpreview.mozCaptureStream()を呼び出すように手配して、コードが Firefox で動作するようにします。HTMLMediaElement.captureStream()メソッドが接頭辞付きだからです。その後、プレビュー動画の再生開始時に解決する新しいPromiseを作成して返します。 -
プレビュー動画の再生が開始されると、収録するメディアがあることがわかります。 したがって、先ほど作成した
startRecording()関数を呼び出し、プレビュー動画ストリーム(収録するソースメディアとして)と、recordingTimeMS(収録するメディアのミリ秒数として)を渡します。 前述のように、startRecording()は、収録が完了すると、解決ハンドラーが呼び出されるPromise(収録されたメディアデータのチャンクを含むBlobオブジェクトの配列を入力として受け取る)を返します。 -
収録プロセスの解決ハンドラーは、ローカルに
recordedChunksとして知られるメディアデータのBlobの配列を入力として受け取ります。 最初にすることは、Blobコンストラクターがオブジェクトの配列を 1 つのオブジェクトに連結するという事実を利用して、チャンクを MIME タイプが"video/webm"の単一のBlobにマージすることです。 次に、URL.createObjectURL()を使用してBlobを参照する URL を作成します。 これは、ダウンロードされた動画再生要素のsrc属性の値(Blobから動画を再生できるようにする)とダウンロードボタンのリンクのターゲットになります。その後、ダウンロードボタンの
download属性が設定されます。download属性は論理値にすることができますが、ダウンロードするファイルの名前として使用する文字列に設定することもできます。 そのため、ダウンロードリンクのdownload属性を"RecordedVideo.webm"に設定することで、ボタンをクリックすると内容が収録された動画である"RecordedVideo.webm"という名前のファイルをダウンロードするようにブラウザーに指示します。 -
記録されたメディアのサイズと種類は、2 つの動画とダウンロードボタンの下のログ領域に出力されます。
-
すべての
Promiseのcatch()は、log()関数を呼び出すことによってエラーをログ領域に出力します。
停止ボタンの処理
最後のコードでは、addEventListener() を使用して停止ボタンの click イベントのハンドラーを追加します。
stopButton.addEventListener(
"click",
() => {
stop(preview.srcObject);
},
false,
);
これは先ほど説明した stop() 関数を呼び出すだけです。
結果
残りの HTML と上に示されていない CSS をすべてまとめると、次のようになり、動作します。
こちらでデモ全体を見ることができ、ブラウザーの開発者ツールを使ってページを検査し、 API が使用されている方法の説明には重要ではないので上に表示されない部分も含めて、すべてのコードを見てみることができます。