阅读量:0
该技术方案可用于各浏览器自定义相机开发
相机UI(index.html)
<!DOCTYPE html> <html lang="zh" prew="-1"> <head> <meta charset="UTF-8"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=5, minimum-scale=1, width=device-width" /> <title>自定义相机</title> <link rel="stylesheet" href="./style.css"> <script src="./tools.js"></script> <script src="./index.js"></script> </head> <body> <div class="errTip"> <p>Failed to obtain the rear camera of the device. Please try another solution to obtain resources!</p> <button class="errBtn">GO Back</button> </div> <div class="takeOffTip"></div> <div class="imgBoxDom"> <div class="imgBox"> <img src="./center.png" style="width: 4vw;"> </div> </div> <div class="rightBtnBox"> <div class="takeBtn"></div> <div class="cancleBtn btn"></div> </div> <div class="bottomBtnBox"> <div class="reTakeBtn btn bottonSize"></div> <div class="nextBtn btn bottonSize"></div> </div> <div class="loading-css"> Loading... </div> </body> </html>
相机UI样式(style.css)
* { margin: 0; padding: 0; box-sizing: border-box; border: 0; } html, body { width: 100%; height: 100%; overflow: hidden; background-color: #000; color: #fff; } .cancleBtn { padding: 2vw 0; width: 100%; } .takeOffTip { position: fixed; padding-top: 2vw; top: 0; left: 0; width: 100%; font-size: 1.8vw; text-align: center; color: #fff; } .bottonSize { height: 100%; line-height: 6vw; line-height: 6dvw; padding: 0 1.5vw; } .bottomBtnBox, .rightBtnBox { position: fixed; right: 0; display: flex; justify-content: space-between; align-items: center; background-color: #000; z-index: 10; } .bottomBtnBox { bottom: 0; width: 100%; height: 6vw; height: 6dvw; } .rightBtnBox { flex-direction: column; top: 0; height: 100%; width: 6vw; width: 6dvw; } html[prew='-1'] .bottomBtnBox, html[prew='0'] .bottomBtnBox, html[prew='-1'] .rightBtnBox, html[prew='1'] .rightBtnBox, html[prew='1'] .customer_carema { display: none; } html[prew='1'] .imgBox { border: 0; font-size: 0; opacity: 0; } .takeBtn { padding: 4px; width: 5vw; width: 5dvw; height: 5vw; height: 5dvw; background-color: #fff; border-radius: 50%; } .takeBtn::before { content: ''; display: block; width: 100%; height: 100%; border: 5px solid #000; background-color: #fff; border-radius: 50%; box-sizing: border-box; } .rightBtnBox::before { content: ''; display: block; } .btn { background-color: #000; text-align: center; font-size: 1.5vw; color: #fff; } .customer_video, .carema_img, .cuteImg { width: 100%; height: 100%; object-fit: cover; } .imgBoxDom { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; z-index: 9; } .imgBox { width: var(--carema-box-width); height: var(--carema-box-height); border: 2px solid #fff; display: flex; justify-content: center; align-items: center; font-size: 10vw; z-index: 10; border-radius: 2vw; } .errTip { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 8888; display: none; flex-direction: column; justify-content: center; align-items: center; background-color: #000; } .errTip>p { padding-bottom: 20px; color: #fff; } .errTip button { padding: 10px 30px; } html[prew='2'] .errTip { display: flex; } html[loaded='1'] .loading-css { display: none; } .loading-css { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: #000; z-index: 9999; } .loading-css::before { margin-bottom: 10px; content: ''; width: 50px; height: 50px; display: inline-block; border: 3px solid #f3f3f3; border-top: 3px solid rgb(160, 155, 155); border-radius: 50%; animation: loading-360 0.8s infinite linear; } @keyframes loading-360 { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
调试UI(carema.html)
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=5, minimum-scale=1, width=device-width" /> <title>调试相机</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; border: 0; } img { max-width: 100%; } .btnList { padding: 10px; } label[type='file'], button { padding: 0 10px; height: 32px; line-height: 32px; display: inline-block; font-size: 14px; appearance: auto; border: 1px solid #999; background-color: #dcdcdc; } label>input { font-size: 0; width: 0; height: 0; overflow: hidden; } .showImg { padding: 5px; display: flex; flex-wrap: wrap; } .showImg>.box { width: 33.33%; padding: 5px; } .showImg>.box>.img { width: 100%; height: 20vw; overflow: hidden; border-radius: 10px; border: 2px solid #888; } .showImg>.box>.img>img { width: 100%; height: 100%; object-fit: cover; } html, body { height: 100%; height: 100%; } body { display: flex; flex-direction: column; } .showImg { flex: 1; overflow-x: hidden; } </style> </head> <body> <div class="btnList"> <button onclick="openCarema('HK_ID')">COMM_ID_IMG</button> <button onclick="openCarema('LANDING')">LANDING_IMG</button> <label name="upload" type="file"> LOCAL_IMG <input type="file" id="upload"> </label> </div> <div class="showImg" id="showImg"></div> </body> <script> function fileToBase64(file) { return new Promise((resolve, reject) => { // 创建一个新的 FileReader 对象 var reader = new FileReader(); // 读取 File 对象 reader.readAsDataURL(file); // 加载完成后 reader.onload = function () { // 将读取的数据转换为 base64 编码的字符串 var base64String = reader.result.split(",")[1]; // 解析为 Promise 对象,并返回 base64 编码的字符串 resolve(base64String); }; // 加载失败时 reader.onerror = function () { reject(new Error("Failed to load file")); }; }); } function showImg(url) { var showImgDom = document.getElementById('showImg'); var img = document.createElement('img'); img.src = `data:image/jpeg;base64,${url}`; var div = document.createElement('div'); var cDiv = document.createElement('div'); div.append(cDiv); cDiv.append(img); div.className = 'box'; cDiv.className = "img"; showImgDom.insertBefore(div, showImgDom.firstChild); } document.getElementById('upload').addEventListener('change', function ($event) { var file = $event.target.files[0]; fileToBase64(file).then(showImg); }) function openCarema(idType) { var openId = Date.now() + ''; window.open(`./index.html?openId=${openId}&idType=${idType}&isDev=1`); window.addEventListener('message', function (res) { var resOpenId = res.data.openId; var mothod = res.data.mothod; var file = res.data.imgUrl; console.log(resOpenId, mothod, file); if (mothod === "success_file" && openId === resOpenId) fileToBase64(file).then(showImg); }) } </script> </html>
相机逻辑基础(index.js)
function WbCRM() { this.body = document.body; this.html = document.documentElement; this.takeBtn = document.querySelector('.takeBtn'); this.imgBox = document.querySelector('.imgBox'); this.reTakeBtn = document.querySelector('.reTakeBtn'); this.cancleBtn = document.querySelector('.cancleBtn'); this.nextBtn = document.querySelector('.nextBtn'); var errBtn = document.querySelector('.errBtn'); this.video = null; this.err = null; this.fullImg = null; this.file = ''; this.idType = ''; this.isDev = false; this.stream = null; this.openId = ''; this.ratio = window.devicePixelRatio || 1; this.videoWidth = this.body.clientWidth * this.ratio; this.videoHeight = this.body.clientHeight * this.ratio; this.html.setAttribute('prew', '-1'); var isMp3 = !(navigator.userAgent.match(/Firefox/)); var audio = new Audio(); audio.autoplay = isMp3 ? './shutter.mp3' : './shutter.ogg'; this.audio = audio; console.log(isMp3,audio); this.mediaDevices = (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) ? navigator.mediaDevices : ((navigator.mozGetUserMedia || navigator.webkitGetUserMedia) ? { getUserMedia: function (c) { return new Promise(function (y, n) { (navigator.mozGetUserMedia || navigator.webkitGetUserMedia).call(navigator, c, y, n); }); } } : null); this.setDom(); this.setCarema(); this.takeBtn.addEventListener('click', this.takePhoto.bind(this)); this.nextBtn.addEventListener('click', this.next.bind(this)); this.reTakeBtn.addEventListener('click', this.reTake.bind(this)); this.cancleBtn.addEventListener('click', this.cancle.bind(this)); errBtn.addEventListener('click', this.openErro.bind(this)); } WbCRM.prototype.openErro = function () { this.sendMsg('open_erro'); } WbCRM.prototype.cancle = function () { this.removeStream(); this.sendMsg('off_carema'); } WbCRM.prototype.next = function () { if (this.fullImg) this.fullImg.remove(); this.removeStream(); this.sendMsg('success_file'); } WbCRM.prototype.reTake = function () { this.file = null; this.err = null; if (this.fullImg) this.fullImg.remove(); this.html.setAttribute('loaded', 0); this.removeStream(); this.setCarema(); } WbCRM.prototype.cutImage = function () { var boxWidth = this.imgBox.clientWidth * this.ratio; var boxHeight = this.imgBox.clientHeight * this.ratio; var vLeft = (this.videoWidth - boxWidth) / 2; var vTop = (this.videoHeight - boxHeight) / 2; var nCanvas = wbCRMTools.drawHighDefinitionImg(boxWidth, boxHeight); var nCtx = nCanvas.getContext('2d'); nCtx.drawImage(this.fullImg, -vLeft, -vTop); var cutImage = nCtx.getImageData(0, 0, boxWidth, boxHeight); wbCRMTools.changeImgData(cutImage?.data || [], this.idType || ''); nCtx.putImageData(cutImage, 0, 0); reImgUrl = nCanvas.toDataURL('image/jpeg'); var cImg = document.createElement('img'); cImg.src = reImgUrl; this.file = wbCRMTools.canvas2File(reImgUrl); wbCRMTools.clearCanvas(nCtx, nCanvas); cImg.className = "cuteImg"; this.imgBox.append(cImg); this.html.setAttribute('prew', '1'); this.removeStream(); } WbCRM.prototype.takePhoto = function () { var gCanvas = wbCRMTools.drawHighDefinitionImg(this.videoWidth, this.videoHeight); var originalCtx = gCanvas.getContext('2d'); originalCtx.drawImage(this.video, 0, 0, this.videoWidth, this.videoHeight); var imgUrl = gCanvas.toDataURL('image/jpeg'); var fullImg = document.createElement("img"); fullImg.className = "carema_img"; fullImg.src = imgUrl; this.fullImg = fullImg; this.body.append(fullImg); wbCRMTools.clearCanvas(originalCtx, gCanvas); this.audio.play(); fullImg.onload = this.cutImage.bind(this); } WbCRM.prototype.sendMsg = function (mothod) { this.audio.remove(); const origin = this.isDev ? undefined : window.location.origin; window.opener.postMessage({ mothod: mothod, file: this.file, openId: this.openId, error: this.err }, origin); window.close(); } WbCRM.prototype.removeStream = function () { var self = this; if (self.stream) { self.stream.getTracks().forEach(function (track) { if (track.readyState === 'live') track.stop(); self.stream.removeTrack(track); }); } if (this.video) this.video.remove(); var cuteImgList = document.querySelectorAll('.cuteImg'); cuteImgList.forEach(function (dom) { dom.remove(); }) } WbCRM.prototype.setDom = function () { this.openId = wbCRMTools.getUrlParam('openId'); var okText = wbCRMTools.getUrlParam('continue'); var cancelText = wbCRMTools.getUrlParam('cancel'); var retakeText = wbCRMTools.getUrlParam('retake'); var idType = wbCRMTools.getUrlParam('idType') || ''; var takeOffTip = wbCRMTools.getUrlParam('takeOffTip'); const isDev = wbCRMTools.getUrlParam('isDev'); this.isDev = isDev === '1'; this.nextBtn.innerText = okText || 'Cuntinue'; this.cancleBtn.innerText = cancelText || 'Cancel'; this.reTakeBtn.innerText = retakeText || 'Retake'; document.querySelector('.takeOffTip').innerHTML = takeOffTip; this.html.setAttribute('loaded', 0); this.html.style.setProperty('--carema-box-width', '64.512vw'); this.html.style.setProperty('--carema-box-height', '40.6789vw'); if (idType === "LANDING") { this.html.style.setProperty('--carema-box-width', '51.2vw'); this.html.style.setProperty('--carema-box-height', '44.5935vw'); } this.idType = idType; } WbCRM.prototype.setVideo = function (stream) { var video = document.createElement('video'); video.setAttribute('autoplay', 'autoplay'); video.setAttribute('playsinline', 'playsinline'); video.className = 'customer_video'; this.video = video; this.stream = stream; this.body.append(video); var self = this; video.onloadedmetadata = function (e) { self.stream = stream; self.loaded = true; self.html.setAttribute('loaded', 1); }; video.onplay = function () { self.html.setAttribute('prew', '0'); } // as window.URL.createObjectURL() is deprecated, adding a check so that it works in Safari. // older browsers may not have srcObject if ("srcObject" in video) { video.srcObject = stream; } else { // using URL.createObjectURL() as fallback for old browsers video.src = window.URL.createObjectURL(stream); } } WbCRM.prototype.setCarema = function () { const videoConf = this.isDev ? {} : { width: { min: 1024, ideal: 2360, max: 2732 }, height: { min: 776, ideal: 1640, max: 2048 }, facingMode: { exact: "environment" } } var self = this; this.mediaDevices.getUserMedia({ audio: false, video: videoConf }).then(this.setVideo.bind(this)).catch(function (error) { self.err = error.toString(); self.html.setAttribute('prew', '2'); self.html.setAttribute('loaded', '1'); }) } window.addEventListener('load', function () { var wbCRM = new WbCRM(); window.addEventListener('visibilitychange', function () { wbCRM.removeStream(); window.close(); }); });
图片出路和文件生成工具(tools.js)
var wbCRMTools = { drawHighDefinitionImg: function (width, height) { const canvas = document.createElement('canvas'); canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; canvas.width = width; canvas.height = height; return canvas; }, clearCanvas: function (ctx, canvas) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); canvas.height = 0; canvas.width = 0; canvas.remove(); canvas.parentNode?.removeChild(canvas); }, changeImgData: function (data, idType) { const isGrayscale = ['PASSPORT', 'LANDING', 'ENTRYPERMIT', 'SUP_LEGAL_ID'].some(imgType => idType.indexOf(imgType) !== -1); let contrast = 35; const thereshold = 20; if ('LANDING' === idType) contrast = 45; // gaussBlur will use in the feature, cancel this fun now, don`t delete please // this.gaussBlur(imageData, 1); // If MacId and HK-LANDING change cavans-img-code. const factor = (255 + contrast) / (255.01 - contrast); //add .1 to avoid /0 error const denominator = 1 / (1 - contrast / 255) - 1; const setCV = cv => cv + (cv - thereshold) * denominator; const setCTV = cv => cv + (cv - thereshold) * contrast / 255; const getRGB = cv => factor * (cv - 128) + 128; // Data array data-length. const len = data?.length || 0; // loop value to change cavans imgData; for (let index = 0; index < len; index += 4) { let R = data[index]; //r value let G = data[index + 1]; //g value let B = data[index + 2] //b value if (contrast || thereshold) { R = getRGB(R); //r value G = getRGB(G); //g value B = getRGB(B); //b value } const isColorNum = index % 4 === 0; if (isColorNum) { R = contrast ? setCV(R) : setCTV(R); G = contrast ? setCV(G) : setCTV(G); B = contrast ? setCV(B) : setCTV(B); if (isGrayscale) { const vNum = Math.round((R + G + B) / 3); R = vNum; G = vNum; B = vNum; data[index + 3] = 255; } data[index] = R; data[index + 1] = G; data[index + 2] = B; } } }, getUrlParam: function (urlKey) { var url = window.location.search; var reg = new RegExp("(^|&)" + urlKey + "=([^&]*)(&|$)"); var result = url.substring(1).match(reg); return result ? decodeURIComponent(result[2]) : null; }, canvas2File: function (dataUrl) { let arr = dataUrl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } const nowId = Date.now(); const fileName = `takePhoto_${nowId}.jpeg`; const blob = new Blob([u8arr], { type: mime, name: fileName }); blob.lastModifiedDate = new Date(); return new File([blob], fileName, { type: "image/jpeg" }); } }