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