vue3实现自定义相机拍照、扫描二维码等功能

核心语法是 navigator.getUserMedia,API线上只支持https访问(出于安全考虑),
 开发环境localhost与file路径形式访问, 否则会报错navigator.getUserMedia=undefined
<template>
  <div id="photo-content" style="height: 100%">
    <div v-if="show" id="video-wrapper">
      <div id="masking"></div>
      <p class="notice">{{info}}</p>
      <div id="photo" @click="photo">
        <div></div>
      </div>
    </div>
    <div id="notSupport" v-else>
      <p>Sorry, the current browser does not support this feature</p>
      <p>Use the built-in Safari browser to open it</p>
      <button @click="copy">Copy current link</button>
    </div>
    <video
        id="video"
        autoPlay
        muted
        playsInline
        width="100%"
        height="100%">
    </video>
    <canvas id="canvas"></canvas>
  </div>
</template>

<script setup>
import {onActivated, onBeforeMount, onMounted, ref} from "vue";

let info = ref('')
let show = ref(true);
const ua = window.navigator.userAgent.toLowerCase();
if(ua.match(/micromessenger/i)
    && ua.match(/micromessenger/i)[0] === 'micromessenger'
    && (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) ) {
  console.log('当前是ios 微信内置浏览器, 需要用自带safari')
  show.value = false
} else {
  show.value = true
}
/**
 * 访问用户媒体设备的兼容方法
 */
const getUserMedia = (constrains) => {
  if (navigator.mediaDevices?.getUserMedia) {
    //最新标准API
    return navigator.mediaDevices.getUserMedia(constrains);
  } else if (navigator.webkitGetUserMedia) {
    //webkit内核浏览器
    return navigator.webkitGetUserMedia(constrains);
  } else if (navigator.mozGetUserMedia) {
    //Firefox浏览器
    return navigator.mozGetUserMedia(constrains);
  } else if (navigator.getUserMedia) {
    //旧版API
    return navigator.getUserMedia(constrains);
  } else {
    return new Promise((resolve, inject) => {
      inject({name: '该设备', message: '暂时无法提供流媒体功能'})
    })
  }
}
/**
 * 该函数需要接受一个video的dom节点作为参数
 */
const getUserMediaStream = (videoNode) => {
  /**
   * 调用api成功的回调函数
   */
  function success(stream, video) {
    return new Promise((resolve) => {
      video.srcObject = stream;
      video.onloadedmetadata = function () {
        video.play();
        resolve();
      };
    });
  }

  //调用用户媒体设备,访问摄像头 exact: 'environment' 后置摄像头  user 前置摄像头
  return getUserMedia({
    audio: false,
    video: { facingMode: { exact: 'environment' } },
    // video: true,
    // video: { facingMode: { exact: 'environment', width: 1280, height: 720 } },
  }).then(res => {
    return success(res, videoNode);
  }).catch(error => {
    console.error('访问用户媒体设备失败:', error.name, error.message);
    // return Promise.reject();
  });
}
/**
 * 获取元素实际的大小尺寸
 */
const getXYRatio = () => {
  // videoHeight为video 真实高度
  // offsetHeight为video css高度
  const video = document.getElementById('video');
  const { videoHeight: vh, videoWidth: vw, offsetHeight: oh, offsetWidth: ow } = video;
  return {
    yRatio: height => {
      return (vh / oh) * (oh / vh) * height;
    },
    xRatio: width => {
      return (vw / ow) * (ow / vw) * width;
    },
  };
}
/**
 * 裁切上传相关核心代码
 */
