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

はじめに

音声/動画キャプチャは、長い間、ウェブ開発における「究極の目標」のひとつです。長年にわたって、私たちはブラウザ プラグイン(FlashSilverlight)に頼る必要がありました。さあ!

そこで役立つのが HTML5 です。HTML5 の台頭により、デバイス ハードウェアへのアクセスが急増しました。Geolocation(GPS)、Orientation API(加速度計)、WebGL(GPU)、Web Audio API(オーディオ ハードウェア)が最適な例です。これらの機能は非常に強力で、システムの基礎となるハードウェア機能上にある高レベルの JavaScript API を公開します。

このチュートリアルでは、ウェブアプリからユーザーのカメラとマイクにアクセスできるようにする新しい API の GetUserMedia を紹介します。

getUserMedia() への道

その歴史をご存じない方もいるかもしれませんが、getUserMedia() API の到着までの道のりは興味深い話です。

ここ数年で、「Media Capture API」のいくつかの亜種が進化しました。多くの人々は、ウェブ上のネイティブ デバイスにアクセスできることの必要性を認識していましたが、それを受けて、全員とその母親が新しい仕様を編み出しました。事態が複雑になり、W3C は最終的にワーキング グループを立ち上げることにしました。その唯一の目的は、わかってるよ!Device API Policy(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 メディア キャプチャでは、メディア ファイルの記録や時間内のスナップショットの取得のみが可能です。

サポート:

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

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

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

Opera は、<device> 要素に基づく動画キャプチャの初期実装を作成した最初のブラウザの 1 つです。その直後(正確には同じ日)に、WhatWG は <device> タグを廃止して、次の新しいタグを使用することにしました。今回は navigator.getUserMedia() という JavaScript API です。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> は組み込まれていませんでした。 心配する API が 1 つ減ったと思います。<device> には 2 つの優れた機能がありました。1)セマンティックであること、2)オーディオ/ビデオ デバイス以上のものをサポートするように簡単に拡張可能でした。

深呼吸します。動きが速い!

ラウンド 3: WebRTC

<device> 要素は最終的に Dodo の登場です。

WebRTC(ウェブ リアルタイム コミュニケーション)への取り組みが拡大したことで、適切なキャプチャ API を見つけるペースが加速しました。この仕様は W3C WebRTC ワーキング グループが監督しています。Google、Opera、Mozilla など数社で実装が可能です。

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

サポート:

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> と連携して機能します。<video> 要素に src 属性を設定したり、<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);

その他の構成については、制約 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;
}