vue-h5:在h5中实现相机拍照加上身份证人相框和国徽框

news2024/11/14 11:59:22

1.基础功能

参考:
https://blog.csdn.net/weixin_45148022/article/details/135696629

https://juejin.cn/post/7327353533618978842?searchId=20241101133433B2BB37A081FD6A02DA60

https://www.freesion.com/article/67641324321/

https://github.com/AlexKratky/vue-camera-lib

效果:在这里插入图片描述

调用组件的

主要组件方法:openCamera,closeCamera

Upload.vue组件

<template>
    <div id="cameraContainer">
        <div ref="takePhotoDiv" class="take-photo" style="display: none">
            <video ref="video"  id="video-fix" :width="width" :height="height" autoplay   webkit-playsinline playsinline></video>
            <div class="frame-container">
                <div class="mask" >
<!--                  头像页图标-->
                  <img v-if="props.currPhotoType=='head'" class="img-head" src="../assets/image/idcard1.svg">
<!--                  国徽页图标-->
                  <img v-if="props.currPhotoType=='mark'" class="img-mark" src="../assets/image/idcard2.svg">
                  <div class="tips">请将{{props.currPhotoType=='head'?'身份证人像面':'身份证国徽面'}}完全置于取景框内</div>
                </div>
            </div>
        </div>
<!--      拍照按钮-->
      <div id="captureButton"  @click="takePhoto">
        <div class="cap-inner"></div>
      </div>

    </div>

    <canvas ref="canvas" style="display: none"></canvas>
  
    <img ref="photo" id="photo" alt="入职文件" style="display: none" />

</template>
<script setup lang="ts">
import { showToast } from "vant/lib/toast";
import { nextTick, onMounted, ref,inject } from "vue";
import {base64ToBlob, base64ToFile, putFile} from "@/common/services/OSSFile.ts";
import {FileUploadType} from "@/common/enum/FileUploadType.ts";
import {ElLoading} from "element-plus";
const props=defineProps({
  currPhotoType:String
})
const emit=defineEmits(['okUploadImg'])
const video = ref<HTMLVideoElement | null>(null);
// const frame = ref<HTMLDivElement | null>(null);
const photo = ref<HTMLImageElement | null>(null);
const canvas = ref<HTMLCanvasElement | null>(null);
const mediaStream = ref<any>();
const takePhotoDiv = ref<HTMLDivElement | null>(null);

const width=ref()
const height=ref()
onMounted(()=>{

  //设置摄像头宽高
  width.value=window.innerHeight
  height.value=window.innerWidth

})


const getVideoMedia = () => {
    if (video.value) {
      // ----------兼容性代码------------
      // 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
      if (navigator.mediaDevices === undefined) {
        navigator.mediaDevices = {};
      }

// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
// 因为这样可能会覆盖已有的属性。这里我们只会在没有 getUserMedia 属性的时候添加它。
      if (navigator.mediaDevices.getUserMedia === undefined) {
        navigator.mediaDevices.getUserMedia = function (constraints) {
          // 首先,如果有 getUserMedia 的话,就获得它
          var getUserMedia =
              navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

          // 一些浏览器根本没实现它 - 那么就返回一个 error 到 promise 的 reject 来保持一个统一的接口
          if (!getUserMedia) {
            return Promise.reject(
                new Error("getUserMedia is not implemented in this browser"),
            );
          }

          // 否则,为老的 navigator.getUserMedia 方法包裹一个 Promise
          return new Promise(function (resolve, reject) {
            getUserMedia.call(navigator, constraints, resolve, reject);
          });
        };
      }

      // ----------兼容性代码------------


        // 获取用户媒体设备权限
        navigator.mediaDevices
            // 强制使用后置摄像头
            .getUserMedia({ video: { facingMode: { exact: "environment" } }, audio: false })
            //前置
            // .getUserMedia({ video: true, audio: false })
            .then((stream) => {
                // if (video.value) {
                //     video.value.srcObject = stream;
                //     mediaStream.value = stream;
                // }

              //兼容性写法
              if ("srcObject" in video.value) {
                video.value.srcObject = stream;
              } else {
                // 防止在新的浏览器里使用它,应为它已经不再支持了
                video.value.src = window.URL.createObjectURL(stream);
              }
              video.value.onloadedmetadata = function (e) {
                video.value.play();
              };
            })
            .catch((error) => {
                console.error("获取相机权限失败:", error);
                showToast('获取相机权限失败');
            });
    }
}


