开头直接切入,不需要标题,直接以对话式进入。
第一次在 Vue3 后台管理系统里集成视频播放器,我图省事,直接找了一个看起来最顺眼的组件库自带播放器,结果半个小时就放弃了。不是样式太丑,就是 API 设计得太"有个性",要么干脆和 Vue3 的响应式系统"八字不合"。后来换了@videojs-player/vue这套基于 Video.js 的 Vue3 封装,才算真正把"在项目里播视频"这件事彻底打通。这篇文章我就把这套组件从安装、基础用法、事件监听、实例访问,到 HLS 直播流、多清晰度切换、皮肤定制的完整链路都捋一遍,把我实际开发中踩过的坑也一并交代清楚。适合正在做 Vue3 后台、数据可视化大屏、商城或者任何需要内嵌视频播放器的前端同学参考。
1. 为什么我在 Vue3 项目里最终选了 @videojs-player/vue
先说结论:Vue3 生态里视频播放器的封装方案不少,但 @videojs-player/vue 是目前少有的、同时兼顾了"保留原生 Video.js 能力"和"Vue3 响应式开发体验"的封装。
在最终敲定这套方案之前,我实际对比过几条路:
| 方案 | 优点 | 缺点 |
|---|---|---|
直接用原生<video>标签 | 零依赖、最简单 | 控件样式的浏览器差异巨大,功能全靠自己造轮子 |
| element-plus / antd-vue 等 UI 库自带播放器 | 风格统一、上手快 | 实际就是个阉割版<video>,功能太少,HLS、清晰度切换基本没戏 |
| vue3-video-play 等轻量封装 | API 简洁 | 扩展性弱,想深入定制 Video.js 插件时无从下手 |
| @videojs-player/vue | 完整保留 Video.js 全部能力,提供 Vue3 组件式 API | 需要额外理解 Video.js 本身的概念 |
我的判断标准其实就三条:第一,底层必须是 Video.js,因为它是目前最成熟的开源 HTML5 视频播放器框架,插件生态丰富;第二,封装层要贴合 Vue3 的响应式写法,不能每个配置项都要手动 ref 去同步;第三,遇到复杂场景(直播流、多清晰度、自定义字幕)时,得能拿到原生的 player 实例。同时满足这三条的,就是 @videojs-player/vue。
这个组件库的本质是一层"翻译官":把 Vue3 的 props、事件、响应式数据,翻译成 Video.js 底层的 options、事件回调和 player 实例方法。它的核心依赖video.js本身并不关心你用的什么前端框架,而这一层封装让你的代码写起来像在写 Vue 组件,而不是在操作 DOM。
这里插一句版本问题。截止我写这篇文章时,@videojs-player/vue 的 2.x 版本对应 Vue3,1.x 版本是给 Vue2 用的,两者 API 有差异。如果你的项目是 Vue3,安装的时候一定要确认装的是@latest,不要复制老教程里的npm install @videojs-player/vue就直接跑,因为 npm 默认安装的确实是最新版,但网上大量文章还停留在 Vue2 时代的写法。
2. 安装与版本选型:依赖关系和兼容性检查
安装命令很简单,但有两个坑要提前说。
npm install @videojs-player/vue video.js注意这里video.js也需要显式安装。@videojs-player/vue 把 video.js 放在了peerDependencies(对等依赖)里,意思是你需要自己在项目里安装它。某些老版本的 npm 不会自动安装 peerDependencies,你不装的话跑起来直接报Cannot find module 'video.js'。
安装完成之后,我建议你打开package.json确认一下版本:
"dependencies": { "@videojs-player/vue": "^2.0.0", "video.js": "^8.x.x" }video.js 在 2023 年底左右发布了 8.x 版本,和 7.x 相比在 CSS 结构和一些内部 API 上有调整。@videojs-player/vue 2.x 对这两者都兼容,但如果后续你要自己写 Video.js 插件,建议以你实际安装的版本为准去查对应版本文档,因为 7 和 8 在videojs.registerPlugin等方法上行为一致,但在部分 UI 覆写上不一样。
安装完成后,在组件里引入的方式有两种。
方式一:在 SFC 里直接引入(推荐)
<script setup> import { VideoPlayer } from '@videojs-player/vue' import 'video.js/dist/video-js.css' </script> <template> <VideoPlayer src="https://vjs.zencdn.net/v/oceans.mp4" /> </template>方式二:全局注册
在main.js中:
import { createApp } from 'vue' import App from './App.vue' import { VideoPlayer } from '@videojs-player/vue' import 'video.js/dist/video-js.css' const app = createApp(App) app.component('VideoPlayer', VideoPlayer) app.mount('#app')两种方式我都用过,如果你只是在某个页面里需要播放器,用第一种局部引入就够了,还能让打包体积按需走。如果整个后台系统很多地方都要用,第二种全局注册写起来更省事。
还有一点:记得引 CSS。很多人安装完播放器发现一坨原生 HTML 控件堆在页面上,样式全没生效,就是漏了import 'video.js/dist/video-js.css'这一步。
3. 基础用法:几十行代码跑通第一个播放器
理论铺垫结束,直接看代码。跑通一个最基础的可播放视频,模板里就一行:
<template> <VideoPlayer :src="videoSrc" autoplay controls style="width: 100%" /> </template> <script setup> import { ref } from 'vue' import { VideoPlayer } from '@videojs-player/vue' import 'video.js/dist/video-js.css' const videoSrc = ref('https://vjs.zencdn.net/v/oceans.mp4') </script>autoplay是自动播放,controls是显示控制条。src是视频地址,你可以直接传一个 mp4 链接,也可以传一个包含多清晰度的对象。我们后面专门讲。
先别急着跑,这里有个容易被忽略的点:VideoPlayer 组件渲染出的 DOM 结构里,默认包含一个<video>标签和 Video.js 生成的整套控件层。它的根容器是一个<div>,这个 div 默认没有高度。如果你不给它设置宽度和高度,或者它的父容器没有明确高度,播放器可能只显示一条进度条或者干脆看不到。
我一般习惯给它一个固定的容器:
<div style="width: 800px; height: 450px"> <VideoPlayer src="..." style="width: 100%; height: 100%" /> </div>或者用 class 控制:
<VideoPlayer class="video-wrap" src="..." />.video-wrap { width: 100%; aspect-ratio: 16 / 9; }这里用aspect-ratio是 CSS 里最省事的写法,播放器会一直保持 16:9 的比例,不需要在 JS 里算高度。Safari 的某些老版本对 aspect-ratio 支持不太行,但后台系统基本都是 Chromium 内核,实测下来问题不大。
跑通了基础播放之后,你会发现 Video.js 的默认皮肤是深色底、蓝色高亮的,和很多后台系统的设计风格不太搭。这个我们留到"视觉定制"那一节专门处理。
4. 核心配置项拆解:每个属性背后的行为差异
@videojs-player/vue 的options属性对应 Video.js 的配置对象,但组件同时放开了很多顶层 props,让你可以直接通过属性方式传递。先看一个稍微完整一点的例子:
<template> <VideoPlayer :src="videoSrc" :options="playerOptions" autoplay controls :loop="false" :muted="false" :playsinline="true" preload="auto" @ready="onPlayerReady" @play="onPlay" @pause="onPause" @ended="onEnded" @timeupdate="onTimeUpdate" @error="onError" /> </template>4.1 最常用的顶层 props 详解
| 属性名 | 类型 | 默认值 | 作用 |
|---|---|---|---|
src | string / object | 无 | 视频地址,支持字符串和 Video.js source 对象 |
poster | string | 无 | 封面图地址 |
controls | boolean | true | 是否显示控制条 |
autoplay | boolean / string | false | 是否自动播放。传入'muted'表示静音自动播放 |
muted | boolean | false | 是否静音 |
loop | boolean | false | 是否循环播放 |
preload | string | 'metadata' | 预加载策略:'none'/'metadata'/'auto' |
playsinline | boolean | false | iOS Safari 内联播放 |
crossorigin | string | 无 | CORS 属性 |
poster | string | 无 | 封面图 |
options | object | 无 | 传给 Video.js 的完整配置 |
有两个属性你要特别留意,因为它们的"默认值"和直觉相反。
controls默认是true,但如果你通过options传入配置时没写controls,Video.js 的行为是显示控制条。而如果你在 options 里写了controls: false,又同时用 props 传了controls,props 的优先级更高,最终还是会显示。
autoplay建议不要直接传true。现代浏览器的自动播放策略非常严格:带声音的自动播放几乎都会被浏览器拦截。如果你确实需要进页面自动播放,最好传'muted',即静音自动播放。但需要注意,当传递'muted'时,即使播放器界面上显示的是静音状态,用户点一下按钮就能恢复声音,体验并不差。
4.2 options 里的几个"进阶"配置
除了顶层 props,更多底层能力是通过options传入的。下面这个配置覆盖了我日常最常用的场景:
const playerOptions = { autoplay: false, controls: true, preload: 'auto', language: 'zh-CN', playbackRates: [0.5, 1, 1.5, 2], // 倍速播放 controlBar: { volumePanel: { inline: false }, pictureInPictureToggle: true // 画中画按钮 }, notSupportedMessage: '当前浏览器不支持播放该视频', sources: [ { src: 'https://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' } ] }language: 'zh-CN'这句话很多人会忽略。Video.js 默认是英文界面,不设置的话,你的播放器显示的是"Play""Mute""Settings"这些英文,后台管理系统里给客户演示时非常掉价。设置成zh-CN后,按钮文本会变成中文。
不过要注意一点:中文化依赖 Video.js 的语言包。Video.js 8.x 内置了zh-CN语言包,你不需要额外下载,直接设置language: 'zh-CN'即可。如果你用的是 7.x,可能需要手动引入video.js/dist/lang/zh-CN.json并注册,这点版本之间差异比较大。
playbackRates是倍速播放列表。默认 Video.js 是没有倍速按钮的,加上这个配置后,控制条会出现一个"速度"选项,用户可以选择 0.5、1、1.5、2 倍速。
4.3 src 和 options.sources 的区别
这是最让我一开始踩坑的地方:顶层srcprop 和 options 里的sources数组是什么关系?
其实很简单:组件内部会把src转成{ src: yourSrc, type: 根据文件后缀推断 }的形式,合并进最终的 options.sources。两者都传时,顶层src会覆盖 options 里的 sources。
这里有一个实际经验:如果你的视频源不是 mp4,而是 HLS(m3u8)或者 DASH(mpd)格式,强烈建议用 options.sources 明确指定 type,不要依赖组件自动推断。例如:
const playerOptions = { sources: [ { src: 'https://example.com/live/stream.m3u8', type: 'application/x-mpegURL' } ] }因为组件按文件后缀推断 type 时,对.m3u8的处理不一定符合 HLS 播放器的要求。显式指定type: 'application/x-mpegURL'是最稳妥的。
5. 访问播放器实例与事件监听:掌控播放器的真正钥匙
很多人的需求不只是"放个视频",而是要响应播放进度、上传学习记录、在某个时间点做弹窗,或者拿到 player 实例调用 API。这一节讲清楚。
5.1 通过 ref 获取 player
@videojs-player/vue 在组件挂载完成后,会把 Video.js 的 player 实例传给ready事件,同时我们也给组件加一个ref:
<template> <VideoPlayer ref="videoPlayerRef" :src="videoSrc" @ready="onPlayerReady" /> </template> <script setup> import { ref } from 'vue' import { VideoPlayer } from '@videojs-player/vue' const videoPlayerRef = ref(null) let player = null const onPlayerReady = (payload) => { // payload 就是 Video.js 的 player 实例 player = payload console.log(player.currentTime()) } </script>拿到 player 实例之后,Video.js 的几乎所有 API 都可以直接用:
player.play() // 播放 player.pause() // 暂停 player.currentTime(30) // 跳转到第 30 秒 player.playbackRate(1.5) // 设置倍速 player.volume(0.5) // 设置音量 player.src({ src: '新地址', type: 'video/mp4' }) // 动态换源 player.reset() // 重置播放器状态这里有一个 5.2 的坑:ready事件的触发时机。由于 Video.js 的初始化是异步的,如果你在 Vue 组件的onMounted里立刻通过videoPlayerRef.value去拿什么,通常会拿到null或者未完全初始化的实例。正确的方式是在ready回调里操作,或者用一个标记 ref 记录是否 ready,再在需要的地方等待。
const isReady = ref(false) const onPlayerReady = (playerInstance) => { isReady.value = true player = playerInstance } // 在某个按钮点击事件里使用 const jumpTo = (time) => { if (isReady.value) { player.currentTime(time) } }5.2 事件监听:组件事件 vs player 事件
组件本身也暴露了很多事件,直接写在模板上即可:
<VideoPlayer @play="...一些逻辑" @pause="...另外一些逻辑" />但更复杂的事件,比如timeupdate(播放进度更新)和loadedmetadata(元数据加载完成),在模板上监听也没问题。下面是完整的写法:
<template> <VideoPlayer @timeupdate="(currentTime, event) => handleTimeUpdate(currentTime, event)" @loadedmetadata="onLoadedMetaData" /> </template> <script setup> function handleTimeUpdate(currentTime, event) { console.log('当前播放时间:', currentTime) } function onLoadedMetaData(payload) { const { player, duration } = payload console.log('视频总时长:', duration) } </script>注意这里timeupdate事件的第一个参数是当前播放时间(秒),第二个才是原生 event。文档里写得很清楚,但新手容易当成原生事件直接取event.target.currentTime。
我的建议:高频事件(如 timeupdate)里的逻辑一定要轻量。如果你想在 timeupdate 里上报学习进度,建议做一个节流,比如每 5 秒上报一次,而不是每一帧都发请求:
let lastTime = 0 const onTimeUpdate = (currentTime) => { if (currentTime - lastTime >= 5) { lastTime = currentTime reportProgress(currentTime) // 上报请求 } }5.3 一些常用事件速查表
| 事件名 | 说明 |
|---|---|
ready | 播放器初始化完成,返回 player 实例 |
play | 开始播放时触发 |
pause | 暂停时触发 |
ended | 播放结束时触发 |
timeupdate | 播放进度更新,约 250ms 触发一次 |
loadedmetadata | 视频元数据(时长、宽高等)加载完成 |
waiting | 视频缓冲等待时触发 |
playing | 从缓冲状态恢复播放时触发 |
fullscreenchange | 全屏状态变化时触发 |
error | 播放错误时触发 |
volumechange | 音量变化时触发 |
5.4 动态切换视频源
在后台管理系统里,"切换视频"是最常见的操作。比如课程列表点一个视频,播放器播另一个。
<template> <div> <button @click="changeVideo('https://vjs.zencdn.net/v/oceans.mp4')">视频1</button> <button @click="changeVideo('https://vjs.zencdn.net/v/elephants-dream.mp4')">视频2</button> <VideoPlayer :src="videoSrc" @ready="onReady" /> </div> </template> <script setup> import { ref } from 'vue' const videoSrc = ref('https://vjs.zencdn.net/v/oceans.mp4') let player = null const onReady = (instance) => { player = instance } const changeVideo = (src) => { videoSrc.value = src // 如果你需要自动播放、从头开始播放: if (player) { player.currentTime(0) player.play() } } </script>在这里,videoSrc的响应式变化会驱动组件内部更新,组件会调用 Video.js 的src()方法换源。但换源之后视频会不会从头播放,取决于 Video.js 的配置,和组件无关。如果你希望换源后立即从头播,需要自己调用player.currentTime(0)和player.play()。
另外有一个很实用的技巧:切换 src 后,海报图最好也同步更新。你可以使用posterprop 绑定响应式数据。
6. 响应式数据驱动的几种绑定方式:别让播放器和数据脱节
Vue3 最大的特性就是响应式,但 Video.js 是在内部维护了一套自己的状态。如何让这两套状态"对齐"是实际开发中最容易出问题的点。
6.1 用 computed 动态生成播放器配置
当播放器配置和组件内部的业务数据强相关时,建议用 computed 来生成配置:
const currentLesson = ref({ videoUrl: 'xxx.mp4', poster: 'xxx.jpg', title: '第 1 课' }) const playerOptions = computed(() => { return { sources: [ { src: currentLesson.value.videoUrl, type: 'video/mp4' } ], poster: currentLesson.value.poster, language: 'zh-CN' } })<VideoPlayer :options="playerOptions" />这样每次currentLesson变化时,组件会收到新的 options 对象,并执行对应的更新逻辑。注意:options 和 src 同时变化时,src 的优先级更高,所以二选一即可,不要既传 src 又传 options.
6.2 响应式属性 vs 直接调用 player 方法
我见过不少开发者在代码里纠结:到底是用响应式绑定控制播放,还是直接调 player 方法?我的建议是有一个简单的判断标准:
- "用户看得到的状态"用响应式绑定,比如
:muted="isMuted"、:src="videoSrc"; - "瞬时的动作"直接调方法,比如 play、pause、seek。
一个典型的错误是把play/pause做成响应式数据:
<VideoPlayer :isPlay="isPlaying" />@videojs-player/vue 确实提供了isPlay这个 prop,但它更偏向于控制"初始状态"。如果你把播放状态做成受控的(即完全由外部数据控制播放和暂停),会出现一些时序问题——比如用户手动点暂停后,isPlaying仍然是true,下次数据一变,播放器又自动播放了,体验非常诡异。
所以我的做法是:播放暂停这种高频瞬时操作,用 ref + 事件回调来同步状态;播放地址、静音、倍速等配置,用响应式 props。这样可以避免"受控/非受控"混乱的问题。
6.3 监听视频进度并同步到 UI
在后台管理系统里,经常需要做一个"当前播放到第几分钟"的组件外 UI。比如:
<template> <div> <VideoPlayer :src="videoSrc" @timeupdate="onTimeUpdate" @durationchange="onDurationChange" /> <div class="progress-info"> 当前进度:{{ formattedCurrent }} / {{ formattedDuration }} </div> </div> </template> <script setup> import { ref, computed } from 'vue' const currentTime = ref(0) const duration = ref(0) const onTimeUpdate = (time) => { currentTime.value = time } const onDurationChange = ({ duration: d }) => { duration.value = d } const formattedCurrent = computed(() => formatTime(currentTime.value)) const formattedDuration = computed(() => formatTime(duration.value)) function formatTime(seconds) { const m = Math.floor(seconds / 60) const s = Math.floor(seconds % 60) return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` } </script>这里用了durationchange事件来获取总时长。注意组件事件的第一个参数是随事件变化的,durationchange的第一个参数是一个 payload 对象,我习惯结构出 duration。每个事件参数结构略有不同,建议用的时候打印一次确认,别盲写。
7. Vite / TypeScript / 后台管理系统里实际踩坑记录
理论上这一节应该叫"常见问题",但我更想把真实项目里遇到过的、让我挠头的事情具体列出来,可能更有参考价值。
7.1 Vite 构建时 video.js 样式加载失败
在 Vite 项目中引入video.js/dist/video-js.css本身没问题,但如果你用的是非标准路径或者二次封装的库,偶尔会遇到构建警告:
Failed to parse source map from ...这个警告通常不影响功能,只是 source map 解析不出来。可以忽略。如果你的项目要求零警告,可以在vite.config.js里配置:
// vite.config.js export default defineConfig({ build: { sourcemap: false } })但更实际地说,大多数冲突来自样式顺序。如果你在组件里import 'video.js/dist/video-js.css'之后,又引入了其他 UI 库的全局样式,播放器的部分样式可能被覆盖。建议把 video-js.css 放在全局样式文件的更靠前位置,或者确保播放器容器内不使用 UI 库的全局 reset 类。
7.2 TypeScript 下的类型推导
@videojs-player/vue 自带类型声明,用起来还算舒服。在<script setup>中:
import { VideoPlayer } from '@videojs-player/vue' import type { VideoPlayerReadyEvent } from '@videojs-player/vue'事件参数的类型需要具体查看。如果不确定,我习惯先不写类型,鼠标悬停在 vscode 里看它推导出来的类型,再回填。这在 TS 项目里非常实用,不需要每次都翻文档。
如果确实遇到类型问题,比如某个事件报错,可以做一个简单的声明:
const onPlayerReady = (payload: any) => { // 这里 any 也不是不行,但建议拿到实例后转为 Player 类型 const player = payload as Player }需要安装@types/video.js才能用Player类型:
npm install -D @types/video.js这里我提醒一句:技术债要尽早还。用any虽然一时省事,但当你的播放器生命周期函数多了之后,IDE 就完全失去代码提示了,你会被各种拼写错误反复折磨。
7.3 后台管理系统中的"路由缓存"问题
后台管理系统几乎都会用到 keep-alive 缓存页面,比如从列表页进入详情页,返回时希望保留列表状态。但这和视频播放器有一个隐蔽的冲突:keep-alive 缓存的组件不会重新执行 onMounted,而播放器依赖 DOM 挂载完成才能初始化。
如果你把 VideoPlayer 放在一个被 keep-alive 缓存的页面里,路由切换后回来,播放器可能已经"死"了,表现为黑屏、不能播放、控制条没有反应。
我的解决办法有两个。
方案一:不缓存播放器所在页面。
在路由配置里给播放器页面设置meta: { keepAlive: false },并在 keep-alive 上判断:
<router-view v-slot="{ Component }"> <keep-alive :include="cachedViews"> <component :is="Component" /> </keep-alive> </router-view>方案二:在 onActivated 生命周期里重新初始化。
如果你的播放器确实必须处于缓存组件中,可以在onActivated钩子里调用组件实例重新初始化,但建议直接销毁重建:
import { nextTick, ref } from 'vue' const showPlayer = ref(true) onActivated(async () => { // 销毁后重新创建播放器组件 showPlayer.value = false await nextTick() showPlayer.value = true })实战中我们要尽量避免这个场景。后台管理系统中,视频播放页通常不需要缓存——你从课程列表进入播放页,用户返回列表后再回到播放页,大概率是期望从头重新加载列表,而不是恢复上一次的视频进度。
7.4 打包体积过大
引入 Video.js 和 @videojs-player/vue 之后,bundle 体积会明显增加不少。在后台管理系统首屏优化时,这一步经常被忽略。解决方案是路由级别代码分割:
const routes = [ { path: '/video', component: () => import('@/views/VideoPage.vue') // 播放器只在这里引用 } ]把播放器组件放到某个页面级组件里,并用动态 import 加载路由,播放器的 JS 和 CSS 会自动拆成一个独立的 chunk,只有在进入该路由时才加载。这是最省事的优化方案。
7.5 跨域问题:视频源不允许跨域
这个问题在开发环境最常见。你在本地跑 Vue 项目http://localhost:5173,视频源在另一个域名下或者 CDN 上,控制台会报 CORS 错误。
Video.js 的很多能力(比如 canvas 截图、运行时分析)依赖视频资源支持跨域。遇到 CORS 报错时,有三个方向排查:
- 确认视频源服务端配置了
Access-Control-Allow-Origin: *或允许你的域名; - 给 VideoPlayer 组件传
crossorigin="anonymous"属性; - 如果是开发环境且视频 URL 是相对路径,确认 vite 的 proxy 配置没有覆盖到视频地址。
其中第三条最坑——我在实际项目中遇到过一次,视频 URL 恰好以/api开头,结果被 vite 开发代理转发到了后端服务,而不是 CDN,于是返回了个 HTML 页面,播放器直接报错。
8. HLS 直播流与多清晰度切换的实战整合
到了这个章节,才算真正进入"进阶"领域。开发后台系统的同学可能很少接触到直播流,但做在线教育、展会直播、大屏监控这类项目时,HLS 播放是刚需。
8.1 用视频js-contrib-hls还是用hls.js
先澄清一个概念:Video.js 本身不支持 HLS 流媒体播放。要在 Video.js 里播 m3u8 格式,需要额外引入 HLS 支持。
Video.js 官方维护了一个@videojs/http-streaming(VHS)库,负责在 Video.js 8.x 中处理 HLS。好消息是,Video.js 8.x 已经默认集成了 VHS,所以如果你安装的是最新版 video.js,不需要额外配置就能播放 m3u8。
如果你用的是 video.js 7.x,则需要额外引入:
npm install @videojs/http-streamingimport '@videojs/http-streaming'不过实际项目中,很多团队采用的是 HLS 主流的策略是使用hls.js库,然后在 Video.js 中通过配置video.js-contrib-hls或者使用 VHS。这里我很直接地说:在 Vue3 项目里,直接用 video.js 8.x + VHS 最省事,因为不用额外引入 hls.js,也不用处理 hls.js 和 video.js 的事件通道。
下面是一个简单的 HLS 播放示例:
<template> <VideoPlayer :options="hlsOptions" /> </template> <script setup> import { VideoPlayer } from '@videojs-player/vue' const hlsOptions = { controls: true, language: 'zh-CN', sources: [ { src: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', type: 'application/x-mpegURL' } ] } </script>这就够了。video.js 8.x 会自动侦测 m3u8 地址,调用内置的 VHS 进行播放。
8.2 多清晰度切换:定义多个 source 并动态切换
在线教育里"标清/高清/超清"切换是非常常见的需求。实现思路并不复杂,就是在 options 里预置多个清晰度,供用户选择时调用player.src()切换。
const resolutions = { '标清': { src: 'https://example.com/video/720p.mp4', type: 'video/mp4' }, '高清': { src: 'https://example.com/video/1080p.mp4', type: 'video/mp4' }, '超清': { src: 'https://example.com/video/4k.mp4', type: 'video/mp4' } } const currentRes = ref('标清') const switchResolution = (quality) => { if (quality === currentRes.value) return const currentTime = player.currentTime() player.src(resolutions[quality]) player.ready(() => { player.currentTime(currentTime) player.play() }) }这里的一个经验是:切换清晰度时,先记录当前播放时间,换源后 restore 播放时间,避免让用户从头看。
如果视频是 HLS 的,Video.js 底层通常都有自适应码率机制,你也可以手动控制。在配置里加:
const hlsOptions = { html5: { vhs: { overrideNative: true, enableLowInitialPlaylist: true } } }8.3 直播场景:自动跟随直播流、无进度条
如果是直播流,建议把控制条上的进度条隐藏或禁用,否则用户拖动进度条没有意义。你可以在 CSS 里覆盖:
.video-js .vjs-progress-control { display: none; }另外,直播场景中ended事件要小心处理——直播流一般没有"播放结束",这个事件几乎不会触发,不要依赖它做业务逻辑。
9. 视觉定制:想让播放器融入你项目的 UI 风格
Video.js 的默认皮肤质感不错,但到了实际项目中,很少有人能直接接受深蓝色高亮的默认 UI。好在 Video.js 的皮肤是通过 CSS 变量 + 类名覆盖实现的,改起来比较直接。
9.1 用 CSS 变量调整全局配色
Video.js 8.x 使用了--vjs-*系列 CSS 变量来控制主要颜色。最常用的几个:
.video-js { /* 主色调 */ --vjs-theme-primary: #409eff; /* 控件聚焦时的颜色 */ --vjs-focus-outline-color: #409eff; /* 控制条背景 */ --vjs-control-bar-bg: rgba(0, 0, 0, 0.7); }把--vjs-theme-primary改成你们项目的品牌色,播放器高亮部分会跟着变。实测下来,进度条已播放部分、音量条、按钮聚焦颜色都会受影响。
9.2 覆盖组件内部的类名
有时候光改主色不够,还要改具体控件样式。可以用深一点的类名选择器:
.video-js .vjs-play-progress { background-color: #ff9900; } .video-js .vjs-volume-level { background-color: #ff9900; } .video-js .vjs-big-play-button { background-color: rgba(255, 153, 0, 0.8); border: none; border-radius: 50%; width: 64px; height: 64px; line-height: 64px; }这里的大播放按钮(big play button)是很多项目必改的——默认是一个圆角矩形,改成圆形会更符合现在的主流设计审美。
9.3 自定义控制条按钮:挂载你自己的图标
如果你需要在控制条上增加一个自定义按钮(比如"截图""弹幕开关"),可以用player.getChild('controlBar')添加。
const onPlayerReady = (playerInstance) => { const controlBar = playerInstance.getChild('controlBar') const screenshotButton = playerInstance.controlBar.addChild('button', { controlText: '截图', className: 'vjs-screenshot-button' }) screenshotButton.on('click', () => { // 截图逻辑 const canvas = document.createElement('canvas') // ... }) }这个 API 在 @videojs-player/vue 里同样可用,因为拿到的是原生 player 实例。不过这里确实绕过了 Vue 的响应式体系,按钮上的文字要用controlText设置。
9.4 加载动画与错误提示定制的坑
播放器在缓冲时会显示一个 loading 动画。默认是转圈,想改成文字提示"加载中...",可以直接在配置里设置:
const playerOptions = { loadingSpinner: false, textTrackSettings: { // ... } }但更简单的是在 DOM 层面做:用 CSS 隐藏默认 spinner,然后在容器上加自己的 loading 元素。
.video-js .vjs-loading-spinner { display: none; }可以给外层套一个 div,根据播放状态用 v-if 控制显示自己的 loading 组件:
<div class="player-wrapper"> <VideoPlayer :src="videoSrc" @waiting="loading = true" @playing="loading = false" /> <div v-if="loading" class="custom-loading">加载中...</div> </div>这个方法也是我在做在线教育后台时常用的方式,因为默认的转圈 loading 在深色背景上不明显,换成品牌色的 loading 组件体验更好。
10. 一些来自实践的额外建议
最后这一节,我不做总结了,就说几个我实际开发中总结出来的"顺手"技巧。
技巧一:统一封装一个"视频播放器"业务组件。
不要在十几个页面里直接毫无包装地使用 VideoPlayer。建议在项目内部再包一层BaseVideoPlayer.vue,把常用的 options、视频源、上报逻辑都集中起来。这样如果哪天 Video.js 升级导致 API 变化,你只需要改一个文件。
<!-- BaseVideoPlayer.vue --> <template> <VideoPlayer :src="source" :options="mergedOptions" v-bind="$attrs" @ready="emitReady" @timeupdate="onTimeUpdate" /> </template> <script setup> import { computed } from 'vue' import { VideoPlayer } from '@videojs-player/vue' const props = defineProps({ source: { type: String, required: true }, poster: String, // ... }) const mergedOptions = computed(() => ({ language: 'zh-CN', playbackRates: [0.5, 1, 1.5, 2], poster: props.poster, // 你的默认配置 })) </script>技巧二:封面图非常影响用户对播放器"是否加载成功"的第一印象。
很多后台在视频加载慢的时候,用户看到黑屏会一直点播放按钮。设置一个好看的poster封面图,哪怕视频没加载出来,用户也知道"这是一个待播放的视频"。封面图不要用视频第一帧,因为第一帧大概率是纯黑或模糊的。
技巧三:监控 error 事件并上报。
如果视频源是客户提供的(尤其是一些政务、教育客户),时不时会遇到转码问题、防盗链问题、域名白名单问题。在 error 事件里把错误信息收集起来上报,可以省去大量排查时间。
const onError = (payload) => { console.error('播放错误', payload) // 上报逻辑 // reportError({ type: 'video', code: payload?.code, message: payload?.message }) }技巧四:不要在 SSR / Nuxt 项目里直接引入该组件。
@videojs-player/vue 依赖 window / document 等浏览器 API,Nuxt 服务端渲染时会直接白屏或报错。如果要在 Nuxt 中用,需要把播放器组件挂在一个 client-only 的包装里,或者用dynamic import且关闭 ssr。这个限制不算独特,几乎所有的播放器库都一样,但确实值得提前知道。
技巧五:多视频页面注意实例销毁。
如果你的页面里用 v-for 渲染多个播放器,组件销毁时要确保每个 player 都执行了 dispose,否则会留下大量 DOM 事件监听,造成内存泄漏。@videojs-player/vue 在组件卸载时会自动调用player.dispose(),但你如果通过 ref 拿了一堆 player 实例,最好也手动做清理:
onBeforeUnmount(() => { player?.dispose() player = null })