功能、约束和设置

本文讨论了**约束**和**功能**这两个概念,以及媒体设置,并包含一个我们称为约束练习器的示例。约束练习器允许您试验将不同的约束集应用于来自计算机 A/V 输入设备(如网络摄像头和麦克风)的音频和视频轨道的结果。

从历史上看,编写与 Web API 密切相关的 Web 脚本存在一个众所周知的挑战:通常,您的代码需要知道 API 是否存在,如果存在,它在正在运行的用户代理上的限制是什么。弄清楚这一点通常很困难,并且通常涉及查看正在运行的用户代理(或浏览器)的组合、其版本、查看某些对象是否存在、尝试查看各种内容是否有效或无效以及确定发生的错误等等。结果是产生了大量非常脆弱的代码,或者依赖于为您解决这些问题的库,然后实现polyfill来代表您修复实现中的漏洞。

功能和约束允许浏览器和网站或应用程序交换有关浏览器实现支持的**可约束属性**以及每个属性支持的值的信息。

概述

该过程的工作原理如下(以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()以获取所有受支持功能以及每个功能在当前平台和用户代理上接受的值或值的范围的列表。此函数返回一个对象,该对象列出浏览器支持的每个可约束属性以及每个属性支持的值或值的范围。

**注意:**并非所有主要浏览器都已实现 getCapabilities()。目前,您必须尝试获取所需内容,如果无法获取,请在此时决定该怎么做。例如,请参阅 Firefox Firefox 错误 1179084

应用约束

使用约束的第一种也是最常见的方法是在调用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(() => {
      return new Promise((resolve) => {
        videoElement.onloadedmetadata = resolve;
      });
    })
    .then(() => {
      getCurrentSettings();
    })
    .catch(handleError);
}

这里有几个步骤

  1. 它调用 buildConstraints() 从编辑框中的代码创建两个轨道的 MediaTrackConstraints 对象。
  2. 它调用 navigator.mediaDevices.getUserMedia(),传入视频和音频轨道的约束条件对象。这将返回一个 MediaStream,其中包含来自与输入匹配的源(通常是网络摄像头,但如果您提供正确的约束条件,则可以从其他源获取媒体)的音频和视频。
  3. 获取流后,将其附加到 <video> 元素,以便在屏幕上可见,并且我们将音频轨道和视频轨道抓取到变量 audioTrackvideoTrack 中。
  4. 然后,我们设置一个 promise,该 promise 在视频元素上发生 loadedmetadata 事件时解析。
  5. 发生这种情况时,我们知道视频已开始播放,因此我们调用 getCurrentSettings() 函数(如上所述)以显示浏览器在考虑我们的约束条件和硬件功能后确定的实际设置。
  6. 如果发生错误,我们将使用稍后将在本文中介绍的 handleError() 方法记录它。

我们还需要设置一个事件侦听器以监视“开始视频”按钮是否被点击

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

应用约束条件集更新

接下来,我们为“应用约束条件”按钮设置一个事件侦听器。如果单击它并且没有正在使用的媒体,我们将调用 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);
      }
    }
  },
  false,
);

处理停止按钮

然后,我们设置停止按钮的处理程序。

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> 元素中删除流。

编辑器中的简单制表符支持

此代码通过在任一约束条件编辑框获得焦点时使制表符键插入两个空格字符,为 <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, false);
audioConstraintEditor.addEventListener("keydown", keyDownHandler, false);

显示浏览器支持的可约束属性

拼图的最后一块重要内容:代码,用于向用户显示其浏览器支持的可约束属性列表以供参考。每个属性都是指向 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

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