const takePhoto = () => {
    nextTick(async () => {
      console.log(video.value)
        if (canvas.value && video.value && photo.value) {
            const context = canvas.value.getContext("2d");
            // 设置画布尺寸与取景框相同
            canvas.value.width = video.value.videoWidth;
            canvas.value.height = video.value.videoHeight;
            // 绘制取景框内的画面到画布
            if (context) {
                context.drawImage(video.value, 0, 0);
                // 将画布内容转为图片并显示
                photo.value.src = canvas.value.toDataURL();
                photo.value.style.display = "block";
                // 关闭video
                console.log('video', video.value);

                video.value.pause();

                // 关闭摄像头
                mediaStream.value?.getTracks().forEach((track: any) => track.stop());
              video.value=null
            }
        }
 
      console.log(photo.value)
      // console.log(photo.value.src)   将文件流传给后台上传,下列代码根据实际情况自定
      let file:any=photo.value.src
      let idtype=props.currPhotoType=='head'?FileUploadType.BIZ_TYPE_IDCARD2:FileUploadType.BIZ_TYPE_IDCARD1
      //文件名:时间戳+1000以内的随机数
      let  fileName=new Date().getTime()+ Math.floor(Math.random()*1000)+'.jpg'

      const loadingInstance = ElLoading.service({ fullscreen: true, background: 'rgba(0,0,0,0.1)', text: '请求中...' });
      let data = await putFile(fileName,idtype, base64ToFile(file,fileName));
      if(data){
        loadingInstance.close()
        sendValue({
          file:file,
          type:props.currPhotoType,
          url:data
        })
        showToast('上传成功!')
        emit('okUploadImg',{status:1})
      }else{
        loadingInstance.close()
        showToast('上传失败!')
        emit('okUploadImg',{status:2})

      }

    })
}


const passValue:any = inject("getIdFile")
//3.孙组件在函数中调用爷爷传递过来的函数,并在()中传递要传递的数据
const sendValue = (file) => {
  passValue(file)
}
//4.调用这个函数(也可以使用点击事件等方式触发)


//关闭相机
const closeCamera=()=>{
  // 关闭摄像头
  mediaStream.value?.getTracks().forEach((track: any) => track.stop());
  video.value=null
}
//dakai相机
const openCamera=()=>{
  console.log('打开相机')
  //打开相机
  if (takePhotoDiv.value) {
    takePhotoDiv.value.style.display = 'block'
    getVideoMedia()
  }
}

defineExpose({
  openCamera,closeCamera
})
</script>
<style scoped lang="less">

</style>
#cameraContainer {
    position: relative;
    //width: 324px;
    //height: 216px;
  width:100vw;
  height: 100vh;
  background: #000;
  overflow: hidden;
  .take-photo{
    //height:85.6*6px;
    //width: 53.98*6px;
    height: 70%;
    width: 90%;
    overflow: hidden;
    background: #000;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) ;
  }
  #video-fix{
    position: absolute;
    top: 50%;
    left: 50%;
    //transform: translate(-50%, -50%) rotate(90deg);
    transform: translate(-50%, -50%);
  }
}

#video {
    object-fit: cover;

}


.frame-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.mask {
  position: absolute;
  height:85.6*5px;
  width: 53.98*5px;
  border: 1px solid #fdfdfd;
  border-radius: 5px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  .img-head{
    position: absolute;
    bottom: 4.5%;
    right: 13.7%;
    height: 28%;
    width: 53%;
    transform: rotate(90deg);
  }
  .img-mark{
    position: absolute;
    top:7%;
    right: 9%;
    width: 37%;
    height: 22.5%;
    transform: rotate(90deg);
  }
  .tips{
    position: absolute;
    left: -50%;
    top: 50%;
    color: #fff;
    transform: rotate(90deg);
    font-size: 14px;
    background: #555657;
    border-radius: 5px;
  }

}

#frame {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 200px;
    height: 90px;
    z-index: 10;
    background-color: transparent;
}


#photo {
    display: none;

}
#captureButton{
  width: 100px;
  height: 100px;
  border-radius: 50%;
  background: #ffffff;
  position: absolute;
  bottom: 50px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  justify-content: center;
  align-items: center;
  .cap-inner{
    background: #fff;
    width: 85%;
    height: 85%;
    border-radius: 50%;
    border: 3px solid #000;

  }
}

base64转文件流

