getUserMedia()方法

getUserMedia()方法用于提示用户授权从媒体设备中采集数据,并以MediaStream(包含所请求轨道的MediaStreamTrack)对象返回。

语法:

mediaDevices.getUserMedia(constraints);

getUserMedia()方法返回 Promise,如果用户拒绝授权,则会被拒绝(reject)。

constraints定义了约束条件,浏览器会尝试给出满足条件的媒体流,若系统中的所有设备都不满足条件,则返回的 Promise 会被拒绝。

返回的 Promise 也可能永久处于待定(pending)状态,例如用户忽略授权提醒,注意这不会导致页面交互的阻塞。

constraints 选项

constraints是一个选项对象,可以包含属性audiovideo,分别代表音频和视频轨道的约束条件,默认值为false。除此之外,取值还可以是trueMediaTrackConstraints对象。

当取值为true时,浏览器通常会从系统设置或当前源设置的默认设备获取轨道。前面章节中出现的例子就属于这种用法。

至于使用MediaTrackConstraints对象来表示约束,我们先来看几个例子。

constraints 示例

假设我们想获取只包含视频轨的流,分辨率优先选择 1280x720,那么可以像如下方式调用:

getUserMedia({
  video: {
    width: 1280,
    height: 720,
  },
});

某些情况下,上述调用参数相当于:

getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
  },
});

对于用ideal指定的参数,浏览器会尽可能满足,如果无法满足,也会尝试其它尽可能接近的方案。

如果我们要求视频分辨率至少需要达到 640x480,可以用min来指定:

getUserMedia({
  video: {
    width: { min: 640 },
    height: { min: 480 },
  },
});

也可以指定理想情况下的分辨率,如无法满足,在指定的范围内寻找硬件能支持的最接近的指标。通过minmax限定范围:

getUserMedia({
  video: {
    width: { min: 640, ideal: 1280, max: 1920 },
    height: { min: 480, ideal: 720, max: 1080 },
  },
});

如我们希望获取精确指定的分辨率,如 1280x720,可以使用exact关键字:

getUserMedia({
  video: {
    width: { exact: 1280 },
    height: { exact: 720 },
  },
});

对于移动设备,某些场景可能需要强制使用前置摄像头(例如视频会议、身份验证等),指定facingModeuser即可:

getUserMedia({
  video: {
    facingMode: { exact: "user" },
  },
});

可以组合使用多个条件来增加约束,例如优先使用后置摄像头,分辨率至少 320x240:

getUserMedia({
  video: {
    facingMode: { ideal: "environment" },
    width: { min: 320 },
    height: { min: 240 },
  },
});

通常在 UI 界面会允许用户在设备列表中(通过enumerateDevices()获取)选择要使用的设备,一旦用户做出选择,获得对应设备的deviceId后,可以按deviceId来获取媒体轨道:

getUserMedia({
  video: {
    deviceId: { exact: "xxxxxxxx" },
  },
});

如需验证所获取的流是否满足条件,可以调用MediaStreamTrack.getSettings()查看当前轨道的设置:

const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: 320,
    height: 240,
  },
});
stream.getTracks()[0].getSettings(); /*
{
    "aspectRatio": 1.3333333333333333,
    "brightness": 128,
    "colorTemperature": 4000,
    "contrast": 128,
    "deviceId": "ea3ff7be276fe47df7a03f80edb081a71bc3f2f14ce1bd88dc568c555f679f16",
    "focusDistance": 0,
    "focusMode": "continuous",
    "frameRate": 30,
    "groupId": "fa6bdabb020c0cc2d3f6aa480aa870fa028e56ada1ac56933f1648bf9f383d8f",
    "height": 240,
    "resizeMode": "crop-and-scale",
    "saturation": 128,
    "sharpness": 128,
    "whiteBalanceMode": "continuous",
    "width": 320
}
*/

constraints 语法

从上例中可以看到,每个参数都可以直接指定约束值,或用ideal指定约束值。浏览器尝试获取满足条件的媒体流,若无法满足,会寻找最接近匹配的方案。

如需指定精确匹配的条件,使用exact关键字来约束条件。

