ユーザーの動画を撮影する

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 にアタッチして、サーバーにアップロードする
  • フレームをキャンバスに描画してフィルタを適用する

入力要素を使用して動画データにアクセスする方法は汎用的ですが、好ましい方法ではありません。本当の意味で重要なのは、カメラにアクセスして、ページ内で優れたエクスペリエンスを直接提供することです。

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

最新のブラウザはカメラに直接アクセスできるため、ウェブページと完全に統合されたエクスペリエンスを実現できます。よって、ユーザーはブラウザから離れる必要がありません。

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

WebRTC 仕様の API(getUserMedia())を使用して、カメラに直接アクセスできます。getUserMedia() を使用すると、接続済みのマイクまたはカメラに対するアクセス権の付与を求めるメッセージがユーザーに表示されます。

アクセスが許可されると、API によって、カメラまたはマイクからのデータが含まれる Stream が返されます。このストリームは <video> 要素にアタッチしたり、WebRTC ストリームにアタッチしたり、MediaRecorder API を使用して保存したりできます。

カメラからデータを取得するために、getUserMedia() API に渡す constraints オブジェクトで 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 を呼び出すと、カメラへのアクセス権をサイトに付与するよう求める画面が表示されます。

ユーザーは、マシン上の高機能なデバイスへのアクセス権を要求されることを好まず、リクエストを拒否する傾向があります。また、プロンプトが表示された理由がわからない場合は、リクエストを無視することもあります。初めてカメラが必要になったときに、一度だけアクセス権を要求することをおすすめします。アクセス権が付与されると、ユーザーに再度プロンプトが表示されることはありません。ただし、ユーザーがアクセスを拒否した場合は、再度アクセスしてユーザーにパーミッションを要求できなくなります。

Permission API を使用してアクセス権の有無を確認する

getUserMedia API では、すでにカメラへのアクセス権があるかどうかはわかりません。これは問題になります。適切な UI を表示してカメラへのアクセスをユーザーに許可してもらうには、カメラへのアクセス権を求める必要があります。

この問題は、一部のブラウザでは Permission API を使用することで解決できます。navigator.permission API を使用すると、プロンプトを再度表示する必要なく、特定の API にアクセスできるかどうかを照会できます。

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

  • 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 () {};
});

フィードバック