[enhancement] optimize video & animation fluency
corxit opened this issue · 0 comments
corxit commented
Hi, thanks for this demo, saved me a lot of time!
I've made some changes based on my app. Hope these tips help!
- canvas rendered video cause fluency problem, thus I replaced canvas displaying as original
<video/>
- ratio not work properly in some cases. I made a fullscreen wrapper to keep the ratio fixed.
- cover animation get stuck while
tick()
running. I made a new animation based ontransform
.
<template>
<div class="scaner" ref="scaner" @click="resume">
<!--<div class="banner" v-if="showBanner">-->
<!-- <i class="close_icon" @click="() => showBanner = false"></i>-->
<!-- <p class="text">若当前浏览器无法扫码,请切换其他浏览器尝试</p>-->
<!--</div>-->
<div class="cover">
<p class="line"></p>
<!--<span class="square top left"></span>-->
<!--<span class="square top right"></span>-->
<!--<span class="square bottom right"></span>-->
<!--<span class="square bottom left"></span>-->
<p class="tips">{{ active ? '扫描二维码':'扫码暂停 点击屏幕继续扫码' }}</p>
</div>
<div class="video-wrapper" ref="CanvasWrapper">
<video
class="source"
ref="video"
>
<!--v-show="showPlay"-->
<!--controls-->
</video>
<canvas class="canvas-video" ref="canvas" />
<div v-if="code != null" class="code-indicator" :style="{ top: codePosition.y+'px', left:codePosition.x+'20px', }"></div>
</div>
<!--<button v-show="showPlay" @click="run">开始</button>-->
</div>
</template>
<script>
// eslint-disable-next-line no-unused-vars
import adapter from 'webrtc-adapter';
import jsQR from 'jsqr';
export default {
name: 'Scaner',
props: {
// 使用后置相机
useBackCamera: {
type: Boolean,
default: true
},
// 扫描识别后停止
stopOnScaned: {
type: Boolean,
default: true
},
drawOnfound: {
type: Boolean,
default: true
},
// 线条颜色
lineColor: {
type: String,
default: '#03C03C'
},
// 线条宽度
lineWidth: {
type: Number,
default: 2
},
responsive: {
type: Boolean,
default: false
}
},
data () {
return {
showPlay: false,
showBanner: true,
// videoWidth: null,
// videoHeight: null,
active: false,
code:null,
}
},
computed: {
codePosition(){
if (this.code == null){
return null;
}
return {
x: this.code.location.topLeftCorner.x + (this.code.location.bottomRightCorner.x - this.code.location.topLeftCorner.x)/2 - 25,
y: this.code.location.topLeftCorner.y + (this.code.location.bottomRightCorner.y - this.code.location.topLeftCorner.y)/2 - 25,
}
},
},
watch: {
// @Depracated!
// active: {
// immediate: true,
// handler(active) {
// if (!active) {
// this.pause();
// //this.fullStop();
// }
// }
// }
},
methods: {
// 画线
drawLine (begin, end) {
this.canvas.beginPath();
this.canvas.moveTo(begin.x, begin.y);
this.canvas.lineTo(end.x, end.y);
this.canvas.lineWidth = this.lineWidth;
this.canvas.strokeStyle = this.lineColor;
this.canvas.stroke();
},
// 画框
drawBox (location) {
if (this.drawOnfound) {
this.drawLine(location.topLeftCorner, location.topRightCorner);
this.drawLine(location.topRightCorner, location.bottomRightCorner);
this.drawLine(location.bottomRightCorner, location.bottomLeftCorner);
this.drawLine(location.bottomLeftCorner, location.topLeftCorner);
}
},
tick () {
if (this.$refs.video && this.$refs.video.readyState === this.$refs.video.HAVE_ENOUGH_DATA) {
//.这里不设置会影响分辨率
this.$refs.canvas.width = this.$refs.video.clientWidth;
this.$refs.canvas.height = this.$refs.video.clientHeight;
this.canvas.drawImage(this.$refs.video, 0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
const imageData = this.canvas.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
let code = false;
try {
code = jsQR(imageData.data, imageData.width, imageData.height);
} catch (e) {
console.error(e);
}
if (code) {
//@Deprecated!
//this.drawBox(code.location);
this.found(code);
}
}
this.run();
},
// 初始化
setup () {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
this.previousCode = null;
this.parity = 0;
this.active = true;
this.canvas = this.$refs.canvas.getContext("2d");
const facingMode = this.useBackCamera ? { exact: 'environment' } : 'user';
const handleSuccess = stream => {
if (this.$refs.video.srcObject !== undefined) {
this.$refs.video.srcObject = stream;
} else if (window.videoEl.mozSrcObject !== undefined) {
this.$refs.video.mozSrcObject = stream;
} else if (window.URL.createObjectURL) {
this.$refs.video.src = window.URL.createObjectURL(stream);
} else if (window.webkitURL) {
this.$refs.video.src = window.webkitURL.createObjectURL(stream);
} else {
this.$refs.video.src = stream;
}
this.$refs.video.playsInline = true;
const playPromise = this.$refs.video.play();
playPromise.catch(() => (this.showPlay = true));
playPromise.then(this.run);
};
navigator.mediaDevices
.getUserMedia({
video: {
facingMode,
width: 1280,
height: 720
}
})
.then(handleSuccess)
.catch(() => {
navigator.mediaDevices
.getUserMedia({ video: true })
.then(handleSuccess)
.catch(error => {
this.$emit("error-captured", error);
});
});
}
},
resume(){
if (!this.active){
this.code = null;
this.active = true;
this.$refs.video.play();
this.run();
}
},
pause(){
this.active = false;
this.$refs.video.pause();
},
run () {
if (this.active) {
setTimeout(()=>{
this.tick();
},50);
//@Deprecated!
//requestAnimationFrame(this.tick);
}
},
found (code) {
this.code = code;
let codeString = code.data;
this.$emit("code-scanned", codeString);
if (this.stopOnScaned){
this.pause();
}
// @Depracated!
// if (this.previousCode !== codeString) {
// this.previousCode = codeString;
// } else if (this.previousCode === codeString) {
// this.parity += 1;
// }
// if (this.parity > 2) {
// this.code = code;
// this.parity = 0;
// this.$emit("code-scanned", codeString);
// if (this.stopOnScaned){
// this.pause();
// }
// }
},
// 完全停止
fullStop () {
if (this.$refs.video && this.$refs.video.srcObject) {
this.$refs.video.srcObject.getTracks().forEach(t => t.stop());
}
}
},
mounted () {
this.setup();
},
beforeDestroy () {
this.fullStop();
}
}
</script>
<style lang="css" scoped>
.scaner {
background: #000000;
position: absolute;
top: 0px;
left: 0;
width: 100%;
height: 100%;
}
.scaner .video-wrapper{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/*width: 100%;*/
/*min-width: 100vh;*/
/*height: 100%;*/
/*min-height: 100vw;*/
}
.scaner .video-wrapper video.source{
position: relative;
min-width: 100vw;
min-height: 100vh;
}
.scaner .video-wrapper .canvas-video{
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
opacity: 0;
}
.scaner .video-wrapper .code-indicator{
position: absolute;
width: 50px;
height: 50px;
border-radius: 100px;
background-color: #5F68E8;
border: 3px solid #fff;
z-index: 9999;
}
.scaner .cover {
width: 100%;
height: 60vh;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-60%);
z-index: 1111;
}
.scaner .cover .tips {
position: absolute;
bottom: -20%;
width: 100%;
text-align: center;
font-size: 14px;
color: #FFFFFF;
opacity: 0.8;
}
.scaner .cover .line {
width: 80%;
height: 6px;
border-radius: 100%;
margin-left: 10%;
background: #5F68E8;
background: linear-gradient(to right, #0000, #5F68E8, #0165FF, #5F68E8, #0000);
position: absolute;
animation: scan 3s infinite ease, opacity 3s infinite ease;
animation-fill-mode: both;
}
@keyframes scan {
0% {transform: translate3d(0,0,0)}
100% {transform: translate3d(0,60vh,0)}
}
@keyframes opacity {
0% {opacity:0}
15% {opacity:1}
45% {opacity:1}
100% {opacity:0}
}
</style>