メディア要素の記録

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

HTML の内容

まずは HTML の要点を見てみましょう。 これ以上のものはありませんが、アプリのコア操作の一部ではなく、単なる情報提供です。

<div class="left">
  <div id="startButton" class="button">
    Start
  </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">
    Stop
  </div>
  <h2>Recording</h2>
  <video id="recording" width="160" height="120" controls></video>
  <a id="downloadButton" class="button">
    Download
  </a>
</div>

右欄には、Stop(停止)ボタンと録画された動画の再生に使用する <video> 要素があります。 再生パネルには autoplay を設定せずに(メディアが到着しても再生が開始されない)、controls を設定して、再生や一時停止などのユーザーコントロールを表示するように指示しています。

再生要素の下には、録画した動画をダウンロードするためのボタンがあります。

JavaScript の内容

それでは、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.innerHTML += msg + "\n";
}

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

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

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

メディア録画の開始

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(
    () => recorder.state == "recording" && recorder.stop()
  );
 
  return Promise.all([
    stopped,
    recorded
  ])
  .then(() => data);
}

startRecording() は2つの入力パラメータを取ります。 録画元の MediaStream と記録するミリ秒単位の長さです。 指定されたミリ秒数以下のメディアを常に録画しますが、その時間に達する前にメディアが停止すると、MediaRecorder も自動的に録画を停止します。

2行目
入力ストリーム(stream)の録画を処理する MediaRecorder を作成します。
3行目
空の配列 data を作成します。 これは、ondataavailable イベントハンドラに提供されたメディアデータの Blob を保持するために使用します。
5行目
dataavailable イベントのハンドラを設定します。 受信したイベントの data プロパティはメディアデータを含む Blob です。 イベントハンドラは単純に Blobdata 配列にプッシュ(末尾に追加)します。
6〜7行目
recorder.start() を呼び出して録画処理を開始し、recorder の更新された状態と録画される秒数とともにメッセージをログに出力します。
9〜12行目
stopped という名前の新しい Promise を作成します。 これは、MediaRecorderonstop イベントハンドラが呼び出されると解決し、MediaRecorderonerror イベントハンドラが呼び出されると拒否します。 拒否ハンドラは、発生したエラーの名前を入力として受け取ります。
14〜16行目
recorded という名前の新しい Promise を作成します。 これは、指定されたミリ秒数が経過すると解決します。 解決すると、MediaRecorder が録画中の場合は停止します。
18〜22行目
これらの行は、2つの Promisestoppedrecorded)の両方が解決したときに満たされる新しい Promise を作成します。 それが解決すると、配列データは startRecording() によってその呼び出し元に返されます。

入力ストリームの停止

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

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

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

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

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

startButton.addEventListener("click", function() {
  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(log);
}, false);

click イベントが発生すると、次のようになります。

2〜4行目
navigator.mediaDevices.getUserMedia() は、動画トラックと音声トラックの両方を持つ新しい MediaStream を要求するために呼び出します。 これが録画するストリームです。
5〜9行目
getUserMedia() から返された Promise が解決すると、プレビューの <video> 要素の srcObject プロパティを入力ストリームに設定し、ユーザーのカメラでキャプチャしている動画をプレビューボックスに表示します。 <video> 要素はミュートしているので、音声は再生しません。 Download(ダウンロード)ボタンのリンクも、ストリームを参照するように設定します。 次に、8行目で、MediaRecorder.captureStream() メソッドがなければ接頭辞が付いた preview.mozCaptureStream() を呼び出すように preview.captureStream() を設定して、コードが Firefox で機能するようにします。 その後、プレビュー動画の再生開始時に解決する新しい Promise を作成して返します。
10行目
プレビュー動画の再生が開始されると、録画するメディアがあることがわかります。 したがって、先ほど作成した startRecording() 関数を呼び出し、プレビュー動画ストリーム(録画するソースメディアとして)と、recordingTimeMS(録画するメディアのミリ秒数として)を渡します。 前述のように、startRecording() は、録画が完了すると、解決ハンドラが呼び出される Promise(録画されたメディアデータのチャンクを含む Blob オブジェクトの配列を入力として受け取る)を返します。
11〜15行目
録画プロセスの解決ハンドラは、ローカルに recordedChunks として知られるメディアデータの Blob の配列を入力として受け取ります。 最初にすることは、Blob() コンストラクターがオブジェクトの配列を1つのオブジェクトに連結するという事実を利用して、チャンクを MIME タイプが "video/webm" の単一の Blob にマージすることです。 次に、URL.createObjectURL() を使用して Blob を参照する URL を作成します。 これは、ダウンロードされた動画再生要素の src 属性の値(Blob から動画を再生できるようにする)とダウンロードボタンのリンクのターゲットになります。

その後、ダウンロードボタンの download 属性が設定されます。 download 属性は Boolean にすることができますが、ダウンロードするファイルの名前として使用する文字列に設定することもできます。 そのため、ダウンロードリンクの download 属性を "RecordedVideo.webm" に設定することで、ボタンをクリックすると内容が録画された動画である "RecordedVideo.webm" という名前のファイルをダウンロードするようにブラウザーに指示します。

17〜18行目
記録されたメディアのサイズと種類は、2つの動画とダウンロードボタンの下のログ領域に出力されます。
20行目
すべての Promisecatch() は、log() 関数を呼び出すことによってエラーをロギング領域に出力します。

停止ボタンの取り扱い

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

stopButton.addEventListener("click", function() {
  stop(preview.srcObject);
}, false);

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

結果

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

API がどのように使用されているかの説明には重要ではないため上で隠されている部分も含めて、すべてのコードを見ることができます。

関連情報

ドキュメントのタグと貢献者

このページの貢献者: Wind1808
最終更新者: Wind1808,