一、前言
在使用 video.js 开发视频播放功能时,我们经常需要根据不同网络环境或视频源,提供"多清晰度切换"功能(例如:720P、1080P、4K 等)。
虽然 video.js 提供了插件机制,但它本身没有自带清晰度切换逻辑,因此我们可以 通过继承组件的方式自定义一个清晰度下拉按钮,实现动态切换视频源的能力。
本文将结合完整源码,讲解实现思路与关键细节。
二、核心思路
整个功能的实现分为三个主要部分:
-
初始化播放器 :创建
video.js实例并配置视频源; -
自定义组件
QualityDropdown:继承 video.js 的Button,实现一个带下拉菜单的按钮; -
事件与逻辑控制:处理点击、菜单显示/隐藏、视频源切换等逻辑。
三、源码
const initVideo = () => {
const url = result.value.videoUrl;
// 初始化播放器
player.value = videojs(
videoPlayer.value,
{
controls: true,
autoplay: false,
preload: 'auto',
fluid: false,
responsive: true,
width: '100%',
height: '100%',
sources: [{ src: url, type: getVideoType(url) }],
},
() => {
const videoPlayerInstance = player.value;
// ---------- 添加清晰度切换按钮 ----------
const Button = videojs.getComponent('Button');
class QualityDropdown extends Button {
constructor(player, options) {
super(player, options);
this.controlText('清晰度');
this.sources = options.sources || [];
this.current = 0;
this.el().classList.add('vjs-quality-button');
this.el().innerText = this.sources[this.current]?.label || '清晰度';
// 创建下拉菜单
this.menu = document.createElement('div');
this.menu.className = 'vjs-quality-menu';
this.menu.style.display = 'none';
this.menu.style.position = 'absolute';
this.menu.style.background = 'rgba(0,0,0,0.8)';
this.menu.style.color = '#fff';
this.menu.style.padding = '5px 0';
this.menu.style.borderRadius = '4px';
this.menu.style.top = '-70px';
this.menu.style.left = '0';
this.menu.style.zIndex = '1000';
this.el().appendChild(this.menu);
this.sources.forEach((source, index) => {
const item = document.createElement('div');
item.innerText = source.label;
item.style.padding = '5px 10px';
item.style.cursor = 'pointer';
item.onmouseenter = () => (item.style.background = 'rgba(255,255,255,0.2)');
item.onmouseleave = () => (item.style.background = 'transparent');
item.onclick = () => {
this.current = index;
this.player().src(source);
this.player().play();
this.el().firstChild.nodeValue = source.label;
this.menu.style.display = 'none';
};
this.menu.appendChild(item);
});
// 点击按钮显示/隐藏菜单
this.el().onclick = (e) => {
e.stopPropagation();
this.menu.style.display = this.menu.style.display === 'none' ? 'block' : 'none';
};
// 点击页面其他地方隐藏菜单
document.addEventListener('click', () => {
this.menu.style.display = 'none';
});
}
}
// ---------- 添加清晰度切换按钮 ----------
videojs.registerComponent('QualityDropdown', QualityDropdown);
const qualityBtn = videoPlayerInstance.addChild('QualityDropdown', { sources: result.value.videoSources });
// 插入到控制条末尾(进度条后面)
const controlBarChildren = videoPlayerInstance.controlBar.children();
videoPlayerInstance.controlBar.addChild(qualityBtn, {}, controlBarChildren.length - 1);
// ---------- 原有事件监听 ----------
videoPlayerInstance.on('timeupdate', () => {
const currentTimeInSeconds = Math.floor(videoPlayerInstance.currentTime());
updatePositionByTime(currentTimeInSeconds);
console.log(`当前时间: ${videoPlayerInstance.currentTime().toFixed(2)}秒 / 总时长: ${videoPlayerInstance.duration().toFixed(2)}秒`);
});
videoPlayerInstance.on('pause', () => console.log('视频已暂停'));
videoPlayerInstance.on('seeking', () => console.log('正在跳转到新位置...'));
videoPlayerInstance.on('seeked', () => console.log('跳转完成,当前时间:', videoPlayerInstance.currentTime()));
videoPlayerInstance.on('loadedmetadata', () => {
console.log('视频元数据加载完成');
videoPlayerInstance.dimensions(videoPlayerInstance.width(), videoPlayerInstance.height());
});
window.addEventListener('resize', () => {
videoPlayerInstance.dimensions(videoPlayerInstance.width(), videoPlayerInstance.height());
});
}
);
};
四、关键实现解析
1️⃣ 继承 video.js 组件系统
video.js 的组件化体系允许我们继承基础组件(如 Button),从而快速扩展功能。
const Button = videojs.getComponent('Button');
class QualityDropdown extends Button { ... }
videojs.registerComponent('QualityDropdown', QualityDropdown);
这段代码的核心在于:
-
保留原有按钮行为;
-
增加自定义下拉菜单;
-
在
controlBar中动态注册。
2️⃣ 动态生成菜单项
我们通过遍历 sources 数组,为每种清晰度创建一个 <div> 选项:
*
this.sources.forEach((source, index) => {
const item = document.createElement('div');
item.innerText = source.label;
...
});
切换清晰度的核心逻辑
点击选项后,调用:
*
this.player().src(source);
this.player().play();
重新设置视频源,并立即播放。
由于 video.js 会自动重新加载元数据,这种切换方式简单直接,兼容性很好。