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

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能打开电脑前置,不是最终效果

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何使用IDEA创建Maven/SSM工程?

鉴于很多学校还在教授SSMJSP&#xff0c;很多同学不会使用IDEA创建Maven工程&#xff0c;这里进行说明 windows下安装jdk并配置环境 添加链接描述Windows下安装Maven并配置环境 首先你要本地安装jdk&#xff0c;Maven并配置基础环境变量&#xff0c;然后对IDEA进行jdk、Mave…

SD-WAN远程监控组网解决方案

在当今&#xff0c;企业对远程监控与管理的需求日益增加。无论是连锁门店的实时运营监控&#xff0c;还是跨区域的团队项目协作&#xff0c;企业都需要高效、稳定且安全的网络支持。传统网络连接方式&#xff0c;例如MPLS&#xff08;多协议标签交换&#xff09;&#xff0c;虽…

SQL练习(2)

题源&#xff1a;牛客官网 选择题 假设创建新用户nkw&#xff0c;现在想对于任何IP的连接&#xff0c;仅拥有user数据库里面的select和insert权限&#xff0c;则列表语句中能够实现这一要求的语句是&#xff08;&#xff09; A grant select ,insert on *.* to nkw% B grant…

Python http打印(http打印body)flask demo(http调试demo、http demo、http printer)

文章目录 代码解释 代码 # flask_http_printer.pyfrom flask import Flask, request, jsonify import jsonapp Flask(__name__)app.route(/printinfo, methods[POST]) def print_info():# 分隔符separator "-" * 60# 获取请求头headers request.headers# 获取 JS…

1163:阿克曼(Ackmann)函数

【题目描述】 阿克曼(Ackmann)函数A(m&#xff0c;n)中&#xff0c;m&#xff0c;n定义域是非负整数(m≤3,n≤10)&#xff0c;函数值定义为&#xff1a; 【输入】 输入m和n。 【输出】 函数值。 【输入样例】 2 3 【输出样例】 9代码实现 #include <stdio.h>/*1163&a…

Docker配置及简单应用

谈论/理解 Docker 的常用核心部分&#xff0c;以下皆在 Ubuntu 操作系统下进行 1 国内源安装 Docker-ce 1.1 配置 Linux 内核流量转发 因为docker和宿主机的端口映射&#xff0c;本质是内核的流量转发功能&#xff0c;所以要对其进行配置 1.1.1 未配置流量转发 如果没有配置流…

前端请求后端php接口跨域 cors问题

只需要后端在网站的入口文件 一般都是 index.php 加上 这几行代码就可以了 具体的参数可以根据需要去修改 header("Access-Control-Allow-Origin: *"); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Heade…

Qt(程序打包)

Qt开发的程序最终都是要给用户使用的&#xff0c;用户的电脑上不可能装一个Qt的开发环境导入项目使用。因此项目项目开发完成后需要打包——制作成安装包&#xff0c;用户直接下载并安装即可使用。 1. 设置图标 设置图标的操作步骤如下&#xff1a; 1. 准备好图标文件&#xff…

LLM - 使用 LLaMA-Factory 微调大模型 Qwen2-VL SFT(LoRA) 图像数据集 教程 (2)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/143725947 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 LLaMA-…

HDLBIts习题(3):使用冒号表示位宽时,冒号两端必须是常量

&#xff08;1&#xff09;易错习题1&#xff1a;Circuits - Combinational Logic - Multiplexers - 256-to-1 4bit multiplexer 使用冒号表示位宽时&#xff0c;冒号两端必须是常量&#xff0c;因此如果使用变量&#xff0c;可以使用位拼接的方法。 &#xff08;2&#xff09;…

人工智能助手是否让程序员技能退化?

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

shell排序

任务描述 本关任务&#xff1a;实现shell排序算法&#xff0c;void ShellSort(int R[],int N,int d[],int t)。 相关知识 基本思想&#xff1a;对待排记录序列先作“宏观”调整&#xff0c;再作“微观”调整&#xff0c;它是由Donald.L.Shell在1959年提出来的。 所谓“宏观”…

C/C++中的结构体和联合体

c语言中为我们提供了几种基本数据类型&#xff0c;但是在实际编程中&#xff0c;有时候要表达复杂数据关系时&#xff0c;单单使用基本数据类型不能很好的表示&#xff0c;为了解决这种问题&#xff0c;可以自己构建一个结构体数据类型&#xff0c;定义的结构体数据类型与基本数…

MIT 6.S081 Lab1: Xv6 and Unix utilities翻译

Lab1: Xv6 and Unix utilities 文章目录 Lab1: Xv6 and Unix utilities实验任务启动xv6(难度&#xff1a;Easy)sleep(难度&#xff1a;Easy)pingpong&#xff08;难度&#xff1a;Easy&#xff09;Primes(素数&#xff0c;难度&#xff1a;Moderate/Hard)find&#xff08;难度&…

MySQL 如何用C语言连接

✨✨✨励志成为超级技术宅 ✨✨✨ 本文主要讲解在Linux服务器上&#xff0c;如何使用c语言MySQL库的接口来对MySQL数据库进行操作&#xff0c;如果没有服务器安装MySQL&#xff0c;也可以先学学看怎么用c语言mysql库的接口&#xff0c;还是比较容易的了。(●☌◡☌●)。那么开…

雨晨 24H2 Windows 11 IoT ltsc 2024 IE 极简版 26100.2222

文件: 雨晨 24H2 Windows 11 IoT ltsc 2024 IE 极简版 26100.2222 install.wim 大小: 1737837354 字节 修改时间: 2024年11月12日, 星期二, 12:41:56 MD5: 3511B5416EA18DD4AD2D75F133C49E25 SHA1: 817E4DF1F58AA5A584E5D9263F282C1D20533969 CRC32: EB1C1B7B 简述&#xff1a…

丹摩征文活动|FLUX.1+ComfyUI的详细部署以及实验总结

公主请阅 1. FLUX.1的简介2. 部署过程创建资源ComfyUI的部署操作部署FLUX.1 如何使用&#xff1f;实验总结&#xff1a;环境搭建与工具安装实验步骤实验结果分析总结 1. FLUX.1的简介 FLUX.1 是由黑森林实验室开发的图像生成工具&#xff0c;分为三个版本&#xff1a; FLUX-1-…

【电力系统】永磁同步电机调速系统带有扰动观测器

【电力系统】永磁同步电机调速系统带有扰动观测器( DOB)的最优滑模控制、改进补偿滑模控制、传统滑模、PID控制研究 摘要 本文研究了永磁同步电机&#xff08;PMSM&#xff09;调速系统中的不同控制策略&#xff0c;包括最优滑模控制、改进补偿滑模控制、传统滑模控制以及PID控…

手动实现h5移动端点击全屏按钮横屏展示图片,左右滑动切换,处理页面会随着手指滑动问题

页面提供全屏按钮,全屏展示的容器 <div class"container"><button click"openSwiper">点击全屏查看</button><!-- 大图 --><divclass"full"v-if"showSwiper"touchstart"handleTouchStart"touch…

RHEL 网络配置(Linux网络服务器 09)

0 引入 对于Linux系统的网络管理员来说&#xff0c;掌握Linux服务器的网络配置是至关重要的&#xff0c;同时管理远程主机也是网络管理员必须掌握的。这些是后续网络服务配置的基础。 本文&#xff0c;我们讲解如何使用nmtui命令配置网络参数&#xff0c;以及通过nmtui命令查…