一个 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>

写在最后

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

感谢彦祖们的阅读

个人能力有限

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

相关推荐
小白学习日记33 分钟前
【复习】HTML常用标签<table>
前端·html
程序员大金36 分钟前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
john_hjy37 分钟前
11. 异步编程
运维·服务器·javascript
风清扬_jd1 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo1 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀1 小时前
CSS——属性值计算
前端·css
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
李是啥也不会2 小时前
数组的概念
javascript