大多數智慧手機都有前置和後置攝像頭,當你在建立視訊應用時你可能想要選擇或者切換前置、後置攝像頭。
如果你開發的是一款聊天應用,你很可能會想呼叫前置攝像頭,但如果你開發的是一款拍照軟體,那麼你會更傾向於使用後置攝像頭。在這篇文章中我們將探討如何通過 mediaDevices API 和 media constraints (媒體約束) 選擇或者切換攝像頭。
準備工作
要跟著本文一起動手實踐你需要:
- 一款擁有兩個可供測試的攝像頭的 iOS 或 Android 裝置,如果你的電腦有兩個攝像頭那也可以
- ngrok 以便你能通過移動裝置輕鬆訪問到你的專案(也因為我覺得 ngrok 炒雞棒)
- 這個 GitHub 庫 的程式碼讓你起步
要獲取程式碼,先把這個專案 clone 下來然後 checkout 到 initial-project
tag 下。
1 2 |
git clone https://github.com/philnash/mediadevices-camera-selection.git -b initial-project cd mediadevices-camera-selection |
這個起步專案已經為你準備好了一些 HTML 和 CSS,所以我們就可以把注意集中到 JavaScript 上了。你可以直接開啟 index.html,但我建議你用一款 webserver 把這些檔案託管起來。我喜歡用 npm 的 serve 模組。我在這個庫裡已經引入了 serve,要使用它你需要先用 npm 安裝依賴然後啟動這個服務。
1 2 |
npm install npm start |
服務執行起來後,我們要用 ngrok 開啟一條隧道。serve 用 5000 埠託管檔案,要用 ngrok 開隧道通到這個埠,新開一個命令列視窗輸入以下命令:
1 |
ngrok http 5000 |
好了你現在可以公網訪問這個站點了,你可以在移動裝置上開啟這個網站,這樣接下來就可以測試啦。確保你開啟的是 HTTPS 的 URL,因為我們用的 API 只能在安全環境下使用。
網站看起來像這樣:
獲取 media stream
我們的第一個任務是從任意攝像頭獲取視訊流顯示到螢幕上。完成這個之後我們再調研如何選擇特定攝像頭。開啟 app.js , 我們以從 DOM 中選擇按鈕和 video 元素開始:
1 2 3 |
// app.js const video = document.getElementById('video'); const button = document.getElementById('button'); |
當使用者點選或觸控按鈕時,我們要使用 mediaDevices API 請求攝像頭許可權。要這樣做,我們要呼叫 navigator.mediaDevices.getUserMedia ,傳遞 media constraints 物件。讓我們從簡單的 constraints 開始,我們只需要視訊,因此我們把 video 設定為 true,audio 設定為 false。
getUserMedia 會返回一個 promise,當 resolve 的時候我們就可以訪問到攝像頭的媒體流了。把媒體流賦值給 video 元素的 srcObj 屬性,我們就能從螢幕上看到視訊了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
button.addEventListener('click', event => { const constraints = { video: true, audio: false }; navigator.mediaDevices .getUserMedia(constraints) .then(stream => { video.srcObject = stream; }) .catch(error => { console.error(error); }); }); |
儲存檔案,重新載入頁面然後點選按鈕。你應該能看到一個許可權對話方塊請求訪問你的攝像頭,一旦授權螢幕上就應該會出現視訊。在你的電腦和手機上試一試,我在我的 iPhone 上試了,被選擇的是前置攝像頭。
如果你用的是一部 iPhone 手機,確認你在 Safari 裡嘗試,因為其他瀏覽器貌似並沒有效果。
可用攝像頭
media Devices API 為我們提供了一種列舉所有可用音訊和視訊輸入裝置的方式。我們要用 enumerateDevices 函式來為<select>框構建選項,這樣我們就能用它來選擇我們想看的攝像頭了。再次開啟 app.js,從 DOM 中選出<select>元素:
1 2 3 |
const video = document.getElementById('video'); const button = document.getElementById('button'); const select = document.getElementById('select'); |
enumerateDevices 會返回一個 promise,所以讓我們寫一個用來接受 promise 結果的函式吧。這個函式接收一個 media device 陣列作為引數。
首先要做的是清空<select>現有的任何選項,然後插入一個空的
<option>。接著迴圈遍歷所有裝置,過濾掉非 “videoinput”型別的裝置。然後我們建立一個<option>元素,用裝置 ID 當作 option value,裝置 label 當作 option text。我們還要處理一種情況,如果一個裝置沒有 label 存在,生成一個簡單的 “Camera n” 作為標籤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const video = document.getElementById('video'); const button = document.getElementById('button'); const select = document.getElementById('select'); function gotDevices(mediaDevices) { select.innerHTML = ''; select.appendChild(document.createElement('option')); let count = 1; mediaDevices.forEach(mediaDevice => { if (mediaDevice.kind === 'videoinput') { const option = document.createElement('option'); option.value = mediaDevice.deviceId; const label = mediaDevice.label || `Camera ${count++}`; const textNode = document.createTextNode(label); option.appendChild(textNode); select.appendChild(option); } }); } |
在 app.js 末尾呼叫一下 enumerateDevices。
1 |
navigator.mediaDevices.enumerateDevices().then(gotDevices); |
重新整理頁面,看一下按鈕旁邊的下拉選擇框。如果你用的是 Android ,或者使用 Chrome 或 Firefox,你就能看到可用的攝像頭名稱了。
然而在 iPhone 上,你將看到我們函式生成的通用名字 “Camera 1” 和 “Camera 2”。在 iOS 上只有你授權至少一個攝像頭給網站,你才能看到攝像頭的名字。這讓在我們的介面上選擇攝像頭變得更不方便,因為儘管你能獲取到裝置 ID,你還是不能分辨哪個攝像頭是哪個。
目前我們還沒有處理下拉選擇框來改變攝像頭。在這之前,讓我們來看另一種能改變哪個攝像頭被使用的方法。
FacingMode
FacingMode 約束是一個可以用來選擇攝像頭的替代方法。這個方法比起通過 enumerateDevices 函式獲取 ID 來說更不那麼精確,但在移動裝置上效果非常好。對於這個約束,一共有四種選項可供你選擇:使用者(user),環境(environment),左(left),右(right)。MDN 上的文件對這個約束做了詳細介紹, 以本文的目的我們將使用使用者和環境模式,在移動裝置上它們正好對應到前置和後置攝像頭。
要使用 facingMode 約束我們需要修改呼叫 getUserMedia 時使用的 constraints 物件。對於 video 我們需要一個物件來控制具體的約束,而不是給一個 true 值。像這樣修改程式碼來使用前置攝像頭:
1 2 3 4 5 6 7 8 |
button.addEventListener('click', event => { const videoConstraints = { facingMode: 'user' }; const constraints = { video: videoConstraints, audio: false }; |
現在可以用你的手機測試。你應該能看到前置攝像頭被使用。更改 facingMode 為 environment 再試一次, 使用的應該是後置攝像頭。讓我們把這些程式碼和上面通過 enumerateDevices 獲取到的結果放到一塊兒,只要我們獲得了讀取攝像頭資料的許可權,就能構建一個攝像頭切換器了。
切換攝像頭
現在我們有在首次選擇時挑選使用者或環境攝像頭的程式碼了,但如果我們要切換攝像頭那還有一丟丟額外的工作要做。
首先,我們應該保留對當前流的引用,這樣當我們切換到另一個流時就能停止當前流。在 app.js 的最前面新增一個額外的變數和輔助函式來停止流中的軌。
1 2 3 4 5 6 7 8 9 10 |
const video = document.getElementById('video'); const button = document.getElementById('button'); const select = document.getElementById('select'); let currentStream; function stopMediaTracks(stream) { stream.getTracks().forEach(track => { track.stop(); }); } |
函式 stopMediaTracks 接收一個媒體流,迴圈遍歷流中的每一個媒體軌道,呼叫 stop 方法停止媒體軌。
我們要在點選同一個按鈕時改變攝像頭,所以我們需要更新一下按鈕的事件監聽器了。如果當前有媒體流,我們應該先停止掉它。然後我們要檢查<select>元素看是否選擇了特定的裝置,然後基於此構造 media constraints 物件。
這樣修改按鈕的點選處理函式和 video constraints:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
button.addEventListener('click', event => { if (typeof currentStream !== 'undefined') { stopMediaTracks(currentStream); } const videoConstraints = {}; if (select.value === '') { videoConstraints.facingMode = 'environment'; } else { videoConstraints.deviceId = { exact: select.value }; } const constraints = { video: videoConstraints, audio: false }; |
當我們想通過 deviceId 來選擇裝置時,使用 exact 約束。 可是對於 facingMode,我們沒有使用 exact 約束, 否則在一個無法識別有沒有使用者或環境模式的裝置上將會失敗,導致我們什麼媒體裝置也拿不到。
當我們獲得使用視訊的許可權時,在點選處理函式內,我們還要修改一些別的東西。把傳遞給函式的新流賦值給 currentStream 以便後續呼叫 stop,觸發另一次 enumerateDevices 的呼叫。
enumerateDevices 返回一個 promise,所以在我們的 then 函式中可以直接返回它,然後鏈式建立一個新的 then 把結果傳遞給 gotDevices 函式處理。
用以下程式碼替換現有的 getUserMedia 呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
button.addEventListener('click', event => { if (typeof currentStream !== 'undefined') { stopMediaTracks(currentStream); } const videoConstraints = {}; if (select.value === '') { videoConstraints.facingMode = 'environment'; } else { videoConstraints.deviceId = { exact: select.value }; } const constraints = { video: videoConstraints, audio: false }; navigator.mediaDevices .getUserMedia(constraints) .then(stream => { currentStream = stream; video.srcObject = stream; return navigator.mediaDevices.enumerateDevices(); }) .then(gotDevices) .catch(error => { console.error(error); }); }); |
當你新增完所有的程式碼,你的 app.js 應該看起來像這個檔案一樣。重新整理頁面然後你就能愉快地選擇和改變攝像頭了。這個頁面在移動裝置和電腦上都有效。
下一步
我們已經看到如何通過使用 facingMode 和 deviceId 約束來選擇使用者的攝像頭。記住,在你有許可權使用攝像頭之前,facingMode 更可靠,但是選擇 deviceId 更加精確。你可以從 GitHub 倉庫 中得到所有本文中的程式碼,你也可以從這裡嘗試線上版的應用。
如果你正在使用 Twilio Video 構建視訊應用,你可以在呼叫 connect 或者 createLocalVideoTrack 的時候使用這些 constraints。
對於視訊聊天來說,選擇和切換攝像頭是非常有用的功能,允許使用者在你的應用介面準確地選擇他們想用的攝像頭,並且還能做到在視訊通話時分享你的螢幕。
還有哪些其他你想看到的在視訊聊天中有用的功能?或者對這個功能有什麼疑問?歡迎在評論中留言或者在 Twitter 上 @philnash。