Vue2 使用 Knova Canvas 合成图片、多个视频、音频在一个画面中并播放,自定义 video control 控制条
本文转载https://blog.csdn.net/RosaChampagne/article/details/128020428?spm=1001.2014.3001.5502的文章
安装插件
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; } } } }