使用 getUserMedia() 拍攝靜態照片
本文件展示瞭如何使用 navigator.mediaDevices.getUserMedia() 來訪問支援 getUserMedia() 的電腦或手機上的攝像頭並用它拍照。

您也可以直接跳轉到 演示。
HTML 標記
我們的 HTML 介面有兩個主要的操作部分:流和捕獲面板,以及展示面板。其中每一部分都並排呈現在各自的 <div> 中,以便於樣式設定和控制。我們有一個 <button> 元素(permissions-button),稍後可以在 JavaScript 中使用它來允許使用者透過 getUserMedia() 針對每個裝置允許或阻止攝像頭許可權。
左側的框包含兩個元件:一個 <video> 元素,它將接收來自 navigator.mediaDevices.getUserMedia() 的流,以及一個 <button> 來開始影片捕獲。這很簡單,當我們進入 JavaScript 程式碼時,我們會看到它是如何組合在一起的。
<div class="camera">
<video id="video">Video stream not available.</video>
<button id="start-button">Capture photo</button>
</div>
接下來,我們有一個 <canvas> 元素,捕獲的幀將儲存在此元素中,可能以某種方式進行處理,然後轉換為輸出影像檔案。這個 canvas 透過使用 display: none 進行樣式設定而被隱藏,以避免螢幕混亂——使用者不需要看到這個中間階段。
我們還有一個 <img> 元素,我們將在此元素中繪製圖像——這是顯示給使用者的最終影像。
<canvas id="canvas"></canvas>
<div class="output">
<img id="photo" alt="The screen capture will appear in this box." />
</div>
JavaScript 程式碼
現在讓我們看一下 JavaScript 程式碼。我們將它分解成幾個小塊,以便更容易解釋。
初始化
我們首先設定將要使用的各種變數。
const width = 320; // We will scale the photo width to this
let height = 0; // This will be computed based on the input stream
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和流的 縱橫比 來計算。 流式傳輸-
指示當前是否有活動的影片流正在執行。
video-
對
<video>元素的引用。 canvas-
對
<canvas>元素的引用。 照片-
對
<img>元素的引用。 -
用於觸發捕獲的
<button>元素的引用。 -
用於控制頁面是否可以訪問裝置的
<button>元素的引用。
獲取媒體流
下一個任務是獲取媒體流:我們定義了一個事件監聽器,當用戶單擊“允許攝像頭”按鈕時,該監聽器會呼叫 MediaDevices.getUserMedia() 並請求影片流(無音訊)。它返回一個 Promise,我們為其附加了成功和失敗的回撥函式。
allowButton.addEventListener("click", () => {
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
});
});
成功回撥函式接收一個 stream 物件作為輸入,並將其設定為我們 <video> 元素的源。一旦流與 <video> 元素連結,我們就透過呼叫 HTMLMediaElement.play() 來開始播放它。
如果開啟流失敗,則會呼叫錯誤回撥函式。例如,如果連線的攝像頭不相容,或者使用者拒絕了訪問,就會發生這種情況。
監聽影片開始播放
在 <video> 上呼叫 HTMLMediaElement.play() 後,在影片流開始流動之前會有一段(希望是短暫的)時間。為了避免在此之前阻塞,我們向 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 之間的尺寸差異來設定影片的高度。
最後,透過在每個元素的兩個屬性上呼叫 Element.setAttribute() 並相應地設定寬度和高度,將影片和 canvas 的 width 和 height 設定為匹配。最後,我們將 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() 用該顏色填充整個 canvas。
此函式中的最後一步是將 canvas 轉換為 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();
}
}
與任何需要處理 canvas 內容的情況一樣,我們首先獲取隱藏 canvas 的 2D 繪圖上下文。
然後,如果寬度和高度都非零(這意味著至少有潛在的有效影像資料),我們將 canvas 的寬度和高度設定為與捕獲幀匹配,然後呼叫 drawImage() 將影片的當前幀繪製到上下文中,用幀影像填充整個 canvas。
注意:這利用了 HTMLVideoElement 介面在任何接受 HTMLImageElement 作為引數的 API 中看起來都像 HTMLImageElement 的事實,影片的當前幀被呈現為影像的內容。
一旦 canvas 包含捕獲的影像,我們就透過對其呼叫 HTMLCanvasElement.toDataURL() 將其轉換為 PNG 格式;最後,我們呼叫 photo.setAttribute() 來使我們的捕獲的靜態框顯示影像。
如果沒有有效的影像可用(即 width 和 height 都為 0),我們透過呼叫 clearPhoto() 來清除捕獲幀框的內容。
演示
單擊“允許攝像頭”以選擇一個輸入裝置並允許頁面訪問攝像頭。影片開始播放後,您可以單擊“拍攝照片”以將流中的靜態畫面捕獲為繪製在右側 canvas 上的影像。
有趣的濾鏡
由於我們透過從 <video> 元素抓取幀來捕獲使用者攝像頭的影像,因此我們可以對影片應用有趣的 CSS filter 效果。這些濾鏡範圍從基本(使影像黑白)到複雜(高斯模糊和色相旋轉)。
#video {
filter: grayscale(100%);
}
要將影片濾鏡應用於照片,takePicture() 函式需要進行以下更改。
function takePicture() {
const context = canvas.getContext("2d");
if (width && height) {
canvas.width = width;
canvas.height = height;
// Get the computed CSS filter from the video element.
// For example, it might return "grayscale(100%)"
const videoStyles = window.getComputedStyle(video);
const filterValue = videoStyles.getPropertyValue("filter");
// Apply the filter to the canvas drawing context.
// If there's no filter (i.e., it returns "none"), default to "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 開發者工具的 Style Editor 來玩這個效果;有關如何操作的更多資訊,請參閱 Edit CSS filters。
使用特定裝置
如果需要,您可以將允許的影片源集限制為特定裝置或一組裝置。為此,請呼叫 MediaDevices.enumerateDevices。當 Promise 以描述可用裝置的 MediaDeviceInfo 物件陣列 fulfilled 時,找到您想要允許的裝置,並在傳遞給 getUserMedia() 的 MediaTrackConstraints 物件中指定相應的 deviceId 或 deviceIds。