Taking still photos with WebRTC
本文介绍如何使用 WebRTC在 支持 WebRTC 的计算机或手机上访问摄像机,并用其拍照。 尝试一下这个示例,然后继续阅读,了解它的工作原理。
如果你喜欢,你也可以直接跳转到 GitHub 上的代码。
HTML 标记
我们的 HTML 界面有两个主要的操作区域:流和捕获面板以及演示面板。 它们俩都在它们自己的 <div>
中并排呈现,以便于造型和控制。
左边的面板包含两个组件:一个 <video>
元素,它将接收来自 WebRTC 的流,以及用户点击捕获视频帧的 <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 代码。 我们将把它分解成几个小的部分,使其更容易解释。
初始化
我们首先将整个脚本包装在匿名函数中,以避免使用全局变量,然后设置我们将要使用的各种变量。
(function() {
var width = 320; // We will scale the photo width to this
var height = 0; // This will be computed based on the input stream
var streaming = false;
var video = null;
var canvas = null;
var photo = null;
var startbutton = null;
那些变量是:
startup( )函数
当页面加载完成时,startup( )
函数运行,由window.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(function(stream) {
video.srcObject = stream;
video.play();
})
.catch(function(err) {
console.log("An error occured! " + err);
});
在这里,我们正在调用 MediaDevices.getUserMedia()
并请求视频流(无音频)。 它返回一个 promise,我们给它附加成功和失败情况下的回调方法。
成功回调接收一个 stream 对象作为输入。它是新视频的 <video>
元素的源。
一旦流被链接到
元素,我们通过调用 <video>
HTMLMediaElement.play()
开始播放。
如果打开流失败,则调用失败回调函数。 在没有连接兼容的相机,或者用户拒绝访问时,则会发生这种情况。
监听视频开始播放
在
上调用 <video>
HTMLMediaElement.play()
之后,在视频流开始流动之前,有一段(希望简短)的时间段过去了。 为了避免在此之前一直阻塞,我们为
加上一个 <video>
canplay (en-US)
事件的监听器,当视频播放实际开始时会触发该事件。 那时,视频对象中的所有属性都已基于流的格式进行配置。
video.addEventListener('canplay', function(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);
这个回调什么都不做,除非它是第一次被调用; 这是通过查看我们的流变量的值进行测试,这是第一次运行此方法时为false。
如果这是第一次运行,我们会根据视频的实际大小,video.videoWidth和要渲染视频宽度的宽度之间的大小差异来设置视频的高度。
最后,通过在每个元素的两个属性的每一个上调用Element.setAttribute()来设置视频和画布的宽度和高度,并根据需要设置宽度和高度。 最后,我们将流变量设置为true,以防止我们再次无意中运行此设置代码。
处理按钮上的点击
为了在每次用户单击启动按钮时捕获静态照片,我们需要向按钮添加一个事件侦听器,以便在发出点击事件时被调用:
startbutton.addEventListener('click', function(ev){
takepicture();
ev.preventDefault();
}, false);
这个方法很简单:它只是调用我们的takepicture()函数,在从流中捕获一个帧的部分中定义,然后在接收的事件上调用Event.preventDefault(),以防止点击被多次处理。
包装startup()方法
startup()方法中只有两行代码:
clearphoto();
}
这就是我们在Clearflow()方法中,我们将在下面的清理照片框中进行描述。
清除照片框
清除照片框包括创建一个图像,然后将其转换为可以显示最近捕获的帧的<img>元素使用的格式。 该代码如下所示:
function clearphoto() {
var context = canvas.getContext('2d');
context.fillStyle = "#AAA";
context.fillRect(0, 0, canvas.width, canvas.height);
var data = canvas.toDataURL('image/png');
photo.setAttribute('src', data);
}
我们首先得到对我们用于屏幕外渲染的隐藏的<canvas>元素的引用。 接下来,我们将fillStyle设置为#AAA(相当浅灰色),并通过调用fillRect()来填充整个画布。
最后在此功能中,我们将画布转换为PNG图像,并调用photo.setAttribute()以使我们捕获的静止框显示图像。
从流中捕获帧
最后一个定义的功能是整个练习的重点:takepicture()函数,其捕获当前显示的视频帧的作业将其转换为PNG文件,并将其显示在捕获的帧框中。 代码如下所示:
function takepicture() {
var context = canvas.getContext('2d');
if (width && height) {
canvas.width = width;
canvas.height = height;
context.drawImage(video, 0, 0, width, height);
var data = canvas.toDataURL('image/png');
photo.setAttribute('src', data);
} else {
clearphoto();
}
}
正如我们需要处理画布内容的情况一样,我们首先得到隐藏画布的2D绘图上下文。
然后,如果宽度和高度都是非零(意味着至少有潜在有效的图像数据),我们将画布的宽度和高度设置为与捕获帧的宽度和高度相匹配,然后调用drawImage()来绘制当前的 将视频帧放入上下文中,用帧图像填充整个画布。
注意:这可以利用HTMLVideoElement接口看起来像任何接受HTMLImageElement作为参数的API的HTMLImageElement,将视频的当前帧呈现为图像的内容。
一旦画布包含捕获的图像,我们通过调用它的HTMLCanvasElement.toDataURL()将它转换为PNG 格式; 最后,我们调用photo.setAttribute()来使我们捕获的静态框显示图像。
如果没有可用的有效图像(即宽度和高度均为0),则通过调用clearphoto()清除捕获帧框的内容。
// 加载完毕后开始运行
window.addEventListener("load", startup, false);
})();
过滤器的乐趣
由于我们通过从<video>元素中抓取帧来捕获用户网络摄像头的图像,因此我们可以非常轻松地将过滤器和有趣的效果应用于视频。 事实证明,使用过滤器属性应用于元素的任何CSS过滤器都会影响捕获的照片。 这些过滤器可以从简单(使图像黑白)到极限(高斯模糊和色调旋转)。
您可以使用例如Firefox开发人员工具的风格编辑器来播放此效果; 有关如何执行此操作的详细信息,请参阅编辑CSS过滤器。