当然不想了解就直接跳转看完整代码

需要注意的是,此功能仅在本地环境或者线上的安全环境(HTTPS)下可用

言归正传,在日常业务开发中,我们经常会遇到调用摄像头的场景

最常见的就是前端调用摄像头,然后生成一张照片,用于业务开发

常规开发

首先我们来看看常规开发模式

代码其实非常简单,直接上代码(已附上完整注释)

渲染视频流

 1const video = document.querySelector('#video')
 2
 3navigator.mediaDevices.enumerateDevices().then((devices) => {
 4  
 5  const videoDevices = devices.filter(
 6    (device) => device.kind === 'videoinput'
 7  )
 8  if(!videoDevices) return console.error(`暂无摄像头`)
 9  deviceId = videoDevices[0].deviceId
10  
11  renderStream(deviceId)
12})
13
14function renderStream(deviceId){
15  
16  
17  const constraints = {
18    video:{
19      width: 500,
20      height: 500,
21      deviceId
22    }
23  }
24  navigator.mediaDevices.getUserMedia(constraints).then(stream => {
25    
26    video.srcObject = stream
27    
28    video.onloadedmetadata = () => {
29      video.play()
30    }
31  })
32  .catch((err) => {
33    console.log(err.name + ': ' + err.message)
34  })
35}

拍照功能

常规的思路,我们肯定是截取 video 的某一帧,然后渲染到 canvas 上面,利用 canvas.toDataURL 转换成数据

 1function generateScreenshot(video) {
 2  
 3  const canvas = document.createElement('canvas');
 4  canvas.width = video.videoWidth;
 5  canvas.height = video.videoHeight;
 6
 7  
 8  canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
 9
10  
11  const imageSrc = canvas.toDataURL('image/png');
12
13  
14  return imageSrc;
15}

其实也是因为笔者在真实项目开发中遇到的

canvas.drawImagechromium 有个内存泄露的问题

详见 issues.chromium.org/issues/4047…

才进行了优化后的改造

注意点

vue 等单页面项目特别要注意的是,在 beforeDestroy 钩子函数我们需要把视频流给释放

 1beforeDestroy(){
 2  
 3  this.stream.getTracks().forEach(track => {
 4    track.enabled = false
 5    track.stop()
 6    stream.removeTrack(track)
 7  })
 8  
 9  video.srcObject = null
10}

改用 useUserMedia hook

先上完整代码,便于后续阅读

完整代码

  • useUserMedia.js
 1function useUserMedia(constraints) {
 2  if (!navigator.mediaDevices) {
 3    return `navigator.mediaDevices is undefined`
 4  }
 5  return new Promise((resolve, reject) => {
 6    navigator.mediaDevices.getUserMedia(constraints)
 7      .then(stream => {
 8        
 9        const stop = () => {
10          stream.getTracks().forEach(track => {
11            track.enabled = false
12            track.stop()
13            stream.removeTrack(track)
14          })
15        }
16        
17        const track = stream.getVideoTracks()[0]
18        const imageCapture = new ImageCapture(track)
19        resolve({
20          stream,
21          stop,
22          imageCapture
23        })
24      })
25      .catch(reject)
26  })
27}

stop 方法就不做赘述了, 就是基于上面的封装了一下

主要介绍下 ImageCapture api

实验性的:这是一项实验性技术。在将其用于生产之前,请仔细查看浏览器兼容性表。

它可以从 MediaStreamTrack 中捕获静止帧(也就是我们所说的拍照功能)

下文会对拍照功能做详解

渲染视频流

有了这个 hook 我们渲染视频流就非常简单了

 1renderStream(deviceId){
 2  const constraints = {
 3    video:{
 4      width: 500,
 5      height: 500,
 6      deviceId
 7    }
 8  }
 9  const {stream,stop,imageCapture} = useUserMedia(constraints)
10  this.imageCapture = imageCapture
11  this.stop = stop 
12  video.srcObject = stream
13  video.onloadedmetadata = () => {
14    video.play()
15  }
16}
17
18beforeDestroy(){
19  this.stop() 
20}

拍照功能

重点来看下使用 hook 的拍照功能有多方便吧

 1function takePhoto(){
 2  this.imageCapture.takePhoto().then((blob) => {
 3    
 4    
 5    
 6  })
 7}

本期内容,主要还是代码,没有过多的花里胡哨的注释

相信彦祖们能够一眼秒懂

主要还是对于使用业务的 api 的使用和调研

 1<!DOCTYPE html>
 2<html lang="en">
 3
 4<head>
 5  <meta charset="UTF-8">
 6  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7  <title>Document</title>
 8</head>
 9
10<body>
11  <video id="video">
12
13  </video>
14
15  <img width="200" height="200" id="img"/>
16
17  <button>拍照</button>
18</body>
19<script>
20function useUserMedia(constraints) {
21  if (!navigator.mediaDevices) {
22    return `navigator.mediaDevices is undefined`
23  }
24  return new Promise((resolve, reject) => {
25    navigator.mediaDevices.getUserMedia(constraints)
26      .then(stream => {
27        
28        const stop = () => {
29          stream.getTracks().forEach(track => {
30            track.enabled = false
31            track.stop()
32            stream.removeTrack(track)
33          })
34        }
35        
36        const track = stream.getVideoTracks()[0]
37        const imageCapture = new ImageCapture(track)
38        resolve({
39          stream,
40          stop,
41          imageCapture
42        })
43      })
44      .catch(reject)
45  })
46}
47
48const video = document.querySelector('#video')
49const img = document.querySelector('#img')
50
51navigator.mediaDevices.enumerateDevices().then((devices) => {
52  
53  const videoDevices = devices.filter(
54    (device) => device.kind === 'videoinput'
55  )
56  if(!videoDevices) return console.error(`暂无摄像头`)
57  deviceId = videoDevices[0].deviceId
58  
59  renderStream(deviceId)
60})
61let _imageCapture = null
62async function  renderStream(deviceId){
63    const constraints = {
64      video:{
65        width:200,
66        height:200,
67        deviceId
68      }
69    }
70    const {stream,imageCapture} = await useUserMedia(constraints)
71    _imageCapture = imageCapture
72    video.srcObject = stream
73    video.onloadedmetadata = () => {
74      video.play()
75    }
76}
77
78document.querySelector('button').addEventListener('click',takePhoto)
79
80function takePhoto(){
81  _imageCapture.takePhoto().then((blob) => {
82    const imgSrc = URL.createObjectURL(blob); 
83    img.src = imgSrc
84  })
85}
86
87</script>
88
89</html>

一周的梅农,一辈子的码农,牛马一生,唉~

感谢彦祖们的阅读

个人能力有限

_如有不对,欢迎指正_🌟 _如有帮助,建议小心心大拇指三连_🌟

个人笔记记录 2021 ~ 2025