基于video封装开发前端相机jsdk实现拍照

179 阅读3分钟

实现思路:

  • 通过html5 video获取媒体流
const constraints = {
    audio: false,
    video: {
        // frameRate: {ideal: 20},
        // cursor:'never',
        // 'displaySurface':'monitor',
        'facingMode': this.faceMode == 1 ? "user" : "environment",
        // 'width': {ideal: 480 },
        // 'height':{ideal: 640 }
    }
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    video.srcObject = stream;
    // 获取视频轨道
    this.videoSteam = stream.getTracks();
    video.play();
})
  • 利用canvas 进行视频流截图获取到图片base64;
  • videoMask.getBoundingClientRect();
  • video.getBoundingClientRect();
canvas.width = maskPos.width;
canvas.height = maskPos.height;
const context = canvas.getContext('2d');
context.drawImage(video, -(videoPos.width - maskPos.width) / 2, -(videoPos.height - maskPos.height) / 2, videoPos.width, videoPos.height);
const base64Img = canvas.toDataURL("image/png

注意事项

  • navigator.mediaDevices.getUserMedia,为浏览器获取媒体流的方法:
    • localhost 域
    • 开启了 HTTPS 的域
    • 使用 file:/// 协议打开的本地文件 其他情况下,比如在一个 HTTP 站点上,navigator.mediaDevices 的值为 undefined。 如果想要 HTTP 环境下也能使用和调试 MediaDevices.getUserMedia(),可通过开启 Chrome 的相应参数。

jsdk封装好了直接上代码拿去玩撒。。。

(function(window){
    class WebCamera {
        constructor() {
            this.viewBoxId=this.randomString();
            this.faceMode=1;
            this.videoSteam=null;
            // callback回调函数
            this.callback=null;
            // 是否显示取景框
            this.isShowFindFrame=2;
        }
        //初始化
        init(options) {
            this.faceMode=options.faceMode||1;
            this.callback=options.callback||null;
            this.isShowFindFrame=options.isShowFindFrame||2;
            if(!document.getElementById(this.viewBoxId)){
                this.initViewDom();
            }else{
                const mask=document.getElementById('videoMask');
                mask.style.display=this.isShowFindFrame==1?'block':'none';
                this.open();
            }
           
        }
        initViewDom() {
            const videoWrap=this.createElement('div', {
                id:this.viewBoxId,
                styles:{
                    position: 'fixed',
                    left: '0px',
                    right: '0',
                    bottom: '0',
                    top: '0px',
                    width: '100%',
                    height: '100%',
                    display:'flex',
                    flexFlow:'column',
                    alignItems:'center',
                    justifyContent: 'center',
                    background:'#f2f4fa'
                }
            })
            // video容器
            const videoBox=this.createElement('div',{
                    innerHTML:`<video
                    id=${this.viewBoxId+"_video"}
                    style="width:100%;height:auto; transform: rotateY(${this.faceMode==1?'180deg':'0deg'});"
                    autoplay="autoplay" playsinline="playsinline">
                    </video>`,
                    styles:{
                        width:'100%',
                        height:'100%',
                        position: 'relative',
                        backgroundColor: 'black',
                        overflow: 'hidden',
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                        flexFlow: 'column'
                    }
            })
            const height=(window.innerWidth<window.innerHeight)? window.innerWidth*9/16:window.innerHeight-20;
            const width=(window.innerWidth<window.innerHeight)?window.innerWidth-20:window.innerHeight*16/9;
            const mask=this.createElement('div', {
                 id:'videoMask',
                    styles:{
                    display:this.isShowFindFrame==1?'block':'none',
                    position:'absolute',
                    top:'50%',
                    left:'50%',
                    height:`${height}px`,
                    border:'1px solid #51de20',
                    width:`${width}px`,
                    boxSizing: 'border-box',
                    transform: 'translate(-50%, -50%)'
                }
            })
    
            const videoBtn=this.createElement('div', {
                id:'tick_video_Btn',
                styles:{
                    width:'70px',
                    height:'70px',
                    borderRadius:'50%',
                    position: 'absolute',
                    bottom: '30px',
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    letterSpacing: '4px',
                    color:'white',
                    fontSize:'18px',
                    fontWeight:'bold',
                    border:'3px solid white',
                    padding:'2px'
                },
                innerHTML:'<div style="background:white; border:1px solid black;width: 100%;height: 100%;border-radius: 50%;"></div>',
                onclick:(e)=>{
                    this.saveImg()
                }
            })
    
            const videoBtnWrap=this.createElement('div', {
                styles:{
                    width:'100%',
                    height:'100px',
                    position: 'absolute',
                    bottom: '0px',
                    display: 'flex',
                    justifyContent: 'center'
                },
            })
            const domFrame=document.createDocumentFragment();
            videoWrap.appendChild(videoBox);
            videoWrap.appendChild(mask);
            videoWrap.appendChild(videoBtnWrap);
            videoBtnWrap.appendChild(videoBtn)
            domFrame.append(videoWrap)
            const errorBox=this.createElement('div', {id:`errorMsg_${this.viewBoxId}`})
            document.body.appendChild(domFrame);
            this.createVideo();
        }
        // interface MediaTrackConstraintSet {
        //     // 画面比例
        //     aspectRatio?: ConstrainDouble;
        //     // 设备ID,可以从enumerateDevices中获取
        //     deviceId?: ConstrainDOMString;
        //     // 摄像头前后置模式,一般适用于手机
        //     facingMode?:user --〉前置 
        //     // 帧率,采集视频的目标帧率
        //     frameRate?: ConstrainDouble;
        //     // 组ID,用一个设备的输入输出的组ID是同一个
        //     groupId?: ConstrainDOMString;
        //     // 视频高度
        //     height?: ConstrainULong
        //     // 视频宽度
        //     width?: ConstrainULong;
        // }
        createVideo() {

            navigator.mediaDevices.enumerateDevices().then(function(devices) {
                console.log(devices,"00000000")
                devices.forEach(function(device) {
                    console.log(device)
                //    if (device.kind === 'videoinput') {//视频输入设备
                //         console.log(device)
                //   }
                })
              }).catch(e=>{
                  console.log(e)
              })


            const video= document.getElementById(this.viewBoxId+"_video");
            const videoBtn=document.getElementById('tick_video_Btn');
            const constraints={
                audio: false,
                video:{
                    frameRate: {ideal: 20}
                    // cursor:'never',
                    // displaySurface:'monitor',
                    // 'facingMode': this.faceMode==1? "user" : "environment",
                    // 'width': {ideal: 480 },
                    // 'height':{ideal: 640 }
                }
            }
    
       
    
            navigator.mediaDevices.getUserMedia(constraints).then((stream)=> {
                video.srcObject = stream;
                console.log(video.srcObject)
                // 获取视频轨道
                this.videoSteam=stream.getTracks();
                video.play();
            }).catch((error)=> {
                const errorConfig={
                     'NotAllowedError': '用户已禁止使用相机,请查看相关权限',
                    'PermissionDeniedError':'你还没有开启相机权限',
                    'AbortError':'其它异常',
                    'InvalidStateError':'拒绝异常',
                    'NotFoundError':'无法获取视频源',
                    'NotReadableError':'相机被占用',
                    'TypeError':'类型错误'
                }
                const message=errorConfig[error.name];
                this.$message(message)
            })
        }
        randomString = function (len) {
            len = len || 32;
            let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
            let maxPos = $chars.length;
            let pwd = '';
            for (let i = 0; i < len; i++) {
                pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
            }
            return pwd;
        }
        /**
         * 保存生成图像
         * @name saveImg
         * @description 点击拍照回保存图片函数
         * @abstract 点击拍照回保存图片函数 {clipPath:裁剪后的图片Base64,orignPath:原图Base64}
         */
        saveImg() {
           
            const base64Str=this.getClipPath();
            const orignBase64=this.getOriginPath();
            if(this.callback){
                this.callback({
                    clipPath:base64Str,
                    orignPath:orignBase64
                });
            }
            this.closeVideo();
        }
        /**
         * 暂停播放
         */
        closeVideo(){
            const videoId=this.viewBoxId;
            const videoBox=document.getElementById(videoId);
            const video=document.getElementById(`${videoId}_video`)
            const tracks=this.videoSteam||[];
            tracks.forEach(function(track) {
                track.stop();
            });
            video.srcObject = null;
            videoBox.style.display="none"
    
        }
        /**
         * 重新播放
         */
        open(){
            const videoId=this.viewBoxId;
            const videoBox=document.getElementById(videoId);
            videoBox.style.display='block';
            this.createVideo();
        }
        getClipPath(){
            let canvas=document.createElement("canvas");
            const videoBoxId=this.viewBoxId+"_video";
            const video = document.getElementById(videoBoxId);
            const videoMask=document.getElementById('videoMask');
            const maskPos=videoMask.getBoundingClientRect();
            const videoPos=video.getBoundingClientRect();
            // const height = videoPoswidth / (4/3);
            
            canvas.width = maskPos.width;
            canvas.height = maskPos.height;
            const context= canvas.getContext('2d');
            context.drawImage(video, -(videoPos.width-maskPos.width)/2,-(videoPos.height-maskPos.height)/2,videoPos.width,videoPos.height);
            const base64Img=canvas.toDataURL("image/png");
            canvas=null;
            return base64Img;
    
    
    
        }
        getOriginPath(){
            let videoCavans=document.createElement("canvas");
            const videoBoxId=this.viewBoxId+"_video";
            const video = document.getElementById(videoBoxId);
            const videoPos=video.getBoundingClientRect();
            videoCavans.width=videoPos.width;
            videoCavans.height=videoPos.height;
            const videoContxt=videoCavans.getContext('2d');
            videoContxt.drawImage(video,0,0,videoPos.width,videoPos.height)
            const orignBase64=videoCavans.toDataURL("image/png");
            videoCavans=null;
            return orignBase64
        }
        $message(msg) {
            const messageBox = document.getElementById(`errorMsg_${this.viewBoxId}`)||document.body;
            const mesageItem = this.createElement('div', {
              className: 'errorBox',
              styles:{
                width: '238px',
                height: '146px',
                fontSize: '14px',
                position:'absolute',
                zIndex:'9999',
                background:'white',
                borderRadius:'14px',
               
                boxSizing:'border-box',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                display:'flex',
                justifyContent:'center',
                flexFlow:'column',
                alignItems:'center'
                // padding: '9px 12px',
                // background: '#fff',
                // borderRadius: '8px',
                // boxShadow: '0 6px 16px 0 rgb(0 0 0 / 8%), 0 3px 6px -4px rgb(0 0 0 / 12%), 0 9px 28px 8px rgb(0 0 0 / 5%)',
                // pointerEvents: 'all',
                // opacity: 0,
                // animation: 'fadeOut 2s linear forwards'
              },
             
            })
            const errorInfo=this.createElement('div', {
                styles:{
                    fontSize: '14px',
                    fontFamily: 'PingFangSC-Regular, PingFang SC',
                    fontWeight: '400',
                    color: '#363A45',
                    lineHeight: '18px',
                    flex:1,
                    padding:'16px 16px 10px 16px',
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center'
                },
                innerHTML:msg
            })
            const closeBtn=this.createElement('div', {
                styles:{
                    width: '100%',
                    height: '44px',
                    background: 'rgba(255,255,255,0)',
                    textAlign:'center',
                    color:'#3591F4',
                    borderTop: '1px solid #DDE6F0',
                    lineHeight: '44px',
                    fontSize:'17px',
                    cursor:'pointer'
                },
                onclick:(e)=>{
                    messageBox.removeChild(mesageItem);
                    this.closeVideo();
                },
                innerText:'确定'
            })
            mesageItem.appendChild(errorInfo);
            mesageItem.appendChild(closeBtn);
            messageBox.appendChild(mesageItem);
           
          }
        createElement(type,propertys){
            const element=document.createElement(type);
            for(let key in propertys){
                switch(key.toLowerCase()){
                        case 'innertext':
                            element.innerText=propertys[key]
                        break;
                        case 'innerhtml':
                            element.innerHTML=propertys[key]
                        break;
                        case 'id':
                            element.id= propertys[key]
                        break;
                        case 'classname':
                            element.className= propertys[key]
                        case 'onclick':
                                element.onclick= propertys[key];
                        break;
                        case 'mouseDown':
                                element.onmousedown= propertys[key];
                        break;
                        case 'mouseMove':
                                element.onmousemove= propertys[key];
                        break;
                        case 'mouseUp':
                                element.onmouseup= propertys[key];
                        break;
                        case 'oninput':
                            element.oninput= propertys[key];
                        break;
                        case 'styles':
                            for(let name in propertys[key]){
                                element.style[name]=propertys[key][name];
                            }
                        break;
                        default:
                            element.setAttribute(key,propertys[key]);
                }
            }
            return element;
        }
    }
    window.testWebCamera=new WebCamera()
    
    }(window))

界面调用demo

<!DOCTYPE html>
<html>
<head>
    <title>拍照</title>
    <!-- <script src="fileSaver.js"></script> -->
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <!-- <script src="./camera.js"></script> -->
    <script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/eruda/2.4.1/eruda.min.js"></script> 
    <script src="./camera.js"></script>
    <script>
        eruda.init();
    </script>
    </head>
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        html{
            margin: 0;
            padding: 0;
        }
    </style>
<body >
    <button onclick="testOpen()">拍照</button>
     <div onclick="openFile()">打开文件</div>
</body>
<script>

	async function openFile(){
            const pickerOpts = {
                types: [
                    {
                    description: "Images",
                    accept: {
                        "image/*": [".png", ".gif", ".jpeg", ".jpg"],
                    },
                    },
                ],
                excludeAcceptAllOption: true,
                multiple: false,
                };
                let fileHandle;
                [fileHandle] = await window.showOpenFilePicker(pickerOpts);
                console.log(fileHandle);
        }

    function testOpen(){
        testWebCamera.init({
          isShowFindFrame:1,
          faceMode:1, //1前置 2:后置,
          callback:(result)=>{
              console.log(result)
          }
        })
    }
  </script>
</html>