ユーザーから画像をキャプチャする

ほとんどのブラウザはユーザーのカメラにアクセスできます。

現在、多くのブラウザには、ユーザーによる動画および音声ファイルの入力を処理する機能が備わっています。ただしブラウザによっては、この機能が動的に組み込まれている場合や、ユーザーの端末上にある別のアプリに処理が委ねられる場合があります。さらに、すべてのデバイスにカメラが搭載されているわけではありません。では、ユーザー作成の画像を使用して、あらゆるデバイスで適切に動作するエクスペリエンスを作成するにはどうすればよいでしょうか。

簡単なケースから始める

エクスペリエンスを段階的に向上させるには、まず、あらゆるデバイスで機能するものから始める必要があります。最も簡単な方法は、事前に録音済みのファイルをユーザーに要求することです。

URL を尋ねる

これは最もサポートされている方法ですが、満足度が最も低い方法です。お客様に URL を教えていただき、その URL を使用します。画像を表示するだけであれば、どこでも機能します。img 要素を作成し、src を設定するだけで完了です。

ただし、画像を操作する場合は、もう少し複雑になります。CORS では、サーバーが適切なヘッダーを設定し、画像を crossorigin としてマークしない限り、実際のピクセルにアクセスできません。この問題を回避する唯一の実用的な方法は、プロキシ サーバーを実行することです。

ファイル入力

画像ファイルのみを指定できる accept フィルタなど、シンプルなファイル入力要素を使用することもできます。

<input type="file" accept="image/*" />

この方法はすべてのプラットフォームで使用できます。PC の場合、ユーザーは、ファイル システムから画像ファイルをアップロードするように求められます。iOS 版と Android 版の Chrome や Safari では、この方法により、ユーザーはカメラで直接写真を撮る、既存の画像ファイルを選択するなど、画像のキャプチャに使用するアプリを選択できます。

画像とファイルの 2 つのオプションがある Android メニュー 写真の撮影、フォトライブラリ、iCloud の 3 つのオプションがある iOS メニュー

送信されたデータは <form> にアタッチできます。または、入力要素の onchange イベントをリッスンして、イベント targetfiles プロパティを読み取ると、JavaScript でデータを操作することが可能です。

<input type="file" accept="image/*" id="file-input" />
<script>
  const fileInput = document.getElementById('file-input');

  fileInput.addEventListener('change', (e) =>
    doSomethingWithFiles(e.target.files),
  );
</script>

files プロパティは FileList オブジェクトです。これについては後で詳しく説明します。

必要に応じて、要素に capture 属性を追加することもできます。これにより、カメラから画像を取得することをブラウザに指示します。

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

値のない capture 属性を追加すると、使用するカメラをブラウザが決定します。一方、"user" 値と "environment" 値は、それぞれ前面カメラと背面カメラを優先するようにブラウザに指示します。

capture 属性は Android と iOS で機能しますが、パソコンでは無視されます。ただし、Android では、既存の写真をユーザーが選択できなくなります。代わりに、システム カメラアプリが直接起動されます。

ドラッグ&ドロップ

ファイルのアップロード機能をすでに追加している場合は、ユーザー エクスペリエンスを少し豊かにする簡単な方法がいくつかあります。

1 つ目は、ユーザーがデスクトップまたは別のアプリケーションからファイルをドラッグできるように、ページにドロップ ターゲットを追加する方法です。

<div id="target">You can drag an image file here</div>
<script>
  const target = document.getElementById('target');

  target.addEventListener('drop', (e) => {
    e.stopPropagation();
    e.preventDefault();

    doSomethingWithFiles(e.dataTransfer.files);
  });

  target.addEventListener('dragover', (e) => {
    e.stopPropagation();
    e.preventDefault();

    e.dataTransfer.dropEffect = 'copy';
  });
</script>

ファイル入力と同様に、drop イベントの dataTransfer.files プロパティから FileList オブジェクトを取得できます。

dragover イベント ハンドラを使用すると、dropEffect プロパティを使用して、ファイルをドロップしたときに何が起こるかをユーザーに通知できます。

ドラッグ&ドロップは長い間使用されており、主要なブラウザで十分にサポートされています。

クリップボードから貼り付け

既存の画像ファイルを取得する最後の方法は、クリップボードから取得する方法です。このコードは非常にシンプルですが、ユーザー エクスペリエンスを正しく実現するのは少し難しいです。

<textarea id="target">Paste an image here</textarea>
<script>
  const target = document.getElementById('target');

  target.addEventListener('paste', (e) => {
    e.preventDefault();
    doSomethingWithFiles(e.clipboardData.files);
  });
</script>

e.clipboardData.files は別の FileList オブジェクトです)。

クリップボード API で厄介な点は、クロスブラウザを完全にサポートするには、ターゲット要素を選択可能かつ編集可能にする必要があることです。contenteditable 属性を持つ要素と同様に、<textarea><input type="text"> の両方がこの要件に当てはまります。もちろんテキストの編集用にも 設計されています

