使用 getUserMedia() 拍摄静态照片
本文介绍如何使用 navigator.mediaDevices.getUserMedia()
访问支持 getUserMedia()
的计算机或手机上的摄像头并使用它拍照。
如果你愿意,也可以直接跳到 演示。
HTML 标记
我们的 HTML 接口 具有两个主要的操作部分:流和捕获面板以及演示面板。为了方便样式设置和控制,每个部分都在自己的 <div>
中并排呈现。
左侧的第一个面板包含两个组件:一个 <video>
元素,它将接收来自 navigator.mediaDevices.getUserMedia()
的流,以及一个 <button>
,用户点击它来捕获视频帧。
<div class="camera">
<video id="video">Video stream not available.</video>
<button id="startbutton">Take photo</button>
</div>
这很简单,当我们进入 JavaScript 代码时,我们将看到它是如何结合在一起的。
接下来,我们有一个 <canvas>
元素,捕获的帧将存储到其中,可能以某种方式进行操作,然后转换为输出图像文件。通过使用 display: none
对画布进行样式设置,使其保持隐藏状态,以避免屏幕杂乱——用户不需要看到这个中间阶段。
我们还有一个 <img>
元素,我们将把图像绘制到其中——这是最终显示给用户的图像。
<canvas id="canvas"> </canvas>
<div class="output">
<img id="photo" alt="The screen capture will appear in this box." />
</div>
这就是所有相关的 HTML。其余的只是一些页面布局填充和一些提供返回此页面的链接的文本。
JavaScript 代码
现在让我们看看 JavaScript 代码。我们将将其分解成几个小部分,以便更容易解释。
初始化
我们首先将整个脚本包装在一个匿名函数中以避免全局变量,然后设置我们将要使用的各种变量。
(() => {
const width = 320; // We will scale the photo width to this
const height = 0; // This will be computed based on the input stream
const streaming = false;
let video = null;
let canvas = null;
let photo = null;
let startbutton = null;
这些变量是
startup() 函数
startup()
函数在页面加载完成后运行,由 EventTarget.addEventListener
提供。此函数的作用是请求访问用户的网络摄像头,将输出 <img>
初始化为默认状态,并建立接收来自摄像头的每一帧视频和在点击按钮以捕获图像时做出反应所需的事件侦听器。
获取元素引用
首先,我们获取我们需要能够访问的主要元素的引用。
function startup() {
video = document.getElementById('video');
canvas = document.getElementById('canvas');
photo = document.getElementById('photo');
startbutton = document.getElementById('startbutton');
获取媒体流
下一个任务是获取媒体流
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
});
在这里,我们调用 MediaDevices.getUserMedia()
并请求视频流(没有音频)。它返回一个 promise,我们将其附加成功和失败回调。
成功回调以 stream
对象作为输入接收。它是 <video>
元素到我们新流的源。
将流链接到 <video>
元素后,我们通过调用 HTMLMediaElement.play()
开始播放它。
如果打开流失败,则调用错误回调。例如,如果未连接兼容的摄像头或用户拒绝访问,就会发生这种情况。
侦听视频开始播放
在 <video>
上调用 HTMLMediaElement.play()
后,在视频流开始流动之前会有一段(希望很短的)时间。为了避免阻塞直到发生这种情况,我们在 video
上为 canplay
事件添加了一个事件侦听器,当视频播放实际开始时,将传递此事件。此时,video
对象中的所有属性都已根据流的格式进行配置。
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;
}
},
false,
);
此回调除非是第一次调用才会执行任何操作;这是通过查看我们 streaming
变量的值来测试的,该变量在第一次运行此方法时为 false
。
如果确实是第一次运行,我们将根据视频的实际大小 video.videoWidth
与我们将渲染它的宽度 width
之间的差异来设置视频的高度。
最后,视频和画布的 宽度
和 高度
通过在每个元素的这两个属性上调用 Element.setAttribute()
并根据需要设置宽度和高度来相互匹配。最后,我们将 streaming
变量设置为 true
以防止我们无意中再次运行此设置代码。
处理按钮上的点击
为了在用户每次点击 startbutton
时捕获静态照片,我们需要向按钮添加一个事件侦听器,在发出 click
事件时调用该侦听器
startbutton.addEventListener(
"click",
(ev) => {
takepicture();
ev.preventDefault();
},
false,
);
此方法非常简单:它只调用我们在 从流中捕获帧 部分下面定义的 takepicture()
函数,然后在接收到的事件上调用 Event.preventDefault()
以防止点击被处理多次。
总结 startup() 方法
startup()
方法中只有两行代码
clearphoto();
}
在这里,我们调用将在 清除照片框 部分下面介绍的 clearphoto()
方法。
清除照片框
清除照片框包括创建图像,然后将其转换为可用于显示最近捕获的帧的 <img>
元素的格式。代码如下所示
function clearphoto() {
const context = canvas.getContext("2d");
context.fillStyle = "#AAA";
context.fillRect(0, 0, canvas.width, canvas.height);
const data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
}
我们首先获取对用于离屏渲染的隐藏 <canvas>
元素的引用。接下来,我们将 fillStyle
设置为 #AAA
(一种相当浅的灰色),并通过调用 fillRect()
用该颜色填充整个画布。
最后,在此函数中,我们将画布转换为 PNG 图像并调用 photo.setAttribute()
以使我们捕获的静态框显示图像。
从流中捕获帧
还有一个函数需要定义,它也是整个练习的关键:takepicture()
函数,其作用是捕获当前显示的视频帧,将其转换为 PNG 文件,并在捕获的帧框中显示它。代码如下所示
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();
}
}
与任何我们需要处理画布内容的时间一样,我们首先获取隐藏画布的 2D 绘图上下文。
然后,如果宽度和高度都非零(表示至少可能有有效的图像数据),我们将画布的宽度和高度设置为与捕获的帧匹配,然后调用 drawImage()
将视频的当前帧绘制到上下文中,用帧图像填充整个画布。
注意: 这利用了 HTMLVideoElement
接口对于接受 HTMLImageElement
作为参数的任何 API 来说都类似于 HTMLImageElement
的事实,视频的当前帧被呈现为图像的内容。
一旦画布包含捕获的图像,我们就通过调用 HTMLCanvasElement.toDataURL()
将其转换为 PNG 格式;最后,我们调用 photo.setAttribute()
使我们的捕获的静止框显示图像。
如果没有可用的有效图像(即,width
和 height
都为 0),我们通过调用 clearphoto()
清除捕获的帧框的内容。
演示
HTML
<div class="contentarea">
<h1>MDN - navigator.mediaDevices.getUserMedia(): Still photo capture demo</h1>
<p>
This example demonstrates how to set up a media stream using your built-in
webcam, fetch an image from that stream, and create a PNG using that image.
</p>
<div class="camera">
<video id="video">Video stream not available.</video>
<button id="startbutton">Take photo</button>
</div>
<canvas id="canvas"> </canvas>
<div class="output">
<img id="photo" alt="The screen capture will appear in this box." />
</div>
<p>
Visit our article
<a
href="https://mdn.org.cn/en-US/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos">
Taking still photos with WebRTC</a
>
to learn more about the technologies used here.
</p>
</div>
CSS
#video {
border: 1px solid black;
box-shadow: 2px 2px 3px black;
width: 320px;
height: 240px;
}
#photo {
border: 1px solid black;
box-shadow: 2px 2px 3px black;
width: 320px;
height: 240px;
}
#canvas {
display: none;
}
.camera {
width: 340px;
display: inline-block;
}
.output {
width: 340px;
display: inline-block;
vertical-align: top;
}
#startbutton {
display: block;
position: relative;
margin-left: auto;
margin-right: auto;
bottom: 32px;
background-color: rgb(0 150 0 / 50%);
border: 1px solid rgb(255 255 255 / 70%);
box-shadow: 0px 0px 1px 2px rgb(0 0 0 / 20%);
font-size: 14px;
font-family: "Lucida Grande", "Arial", sans-serif;
color: rgb(255 255 255 / 100%);
}
.contentarea {
font-size: 16px;
font-family: "Lucida Grande", "Arial", sans-serif;
width: 760px;
}
JavaScript
(() => {
// The width and height of the captured photo. We will set the
// width to the value defined here, but the height will be
// calculated based on the aspect ratio of the input stream.
const width = 320; // We will scale the photo width to this
let height = 0; // This will be computed based on the input stream
// |streaming| indicates whether or not we're currently streaming
// video from the camera. Obviously, we start at false.
let streaming = false;
// The various HTML elements we need to configure or control. These
// will be set by the startup() function.
let video = null;
let canvas = null;
let photo = null;
let startbutton = null;
function showViewLiveResultButton() {
if (window.self !== window.top) {
// Ensure that if our document is in a frame, we get the user
// to first open it in its own tab or window. Otherwise, it
// won't be able to request permission for camera access.
document.querySelector(".contentarea").remove();
const button = document.createElement("button");
button.textContent = "View live result of the example code above";
document.body.append(button);
button.addEventListener("click", () => window.open(location.href));
return true;
}
return false;
}
function startup() {
if (showViewLiveResultButton()) {
return;
}
video = document.getElementById("video");
canvas = document.getElementById("canvas");
photo = document.getElementById("photo");
startbutton = document.getElementById("startbutton");
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
});
video.addEventListener(
"canplay",
(ev) => {
if (!streaming) {
height = video.videoHeight / (video.videoWidth / width);
// Firefox currently has a bug where the height can't be read from
// the video, so we will make assumptions if this happens.
if (isNaN(height)) {
height = width / (4 / 3);
}
video.setAttribute("width", width);
video.setAttribute("height", height);
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
streaming = true;
}
},
false,
);
startbutton.addEventListener(
"click",
(ev) => {
takepicture();
ev.preventDefault();
},
false,
);
clearphoto();
}
// Fill the photo with an indication that none has been
// captured.
function clearphoto() {
const context = canvas.getContext("2d");
context.fillStyle = "#AAA";
context.fillRect(0, 0, canvas.width, canvas.height);
const data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
}
// Capture a photo by fetching the current contents of the video
// and drawing it into a canvas, then converting that to a PNG
// format data URL. By drawing it on an offscreen canvas and then
// drawing that to the screen, we can change its size and/or apply
// other changes before drawing it.
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();
}
}
// Set up our event listener to run the startup process
// once loading is complete.
window.addEventListener("load", startup, false);
})();
结果
滤镜趣味
使用特定设备
如果需要,您可以将允许的视频源集限制到特定的设备或一组设备。为此,请调用 MediaDevices.enumerateDevices
。当 Promise 以描述可用设备的 MediaDeviceInfo
对象数组的形式 fulfilled 时,找到您要允许的设备,并在传递给 getUserMedia()
的 MediaTrackConstraints
对象中指定相应的 deviceId
或 deviceId
。