擷取 HTML5 中的音訊和視訊

音訊/視訊擷取功能一直是網站開發人員長久以來的「聖杯」。多年以來,我們都必須依賴瀏覽器外掛程式 (FlashSilverlight) 才能完成工作。Come on!

HTML5 可解決這個問題。雖然不明顯,但 HTML5 的興起也帶來了裝置硬體存取權的激增。地理定位 (GPS)、Orientation API (加速計)、WebGL (GPU) 和 Web Audio API (音訊硬體) 都是很好的例子。這些功能非常強大,可公開高層級 JavaScript API,這些 API 位於系統底層硬體功能之上。

本教學課程將介紹新的 API GetUserMedia,可讓網路應用程式存取使用者的攝影機和麥克風。

如果您不瞭解其歷史,我們開發 getUserMedia() API 的過程相當有趣。

過去幾年,我們推出了多個「Media Capture API」變化版本。許多人認為需要在網路上存取原生裝置,但這導致每個人都會編寫新的規格。情況變得如此混亂,以致 W3C 最後決定成立工作小組。單一用途?讓瘋狂變得有意義!Device APIs Policy (DAP) Working Group 的任務是整合並標準化眾多提案。

我來總結一下 2011 年的情況…

第 1 輪:HTML 媒體擷取

HTML 媒體擷取是 DAP 首次嘗試將網路上的媒體擷取標準化。其運作方式是超載 <input type="file">,並為 accept 參數新增值。

如果您想讓使用者使用網路攝影機拍攝快照,可以使用 capture=camera

<input type="file" accept="image/*;capture=camera">

錄製影片或音訊的步驟類似:

<input type="file" accept="video/*;capture=camcorder">
<input type="file" accept="audio/*;capture=microphone">

這麼說來還不錯,對吧?我特別喜歡的是,它會重複使用檔案輸入內容。從語意上來說,這很合理。這個特定的「API」無法執行即時特效 (例如將即時網路攝影機資料算繪至 <canvas>,並套用 WebGL 篩選器)。HTML 媒體擷取功能僅可用於錄製媒體檔案或拍攝即時影像。

支援:

  • Android 3.0 瀏覽器 - 最早實作的項目之一。請觀看這部影片,瞭解實際操作方式。
  • Android 版 Google Chrome (0.16)
  • Firefox 行動版 10.0
  • iOS6 Safari 和 Chrome (部分支援)

第 2 回合:device 元素

許多人認為 HTML Media Capture 的限制太多,因此我們推出了新規格,支援任何類型的 (未來) 裝置。毫無意外,設計會呼叫新的 <device> 元素,這也是 getUserMedia() 的前身。

Opera 是第一個以 <device> 元素初始導入影片擷取功能的瀏覽器。不久之後 (具體來說是同一天),WhatWG 決定捨棄 <device> 標記,改用另一個新興的標記,這次是名為 navigator.getUserMedia() 的 JavaScript API。一週後,Opera 推出了新版本,其中包含支援更新版 getUserMedia() 規格的功能。同年稍晚,微軟也加入了這個行列,發布了支援新規格的 IE9 實驗室

<device> 的內容如下所示:

<device type="media" onchange="update(this.data)"></device>
<video autoplay></video>
<script>
  function update(stream) {
    document.querySelector('video').src = stream.url;
  }
</script>

支援:

很抱歉,目前尚未有任何已發布瀏覽器包含 <device>。我想您不用再擔心 API 了 :) 不過,<device> 有兩個優點:1. 它是語意,2. 它很容易擴充,可支援的裝置不只限於音訊/影像裝置。

深呼吸。這類內容變化迅速!

第 3 回合:WebRTC

<device> 元素最終也走向滅絕之路。

由於 WebRTC (網頁即時通訊) 的規模更大,因此我們更快找到合適的擷取 API。該規格由 W3C WebRTC 工作小組監督。Google、Opera、Mozilla 和其他幾家公司也已導入這項功能。

getUserMedia() 與 WebRTC 相關,因為它是進入該組 API 的閘道。這項權限可讓應用程式存取使用者的本機相機/麥克風串流。

支援:

getUserMedia() 自 Chrome 21、Opera 18 和 Firefox 17 起便已受支援。

