webrtc快速入门——使用 WebRTC 拍摄静止的照片

文章目录

  • 使用 getUserMedia() 拍摄静态照片
    • HTML 标记
    • JavaScript 代码
    • 初始化
      • startup() 函数
        • 获取元素引用
        • 获取流媒体
      • 监听视频开始播放
        • 处理按钮上的点击
        • 包装 startup() 方法
      • 清理照片框
      • 从流中捕获帧
  • 例子代码
      • HTML代码
      • CSS代码
      • JavaScript代码
  • 过滤器
  • 使用特定设备

使用 getUserMedia() 拍摄静态照片

介绍了如何在 WebRTC 的支持下可以访问到电脑或者手机的摄像头并且使用它来拍摄照片。

本文介绍在支持 getUserMedia() 的计算机或手机上如何使用 navigator.mediaDevices.getUserMedia() 访问摄像机,并用其拍照。

HTML 标记

我们的 HTML 界面有两个主要的操作部分:流和捕获面板以及演示面板。它们俩都在它们自己的 <div> 中并排渲染,以便于添加样式和控制。

左边的面板包含两个组件:一个 <video> 元素,它将接收来自 navigator.mediaDevices.getUserMedia() 的流,以及用于用户点击以捕获视频帧的 <button>

<div class="camera"><video id="video">视频流目前不可用。</video><button id="startbutton">拍摄照片</button>
</div> 

这很简单,当我们进入 JavaScript 代码时,我们将看到它们是如何紧密联系在一起的。

接下来,我们有一个 <canvas> 元素,捕获的帧被存储到其中,可能以某种方式进行操作,然后转换为输出图像文件。通过使用样式 display:none 将画布保持隐藏,以避免画面的混乱——用户不需要看到这个中间过程。

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

<canvas id="canvas"> </canvas>
<div class="output"><img id="photo" alt="捕获的图像会显示在这里。" />
</div> 

这是所有相关的 HTML。其余的只是一些页面布局和提供一个返回页面链接的些许文本。

JavaScript 代码

现在来看看 JavaScript 代码。我们将把它分解成几个小的部分,使其更容易解释。

初始化

我们首先将整个脚本包装在匿名函数中,以避免使用全局变量,然后设置我们将要使用的各种变量。

(() => {const width = 320;    const height = 0;     const streaming = false;let video = null;let canvas = null;let photo = null;let startbutton = null; 

这些变量分别是:

width

无论输入视频的尺寸如何,我们将把所得到的图像缩放到宽度为 320 像素。

height

给定流的 width 和宽高比,计算出图像的输出高度。

streaming

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

video

这将是页面加载完成后对 <video> 元素的引用。

canvas

这将是页面加载完成后对 <canvas> 元素的引用。

photo

这将在页面加载完成后引用 <img> 元素。

startbutton

这将引用用于触发捕获的 <button> 元素。我们会在页面加载完成之后得到。

startup() 函数

当页面加载完成时,提供给 EventTarget.addEventListenerstartup() 函数将会运行。此函数的作用是请求访问用户的网络摄像头,将用于输出的 <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.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 和要渲染视频宽度的宽度(witdh)之间的大小差异来设置视频的高度。

最后,通过在视频和画布上调用 Element.setAttribute() 来设置视频和画布的宽度(witdh)和高度(height),以使得两者相互匹配。最后,我们将 streaming 变量设置为 true,以防止我们无意中再次运行此设置代码。

处理按钮上的点击

为了在每次用户点击 startbutton 时捕获静态照片,我们需要向按钮添加一个事件监听器,以便在发出 click 事件时被调用:

startbutton.addEventListener("click",(ev) => {takepicture();ev.preventDefault();},false,
); 

这个方法很简单:它只是调用我们的 takepicture() 函数,在从流中捕获帧的部分中定义,然后在接收的事件上调用 Event.preventDefault(),以防止点击被多次处理。

包装 startup() 方法

startup() 方法中只有两行代码:

这就是我们调用 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() 将视频的当前帧绘制到上下文中,用帧图像填充整个画布。

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

如果没有可用的有效图像(即宽度和高度均为 0),则通过调用 clearphoto() 清除捕获帧框的内容。

例子代码

HTML代码

<div class="contentarea"><h1>MDN——navigator.mediaDevices.getUserMedia(): 静态照片拍摄演示</h1><p>此示例演示了如何使用内置的网络摄像头来获取媒体流,并从中获取图像,以使用该图像来创建一个PNG 图像。</p><div class="camera"><video id="video">视频流目前不可用。</video><button id="startbutton">拍摄照片</button></div><canvas id="canvas"> </canvas><div class="output"><img id="photo" alt="捕获的图像会显示在这里。" /></div><p>访问我们的文章:<ahref="https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API/Taking_still_photos">使用 getUserMedia() 拍摄静态照片</a>以详细了解此处使用的技术。</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: rgba(0, 150, 0, 0.5);border: 1px solid rgba(255, 255, 255, 0.7);box-shadow: 0px 0px 1px 2px rgba(0, 0, 0, 0.2);font-size: 14px;font-family: "Lucida Grande", "Arial", sans-serif;color: rgba(255, 255, 255, 1);
}.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 thislet 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 = "查看以上示例代码的实时演示";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);
})();