/**
 * @description: Base64 转 File
 * @param {string} base64 base64格式的字符串
 * @param {string} fileName 文件名
 * @return {File}
 */
export const base64ToFile = (base64: string, fileName: string): File => {
  const arr: string[] = base64.split(',');
  const type = (arr[0].match(/:(.*?);/) as string[])[1];
  const bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new File([u8arr], fileName, { type });
};

调用组件:

<script setup lang="ts">
import {onMounted, ref} from "vue";
import Upload from "@/components/Upload.vue";

const props=defineProps({
  currPhotoType:String
})
const _show=ref(false)
const uploadRef=ref()

const goBack =()=> {
  // window.history.back() // 删掉van-popup打开时添加的history
  _show.value = false
  //关闭相机
  uploadRef.value.closeCamera()
}

const openModal=()=>{
  _show.value=true
  setTimeout(()=>{
    //打开相机
    uploadRef.value.openCamera()
  },500)

}
onMounted(()=>{

})

const  okUpload=(e)=>{
  if(e.status==1){
    //上传成功,关闭弹框,关闭相机
    goBack()
  }if(e.status==2){
    //上传失败,关闭弹框,关闭相机
    goBack()
  }
}

defineExpose({
  openModal
})
</script>

<template>
<!--全屏弹框组件-->

<!--  @close="selectProjectCloseHandler"   @open="selectProjectOpenHandler"-->
  <van-popup v-model:show="_show"    :overlay="false"  position="bottom" :style="{ width: '100%', height: '100%'}">

    <div class="header">
      <van-nav-bar class="title" left-arrow title="身份证头像页上传" :safe-area-inset-top="true" :fixed="true"
                   @click-left="goBack" />
    </div>
    <div style="color: red">{{props}}</div>

    <Upload  ref="uploadRef" @okUploadImg="okUpload" :currPhotoType="props.currPhotoType"></Upload>
  </van-popup>
</template>

<style scoped lang="less">

</style>

2.问题及方案

2.1 ios游览器打开video相机默认是全屏的

安卓可以正常用video打开相机,ios有问题,打开时全屏的。

在iOS端的Web控件上使用video标签播放视频时,视频会自动全屏播放。

解决方案
ios端video标签必须加webkit-playsinline、playsinline属性。

android端部分视频也会存在自动全屏问题,添加webkit-playsinline属性。

 <video ref="video"  id="video-fix" :width="width" :height="height" autoplay   webkit-playsinline playsinline></video>

2.2 拍出来的图片角度有问题

拍出来图片是顺时针旋转了90度,所以需要在canvas中给图片转正
下面是一个旋转的demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<script type="text/javascript">
    function drawBeauty(beauty){
    var mycv = document.getElementById("cv");  
    var myctx = mycv.getContext("2d");

    myctx.translate(beauty.width / 2, beauty.height / 2);

    //调整这里90*3 旋转至正确角度
     myctx.rotate(((90+90*3) * Math.PI) / 180);

    myctx.drawImage(beauty, -beauty.width / 2, -beauty.height / 2);

    }
    function load(){
    var beauty = new Image();  
    //获取本题图片
    beauty.src = "./asset/WechatIMG134.jpg"; 
   

    if(beauty.complete){
       drawBeauty(beauty);
    }else{
       beauty.onload = function(){
         drawBeauty(beauty);
       };
       beauty.onerror = function(){
         window.alert('美女加载失败,请重试');
       };
    };   
    }//load
    if (document.all) {
      window.attachEvent('onload', load);  
      }else {  
      window.addEventListener('load', load, false);
      }


   
    </script>
    <canvas id="cv"  width="600" height="300" style="border:1px solid #ccc;margin:20px auto;display: block;">
        当前浏览器不支持canvas
        <!-- 如果浏览器支持canvas,则canvas标签里的内容不会显示出来 -->
</canvas>
</body>
</html>

参考:
https://blog.csdn.net/qq_30100043/article/details/106355667
https://www.cnblogs.com/html5test/archive/2012/03/01/2375558.html
https://jelly.jd.com/article/6006b1045b6c6a01506c87e6
https://www.cnblogs.com/Joe-and-Joan/p/10957818.html

2.3 拍出来的照片默认是640*480 ,照片不清晰

简而言之:video宽高要设置成 4:3或16:9才行,这里我设置成了1280*720

