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

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

多くのブラウザで、ユーザーからの動画入力と音声入力にアクセスできるようになりました。ただし、ブラウザによっては、完全な動的インライン エクスペリエンスになる場合もあれば、ユーザーのデバイス上の別のアプリに委任される場合もあります。さらに、すべてのデバイスにカメラが搭載されているわけではありません。では、ユーザー作成の画像を使用して、あらゆるデバイスで適切に動作するエクスペリエンスを作成するにはどうすればよいでしょうか。

シンプルに始め、段階的に進める

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

URL を尋ねる

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

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

ファイル入力

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

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

この方法はすべてのプラットフォームで機能します。パソコンでは、ファイル システムから画像ファイルをアップロードするよう求めるメッセージが表示されます。iOS と Android の Chrome と Safari では、この方法で画像の撮影に使用するアプリを選択できます。カメラで直接写真を撮影するオプションや、既存の画像ファイルを選択するオプションなどがあります。

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

その後、入力要素の onchange イベントをリッスンし、イベント targetfiles プロパティを読み取ることで、データを <form> に関連付けたり、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 の難しい点は、クロスブラウザで完全にサポートするには、対象要素が選択可能で編集可能である必要があることです。<textarea><input type="text"> の両方が、contenteditable 属性を持つ要素と同様に、この要件を満たします。ただし、これらはテキストの編集用に設計されていることも明らかです。

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

FileList オブジェクトの処理

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

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

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

<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 を使用します。接続されているマイクとカメラへのアクセスを求めるメッセージが表示されます。

getUserMedia() のサポートは非常に優れていますが、まだすべてのブラウザでサポートされているわけではありません。特に、Safari 10 以前では使用できません。これは、この記事の執筆時点では最新の安定版です。ただし、Safari 11 では利用可能になるとApple が発表しています。

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

const supported = 'mediaDevices' in navigator;

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

ただし、カメラからデータを取得するには、video: true という 1 つの制約のみが必要です。

成功すると、カメラのデータを含む MediaStream が 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. カメラのフレームを保持するキャンバス オブジェクトを作成する
  2. カメラ ストリームにアクセスする
  3. 動画要素に添付する
  4. 正確なフレームをキャプチャする場合は、drawImage() を使用して動画要素のデータをキャンバス オブジェクトに追加します。
<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>

カメラから取得したデータをキャンバスに保存すると、さまざまなことができます。以下に例を挙げます。

  • サーバーに直接アップロードする
  • ローカルに保存する
  • 画像にファンキーな効果を適用する

ヒント

不要な場合はカメラからのストリーミングを停止する

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

カメラへのアクセスを停止するには、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() を呼び出すとすぐに、ブラウザにカメラへのアクセス権をサイトに付与するよう求めるメッセージが表示されます。

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

互換性

モバイル ブラウザとパソコンのブラウザの実装の詳細:

また、adapter.js shim を使用して、WebRTC 仕様の変更や接頭辞の違いからアプリを保護することをおすすめします。

フィードバック