这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
背景
在实际项目中,遇到了需要唤起手机摄像头拍照的需求,最开始是通过<input type="file" hidden accept="image/*" capture="camera" />
的方式,可以直接唤起手机相机,但是用户拍照的方向各式各样,导致后续业务处理时,没有达到预期的效果。
基于此,产品同学期望能在用户拍照时给用户一个引导框(也就是平时我们在用第三方证件拍照时的取景框效果)。经过讨论,给出了两种解决方案,一种是通过我们自研,先尝试看一下效果,第二种是使用第三方的 SDK,仅使用他们的拍照功能。
本文档仅涉及第一种,即通过我们自研的方式,实现 H5 拍照选景框的效果。
技术方案
最终效果示例
核心实现
1、核心实现:利用 navigator.mediaDevices.getUserMedia 打开摄像头,将视频流放入 video 标签的 src 中,再通过 canvas.drawImage 的方法,以 video 对象为画布源,绘制最终拍照的图片。
2、代码示例:
1)HTML 示例
<div id="cameraContainer"> <video id="cameraView" width="345" height="210" autoplay></video> <div class="frame-container"> <div class="mask"></div> <div id="frame"> <div class="corner topLeft"></div> <div class="corner topRight"></div> <div class="corner bottomLeft"></div> <div class="corner bottomRight"></div> </div> <div style="margin-top: 6px; text-align: center; color: red"> Please put your ID in the box </div> </div> </div> <button id="captureButton">拍照</button> <canvas id="canvas" style="display: none"></canvas> <img id="photo" alt="Captured Photo" />2)JS 示例
const video = document.getElementById("cameraView"); const frame = document.getElementById("frame"); const captureButton = document.getElementById("captureButton"); const canvas = document.getElementById("canvas"); const photo = document.getElementById("photo"); // 获取用户媒体设备权限 navigator.mediaDevices .getUserMedia({ video: true, audio: false }) .then((stream) => { video.srcObject = stream; }) .catch((error) => { console.error("获取摄像头权限失败:", error); }); captureButton.addEventListener("click", () => { const context = canvas.getContext("2d"); // 设置画布尺寸与取景框相同 console.log(video.videoWidth); canvas.width = video.videoWidth; canvas.height = video.videoHeight; // 绘制取景框内的画面到画布 context.drawImage(video, 0, 0); // 将画布内容转为图片并显示 photo.src = canvas.toDataURL(); photo.style.display = "block"; });
可运行 Demo
1、在 VS Code IDE 中,创建一个 HTML 文件,将下面的代码复制即可。
2、启动 VS Code 的 Live Server 插件(如果没有,可以安装,如果有其他方案也可),然后通过 127.0.0.1 或 localhost 的方式访问,对应的端口和路径,请按照你的 HTML 文件路径来即可。
3、注意:不要用局域网内的 IP 访问,否则会无法唤起摄像头,后面注意事项中会说明原因和解决方案。
<!DOCTYPE html> <html> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover" /> <head> <style> #cameraContainer { position: relative; width: 345px; height: 210px; overflow: hidden; } #cameraView { object-fit: cover; } .frame-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .mask { position: absolute; width: 100%; height: 100%; } #frame { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200px; height: 90px; z-index: 10; background-color: transparent; } .corner { position: absolute; border-color: red; border-style: solid; padding: 6px; } .topLeft { top: 1px; left: 1px; border-width: 2px 0 0 2px; } .topRight { top: 1px; right: 1px; border-width: 2px 2px 0 0; } .bottomLeft { bottom: 1px; left: 1px; border-width: 0 0 2px 2px; } .bottomRight { bottom: 1px; right: 1px; border-width: 0 2px 2px 0; } #photo { display: none; width: 345px; height: 210px; } </style> </head> <body> <div id="cameraContainer"> <video id="cameraView" width="345" height="210" autoplay></video> <div class="frame-container"> <div class="mask"></div> <div id="frame"> <div class="corner topLeft"></div> <div class="corner topRight"></div> <div class="corner bottomLeft"></div> <div class="corner bottomRight"></div> </div> <div style="margin-top: 6px; text-align: center; color: red"> Please put your ID in the box </div> </div> </div> <button id="captureButton">拍照</button> <canvas id="canvas" style="display: none"></canvas> <img id="photo" alt="Captured Photo" /> <script> const video = document.getElementById("cameraView"); const frame = document.getElementById("frame"); const captureButton = document.getElementById("captureButton"); const canvas = document.getElementById("canvas"); const photo = document.getElementById("photo"); // 获取用户媒体设备权限 navigator.mediaDevices .getUserMedia({ video: true, audio: false }) .then((stream) => { video.srcObject = stream; }) .catch((error) => { console.error("获取摄像头权限失败:", error); }); // 拍照按钮点击事件 captureButton.addEventListener("click", () => { const context = canvas.getContext("2d"); // 设置画布尺寸与取景框相同 console.log(video.videoWidth); canvas.width = video.videoWidth; canvas.height = video.videoHeight; // 绘制取景框内的画面到画布 context.drawImage(video, 0, 0); // 将画布内容转为图片并显示 photo.src = canvas.toDataURL(); photo.style.display = "block"; }); </script> </body> </html>
注意事项
1、在实际项目中,需要注意做好容错,可以参考 MDN 中的容错代码,如果无法唤起手机摄像头(用户拒绝、浏览器不支持等),则需要根据实际情况,考虑兼容方案(给出提示、直接唤起原生相机等)
兼容代码如下(developer.mozilla.org/zh-CN/docs/…
// 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象 if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {}; } // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia // 因为这样可能会覆盖已有的属性。这里我们只会在没有 getUserMedia 属性的时候添加它。 if (navigator.mediaDevices.getUserMedia === undefined) { navigator.mediaDevices.getUserMedia = function (constraints) { // 首先,如果有 getUserMedia 的话,就获得它 var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; // 一些浏览器根本没实现它 - 那么就返回一个 error 到 promise 的 reject 来保持一个统一的接口 if (!getUserMedia) { return Promise.reject( new Error("getUserMedia is not implemented in this browser"), ); } // 否则,为老的 navigator.getUserMedia 方法包裹一个 Promise return new Promise(function (resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); }; } navigator.mediaDevices .getUserMedia({ audio: true, video: true }) .then(function (stream) { var video = document.querySelector("video"); // 旧的浏览器可能没有 srcObject if ("srcObject" in video) { video.srcObject = stream; } else { // 防止在新的浏览器里使用它,应为它已经不再支持了 video.src = window.URL.createObjectURL(stream); } video.onloadedmetadata = function (e) { video.play(); }; }) .catch(function (err) { console.log(err.name + ": " + err.message); });2、当业务逻辑获取了 canvas 绘制的图片后,出于性能以及交互体验的考虑,应该关闭 video 播放、以及摄像头,可以参考如下代码:
// 停止 video 播放 // 在合适的地方,保存之前设置 video.src 的 video 对象引用 video.stop() // 关闭摄像头 // 在 navigator.mediaDevices.getUserMedia().then((stream)=>{ //do something }) 中,保存 stream 对象引用 stream?.getTracks()?.forEach(function(track) { track.stop() })
3、本地跑 Demo 时,可以通过 localhost 或 127.0.0.1 的域名方式访问,此时是可以唤起摄像头,但如果用局域网的 IP 则不行,这是因为浏览器的安全限制,必须使用 https 才可以。此时有两种解决方案(仅应用于本地调试)
1)在将 Demo 相关的逻辑放入实际项目中时,启动项目时,如果也支持 localhost / 127.0.0.1 访问,则没有问题
2)如果本地能够支持 https 访问,则也可以唤起摄像头
3)如果上述均不可,则可以设置 chrome 浏览器的安全策略,将对应的域名或 IP 地址,打开为白名单,具体设置方式,请参考 juejin.cn/post/699030…
4、请务必保证线上业务是 https 协议,否则无法正常打开摄像头
风险点及待办
风险点
1、Demo 仅在电脑上尝试,不保证手机的兼容性问题及效果(正式上线前需要QA和产品做相关的兼容测试)
2、因为使用了 video 标签,蒙层也是在 video 标签上盖的,这里可能会涉及到 video 标签的兼容性问题,就是在不同的手机浏览器上,video 标签的优先级可能会很高,导致蒙层或遮罩无法盖住 video 标签(国产手机浏览器为重灾区)
待办
1、针对 Canvas 的取景框遮罩效果,暂未实现,后续如果有要求,可以考虑用图片替换,即让 UI 同学直接出一个镂空的取景框图片,直接替换对应元素即可
2、针对 Canvas 仅绘制取景框内容的功能未实现,这里涉及到如何调整 drawImage 相关的参数,以及裁切绘制后的图片是否可以被业务方所识别的问题,如果参数调整的不合适,会出现图片变形,模糊的情况