首页 > 其他分享 >Vue2 使用 Knova Canvas 合成图片、多个视频、音频在一个画面中并播放,自定义 video control 控制条

Vue2 使用 Knova Canvas 合成图片、多个视频、音频在一个画面中并播放,自定义 video control 控制条

时间:2024-01-11 11:12:36浏览次数:36  
标签:control videoObj Canvas startAt 自定义 endAt playbackRate width videoProgress

本文转载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;
      }
    }
  }
}

 

 

标签:control,videoObj,Canvas,startAt,自定义,endAt,playbackRate,width,videoProgress
From: https://www.cnblogs.com/sttchengfei/p/17958112

相关文章

  • ios开发中:当一个 viewcontroller 中嵌套了一个 viewcontroller.view,pushViewcontrolle
    在iOS开发中,当你在一个UIViewController中嵌套另一个UIViewController的视图时,即使嵌套的视图能够正确显示,该嵌套的UIViewController实例可能不会被完全加入到视图控制器层次结构中。这可能导致一些问题,比如无法使用pushViewController:animated:方法进行导航。原因这种......
  • 使用Winform开发自定义用户控件,以及实现相关自定义事件的处理
    在我们一些非标的用户界面中,我们往往需要自定义用户控件界面,从而实现不同的内容展示和处理规则,本篇内容介绍使用Winform开发自定义用户控件,以及实现相关自定义事件的处理。1、用户控件的界面分析对于比较规范的界面,需要进行一定的分析,以便从中找到对应的规则,逐步细化为自定义用......
  • DevExpress LayoutControl使用
    1.向窗体中添加LayoutControl控件从工具箱,将LayoutControl控件拖入窗体后,最好立即设置该控件的尺寸和位置,否则修改起来会比较麻烦拖入的同时,在下图所示的情况下,1.1点击小三角,弹出LayoutContrl任务1.2然后继续点击ChooseDockStyle右侧的向下小箭头,1.3选择中......
  • 自定义快捷键实操与踩坑
    0.缘起要做一个自定义快捷键的功能,web端实现。这里分为两块逻辑,一部分是快捷键的应用,一部分是快捷键的定义。先从应用说起,快捷键实际上是对浏览器按键动作的监听,不过由于浏览器本身也有快捷键,就会有冲突的情况,自定义的要求应运而生。快捷键的定义,其实类似于设置的功能,也是存、......
  • Java Spring Boot Controller、Service、DAO实践
    如果你用过SpringMVC,那就对Controller、Service、DAO三者的概念一定不陌生,我们通过Controller来接收请求和返回响应,具体的业务处理则放在Service层处理,而DAO则实现具体与数据库等持久层的操作。今天从分层的角度来学习下在SpringBoot中的简单应用,业务场景也很简单,就......
  • vue3利用qrcode.vue并通过canvas合并图片
    <template><canvasid="canvas"width="300"height="400"></canvas><el-buttonstyle="margin-top:20px"type="danger"plain@click="downloadCode"......
  • go 新建一个自定义包
    一、概述在go中新建一个自定义包供其他包使用。步骤:1.新建一个目录2.目录下新建一个xxx.go文件3.在xxx.go文件中使用packagexxx(包名)4.此时你的包已经新建好了5.在需要使用上面包的地方导入即可,如:import"xxxx"p......
  • springboot通过自定义注解@Log实现日志打印
    springboot通过自定义注解@Log实现日志打印效果图实操步骤注意,本代码在springboot环境下运行,jdk1.81.引入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency>......
  • Three.js——十五、Box3、相机动画、lookAt()视线方向、管道漫游案例、OrbitControls
    正投影相机正投影相机和透视相机的区别如果都以高处俯视去看整个场景,正投影相机就类似于2d的可视化的效果,透视相机就类似于人眼观察效果调整left,right,top,bottom范围大小如果你想整体预览全部立方体,就需要调整相机的渲染范围,比如设置上下左右的范围。使用场景:正投影可以......
  • 自定义ADFS登录页
    修改adfs登录页公司名称:Set-AdfsGlobalWebContent-CompanyName"ExchangeOWA" 参考:ADFS自定义:https://learn.microsoft.com/zh-cn/windows-server/identity/ad-fs/operations/ad-fs-customization-in-windows-server#custom-themes-and-advanced-custom-themes 修改ADFS登录页......