功能、約束和設定

Baseline 已廣泛支援

此功能已成熟,可跨多種裝置和瀏覽器版本使用。自 2017 年 9 月以來,它已在瀏覽器中提供。

本文討論了約束功能這兩個概念,以及媒體設定,幷包含一個我們稱之為約束練習器的示例。約束練習器允許您透過對來自計算機 A/V 輸入裝置(例如其攝像頭和麥克風)的音訊和影片軌道應用不同的約束集來實驗結果。

從歷史上看,為 Web 編寫與 Web API 密切協作的指令碼一直存在一個眾所周知的挑戰:通常,您的程式碼需要知道 API 是否存在,如果存在,它在執行的使用者代理上的限制是什麼。弄清楚這一點通常很困難,並且通常涉及檢視正在執行的使用者代理(或瀏覽器)的組合、其版本、檢視某些物件是否存在、嘗試檢視各種功能是否起作用以及確定發生什麼錯誤等等。結果是產生了大量非常脆弱的程式碼,或者依賴於為您解決這些問題的庫,然後實現polyfills來為您修補實現中的漏洞。

功能和約束允許瀏覽器和網站或應用程式交換有關瀏覽器實現支援哪些可約束屬性以及它支援每個屬性的哪些值的資訊。

概述

該過程如下(以MediaStreamTrack為例)

  1. 如果需要,呼叫MediaDevices.getSupportedConstraints()以獲取支援的約束列表,該列表告訴您瀏覽器知道哪些可約束屬性。這並不總是必要的,因為任何未知的屬性在您指定它們時都將被忽略——但如果您有任何無法離開的屬性,您可以首先檢查以確保它們在列表中。
  2. 一旦指令碼知道它希望使用的屬性是否受支援,它就可以透過檢查軌道getCapabilities()方法返回的物件來檢查 API 及其實現的功能;此物件列出了每個受支援的約束以及受支援的值或值範圍。
  3. 最後,呼叫軌道的applyConstraints()方法,透過指定它希望對任何它有偏好的可約束屬性使用的值或值範圍,按照需要配置 API。
  4. 軌道的getConstraints()方法返回傳遞給最近一次applyConstraints()呼叫的約束集。這可能無法表示軌道的實際當前狀態,因為請求的值必須進行調整,並且未表示平臺預設值。要完整表示軌道的當前配置,請使用getSettings()

在媒體捕獲和流 API 中,MediaStreamMediaStreamTrack都具有可約束屬性。

確定是否支援約束

如果您需要知道使用者代理是否支援給定約束,您可以透過呼叫navigator.mediaDevices.getSupportedConstraints()來獲取瀏覽器知道的可約束屬性列表,如下所示

js
const supported = navigator.mediaDevices.getSupportedConstraints();

document.getElementById("frameRateSlider").disabled = !supported["frameRate"];

在此示例中,獲取支援的約束,並且如果不支援frameRate約束,則停用允許使用者配置幀速率的控制元件。

約束的定義方式

單個約束是一個物件,其名稱與正在指定其所需值或值範圍的可約束屬性匹配。此物件包含零個或多個單獨的約束,以及一個可選的名為advanced的子物件,該子物件包含使用者代理必須儘可能滿足的另一組零個或多個約束。使用者代理嘗試按照約束集中指定的順序滿足約束。

最重要的是要理解,大多數約束不是要求;相反,它們是請求。也有例外,我們很快就會講到。

請求設定的特定值

大多數情況下,每個約束可能是一個特定值,表示設定的期望值。例如

js
const constraints = {
  width: 1920,
  height: 1080,
  aspectRatio: 1.777777778,
};

myTrack.applyConstraints(constraints);

在這種情況下,約束表明幾乎所有屬性的值都可以,但需要標準高畫質 (HD) 影片尺寸,以及標準 16:9 的寬高比。不能保證最終的軌道會匹配其中任何一個,但使用者代理應該盡力匹配儘可能多的。

屬性的優先順序很簡單:如果兩個屬性的請求值相互排斥,則將使用約束集中首先列出的那個。例如,如果執行上述程式碼的瀏覽器無法提供 1920x1080 的軌道,但可以提供 1920x900 的軌道,那麼就會提供後者。

