效果图如下:
安装插件
npm install vue-konva@2 konva --save
在main.js中使用
import Vue from 'vue';
import VueKonva from 'vue-konva';
Vue.use(VueKonva);
相关实现代码
html
<template>
<div class="video-preview-wrapper">
<div ref="videoPreviewBox" class="video-preview-box">
<div class="video-box">
<v-stage ref="stage" :config="stageConfig" @click="onControl">
<v-layer ref="layer">
<v-image ref="frame" :config="imageConfig" />
</v-layer>
<v-layer>
<v-image v-for="(cover, index) in videoCovers" :key="index" :config="cover" />
</v-layer>
</v-stage>
</div>
<div class="control-play">
<div class="control-play-btn" @click="onControl">
<i
:class="[{ 'el-icon-video-pause': isPlay }, { 'el-icon-video-play': !isPlay }]"
/>
</div>
<div class="control-progress common-progress">
<div>
<el-slider
v-model="videoProgress"
:show-tooltip="false"
:max="canvas.duration"
input-size="small"
@change="onProgressChange"
/>
</div>
</div>
<div class="current-time">{{ currentTime }}</div>
/
<div class="duration">{{ duration }}</div>
<div class="video-speed-box">
<el-dropdown placement="bottom" @command="onCommand">
<div class="video-speed-show">{{ playbackRate }}x</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="1">0.5x</el-dropdown-item>
<el-dropdown-item command="2">1x</el-dropdown-item>
<el-dropdown-item command="3">1.5x</el-dropdown-item>
<el-dropdown-item command="4">2x</el-dropdown-item>
<el-dropdown-item command="5">3x</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="control-voice common-progress">
<span class="voice-icon" />
<div class="voice-slider">
<el-slider v-model="voiceProgress" input-size="small" @change="onVoiceChange" />
</div>
</div>
<div class="fullscreen" title="全屏" @click="onFullScreen">
<i class="el-icon-full-screen" />
</div>
</div>
</div>
</div>
</template>
相关方法
export default {
name: 'VideoPreview',
data() {
return {
stageConfig: {
width: window.innerWidth,
height: (window.innerHeight - 64),
},
imageConfig: {
image: null,
width: window.innerWidth,
height: (window.innerHeight - 64),
},
canvas: {
duration: 10,
volume: 1,
playbackRate: 1,
frames: [
{
imageUrl: require('./bg1.jpg'),
duration: 3,
videos: [
{
x: 20,
y: 100,
width: 200,
height: 200,
cover: require('./VfE_html5.jpg'),
url: require('./VfE_html5.mp4'),
volume: 1,
playbackRate: 1,
},
{
x: 420,
y: 100,
width: 200,
height: 200,
cover: require('./video_thumb.jpg'),
url: require('./video.mp4'),
volume: 1,
playbackRate: 1,
},
],
audios: [
{
url: require('./dengnixiake.flac'),
volume: 1,
},
],
},
{
imageUrl: require('./bg2.jpg'),
duration: 3,
videos: [
{
x: 100,
y: 100,
width: 200,
height: 200,
cover: require('./VfE_html5.jpg'),
url: require('./flower.mp4'),
volume: 1,
playbackRate: 1,
},
],
audios: [],
},
{
imageUrl: require('./bg3.jpg'),
duration: 2,
videos: [],
audios: [
{
url: require('./shuohaobuku.flac'),
volume: 1,
},
],
},
{
imageUrl: require('./bg4.jpg'),
duration: 2,
videos: [],
audios: [],
},
],
},
videoCovers: [],
videos: [],
audios: [],
isPlay: false,
duration: 0,
currentTime: '00:00:00',
videoProgress: 0,
playbackRate: 1,
voiceProgress: 100,
videoTimeTimer: null,
videoSceneTimer: null,
videoTimers: [],
audioTimers: [],
};
},
watch: {
videoProgress(value) {
// 如果播放完成,则暂停播放,清除视频时间定时器
if (value === this.canvas.duration) {
this.isPlay = false;
clearInterval(this.videoTimeTimer);
}
// 更换视频背景图
this.canvas.frames.forEach(({ imageUrl, startAt, endAt }) => {
if (value >= startAt && value < endAt) {
const img = new Image();
img.src = imageUrl;
img.onload = () => {
if (`http://localhost:8080${imageUrl}` !== this.imageConfig.image.src) {
this.imageConfig.image = img;
}
};
}
});
// 暂停不在播放时间范围内的窗口视频
this.videos
.filter(({ startAt, endAt }) => (this.videoProgress < startAt || this.videoProgress > endAt))
.forEach(({ videoObj }) => {
videoObj.pause();
videoObj.currentTime = 0;
});
// 暂停不在播放时间范围内的音频
this.audios
.filter(({ startAt, endAt }) => (this.videoProgress < startAt || this.videoProgress > endAt))
.forEach(({ audioObj }) => {
audioObj.pause();
audioObj.currentTime = 0;
});
},
},
created() {
this.duration = this.formatVideoTime(this.canvas.duration);
// 计算出每一个场景的开始时间、结束时间
const durationList = this.canvas.frames.map(({ duration }) => duration);
durationList.reduce((prev, current, idx) => {
if (idx <= 1) {
this.canvas.frames[0].startAt = 0;
this.canvas.frames[0].endAt = prev;
}
this.canvas.frames[idx].startAt = prev;
this.canvas.frames[idx].endAt = prev + current;
return prev + current;
});
// 将所有场景的窗口视频、音频初始化
this.canvas.frames.forEach(({ videos, audios, startAt, endAt }) => {
videos.forEach((video) => {
const videoObj = document.createElement('video');
videoObj.src = video.url;
videoObj.muted = true;
videoObj.addEventListener('play', () => {
this.videoCovers = [];
this.timerCallback();
});
this.videos.push({
videoObj,
x: video.x,
y: video.y,
width: video.width,
height: video.height,
startAt,
endAt,
});
});
audios.forEach((audio) => {
const audioObj = document.createElement('audio');
audioObj.src = audio.url;
audioObj.volume = audio.volume;
this.audios.push({ audioObj, startAt, endAt });
});
});
if (this.canvas.frames?.[0]) {
// 第一个场景的视频封面
this.videoCovers = this.canvas.frames[0].videos?.map(({ cover, x, y, width, height }) => {
const image = new Image();
image.src = cover;
return { x, y, width, height, image };
});
// 第一个场景的背景图
const img = new Image();
img.src = this.canvas.frames[0].imageUrl;
img.onload = () => {
this.imageConfig.image = img;
};
}
},
mounted() {
this.ctx = this.$refs.frame.getNode().getContext('2d');
// 在没有cover的情况下,可设置视频首帧为封面
this.videos
.filter(({ startAt, endAt }) => (this.videoProgress >= startAt && this.videoProgress < endAt))
.forEach(({ videoObj, x, y, width, height }) => {
videoObj.addEventListener('loadeddata', () => {
videoObj.play();
this.ctx.drawImage(videoObj, x, y, width, height);
setTimeout(() => {
videoObj.pause();
}, 100);
});
});
},
methods: {
timerCallback() {
this.videos
.filter(({ startAt, endAt }) => (this.videoProgress >= startAt && this.videoProgress < endAt))
.forEach(({ videoObj, x, y, width, height }) => {
if (videoObj.paused || videoObj.ended) {
return;
}
this.ctx.drawImage(videoObj, x, y, width, height);
clearTimeout(this.videoSceneTimer);
this.videoSceneTimer = setTimeout(() => {
this.timerCallback();
}, 0);
});
},
onControl() {
this.isPlay = !this.isPlay;
if (this.canvas.duration <= this.videoProgress) {
this.videoProgress = 0;
}
this.controlPlay();
},
onProgressChange(val) {
// 设置窗口小视频的播放进度
this.videos
.filter(({ startAt, endAt }) => (this.videoProgress >= startAt && this.videoProgress < endAt))
.forEach(({ videoObj, startAt }) => {
videoObj.currentTime = val - startAt;
});
// 设置音频的播放进度
this.audios
.filter(({ startAt, endAt }) => (this.videoProgress >= startAt && this.videoProgress < endAt))
.forEach(({ audioObj, startAt }) => {
audioObj.currentTime = val - startAt;
});
this.updateVideoProgress();
this.controlPlay();
// 显示在播放时间范围内的窗口视频
this.videos
.filter(({ startAt, endAt }) => (this.videoProgress > startAt && this.videoProgress < endAt))
.forEach(({ videoObj, x, y, width, height }) => {
videoObj.play();
this.ctx.drawImage(videoObj, x, y, width, height);
setTimeout(() => {
videoObj.pause();
}, 100);
});
},
controlPlay() {
clearInterval(this.videoTimeTimer);
if (this.isPlay) {
// 定时器定时更新视频时间
this.videoTimeTimer = setInterval(() => {
this.updateVideoProgress();
}, 1000 / this.playbackRate);
}
this.videoTimers = [];
this.audioTimers = [];
this.videos.forEach(({ videoObj, startAt, endAt }) => {
// 控制视频的播放、暂停
if (this.videoProgress >= startAt && this.videoProgress < endAt) {
if (this.isPlay) {
videoObj.play();
} else {
videoObj.pause();
}
}
// 控制即将播放的视频的播放、暂停
if (this.videoProgress < startAt) {
const videoTimer = setTimeout(() => {
if (this.isPlay) {
videoObj.play();
} else {
videoObj.pause();
}
}, (startAt - this.videoProgress + 1) * 1000);
this.videoTimers.push(videoTimer);
}
});
this.audios.forEach(({ audioObj, startAt, endAt }) => {
// 控制音频的播放、暂停
if (this.videoProgress >= startAt && this.videoProgress < endAt) {
if (this.isPlay) {
audioObj.play();
} else {
audioObj.pause();
}
}
// 控制即将播放的音频的播放、暂停
if (this.videoProgress < startAt) {
const audioTimer = setTimeout(() => {
if (this.isPlay) {
audioObj.play();
} else {
audioObj.pause();
}
}, (startAt - this.videoProgress + 1) * 1000);
this.audioTimers.push(audioTimer);
}
});
},
updateVideoProgress() {
if (this.videoProgress >= this.canvas.duration) {
this.videoProgress = this.canvas.duration;
} else {
this.videoProgress += 1;
}
this.currentTime = this.formatVideoTime(this.videoProgress);
},
formatVideoTime(time) {
const currentTime = time;
let hour = parseInt(currentTime / 3600, 10);
let minute = parseInt((currentTime % 3600) / 60, 10);
let seconds = parseInt(currentTime % 60, 10);
hour = hour < 10 ? `0${hour}` : hour;
minute = minute < 10 ? `0${minute}` : minute;
seconds = seconds < 10 ? `0${seconds}` : seconds;
return `${hour}:${minute}:${seconds}`;
},
onCommand(val) {
let playbackRate = 0;
switch (val) {
case '1':
playbackRate = 0.5;
break;
case '2':
playbackRate = 1;
break;
case '3':
playbackRate = 1.5;
break;
case '4':
playbackRate = 2;
break;
case '5':
playbackRate = 3;
break;
default:
playbackRate = 1;
break;
}
this.playbackRate = playbackRate;
this.videos
.filter(({ startAt, endAt }) => (this.videoProgress >= startAt && this.videoProgress < endAt))
.forEach(({ videoObj }) => {
videoObj.playbackRate = playbackRate;
});
this.audios
.filter(({ startAt, endAt }) => (this.videoProgress >= startAt && this.videoProgress < endAt))
.forEach(({ audioObj }) => {
audioObj.playbackRate = playbackRate;
});
},
onVoiceChange(val) {
const newVolume = val / 100;
this.audios
.filter(({ startAt, endAt }) => (this.videoProgress >= startAt && this.videoProgress < endAt))
.forEach(({ audioObj }) => {
audioObj.volume = newVolume;
});
},
onFullScreen() {
const element = this.$refs.videoPreviewBox;
const isFullScreen = document.fullscreen || document.mozFullScreen || document.webkitIsFullScreen ||
document.webkitFullScreen || document.msFullScreen;
if (isFullScreen) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
return;
}
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullScreen();
}
},
},
};
css样式
.video-preview-wrapper {
position: relative;
height: 100%;
.video-preview-box {
.video-box {
position: absolute;
}
.control-play {
width: 100%;
position: absolute;
left: 0;
bottom: 5%;
display: flex;
align-items: center;
padding: 0 10px;
color: #fff;
.control-play-btn {
margin-right: 20px;
font-size: 24px;
cursor: pointer;
}
.control-progress {
width: 60%;
}
.current-time {
margin: 0 10px 0 20px;
}
.duration {
margin-left: 10px;
}
.video-speed-box {
width: 40px;
display: flex;
justify-content: center;
margin: 0 20px;
background-color: aliceblue;
cursor: pointer;
.el-dropdown {
width: 100%;
text-align: center;
}
}
.control-voice {
width: 10%;
}
.fullscreen {
margin-left: 20px;
cursor: pointer;
}
}
}
}