擷取使用者提供的圖片

大多數瀏覽器都可以存取使用者的相機。

許多瀏覽器現在都能存取使用者的影片和音訊輸入內容。不過,視瀏覽器而定,這可能會是完整的動態內嵌體驗,也可能會委派給使用者裝置上的其他應用程式。此外,並非所有裝置都有相機那麼,您要如何打造使用者產生圖片的體驗,讓圖片在任何地方都能正常運作呢?

從簡單開始,逐步增加

如果想要逐步提升體驗,您必須先從任何地方都能順暢使用。最簡單的方式就是請使用者提供預錄檔案。

要求提供網址

這是最受支援但最不滿意的選項。請使用者提供網址,然後使用該網址。只要顯示圖片,這項功能在任何地方都適用。建立 img 元素、設定 src,即可完成。

不過,如果您想以任何方式操控圖片,情況就會複雜一些。除非伺服器設定適當的標頭,且您將圖片標示為跨來源,否則 CORS 會阻止您存取實際像素。唯一可行的解決方法是執行 Proxy 伺服器。

檔案輸入

您也可以使用簡單的檔案輸入元素,包括 accept 篩選器,指出您只需要圖片檔案。

<input type="file" accept="image/*" />

這個方法適用於所有平台。在桌上型電腦上,它會提示使用者從檔案系統上傳圖片檔。在 iOS 和 Android 裝置上的 Chrome 和 Safari 中,此方法可讓使用者選擇要透過哪個應用程式擷取圖片,包括直接使用相機拍照,或選擇現有的圖片檔。

Android 選單,包含兩個選項:擷取圖片和檔案 iOS 選單,提供三個選項:拍照、相片庫、iCloud

接著,可以將資料附加至 <form>,或透過 JavaScript 操控,方法是監聽輸入元素的 onchange 事件,然後讀取事件 targetfiles 屬性。

<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,所以我應該稍微說明一下。

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() 將其上傳至伺服器

以互動方式存取攝影機

您已掌握基礎知識,現在是時候逐步改善了!

現代瀏覽器可直接存取相機,讓您建構與網頁完全整合的體驗,使用者不必離開瀏覽器。

取得相機存取權

您可以使用 WebRTC 規格中的 getUserMedia() API,直接存取攝影機和麥克風。這會提示使用者存取已連接的麥克風和相機。

支援 getUserMedia() 雖然很好,但目前尚未支援所有地區。特別要注意的是,這項功能不適用於 Safari 10 以下版本,而這也是本文撰寫時的最新穩定版。不過,Apple 已宣布 Safari 11 將支援這項功能。

不過,偵測支援功能非常簡單。

const supported = 'mediaDevices' in navigator;

呼叫 getUserMedia() 時,您必須傳入描述所需媒體類型的物件。這些選項稱為限制條件。以下列舉幾個可能的限制,包括偏好前置或後置鏡頭、需要的音訊以及偏好的串流解析度。

不過,如要從攝影機取得資料,您只需要一個限制,也就是 video: true

如果成功,API 會傳回包含相機資料的 MediaStream,您可以將其附加至 <video> 元素並播放,以顯示即時預覽畫面,或是將其附加至 <canvas> 以取得快照。

<video id="player" controls playsinline 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 playsinline 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 playsinline 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 規格變更和前置字元差異的影響。

意見回饋