录制文件播放:自定义视频播放器
好,我们进入第25章。前面我们搞定了录制和截图,现在轮到播放了。你可能会想:「播放视频而已,用浏览器自带的<video>标签不就行了?」嗯,理论上可以,但实际项目中你会发现——默认播放器丑、功能少、没法定制。尤其我们要叠加字幕、控制倍速,原生控件根本不够用。
所以这一章,我带你手写一个自定义播放器。说白了,就是把播放/暂停、进度条、倍速、字幕这些功能,全部自己控制。听起来复杂?其实核心就几个API。
播放器整体架构
先看一张图,理清播放器的模块关系:
你看,所有控制模块都通过事件总线与视频渲染层通信。这样做的好处是——每个模块可以独立开发、独立测试。我在项目中就吃过亏,一开始把所有逻辑写在一个大函数里,后来改倍速功能时差点把进度条搞崩。所以,模块化是必须的。
核心实现:播放/暂停
先来最简单的。播放和暂停,本质上就是调用 video.play() 和 video.pause()。但我们要做的是——用一个按钮切换两种状态。
// 播放器核心状态
const player = {
video: document.getElementById('myVideo'),
isPlaying: false,
playBtn: document.getElementById('playBtn'),
togglePlay() {
if (this.isPlaying) {
this.video.pause();
this.playBtn.textContent = '▶';
} else {
this.video.play();
this.playBtn.textContent = '⏸';
}
this.isPlaying = !this.isPlaying;
},
init() {
this.playBtn.addEventListener('click', () => this.togglePlay());
// 处理视频播放结束
this.video.addEventListener('ended', () => {
this.isPlaying = false;
this.playBtn.textContent = '▶';
});
}
};
player.init();
video.paused 属性来判断状态,而不是自己维护一个 isPlaying 变量。但如果你要扩展功能(比如做播放列表),自己维护状态会更灵活。
进度条控制
进度条是播放器里最容易出bug的地方。为什么?因为有两个方向的数据流:
- 视频播放时:进度条要跟着走(
timeupdate事件) - 用户拖拽进度条时:视频要跳到对应位置(
input事件)
这两者如果处理不好,就会「打架」。我曾经遇到过一个bug:用户拖拽进度条时,timeupdate 事件又把进度条拉回去了,导致进度条来回抖动。解决方案很简单——加一个锁:
let isSeeking = false;
progressBar.addEventListener('input', (e) => {
isSeeking = true;
const percent = e.target.value;
const time = (percent / 100) * video.duration;
video.currentTime = time;
});
video.addEventListener('timeupdate', () => {
if (!isSeeking) {
const percent = (video.currentTime / video.duration) * 100;
progressBar.value = percent;
}
});
progressBar.addEventListener('change', () => {
isSeeking = false;
});
input 事件在拖拽过程中会频繁触发,而 change 事件只在松开鼠标时触发。所以用 change 来解锁是最合适的。
倍速播放
倍速播放其实就一行代码:video.playbackRate = 2。但实际项目中,我们需要提供一个选择器,让用户选 0.5x、1x、1.5x、2x 等。
我建议用 <select> 或者一组按钮来实现。这里我展示按钮方案:
const speeds = [0.5, 1, 1.5, 2];
let currentSpeed = 1;
speedBtns.forEach(btn => {
btn.addEventListener('click', () => {
const rate = parseFloat(btn.dataset.speed);
video.playbackRate = rate;
currentSpeed = rate;
// 高亮当前选中的按钮
speedBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
避坑指南: 我曾经遇到过一个问题——倍速播放时音频会变调。后来发现是浏览器默认的音频处理方式导致的。解决方案是:在 <video> 标签上加 preservesPitch="true" 属性。这样即使 2 倍速,声音也不会变成「小黄人」。
字幕叠加
字幕叠加有两种方式:
- WebVTT 字幕轨道:原生支持,但样式定制有限
- 自定义叠加层:完全控制样式,但需要自己解析字幕文件
我个人更推荐第二种,尤其是你要做直播录制回放时。因为直播的字幕往往是实时生成的,没法提前做成 VTT 文件。
下面是一个简单的字幕叠加实现:
// 假设字幕数据格式:[{start: 0, end: 5, text: '你好'}, ...]
const subtitles = [
{ start: 0, end: 3, text: '欢迎收看 WebRTC 实战课程' },
{ start: 3, end: 6, text: '今天我们讲自定义播放器' },
{ start: 6, end: 10, text: '字幕叠加其实很简单' }
];
const subtitleEl = document.getElementById('subtitle');
video.addEventListener('timeupdate', () => {
const currentTime = video.currentTime;
const activeSub = subtitles.find(
sub => currentTime >= sub.start && currentTime <= sub.end
);
if (activeSub) {
subtitleEl.textContent = activeSub.text;
subtitleEl.style.display = 'block';
} else {
subtitleEl.style.display = 'none';
}
});
find 遍历会卡顿。我建议用二分查找,或者维护一个「当前字幕索引」变量,每次只检查前后几条。
完整播放器代码结构
最后,我把所有模块整合成一个类。这样代码更清晰:
class CustomPlayer {
constructor(videoEl) {
this.video = videoEl;
this.subtitles = [];
this.initControls();
this.initEvents();
}
initControls() {
// 初始化所有控制按钮
this.playBtn = document.querySelector('.play-btn');
this.progressBar = document.querySelector('.progress-bar');
this.speedBtns = document.querySelectorAll('.speed-btn');
this.subtitleEl = document.querySelector('.subtitle-overlay');
}
initEvents() {
// 绑定所有事件
this.playBtn.addEventListener('click', () => this.togglePlay());
this.video.addEventListener('timeupdate', () => this.updateProgress());
this.video.addEventListener('timeupdate', () => this.updateSubtitle());
// ... 其他事件
}
togglePlay() { /* 前面实现 */ }
updateProgress() { /* 前面实现 */ }
updateSubtitle() { /* 前面实现 */ }
setSpeed(rate) { /* 前面实现 */ }
}
const player = new CustomPlayer(document.getElementById('myVideo'));
嗯,到这里,一个完整的自定义播放器就搭好了。你可能会问:「为什么不用现成的库,比如 Video.js?」我的回答是:如果你只需要基础功能,用库没问题。但如果你要做 WebRTC 录制回放、叠加实时字幕、对接自己的信令系统,自己写一个反而更可控。
我在做某个在线教育项目时,就遇到过 Video.js 和 WebRTC 的兼容性问题——播放器内部的事件循环和 WebRTC 的 MediaStream 冲突了。最后花了三天才定位到问题。从那以后,凡是涉及 WebRTC 的播放器,我都自己写。
这一章的内容就到这里。代码都在上面了,你可以直接复制到项目里试试。有问题的话,欢迎在评论区交流。