HTML5 で音声と動画をキャプチャ

はじめに

音声 / 動画のキャプチャは、長い間ウェブ開発の「聖杯」でした。長年にわたり、ブラウザ プラグイン(Flash または Silverlight)に頼らざるを得ませんでした。さあ!

HTML5 に対応しました。一見するとわかりにくいかもしれませんが、HTML5 の普及に伴い、デバイスのハードウェアへのアクセスが急増しています。位置情報(GPS)、Orientation API(加速度計)、WebGL(GPU)、Web Audio API(オーディオ ハードウェア)などが最適な例です。これらの機能は非常に強力で、システムの基盤となるハードウェア機能の上に構築された高レベルの JavaScript API を公開します。

このチュートリアルでは、ウェブアプリがユーザーのカメラとマイクにアクセスできるようにする新しい API である GetUserMedia について説明します。

getUserMedia() への道

getUserMedia() API の誕生には興味深い経緯があります。

この数年間で、「Media Capture API」の複数のバリエーションが進化しました。ウェブ上のネイティブ デバイスにアクセスできる必要性は、多くの人が認識していましたが、そのために全員と母親が新しい仕様を作成する必要に迫られました。事態が複雑化したため、W3C はついにワーキング グループを結成することに決めました。唯一の目的は混乱を解消しましょう。デバイス API ポリシー(DAP)ワーキング グループは、多数の提案を統合して標準化することを任されています。

2011 年に発生した事象を簡単にまとめると、

ラウンド 1: HTML メディア キャプチャ

HTML Media Capture は、DAP がウェブでのメディア キャプチャの標準化に初めて取り組んだものです。これは、<input type="file"> をオーバーロードし、accept パラメータに新しい値を追加することで機能します。

ユーザーがウェブカメラで自分のスナップショットを撮れるようにするには、capture=camera を使用します。

<input type="file" accept="image/*;capture=camera">

動画や音声の録画は、同様の手順で行えます。

<input type="file" accept="video/*;capture=camcorder">
<input type="file" accept="audio/*;capture=microphone">

便利ですよね。ファイル入力を再利用できる点が特に気に入っています。意味的には、これは非常に理にかなっています。この「API」の欠点は、リアルタイム エフェクト(ライブ ウェブカメラ データを <canvas> にレンダリングして WebGL フィルタを適用するなど)を実行できないことです。HTML Media Capture では、メディア ファイルを録画するか、スナップショットを取得するのみです。

サポート:

  • Android 3.0 ブラウザ - 最初の実装の 1 つ。実際の動作を確認するには、こちらの動画をご覧ください。
  • Chrome for Android(0.16)
  • Firefox Mobile 10.0
  • iOS6 Safari と Chrome(一部サポート)

ラウンド 2: デバイス要素

多くの人は、HTML Media Capture は制限が多すぎると考えていたため、あらゆる種類の(将来の)デバイスをサポートする新しい仕様が登場しました。当然のことながら、この設計では新しい要素(getUserMedia() の前身となる <device> 要素)が必要でした。

Opera は、<device> 要素に基づく動画キャプチャの初期実装を作成した最初のブラウザの 1 つです。その後まもなく(正確には同じ日)、WhatWG は <device> タグを廃止し、別の新興の JavaScript API である navigator.getUserMedia() に置き換えることを決定しました。1 週間後、Opera は更新された getUserMedia() 仕様のサポートを含む新しいビルドをリリースしました。その年の後半、Microsoft も新しい仕様をサポートする Lab for IE9 をリリースしてこの動きに加わりました。

<device> は次のように表示されます。

<device type="media" onchange="update(this.data)"></device>
<video autoplay></video>
<script>
  function update(stream) {
    document.querySelector('video').src = stream.url;
  }
</script>

サポート:

残念ながら、リリースされたブラウザには <device> が含まれていませんでした。<device> には 2 つの優れた点がありました。1 つはセマンティックであること、もう 1 つは音声 / 動画デバイス以外のデバイスをサポートするように簡単に拡張できることです。

深呼吸します。状況は急速に変化しています。

ラウンド 3: WebRTC

<device> 要素はやがて Dodo の形になっていました。

WebRTC(ウェブ リアルタイム通信)の取り組みが拡大したことで、適切なキャプチャ API を見つけるペースが加速しました。この仕様は W3C WebRTC ワーキング グループによって管理されています。Google、Opera、Mozilla、その他のいくつかのブラウザで実装されています。

getUserMedia() は、その一連の API へのゲートウェイであるため、WebRTC に関連しています。ユーザーのローカルのカメラ/マイク ストリームにアクセスする手段となります。

