核心语法是 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>