对于数值型约束,还可以指定minmax来限制所允许的取值范围。

常用音视频 constraints 约束

通用约束

deviceId用于指定设备 ID,string 类型。

groupId用于指定设备所属组 ID,string 类型。

音频轨约束

autoGainControl用于指定是否启用音频自动增益,boolean 类型,默认值为true,也即{ideal:true}

echoCancellation用于指定是否启用音频回声消除,boolean 类型,默认值为true。对避免音频形成环路很有帮助。例如对着麦克风说话,声音又从音箱回放出来,再次进入麦克风形成循环;也可以是网络通话场景,对方接收到音频后回放出来,并再次通过对方的麦克风回到本地并播放出来,形成循环。

noiseSuppression用于指定是否启噪声抑制,即通常说的降噪,boolean 类型,默认值为true

channelCount用于指定声道数,例如1为单声道,2为双声道。

sampleRate用于指定采样频率,数值类型,例如48000,即 48kHz。

sampleSize用于指定采样位数,即采样精度,数值类型,例如16,即每个声道单次采样精度为 16bit。

视频轨约束

width用于指定分辨率宽度,数值类型。

height用于指定分辨率高度,数值类型。

aspectRatio用于指定宽高比,数值类型,对于 16:9 的视频而言,值为1.7777777778

resizeMode指定浏览器从设备获取轨道原始采样后,使用何种调整方式以满足分辨率约束。设为crop-and-scale允许浏览器通过裁剪和缩放来调整分辨率;设为none表示不调整。

frameRate用于指定采样帧率,数值类型。

facingMode选择某种朝向的摄像头,可以是user朝向用户即前置,environment朝向周围环境即后置。如果设备支持,也可以设置leftright选择在用户左侧或右侧的摄像头。

示例:通过摄像头保存自拍照

现在,我们演示一下如何通过摄像头抓取一帧画面,并将其保存为图片,以此实现保存自拍照的功能。总体流程如下:

  1. 调用getuserMedia()获取流
  2. HTMLVideoElement上播放流
  3. 点击按钮后,在 Offscreen Canvas Context 上调用drawImage(video)将当前一帧绘制到画布
  4. 调用canvas.convertToBlob()转化为Blob对象
  5. 创建超链接指向 Blob 并触发下载

页面基本 HTML 结构如下:

<body>
  <video muted playsinline></video>
  <button>Download</button>
</body>

尝试获取视频流,采用竖屏模式,尽可能使用前置摄像头,然后用video来播放:

const video = document.querySelector("video");

(async () => {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: {
      aspectRatio: 9 / 16,
      facingMode: "user",
    },
  });
  video.srcObject = stream;
  await video.play();
})();

执行上述代码后,可以发现当摄像头正对人脸时,从屏幕观察到的水平方向和实际方向是相反的,例如伸出左手,在屏幕上可以看到手从右边伸出。一般应对这种情况的方案是使画面水平翻转,也就是镜像(mirror)模式,就好象在照镜子一样。我们使用 CSS 来水平翻转video元素:

video {
  transform: scaleX(-1);
}

await video.play()之后,可以得到视频轨实际的分辨率,用它来构造OffscreenCanvas

const width = video.videoWidth;
const height = video.videoHeight;

const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext("2d");

虽然通过 CSS 将video元素进行了水平翻转,但那只是视觉上的效果,调用context.drawImage()后得到的图像依然是翻转前的。为了解决该问题,我们需要对 Canvas 坐标系执行类似的翻转。由于翻转是以原点作为中心点的,翻转后 x 轴指向了 canvas 左方且在画布左边界之外,我们再通过平移坐标系将需要的宽度移动到可见区域内:

context.scale(-1, 1);
context.translate(-width, 0);

当用户点击按钮时,就可以将视频帧绘制到 Canvas 上,导出并下载:

const button = document.querySelector("button");
button.addEventListener("click", async () => {
  context.clearRect(0, 0, width, height);
  context.drawImage(video, 0, 0);
  const blob = await canvas.convertToBlob({ type: "image/png" });
  const url = URL.createObjectURL(blob);

  const link = document.createElement("a");
  link.download = "portrait.png";
  link.href = url;
  link.click();
});