一个 API,带你打开前端摄像头之门🥳

题外话

彦祖们,前断时间去做梅农了,没有更新文章

先带大家看一看,余姚杨梅山的九色风光

如果说这些照片,我是用前端 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.drawImagechromium 有个内存泄露的问题

详见 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>

写在最后

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

感谢彦祖们的阅读

个人能力有限

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

相关推荐
星就前端叭1 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
m0_748234521 小时前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
Web阿成1 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
噢,我明白了1 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__2 小时前
APIs-day2
javascript·css·css3
苹果醋32 小时前
Golang的文件加密工具
运维·vue.js·spring boot·nginx·课程设计
jwensh2 小时前
【Jenkins】Declarative和Scripted两种脚本模式有什么具体的区别
运维·前端·jenkins
关你西红柿子2 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
益达是我2 小时前
【Chrome】浏览器提示警告Chrome is moving towards a new experience
前端·chrome
济南小草根2 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js