背景
由于h5通过 <input type="file />
方式吊起拍照的为系统相机,给用户的体验并不是很好,没有裁切框,也无法在系统相机上附加 tips
蒙层进行扩展,比如在蒙层上告知用户拍照的注意事项。所以业务上需要实现一个自定义拍照身份证的页面。
前期准备工作
各端兼容性现状
结论: 安卓:chrome53版本之后支持该api。 ios:仅safari11+支持。ios微信内置浏览器、Chrome、Edge等其它浏览器均不支持。
考虑替代方案
以下情况,均需要考虑替代方案: 第一类:当满足下列条件,均需要采用系统相机拍照方案 1.用户不提供摄像头权限。 2.命中以下其中任意一条错误
AbortError
[中止错误]NotAllowedError
[拒绝错误]NotFoundError
[找不到错误]NotReadableError
[无法读取错误]OverConstrainedError
[无法满足要求错误]SecurityError
[安全错误]TypeError
[类型错误]
3.用户浏览器不支持该api ** 第二类:当ios用户使用非safari浏览器访问h5页面时 由于ios只有safari11+可以吊起后置摄像头视频流,如果ios用户在非safari浏览器打开h5登陆页,都要直接引导用户复制链接到safari浏览器打开,避免接下来无法进行自定义拍照。这里牛客做的就比较好,可以仿照牛客做一个引导按钮。
正片开始
我们的主角是 MediaDevices.getUserMedia()
, MDN
对该api的介绍如下
能力检测
由于不同浏览器对于标准的实现不一致,需要作api能力的兼容,避免用户浏览器无法正常调用该api。
//访问用户媒体设备的兼容方法
function getUserMedia(constrains) {
if (navigator.mediaDevices?.getUserMedia) {
//最新标准API
return navigator.mediaDevices.getUserMedia(constrains);
} else if (navigator.webkitGetUserMedia) {
//webkit内核浏览器
return navigator.webkitGetUserMedia(constrains);
} else if (navigator.mozGetUserMedia) {
//Firefox浏览器
return navigator.mozGetUserMedia(constrains);
} else if (navigator.getUserMedia) {
//旧版API
return navigator.getUserMedia(constrains);
}
}
在页面上放置一个video元素
<video
id="video"
autoPlay
muted
playsInline
style={{
width: '100%',
}}
></video>
有几个注意点⚠️
对于这两种类型的视频,可以通过 <video autoplay>
或 video.play()
两种方式来自动播放,无需用户主动操作。但是,如果它们在播放时变得有声音(获取了音轨,或者 muted
属性被取消),Safari 会暂停播放。
- 只有提供
muted
属性,让视频静音,才可以通过<video autoplay>
或video.play()
两种方式来进行播放 - 必须提供
playsInline
属性,不然在ios上会只播放一帧
调用封装好的getUserMedia,获取用户媒体流
调用时,我们可以给constrains
对象可以多种不同的值,来获取用户设备底层各种不同的媒体流。
video: true
(默认调取前置摄像头)- 为了调取后置摄像头,需要通过
facingMode: { exact: 'environment' }
来进行调用**(如果后置摄像头不存在,则会导致获取媒体流失败 - 为了获取特定分辨率的视频流,我们可以指定相应的
width
height
(但这种方式有缺陷,一旦用户设备不存在对于像素流,则会导致获取媒体流失败,所以,我们不对像素进行定制,使用自动获取到的媒体流像素)
/**
* 该函数需要接受一个video的dom节点作为参数
*/
function getUserMediaStream(videoNode) {
/**
* 调用api成功的回调函数
*/
function success(stream, video) {
return new Promise((resolve, reject) => {
video.srcObject = stream;
video.onloadedmetadata = function () {
video.play();
resolve();
};
});
}
//调用用户媒体设备,访问摄像头
return getUserMedia({
audio: false,
video: { facingMode: { exact: 'environment' } },
// video: true,
// video: { facingMode: { exact: 'environment', width: 1280, height: 720 } },
})
.then(res => {
return success(res, videoNode);
})
.catch(error => {
console.log('访问用户媒体设备失败:', error.name, error.message);
return Promise.reject();
});
}
当前效果:
增加裁切框和外部阴影
- 裁切框我们根据需求写到页面中,之后会通过
getBoundingClientRect
获取裁切框的位置进行裁切。 - 外部阴影使用
box-shadow
即可
<div className={styles['shadow-layer']} style={{ height: `${videoHeight}px` }}>
<div id="capture-rectangle" className={styles['capture-rectangle']}></div>
</div>
@function remB($px) {
@return ($px/75) * 1rem;
}
.shadow-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 1;
overflow: hidden;
.capture-rectangle {
margin: remB(200) auto 0;
width: remB(700); // 这里写上我们需要裁切的宽
height: remB(450); // 这里写上我们需要裁切的高
border: 1px solid #fff;
border-radius: remB(20);
z-index: 2;
box-shadow: 0 0 0 remB(1000) rgba(0, 0, 0, 0.7); // 外层阴影
}
}
当前效果:
完成实时照片裁切,上传服务端进行OCR识别
裁切用到的是 canvas.getContext('2d).drawImage
的能力。
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
可以我们传入 video
作为 source
进行裁切。
这里要注意
sx
和sy
对应的是距离真实video
元素的top
left
距离,不是页面中video
的大小,拿到裁切框位置大小之后,需要做转换,再进行裁切,否则裁切位置会对不上。
/**
* 获取video中对应的真实size
*/
function getXYRatio() {
// videoHeight为video 真实高度
// offsetHeight为video css高度
const { videoHeight: vh, videoWidth: vw, offsetHeight: oh, offsetWidth: ow } = video;
return {
yRatio: height => {
return (vh / oh) * height;
},
xRatio: width => {
return (vw / ow) * width;
},
};
}
在调用 getUserMediaStream
成功之后,我们开始捕捉视频流,每隔几秒进行截图,发送到服务器。
/** 裁切上传相关核心代码 */
const Photo = () => {
const [videoHeight, setVideoHeight] = useState(0);
const ref = useRef(null);
useEffect(() => {
const video = document.getElementById('video');
const rectangle = document.getElementById('capture-rectangle');
const _canvas = document.createElement('canvas');
_canvas.style.display = 'block';
getUserMediaStream(video)
.then(() => {
setVideoHeight(video.offsetHeight);
startCapture();
})
.catch(err => {
showFail({
text: '无法调起后置摄像头,请点击相册,手动上传身份证',
duration: 6,
});
});
function startCapture() {
ref.current = setInterval(() => {
const { yRatio, xRatio } = getXYRatio();
/** 获取裁切框的位置 */
const { left, top, width, height } = rectangle.getBoundingClientRect();
const context = _canvas.getContext('2d');
_canvas.width = width;
_canvas.height = height;
// void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
context.drawImage(
video,
xRatio(left + window.scrollX),
yRatio(top + window.scrollY),
xRatio(width),
yRatio(height),
0,
0,
width,
height,
);
// 获取当前截图的base64编码
const base64 = _canvas.toDataURL('image/jpeg');
// 这里可以再根据场景做base64压缩
// 每2秒调用OCR接口,上传base64到服务端进行识别
}, 2000);
}
/** 清空定时器 */
return () => clearInterval(ref.current);
}, []);
}
注意:
sx
、 sy
的值是相对根元素的,通过 getBoundingClientRect
拿到的 top
和 left
是相当于视口的,需要加上 scroll
的值。
结语
实际上 getUserMedia
在安卓和 MacOs
上跑起来几乎没有问题,但是社区中对于该 api
的讨论太少了,可能大部分人甚至不知道这个 api
的存在,在 ios
真机上进行调试时,一开始只展示有一帧,便静止了,报错不会给予开发者比较详细的提示,我一开始大部分时间都花在了研究 ios
端为什么无法正常调用该 api
。不过这种业务场景在 app
上应该是比较常见的,本文仅为h5该业务场景的实现方式。
附一张最终效果图:
References
1.iOS13 getUserMedia not working on chrome and edge stackoverflow.com/questions/6… bugs.webkit.org/show_bug.cg… It prevents ALL other browsers on iOS to offer video-conferencing, while Safari can => it's a nasty anti-competitive behaviour that will for sure be scrutinized by US House Antitrust Committee & EU Commission, and Apple should not accumulate evidence of evil conduct.
2.MDN getUserMedia developer.mozilla.org/zh-CN/docs/…
3.ios10+视频播放新策略 imququ.com/post/new-vi…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!