mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-23 15:47:23 +08:00
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user