const photo = () => {
  const video = document.getElementById('video');
  const rectangle = document.getElementById('masking');
  const _canvas = document.getElementById('canvas');
  _canvas.style.display = 'block';


  startCapture();
  function startCapture() {
    const { yRatio, xRatio } = getXYRatio();
    /** 获取裁切框的位置 */
    const { left, top, width, height } = rectangle.getBoundingClientRect();

    const context = _canvas.getContext('2d');
    _canvas.width = width < 510 ? 510 : width;
    _canvas.height = height < 510 ? 510 : height;
    // change non-opaque pixels to white
    var imgData=context.getImageData(0,0,_canvas.width,_canvas.height);
    var data=imgData.data;
    for(var i=0;i<data.length;i+=4){
      if(data[i+3]<255){
        data[i]=255;
        data[i+1]=255;
        data[i+2]=255;
        data[i+3]=255;
      }
    }
    context.putImageData(imgData,0,0);

    // void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
    // 参数依次是 源,裁切X轴起点, 裁切Y轴起点,裁切宽度, 裁切高度,画布X起始位置, 画布Y起始位置, 画布宽, 画布高
    context.drawImage(
        video,
        xRatio(left + window.scrollX),
        yRatio(top + window.scrollY),
        xRatio(width),
        yRatio(height),
        width > 510 ? 0 : (510-width)/2,
        height > 510 ? 0 : (510-height)/2,
        width,
        height,
    );

    // 获取当前截图的base64编码
    const base64 = _canvas.toDataURL('image/jpeg');
    // 这里可以再根据场景做base64压缩以及其他操作
  }
}
/**
 * 复制功能
 */
const copy = () => {
  // 动态创建 input 元素
  const aux = document.createElement("input");
  // 获得需要复制的内容
  aux.setAttribute("value", window.location.origin);
  // 添加到 DOM 元素中
  document.body.appendChild(aux);
  // 执行选中
  // 注意: 只有 input 和 textarea 可以执行 select() 方法.
  aux.select();
  // 执行复制命令
  document.execCommand("copy");
  // 将 input 元素移除
  document.body.removeChild(aux);
  alert('复制成功')
}

onMounted(() => {
  document.getElementById('photo-content').addEventListener('touchmove', function () {
    event.preventDefault();
  }, { passive: false });
  if (show.value) {
    console.log(video.videoHeight, video.videoWidth ,video.offsetHeight ,video.offsetWidth)
    getUserMediaStream(video)
  }
})
onActivated(() => {
  const video = document.getElementById('video');

  if(!(/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent))) {
    console.log('andriod')
    getUserMediaStream(video)
  }
  let curHeight = 0
  if (route.query.type === 'passport') {
    curHeight = "11rem"
    info.value = 'Please place your Passport completely in the viewfinder frame'
  } else {
    curHeight = "9rem"
    info.value = 'Please place your ID card completely in the viewfinder frame'
  }
  document.getElementById('masking').style.height = curHeight
})
onBeforeMount(() => {
 // xxx
})

</script>

<style lang="less" scoped>
body {
  padding: 0;
  margin: 0;
  overflow: hidden;
}
.main-wrapper {
  height: 100% !important;
}
body.noscroll {
  overflow: hidden;
}
#video-wrapper {
  position: absolute;
  width: 100%;
  height: 100vh;
  background: transparent;
  opacity: 1;
  z-index: 1;
}
#video {
  width: 100%;
  height: 100%;
  //object-fit: cover;
  object-fit: none;
  object-position: 0 0;
}

#masking {
  margin: 6rem auto 0;
  width: 15rem;
  height: 9rem;
  border: 2px solid #fff;
  border-radius: 0.3rem;
  background: transparent;
  box-shadow: 0 0 0 50rem rgba(0, 0, 0, 0.7); // 外层阴影
}
.notice {
  padding-top: 1rem;
  font-size: 12px;
  color: #fff;
}
@media (max-width: 800px) {
  .notice {
    font-size: 20px;
  }
}

#photo {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: 2rem;
  width: 3rem;
  height: 3rem;
  border: 5px solid #fff;
  border-radius: 100%;
  z-index: 108;
}
#photo div {
  width: 100%;
  height: 100%;
  border-radius: 100%;
  background: #fff;
  opacity: 0.95;
}
#notSupport {
  width: 100%;
  position: fixed;
  top: 0;
  z-index: 1000;
  font-size: 1rem;
  text-align: center;
}

#notSupport p:nth-child(1) {
  text-align: center;
  color: red;
}
button {
  padding: 0.2rem;
  font-size: 1rem;
}
#canvas {
  position: absolute;
  bottom: -1000px;
  display: none;
}
</style>

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值