擷取使用者提供的圖片

大多數瀏覽器都能使用使用者的相機,

Mat Scale

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

簡單開始,逐步展開

如果想逐步提升體驗,您必須先從在所有平台都適用的解決方案著手。最簡單的方法就是要求使用者提供預錄的檔案。

要求提供網址

這是支援但最不滿意的選項。請使用者為您提供網址,然後開始使用該網址。如果只是顯示圖片,在所有位置都能使用。建立 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,因此我應該稍微談談這是什麼。

FileList 類似於 Array,其中包含數字鍵和 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() 將伺服器上傳到伺服器

以互動方式存取相機

既然您已經提升了自己的基地,接下來就是要逐步強化!

新式瀏覽器可直接存取相機,讓您打造與網頁完全整合的體驗,因此使用者完全不必離開瀏覽器。

取得相機存取權

您可以在名為 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,因此您必須照顧使用者的攝影機拍攝快照。

整個流程如下:

  1. 建立畫布物件,用來容納相機中的相框
  2. 存取攝影機串流畫面
  3. 附加到影片元素
  4. 如要擷取精確影格,請使用 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 規格異動和前置字串差異的影響。

意見回饋