使用 getUserMedia() 拍摄静态照片

本文档展示了如何使用 navigator.mediaDevices.getUserMedia() 来访问支持 getUserMedia() 的电脑或手机上的摄像头并用它拍照。

getUserMedia-based image capture app — on the left we have a video stream taken from a webcam and a take photo button, on the right we have the still image output from taking the photo

您也可以直接跳转到 演示

HTML 标记

我们的 HTML 界面有两个主要的操作部分:流和捕获面板,以及展示面板。其中每一部分都并排呈现在各自的 <div> 中,以便于样式设置和控制。我们有一个 <button> 元素(permissions-button),稍后可以在 JavaScript 中使用它来允许用户通过 getUserMedia() 针对每个设备允许或阻止摄像头权限。

左侧的框包含两个组件:一个 <video> 元素,它将接收来自 navigator.mediaDevices.getUserMedia() 的流,以及一个 <button> 来开始视频捕获。这很简单,当我们进入 JavaScript 代码时,我们会看到它是如何组合在一起的。

html
<div class="camera">
  <video id="video">Video stream not available.</video>
  <button id="start-button">Capture photo</button>
</div>

接下来,我们有一个 <canvas> 元素,捕获的帧将存储在此元素中,可能以某种方式进行处理,然后转换为输出图像文件。这个 canvas 通过使用 display: none 进行样式设置而被隐藏,以避免屏幕混乱——用户不需要看到这个中间阶段。

我们还有一个 <img> 元素,我们将在此元素中绘制图像——这是显示给用户的最终图像。

html
<canvas id="canvas"></canvas>
<div class="output">
  <img id="photo" alt="The screen capture will appear in this box." />
</div>

JavaScript 代码

现在让我们看一下 JavaScript 代码。我们将它分解成几个小块,以便更容易解释。

初始化

我们首先设置将要使用的各种变量。

js
const width = 320; // We will scale the photo width to this
let height = 0; // This will be computed based on the input stream

let streaming = false;

const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const photo = document.getElementById("photo");
const startButton = document.getElementById("start-button");
const allowButton = document.getElementById("permissions-button");

这些变量是

width

无论传入视频的大小如何,我们将把生成的图像缩放到宽度为 320 像素。

height

输出图像的高度将根据 width 和流的 纵横比 来计算。

流式传输

指示当前是否有活动的视频流正在运行。

video

<video> 元素的引用。

canvas

<canvas> 元素的引用。

照片

<img> 元素的引用。

startButton

用于触发捕获的 <button> 元素的引用。

allowButton

用于控制页面是否可以访问设备的 <button> 元素的引用。

获取媒体流

下一个任务是获取媒体流:我们定义了一个事件监听器,当用户单击“允许摄像头”按钮时,该监听器会调用 MediaDevices.getUserMedia() 并请求视频流(无音频)。它返回一个 Promise,我们为其附加了成功和失败的回调函数。

js
allowButton.addEventListener("click", () => {
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: false })
    .then((stream) => {
      video.srcObject = stream;
      video.play();
    })
    .catch((err) => {
      console.error(`An error occurred: ${err}`);
    });
});

成功回调函数接收一个 stream 对象作为输入,并将其设置为我们 <video> 元素的源。一旦流与 <video> 元素链接,我们就通过调用 HTMLMediaElement.play() 来开始播放它。

如果打开流失败,则会调用错误回调函数。例如,如果连接的摄像头不兼容,或者用户拒绝了访问,就会发生这种情况。

监听视频开始播放

<video> 上调用 HTMLMediaElement.play() 后,在视频流开始流动之前会有一段(希望是短暂的)时间。为了避免在此之前阻塞,我们向 video 添加了对 canplay 事件的监听器,当视频播放实际开始时,会发出此事件。届时,video 对象中的所有属性都已根据流的格式进行了配置。

js
video.addEventListener("canplay", (ev) => {
  if (!streaming) {
    height = video.videoHeight / (video.videoWidth / width);

    video.setAttribute("width", width);
    video.setAttribute("height", height);
    canvas.setAttribute("width", width);
    canvas.setAttribute("height", height);
    streaming = true;
  }
});

除非这是第一次调用此回调,否则它什么也不做;这是通过查看我们的 streaming 变量的值来测试的,该变量在第一次运行此方法时为 false

如果确实是第一次运行,我们将根据视频的实际大小 video.videoWidth 和我们将要渲染的宽度 width 之间的尺寸差异来设置视频的高度。

最后,通过在每个元素的两个属性上调用 Element.setAttribute() 并相应地设置宽度和高度,将视频和 canvas 的 widthheight 设置为匹配。最后,我们将 streaming 变量设置为 true,以防止我们无意中再次运行此设置代码。

