自定义webIpad证件相机(webRTC)

该技术方案可用于各浏览器自定义相机开发

相机UI(index.html)

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)

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)

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)

javascript 复制代码
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)

javascript 复制代码
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" });
    }
}

文件目录

效果图

相关推荐
youngong3 天前
强迫症之用相机快门数批量重命名文件
数码相机·文件管理
weixin_466485116 天前
halcon标定助手的使用
数码相机
诸葛务农8 天前
ToF(飞行时间)相机在人形机器人非接触式传感领域内的应用
数码相机·机器人
塞北山巅8 天前
相机自动曝光(AE)核心算法——从参数调节到亮度标定
数码相机·算法
美摄科技9 天前
相机sdk是什么意思?
数码相机
phyit9 天前
全景相机领域,影石何以杀出重围?
数码相机
鄃鳕9 天前
装饰器【Python】
开发语言·python·数码相机
聪明不喝牛奶9 天前
【已解决】海康威视相机如何升级固件
数码相机
PAQQ9 天前
1站--视觉搬运工业机器人工作站 -- 相机部分
数码相机·机器人
诸葛务农9 天前
人形机器人基于视觉的非接触式触觉传感技术
数码相机·机器人