一个很实用的vue视频播放器:vue-video-player
引言
目标
实现类似el-image组件的视频查看器,支持预览和切换。但 element -ui 中没有封装对于视频的查看组件,在多方调研后,引入vue-video-player实现这一功能。
功能介绍
vue-video-player 是一个基于 Video.js 封装的 Vue 组件库,旨在为 Vue 开发者提供一套简洁、可复用的视频播放器集成方案。其本质是将 Video.js 的强大功能(如 HLS 支持、字幕加载、全屏控制等)通过 Vue 的组件化机制进行封装,从而实现声明式调用和响应式更新。
官方文档
- vue-video-player:github.com/surmon-chin…
- 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) |
错误使用案例
- 在安装依赖时未注意
版本约束,导致运行时报错:
[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)
使用
基本用法
- 属性配置
我们可以通过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():暂停当前播放的音频/视频。 - ……
- 事件
-
waiting:当视频由于需要缓冲下一帧而停止时触发。 -
canplay:当浏览器可以开始播放音频/视频时触发。 -
error:当在音频/视频加载期间发生错误时触发。 -
loadedmetadata:当浏览器已加载音频/视频的元数据时触发。 - ……
- 支持的视频格式
- 可参考文档【测试说明】部分,cloud.tencent.com/developer/a…
- 若要播放m3u8视频流:1、需要引入
video.js并绑定到window上;2、安装依赖videojs-contrib-hls并引入;3、sources 要指定type: application/x-mpegURL
二次封装
基于用户操作习惯,我们需要对播放器进行二次封装,主要包括:
播放器动态宽高
- 解决什么问题
- 播放器配置项中自带的
fluid属性,可以调整视频比例来自适应容器大小,但这会导致与原始比例严重失调,比如在网页上通常是宽〉高,但如果视频是竖屏的,这时就会压缩视频高度适应容器,视觉效果大打折扣。 - 视频不足以撑满整个容器时,会存在黑边
- 播放器配置项中自带的
- 解决方案:如下流程图所示,基于当前传入的视频原始尺寸、视窗宽高、设定的最大宽高,动态计算当前视频下播放器的宽高,实现在设定的最大宽高范围内:
- 视频宽或者高大于设定最大宽高,基于比例缩放视频宽高
- 视频宽和高都不超过设定的最大宽高,使用原始视频宽高
- 实现效果
- 视频宽高和播放器宽高完全一致,避免存在黑边的现象
- 缩放后依然保持视频原始比例,保证视觉效果
- 通过CSS 样式调整,可以将播放器背景设置为transparent,当视频加载时,就不会一直呈现黑色背景
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 });
}
},
}
多视频切换播放
- 解决什么问题:当前业务背景下,多个视频在弹窗内按顺序排列,如果要查看其他视频,需要退出当前视频后,再点击另一个视频查看,操作麻烦
- 解决方案:
- 在视频预览页面增加左、右箭头icon,绑定click事件,基于当前视频索引,当切换上一条视频时,父组件将index-1索引的视频信息传入播放器组件,播放器重新渲染;切换下一条视频时,同理。
- 监听
keyDown事件,按下键盘左箭头、右箭头时,同上面逻辑。 - 增加
watch监听,当监听到视频数据 viewerData 更新时,重置视频预览数据,同时进行视频切源
- 实现效果
- 点击左右箭头,支持上一条/下一条切换视频
- 监听键盘事件,支持键盘左右箭头事件来切换视频
- 部分代码
// 重置视频状态
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);
},
视频加载提示
- 解决什么问题
- 视频加载时,页面没有内容显示,用户对视频加载无感知
- 解决方案:在视频播放器容器中,增加提示块
- 视频加载中,设置 isLoading: true,渲染提示块,提示内容:视频正在加载中,请稍后……
- 视频加载失败,设置 loadingError: true,渲染提示块,提示内容:视频加载失败;增加
el-icon-refresh-left图标,绑定click事件支持重新加载 - 重新加载事件包括:1、重置预览数据,2、视频切源
- 视频加载完成,提示块不可见,播放视频
- 实现效果
- 视频加载中、加载失败提示,用户可感知视频加载进度
- 加载失败时支持重新加载,避免因偶发网络原因导致的失败,用户无需刷新/退出就能再次尝试加载视频
- 部分代码
<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>
双击进入/退出全屏
- 解决什么问题:video-player组件未显示配置双击进入/退出全屏事件,需要手动绑定dbclick事件
- 解决方案:为视频播放器绑定dbclick事件
- 实现效果:非全屏状态下双击全屏播放,反之退出全屏状态
- 部分代码
// 双击切换全屏
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();
}
},