import Hls from 'hls.js';
import Artplayer from 'artplayer';
import artplayerPluginDanmuku from 'artplayer-plugin-danmuku';
/**
* 视频播放器基类
*/
export abstract class BaseVideoPlayer {
protected _hasPlayed: boolean = false;
private _isUpdatingPlayCount: boolean = false;
/**
* 处理 m3u8 视频播放
*/
protected playM3u8(video: HTMLVideoElement, url: string, art: Artplayer): void {
// 初始化阶段不预加载视频内容,降低未点击播放时的 CDN 流量消耗。
video.preload = 'none';
// 懒加载场景下,尚未触发播放时主动隐藏 loading,避免一直展示等待状态。
const loadingHideTimer = window.setTimeout(() => {
art.loading.show = false;
}, 100);
let sourceLoaded = false;
let pendingUserPlay = false;
let playRequestPending = false;
const requestPlayback = (): void => {
if (!pendingUserPlay || playRequestPending) {
return;
}
playRequestPending = true;
void video
.play()
.then(() => {
pendingUserPlay = false;
})
.catch(() => {
// 源还没准备好或被浏览器策略拦截时,等待后续就绪事件再重试。
})
.finally(() => {
playRequestPending = false;
});
};
const retryPendingPlayback = (): void => {
if (!pendingUserPlay) {
return;
}
requestPlayback();
};
const clearPendingPlayback = (): void => {
pendingUserPlay = false;
};
video.addEventListener('play', clearPendingPlayback);
const loadSource = (): void => {
if (sourceLoaded) {
return;
}
sourceLoaded = true;
if (Hls.isSupported()) {
// 销毁旧的 hls 实例
if (art.hls) {
(art.hls as Hls)?.destroy();
}
const hls = new Hls();
hls.on(Hls.Events.MANIFEST_PARSED, retryPendingPlayback);
hls.loadSource(url);
hls.attachMedia(video);
art.hls = hls;
art.on('destroy', () => hls.destroy());
return;
}
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生支持
video.addEventListener('loadedmetadata', retryPendingPlayback, { once: true });
video.addEventListener('canplay', retryPendingPlayback, { once: true });
video.src = url;
video.load();
return;
}
art.notice.show = '不支持播放该视频';
};
const triggerLoadAndPlay = (): void => {
pendingUserPlay = true;
loadSource();
requestPlayback();
removeLazyLoadTriggers();
};
const onPointerDown = (): void => {
triggerLoadAndPlay();
};
const onKeyDown = (event: KeyboardEvent): void => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
triggerLoadAndPlay();
};
const playerElement = art.template.$player;
const removeLazyLoadTriggers = (): void => {
playerElement.removeEventListener('pointerdown', onPointerDown, true);
playerElement.removeEventListener('keydown', onKeyDown, true);
};
// 在首次用户交互时才真正挂载媒体源并开始播放。
playerElement.addEventListener('pointerdown', onPointerDown, true);
playerElement.addEventListener('keydown', onKeyDown, true);
art.on('destroy', () => {
window.clearTimeout(loadingHideTimer);
video.removeEventListener('play', clearPendingPlayback);
removeLazyLoadTriggers();
});
}
/**
* 创建弹幕插件配置
* @param videoId 视频 ID
* @param onDanmakuSent 弹幕发送成功后的回调(可选)
*/
protected createDanmakuPlugin(
videoId: string,
onDanmakuSent?: () => void
): ReturnType<typeof artplayerPluginDanmuku> {
return artplayerPluginDanmuku({
danmuku: async () => {
try {
const response = await fetch(`/history/danmaku/v2/${videoId}`);
return await response.json();
} catch (e) {
console.error('Failed to load danmaku', e);
return [];
}
},
async beforeEmit(danmaku) {
if (!danmaku.text || danmaku.text.trim() === '') {
return false;
}
(danmaku as unknown as Record<string, string>)['id'] = videoId;
try {
await fetch(`/send/danmaku/v2`, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: JSON.stringify(danmaku),
});
// 执行回调(如更新弹幕计数)
onDanmakuSent?.();
return true;
} catch (e) {
console.error('Failed to send danmaku', e);
return false;
}
},
});
}
/**
* 处理首次播放事件(更新播放计数)
* @param videoId 视频 ID
* @param onFirstPlay 首次播放计数更新成功后的回调(可选)
* @returns 播放事件处理器
*/
protected createFirstPlayHandler(
videoId: string,
onFirstPlay?: () => void
): () => Promise<void> {
return async () => {
if (this._hasPlayed || this._isUpdatingPlayCount) {
return;
}
this._isUpdatingPlayCount = true;
try {
const response = await fetch(`/play/${videoId}`, { method: 'PATCH' });
if (!response.ok) {
throw new Error(`Play count update failed with status ${response.status}`);
}
this._hasPlayed = true;
onFirstPlay?.();
} catch (e) {
console.error('Failed to update play count', e);
} finally {
this._isUpdatingPlayCount = false;
}
};
}
/**
* 获取通用的 Artplayer 配置选项(可被子类覆盖或扩展)
*/
protected getCommonArtplayerOptions(): Partial<Artplayer['option']> {
return {
type: 'm3u8',
customType: {
m3u8: this.playM3u8.bind(this),
},
// 避免浏览器在未点击播放时提前下载
moreVideoAttr: {
preload: 'none',
},
// 是否在移动端添加长按视频快进功能
fastForward: true,
// 是否在移动端的网页全屏时,根据视频尺寸和视口尺寸,旋转播放器
autoOrientation: true,
};
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个使用 TypeScript 编写的视频播放器基类,基于 Artplayer 播放器和 HLS.js 库,提供 m3u8 视频播放、弹幕功能和播放计数等核心能力。
核心功能模块
1. playM3u8 方法 - m3u8 视频播放核心逻辑
这是整个类的核心方法,实现了懒加载 + HLS 流媒体播放:
关键设计点:
流量优化:
video.preload = 'none'; // 不预加载视频在用户未主动播放前不加载视频内容,节省 CDN 流量。
UI 体验优化:
const loadingHideTimer = window.setTimeout(() => { art.loading.show = false; }, 100);在懒加载场景下,隐藏 loading 动画避免误导用户。
播放状态管理:
sourceLoaded: 视频源是否已加载pendingUserPlay: 用户是否触发了播放意图playRequestPending: 是否有播放请求正在处理
HLS 播放实现:
if (Hls.isSupported()) { const hls = new Hls(); hls.on(Hls.Events.MANIFEST_PARSED, retryPendingPlayback); hls.loadSource(url); hls.attachMedia(video); }使用 hls.js 在支持 MSE 的浏览器中播放 m3u8。
Safari 兼容:
if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url; video.load(); }Safari 原生支持 HLS,直接设置 src。
懒加载触发机制:
playerElement.addEventListener('pointerdown', onPointerDown, true); playerElement.addEventListener('keydown', onKeyDown, true);在用户首次点击或按键(Enter/空格)时才加载并播放视频。
2. createDanmakuPlugin 方法 - 弹幕功能
创建 Artplayer 弹幕插件配置:
return artplayerPluginDanmuku({
danmuku: async () => {
// 从服务器获取历史弹幕
const response = await fetch(`/history/danmaku/v2/${videoId}`);
return await response.json();
},
async beforeEmit(danmaku) {
// 发送弹幕到服务器
await fetch(`/send/danmaku/v2`, {
method: 'POST',
body: JSON.stringify(danmaku),
});
onDanmakuSent?.(); // 触发回调(如更新弹幕计数)
return true;
},
});
关键特性:
- 异步加载历史弹幕
- 发送新弹幕前校验(空内容拦截)
- 支持发送成功后的回调通知
3. createFirstPlayHandler 方法 - 播放计数统计
处理首次播放时的播放计数更新:
protected createFirstPlayHandler(videoId: string, onFirstPlay?: () => void) {
return async () => {
if (this._hasPlayed || this._isUpdatingPlayCount) return;
this._isUpdatingPlayCount = true;
await fetch(`/play/${videoId}`, { method: 'PATCH' });
this._hasPlayed = true;
onFirstPlay?.();
};
}
防抖机制:
_hasPlayed: 确保每个视频只统计一次_isUpdatingPlayCount: 防止并发请求重复统计
4. getCommonArtplayerOptions 方法 - 播放器通用配置
返回 Artplayer 的基础配置:
return {
type: 'm3u8',
customType: {
m3u8: this.playM3u8.bind(this), // 自定义 m3u8 播放逻辑
},
moreVideoAttr: {
preload: 'none', // 与懒加载策略一致
},
fastForward: true, // 移动端长按快进
autoOrientation: true, // 移动端自动旋转
};
类设计特点
抽象基类模式
export abstract class BaseVideoPlayer
使用 abstract 关键字定义基类,子类需实现具体的播放器初始化逻辑。
受保护的方法
所有核心方法都使用 protected 修饰符,允许子类访问和复用,但外部无法直接调用。
可选回调机制
onDanmakuSent?: () => void
onFirstPlay?: () => void
通过可选回调参数,允许子类在关键事件发生时执行自定义逻辑(如更新 UI 计数)。
技术亮点
- 流量优化:懒加载 + preload='none' 组合策略
- 跨平台兼容:HLS.js(现代浏览器)+ 原生支持(Safari)
- 健壮性设计:防抖、状态管理、错误处理
- 扩展性强:基类提供核心能力,子类按需扩展
- 资源清理:监听
destroy事件释放 HLS 实例和事件监听器
这个基类为构建完整的视频播放功能提供了坚实的基础架构。
AI 正在分析代码…
评论加载中...