阅读视图

发现新文章,点击刷新页面。

一个很实用的vue视频播放器:vue-video-player

引言

目标

实现类似el-image组件的视频查看器,支持预览和切换。但 element -ui 中没有封装对于视频的查看组件,在多方调研后,引入vue-video-player实现这一功能。

功能介绍

vue-video-player 是一个基于 Video.js 封装的 Vue 组件库,旨在为 Vue 开发者提供一套简洁、可复用的视频播放器集成方案。其本质是将 Video.js 的强大功能(如 HLS 支持、字幕加载、全屏控制等)通过 Vue 的组件化机制进行封装,从而实现声明式调用和响应式更新。

官方文档

  1. vue-video-player:github.com/surmon-chin…
  2. video.js:docs.videojs.com/docs/api/pl…

安装

版本兼容性

随着 Vue3 的发布及其 Composition API 的普及,vue-video-player 的维护团队逐步将开发重心转向 Vue3 生态。对于新版本有如下改变:

  • 6.x 及以上版本开始依赖 Vue3 的 runtime-core 和新的组件模型;
  • 不再支持Vue.use()这种全局注册方式;
  • 使用了 Vue3 特有的响应式系统(Proxy 代替 defineProperty);
  • 构建工具链升级至 Vite,导致与 Vue2 项目的 webpack 配置存在冲突风险。

版本选择策略

需求场景 建议版本 安装命令 引入方式
Vue2 项目 ^5.0.2 npm install vue-video-player@^5.0.2 Vue.use(VueVideoPlayer)
Vue3 项目 ^6.0.0 npm install vue-video-player@latest app.use(VueVideoPlayer)

错误使用案例

  1. 在安装依赖时未注意版本约束,导致运行时报错:
[Vue warn]: Unknown custom element: <video-player>
Did you register the component correctly?

2. Vue2 版本最佳实践建议:

// package.json 中显式锁定版本
"dependencies": {
  "vue-video-player": "^5.0.2",
  "video.js": "^7.10.2"
}

// main.js 中正确引入
import Vue from 'vue'
import VueVideoPlayer from 'vue-video-player'
import 'vue-video-player/node_modules/video.js/dist/video-js.css'

Vue.use(VueVideoPlayer)

使用

基本用法

  1. 属性配置

我们可以通过playerOptions配置自定义属性,关键属性包括src(视频地址)、:controls(是否显示控制栏)、:autoplay(自动播放)、:loop(循环播放)以及:volume(音量设置)等。

<template>
  <div v-if="visible" class="video-mask-wrapper" tabindex="0">
    <div class="viewer-wrapper" @click.self="handleClose">
         ……
      <div class="video-player-wrapper" :style="videoBoxStyle">
        <video-player
          :key="`${index}-${viewerData.subLink}`"
          ref="videoPlayer"
          class="video-player"
          :options="playerOptions"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    ……
  },
  computed: {
    playerOptions() {
      return {
        autoplay: true, // 自动播放
        controls: true, // 显示播放控制条
        preload: 'metadata',
        fluid: false, // 自适应容器,设为false,使用自定义css样式控制
        sources: [{ src: this.viewerData.subLink, type: 'video/mp4' }],
        controlBar: {
          volumePanel: { inline: false }, // 音量面板,inline置为false时:点击音量图标时弹出独立的垂直滑块
          playToggle: true, // 控制条的播放暂停按钮
        },
        bigPlayButton: false, // 隐藏大播放按钮
      };
    },
  },
};
</script>

<style scoped lang="scss">
.video-mask-wrapper {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 2000;
  outline: none;
  .viewer-wrapper {
    position: absolute;
    inset: 0;
    z-index: 2002;
    display: flex;
    align-items: center;
    justify-content: center;
    .video-player-wrapper {
      position: relative;
      max-width: 1000px;
      max-height: 850px;
      .video-player {
        width: 100%;
        height: 100%;
      }
    }
  }
}
::v-deep .video-js {
  width: 100% !important;
  height: 100% !important;
}
::v-deep .video-js .vjs-tech {
  width: 100% !important;
  height: 100% !important;
  object-fit: contain;
  background-color: transparent;
}
</style>

2. 方法

const player = this.$refs.videoPlayer && this.$refs.videoPlayer.player;