像這樣指定單個值的簡單約束總是被視為非必需的。使用者代理會嘗試提供您請求的內容,但不能保證您獲得的內容會匹配。但是,如果您在呼叫MediaStreamTrack.applyConstraints()時使用屬性的簡單值,則請求將始終成功,因為這些值將被視為請求,而不是要求。

指定值範圍

有時,屬性值在某個範圍內都是可以接受的。您可以指定具有最小值和/或最大值的範圍,如果您選擇,甚至可以指定範圍內的理想值。如果您提供一個理想值,瀏覽器將嘗試在給定其他指定約束的情況下,儘可能接近該值。

js
const supports = navigator.mediaDevices.getSupportedConstraints();

if (
  !supports["width"] ||
  !supports["height"] ||
  !supports["frameRate"] ||
  !supports["facingMode"]
) {
  // We're missing needed properties, so handle that error.
} else {
  const constraints = {
    width: { min: 640, ideal: 1920, max: 1920 },
    height: { min: 400, ideal: 1080 },
    aspectRatio: 1.777777778,
    frameRate: { max: 30 },
    facingMode: { exact: "user" },
  };

  myTrack
    .applyConstraints(constraints)
    .then(() => {
      /* do stuff if constraints applied successfully */
    })
    .catch((reason) => {
      /* failed to apply constraints; reason is why */
    });
}

在這裡,在確保必須找到匹配的可約束屬性(widthheightframeRatefacingMode)受支援後,我們設定了約束,要求寬度不小於 640 且不大於 1920(但最好是 1920),高度不小於 400(但理想是 1080),寬高比為 16:9(1.777777778),幀率不大於每秒 30 幀。此外,唯一可接受的輸入裝置是面向使用者的攝像頭(“自拍相機”)。如果無法滿足widthheightframeRatefacingMode約束,則applyConstraints()返回的 Promise 將被拒絕。

注意:使用任何或所有maxminexact指定的約束總是被視為強制性的。如果在呼叫applyConstraints()時無法滿足使用其中一個或多個的任何約束,則 Promise 將被拒絕。

高階約束

所謂高階約束是透過向約束集新增一個advanced屬性來建立的;此屬性的值是一個附加約束集陣列,這些約束集被認為是可選的。此功能幾乎沒有用例,並且有人對將其從規範中刪除感興趣,因此此處將不討論它。如果您希望瞭解更多資訊,請參閱媒體捕獲和流規範的第 11 節,在示例 2 之後。

檢查功能

您可以呼叫MediaStreamTrack.getCapabilities()來獲取當前平臺和使用者代理支援的所有功能及其接受的值或值範圍的列表。此函式返回一個物件,該物件列出了瀏覽器支援的每個可約束屬性以及每個屬性支援的值或值範圍。

例如,以下程式碼片段將導致使用者被要求授予訪問其本地攝像頭和麥克風的許可權。一旦授予許可權,MediaTrackCapabilities物件將被記錄到控制檯,詳細說明每個MediaStreamTrack的功能

js
navigator.mediaDevices
  .getUserMedia({ video: true, audio: true })
  .then((stream) => {
    const tracks = stream.getTracks();
    tracks.map((t) => console.log(t.getCapabilities()));
  });

一個示例功能物件如下所示

json
{
  "autoGainControl": [true, false],
  "channelCount": {
    "max": 1,
    "min": 1
  },
  "deviceId": "jjxEMqxIhGdryqbTjDrXPWrkjy55Vte70kWpMe3Lge8=",
  "echoCancellation": [true, false],
  "groupId": "o2tZiEj4MwOdG/LW3HwkjpLm1D8URat4C5kt742xrVQ=",
  "noiseSuppression": [true, false]
}

物件的具體內容將取決於瀏覽器和媒體硬體。

應用約束

使用約束的第一種也是最常見的方法是在呼叫getUserMedia()時指定它們

js
navigator.mediaDevices
  .getUserMedia({
    video: {
      width: { min: 640, ideal: 1920 },
      height: { min: 400, ideal: 1080 },
      aspectRatio: { ideal: 1.7777777778 },
    },
    audio: {
      sampleSize: 16,
      channelCount: 2,
    },
  })
  .then((stream) => {
    videoElement.srcObject = stream;
  })
  .catch(handleError);

