ユーザーからの動画の録画

Mat Scales

現在、多くのブラウザには、ユーザーからの動画入力と音声入力にアクセスする機能が備わっています。ただし、ブラウザによっては、完全な動的インライン エクスペリエンスになる場合もあれば、ユーザーのデバイス上の別のアプリに委任される場合もあります。

シンプルに始めて、段階的に

最も簡単な方法は、お客様に事前に録画したファイルを提供するよう依頼することです。これを行うには、シンプルなファイル入力要素を作成し、動画ファイルのみを受け入れることを示す accept フィルタと、カメラから直接取得することを示す capture 属性を追加します。

<input type="file" accept="video/*" capture />

この方法はすべてのプラットフォームで機能します。パソコンでは、ファイル システムからファイルをアップロードするようユーザーに求めます(capture 属性は無視されます)。iOS の Safari では、カメラアプリが開き、動画を録画してウェブページに送信できます。Android では、動画を録画してウェブページに送信する前に、どのアプリを使用するかを選択できます。

多くのモバイル デバイスには複数のカメラが搭載されています。ユーザーの方向を向いたカメラを使用する場合は capture 属性を user に設定し、外側を向いたカメラを使用する場合は environment に設定します。

<input type="file" accept="video/*" capture="user" />
<input type="file" accept="video/*" capture="environment" />

これはあくまでヒントであり、ブラウザがこのオプションをサポートしていない場合や、リクエストされたカメラタイプが利用できない場合は、ブラウザが別のカメラを選択することがあります。

ユーザーが録画を終了してウェブサイトに戻ったら、何らかの方法でファイルデータを取得する必要があります。onchange イベントを入力要素にアタッチし、イベント オブジェクトの files プロパティを読み取ることで、簡単にアクセスできます。

<input type="file" accept="video/*" capture="camera" id="recorder" />
<video id="player" controls></video>
<script>
  var recorder = document.getElementById('recorder');
  var player = document.getElementById('player');

  recorder.addEventListener('change', function (e) {
    var file = e.target.files[0];
    // Do something with the video file.
    player.src = URL.createObjectURL(file);
  });
</script>

ファイルにアクセスできるようになると、そのファイルに対してあらゆる操作を行うことができます。たとえば、下記の設定が可能です。

  • <video> 要素に直接アタッチして再生できるようにする
  • ユーザーのデバイスにダウンロードする
  • XMLHttpRequest にアタッチしてサーバーにアップロードする
  • フレームをキャンバスに描画してフィルタを適用する

動画データにアクセスする入力要素メソッドは広く使用されていますが、最も魅力のないオプションです。カメラにアクセスして、ページ内で直接優れたエクスペリエンスを提供したいと考えています。

カメラにインタラクティブにアクセスする

最新のブラウザはカメラに直接接続できるため、ウェブページと完全に統合されたエクスペリエンスを構築でき、ユーザーがブラウザを離れることはありません。

カメラへのアクセス権を取得する

getUserMedia() という WebRTC 仕様の API を使用して、カメラに直接アクセスできます。getUserMedia() は、接続されているマイクとカメラへのアクセスをユーザーに求めます。

成功すると、API はカメラまたはマイクのデータを含む Stream を返します。その後、<video> 要素にアタッチするか、WebRTC ストリームにアタッチするか、MediaRecorder API を使用して保存できます。

カメラからデータを取得するには、getUserMedia() API に渡される制約オブジェクトで video: true を設定します。

<video id="player" controls></video>
<script>
  var player = document.getElementById('player');

  var handleSuccess = function (stream) {
    player.srcObject = stream;
  };

  navigator.mediaDevices
    .getUserMedia({audio: true, video: true})
    .then(handleSuccess);
</script>

特定のカメラを選択する場合は、まず利用可能なカメラを列挙します。

navigator.mediaDevices.enumerateDevices().then((devices) => {
  devices = devices.filter((d) => d.kind === 'videoinput');
});

その後、getUserMedia を呼び出すときに、使用する deviceId を渡すことができます。

navigator.mediaDevices.getUserMedia({
  audio: true,
  video: {
    deviceId: devices[0].deviceId,
  },
});

これだけではあまり役に立ちません。できるのは、動画データを取得して再生することだけです。