ユーザーがテキストを入力できないようにしたい場合は、処理をスムーズに行うのが難しいことがあります。他の要素をクリックしたときに選択される非表示の入力などのトリックを使用すると、ユーザー補助の維持が難しくなる可能性があります。

FileList オブジェクトの処理

上記のメソッドのほとんどは FileList を生成するため、FileList について少し説明します。

FileListArray に似ています。数値キーと length プロパティがありますが、実際には配列ではありません。forEach()pop() などの配列メソッドはなく、反復処理できません。もちろん、Array.from(fileList) を使用して実際の配列を取得することもできます。

FileList のエントリは File オブジェクトです。Blob オブジェクトとまったく同じですが、namelastModified の読み取り専用プロパティが追加されています。

<img id="output" />
<script>
  const output = document.getElementById('output');

  function doSomethingWithFiles(fileList) {
    let file = null;

    for (let i = 0; i < fileList.length; i++) {
      if (fileList[i].type.match(/^image\//)) {
        file = fileList[i];
        break;
      }
    }

    if (file !== null) {
      output.src = URL.createObjectURL(file);
    }
  }
</script>

この例では、画像の MIME タイプを持つ最初のファイルを検索しますが、複数の画像を一度に選択、貼り付け、ドロップすることもできます。

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

  • <canvas> 要素に描画して操作できるようにする
  • ユーザーのデバイスにダウンロードする
  • fetch() を使用してサーバーにアップロードする

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

基本を押さえたら、次は機能を拡張していきましょう。

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

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

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

getUserMedia() のサポートは非常に良好ですが、まだ一部の地域では対応していません。特に Safari 10 以前では利用できず、現時点では Safari 10 が最新の安定版です。ただし、Safari 11 では利用可能になるとApple が発表しています。

ただし、サポートの検出は非常に簡単です。

const supported = 'mediaDevices' in navigator;

getUserMedia() を呼び出すときに、必要なメディアの種類を記述するオブジェクトを渡す必要があります。これらの選択肢を制約と呼びます。前面カメラと背面カメラのどちらを使用するか、音声を使用するかどうか、ストリーミングの解像度など、いくつかの制約があります。

ただし、カメラからデータを取得するために必要な制約は 1 つだけです(video: true)。

アクセスに成功すると、API からカメラデータを含む MediaStream が返されます。これを <video> 要素にアタッチして再生すると、リアルタイム プレビューを表示できます。または、<canvas> にアタッチしてスナップショットを取得することも可能です。

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

  const constraints = {
    video: true,
  };

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

それだけでは、あまり役に立ちません。ユーザーができることは、動画データを取得して再生することだけです。画像を取得するには、少し手間がかかります。

スナップショットを取得する

画像を取得するための最適なオプションは、動画からキャンバスにフレームを描画することです。

Web Audio API とは異なり、ウェブ上の動画専用のストリーミング処理 API がないため、ユーザーのカメラからスナップショットを取得する際は、やや巧妙な処理が必要になります。

プロセスは次のとおりです。

  1. カメラから取得したフレームを保持する canvas オブジェクトを作成します
  2. カメラ ストリームにアクセスする
  3. 動画要素にアタッチする
  4. 正確なフレームを取得する必要がある場合は、drawImage() を使用して、video 要素のデータを canvas オブジェクトに追加します。
<video id="player" controls playsinline autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    // Draw the video frame to the canvas.
    context.drawImage(player, 0, 0, canvas.width, canvas.height);
  });

  // Attach the video stream to the video element and autoplay.
  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

カメラから取得したデータを canvas に保存すると、そのデータをさまざまな方法で操作できます。以下に例を挙げます。

  • データをサーバーに直接アップロードする
  • データをローカルで保存する
  • 画像に斬新な効果を適用する

ヒント

不要になった時点でカメラからのストリーミングを停止する

カメラが不要になったら、使用を停止することをおすすめします。これにより、バッテリーと処理能力を節約できるだけでなく、ユーザーがアプリを信頼できるようになります。

カメラへのアクセスを停止するには、getUserMedia() から返されたストリームの各動画トラックで stop() を呼び出すだけです。

<video id="player" controls playsinline autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    context.drawImage(player, 0, 0, canvas.width, canvas.height);

    // Stop all video streams.
    player.srcObject.getVideoTracks().forEach(track => track.stop());
  });

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    // Attach the video stream to the video element and autoplay.
    player.srcObject = stream;
  });
</script>

カメラを適切に使用するためにパーミッションを要求する

ユーザーが、サイトによるカメラへのアクセスを許可したことがない場合は、getUserMedia() を呼び出すと、カメラへのアクセス権をサイトに付与するよう求める画面が表示されます。

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

互換性

モバイルおよび PC でのブラウザ実装に関する詳細:

WebRTC 仕様の変更と接頭辞の差異によるアプリへの影響を軽減するために、adapter.js shim を使用することをおすすめします。

フィードバック