pc端调用摄像头,本地实现H5拍照功能;
解决拍照模糊问题;
解决角度问题;
文件目录
-----wlCamera
-------img
----------camera.png
----------left.png
----------right.png
-----------tip,png
-------less
----------index.modules.less
-------ts
----------h5.ts
----------interface.ts
-------index.tsx
index.tsx
import { defineComponent, reactive, ref } from "vue";
import { Modal, Button } from 'view-ui-plus';
import tipPng from "./img/tip.png";
import cameraPng from "./img/camera.png";
import leftPng from "./img/left.png";
import rightPng from "./img/right.png";
import H5 from "./ts/h5"
import styles from "./less/index.module.less"
import {Props} from "./ts/interface"
export default defineComponent({
emits: ["FileEvent"],
props: {
fileName: {
type: String,
required: true
},
ratio: {
type: Object,
default: () => {
return {
width: 1280,
height: 720
}
}
}
},
setup (props: Props, {emit, slots}) {
let W = props.radio?.width || 1280, H = props.radio?.height || 720;
let url = ref<string>(""),
text = ref<string>("请点击左上角允许访问摄像头。"),
popVb = ref<boolean>(false),
textShow = ref<boolean>(true),
popCamera = ref<boolean>(false),
imgUtilSrc = ref<string>(""),
videoEle: HTMLVideoElement,
cameraH5 = new H5({radio: {width: W, height: H}, fileName: props.fileName});
const videoRef = ref<any>(null);
cameraH5.navigatorExtendH5();
const onClick = () => {
popCamera.value = true;
cameraH5.getCaremaPermissionsH5(stream => {
if (typeof stream == "string") {
text.value = stream;
textShow.value = true;
popVb.value = stream == "系统无访问摄像头权限,配置后才能使用。";
url.value = location.origin;
} else {
popVb.value = false;
textShow.value = false;
videoEle = document.getElementById("video") as HTMLVideoElement;
videoEle.srcObject = stream;
videoEle.onloadedmetadata = function (e) {
videoEle.play();
};
}
})
}
const Camera = () => {
let file: File = cameraH5.createCanvas(videoEle);
cameraH5.CloseVideoH5();
popCamera.value = false;
emit("FileEvent", file);
}
const rotateLeft = () => {
cameraH5.rotateCount -= 1;
videoRef.value.style.transform = 'translate(-50%, -50%) rotate(' + 90 * cameraH5.rotateCount +'deg)';
imgUtilSrc.value = cameraH5.setImageSrc(videoEle);
}
const rotateRight = () => {
cameraH5.rotateCount += 1;
videoRef.value.style.transform = 'translate(-50%, -50%) rotate(' + 90 * cameraH5.rotateCount +'deg)';
imgUtilSrc.value = cameraH5.setImageSrc(videoEle);
}
return () => (
<>
<span onClick={onClick}>{slots.default?.() || <Button>拍照</Button>}</span>
<Modal z-index={9999} v-model={popVb.value} title="多媒体设置" width={900} footer-hide={true} draggable={true}>
<div id="chrome-tip-dialog" class="pd20">
<p>1. 打开在浏览器地址栏中输入 chrome://flags/#unsafely-treat-insecure-origin-as-secure</p>
<p>2. Insecure origins treated as secure 切换成 Enabled 状态, 如图所示:</p>
<div class="img-camera-tip">
<img src={tipPng} alt=""></img>
</div>
<p>3. 输入框中输入域名 {url.value}</p>
<p>4. 点击重启后生效。</p>
</div>
</Modal>
<Modal v-model={popCamera.value} z-index={1010} title="拍照" width={1280} styles={{top: "10px"}} footer-hide={true} draggable={true} onOnCancel={() => cameraH5.CloseVideoH5()}>
<div class={styles.content}>
<div class={styles.tip} v-show={textShow.value}>{text.value}</div>
<video ref={videoRef} class={styles.video} id="video"></video>
<canvas id="canvas" class={styles.canvas}></canvas>
<div v-show={!textShow.value} class={styles.toolBtns}>
<div class={styles.btn} onClick={rotateRight}>
{slots.iconRight?.() || <img class={styles.btnImg} title="右转" src={rightPng} alt="" />}
</div>
<div class={styles.btn} onClick={Camera}>
{slots.iconCamera?.() || <img class={styles.btnCameraImg} title="拍照" src={cameraPng} alt="" />}
</div>
<div class={styles.btn} onClick={rotateLeft}>
{slots.iconLeft?.() || <img class={styles.btnImg} title="左转" src={leftPng} alt="" />}
</div>
</div>
</div>
</Modal>
</>
)
}
})
ts
h5.ts
import {Props} from "./interface"
export default class H5 {
private ratioWidth: number;
private ratioHeight: number;
private fileName: string;
rotateCount: number;
private Angle: number;
MediaStreamTrack: any;
constructor (cfg: Props) {
this.ratioWidth = cfg.radio?.width || 1280;
this.ratioHeight = cfg.radio?.height || 720;
this.fileName = cfg.fileName;
this.rotateCount = 0;
this.Angle = 0;
}
navigatorExtendH5 () {
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function (constraints: MediaStreamConstraints) {
var getUserMedia = navigator.getUserMedia;
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
}
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
}
}
}
getCaremaPermissionsH5 (cb) {
window.navigator.mediaDevices.getUserMedia({ audio: false, video: {width: this.ratioWidth, height: this.ratioHeight} }).then(stream => {
this.MediaStreamTrack = stream.getTracks()[0];
cb && cb(stream);
}).catch(err => {
var tip = '系统无访问摄像头权限,配置后才能使用。'
if (err.name == 'PermissionDeniedError') {
tip = '无权限访问摄像头'
} else if (err.name.indexOf('NotFoundError') > -1) {
tip = '摄像头未连接'
}
cb && cb(tip);
})
}
transfer (stream: MediaStream, cb) { // 录制视频
const mediaRecorder = new MediaRecorder(stream);
let recordedBlobs: Blob[] = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
};
mediaRecorder.onstop = function(e) {
const blob = new Blob(recordedBlobs, { type: 'video/mp4' });
cb && cb(blob)
}
mediaRecorder.start();
setTimeout(function () {
mediaRecorder.stop();
}, 10000);
}
dataURLtoFile (dataurl) {
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], this.fileName, {type: mime});
}
getPX (ele, property) {
return Number(ele.style[property].slice(0, -2))
}
createCanvas (videoEle) {
let fileUrl = this.setImageSrc(videoEle);
return this.dataURLtoFile(fileUrl);
}
CloseVideoH5 () {
this.rotateCount = 0;
this.Angle = 0;
this.MediaStreamTrack && this.MediaStreamTrack.stop();
}
setImageSrc (videoEle) {
var canvas = document.createElement('canvas');
var ctx: CanvasRenderingContext2D = canvas.getContext('2d') as CanvasRenderingContext2D;
canvas.width = this.ratioWidth;
canvas.height = this.ratioHeight;
this.Angle = (90 * this.rotateCount) % 360
if (this.Angle < 0) {
this.Angle += 360
}
switch (this.Angle) {
case 90: // 旋转90度
canvas.width = this.ratioHeight;
canvas.height = this.ratioWidth;
ctx.rotate(Math.PI / 2);
ctx.drawImage(videoEle, 0, -this.ratioHeight, this.ratioWidth, this.ratioHeight);
break;
case 180: // 旋转180度
ctx.rotate(Math.PI);
ctx.drawImage(videoEle, -this.ratioWidth, -this.ratioHeight, this.ratioWidth, this.ratioHeight);
break;
case 270: // 旋转270度
canvas.width = this.ratioHeight;
canvas.height = this.ratioWidth;
ctx.rotate(3 * Math.PI / 2);
ctx.drawImage(videoEle, -this.ratioWidth, 0, this.ratioWidth, this.ratioHeight);
break;
default:
ctx.drawImage(videoEle, 0, 0, this.ratioWidth, this.ratioHeight);
}
return canvas.toDataURL('image/png');
}
}
interface.ts
interface Radio {
width: number,
height: number,
}
interface Props {
fileName: string,
radio?: Radio
}
export {Radio, Props}
img
less
.video, .canvas {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: absolute;
transform-origin: center center;
margin: 0 auto;
}
.content {
position: relative;
text-align: center;
border: 5px dashed #999;
margin: 0 auto;
overflow: hidden;
height: 720px;
}
.tip {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: absolute;
font-size: 16px;
color: #E6A23C;
}
.toolBtns {
position: absolute;
bottom: 20px;
width: 100%;
height: 40px;
line-height: 40px;
}
.btn {
width: 40px;
margin: 0 5px;
cursor: pointer;
display: inline-block;
}
.btnImg {
width: 40px;
}
.btnCameraImg {
width: 35px;
}