demo源码:https://github.com/ericlee33/h5-id-card ,如果觉得有帮助的话,欢迎点个star👏
背景
由于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浏览器打开,避免接下来无法进行自定义拍照。这里牛客做的就比较好,可以仿照牛客做一个引导按钮。
正片开始
本文不涉及替代方案的兼容逻辑,可以自行在
Promise.reject()
时进行对应处理。
我们的主角是 MediaDevices.getUserMedia()
, MDN
对该api的介绍如下
MediaDevices.getUserMedia()
会提示用户给予使用媒体输入的许可,媒体输入会产生一个MediaStream
,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。
能力检测
由于不同浏览器对于标准的实现不一致,需要作api能力的兼容,避免用户浏览器无法正常调用该api。
1 | //访问用户媒体设备的兼容方法 |
在页面上放置一个video元素
1 | <video |
有几个注意点⚠️
iOS 10 Safari 允许自动播放以下两种视频:
- 无音轨视频;
- 无声音视频(设置了
muted
属性);
对于这两种类型的视频,可以通过 <video autoplay>
或 video.play()
两种方式来自动播放,无需用户主动操作。但是,如果它们在播放时变得有声音(获取了音轨,或者 muted
属性被取消),Safari 会暂停播放。
- 只有提供
muted
属性,让视频静音,才可以通过<video autoplay>
或video.play()
两种方式来进行播放 - 必须提供
playsInline
属性,不然在ios上会只播放一帧
调用封装好的getUserMedia,获取用户媒体流
调用时,我们可以给constrains
对象可以多种不同的值,来获取用户设备底层各种不同的媒体流。
video: true
(默认调取前置摄像头)- 为了调取后置摄像头,需要通过
facingMode: { exact: 'environment' }
来进行调用**(如果后置摄像头不存在,则会导致获取媒体流失败 - 为了获取特定分辨率的视频流,我们可以指定相应的
width
height
(但这种方式有缺陷,一旦用户设备不存在对于像素流,则会导致获取媒体流失败,所以,我们不对像素进行定制,使用自动获取到的媒体流像素)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/**
* 该函数需要接受一个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
即可1
2
3<div className={styles['shadow-layer']} style={{ height: `${videoHeight}px` }}>
<div id="capture-rectangle" className={styles['capture-rectangle']}></div>
</div>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22@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);
MDN对该属性说明:
image
绘制到上下文的元素。允许任何的 canvas 图像源(CanvasImageSource
),例如:CSSImageValue
(en-US),HTMLImageElement
,SVGImageElement
(en-US),HTMLVideoElement
,HTMLCanvasElement
,ImageBitmap
或者OffscreenCanvas
。
https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/drawImage
可以我们传入 video
作为 source
进行裁切。
这里要注意
sx
和sy
对应的是距离真实video
元素的top
left
距离,不是页面中video
的大小,拿到裁切框位置大小之后,需要做转换,再进行裁切,否则裁切位置会对不上。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 获取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
成功之后,我们开始捕捉视频流,每隔几秒进行截图,发送到服务器。
1 | /** 裁切上传相关核心代码 */ |
注意: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
https://stackoverflow.com/questions/63084076/ios13-getusermedia-not-working-on-chrome-and-edge
https://bugs.webkit.org/show_bug.cgi?id=208667
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 https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
3.ios10+视频播放新策略 https://imququ.com/post/new-video-policies-for-ios10.html