player.pause();
player.load();
player.src([{ src: newSrc, type: 'video/mp4' }]);
  • addTextTrack():向音频/视频添加新的文本轨道。
  • canPlayType():检测浏览器是否能播放指定的音频/视频类型。
  • load():重新加载音频/视频元素。
  • play():开始播放音频/视频。
  • pause():暂停当前播放的音频/视频。
  • ……
  1. 事件
  • waiting:当视频由于需要缓冲下一帧而停止时触发。
  • canplay:当浏览器可以开始播放音频/视频时触发。
  • error:当在音频/视频加载期间发生错误时触发。
  • loadedmetadata:当浏览器已加载音频/视频的元数据时触发。
  • ……
  1. 支持的视频格式
  • 可参考文档【测试说明】部分,cloud.tencent.com/developer/a…
  • 若要播放m3u8视频流:1、需要引入video.js并绑定到window上;2、安装依赖videojs-contrib-hls并引入;3、sources 要指定type: application/x-mpegURL

二次封装

基于用户操作习惯,我们需要对播放器进行二次封装,主要包括:

播放器动态宽高

  1. 解决什么问题
    • 播放器配置项中自带的fluid 属性,可以调整视频比例来自适应容器大小,但这会导致与原始比例严重失调,比如在网页上通常是宽〉高,但如果视频是竖屏的,这时就会压缩视频高度适应容器,视觉效果大打折扣。
    • 视频不足以撑满整个容器时,会存在黑边
  2. 解决方案:如下流程图所示,基于当前传入的视频原始尺寸、视窗宽高、设定的最大宽高,动态计算当前视频下播放器的宽高,实现在设定的最大宽高范围内:
    • 视频宽或者高大于设定最大宽高,基于比例缩放视频宽高
    • 视频宽和高都不超过设定的最大宽高,使用原始视频宽高
  3. 实现效果
    • 视频宽高和播放器宽高完全一致,避免存在黑边的现象
    • 缩放后依然保持视频原始比例,保证视觉效果
    • 通过CSS 样式调整,可以将播放器背景设置为transparent,当视频加载时,就不会一直呈现黑色背景

动态计算播放器尺寸.jpg 4. 部分代码

 computed: {
    // 将动态计算的视频宽高绑定到播放容器
    getVideoBoxStyle() {
      const w = this.boxWidth || 0;
      const h = this.boxHeight || 0;
      const style = {};

      if (w > 0 && h > 0) {
        style.width = w + 'px';
        style.height = h + 'px';
      }

      return style;
    },
  },    
methods:{
    // 基于原始尺寸和当前可用空间,计算播放器尺寸    
    handleBoxSizeResize() {
      const { w, h } = this.naturalVideo || {};
      const { LIMIT_W, LIMIT_H } = this;

      // 取视窗宽高与设置的最大宽高的最小值,作为播放器的最大宽高
      const maxW = Math.min(window.innerWidth, LIMIT_W);
      const maxH = Math.min(window.innerHeight, LIMIT_H);

      // 取宽度、高度缩放比例的最小值,保证视频完整显示
      const scale = Math.min(1, Math.min(maxW / w, maxH / h));

      this.boxWidth = Math.max(0, Math.round(w * (isFinite(scale) ? scale : 1)));
      this.boxHeight = Math.max(0, Math.round(h * (isFinite(scale) ? scale : 1)));
    },

    // 基于最大宽高,动态计算视频宽高
    getVideoContainerSize() {
      const player = this.$refs.videoPlayer.player;
      const node = player.el().querySelector('video');

      const compute = () => {
        const originVideoWidth = node.videoWidth;
        const originVideoHeight = node.videoHeight;
        this.naturalVideo = { w: originVideoWidth, h: originVideoHeight };
        this.handleBoxSizeResize();
      };

      // 若已加载元数据,直接计算;否则监听到加载后,执行compute
      if (node && node.readyState >= 1) {
        compute();
      } else if (node) {
        node.addEventListener('loadedmetadata', compute, { once: true });
      }
    },
}

多视频切换播放

  1. 解决什么问题:当前业务背景下,多个视频在弹窗内按顺序排列,如果要查看其他视频,需要退出当前视频后,再点击另一个视频查看,操作麻烦
  2. 解决方案:
    • 在视频预览页面增加左、右箭头icon,绑定click事件,基于当前视频索引,当切换上一条视频时,父组件将index-1索引的视频信息传入播放器组件,播放器重新渲染;切换下一条视频时,同理。
    • 监听keyDown事件,按下键盘左箭头、右箭头时,同上面逻辑。
    • 增加watch监听,当监听到视频数据 viewerData 更新时,重置视频预览数据,同时进行视频切源
  3. 实现效果
    • 点击左右箭头,支持上一条/下一条切换视频
    • 监听键盘事件,支持键盘左右箭头事件来切换视频
  4. 部分代码
