发生什么事了
最近接到一个需求,要在前端调用摄像头,拍下照片然后进行裁剪(因为要进行图像识别所以对图片范围有要求)。
众所周知,H5 提供了调用媒体设备的接口,那么我们的思路就比较清晰了,首先调用摄像头获取视频流,然后将视频流设置给一个 video 标签显示图像内容。当拍照的时候,我们在 video 中截取一帧画在 canvas 上面,然后再在canvas上面进行裁剪,最后将裁剪的部分转为 base64 格式用来保存/上传。
主要方案
- 调用媒体设备获取视频流,交给 video 标签,展示摄像头内容
- 将 video 的一帧画在 canvas 上面,达成拍照效果
- 在 canvas 上面裁剪,然后转为 base64 格式输出
(不知道拍点什么就用手指头盖住了🤪)
代码
调用摄像头
// 判断是否支持
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({
// 不需要音频
audio: false,
// 配置分辨率
video: {
width: {
ideal: 300,
},
height: {
ideal: 300,
},
// 帧率
// frameRate: { ideal: 10, max: 15 } }
// 前置/后置摄像头
// facingMode: "user", // 后置为 environment
},
})
.then(function (stream) {
const video = document.getElementById('video');
// video 显示摄像头影像
video.srcObject = stream;
// 需要调用播放 API
video.play();
})
.catch((e) => {
console.error(e);
console.log('未获得授权');
});
} else {
console.log('未获取到权限');
}
这里我们使用了 Navigator.mediaDevices.getUserMedia API 获取视频流,这个接口返回一个 Promise 对象,这时候用户会收到权限申请,如果授权我们会取到一个 MediaStream 对象,然后赋值给 video 标签的 srcObject 属性,最后调用播放就可以看到影像了。
tips:
- 视频流的分辨率最好和 video 标签比例相同,否则会导致黑边的情况。
- 同比例的情况下分辨率越高影像越清晰。
- 适当降低帧率可以降低资源占用。
拍照
我们将 video 的一帧画在 canvas 上面,然后就可以轻易的导出常见的图片格式
const video = document.querySelector('#video');
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
video && ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const url = canvas.toDataURL('image/png');
// 下载图片
const aElement = document.createElement("a");
aElement.href = url
aElement.download = "测试照片";
document.body.append(aElement);
aElement.click();
document.body.removeChild(aElement);
tips:
- 这里需要注意 canvas 绘图的范围的比例要和前面的 video 一致,否则会导致图片拉伸变形
图片裁剪
这里介绍一个库 react-cropper,它基于 cavas 开发,支持传入图片 url 进行裁剪,同时支持导出。
<Cropper
ref={cropperRef}
style={{
width: "300px",
height: "300px",
position: "absolute",
top: 0,
left: 0,
visibility: visible.showCanvas ? "visible" : "hidden",
}}
zoomTo={1}
initialAspectRatio={1}
preview=".img-preview"
src={imgSrc}
viewMode={1}
minCropBoxHeight={100}
minCropBoxWidth={100}
background={false}
responsive={true}
autoCropArea={1}
checkOrientation={false}
guides={true}
/>
// 导出
const url = cropperRef.current.cropper
.getCroppedCanvas()
.toDataURL("image/png");
当然大佬们也可以自己实现类似功能,直接在 canvas 上面实现裁剪功能
完整组件代码
import React, { useEffect, useState, useRef } from 'react';
import Cropper, { ReactCropperElement } from 'react-cropper';
import 'cropperjs/dist/cropper.css';
import './style.css';
export default function App() {
const [visible, setVisible] = useState({
showVideo: true,
showResult: false,
});
const [stream, setStream] = useState();
const [imgSrc, setImgSrc] = useState('');
const cropperRef = useRef(null);
useEffect(() => {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({
audio: false,
video: {
// 分辨率
width: {
ideal: 300,
},
height: {
ideal: 300,
},
// 帧率
// frameRate: { ideal: 10, max: 15 } }
},
})
.then(function (stream) {
setStream(stream);
const video = document.getElementById('video');
video.srcObject = stream;
video.play();
})
.catch((e) => {
console.error(e);
console.log('未获得授权');
});
} else {
console.log('未获取到权限');
}
}, []);
const takePhoto = async () => {
if (!stream) return;
try {
const video = document.querySelector('#video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
video && ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const url = canvas.toDataURL('image/png');
setImgSrc(url);
setVisible({
showVideo: false,
showResult: true,
});
} catch (err) {
console.error('takePhoto failed: ', err);
}
};
const save = () => {
const aElement = document.createElement('a');
aElement.href = cropperRef.current.cropper
.getCroppedCanvas()
.toDataURL('image/png');
aElement.download = '测试照片';
document.body.append(aElement);
aElement.click();
document.body.removeChild(aElement);
};
const replay = () => {
setVisible({
showVideo: true,
showResult: false,
});
};
return (
<div class="wrapper">
<video
id="video"
width={300}
height={300}
style={{ visibility: visible.showVideo ? 'visible' : 'hidden' }}
/>
<canvas
id="canvas"
width="300"
height="300"
style={{ visibility: 'hidden' }}
/>
<Cropper
ref={cropperRef}
style={{
width: '300px',
height: '300px',
position: 'absolute',
top: 0,
left: 0,
visibility: visible.showResult ? 'visible' : 'hidden',
}}
zoomTo={1}
initialAspectRatio={1}
preview=".img-preview"
src={imgSrc}
viewMode={1}
minCropBoxHeight={100}
minCropBoxWidth={100}
background={false}
responsive={true}
autoCropArea={1}
checkOrientation={false}
guides={true}
/>
<button
style={{ position: 'absolute', left: '300px', top: 0 }}
onClick={takePhoto}
>
拍照
</button>
{visible.showResult && (
<>
<button
style={{ position: 'absolute', left: '300px', top: 0 }}
onClick={save}
>
保存
</button>
<button
style={{ position: 'absolute', left: '300px', top: '40px' }}
onClick={replay}
>
重拍
</button>
</>
)}
</div>
);
}