题外话
彦祖们,前断时间去做梅农了,没有更新文章
先带大家看一看,余姚杨梅山的九色风光
如果说这些照片,我是用前端 ImageCapture
api 拍的,彦祖们你们相信吗?
评论区请回答
- 山上偶遇的狗子
-
凌晨四点的梅农
-
拍的不错的杨梅
-
溪水太凉快了
-
忙着快递发货
前言
阅读此文前,希望彦祖们先去了解下这两个 API
当然不想了解就直接跳转看完整代码
需要注意的是,此功能仅在本地环境或者线上的安全环境(HTTPS)下可用
场景
言归正传,在日常业务开发中,我们经常会遇到调用摄像头的场景
最常见的就是前端调用摄像头,然后生成一张照片,用于业务开发
正式开发
常规开发
首先我们来看看常规开发模式
代码其实非常简单,直接上代码(已附上完整注释)
渲染视频流
js
const video = document.querySelector('#video')
// 获取设备列表(注意这是异步 api, 一定要获取到 deviceId 后再渲染)
navigator.mediaDevices.enumerateDevices().then((devices) => {
// 过滤摄像头列表
const videoDevices = devices.filter(
(device) => device.kind === 'videoinput'
)
if(!videoDevices) return console.error(`暂无摄像头`)
deviceId = videoDevices[0].deviceId
// 渲染视频流
renderStream(deviceId)
})
function renderStream(deviceId){
// 获取视频流
// 其他参数见 https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#constraints
const constraints = {
video:{
width: 500,
height: 500,
deviceId
}
}
navigator.mediaDevices.getUserMedia(constraints).then(stream => {
// 获取到视频流后 赋值给 video 渲染
video.srcObject = stream
// 数据加载完后进行播放
video.onloadedmetadata = () => {
video.play()
}
})
.catch((err) => {
console.log(err.name + ': ' + err.message)
})
}
拍照功能
常规的思路,我们肯定是截取 video 的某一帧,然后渲染到 canvas 上面,利用 canvas.toDataURL
转换成数据
js
function generateScreenshot(video) {
// 创建一个canvas元素
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// 在canvas上绘制视频帧
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
// 将canvas转换为图片地址
const imageSrc = canvas.toDataURL('image/png');
// 返回图片地址
return imageSrc;
}
其实也是因为笔者在真实项目开发中遇到的
canvas.drawImage
在 chromium
有个内存泄露的问题
详见 issues.chromium.org/issues/4047...
才进行了优化后的改造
注意点
vue
等单页面项目特别要注意的是,在 beforeDestroy
钩子函数我们需要把视频流给释放
js
beforeDestroy(){
// 关闭视频流
this.stream.getTracks().forEach(track => {
track.enabled = false
track.stop()
stream.removeTrack(track)
})
// 别忘记把 video.srcObject 置空, 否则会导致内存泄露
video.srcObject = null
}
改用 useUserMedia hook
先上完整代码,便于后续阅读
完整代码
- useUserMedia.js
js
function useUserMedia(constraints) {
if (!navigator.mediaDevices) {
return `navigator.mediaDevices is undefined`
}
return new Promise((resolve, reject) => {
navigator.mediaDevices.getUserMedia(constraints)
.then(stream => {
// 提供停止视频流的方法
const stop = () => {
stream.getTracks().forEach(track => {
track.enabled = false
track.stop()
stream.removeTrack(track)
})
}
// 提供视频流轨道截图实例
const track = stream.getVideoTracks()[0]
const imageCapture = new ImageCapture(track)
resolve({
stream,
stop,
imageCapture
})
})
.catch(reject)
})
}
stop
方法就不做赘述了, 就是基于上面的封装了一下
主要介绍下 ImageCapture api
实验性的:这是一项实验性技术。在将其用于生产之前,请仔细查看浏览器兼容性表。
它可以从 MediaStreamTrack
中捕获静止帧(也就是我们所说的拍照功能)
下文会对拍照功能做详解
渲染视频流
有了这个 hook 我们渲染视频流就非常简单了
js
renderStream(deviceId){
const constraints = {
video:{
width: 500,
height: 500,
deviceId
}
}
const {stream,stop,imageCapture} = useUserMedia(constraints)
this.imageCapture = imageCapture
this.stop = stop
video.srcObject = stream
video.onloadedmetadata = () => {
video.play()
}
}
beforeDestroy(){
this.stop() // 停止视频流
}
拍照功能
重点来看下使用 hook 的拍照功能有多方便吧
js
function takePhoto(){
this.imageCapture.takePhoto().then((blob) => {
// 这里可以按业务需求改造
// const file = new File([blob],'test.jpg') // 生成文件
// const imgSrc = URL.createObjectURL(blob); // 生成软连接地址,别忘记 revokeObjectURL;
})
}
本期内容,主要还是代码,没有过多的花里胡哨的注释
相信彦祖们能够一眼秒懂
主要还是对于使用业务的 api 的使用和调研
完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<video id="video">
</video>
<img width="200" height="200" id="img"/>
<button>拍照</button>
</body>
<script>
function useUserMedia(constraints) {
if (!navigator.mediaDevices) {
return `navigator.mediaDevices is undefined`
}
return new Promise((resolve, reject) => {
navigator.mediaDevices.getUserMedia(constraints)
.then(stream => {
// 提供停止视频流的方法
const stop = () => {
stream.getTracks().forEach(track => {
track.enabled = false
track.stop()
stream.removeTrack(track)
})
}
// 提供视频流轨道截图实例
const track = stream.getVideoTracks()[0]
const imageCapture = new ImageCapture(track)
resolve({
stream,
stop,
imageCapture
})
})
.catch(reject)
})
}
const video = document.querySelector('#video')
const img = document.querySelector('#img')
// 获取设备列表(注意这是异步 api, 一定要获取到 deviceId 后再渲染)
navigator.mediaDevices.enumerateDevices().then((devices) => {
// 过滤摄像头列表
const videoDevices = devices.filter(
(device) => device.kind === 'videoinput'
)
if(!videoDevices) return console.error(`暂无摄像头`)
deviceId = videoDevices[0].deviceId
// 渲染视频流
renderStream(deviceId)
})
let _imageCapture = null
async function renderStream(deviceId){
const constraints = {
video:{
width:200,
height:200,
deviceId
}
}
const {stream,imageCapture} = await useUserMedia(constraints)
_imageCapture = imageCapture
video.srcObject = stream
video.onloadedmetadata = () => {
video.play()
}
}
document.querySelector('button').addEventListener('click',takePhoto)
function takePhoto(){
_imageCapture.takePhoto().then((blob) => {
const imgSrc = URL.createObjectURL(blob); // 生成软连接地址,别忘记 revokeObjectURL;
img.src = imgSrc
})
}
</script>
</html>
写在最后
一周的梅农,一辈子的码农,牛马一生,唉~
感谢彦祖们的阅读
个人能力有限
如有不对,欢迎指正 🌟 如有帮助,建议小心心大拇指三连🌟