功能、约束和设置

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.org.cn/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

浏览器兼容性

另见