カメラから元データにアクセスする

カメラから未加工の動画データにアクセスするには、各フレームを <canvas> に描画し、ピクセルを直接操作します。

2D キャンバスの場合、コンテキストの drawImage メソッドを使用して、<video> 要素の現在のフレームをキャンバスに描画できます。

context.drawImage(myVideoElement, 0, 0);

WebGL キャンバスでは、<video> 要素をテクスチャのソースとして使用できます。

gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  myVideoElement,
);

どちらの場合も、再生中の動画の現在のフレームが使用されます。複数のフレームを処理するには、毎回動画をキャンバスに再描画する必要があります。

詳しくは、画像や動画にリアルタイム エフェクトを適用するに関する記事をご覧ください。

カメラのデータを保存する

カメラからデータを保存する最も簡単な方法は、MediaRecorder API を使用することです。

MediaRecorder API は、getUserMedia によって作成されたストリームを受け取り、ストリームから目的の宛先にデータを段階的に保存します。

<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
  let shouldStop = false;
  let stopped = false;
  const downloadLink = document.getElementById('download');
  const stopButton = document.getElementById('stop');

  stopButton.addEventListener('click', function() {
    shouldStop = true;
  })

  var handleSuccess = function(stream) {
    const options = {mimeType: 'video/webm'};
    const recordedChunks = [];
    const mediaRecorder = new MediaRecorder(stream, options);

    mediaRecorder.addEventListener('dataavailable', function(e) {
      if (e.data.size > 0) {
        recordedChunks.push(e.data);
      }

      if(shouldStop === true && stopped === false) {
        mediaRecorder.stop();
        stopped = true;
      }
    });

    mediaRecorder.addEventListener('stop', function() {
      downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
      downloadLink.download = 'acetest.webm';
    });

    mediaRecorder.start();
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: true })
      .then(handleSuccess);
</script>

この例では、データを配列に直接保存しています。この配列は、後で Blob に変換して、ウェブサーバーに保存したり、ユーザーのデバイスのストレージに直接保存したりできます。

カメラの使用権限を責任を持ってリクエストする

ユーザーが以前にサイトにカメラへのアクセスを許可していない場合、getUserMedia を呼び出した時点で、ブラウザはユーザーにサイトへのカメラへのアクセス許可を求めるプロンプトを表示します。

ユーザーは、マシン上の強力なデバイスへのアクセスを求めるプロンプトが表示されることを嫌います。リクエストをブロックしたり、プロンプトが作成されたコンテキストを理解していない場合は無視したりすることがよくあります。ベスト プラクティスは、最初に必要になったときにのみカメラへのアクセスをリクエストすることです。ユーザーがアクセスを許可すると、再度アクセスを求められることはありません。ただし、ユーザーがアクセスを拒否した場合、再度アクセスを求めてユーザーに許可を求めることはできません。

Permissions API を使用して、すでにアクセス権があるかどうかを確認する

getUserMedia API では、カメラへのアクセス権がすでにあるかどうかはわかりません。ここで問題が生じます。カメラへのアクセスを許可してもらうための優れた UI を提供するには、カメラへのアクセスをリクエストする必要があります。

この問題は、一部のブラウザでは Permission API を使用することで解決できます。navigator.permission API を使用すると、再度プロンプトを表示することなく、特定の API にアクセスする機能の状態をクエリできます。

ユーザーのカメラにアクセスできるかどうかをクエリするには、{name: 'camera'} を query メソッドに渡します。次のいずれかが返されます。

  • granted - ユーザーが以前にカメラへのアクセスを許可している。
  • prompt - ユーザーがアクセス権を付与していないため、getUserMedia を呼び出すとプロンプトが表示されます。
  • denied - システムまたはユーザーがカメラへのアクセスを明示的にブロックしているため、カメラにアクセスできません。

また、ユーザーが必要な操作を行えるようにユーザー インターフェースを変更する必要があるかどうかを、すぐに確認できるようになりました。

navigator.permissions.query({name: 'camera'}).then(function (result) {
  if (result.state == 'granted') {
  } else if (result.state == 'prompt') {
  } else if (result.state == 'denied') {
  }
  result.onchange = function () {};
});

フィードバック