開始使用

有了 navigator.mediaDevices.getUserMedia(),我們終於可以不必使用外掛程式,就能存取網路攝影機和麥克風輸入內容。相機存取權現在只需呼叫即可取得,而非安裝應用程式。並直接嵌入瀏覽器。你是否躍躍欲試了呢?

特徵偵測

特徵偵測功能可簡單檢查 navigator.mediaDevices.getUserMedia 是否存在:

if (navigator.mediaDevices?.getUserMedia) {
  // Good to go!
} else {
  alert("navigator.mediaDevices.getUserMedia() is not supported");
}

取得輸入裝置的存取權

如要使用網路攝影機或麥克風,我們需要取得權限。navigator.mediaDevices.getUserMedia() 的第一個參數是一個物件,可指定您要存取的每種媒體類型的詳細資料和需求。舉例來說,如果您想存取網路攝影機,第一個參數應為 {video: true}。如要同時使用麥克風和攝影機,請傳送 {video: true, audio: true}

<video autoplay></video>

<script>
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: true })
    .then((localMediaStream) => {
      const video = document.querySelector("video");
      video.srcObject = localMediaStream;
    })
    .catch((error) => {
      console.log("Rejected!", error);
    });
</script>

好的。那麼,發生了什麼事?媒體擷取功能是新 HTML5 API 搭配使用的絕佳範例。它可與其他 HTML5 好友 <audio><video> 搭配使用。請注意,我們並未在 <video> 元素上設定 src 屬性或加入 <source> 元素。我們並未將影片網址提供給媒體檔案,而是將 srcObject 設為代表網路攝影機的 LocalMediaStream 物件。

我也會將 <video> 傳送至 autoplay,否則會在第一個影格凍結。新增 controls 也能正常運作。

設定媒體限制條件 (解析度、高度、寬度)

getUserMedia() 的第一個參數也可以用來指定傳回媒體串流的更多需求 (或限制)。舉例來說,您可以除了要求基本影片存取權 (例如 {video: true}) 外,還要求串流必須是 HD 畫質:

const hdConstraints = {
  video: { width: { exact:  1280} , height: { exact: 720 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);
const vgaConstraints = {
  video: { width: { exact:  640} , height: { exact: 360 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);

如需其他設定,請參閱 constraints API

選取媒體來源

MediaDevices 介面的 enumerateDevices() 方法會要求可用媒體輸入和輸出裝置的清單,例如麥克風、相機、耳機等。系統會使用描述裝置的 MediaDeviceInfo 物件陣列解析傳回的 Promise。

在這個範例中,系統會選取最近找到的麥克風和相機做為媒體串流來源:

if (!navigator.mediaDevices?.enumerateDevices) {
  console.log("enumerateDevices() not supported.");
} else {
  // List cameras and microphones.
  navigator.mediaDevices
    .enumerateDevices()
    .then((devices) => {
      let audioSource = null;
      let videoSource = null;

      devices.forEach((device) => {
        if (device.kind === "audioinput") {
          audioSource = device.deviceId;
        } else if (device.kind === "videoinput") {
          videoSource = device.deviceId;
        }
      });
      sourceSelected(audioSource, videoSource);
    })
    .catch((err) => {
      console.error(`${err.name}: ${err.message}`);
    });
}

async function sourceSelected(audioSource, videoSource) {
  const constraints = {
    audio: { deviceId: audioSource },
    video: { deviceId: videoSource },
  };
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
}

請參閱 Sam Dutton 的精彩示範,瞭解如何讓使用者選取媒體來源。

安全性

瀏覽器會在呼叫 navigator.mediaDevices.getUserMedia() 時顯示權限對話方塊,讓使用者選擇是否授予或拒絕相機/麥克風存取權。例如,以下是 Chrome 的權限對話方塊:

Chrome 中的權限對話方塊
Chrome 中的權限對話方塊

提供備用項

如果使用者不支援 navigator.mediaDevices.getUserMedia(),如果 API 不支援,且/或呼叫因某些原因失敗,則可以改用現有的影片檔案:

if (!navigator.mediaDevices?.getUserMedia) {
  video.src = "fallbackvideo.webm";
} else {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  video.srcObject = stream;
}