メディア要素での収録

MediaStream 収録 API の使用の記事では、 MediaRecorder インターフェイスを使用して navigator.mediaDevices.getUserMedia() からたハードウェアデバイスによって生成された MediaStream をキャプチャする方法について説明しましたが、記録する MediaStream のソースとして HTML メディア要素(<audio> または <video>)も使用できます。 この記事では、それを実現する例を見ていきます。

メディア要素の収録の例

HTML

まずは 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 属性は、ユーザーのマイクからの音声をスピーカーに出力しないように使用していることに注意してください。 出力すると醜いフィードバックループ(ハウリング)を引き起こします。

html
<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 コードを見てみましょう。 結局のところ、これがアクションの大部分が起こるところです!

グローバル変数の設定

必要なグローバル変数をいくつか設定することから始めます。

js
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 秒)に設定されています。 これは、収録する動画の長さを指定します。

ユーティリティ関数

次に、後で使用するユーティリティ関数をいくつか作成します。

js
function log(msg) {
  logElement.innerText += `${msg}\n`;
}

log() 関数は、ユーザーと情報を共有できるように、テキスト文字列を <div> に出力するために使用します。 それほどきれいではありませんが、この仕事は私たちの目的のために行われます。

js
function wait(delayInMS) {
  return new Promise((resolve) => setTimeout(resolve, delayInMS));
}

wait() 関数は、指定したミリ秒数が経過すると解決する新しい Promise を返します。 タイムアウトハンドラー関数としてプロミスの解決ハンドラーを指定して、setTimeout() を呼び出すアロー関数を使用して動作します。 これにより、タイムアウトを使用するときにプロミス構文を使用できます。 これは、後で説明するように、プロミスを連鎖させるときに非常に便利です。

メディア収録の開始

startRecording() 関数は収録プロセスの開始を処理します。

js
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 です。 イベントハンドラーは単純に Blobdata 配列にプッシュ(末尾に追加)します。
  • recorder.start() を呼び出して収録処理を開始し、recorder の更新された状態と収録される秒数とともにメッセージをログに出力します。
  • stopped という名前の新しい Promise を作成します。 これは、MediaRecorderonstop イベントハンドラーが呼び出されると解決し、 onerror イベントハンドラーが呼び出されると拒否します。 拒否ハンドラーは、発生したエラーの名前を入力として受け取ります。
  • recorded という名前のもう一つの Promise を作成します。 これは、指定されたミリ秒数が経過すると解決します。 解決すると、MediaRecorder が収録中の場合は停止します。
  • 最後に、Promise.all を使用して Promisestoppedrecorded)の両方が解決したときに満たされる新しい Promise を作成します。 それが解決すると、配列データは startRecording() によってその呼び出し元に返されます。

入力ストリームの停止

stop() 関数は単に入力メディアを停止します。

js
function stop(stream) {
  stream.getTracks().forEach((track) => track.stop());
}

これは MediaStream.getTracks() を呼び出し、forEach() を使用してストリーム内の各トラックの MediaStreamTrack.stop() を呼び出すことによって機能します。

入力ストリームを取得してレコーダーを設定

それでは、この例で最も複雑なコードを見てみましょう。 開始ボタンをクリックしたときのイベントハンドラーです。

js
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 つの動画とダウンロードボタンの下のログ領域に出力されます。

  • すべての Promisecatch() は、log() 関数を呼び出すことによってエラーをログ領域に出力します。

停止ボタンの処理

最後のコードでは、addEventListener() を使用して停止ボタンの click イベントのハンドラーを追加します。

js
stopButton.addEventListener(
  "click",
  () => {
    stop(preview.srcObject);
  },
  false,
);

これは先ほど説明した stop() 関数を呼び出すだけです。

結果

残りの HTML と上に示されていない CSS をすべてまとめると、次のようになり、動作します。

こちらでデモ全体を見ることができ、ブラウザーの開発者ツールを使ってページを検査し、 API が使用されている方法の説明には重要ではないので上に表示されない部分も含めて、すべてのコードを見てみることができます。

関連情報