<video ref="video"  id="video-fix" width="1280" height="720" autoplay   webkit-playsinline playsinline></video>
<canvas ref="canvas" style="display: none" width="1280" height="720"></canvas>

   var constraints = {
        audio: false,
        video: {
             width: { min: 1280, max: 1560 }
          , height: { min: 720, max: 1440 },
          facingMode: { exact: "environment" }//设置后置,注释掉就是前置
        }
      };

navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError)

https://stackoverflow.com/questions/15849724/capture-high-resolution-video-image-html5

2.4 本地local能打开电脑前置,不是最终效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2239196.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Elasticsearch入门到落地】1、初识Elasticsearch

一、什么是Elasticsearch Elasticsearch&#xff08;简称ES&#xff09;是一款非常强大的开源搜索引擎&#xff0c;可以帮助我们从海量数据中快速找到需要的内容。它使用Java编写&#xff0c;基于Apache Lucene来构建索引和提供搜索功能&#xff0c;是一个分布式、可扩展、近实…

Rust开发一个命令行工具(一,简单版持续更新)

依赖的包 cargo add clap --features derive clap命令行参数解析 项目目录 代码 main.rs mod utils;use clap::Parser; use utils::{editor::open_in_vscode,fs_tools::{file_exists, get_file, is_dir, list_dir, read_file}, }; /// 在文件中搜索模式并显示包含它的行。…

Xshell,Shell的相关介绍与Linux中的权限问题

目录 XShell的介绍 Shell的运行原理 Linux当中的权限问题 Linux权限的概念 Linux权限管理 文件访问者的分类&#xff08;人&#xff09; 文件类型和访问权限&#xff08;事物属性&#xff09; 文件权限值的表示方法 文件访问权限的相关设置方法 如何改变文件的访问权…

golang 实现比特币内核:公钥的 SEC 编码格式详解

比特币作为区块链的一个应用,它建立在分布式系统之上,‘节点’遍布全球。为了使所有节点协同工作并作为一个整体系统运行,需要保持所有节点同步在相同的状态中,也就是说节点之间需要频繁通信,并且相互交换大量数据消息。这要求在网络上传输的消息或数据要使用某种格式编码…

【JAVA】使用IDEA创建maven聚合项目

【JAVA】使用IDEA创建maven聚合项目 1.效果图 2.创建父模块项目 2.1删除父模块下面的src目录以及不需要的maven依赖 3创建子模块项目 3.1右击父模块项目选择Module… 3.2创建子模块 3.3删除子模块下不需要的maven依赖 4.子模块创建完成后引入SpringBoot依赖启动项目

《Django 5 By Example》阅读笔记:p17-p53

《Django 5 By Example》学习第2天&#xff0c;p17-p53总结&#xff0c;总计37页。 一、技术总结 1.数据库迁移 python manage.py makemigrations blog python manage.py sqlmigrate blog 0001 python manage.py migrate 2.ORM Django自带ORM。 3.view (1)定义 p42, …

基于物联网的智能超市快速结算系统

摘 要 当今社会的商品层出不穷&#xff0c;人们因为越来越多大型仓储超市的出现使得生活更加便利&#xff0c;但许多随之而来的新问题也给人们带来了许多的不便&#xff0c;例如商家一直被更换标签不及时、货物丢失、超市内物品更换处理不及时、超市内人流高峰期人流控制不得…

阿里云Linux安装Docker服务报错问题

今天使用了阿里云99计划的服务器&#xff0c;之前用惯了 CentOS&#xff0c;这次想体验下阿里云调教的 Alibaba Cloud Linux 3 系统性能&#xff0c;但是在安装 docker 的时候遇到了问题&#xff01; 传统安装方式 之前习惯安装docker方式&#xff1a; #查看是否已经安装的D…

数据结构《链表》

文章目录 前言一、什么是链表&#xff1f;二、单向链表2.1 单向链表的个人实现2.2 单向链表的例题 三、双向链表3.1 双向链表的个人实现3.2 关于真正的java中提供的链表的使用 总结 前言 提示&#xff1a;概念来源于&#xff1a;>>LinkedList<< 一、什么是链表&am…

typesScript 制作一个简易的区块链(2)

pow 机制 1.哈希函数的特点 说到 pow 机制&#xff0c;就离不开哈希函数&#xff0c;哈希函数具有以下特点&#xff1a; 输入长度不固定&#xff0c;输出长度固定输入不同&#xff0c;输出不同输入相同&#xff0c;输出相同不可逆雪崩效应 雪崩效应&#xff1a;输入变量中只…

