该技术方案可用于各浏览器自定义相机开发
相机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 srcObjectif ("srcObject" in video) {video.srcObject = stream;} else {// using URL.createObjectURL() as fallback for old browsersvideo.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 errorconst 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 valuelet G = data[index + 1]; //g valuelet B = data[index + 2] //b valueif (contrast || thereshold) {R = getRGB(R); //r valueG = getRGB(G); //g valueB = 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" });}
}
文件目录
效果图