メディア要素での収録
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 が使用されている方法の説明には重要ではないので上に表示されない部分も含めて、すべてのコードを見てみることができます。