refactor(player): 重构播放控制系统,移除 Howler.js 改用原生 HTMLAudioElement

- 新建 playbackController.ts,使用 generation-based 取消替代 playbackRequestManager 状态机
- audioService 重写:单一持久 HTMLAudioElement + Web Audio API,createMediaElementSource 只调一次
- playerCore 瘦身为纯状态管理,移除 handlePlayMusic/playAudio/checkPlaybackState
- playlist next/prev 简化,区分用户手动切歌和歌曲自然播完
- MusicHook 适配 HTMLAudioElement API(.currentTime/.duration/.paused)
- preloadService 从 Howl 实例缓存改为 URL 可用性验证
- 所有 view/component 调用者迁移到 playbackController.playTrack()

修复:快速切歌竞态、seek 到未缓冲位置失败、重启后自动播放循环提示、EQ 重建崩溃
This commit is contained in:
alger
2026-03-29 13:18:05 +08:00
parent 167f081ee6
commit 0cfec3dd82
17 changed files with 1150 additions and 1919 deletions
+20 -255
View File
@@ -1,208 +1,51 @@
/**
* 播放请求管理
* 负责管理播放请求的队列、取消、状态跟踪,防止竞态条件
* 薄请求 ID 追踪
* 用于 usePlayerHooks.ts 内部检查请求是否仍为最新。
* 实际的取消逻辑在 playbackController.ts 中(generation ID)。
*/
import type { SongResult } from '@/types/music';
/**
* 请求状态枚举
*/
export enum RequestStatus {
PENDING = 'pending',
ACTIVE = 'active',
COMPLETED = 'completed',
CANCELLED = 'cancelled',
FAILED = 'failed'
}
/**
* 播放请求接口
*/
export interface PlaybackRequest {
id: string;
song: SongResult;
status: RequestStatus;
timestamp: number;
abortController?: AbortController;
}
/**
* 播放请求管理器类
*/
class PlaybackRequestManager {
private currentRequestId: string | null = null;
private requestMap: Map<string, PlaybackRequest> = new Map();
private requestCounter = 0;
private counter = 0;
/**
* 生成唯一的请求ID
*/
private generateRequestId(): string {
return `playback_${Date.now()}_${++this.requestCounter}`;
}
/**
* 创建新的播放请求
* @param song 要播放的歌曲
* @returns 新请求的ID
* 创建新请求,使之前的请求失效
*/
createRequest(song: SongResult): string {
// 取消所有之前的请求
this.cancelAllRequests();
const requestId = this.generateRequestId();
const abortController = new AbortController();
const request: PlaybackRequest = {
id: requestId,
song,
status: RequestStatus.PENDING,
timestamp: Date.now(),
abortController
};
this.requestMap.set(requestId, request);
const requestId = `req_${Date.now()}_${++this.counter}`;
this.currentRequestId = requestId;
console.log(`[PlaybackRequestManager] 创建新请求: ${requestId}, 歌曲: ${song.name}`);
console.log(`[RequestManager] 新请求: ${requestId}, 歌曲: ${song.name}`);
return requestId;
}
/**
* 激活请求(标记为正在处理)
* @param requestId 请求ID
* 检查请求是否仍为当前请求
*/
activateRequest(requestId: string): boolean {
const request = this.requestMap.get(requestId);
if (!request) {
console.warn(`[PlaybackRequestManager] 请求不存在: ${requestId}`);
return false;
}
if (request.status === RequestStatus.CANCELLED) {
console.warn(`[PlaybackRequestManager] 请求已被取消: ${requestId}`);
return false;
}
request.status = RequestStatus.ACTIVE;
console.log(`[PlaybackRequestManager] 激活请求: ${requestId}`);
return true;
isRequestValid(requestId: string): boolean {
return this.currentRequestId === requestId;
}
/**
* 完成请求
* @param requestId 请求ID
* 激活请求(兼容旧调用,直接返回 isRequestValid 结果)
*/
activateRequest(requestId: string): boolean {
return this.isRequestValid(requestId);
}
/**
* 标记请求完成
*/
completeRequest(requestId: string): void {
const request = this.requestMap.get(requestId);
if (!request) {
return;
}
request.status = RequestStatus.COMPLETED;
console.log(`[PlaybackRequestManager] 完成请求: ${requestId}`);
// 清理旧请求(保留最近3个)
this.cleanupOldRequests();
console.log(`[RequestManager] 完成: ${requestId}`);
}
/**
* 标记请求失败
* @param requestId 请求ID
*/
failRequest(requestId: string): void {
const request = this.requestMap.get(requestId);
if (!request) {
return;
}
request.status = RequestStatus.FAILED;
console.log(`[PlaybackRequestManager] 请求失败: ${requestId}`);
}
/**
* 取消指定请求
* @param requestId 请求ID
*/
cancelRequest(requestId: string): void {
const request = this.requestMap.get(requestId);
if (!request) {
return;
}
if (request.status === RequestStatus.CANCELLED) {
return;
}
// 取消AbortController
if (request.abortController && !request.abortController.signal.aborted) {
request.abortController.abort();
}
request.status = RequestStatus.CANCELLED;
console.log(`[PlaybackRequestManager] 取消请求: ${requestId}, 歌曲: ${request.song.name}`);
// 如果是当前请求,清除当前请求ID
if (this.currentRequestId === requestId) {
this.currentRequestId = null;
}
}
/**
* 取消所有请求
*/
cancelAllRequests(): void {
console.log(`[PlaybackRequestManager] 取消所有请求,当前请求数: ${this.requestMap.size}`);
this.requestMap.forEach((request) => {
if (
request.status !== RequestStatus.COMPLETED &&
request.status !== RequestStatus.CANCELLED
) {
this.cancelRequest(request.id);
}
});
}
/**
* 检查请求是否仍然有效(是当前活动请求)
* @param requestId 请求ID
* @returns 是否有效
*/
isRequestValid(requestId: string): boolean {
// 检查是否是当前请求
if (this.currentRequestId !== requestId) {
console.warn(
`[PlaybackRequestManager] 请求已过期: ${requestId}, 当前请求: ${this.currentRequestId}`
);
return false;
}
const request = this.requestMap.get(requestId);
if (!request) {
console.warn(`[PlaybackRequestManager] 请求不存在: ${requestId}`);
return false;
}
// 检查请求状态
if (request.status === RequestStatus.CANCELLED) {
console.warn(`[PlaybackRequestManager] 请求已被取消: ${requestId}`);
return false;
}
return true;
}
/**
* 检查请求是否应该中止(用于 AbortController
* @param requestId 请求ID
* @returns AbortSignal 或 undefined
*/
getAbortSignal(requestId: string): AbortSignal | undefined {
const request = this.requestMap.get(requestId);
return request?.abortController?.signal;
console.log(`[RequestManager] 失败: ${requestId}`);
}
/**
@@ -211,84 +54,6 @@ class PlaybackRequestManager {
getCurrentRequestId(): string | null {
return this.currentRequestId;
}
/**
* 获取请求信息
* @param requestId 请求ID
*/
getRequest(requestId: string): PlaybackRequest | undefined {
return this.requestMap.get(requestId);
}
/**
* 清理旧请求(保留最近3个)
*/
private cleanupOldRequests(): void {
if (this.requestMap.size <= 3) {
return;
}
// 按时间戳排序,保留最新的3个
const sortedRequests = Array.from(this.requestMap.values()).sort(
(a, b) => b.timestamp - a.timestamp
);
const toKeep = new Set(sortedRequests.slice(0, 3).map((r) => r.id));
const toDelete: string[] = [];
this.requestMap.forEach((_, id) => {
if (!toKeep.has(id)) {
toDelete.push(id);
}
});
toDelete.forEach((id) => {
this.requestMap.delete(id);
});
if (toDelete.length > 0) {
console.log(`[PlaybackRequestManager] 清理了 ${toDelete.length} 个旧请求`);
}
}
/**
* 重置管理器(用于调试或特殊情况)
*/
reset(): void {
console.log('[PlaybackRequestManager] 重置管理器');
this.cancelAllRequests();
this.requestMap.clear();
this.currentRequestId = null;
this.requestCounter = 0;
}
/**
* 获取调试信息
*/
getDebugInfo(): {
currentRequestId: string | null;
totalRequests: number;
requestsByStatus: Record<string, number>;
} {
const requestsByStatus: Record<string, number> = {
[RequestStatus.PENDING]: 0,
[RequestStatus.ACTIVE]: 0,
[RequestStatus.COMPLETED]: 0,
[RequestStatus.CANCELLED]: 0,
[RequestStatus.FAILED]: 0
};
this.requestMap.forEach((request) => {
requestsByStatus[request.status]++;
});
return {
currentRequestId: this.currentRequestId,
totalRequests: this.requestMap.size,
requestsByStatus
};
}
}
// 导出单例实例
export const playbackRequestManager = new PlaybackRequestManager();