video.js自定义预览组件-旋转、下载、画中画、放大缩小功能

使用video.js实现视频播放功能

效果图 - 这里以弹窗展示为例

在这里插入图片描述
在这里插入图片描述

注意:记得安装video.js插件!!!

在这里插入图片描述

代码

父级使用:
在这里插入图片描述

videoPreview.vue文件

<!-- 视频预览组件 -->
<template>
  <el-dialog
    id="previewFileDialog"
    title="预览"
    :visible.sync="baseDialogVisible"
    :append-to-body="true"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    :class="isFull ? 'video-preview-dialog-isFull' : 'video-preview-dialog'"
  >
      <videoJs
        :options="videoOptions"
        class="video-box"
        @isFullscreen="isFullscreen"/>
  </el-dialog>
</template>

<script>
import 'video.js/dist/video-js.css';
import videoJs from "@/components/videoPreview/videoJs";

export default {
  name: "index",
  components: {
    videoJs
  },
  props: {
    viewFileUrl: { // 视频url
      type: String,
      default:''
    },
    dialogVisible:{
      type: Boolean,
      default:false
    },
    width: { // 视频宽
      type: String,
      default:'1240'
    },
    height: { // 视频高
      type: String,
      default:'760'
    },
  },
  data(){
    return{
      isFull: false, // 是否全屏
      videoOptions:{
        controls: true,// 开启交互,即是用户可控。
        muted: true,// 开启视频时是否静音
        autoplay:true, // 自动播放
        language: "zh-CN",
        // fluid: true,// 根据外层css样式大小,自动填充宽高!比较实用,可搭配响应式
        reload: "auto",// 重载
        // 其余设置根据需求添加!
        poster: '',// 视频封面
        sources: [// 视频播放源,建议本地
          {
            src: this.viewFileUrl,
            type: "video/mp4"
          }
        ],
        userActions: {
          doubleClick: false, // 限制双击
        }
      }
    }
  },
  computed: {
    baseDialogVisible: {
      get() {
        return this.dialogVisible
      },
      set(val) {
        this.$emit('update:dialogVisible', val) // visible 改变的时候通知父组件-记得父组件使用:dialogVisible.sync实现
      }
    },
  },
  mounted() {
    this.initWH();
    window.addEventListener('resize', this.calculateDialogTop)
  },

  methods:{
    initWH(){
      let dom = document.querySelector('.video-preview-dialog .el-dialog');
      const pageHeight = window.innerHeight;
      dom.style.width = `${this.width}px`;
      dom.style.height = `${this.height}px`;
      let top = (pageHeight - this.height) / 2;
      dom.style.marginTop = `${top}px`;
    },

    // 监听页面大小变化-更改top
    calculateDialogTop() {
      this.$nextTick(() => {
        let dom = document.querySelector('.video-preview-dialog .el-dialog');
        if (dom) {
          const windowHeight = window.innerHeight;
          const dialogTop = (windowHeight - this.height) / 2;
          const top = dialogTop > 0 ? dialogTop : 0;
          dom.style.marginTop = `${top}px`;
        }
      })
    },

    isFullscreen(state){
      this.isFull = state;
      if(!state){
        this.calculateDialogTop()
      }
    }
  },

  destroyed() {
    window.removeEventListener('resize', this.calculateDialogTop);
  },
}
</script>
<style scoped lang="scss">
.video-preview-dialog{
  ::v-deep .el-dialog{
    border-radius: 4px;
    .el-dialog__header{
      height: 44px;
      padding: 0 20px !important;
      line-height: 44px;
      .el-dialog__headerbtn{
        top: 14px;
        z-index: 9999;
      }
    }
    .el-dialog__body{
      padding: 20px;
      height: calc(100% - 44px);
      width: 100%;
      position: relative;
      .video-box{
        position: relative;
        width: 100%;
        height: 100%;
        overflow: hidden;
      }
      ::v-deep .video-js .vjs-tech{
        width: 100% !important;
        height: calc(100% * (1200 / 767));
      }
    }
  }
}
/* 全屏情况 */
.video-preview-dialog-isFull{
  ::v-deep .el-dialog{
    width: 100vw !important;
    height: 100vh !important;
    border-radius: 4px;
    margin-top: 0 !important;
    .el-dialog__header{
      display: none;
    }
    .el-dialog__body{
      padding: 0;
      height: 100vh;
      width: 100vw;
    }
  }
}
</style>

