getUserMedia() による写真の撮影
この記事では、navigator.mediaDevices.getUserMedia() を使用して、 getUserMedia() に対応しているコンピューターや携帯電話のカメラにアクセスして写真を撮影する方法について示しています。

お望みならばデモに直接ジャンプすることもできます。
HTML のマークアップ
HTML のインターフェイス には、ストリーム・キャプチャパネルとプレゼンテーションパネルの 2 つの主要な操作セクションがあります。
これらはそれぞれ自分自身で <div> の中に横に並んで表示され、スタイル設定や制御が容易にできるようになっています。
<button> 要素 (permissions-button) があり、後で JavaScript で使用して、getUserMedia() を通じて端末ごとにカメラの権限を許可または拒否できるようにします。
左側のボックスには 2 つの構成要素があります。 <video> 要素は navigator.mediaDevices.getUserMedia() からストリームを受け取り、 <button> は映像のキャプチャを開始するためのものです。
これは直感的ですが、JavaScript コードを見ていくうちに、全体がどうつながっているかが分かってくるでしょう。
<div class="camera">
<video id="video">映像ストリームが利用できません。</video>
<button id="start-button">写真をキャプチャ</button>
</div>
次に、 <canvas> 要素があります。この要素にキャプチャしたフレームを格納し、何らかの方法で操作した後、出力画像ファイルに変換する可能性があります。
このキャンバスは、display: none でスタイル設定することで非表示にし、画面が煩雑にならないようにしています(ユーザーはこの中間段階を見る必要がないため)。
また、画像を描画するための <img> 要素も保有しており、これがユーザーに見せる最終的な表示となります。
<canvas id="canvas"></canvas>
<div class="output">
<img
id="photo"
src=""
alt="スクリーンキャプチャ結果はこのボックスに表示されます。" />
</div>
JavaScript コード
それでは、JavaScript コードを見ていきましょう。説明しやすくするために、いくつか小さなブロックに分けていきます。
初期化
まず、これから使用するさまざまな変数を設定します。
const width = 320; // 写真の幅をこのサイズに変倍する
let height = 0; // これは入力ストリームに基づいて計算される
let streaming = false;
const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const photo = document.getElementById("photo");
const startButton = document.getElementById("start-button");
const allowButton = document.getElementById("permissions-button");
変数は次の通りです。
width-
入力される映像のサイズが何であれ、出来上がった画像を幅 320 ピクセルになるように拡大縮小します。
height-
画像の出力高さは,ストリームの
widthとアスペクト比が指定された場合に計算されます. streaming-
現在、アクティブな動画ストリームが実行されているか否かを示します。
video-
<video>要素への参照です。 canvas-
<canvas>要素への参照です。 photo-
<img>要素への参照です。 -
キャプチャを起動するために使用される
<button>要素への参照です。 -
ページが端末にアクセスできるかどうかを制御するために使用される
<button>要素への参照です。
メディアストリームの取得
次の仕事は、メディアストリームを取得することです。ユーザーが「カメラの使用を許可」ボタンをクリックした際に、MediaDevices.getUserMedia() を呼び出し、音声なしのビデオストリームをリクエストするイベントリスナーを定義します。
これによりプロミスが返されるため、成功時および失敗時のコールバックを設定します。
allowButton.addEventListener("click", () => {
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`エラーが発生: ${err}`);
});
});
成功時のコールバックには、入力として stream オブジェクトが渡されます。これは、<video> 要素のソースとして設定されます。
ストリームが <video> 要素に関連付けられると、HTMLMediaElement.play() を呼び出して再生を開始します。
エラーコールバックは、ストリームを開くことがうまくいかない場合に呼び出されます。 これは例えば、互換性のあるカメラが接続されていない場合や、ユーザーがアクセスを拒否した場合などに起こります。
映像の再生が始まるのを待ち受けする
HTMLMediaElement.play() を <video> に対して呼び出した後、映像のストリームが流れ始めるまでに(期待する短い時間ですが)経過する時刻があります。そうなるまでブロックされることを避けるために、 video に canplay イベント用のイベントリスナーを追加し、映像の再生が実際に始まると配信されるようにします。この点で、 video オブジェクトのすべてのプロパティは、ストリームの形式に基づいて設定されています。
video.addEventListener("canplay", (ev) => {
if (!streaming) {
height = video.videoHeight / (video.videoWidth / width);
video.setAttribute("width", width);
video.setAttribute("height", height);
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
streaming = true;
}
});
このコールバックは、初めて呼び出されたとき以外は何もしません。このテストでは、変数 streaming の値を確認し、このメソッドが最初に実行されたときは false になっていることを確認します。
このメソッドが最初に実行された場合は、動画の実際のサイズである video.videoWidth とレンダリングする幅である width のサイズ差に基づいて、動画の高さを設定します。
最後に、動画とキャンバスの両方の width と height を、それぞれの要素の 2 つのプロパティのそれぞれに対して Element.setAttribute() を呼び出して、適切な幅と高さを設定することによって、互いに一致するように設定します。最後に、誤ってこの設定コードを再度実行しないように、変数 streaming に true を設定しています。
ボタンのクリックを処理する
ユーザーが startButton をクリックするたびに静止画を撮影するには、ボタンにイベントリスナーを追加して、 click イベントが発行されたときに呼び出されるようにする必要があります。
startButton.addEventListener("click", (ev) => {
takePicture();
ev.preventDefault();
});
このメソッドは直感的で、下記のストリームからのフレームのキャプチャ で定義されている takePicture() 関数を呼び出した後、受け取ったイベントで Event.preventDefault() を呼び、クリック処理が複数回行われないようにしています。
写真ボックスのクリア
写真ボックスをクリアするには、画像を作成し、それを <img> 要素で使用可能な形式に変換して、最も最近撮影したフレームを表示する必要があります。そのコードは次のようになります。
function clearPhoto() {
const context = canvas.getContext("2d");
context.fillStyle = "#aaaaaa";
context.fillRect(0, 0, canvas.width, canvas.height);
const data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
}
clearPhoto();
まず、オフスクリーンレンダリングに使用する非表示の <canvas> 要素への参照を取得することから始めます。次に、fillStyle を #aaaaaa (かなり明るい灰色) に設定し、fillRect() を呼び出してキャンバス全体をその色で塗りつぶします。
最後に、キャンバスを PNG 画像に変換して photo.setAttribute() を呼び出し、キャプチャした静止画を表示させています。
ストリームからのフレームのキャプチャ
定義する最後の関数があり、この点がこの演習のポイントです。 takePicture() 関数は、現在表示されている動画フレームをキャプチャし、 PNG ファイルに変換して、キャプチャしたフレーム枠に表示するのがその仕事です。コードは次のようになります。
function takePicture() {
const context = canvas.getContext("2d");
if (width && height) {
canvas.width = width;
canvas.height = height;
context.drawImage(video, 0, 0, width, height);
const data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
} else {
clearPhoto();
}
}
キャンバスのコンテンツを操作する必要があるときはいつでもそうですが、まず非表示のキャンバスの 2D 描画コンテキストを取得することから始めます。
次に、幅と高さがどちらも 0 でない場合(少なくとも有効な画像データがある可能性があるということ)、キャンバスの幅と高さをキャプチャしたフレームの幅と高さに一致するように設定し、 drawImage() を呼び出して動画の現在のフレームをコンテキストに描き、全体の画像をキャンバスで塗りつぶすようにします。
メモ:
このインターフェイスは、HTMLImageElement を引数として受け入れる任意の API からは HTMLVideoElement が HTMLImageElement のように見えることを利用しており、動画の現在のフレームが画像のコンテンツとして表示されるように工夫されています。
キャンバスにはキャプチャした画像が格納されたら、HTMLCanvasElement.toDataURL() を呼び出して PNG 形式に変換し、最後に photo.setAttribute() を呼び出してキャプチャした静止画ボックスにその画像を表示させます。
利用できる有効な画像がない場合(つまり、width と height がどちらも 0 の場合)は、clearPhoto() を呼び出して、キャプチャしたフレームボックスのコンテンツを消去します。
デモ
「カメラの使用を許可」をクリックして入力機器を指定し、このページがカメラにアクセスすることができるようにしてください。 動画が開始したら、「写真をキャプチャ」をクリックすると、ストリームの静止画を右側のキャンバスに描画された画像として保存することができます。
フィルターで楽しむ
<video> 要素からフレームを取得することによって、ユーザーのウェブカメラから画像をキャプチャしているので、映像にフィルターや楽しい効果をとても簡単に適用することができます。結局のところ、 filter プロパティを使用して要素に適用した CSS フィルターは、キャプチャした写真に影響を与えます。これらのフィルターは、単純なもの(画像を白黒にする)から極端なもの(ガウスぼかしや色相回転)までの範囲があります。
#video {
filter: grayscale(100%);
}
写真に動画フィルターを適用するには、takePicture()関数に以下の変更を加える必要があります。
function takePicture() {
const context = canvas.getContext("2d");
if (width && height) {
canvas.width = width;
canvas.height = height;
// この video 要素から CSS フィルターの計算値を取得
// この例では、"grayscale(100%)" などが返される可能性がある
const videoStyles = window.getComputedStyle(video);
const filterValue = videoStyles.getPropertyValue("filter");
// このフィルターをキャンバスの描画コンテキストに適用する
// フィルターがない場合("none" が返された場合)、デフォルトで "none" とする
context.filter = filterValue !== "none" ? filterValue : "none";
context.drawImage(video, 0, 0, width, height);
const dataUrl = canvas.toDataURL("image/png");
photo.setAttribute("src", dataUrl);
} else {
clearPhoto();
}
}
この効果は例えば、 Firefox の開発者ツールのスタイルエディターで再生することができます。やり方の詳細は CSS フィルターの編集を参照してください。
特定の機器の使用
必要に応じて、許可される動画ソースのセットを特定の機器または一連の機器に制限することができます。これを行うには、MediaDevices.enumerateDevices を呼び出します。利用できる機器を記述した MediaDeviceInfo オブジェクトの配列でプロミスが履行されたら、許可したいものを探し、 getUserMedia() に渡される MediaTrackConstraints オブジェクトで対応する deviceId または deviceId を指定します。