内容简介:特别声明,本文根据要构建一个让我们构建一具自定义的
特别声明,本文根据 @David East 的《 HOW TO BUILD A SIMPLE CAMERA COMPONENT 》一文所整理。
要构建一个 camera
组件,我们首先要了解所需的浏览器API。
- 使用
MediaDevices
API获取相机访问权限 - 使用
video
元素播放MediaStream
- 使用
canvas
元素以blob
或base64
形式拍照
让我们构建一具自定义的 camera
元素,这样你就不必再担心把这些代码连接起来。
使用自定义元素构建可跨框架重用的组件
这篇文章并没有指定在哪个框架构建摄像头组件。叶节点(Leaf Node)组件应该是可重用的。自定义元素是一种新的浏览器标准,允许你构建可在大多数JavaScript框架中移植的可重用元素。如果你不熟悉自定义元素(Custom Elements)并不重要。因为接下来的示例都是一些简单的示例,所以使用自定义元素并不复杂。在高级情况下,它会变得复杂,但我们将会避开这些。这是一个简单的例子:
class HelloElement extends HTMLElement { constructor() { // 调用构造函数不是必须的。如果你这样做,一定要确认调用了`super()` super(); } // 当元素连接到DOM时调用这个函数 connectedCallback() { // 附上一个shadow,这样任何人都不会弄乱你的样式 const shadow = this.attachShadow({ mode: 'open' }); shadow.textContent = 'Hello world!'; } } // 定义标签名,它必须有一个破折号 customElements.define('hello-element', HelloElement);
在HTML中你可以像下面这样调用自定义的元素 hello-element
:
<hello-element></hello-element>
你在浏览器运行上面的代码之后,将看到的效果如下图所示:
这是自定义元素的简单用法。就像我说的,它也可以变得更复杂,但我们这里将要构建的是一个摄像头组件,而且是一个简单的摄像头组件,所以会尽量让它保持简单。
摄像头组件需要一个video元素和一个隐藏的canvas元素
让我们从简单的 camera
组件开始。
class SimpleCamera extends HTMLElement { constructor() { super(); } connectedCallback() { const shadow = this.attachShadow({ mode: 'open' }) this.videoElement = document.createElement('video') this.canvasElement = document.createElement('canvas') this.videoElement.setAttribute('playsinline', true) this.canvasElement.style.display = 'none' shadow.appendChild(this.videoElement) shadow.appendChild(this.canvasElement) } } customElements.define('simple-camera', SimpleCamera)
该组件只添加了两个元素:一个是 video
元素和一个隐藏的 canvas
元素。
在 iOS 10 Safari 中,通过 playsinline
可以让视频内联播放。设置了 playsinline
属性的视频在播放时不会自动全屏,但用户可以点击全屏按钮来手动全屏;没有设置 playsinline
的视频会在播放时自动全屏。无论是否设置 playsinline
属性,退出全屏后视频都会继续播放。
playsinline
属性在 iOS 10 之前需要写成 webkit-playsinline
,它的浏览器厂商前缀在 iOS 10 中被移除。但是目前 iOS 微信还不支持去掉前缀的写法,两个属性最好都加上。
显然, <video>
的 autoplay
必须和 playsinline
属性一起使用。也就是说,只有默认内联播放的视频才有可能自动播放,这一点很容易理解。
然后在HTML中像下面这样调用自定义好的元素:
<simple-camera></simple-camera>
这样就可以为摄像机创建一个元素。也可以开始播放一些视频。
好像啥也没有一样,是不。不急,咱们继续往下。
通过 MediaDevices
API授权访问摄像头
使用 navigator.mediaDevices.getUserMedia()
方法,授权用户访问摄像头。
navigator.mediaDevices.getUserMedia(constraints).then((mediaStream) => { })
请注意, getUserMedia()
会返回一个 Promise
。如果返回成功, Promise
会解析 MediaStream
。此流(Stream)将会用于 video
元素。如果 Promise
拒绝( rejects
),表示用户未授权访问摄像头。然而! Promise
有可能永远不会解决( resolve
)或拒绝( reject
)。用户可以决定永远不对权限弹出框执行操作。那不是很好玩吗?
浏览器对 MediaDevices
的支持很强大,但很奇怪
MediaDevices
API得到浏览器强大的支持。它可以在所有现代浏览器中使用。然而,在IE中没有得到支持,所以你需要对该特性做一个检查。
if (navigator.mediaDevices.getUserMedia === undefined) { navigator.mediaDevices.getUserMedia(constraints).then((mediaStream) => { }) }
然而,一些浏览器版本对 MediaDevices
的API只有部分支持,有些则需要添加浏览器供应商的前端才能实现。 MDN文章 中有一个关于设置Polyfills的部分有介绍到这方面的知识。幸运的是,这些Polyfill应该应用在元素之外,所以我们不需要在元素中考虑这个。
为mediaStream的audio和video设置相应的约束
getUserMedia()
方法接受一组约束。这些限制有助于在用户接受权限后配置流。它们具有 MediaStreamConstraints
的类型。你可以指定两个主要属性: audio
和 video
。
if (navigator.mediaDevices.getUserMedia === undefined) { navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: 'user' } }).then((mediaStream) => { }) }
audio
属性是一个简单的布尔值。你要么请求用户的音频,要么不请求。 video
属性要复杂得多。视频约束,也称为 MediaTrackConstraints
,指定了视频流可能需要的所有内容: echoCancellation
、 latency
、 sampleRate
、 sampleSize
、 volume
、 noiseSuppression
、 frameRate
、 aspectRatio
、 facingMode
,当然还有 width
和 height
。
有很多约束。然而,除非你正开发一个摄像头应用程序,否则你只需要几个。即: height
、 width
和 facingMode
。
将MediaStream分配给video元素
现在已经配置了 MediaStream
,就可以将其分配给 video
元素。
open(constraints) { return navigator.mediaDevices.getUserMedia(constraints) .then((mediaStream) => { // 分配MediaStream this.videoElement.srcObject = mediaStream // 加载时播放流 this.videoElement.onloadedmetadata = (e) => { this.videoElement.play() } }) }
video
元素有一个 srcObject
。它在分配 MediaStream
时从设置的摄像头流式输出。上面的代码片段在元素上添加了一个 open
方法。自定义元素具有可调用方法。如果用户调用这个 open
方法,它将启动视频流。
<script> (async function() { const camera = document.querySelector('simple-camera') await camera.open({ video: { facingMode: 'user' } }) }()) </script>
现在我们可以播放视频,让我们拍照。
使用canvas将照片作为blob拍摄
canvas
元素能够从 video
元素中绘制帧。使用此功能,你可以在不可见的 canvas
上绘制,然后将图像导出为 blob
。
_drawImage() { const imageWidth = this.videoElement.videoWidth const imageHeight = this.videoElement.videoHeight const context = this.canvasElement.getContext('2d') this.canvasElement.width = imageWidth this.canvasElement.height = imageHeight context.drawImage(this.videoElement, 0, 0, imageWidth, imageHeight) return { imageHeight, imageWidth } }
这个私有的 _drawImage()
方法将不可见的 canvas
的 height
和 width
设置为 video
的大小。然后在上下文( context
)中使用 drawImage()
方法。提供 video
元素的 x
、 y
位置, width
和 height
。这将在不可见的 canvas
上绘图,并将相关设置创建为一个 blob
。
takeBlobPhoto() { const {imageHeight, imageWidth} = this._drawImage() return new Promise((resolve, reject) => { this.canvasElement.toBlob((blob) => { resolve({blob, imageHeight, imageWidth}) }) }) }
canvas
元素有一个 toBlob()
方法。由于它是异步的,所以你可以将它转换为一个 Promise
,这样它就更容易使用。
现在你可以开始控制这个相机了:
<simple-camera></simple-camera> <button id="btnPhoto">Take Blob</button> <script> (async function(){ const camera = document.querySelector('simple-camera') const btnPhoto = document.querySelector('#btnPhoto') await camera.open({ video: { facingMode: 'user' } }) btnPhoto.addEventListener('click', async event => { const photo = await camera.takeBlobPhoto() }) }()) </script>
当你需要上传一个文件时, blob
是最好的。但是有时候,在 image
标签中插入一个 base64
编码的字符串会更好。 canvas
有相应的解决方案。
使用canvas把拍摄的图片转换为base64
canvas
元素有现代战争 toDataURL()
方法。该方法获取 canvas
的当前内容,并将其输出成 base64
编码的图像。
takeBase64Photo({type, quality} = {type: 'png', quality: 1}) { const {imageHeight, imageWidth} = this._drawImage() const base64 = this.canvasElement.toDataURL('image/' + type, quality) return {base64, imageHeight, imageWidth} }
takeBase64()
方法调用 toDataURL()
方法并返回它的 base64
值。注意,你可以指定图像类型和图像质量。
<simple-camera></simple-camera> <button id="btnBlobPhoto">Take Blob</button> <button id="btnBase64Photo">Take Base64</button> <script> (async function() { const camera = document.querySelector('simple-camera') const btnBlobPhoto = document.querySelector('#btnBlobPhoto') const btnBase64Photo = document.querySelector('#btnBase64Photo') await camera.open({video: {facingMode: 'user'}}) btnBlobPhoto.addEventListener('click', async event => { const photo = await camera.takeBlobPhoto() }) btnBase64Photo.addEventListener('click', async event => { const photo = camera.takeBase64Photo({type: 'jpeg', quality: 0.8}) }) }()) </script>
把所有代码结合到一起:
<script> class SimpleCamera extends HTMLElement { constructor() { super(); } connectedCallback() { const shadow = this.attachShadow({ mode: 'open' }) this.videoElement = document.createElement('video') this.canvasElement = document.createElement('canvas') this.videoElement.setAttribute('playsinline', true) this.canvasElement.style.display = 'none' shadow.appendChild(this.videoElement) shadow.appendChild(this.canvasElement) } open(constraints) { return navigator.mediaDevices.getUserMedia(constraints).then((mediaStream) => { this.videoElement.srcObject = mediaStream console.log(mediaStream) this.videoElement.onloadedmetadata = (e) => { this.videoElement.play() } }) } _drawImage() { const imageWidth = this.videoElement.videoWidth const imageHeight = this.videoElement.videoHeight const context = this.canvasElement.getContext('2d') this.canvasElement.width = imageWidth this.canvasElement.height = imageHeight context.drawImage(this.videoElement, 0, 0, imageWidth, imageHeight) return { imageHeight, imageWidth } } takeBlobPhoto() { const {imageHeight, imageWidth} = this._drawImage() this.canvasElement.style.display="block" const card = document.createElement('div') card.classList.add('card') document.querySelector('.wrapper').appendChild(card) card.appendChild(this.canvasElement) return new Promise((resolve, reject) => { this.canvasElement.toBlob((blob) => { resolve({blob, imageHeight, imageWidth}) }) }) } takeBase64Photo({type, quality} = {type: 'png', quality: 1}) { const {imageHeight, imageWidth} = this._drawImage() const base64 = this.canvasElement.toDataURL('image/' + type, quality) this.canvasElement.style.display="block" const card = document.createElement('div') card.classList.add('card') document.querySelector('.wrapper').appendChild(card) card.appendChild(this.canvasElement) return {base64, imageHeight, imageWidth} } } customElements.define('simple-camera', SimpleCamera) </script> <div class="wrapper"> <div class="card"> <simple-camera></simple-camera> <div class="active"> <button id="btnBlobPhoto">Take Blob</button> <button id="btnBase64Photo">Take Base64</button> </div> </div> </div> <script> (async function() { const camera = document.querySelector('simple-camera') const btnBlobPhoto = document.querySelector('#btnBlobPhoto') const btnBase64Photo = document.querySelector('#btnBase64Photo') await camera.open({video: {facingMode: 'user'}}) btnBlobPhoto.addEventListener('click', async event => { const photo = await camera.takeBlobPhoto() }) btnBase64Photo.addEventListener('click', async event => { const photo = camera.takeBase64Photo({type: 'jpeg', quality: 0.8}) }) }()) </script>
Demo效果如下:
以上所述就是小编给大家介绍的《如何构建一个简单的摄像头组件》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 8千个城市2千万摄像头暴露:2018摄像头安全报告
- [译] 充分利用多摄像头 API
- html5调用手机摄像头
- jQuery webcam plugin调用摄像头
- 实战:如何使用 JavaScript 访问设备前后摄像头
- 黑客通过摄像头偷删警察蜀黍文件
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。