VideoJs.js文件

<template>
  <video
    ref="videoPlayer"
    id="myVideoPlayer"
    class="video-js"></video>
</template>

<script>
import videoJs from 'video.js';
import 'video.js/dist/video-js.css';
import video_zhCN from 'video.js/dist/lang/zh-CN.json';

export default {
  name: "VideoJs",
  props: {
    options: {
      type: Object,
      default() {
        return {};
      }
    },
  },
  data() {
    return {
      player: null,
      degree:0,
      isFullscreen:false,
      primitivePW:'', // 初始化父级元素大小
      primitivePH:''
    }
  },
  mounted() {
    this.init();
  },
  methods:{
    // 初始化播放器实例
    init(){
      videoJs.addLanguage('zh-CN', video_zhCN); // 设置为中文
      this.player = videoJs(this.$refs.videoPlayer, this.options, function onPlayerReady() {}) // 挂载到this.player
      this.customizeDom(this.player);
      this.handleFullscreenChange(this.player);
      this.handlePauseChange(this.player);
      let dom = document.querySelector('.video-preview-dialog .el-dialog .el-dialog__body .video-js');
      this.primitivePW = dom.clientWidth;
      this.primitivePH = dom.clientHeight;
    },

    // 监听全屏事件
    handleFullscreenChange(_player){
      _player.on('fullscreenchange', () => {
        if(_player.isFullscreen()) {
          // 进入全屏模式
          this.isFullscreen = true;
          this.changeWH(_player,this.isFullscreen)
        } else {
          // 退出全屏模式
          this.isFullscreen = false;
          this.changeWH(_player,this.isFullscreen)
        }
        this.$emit('isFullscreen',this.isFullscreen);
        this.customizeClose()
      });
    },

    // 控制暂停/播放的大按钮显隐
    handlePauseChange(_player){
      let bigPlayButton = document.getElementsByClassName('vjs-big-play-button')[0];
      bigPlayButton.style.display = 'none'
      _player.on('pause', () => {
        bigPlayButton.style.display = 'block'
      })
      _player.on('play', () => {
        bigPlayButton.style.display = 'none'
      })
    },

    onClose(){
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.webkitCancelFullScreen) {
        document.webkitCancelFullScreen();
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
      }
    },

    // 自定义关闭icon的显隐-全屏显示
    customizeClose(type = this.isFullscreen){
      let closeButton = this.player.el_.getElementsByClassName('close-icon')[0]
      closeButton.style.display = type ? 'flex' : 'none';
    },

    // 自定义了播放器的控制栏
    customizeDom(_player){
      let that = this;
      let Button = videoJs.getComponent('Button');
      const Component = videoJs.getComponent("Component");
      const CloseButton = videoJs.getComponent('CloseButton');

      /*
      该类继承自Video.js插件库中的Button类
      子类的构造函数只是简单地调用了父类的构造函数并没有进行其他操作
      */
      class downloadButton extends Button {
        constructor(player, options = {}) {
          super(player, options);
          this.controlText("下载"); // 内容text
        }
        handleClick() { // 下载触发
          let url = _player.options_.sources[0].src;
          that.downloadFile(url)
        }
      }
      class rotationButton extends Button {
        constructor(player, options = {}) {
          super(player, options);
          this.controlText("旋转"); // 内容text
        }
        handleClick() { // 旋转触发
          that.applyRotation(_player)
        }
      }
      CloseButton.prototype.handleClick = () => this.onClose(); // close按钮事件执行
      let closeButton = new CloseButton(this.player);
      this.player.addChild(closeButton); // 添加close按钮
      Component.registerComponent("downloadButton", downloadButton); // 注册为Component.registerComponent方法的组件
      Component.registerComponent("rotationButton", rotationButton);
      let downloadDom = that.player.getChild('controlBar').addChild('downloadButton', {}, 17); // 组件的实例-向控制栏添加下载按钮
      let rotationDom = that.player.getChild('controlBar').addChild('rotationButton', {}, 17); // 组件的实例-向控制栏添加旋转按钮
      downloadDom.addClass("download-icon"); // 添加class
      rotationDom.addClass("rotation-icon");
      closeButton.addClass("close-icon");
    },

    // 动态改变视频的宽高
    changeWH(_player,isFullscreen = this.isFullscreen){
      // 获取视频元素和父级容器元素
      let video = _player.el_.getElementsByTagName('video')[0];
      let container = _player.el_;

      // 获取父容器的宽高
      const containerWidth = !isFullscreen ? this.primitivePW : container.offsetWidth;
      const containerHeight = !isFullscreen ? this.primitivePH : container.offsetHeight;

      // 获取视频原始的宽高
      const videoWidth = video.videoWidth;
      const videoHeight = video.videoHeight;

      // 计算视频旋转后的宽高
      const angle = (this.degree || 0) * Math.PI / 180;
      const rotatedWidth = Math.abs(Math.cos(angle) * videoWidth) + Math.abs(Math.sin(angle) * videoHeight);
      const rotatedHeight = Math.abs(Math.sin(angle) * videoWidth) + Math.abs(Math.cos(angle) * videoHeight);

      // 根据父容器的宽高和视频旋转后的宽高计算缩放比例
      const scaleX = containerWidth / rotatedWidth;
      const scaleY = containerHeight / rotatedHeight;
      const scale = Math.min(scaleX, scaleY);

      // 设置视频元素的宽高
      let wd = rotatedWidth * scale;
      let hg = rotatedHeight * scale;

      let resWidth,resHeight;
      if(this.degree == 90 || this.degree == 270){
        let d = Math.max(wd, hg);
        resHeight = resWidth = d;
      }else{
        resHeight = hg;
        resWidth = wd;
      }
      video.style.width = `${resWidth}px`;
      video.style.height = `${resHeight}px`;
      video.style.left = `${(containerWidth - resWidth) / 2}px`;
      video.style.top = `${(containerHeight - resHeight) / 2}px`;
    },

    // 旋转事件
    applyRotation(_player) {
      this.degree = this.degree === 360 ? 90 : this.degree + 90
      let videoElement = _player.el_.getElementsByTagName('video')[0]; // 视频元素
      videoElement.style.transform = `rotate(${this.degree}deg)`;
      this.changeWH(_player)
    },

    // 下载
    downloadFile(url) {
      // const segments = url.split('/');
      // const lastSegment = segments.pop();
      // const link = document.createElement('a');
      // link.href = url;
      // link.download = lastSegment;
      // document.body.appendChild(link);
      // link.click();
      // document.body.removeChild(link);
      fetch(url).then(res => res.blob()).then(blob => { // 将链接地址字符内容转变成blob地址
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = '';
        link.target = '_blank';
        document.body.appendChild(link);
        link.click();
        link.remove();
      }).catch(() => {
        alert('下载文件失败');
      });
    }
  },

  beforeDestroy() {
    if (this.player) {
      this.player.dispose()
    }
  }
}
</script>
<style scoped lang="scss">
::v-deep .download-icon {
  cursor: pointer;
  height: 28px;
  background: url('~@/assets/download.png') center no-repeat;
  background-size: 18px;
}
::v-deep .rotation-icon{
  cursor: pointer;
  height: 28px;
  background: url('~@/assets/order/rotation.png') center no-repeat;
  background-size: 18px;
}
::v-deep .close-icon{
  background: url('~@/assets/order/video-close.png') center no-repeat;
  background-size: 36px;
  height: 36px !important;
  display: none;
  right: 60px !important;
  top: 60px !important;
}
::v-deep .vjs-close-button .vjs-icon-placeholder::before {
  content: none !important;
}
/* 更改全屏按钮大小 */
::v-deep .vjs-fullscreen-control .vjs-icon-placeholder:before {
  font-size: 2.1em;
  line-height: 1.5em;
}
</style>