[Codesys]常用功能块应用分享-BMOV功能块功能介绍及其使用实例说明

官方说明 功能说明 参数 类型 功能 pbyDataSrcPOINTER TO BYTE指向源数组指针uiSizeUINT要移动数据的BYTE数pbyDataDesPOINTER TO BYTE指向目标数组指针 实例应用-ST IF SYSTEM_CLOCK.AlwaysTrue THENCASE iAutoState OF0: //读写完成信号在下次读写信号的上升沿或复位信号…

【树莓派raspberrypi烧录Ubuntu远程桌面登入树莓派】

提示&#xff1a;本文利用的是Ubuntu主机和树莓派4B开发板&#xff0c;示例仅供参考 文章目录 一、树莓派系统安装下载前准备工作下载安装树莓派的官方烧录软件imagerimager的使用方法 二、主机与树莓SSH连接查看数梅派IP地址建立ssh连接更新树莓派源地址 三、主机端远程桌面配…

Linux权限和开发工具(3)

文章目录 1. 简单理解版本控制器Git1. 如何理解版本控制 2. Git的操作2.1 Git安装2.2 Git提交身份2.3 Git提交命令2.4 Git版本管理2.5 Git下的同步 3. gdb命令3.1解决gdb的难用问题3.2 gdb/cgdb的使用 1. 简单理解版本控制器Git 1. 如何理解版本控制 我们在做项目的时候可能会…

如何在 Django 中生成 Excel 文件并上传至 FastDFS

文章目录 如何在 Django 中生成 Excel 文件并上传至 FastDFS需求背景主要任务 实现步骤 创建 Excel 文件上传 Excel 文件到 FastDFSclient.conf 保存文件 URL 到数据库组合完整的流程总结 如何在 Django 中生成 Excel 文件并上传至 FastDFS 在很多实际应用场景中&#xff0c;我…

电子应用产品设计方案-3:插座式自动温控器设计

一、设计 插座式自动温控器作为一种便捷的温度控制设备&#xff0c;在日常生活和工业应用中发挥着重要作用。它能够根据环境温度的变化自动控制连接设备的电源通断&#xff0c;实现对温度的精确调节和节能控制。本设计旨在提供一种功能强大、易于使用、安全可靠的插座式自动温控…

Redis的常用命令大全

目录 一、Redis简介 1.键值型 2.NoSQL 2.1关联和非关联 2.2查询方式 2.3事务 2.4总结 二、Redis常见命令 2.1 通用命令 2.2 String 命令 2.3 Hash类型 2.4 List类 2.5 Set集合 2.6 SortedSet类型 一、Redis简介 Redis是一种键值型的NoSql数据库&#xff0c;这里…

浅谈PostGIS中的抽稀技术——实现高效空间数据可视化的关键

目录 前言 一、原始数据介绍 1、原始完整数据 2、Qgis中展示原始数据 二、减少数据精度 1、查询函数简介 2、减少精度实战 三、ST_Simplify抽稀实现 1、ST_Simplify函数介绍 2、ST_Simplify抽稀结果 四、ST_SimplifyPreserveTopology抽稀 1、函数介绍 2、抽稀结果 …

网约车管理:规范发展,保障安全与便捷

在数字化时代&#xff0c;网约车已成为城市出行的重要组成部分&#xff0c;为公众提供了前所未有的便捷性。然而&#xff0c;随着网约车行业的迅猛发展&#xff0c;一系列管理问题也随之浮现&#xff0c;如司机资质审核不严、车辆安全标准不一、乘客权益保护不足等。这些问题不…

vue3入门和实战-vue3项目布局

文章目录 前言一、项目目标二、页面布局1.首页布局分析2. 首页布局实现App.vueLayoutIndex.vueLayoutLeft.vueHome.vueHome/components/Header.vueHome/components/Footer.vue3.首页路由4.首页效果显示总结前言 上一节,部署了vue3官方案例,我们需要结合自身项目页面的布局改…

深度学习经典模型之VGGNet

1 VGGNet 1.1 模型介绍 ​ VGGNet是由牛津大学视觉几何小组&#xff08;Visual Geometry Group, VGG&#xff09;提出的一种深层卷积网络结构&#xff0c;他们以7.32%的错误率赢得了2014年ILSVRC分类任务的亚军&#xff08;冠军由GoogLeNet以6.65%的错误率夺得&#xff09;和…