过滤器

由于我们通过从 <video> 元素中抓取帧来捕获用户网络摄像头的图像,因此我们可以非常轻松地将过滤器和有趣的效果应用于视频。事实证明,使用 filter 属性应用于元素的任何 CSS 过滤器都会影响捕获的照片。这些过滤器可以从简单(使图像黑白)到复杂(高斯模糊和色调旋转)。

你可以使用例如 Firefox 开发者工具的样式编辑器来播放此效果;有关如何执行此操作的详细信息,请参阅编辑 CSS 过滤器。

使用特定设备

如果需要,你可以将允许的视频源限定为特定的设备或特定的一组设备。要做到这一点,请调用 MediaDevices.enumerateDevices。若返回的 promise 兑现了一个 MediaDeviceInfo 对象(描述了可用的设备)数组,可以从中选取一个你想要允许的设备,并将对应的 deviceIdMediaTrackConstraints 对象的 deviceId 作为参数传入到 getUserMedia() 中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/107700.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ内容

RabbitMQ是一款开源的消息中间件&#xff0c;支持多个消息协议&#xff0c;包括AMQP、STOMP、MQTT等&#xff0c;它的主要功能是将分散的应用程序连接在一起&#xff0c;以便它们可以相互通信&#xff0c;从而构建可靠的、高效的分布式系统。 RabbitMQ的基础概念包括&#xff…

Vue中如何处理表单数据的双向绑定和验证

在前端开发中&#xff0c;表单是非常常见的交互组件之一。为了实现表单数据的双向绑定和验证&#xff0c;Vue提供了一些强大的功能和方法。以下将详细介绍Vue中如何处理表单数据的双向绑定和验证&#xff0c;并提供具体的代码示例&#xff0c;以帮助读者更好地掌握这方面的知识…

单例模式之「双重校验锁」

单例模式之「双重校验锁」 单例模式 单例即单实例&#xff0c;只实例出来一个对象。一般在创建一些管理器类、工具类的时候&#xff0c;需要用到单例模式&#xff0c;比如JDBCUtil 类&#xff0c;我们只需要一个实例即可&#xff08;多个实例也可以实现功能&#xff0c;但是增…

(ubuntu) 安装JDK

文章目录 前言参看java版本的命令&#xff1a;安装jdk命令安装jps关闭防火墙&#xff1a;查看端口占用&#xff1a;&#xff08;坑&#xff09;ubuntu上Mysql默认标明 区分大小写 前言 提示&#xff1a;常以为人是一个容器&#xff0c;盛着快乐&#xff0c;盛着悲哀。但是人不…

使用 Tkinter Canvas 小部件添加放大镜功能?

一、说明 据我所知&#xff0c;内置的 Tkinter Canvas 类比例不会自动缩放图像。如果您无法使用自定义小部件&#xff0c;则可以缩放原始图像并在调用缩放函数时将其替换在画布上。 二、实现图像放大镜技术细节 我如何将放大和缩小添加到以下脚本中&#xff0c;我想将其绑定到…

初阶数据结构-常见的排序算法

排序 排序的概念常见的排序算法常见排序算法的实现数组的打印 插入排序直接插入排序的实现希尔排序( 缩小增量排序 )希尔排序的实现 交换排序冒泡排序冒泡排序的实现选择排序选择排序的实现堆排序堆排序的实现快速排序快速排序非递归 归并排序归并排序的递归实现归并排序的非递…

Vue 3.0 使用的 diff 算法相比 Vue 2.0 中的双端比对有什么优势?

1、最长递增子序列算法 Vue 3.0 的 diff 算法采用了最长递增子序列算法&#xff0c;能够减少不必要的 DOM 操作&#xff0c;提升性能。 2、静态标记 Vue 3.0 中&#xff0c;编译器会对静态节点进行标记&#xff0c;在更新时可以直接跳过这些静态节点&#xff0c;减少 DOM 操作…

多个子div在父中垂直居中

