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
@@ -409,6 +409,7 @@ import {
} from '@/hooks/MusicHook';
import { useArtist } from '@/hooks/useArtist';
import { usePlayMode } from '@/hooks/usePlayMode';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';
import { getImgUrl, secondToMinute } from '@/utils';
@@ -757,7 +758,7 @@ const handleProgressBarClick = (e: MouseEvent) => {
console.log(`进度条点击: ${percentage.toFixed(2)}, 新时间: ${newTime.toFixed(2)}`);
sound.value.seek(newTime);
audioService.seek(newTime);
nowTime.value = newTime;
};
@@ -817,7 +818,7 @@ const handleMouseUp = (e: MouseEvent) => {
e.preventDefault();
// 释放时跳转到指定位置
sound.value.seek(nowTime.value);
audioService.seek(nowTime.value);
console.log(`鼠标释放,跳转到: ${nowTime.value.toFixed(2)}`);
isMouseDragging.value = false;
@@ -871,7 +872,7 @@ const handleThumbTouchEnd = (e: TouchEvent) => {
// 拖动结束时执行seek操作
console.log(`拖动结束,跳转到: ${nowTime.value.toFixed(2)}`);
sound.value.seek(nowTime.value);
audioService.seek(nowTime.value);
isThumbDragging.value = false;
};
@@ -100,9 +100,9 @@ import { useI18n } from 'vue-i18n';
import { CacheManager } from '@/api/musicParser';
import { playMusic } from '@/hooks/MusicHook';
import { initLxMusicRunner, setLxMusicRunner } from '@/services/LxMusicSourceRunner';
import { reparseCurrentSong } from '@/services/playbackController';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import { useSettingsStore } from '@/store';
import { usePlayerStore } from '@/store/modules/player';
import type { LxMusicScriptConfig } from '@/types/lxMusic';
import type { Platform } from '@/types/music';
import { type MusicSourceGroup, useMusicSources } from '@/utils/musicSourceConfig';
@@ -119,7 +119,6 @@ type ReparseSourceItem = {
lxScriptId?: string;
};
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const { t } = useI18n();
const message = useMessage();
@@ -253,7 +252,7 @@ const reparseWithLxScript = async (source: ReparseSourceItem) => {
selectedSourceId.value = source.id;
SongSourceConfigManager.setConfig(songId, ['lxMusic'], 'manual');
const success = await playerStore.reparseCurrentSong('lxMusic', false);
const success = await reparseCurrentSong('lxMusic', false);
if (success) {
message.success(t('player.reparse.success'));
@@ -283,7 +282,7 @@ const directReparseMusic = async (source: ReparseSourceItem) => {
selectedSourceId.value = source.id;
SongSourceConfigManager.setConfig(songId, [source.platform], 'manual');
const success = await playerStore.reparseCurrentSong(source.platform, false);
const success = await reparseCurrentSong(source.platform, false);
if (success) {
message.success(t('player.reparse.success'));