到这里就完啦!!!后面如果接触到新需求有优化会持续更新哒
提醒自己:这是有段时间之前的代码逻辑了,或许明天的你就有不一样的想法呢!所以仅供参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/485943.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【战略前沿】丹麦正在建造一台英伟达人工智能超级计算机

【原文】Denmark is building an Nvidia AI supercomputer 【作者】Linnea Ahlgren 它将于今年上线&#xff0c;并以新的量子计算软件为特色。 过去一年最大的赢家——芯片制造商英伟达&#xff08;Nvidia&#xff09;和制药制造商诺和诺德&#xff08;Novo Nordisk&#xff0…

【C语言】linux内核pci_alloc_irq_vectors

一、注释 代码中包含了几个关于PCI&#xff08;外围组件互联&#xff09;设备中断请求&#xff08;IRQ&#xff09;向量分配的函数&#xff0c;以及内联函数声明&#xff0c;下面是对这些函数的中文注释&#xff1a; static inline int pci_alloc_irq_vectors_affinity(struc…

曲线生成 | 图解Reeds-Shepp曲线生成原理(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 什么是Reeds-Shepp曲线&#xff1f;2 Reeds-Shepp曲线的运动模式3 Reeds-Shepp曲线算法原理3.1 坐标变换3.2 时间翻转(time-flip)3.3 反射变换(reflect)3.4 后向变换(backwards) 4 仿真实现4.1 ROS C实现4.2 Python实现4.3 Matlab实现 0 专栏介绍 &#x1f5…

【竞技宝】DOTA2:lou神带队速推 AR力克Zero晋级决赛

北京时间2024年3月24日,DOTA2梦幻联赛S23中国区预选赛正在进行之中,昨日进行了本次预选赛的胜者组决赛Zero对阵AR。本场比赛双方前两局战至1-1平,决胜局AR选出一套前期进攻性十足的阵容早早取得优势,最终AR鏖战三局力克Zero晋级决赛。以下是本场比赛的详细战报。 第一局: Zero…

第九篇【传奇开心果系列】Python自动化办公库技术点案例示例:深度解读Python处理PDF文件

传奇开心果博文系列 系列博文目录Python自动化办公库技术点案例示例系列 博文目录前言一、重要作用介绍二、Python库处理PDF文件基础操作和高级操作介绍&#xff08;一&#xff09;基础操作介绍&#xff08;二&#xff09;高级操作介绍 三、Python库处理PDF文件基础操作示例代码…

ESP8266制作WIFI音箱

首先是设备截图 使用的技术: 1、Esp8266播放网络音乐 2、自己搭建一个音乐播放服务,这样播放的内容就由自己而定了,将你的服务对接支付宝,就可以实现支付宝收款语音播报了 代码 esp8266代码 #include <Arduino.h>#ifdef ESP32#include <WiFi.h> #else#inc…

(AtCoder Beginner Contest 325) ---- D - Printing Machine -- 题解

目录 D - Printing Machine&#xff1a; 题目大意&#xff1a; 思路解析&#xff1a; 代码实现&#xff1a; D - Printing Machine&#xff1a; 题目大意&#xff1a; 思路解析&#xff1a; 打印一次后&#xff0c;需要充电一微秒后才能再次打印就可以看作每微妙只能打印一…

2024年3月GESP认证Python编程一级真题试卷

2024年3月GESP认证Python编程一级真题试卷 题目总数&#xff1a;27 总分数&#xff1a;100 选择题 第 1 题 单选题 小杨的父母最近刚刚给他买了一块华为手表&#xff0c;他说手表上跑的是鸿蒙&#xff0c;这个鸿蒙是&#xff1f;&#xff08; &#xff09;。 A.小程…

03. 【Android教程】Genymotion 的安装与使用

在上一章中我们在 Eclipse 当中创建了 AVD&#xff0c;由于性能差只适合测试小型 App。这里将推荐一款性能更佳的 Android 模拟器—— Genymotion。首先我们看看 Genymotion 好在哪里。 1. Genymotion 优势 Genymotion 相对于内置模拟器有如下优势&#xff1a; 运行速度快、画…

[数据结构]二叉树的建立与遍历(递归)

一、二叉树的遍历与建立 首先我们拥有如下二叉树: 要了解二叉树遍历,我们得先了解二叉树的三种遍历方式:前序遍历,中序遍历,后序遍历 1.前序遍历 前序遍历:根,左子树,右子树 遍历的结果就是:1 2 4 8 N N 9 N N 5 10 N N 11 N N 3 6 N N 7 N N 2.中序遍历 中序遍历:左子树…

爆增49.07%!2024国自然面上项目申报,再创新高

毕业推荐 SSCI&#xff08;ABS一星&#xff09; • 社科类&#xff0c;3.0-4.0&#xff0c;JCR2区&#xff0c;中科院3区 • 13天录用&#xff0c;28天见刊&#xff0c;13天检索 SCIE&#xff1a; • 计算机类&#xff0c;6.5-7.0&#xff0c;JCR1区&#xff0c;中科院2区…

大东方保险集团陈志远:洞察保险行业的重要性及未来三年发展前景

在当今社会,保险行业作为风险管理的重要工具,正日益凸显其不可或缺的地位。大东方保险集团陈志远近日在接受采访时,深入探讨了保险行业的重要性以及未来三年的发展前景。 一、保险行业的重要性 陈志远指出,保险行业在现代经济中扮演着举足轻重的角色。它不仅是社会稳定的“减震…

Spring自定义注解防重提交方案(参数形式Token令牌)

防重提交通常在需要防止用户重复提交表单或执行某些敏感操作时使用&#xff0c;以确保系统的数据一致性和安全性&#xff0c;本文章集结了通用场景下防重提交&#xff08;参数形式&Token令牌&#xff09;&#xff0c;采用Java的特性&#xff08;注解和AOP&#xff09;&…

后端如何返回404地址

当我们网站输入不存在的地址&#xff0c;经常会出现404的页面&#xff0c;这是如何做到的 1.添加配置 spring:mvc:view:prefix: /templates/suffix: .html 2.resources下添加templates目录&#xff0c;下面放404的网站 3.添加依赖&#xff0c;版本在主pom里面配置好了&#x…

Memcached非关系型数据库介绍

使用背景 Memcached 不是一个数据库&#xff0c;而是一个高性能的分布式内存对象缓存系统。它主要用于减轻数据库负载&#xff0c;提高动态Web应用的速度、可扩展性和性能。Memcached 的工作原理是将数据存储在内存中&#xff0c;以提供快速的数据访问。当应用程序需要访问数据…

夸克、迅雷网盘项目拉新推广去哪对接?推荐几个一手项目渠道!

在进行夸克、迅雷网盘等项目的拉新推广时&#xff0c;对接合适的渠道和平台是至关重要的。本文将分享几个地推、网推一手渠道&#xff0c;帮助您轻松开展拉新推广项目。 1.任推邦 国内知名项目拉新平台&#xff0c;这个平台对接的项目大多是官方直签&#xff0c;截至目前已经…

Facebook账号防封的有效方法附解禁方法

Facebook作为跨境主要业务平台&#xff0c;一直以来封号率都非常高。相信点进来的各位或多或少地遇见了个人号被封&#xff0c;广告账户被禁&#xff0c;FB主页被封等情况。针对此类问题&#xff0c;今天就小编也来分享自己的Facebook防封经验。 一、Facebook被封原因 主要有以…

【电能管理】安科瑞AEM碳排放功能表/电碳表/碳结算/三相嵌入式电表/尖峰平谷峰谷分时/最大需量/二部制电价/节能降碳/CE认证

什么是电碳表!!! 电碳表是一种计量设备&#xff0c;可以帮助用户了解和控制电力使用中的碳排放。原理是根据实际电力系统的计量数据&#xff0c;动态计算并更新电碳因子&#xff08;平均每度电所蕴含的碳排放量&#xff09;&#xff0c;并且这个数据是实时更新的&#xff0c;真…

【云效测试管理】测试用例、测试计划(用例执行)、缺陷管理、测试报告全流程管理

背景 我们公司之前使用过很多测试管理软件&#xff0c;从最开始原始的Excel来管理缺陷&#xff0c;再到Worktitle管理缺陷&#xff0c;再到现在的云效&#xff1b;用例管理管理在本地。 再后来我们转用云效流水线来部署测试环境&#xff0c;开始尝试发掘云效中的“测试管理”…

泛型可空类型Nullable<T>

.Net Framework 4.8版本开始&#xff0c;引入了可空类型Nullable<T>. 对于引用类型的变量来说&#xff0c;如果未赋值&#xff0c;默认情况下是 Null 值&#xff0c; 对于值类型的变量&#xff0c;如果未赋值&#xff0c;整型变量的默认值为 0,Boolean默认为false&…