在此示例中,在getUserMedia()時應用約束,要求一組理想的影片選項,並帶有回退。

注意:您可以指定一個或多個媒體輸入裝置 ID,以對允許的輸入源設定限制。要收集可用裝置列表,您可以呼叫navigator.mediaDevices.enumerateDevices(),然後對於每個符合所需條件的裝置,將其deviceId新增到最終傳遞給getUserMedia()MediaConstraints物件中。

您還可以透過呼叫軌道的applyConstraints()方法,動態更改現有MediaStreamTrack的約束,並傳入一個表示您希望應用於軌道的約束的物件

js
videoTrack.applyConstraints({
  width: 1920,
  height: 1080,
});

在此程式碼片段中,videoTrack引用的影片軌道將更新,使其解析度儘可能接近 1920x1080 畫素(1080p 高畫質)。

檢索當前約束和設定

記住約束設定之間的區別很重要。約束是指定您需要、想要和願意接受各種可約束屬性(如MediaTrackConstraints文件中所述)的值的方式,而設定是當前每個可約束屬性的實際值。

獲取生效的約束

如果任何時候您需要獲取當前應用於媒體的約束集,您可以透過呼叫MediaStreamTrack.getConstraints()來獲取該資訊,如下例所示。

js
function switchCameras(track, camera) {
  const constraints = track.getConstraints();
  constraints.facingMode = camera;
  track.applyConstraints(constraints);
}

此函式接受一個MediaStreamTrack和一個指示要使用的攝像頭朝向模式的字串,獲取當前約束,將MediaTrackConstraints.facingMode的值設定為指定值,然後應用更新後的約束集。

獲取軌道的當前設定

除非您只使用精確約束(這限制性很強,所以請確保您是認真的!),否則不能保證在應用約束後您實際會得到什麼。可約束屬性在結果媒體中的實際值稱為設定。如果您需要知道媒體的真實格式和其他屬性,可以透過呼叫MediaStreamTrack.getSettings()來獲取這些設定。這會返回一個基於字典MediaTrackSettings的物件。例如

js
function whichCamera(track) {
  return track.getSettings().facingMode;
}

此函式使用getSettings()來獲取軌道當前使用的可約束屬性值,並返回facingMode的值。

示例:約束練習器

在此示例中,我們建立一個練習器,您可以透過編輯描述音訊和影片軌道約束集的原始碼來實驗媒體約束。然後,您可以應用這些更改並檢視結果,包括流的外觀以及應用新約束後實際的媒體設定。

此示例的 HTML 和 CSS 非常簡單,此處不顯示。您可以點選“播放”在演練場中檢視完整程式碼。

預設值和變數

首先是預設約束集,以字串形式表示。這些字串顯示在可編輯的<textarea>中,但這是流的初始配置。

js
const videoDefaultConstraintString =
  '{\n  "width": 320,\n  "height": 240,\n  "frameRate": 30\n}';
const audioDefaultConstraintString =
  '{\n  "sampleSize": 16,\n  "channelCount": 2,\n  "echoCancellation": false\n}';

這些預設值要求一個相當常見的攝像頭配置,但並不強調任何屬性的重要性。瀏覽器應盡力匹配這些設定,但會滿足其認為接近匹配的任何設定。

然後我們將用於儲存影片和音訊軌道的MediaTrackConstraints物件的變數,以及用於儲存影片和音訊軌道本身引用的變數初始化為null

js
let videoConstraints = null;
let audioConstraints = null;

let audioTrack = null;
let videoTrack = null;

我們獲取所有需要訪問的元素的引用。

js
const videoElement = document.getElementById("video");
const logElement = document.getElementById("log");
const supportedConstraintList = document.getElementById("supportedConstraints");
const videoConstraintEditor = document.getElementById("videoConstraintEditor");
const audioConstraintEditor = document.getElementById("audioConstraintEditor");
const videoSettingsText = document.getElementById("videoSettingsText");
const audioSettingsText = document.getElementById("audioSettingsText");

這些元素是

videoElement

將顯示流的<video>元素。

logElement

一個<div>,所有錯誤訊息或其他日誌型別輸出都將寫入其中。

supportedConstraintList

一個<ul>(無序列表),我們以程式設計方式將使用者瀏覽器支援的每個可約束屬性的名稱新增到其中。