在一个div下&#xff0c;有多个子div&#xff0c;且子div都是水平垂直居中 <template><div><div class"far"><!-- 注意需要多包裹一层 --><div><div class"son1">1</div><div class"son2">222…

MATLAB-文件自动批量读取文件,并按文件名称或时间顺序进行数据处理

我在处理文件数据时&#xff0c;发现一个一个文件处理效率太低&#xff0c;因此学习了下MATLAB中自动读取特定路径下文件信息的程序&#xff0c;并根据读取信息使用循环进行数据处理&#xff0c;提高效率&#xff0c;在此分享给大家这段代码并给予一些说明&#xff0c;希望能为…

Open3D 进阶(13)使用PCA将点云投影到主成分空间

目录 一、算法原理<font color="#dd00dd">1、三维点云投影二、代码实现三、结果展示本文由CSDN点云侠原创,原文链接。爬虫网站自重。 一、算法原理 1、三维点云投影 p r o j

IDEA初始配置

1. 详细设置 安装完IDEA之后的简单配置。 1.1 如何打开详细配置界面 1、显示工具栏 2、选择详细配置菜单或按钮 1.2 系统设置 1、默认启动项目配置 启动IDEA时&#xff0c;默认自动打开上次开发的项目&#xff1f;还是自己选择&#xff1f; 如果去掉Reopen projects on …

“Linux免除系统交互操作方法、expect自动化交互工具” 及 “SSH批量修改主机密码脚本”

一、Linux系统免除交互操作方法 1、EOF多文本输入 案例&#xff1a;为机器磁盘进行分区并实现挂载&#xff0c;免交互式操作&#xff0c;如何实现&#xff1f; #!/bin/bash fdisk /dev/sdb <<EOF n p 1 wq EOFmkfs.xfs /dev/sdb1 && mkdir -p /data &&am…

《动手学深度学习 Pytorch版》 8.6 循环神经网络的简洁实现

import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2lbatch_size, num_steps 32, 35 train_iter, vocab d2l.load_data_time_machine(batch_size, num_steps)8.6.1 定义模型 num_hiddens 256 rnn_layer nn.RNN(len(voca…

【python高级】设计模式、类工厂、对象工厂

一、说明 最近试着读Design pattern&#xff0c; 不过有些概念实在太抽象了&#xff0c; 整理一下自己所学抽象工厂的精神&#xff0c;就是要有abstract class&#xff08;not implement&#xff09;&#xff0c;而所有不同种类的对象&#xff0c;都是继承这个abstract class&a…

Linux命令(94)之history

linux命令之history 1.history介绍 linux命令history会记录并显示用户所执行过的所有命令&#xff0c;也可以对其命令进行修改和删除操作。 2.history用法 history [参数] history参数 参数说明-a将当前会话的历史信息追加到历史文件(.bash_history)中-c删除所有条目从而清…

【云计算网络安全】DDoS 攻击类型:什么是 ACK 洪水 DDoS 攻击

文章目录 一、什么是 ACK 洪水 DDoS 攻击&#xff1f;二、什么是数据包&#xff1f;三、什么是 ACK 数据包&#xff1f;四、ACK 洪水攻击如何工作&#xff1f;五、SYN ACK 洪水攻击如何工作&#xff1f;六、文末送书《AWD特训营》内容简介读者对象 一、什么是 ACK 洪水 DDoS 攻…

猫头虎博客带您使用Markdown编辑器

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

BAT026:删除当前目录及子目录下的空文件夹

引言&#xff1a;编写批处理程序&#xff0c;实现批量删除当前目录及子目录下的空文件夹。 一、新建Windows批处理文件 参考博客&#xff1a; CSDNhttps://mp.csdn.net/mp_blog/creation/editor/132137544 二、写入批处理代码 1.右键新建的批处理文件&#xff0c;点击【编辑…

代码随想录训练营二刷第五十八天 | 583. 两个字符串的删除操作 72. 编辑距离

代码随想录训练营二刷第五十八天 | 583. 两个字符串的删除操作 72. 编辑距离 一、583. 两个字符串的删除操作 题目链接&#xff1a;https://leetcode.cn/problems/delete-operation-for-two-strings/ 思路&#xff1a;定义dp[i][j]为要是得区间[0,i-1]和区间[0,j-1]所需要删除…

leetcode做题笔记179. 最大数

给定一组非负整数 nums&#xff0c;重新排列每个数的顺序&#xff08;每个数不可拆分&#xff09;使之组成一个最大的整数。 注意&#xff1a;输出结果可能非常大&#xff0c;所以你需要返回一个字符串而不是整数。 示例 1&#xff1a; 输入&#xff1a;nums [10,2] 输出&am…