前言
哈喽,大家好,我是海怪。
最近一直在看 WebRTC 的用法,也学了一下音视频流的东西,今天就跟大家分享一个好玩的小实战吧——给自己拍照。
**项目已上传至 Github,Repo
页面结构
首先,我们要拆分一下实现步骤:
- 打开摄像头,获取视频流
- 需要一个
<video>
来播放摄像头的画面 - 点击按钮,生成画面,并展示在
<img>
里
因此,我们需要 <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>
元素中的 width
和 height
获得。
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>
里的 src
是 data: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=false
、display: none
、visibility: 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 生成画面的效果。