videoConstraintEditor

一個<textarea>元素,允許使用者編輯影片軌道約束集的程式碼。

audioConstraintEditor

一個<textarea>元素,允許使用者編輯音訊軌道約束集的程式碼。

videoSettingsText

一個<textarea>(始終停用),顯示影片軌道可約束屬性的當前設定。

audioSettingsText

一個<textarea>(始終停用),顯示音訊軌道可約束屬性的當前設定。

最後,我們將兩個約束集編輯器元素的當前內容設定為預設值。

js
videoConstraintEditor.value = videoDefaultConstraintString;
audioConstraintEditor.value = audioDefaultConstraintString;

更新設定顯示

在每個約束集編輯器的右側是第二個文字框,我們用它來顯示軌道可配置屬性的當前配置。此顯示由getCurrentSettings()函式更新,該函式獲取音訊和影片軌道的當前設定,並透過設定其value將相應的程式碼插入到軌道的設定顯示框中。

js
function getCurrentSettings() {
  if (videoTrack) {
    videoSettingsText.value = JSON.stringify(videoTrack.getSettings(), null, 2);
  }

  if (audioTrack) {
    audioSettingsText.value = JSON.stringify(audioTrack.getSettings(), null, 2);
  }
}

流首次啟動後以及每次應用更新的約束後都會呼叫此函式,如下所示。

構建軌道約束集物件

buildConstraints()函式使用兩個軌道的約束集編輯框中的程式碼為音訊和影片軌道構建MediaTrackConstraints物件。

js
function buildConstraints() {
  try {
    videoConstraints = JSON.parse(videoConstraintEditor.value);
    audioConstraints = JSON.parse(audioConstraintEditor.value);
  } catch (error) {
    handleError(error);
  }
}

這使用JSON.parse()將每個編輯器中的程式碼解析為一個物件。如果任何對 JSON.parse() 的呼叫丟擲異常,則呼叫handleError()將錯誤訊息輸出到日誌。

配置並啟動流

startVideo()方法處理影片流的設定和啟動。

js
function startVideo() {
  buildConstraints();

  navigator.mediaDevices
    .getUserMedia({
      video: videoConstraints,
      audio: audioConstraints,
    })
    .then((stream) => {
      const audioTracks = stream.getAudioTracks();
      const videoTracks = stream.getVideoTracks();

      videoElement.srcObject = stream;

      if (audioTracks.length > 0) {
        audioTrack = audioTracks[0];
      }

      if (videoTracks.length > 0) {
        videoTrack = videoTracks[0];
      }
    })
    .then(
      () =>
        new Promise((resolve) => {
          videoElement.onloadedmetadata = resolve;
        }),
    )
    .then(() => {
      getCurrentSettings();
    })
    .catch(handleError);
}

這裡有幾個步驟

  1. 它呼叫buildConstraints(),從編輯框中的程式碼為兩個軌道建立MediaTrackConstraints物件。
  2. 它呼叫navigator.mediaDevices.getUserMedia(),並傳入影片和音訊軌道的約束物件。這會返回一個MediaStream,其中包含來自與輸入匹配的源(通常是網路攝像頭,儘管如果您提供正確的約束,您可以從其他源獲取媒體)的音訊和影片。
  3. 獲取流後,將其附加到<video>元素,使其在螢幕上可見,並將音訊軌道和影片軌道獲取到變數audioTrackvideoTrack中。
  4. 然後我們設定一個 Promise,當影片元素上發生loadedmetadata事件時,該 Promise 將解析。
  5. 發生這種情況時,我們知道影片已經開始播放,因此我們呼叫getCurrentSettings()函式(如上所述)來顯示瀏覽器在考慮我們的約束和硬體功能後確定的實際設定。
  6. 如果發生錯誤,我們使用本文稍後將介紹的handleError()方法將其記錄下來。

我們還需要設定一個事件監聽器來監聽“開始影片”按鈕的點選

js
document.getElementById("startButton").addEventListener("click", () => {
  startVideo();
});

應用約束集更新