// 重置视频状态
    handleVideoStateReset() {
      this.boxWidth = 0;
      this.boxHeight = 0;
      this.naturalVideo = { w: 0, h: 0 };
      this.isLoading = true;
      this.loadError = false;
    },
    // 视频切源
    handleVideoCutResource() {
      const player = this.$refs.videoPlayer && this.$refs.videoPlayer.player;
      const newSrc = this.viewerData && this.viewerData.subLink;

      if (player && newSrc) {
        player.pause();
        player.src([{ src: newSrc, type: 'video/mp4' }]);
        player.load();

        // 监听视频事件
        this.bindVideoEvents(player);

        player.one('loadedmetadata', () => {
          this.getVideoContainerSize();
        });
      }
    },    
    // 上一个视频
    handlePreVideoChange(index) {
      this.handleVideoSwitch(index, -1, this.dialogData.carveUrlList.length);
    },
    // 下一个视频
    handleNextVideoChange(index) {
      this.handleVideoSwitch(index, 1, this.dialogData.carveUrlList.length);
    },
    // 键盘左右箭头切换视频
    handleVideoKeyDown(event) {
      const length = this.dialogData.carveUrlList.length;
      const index = Number(this.videoSafeAreaViewer.index) || 0;

    // 视频查看器未渲染、视频列表为空、只有一个视频时,不执行切换事件
      if (!this.videoSafeAreaViewer.visible || !length || length === 1) {
        return;
      }

      if (event.key === 'ArrowRight') {
        this.handleVideoSwitch(index, 1, length);
      } else if (event.key === 'ArrowLeft') {
        this.handleVideoSwitch(index, -1, length);
      }
    },
    // 切换视频
    handleVideoSwitch(index, step, len) {
      const videoList = this.dialogData.carveUrlList || [];
      const idx = (index + step + len) % len;

      this.handleVideoPreview(videoList[idx], idx, this.dialogData.carveUrlList);
    },

视频加载提示

  1. 解决什么问题
    • 视频加载时,页面没有内容显示,用户对视频加载无感知
  2. 解决方案:在视频播放器容器中,增加提示块
    • 视频加载中,设置 isLoading: true,渲染提示块,提示内容:视频正在加载中,请稍后……
    • 视频加载失败,设置 loadingError: true,渲染提示块,提示内容:视频加载失败;增加el-icon-refresh-left图标,绑定click事件支持重新加载
    • 重新加载事件包括:1、重置预览数据,2、视频切源
    • 视频加载完成,提示块不可见,播放视频
  3. 实现效果
    • 视频加载中、加载失败提示,用户可感知视频加载进度
    • 加载失败时支持重新加载,避免因偶发网络原因导致的失败,用户无需刷新/退出就能再次尝试加载视频
  4. 部分代码
 <div class="video-player-wrapper" :style="getVideoBoxStyle">
        <!-- 视频加载提示 -->
        <div v-show="isLoading || loadingError" class="video-status-tip">
          <div class="status-content">
            <!-- 加载中 -->
            <div v-if="isLoading && !loadingError" class="loading-state">
              <i class="el-icon-loading status-icon"></i>
              <span class="status-text">视频正在加载中,请稍后...</span>
            </div>
            <!-- 加载失败 -->
            <div v-else-if="loadingError" class="error-state">
              <span class="status-text" style="color: #f56c6c"
                >视频加载失败<i class="el-icon-refresh-left" @click.stop="handleVideoReload"></i
              ></span>
            </div>
          </div>
        </div>
        <!-- 视频播放器 -->
        <video-player
          :key="`${index}-${viewerData.subLink}`"
          ref="videoPlayer"
          class="video-player"
          :options="playerOptions"
          @dblclick.native="toggleFullscreen"
          @waiting="handleVideoWaiting"
          @canplay="handleVideoCanPlay"
          @loadeddata="handleVideoLoadedData"
          @error="handleVideoError"
        />
      </div>

双击进入/退出全屏

  1. 解决什么问题:video-player组件未显示配置双击进入/退出全屏事件,需要手动绑定dbclick事件
  2. 解决方案:为视频播放器绑定dbclick事件
  3. 实现效果:非全屏状态下双击全屏播放,反之退出全屏状态
  4. 部分代码
  // 双击切换全屏
    toggleFullscreen() {
      const videoPlayer = this.$refs.videoPlayer;
      const player = videoPlayer && videoPlayer.player;

      if (!player) {
        console.warn('[MKVideoSafeAreaViewer] toggleFullscreen: player not ready');
        return;
      }

      if (player.isFullscreen()) {
        player.exitFullscreen();
      } else {
        player.requestFullscreen();
      }
    },
❌