大多數瀏覽器都能使用使用者的相機,
許多瀏覽器現在都能存取使用者的影片和音訊輸入。不過,視瀏覽器而定,這類瀏覽器可能是完整的動態內嵌體驗,也可以委派至使用者裝置上的其他應用程式。除此之外,並非所有裝置都有攝影機那麼,該如何打造能在任何地方都能順利運作的 使用者產生的圖片呢?
簡單開始,逐步展開
如果想逐步提升體驗,您必須先從在所有平台都適用的解決方案著手。最簡單的方法就是要求使用者提供預錄的檔案。
要求提供網址
這是支援但最不滿意的選項。請使用者為您提供網址,然後開始使用該網址。如果只是顯示圖片,在所有位置都能使用。建立 img
元素並設定 src
,這樣就大功告成了。
不過,如果您想以任何方式操控圖片,操作起來會比較複雜。除非伺服器設定了適當的標頭,而且您將圖片標示為跨來源;CORS 會禁止您存取實際的像素;在這之前,唯一可行的方法就是執行 Proxy 伺服器。
檔案輸入
您也可以使用簡單的檔案輸入元素,包括表示只需要圖片檔的 accept
篩選器。
<input type="file" accept="image/*" />
這個方法適用於所有平台。在電腦上,系統會提示使用者從檔案系統上傳圖片檔。在 iOS 和 Android 版 Chrome 和 Safari 中,這個方法會讓使用者選擇要使用哪個應用程式來拍攝圖片,包括直接用相機拍照,或選擇現有的圖片檔。
接著,資料可以附加至 <form>
或透過 JavaScript 操控,方法是監聽輸入元素的 onchange
事件,然後讀取事件 target
的 files
屬性。
<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 中,使用者將無法再選擇現有圖片。系統會改為直接啟動系統相機應用程式。
拖曳
如果您已新增上傳檔案的功能,有一些簡單方法可以提供更加豐富的使用者體驗。
第一種是為網頁新增放置目標,讓使用者能夠從桌面或其他應用程式拖曳檔案。
<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
類似於 Array
,其中包含數字鍵和 length
屬性,但它「實際上」是陣列。沒有任何陣列方法 (例如 forEach()
或 pop()
) 且無法疊代。當然,您可以使用 Array.from(fileList)
取得真正的陣列。
FileList
的項目為 File
物件。這些物件與 Blob
物件完全相同,但有額外的 name
和 lastModified
唯讀屬性。
<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 以下版本,因此在撰寫期間,該功能仍是最新的穩定版本。不過,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,因此您必須照顧使用者的攝影機拍攝快照。
整個流程如下:
- 建立畫布物件,用來容納相機中的相框
- 存取攝影機串流畫面
- 附加到影片元素
- 如要擷取精確影格,請使用
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()
時,瀏覽器會提示使用者將相機權限授予網站。
使用者不喜歡收到要求存取裝置上的強大裝置,因此經常會封鎖要求;若使用者不瞭解產生提示的情境,就會忽略要求。最佳做法是只在必要時要求存取相機。使用者獲得存取權後,系統就不會再次詢問他們。然而,如果使用者拒絕存取權,除非他們手動變更相機權限設定,否則您將無法再次獲得存取權。
相容性
進一步瞭解如何執行行動版和電腦版瀏覽器:
我們也建議您使用 adapter.js 填充碼,防止應用程式受到 WebRTC 規格異動和前置字串差異的影響。