サポート:

getUserMedia() は、Chrome 21、Opera 18、Firefox 17 以降でサポートされています。

スタートガイド

navigator.mediaDevices.getUserMedia() を使用すると、プラグインなしでウェブカメラとマイクの入力を利用できるようになります。カメラへのアクセスはインストールだけで済むようになりました。ブラウザに直接組み込まれています。ご興味はおありでしょうか。

特徴検出

機能の検出は、navigator.mediaDevices.getUserMedia が存在するかどうかを簡単に確認するものです。

if (navigator.mediaDevices?.getUserMedia) {
  // Good to go!
} else {
  alert("navigator.mediaDevices.getUserMedia() is not supported");
}

入力デバイスへのアクセス権の取得

ウェブカメラまたはマイクを利用するには、権限をリクエストする必要があります。navigator.mediaDevices.getUserMedia() の最初のパラメータは、アクセスするメディアのタイプごとに詳細と要件を指定するオブジェクトです。たとえば、ウェブカメラにアクセスする場合、最初のパラメータは {video: true} にする必要があります。マイクとカメラの両方を使用するには、{video: true, audio: true} を渡します。

<video autoplay></video>

<script>
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: true })
    .then((localMediaStream) => {
      const video = document.querySelector("video");
      video.srcObject = localMediaStream;
    })
    .catch((error) => {
      console.log("Rejected!", error);
    });
</script>

注意が必要です。何が起きているのでしょうか。メディア キャプチャは、新しい HTML5 API が連携する完璧な例です。他の HTML5 の仲間である <audio><video> と連携して動作します。src 属性を設定したり、<video> 要素に <source> 要素を含めたりしていないことに注意してください。動画にメディア ファイルの URL を渡す代わりに、srcObject をウェブカメラを表す LocalMediaStream オブジェクトに設定しています。

また、<video>autoplay を指定しています。指定しないと、最初のフレームでフリーズします。controls を追加しても、想定どおりに動作します。

メディアの制約(解像度、高さ、幅)を設定する

getUserMedia() の最初のパラメータを使用して、返されるメディア ストリームに関するその他の要件(または制約)を指定することもできます。たとえば、動画への基本的なアクセス({video: true} など)を希望するだけでなく、ストリーミングが HD であることを追加で指定することもできます。

const hdConstraints = {
  video: { width: { exact:  1280} , height: { exact: 720 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);
const vgaConstraints = {
  video: { width: { exact:  640} , height: { exact: 360 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);

その他の構成については、constraints API をご覧ください。

メディアソースの選択

MediaDevices インターフェースの enumerateDevices() メソッドは、マイク、カメラ、ヘッドセットなど、使用可能なメディア入力デバイスと出力デバイスのリストをリクエストします。返された Promise は、デバイスを記述する MediaDeviceInfo オブジェクトの配列で解決されます。

この例では、最後に検出されたマイクとカメラがメディア ストリーム ソースとして選択されています。

if (!navigator.mediaDevices?.enumerateDevices) {
  console.log("enumerateDevices() not supported.");
} else {
  // List cameras and microphones.
  navigator.mediaDevices
    .enumerateDevices()
    .then((devices) => {
      let audioSource = null;
      let videoSource = null;

      devices.forEach((device) => {
        if (device.kind === "audioinput") {
          audioSource = device.deviceId;
        } else if (device.kind === "videoinput") {
          videoSource = device.deviceId;
        }
      });
      sourceSelected(audioSource, videoSource);
    })
    .catch((err) => {
      console.error(`${err.name}: ${err.message}`);
    });
}

async function sourceSelected(audioSource, videoSource) {
  const constraints = {
    audio: { deviceId: audioSource },
    video: { deviceId: videoSource },
  };
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
}

ユーザーがメディアソースを選択できるようにする方法については、Sam Dutton の素晴らしいデモをご覧ください。

セキュリティ

ブラウザでは、navigator.mediaDevices.getUserMedia() を呼び出すと権限ダイアログが表示され、ユーザーはカメラ / マイクへのアクセスを許可または拒否できます。たとえば、Chrome の権限ダイアログは次のとおりです。

Chrome の権限ダイアログ
Chrome の権限ダイアログ

フォールバックを提供

navigator.mediaDevices.getUserMedia() をサポートしていない場合、API がサポートされていない場合や、なんらかの理由で呼び出しが失敗した場合に、既存の動画ファイルにフォールバックするという選択肢もあります。

if (!navigator.mediaDevices?.getUserMedia) {
  video.src = "fallbackvideo.webm";
} else {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  video.srcObject = stream;
}