如何用 WebRTC 给自己拍照?

389次阅读  |  发布于2年以前

前言

最近一直在看 WebRTC 的用法,也学了一下音视频流的东西,今天就跟大家分享一个好玩的小实战吧——给自己拍照。

项目已上传至 Github,Repo 地址:https://github.com/haixiangyan/webrtc-take-photo[1]

页面结构

首先,我们要拆分一下实现步骤:

因此,我们需要 <video>, <img> 以及 <button>

<head>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
  <link rel="stylesheet" href="styles.css">
</head>
<body>

<main>
  <video id="video">浏览器不支持 Video</video>

  <canvas id="canvas">
    <img id="photo" alt="拍照后的照片">
  </canvas>
</main>

<div class="actions">
  <button id="takePhotoButton" type="button" class="btn btn-primary">拍照</button>
  <button id="downloadButton" type="button" class="btn btn-success">下载</button>
  <button id="clearButton" type="button" class="btn btn-danger">清空</button>
</div>

<script src="./main.js"></script>

</body>

再加上点 CSS,让整个 App 好看一点~

main {
    padding: 24px 24px 16px;
    display: flex;
}

video {
    margin-right: 16px;
    box-sizing: content-box;
    border: 4px solid #ffaabb;
}

canvas {
    box-sizing: content-box;
    border: 4px solid #aabbff;
}

.actions {
    padding: 0 24px;
}

.actions button {
    margin-right: 16px;
}

左边为摄像头的 <video>,右边则是拍照的图片。

打开摄像头

打开摄像头这一步,其实是调用了 WebRTC 的一个重要接口 navigator.mediaDevices.getUserMedia,通过这个接口不仅可以完成用户对摄像头的使用授权,还可以从返回值里直接拿到视频流:


const start = async () => {
  video = document.getElementById('video');
  canvas = document.getElementById('canvas');
  photo = document.getElementById('photo');
  takePhotoButton = document.getElementById('takePhotoButton');
  downloadButton = document.getElementById('downloadButton');

  // 获取摄像头的视频流
  try {
    video.srcObject = await navigator.mediaDevices.getUserMedia({video: true, audio: false})
    video.play()
  } catch (e) {
    console.error(e)
  }
}

start().then()

将视频流接到 <video> 元素的 srcObject 上,再调用一下 video.play() 就可以展示视频流了:

拍照

从 HTML 结构里我们可以看到 <canvas> 里面还藏着一个 <img>。这里 <canvas> 的作用是负责从视频流中生成图片数据,再将这个数据放到 <img>src 上,这样就完成了我们的拍照功能了。

const takePhoto = () => {
  const context = canvas.getContext('2d')
  if (width && height) {
    // 将 video 元素的 width 和 height 拿过来
    canvas.width = width;
    canvas.height = height;

    context.drawImage(video, 0, 0, width, height);

    // 生成图片
    const data = canvas.toDataURL('image/png');
    photo.setAttribute('src', data);
  } else {
    clearPhoto()
  }
}

不过在调用 drawImage 的时候要传入对应的宽和高,这里的宽和高可以从 <video> 元素中的 widthheight 获得。

const start = async () => {
  video = document.getElementById('video');
  canvas = document.getElementById('canvas');
  photo = document.getElementById('photo');
  takePhotoButton = document.getElementById('takePhotoButton');
  downloadButton = document.getElementById('downloadButton');

  // 获取摄像头的视频流
  try {
    video.srcObject = await navigator.mediaDevices.getUserMedia({video: true, audio: false})
    video.play()
  } catch (e) {
    console.error(e)
  }

  video.addEventListener('canplay', (event) => {
    if (!streaming) {
      // 按比例放大 videoHeight
      height = video.videoHeight / (video.videoWidth / width);

      // 设置 video 的宽高
      video.setAttribute('width', width);
      video.setAttribute('height', height);
      // 设置 canvas 的宽高
      canvas.setAttribute('width', width);
      canvas.setAttribute('height', height);
      streaming = true;
    }
  }, false)

  takePhotoButton.addEventListener('click', (event) => {
    // 拍照
    takePhoto()
  }, false)
}

start().then()

这样一来就能生成视频中定格后的某一个画面了。

从上面可以看到这里的 <img> 里的 srcdata:xxx 的图片数据。

清空图片

如果要清除已经拍好的照片呢?我们可以利用 <canvas>fillRect 来生成一个空白图片,然后再转化成图片数据,放到 src 里就可以了:

// 清空操作
const clearPhoto = () => {
  const context = canvas.getContext('2d')
  // 生成空白图片
  context.fillStyle = "#AAA";
  context.fillRect(0, 0, canvas.width, canvas.height);
  const data = canvas.toDataURL('image/png');
  photo.setAttribute('src', data);
}

// 开始
const start = async () => {
  // ...

  clearButton.addEventListener('click', (event) => {
    clearPhoto();
  })

  // 生成默认空白图片
  clearPhoto();
}

start().then()

或许有的人会觉得:我把图片 v-if=falsedisplay: nonevisibility: hidden 不也可以嘛?

当然可以!这里我只是想再分享另一种思路嘛~ 因为像这种调用 fillRect 来做重置功能的是比较常用的,比较画板里的重置就可以这样来清空画布。

下载

下载则比较简单了,也是面试常考的一道技巧题。

先生成一个 <a> 标签,然后通过 <canvas> 生成 URL,将这个 URL 放到 href 里,用 JS 出发 click 事件,就可模拟下载了:

// 下载操作
const downloadPhoto = () => {
  const link = document.createElement('a');
  link.download = '你的帅照.png';
  link.href = canvas.toDataURL();
  link.click();
}

const start = async () => {
  // ...

  downloadButton.addEventListener('click', (event) => {
    // 下载
    downloadPhoto()
  })
}

start().then()

总结

到这里,这个小实战就结束啦,并没有什么难度,这里稍微做下总结吧。

WebRTC 最重要的 API 就是 await navigator.mediaDevices.getUserMedia({video: true, audio: false}),通过返回值可以获取当前摄像头、麦克风的音视频流。

通过 <video> 元素的 srcObject 属性可以直接接上视频流,这在直播、P2P、视频聊天的场景都可以这样使用。

通过 <canvas>drawImage 以及 fillRect 来生成视频图片以及空白图片数据,再把这些数据放到 <img> 就可以实现 JS 生成画面的效果。

如果你也喜欢我的文章,也可以关注我,你的三连是我最大的动力~

参考资料

[1]项目地址: https://github.com/haixiangyan/webrtc-take-photo

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8