处理按钮点击

要捕获用户每次单击 startButton 时的静态照片,我们需要向按钮添加一个事件监听器,以便在发出 click 事件时调用。

js
startButton.addEventListener("click", (ev) => {
  takePicture();
  ev.preventDefault();
});

此方法很简单:它调用 takePicture() 函数(在下面的 从流中捕获帧 部分定义),然后对接收到的事件调用 Event.preventDefault(),以防止点击被处理多次。

清除照片框

清除照片框涉及创建一个图像,然后将其转换为 <img> 元素可用的格式,该元素显示最近捕获的帧。代码如下。

js
function clearPhoto() {
  const context = canvas.getContext("2d");
  context.fillStyle = "#aaaaaa";
  context.fillRect(0, 0, canvas.width, canvas.height);

  const data = canvas.toDataURL("image/png");
  photo.setAttribute("src", data);
}

clearPhoto();

我们首先获取对用于离屏渲染的隐藏 <canvas> 元素的引用。然后我们将 fillStyle 设置为 #aaaaaa(一种相当浅的灰色),并通过调用 fillRect() 用该颜色填充整个 canvas。

此函数中的最后一步是将 canvas 转换为 PNG 图像,然后调用 photo.setAttribute() 来使我们的捕获的静态框显示图像。

从流中捕获帧

还有最后一个函数要定义,它是整个练习的重点:takePicture() 函数,它的作用是捕获当前显示的视频帧,将其转换为 PNG 文件,并在捕获的帧框中显示它。代码如下。

js
function takePicture() {
  const context = canvas.getContext("2d");
  if (width && height) {
    canvas.width = width;
    canvas.height = height;
    context.drawImage(video, 0, 0, width, height);

    const data = canvas.toDataURL("image/png");
    photo.setAttribute("src", data);
  } else {
    clearPhoto();
  }
}

与任何需要处理 canvas 内容的情况一样,我们首先获取隐藏 canvas 的 2D 绘图上下文

然后,如果宽度和高度都非零(这意味着至少有潜在的有效图像数据),我们将 canvas 的宽度和高度设置为与捕获帧匹配,然后调用 drawImage() 将视频的当前帧绘制到上下文中,用帧图像填充整个 canvas。

注意:这利用了 HTMLVideoElement 接口在任何接受 HTMLImageElement 作为参数的 API 中看起来都像 HTMLImageElement 的事实,视频的当前帧被呈现为图像的内容。

一旦 canvas 包含捕获的图像,我们就通过对其调用 HTMLCanvasElement.toDataURL() 将其转换为 PNG 格式;最后,我们调用 photo.setAttribute() 来使我们的捕获的静态框显示图像。

如果没有有效的图像可用(即 widthheight 都为 0),我们通过调用 clearPhoto() 来清除捕获帧框的内容。

演示

单击“允许摄像头”以选择一个输入设备并允许页面访问摄像头。视频开始播放后,您可以单击“拍摄照片”以将流中的静态画面捕获为绘制在右侧 canvas 上的图像。

有趣的滤镜

由于我们通过从 <video> 元素抓取帧来捕获用户摄像头的图像,因此我们可以对视频应用有趣的 CSS filter 效果。这些滤镜范围从基本(使图像黑白)到复杂(高斯模糊和色相旋转)。

css
#video {
  filter: grayscale(100%);
}

要将视频滤镜应用于照片,takePicture() 函数需要进行以下更改。

js
function takePicture() {
  const context = canvas.getContext("2d");
  if (width && height) {
    canvas.width = width;
    canvas.height = height;

    // Get the computed CSS filter from the video element.
    // For example, it might return "grayscale(100%)"
    const videoStyles = window.getComputedStyle(video);
    const filterValue = videoStyles.getPropertyValue("filter");

    // Apply the filter to the canvas drawing context.
    // If there's no filter (i.e., it returns "none"), default to "none".
    context.filter = filterValue !== "none" ? filterValue : "none";

    context.drawImage(video, 0, 0, width, height);

    const dataUrl = canvas.toDataURL("image/png");
    photo.setAttribute("src", dataUrl);
  } else {
    clearPhoto();
  }
}

您可以尝试使用例如 Firefox 开发者工具的 Style Editor 来玩这个效果;有关如何操作的更多信息,请参阅 Edit CSS filters

使用特定设备

如果需要,您可以将允许的视频源集限制为特定设备或一组设备。为此,请调用 MediaDevices.enumerateDevices。当 Promise 以描述可用设备的 MediaDeviceInfo 对象数组 fulfilled 时,找到您想要允许的设备,并在传递给 getUserMedia()MediaTrackConstraints 对象中指定相应的 deviceIddeviceIds。

另见