接下來,我們為“應用約束”按鈕設定事件監聽器。如果它被點選並且沒有正在使用的媒體,我們呼叫startVideo(),並讓該函式處理以指定的設定啟動流。否則,我們按照以下步驟將更新的約束應用於已經啟用的流

  1. 呼叫buildConstraints()為音訊軌道(audioConstraints)和影片軌道(videoConstraints)構造更新的MediaTrackConstraints物件。
  2. 在影片軌道(如果存在)上呼叫MediaStreamTrack.applyConstraints()以應用新的videoConstraints。如果成功,影片軌道的當前設定框的內容將根據呼叫其getSettings()方法的結果進行更新。
  3. 完成此操作後,在音訊軌道(如果存在)上呼叫applyConstraints()以應用新的音訊約束。如果成功,音訊軌道的當前設定框的內容將根據呼叫其getSettings()方法的結果進行更新。
  4. 如果應用任何一組約束時發生錯誤,則使用handleError()將訊息輸出到日誌中。
js
document.getElementById("applyButton").addEventListener("click", () => {
  if (!videoTrack && !audioTrack) {
    startVideo();
  } else {
    buildConstraints();

    const prettyJson = (obj) => JSON.stringify(obj, null, 2);

    if (videoTrack) {
      videoTrack
        .applyConstraints(videoConstraints)
        .then(() => {
          videoSettingsText.value = prettyJson(videoTrack.getSettings());
        })
        .catch(handleError);
    }

    if (audioTrack) {
      audioTrack
        .applyConstraints(audioConstraints)
        .then(() => {
          audioSettingsText.value = prettyJson(audioTrack.getSettings());
        })
        .catch(handleError);
    }
  }
});

處理停止按鈕

然後我們設定停止按鈕的處理程式。

js
document.getElementById("stopButton").addEventListener("click", () => {
  if (videoTrack) {
    videoTrack.stop();
  }

  if (audioTrack) {
    audioTrack.stop();
  }

  videoTrack = audioTrack = null;
  videoElement.srcObject = null;
});

這會停止活動軌道,將videoTrackaudioTrack變數設定為null,以便我們知道它們已停止,並透過將HTMLMediaElement.srcObject設定為null,從<video>元素中移除流。

編輯器中的簡單選項卡支援

此程式碼透過在任何約束編輯框獲得焦點時,使 Tab 鍵插入兩個空格字元,從而為<textarea>元素添加了簡單的選項卡支援。

js
function keyDownHandler(event) {
  if (event.key === "Tab") {
    const elem = event.target;
    const str = elem.value;

    const position = elem.selectionStart;
    const beforeTab = str.substring(0, position);
    const afterTab = str.substring(position, str.length);
    const newStr = `${beforeTab}  ${afterTab}`;
    elem.value = newStr;
    elem.selectionStart = elem.selectionEnd = position + 2;
    event.preventDefault();
  }
}

videoConstraintEditor.addEventListener("keydown", keyDownHandler);
audioConstraintEditor.addEventListener("keydown", keyDownHandler);

顯示瀏覽器支援的可約束屬性

最後一個重要部分:為使用者參考顯示其瀏覽器支援的可約束屬性列表的程式碼。每個屬性都是指向其 MDN 文件的連結,以方便使用者。有關此程式碼工作原理的詳細資訊,請參閱MediaDevices.getSupportedConstraints()示例

注意:當然,此列表中可能存在非標準屬性,在這種情況下,您可能會發現文件連結並沒有太大幫助。

js
const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
for (const constraint in supportedConstraints) {
  if (Object.hasOwn(supportedConstraints, constraint)) {
    const elem = document.createElement("li");

    elem.innerHTML = `<code><a href='https://mdn.club.tw/docs/Web/API/MediaTrackSupportedConstraints/${constraint}' target='_blank'>${constraint}</a></code>`;
    supportedConstraintList.appendChild(elem);
  }
}

錯誤處理

我們還有一些簡單的錯誤處理程式碼;handleError()用於處理失敗的 Promise,log()函式將錯誤訊息附加到影片下方的一個特殊日誌<div>框中。

js
function log(msg) {
  logElement.innerHTML += `${msg}<br>`;
}

function handleError(reason) {
  log(
    `Error <code>${reason.name}</code> in constraint <code>${reason.constraint}</code>: ${reason.message}`,
  );
}

結果

在這裡您可以看到完整的示例。

規範

規範
媒體捕獲和流
# dom-mediadevices-getsupportedconstraints

瀏覽器相容性

另見