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

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

マットスケール

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

最初はシンプルかつ段階的に

エクスペリエンスを段階的に改善するには、どこでも機能するものから始める必要があります。最も簡単な方法は、録音済みのファイルをユーザーにリクエストすることです。

URL を尋ねる

これは最もサポートされているオプションですが、満足度は最も低いオプションです。ユーザーに URL を提供してもらい、それを使用します。画像を表示するだけの場合、これはどこでも使用できます。img 要素を作成し、src を設定したら、作業は完了です。

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

ファイル入力

画像ファイルのみを指定する 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.filesFileList オブジェクトです)。

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

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

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() を使用してサーバーにアップロード

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

拠点をカバーしたので、次はレベルアップしましょう。

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

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

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

getUserMedia() のサポートは非常に優れていますが、まだどこでも利用できるわけではありません。特に Safari 10 以前ではご利用になれません。Safari 10 は執筆時点では最新の安定版です。ただし、Apple は Safari 11 でのサポートを発表しています。

とはいえ、サポートを見つけるのはとても簡単です。

const supported = 'mediaDevices' in navigator;

getUserMedia() を呼び出すときは、必要なメディアの種類を表すオブジェクトを渡す必要があります。これらの選択肢は制約と呼ばれます。可能な制約としては、前面カメラと背面カメラのどちらを選ぶか、音声が必要か、ストリームの解像度を指定するかどうかなどがあります。

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

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

<video id="player" controls 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 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 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() を呼び出した瞬間に、サイトに対してカメラへのアクセスを許可するよう求められます。

ユーザーは、マシン上の高機能デバイスへのアクセスを求めるプロンプトを好ましくなく、多くの場合、そのリクエストをブロックします。また、プロンプトが作成されたコンテキストがわからなければ、リクエストを無視することもあります。カメラへのアクセスを要求するのは、最初に必要なときだけにすることをおすすめします。アクセス権を付与すると、再度許可を求められることはありません。ただし、ユーザーがアクセスを拒否した場合、ユーザーがカメラの権限設定を手動で変更しない限り、アクセスを再び取得することはできません。

互換性

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

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

フィードバック