mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-19 03:57:28 +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:
@@ -28,11 +28,9 @@ export const useIntelligenceModeStore = defineStore('intelligenceMode', () => {
|
||||
*/
|
||||
const playIntelligenceMode = async () => {
|
||||
const { useUserStore } = await import('./user');
|
||||
const { usePlayerCoreStore } = await import('./playerCore');
|
||||
const { usePlaylistStore } = await import('./playlist');
|
||||
|
||||
const userStore = useUserStore();
|
||||
const playerCore = usePlayerCoreStore();
|
||||
const playlistStore = usePlaylistStore();
|
||||
const { t } = i18n.global;
|
||||
|
||||
@@ -101,7 +99,8 @@ export const useIntelligenceModeStore = defineStore('intelligenceMode', () => {
|
||||
|
||||
// 替换播放列表并开始播放
|
||||
playlistStore.setPlayList(intelligenceSongs, false, true);
|
||||
await playerCore.handlePlayMusic(intelligenceSongs[0], true);
|
||||
const { playTrack } = await import('@/services/playbackController');
|
||||
await playTrack(intelligenceSongs[0], true);
|
||||
} else {
|
||||
message.error(t('player.playBar.intelligenceMode.failed'));
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
const playHistoryStore = usePlayHistoryStore();
|
||||
playHistoryStore.migrateFromLocalStorage();
|
||||
|
||||
await playerCore.initializePlayState();
|
||||
const { initializePlayState: initPlayState } = await import('@/services/playbackController');
|
||||
await initPlayState();
|
||||
await playlist.initializePlaylist();
|
||||
};
|
||||
|
||||
@@ -112,11 +113,7 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
getVolume: playerCore.getVolume,
|
||||
increaseVolume: playerCore.increaseVolume,
|
||||
decreaseVolume: playerCore.decreaseVolume,
|
||||
handlePlayMusic: playerCore.handlePlayMusic,
|
||||
playAudio: playerCore.playAudio,
|
||||
handlePause: playerCore.handlePause,
|
||||
checkPlaybackState: playerCore.checkPlaybackState,
|
||||
reparseCurrentSong: playerCore.reparseCurrentSong,
|
||||
|
||||
// ========== 播放列表管理 (Playlist) ==========
|
||||
playList,
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import i18n from '@/../i18n/renderer';
|
||||
import { getParsingMusicUrl } from '@/api/music';
|
||||
import { useLyrics, useSongDetail } from '@/hooks/usePlayerHooks';
|
||||
import { audioService } from '@/services/audioService';
|
||||
import { playbackRequestManager } from '@/services/playbackRequestManager';
|
||||
import { preloadService } from '@/services/preloadService';
|
||||
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
|
||||
import type { AudioOutputDevice } from '@/types/audio';
|
||||
import type { Platform, SongResult } from '@/types/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
import { getImageLinearBackground } from '@/utils/linearColor';
|
||||
|
||||
import { usePlayHistoryStore } from './playHistory';
|
||||
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
import type { SongResult } from '@/types/music';
|
||||
|
||||
/**
|
||||
* 核心播放控制 Store
|
||||
@@ -43,10 +29,6 @@ export const usePlayerCoreStore = defineStore(
|
||||
);
|
||||
const availableAudioDevices = ref<AudioOutputDevice[]>([]);
|
||||
|
||||
let checkPlayTime: NodeJS.Timeout | null = null;
|
||||
let checkPlaybackRetryCount = 0;
|
||||
const MAX_CHECKPLAYBACK_RETRIES = 3;
|
||||
|
||||
// ==================== Computed ====================
|
||||
const currentSong = computed(() => playMusic.value);
|
||||
const isPlaying = computed(() => isPlay.value);
|
||||
@@ -109,413 +91,6 @@ export const usePlayerCoreStore = defineStore(
|
||||
return newVolume;
|
||||
};
|
||||
|
||||
/**
|
||||
* 播放状态检测
|
||||
* 在播放开始后延迟检查音频是否真正在播放,防止无声播放
|
||||
*/
|
||||
const checkPlaybackState = (song: SongResult, requestId?: string, timeout: number = 6000) => {
|
||||
if (checkPlayTime) {
|
||||
clearTimeout(checkPlayTime);
|
||||
}
|
||||
const sound = audioService.getCurrentSound();
|
||||
if (!sound) return;
|
||||
|
||||
// 如果没有提供 requestId,创建一个临时标识
|
||||
const actualRequestId = requestId || `check_${Date.now()}`;
|
||||
|
||||
const onPlayHandler = () => {
|
||||
console.log(`[${actualRequestId}] 播放事件触发,歌曲成功开始播放`);
|
||||
audioService.off('play', onPlayHandler);
|
||||
audioService.off('playerror', onPlayErrorHandler);
|
||||
checkPlaybackRetryCount = 0; // 播放成功,重置重试计数
|
||||
if (checkPlayTime) {
|
||||
clearTimeout(checkPlayTime);
|
||||
checkPlayTime = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onPlayErrorHandler = async () => {
|
||||
console.log('播放错误事件触发,检查是否需要重新获取URL');
|
||||
audioService.off('play', onPlayHandler);
|
||||
audioService.off('playerror', onPlayErrorHandler);
|
||||
|
||||
// 如果有 requestId,验证其有效性
|
||||
if (requestId && !playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log('请求已过期,跳过重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查重试次数限制
|
||||
if (checkPlaybackRetryCount >= MAX_CHECKPLAYBACK_RETRIES) {
|
||||
console.warn(`播放重试已达上限 (${MAX_CHECKPLAYBACK_RETRIES} 次),停止重试`);
|
||||
checkPlaybackRetryCount = 0;
|
||||
setPlayMusic(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userPlayIntent.value && play.value) {
|
||||
checkPlaybackRetryCount++;
|
||||
console.log(
|
||||
`播放失败,尝试刷新URL并重新播放 (重试 ${checkPlaybackRetryCount}/${MAX_CHECKPLAYBACK_RETRIES})`
|
||||
);
|
||||
// 本地音乐不需要刷新 URL
|
||||
if (!playMusic.value.playMusicUrl?.startsWith('local://')) {
|
||||
playMusic.value.playMusicUrl = undefined;
|
||||
}
|
||||
const refreshedSong = { ...song, isFirstPlay: true };
|
||||
await handlePlayMusic(refreshedSong, true);
|
||||
}
|
||||
};
|
||||
|
||||
audioService.on('play', onPlayHandler);
|
||||
audioService.on('playerror', onPlayErrorHandler);
|
||||
|
||||
checkPlayTime = setTimeout(() => {
|
||||
// 如果有 requestId,验证其有效性
|
||||
if (requestId && !playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log('请求已过期,跳过超时重试');
|
||||
audioService.off('play', onPlayHandler);
|
||||
audioService.off('playerror', onPlayErrorHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
// 双重确认:Howler 报告未播放 + 用户仍想播放
|
||||
// 额外检查底层 HTMLAudioElement 的状态,避免 EQ 重建期间的误判
|
||||
const currentSound = audioService.getCurrentSound();
|
||||
let htmlPlaying = false;
|
||||
if (currentSound) {
|
||||
try {
|
||||
const sounds = (currentSound as any)._sounds as any[];
|
||||
if (sounds?.[0]?._node instanceof HTMLMediaElement) {
|
||||
const node = sounds[0]._node as HTMLMediaElement;
|
||||
htmlPlaying = !node.paused && !node.ended && node.readyState > 2;
|
||||
}
|
||||
} catch {
|
||||
// 静默忽略
|
||||
}
|
||||
}
|
||||
|
||||
if (htmlPlaying) {
|
||||
// 底层 HTMLAudioElement 实际在播放,不需要重试
|
||||
console.log('底层音频元素正在播放,跳过超时重试');
|
||||
audioService.off('play', onPlayHandler);
|
||||
audioService.off('playerror', onPlayErrorHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!audioService.isActuallyPlaying() && userPlayIntent.value && play.value) {
|
||||
audioService.off('play', onPlayHandler);
|
||||
audioService.off('playerror', onPlayErrorHandler);
|
||||
|
||||
// 检查重试次数限制
|
||||
if (checkPlaybackRetryCount >= MAX_CHECKPLAYBACK_RETRIES) {
|
||||
console.warn(`超时重试已达上限 (${MAX_CHECKPLAYBACK_RETRIES} 次),停止重试`);
|
||||
checkPlaybackRetryCount = 0;
|
||||
setPlayMusic(false);
|
||||
return;
|
||||
}
|
||||
|
||||
checkPlaybackRetryCount++;
|
||||
console.log(
|
||||
`${timeout}ms后歌曲未真正播放,尝试重新获取URL (重试 ${checkPlaybackRetryCount}/${MAX_CHECKPLAYBACK_RETRIES})`
|
||||
);
|
||||
|
||||
// 本地音乐不需要刷新 URL
|
||||
if (!playMusic.value.playMusicUrl?.startsWith('local://')) {
|
||||
playMusic.value.playMusicUrl = undefined;
|
||||
}
|
||||
(async () => {
|
||||
const refreshedSong = { ...song, isFirstPlay: true };
|
||||
await handlePlayMusic(refreshedSong, true);
|
||||
})();
|
||||
} else {
|
||||
audioService.off('play', onPlayHandler);
|
||||
audioService.off('playerror', onPlayErrorHandler);
|
||||
}
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
/**
|
||||
* 核心播放处理函数
|
||||
*/
|
||||
const handlePlayMusic = async (music: SongResult, shouldPlay: boolean = true) => {
|
||||
// 如果是新歌曲,重置已尝试的音源和重试计数
|
||||
if (music.id !== playMusic.value.id) {
|
||||
SongSourceConfigManager.clearTriedSources(music.id);
|
||||
checkPlaybackRetryCount = 0;
|
||||
}
|
||||
|
||||
// 创建新的播放请求并取消之前的所有请求
|
||||
const requestId = playbackRequestManager.createRequest(music);
|
||||
console.log(`[handlePlayMusic] 开始处理歌曲: ${music.name}, 请求ID: ${requestId}`);
|
||||
|
||||
const currentSound = audioService.getCurrentSound();
|
||||
if (currentSound) {
|
||||
console.log('主动停止并卸载当前音频实例');
|
||||
currentSound.stop();
|
||||
currentSound.unload();
|
||||
}
|
||||
|
||||
// 验证请求是否仍然有效
|
||||
if (!playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log(`[handlePlayMusic] 请求已失效: ${requestId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 激活请求
|
||||
if (!playbackRequestManager.activateRequest(requestId)) {
|
||||
console.log(`[handlePlayMusic] 无法激活请求: ${requestId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const originalMusic = { ...music };
|
||||
|
||||
const { loadLrc } = useLyrics();
|
||||
const { getSongDetail } = useSongDetail();
|
||||
|
||||
// 并行加载歌词和背景色
|
||||
const [lyrics, { backgroundColor, primaryColor }] = await Promise.all([
|
||||
(async () => {
|
||||
if (music.lyric && music.lyric.lrcTimeArray.length > 0) {
|
||||
return music.lyric;
|
||||
}
|
||||
return await loadLrc(music.id);
|
||||
})(),
|
||||
(async () => {
|
||||
if (music.backgroundColor && music.primaryColor) {
|
||||
return { backgroundColor: music.backgroundColor, primaryColor: music.primaryColor };
|
||||
}
|
||||
return await getImageLinearBackground(getImgUrl(music?.picUrl, '30y30'));
|
||||
})()
|
||||
]);
|
||||
|
||||
// 在更新状态前再次验证请求
|
||||
if (!playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log(`[handlePlayMusic] 加载歌词/背景色后请求已失效: ${requestId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置歌词和背景色
|
||||
music.lyric = lyrics;
|
||||
music.backgroundColor = backgroundColor;
|
||||
music.primaryColor = primaryColor;
|
||||
music.playLoading = true;
|
||||
|
||||
// 更新 playMusic 和播放状态
|
||||
playMusic.value = music;
|
||||
play.value = shouldPlay;
|
||||
isPlay.value = shouldPlay;
|
||||
userPlayIntent.value = shouldPlay;
|
||||
|
||||
// 更新标题
|
||||
let title = music.name;
|
||||
if (music.source === 'netease' && music?.song?.artists) {
|
||||
title += ` - ${music.song.artists.reduce(
|
||||
(prev: string, curr: any) => `${prev}${curr.name}/`,
|
||||
''
|
||||
)}`;
|
||||
}
|
||||
document.title = 'AlgerMusic - ' + title;
|
||||
|
||||
try {
|
||||
// 添加到历史记录
|
||||
const playHistoryStore = usePlayHistoryStore();
|
||||
if (music.isPodcast) {
|
||||
if (music.program) {
|
||||
playHistoryStore.addPodcast(music.program);
|
||||
}
|
||||
} else {
|
||||
playHistoryStore.addMusic(music);
|
||||
}
|
||||
|
||||
// 获取歌曲详情
|
||||
const updatedPlayMusic = await getSongDetail(originalMusic, requestId);
|
||||
|
||||
// 在获取详情后再次验证请求
|
||||
if (!playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log(`[handlePlayMusic] 获取歌曲详情后请求已失效: ${requestId}`);
|
||||
playbackRequestManager.failRequest(requestId);
|
||||
return false;
|
||||
}
|
||||
|
||||
updatedPlayMusic.lyric = lyrics;
|
||||
|
||||
playMusic.value = updatedPlayMusic;
|
||||
playMusicUrl.value = updatedPlayMusic.playMusicUrl as string;
|
||||
music.playMusicUrl = updatedPlayMusic.playMusicUrl as string;
|
||||
|
||||
// 在拆分后补充:触发预加载下一首/下下首(与 playlist store 保持一致)
|
||||
try {
|
||||
const { usePlaylistStore } = await import('./playlist');
|
||||
const playlistStore = usePlaylistStore();
|
||||
// 基于当前歌曲在播放列表中的位置来预加载
|
||||
const list = playlistStore.playList;
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
const idx = list.findIndex(
|
||||
(item: SongResult) =>
|
||||
item.id === updatedPlayMusic.id && item.source === updatedPlayMusic.source
|
||||
);
|
||||
if (idx !== -1) {
|
||||
setTimeout(() => {
|
||||
playlistStore.preloadNextSongs(idx);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('预加载触发失败(可能是依赖未加载或循环依赖),已忽略:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await playAudio(requestId);
|
||||
|
||||
if (result) {
|
||||
// 播放成功,清除 isFirstPlay 标记,避免暂停时被误判为新歌
|
||||
playMusic.value.isFirstPlay = false;
|
||||
playbackRequestManager.completeRequest(requestId);
|
||||
return true;
|
||||
} else {
|
||||
playbackRequestManager.failRequest(requestId);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动播放音频失败:', error);
|
||||
playbackRequestManager.failRequest(requestId);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理播放音乐失败:', error);
|
||||
message.error(i18n.global.t('player.playFailed'));
|
||||
if (playMusic.value) {
|
||||
playMusic.value.playLoading = false;
|
||||
}
|
||||
playbackRequestManager.failRequest(requestId);
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 播放音频
|
||||
*/
|
||||
const playAudio = async (requestId?: string) => {
|
||||
if (!playMusicUrl.value || !playMusic.value) return null;
|
||||
|
||||
// 如果提供了 requestId,验证请求是否仍然有效
|
||||
if (requestId && !playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log(`[playAudio] 请求已失效: ${requestId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const shouldPlay = play.value;
|
||||
console.log('播放音频,当前播放状态:', shouldPlay ? '播放' : '暂停');
|
||||
|
||||
// 检查保存的进度
|
||||
let initialPosition = 0;
|
||||
const savedProgress = JSON.parse(localStorage.getItem('playProgress') || '{}');
|
||||
console.log(
|
||||
'[playAudio] 读取保存的进度:',
|
||||
savedProgress,
|
||||
'当前歌曲ID:',
|
||||
playMusic.value.id
|
||||
);
|
||||
if (savedProgress.songId === playMusic.value.id) {
|
||||
initialPosition = savedProgress.progress;
|
||||
console.log('[playAudio] 恢复播放进度:', initialPosition);
|
||||
}
|
||||
|
||||
// 使用 PreloadService 获取音频
|
||||
// 优先使用已预加载的 sound(通过 consume 获取并从缓存中移除)
|
||||
// 如果没有预加载,则进行加载
|
||||
let sound: Howl;
|
||||
try {
|
||||
// 先尝试消耗预加载的 sound
|
||||
const preloadedSound = preloadService.consume(playMusic.value.id);
|
||||
if (preloadedSound && preloadedSound.state() === 'loaded') {
|
||||
console.log(`[playAudio] 使用预加载的音频: ${playMusic.value.name}`);
|
||||
sound = preloadedSound;
|
||||
} else {
|
||||
// 没有预加载或预加载状态不正常,需要加载
|
||||
console.log(`[playAudio] 没有预加载,开始加载: ${playMusic.value.name}`);
|
||||
sound = await preloadService.load(playMusic.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('PreloadService 加载失败:', error);
|
||||
// 如果 PreloadService 失败,尝试直接播放作为回退
|
||||
// 但通常 PreloadService 失败意味着 URL 问题
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 播放新音频,传入已加载的 sound 实例
|
||||
const newSound = await audioService.play(
|
||||
playMusicUrl.value,
|
||||
playMusic.value,
|
||||
shouldPlay,
|
||||
initialPosition || 0,
|
||||
sound
|
||||
);
|
||||
|
||||
// 播放后再次验证请求
|
||||
if (requestId && !playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log(`[playAudio] 播放后请求已失效: ${requestId}`);
|
||||
newSound.stop();
|
||||
newSound.unload();
|
||||
return null;
|
||||
}
|
||||
|
||||
// 添加播放状态检测
|
||||
if (shouldPlay && requestId) {
|
||||
checkPlaybackState(playMusic.value, requestId);
|
||||
}
|
||||
|
||||
// 发布音频就绪事件
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } })
|
||||
);
|
||||
|
||||
// 时长检查已在 preloadService.ts 中完成
|
||||
|
||||
return newSound;
|
||||
} catch (error) {
|
||||
console.error('播放音频失败:', error);
|
||||
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// 操作锁错误不应该停止播放状态,只需要重试
|
||||
if (errorMsg.includes('操作锁激活')) {
|
||||
console.log('由于操作锁正在使用,将在1000ms后重试');
|
||||
|
||||
try {
|
||||
audioService.forceResetOperationLock();
|
||||
console.log('已强制重置操作锁');
|
||||
} catch (e) {
|
||||
console.error('重置操作锁失败:', e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// 验证请求是否仍然有效再重试
|
||||
if (requestId && !playbackRequestManager.isRequestValid(requestId)) {
|
||||
console.log('重试时请求已失效,跳过重试');
|
||||
return;
|
||||
}
|
||||
if (userPlayIntent.value && play.value) {
|
||||
playAudio(requestId).catch((e) => {
|
||||
console.error('重试播放失败:', e);
|
||||
setPlayMusic(false);
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
// 非操作锁错误,停止播放并通知用户
|
||||
setPlayMusic(false);
|
||||
console.warn('播放音频失败(非操作锁错误),由调用方处理重试');
|
||||
message.error(i18n.global.t('player.playFailed'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 暂停播放
|
||||
*/
|
||||
@@ -540,109 +115,14 @@ export const usePlayerCoreStore = defineStore(
|
||||
setIsPlay(value);
|
||||
userPlayIntent.value = value;
|
||||
} else {
|
||||
await handlePlayMusic(value);
|
||||
const { playTrack } = await import('@/services/playbackController');
|
||||
await playTrack(value);
|
||||
play.value = true;
|
||||
isPlay.value = true;
|
||||
userPlayIntent.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用指定音源重新解析当前歌曲
|
||||
*/
|
||||
const reparseCurrentSong = async (sourcePlatform: Platform, isAuto: boolean = false) => {
|
||||
try {
|
||||
const currentSong = playMusic.value;
|
||||
if (!currentSong || !currentSong.id) {
|
||||
console.warn('没有有效的播放对象');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 SongSourceConfigManager 保存配置
|
||||
SongSourceConfigManager.setConfig(
|
||||
currentSong.id,
|
||||
[sourcePlatform],
|
||||
isAuto ? 'auto' : 'manual'
|
||||
);
|
||||
|
||||
const currentSound = audioService.getCurrentSound();
|
||||
if (currentSound) {
|
||||
currentSound.pause();
|
||||
}
|
||||
|
||||
const numericId =
|
||||
typeof currentSong.id === 'string' ? parseInt(currentSong.id, 10) : currentSong.id;
|
||||
|
||||
console.log(`使用音源 ${sourcePlatform} 重新解析歌曲 ${numericId}`);
|
||||
|
||||
const songData = cloneDeep(currentSong);
|
||||
const res = await getParsingMusicUrl(numericId, songData);
|
||||
|
||||
if (res && res.data && res.data.data && res.data.data.url) {
|
||||
const newUrl = res.data.data.url;
|
||||
console.log(`解析成功,获取新URL: ${newUrl.substring(0, 50)}...`);
|
||||
|
||||
const updatedMusic = {
|
||||
...currentSong,
|
||||
playMusicUrl: newUrl,
|
||||
expiredAt: Date.now() + 1800000
|
||||
};
|
||||
|
||||
await handlePlayMusic(updatedMusic, true);
|
||||
|
||||
// 更新播放列表中的歌曲信息
|
||||
const { usePlaylistStore } = await import('./playlist');
|
||||
const playlistStore = usePlaylistStore();
|
||||
playlistStore.updateSong(updatedMusic);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.warn(`使用音源 ${sourcePlatform} 解析失败`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新解析失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化播放状态
|
||||
*/
|
||||
const initializePlayState = async () => {
|
||||
const { useSettingsStore } = await import('./settings');
|
||||
const settingStore = useSettingsStore();
|
||||
|
||||
if (playMusic.value && Object.keys(playMusic.value).length > 0) {
|
||||
try {
|
||||
console.log('恢复上次播放的音乐:', playMusic.value.name);
|
||||
const isPlaying = settingStore.setData.autoPlay;
|
||||
|
||||
// 本地音乐(local:// 协议)不需要重新获取 URL,保留原始路径
|
||||
const isLocalMusic = playMusic.value.playMusicUrl?.startsWith('local://');
|
||||
|
||||
await handlePlayMusic(
|
||||
{
|
||||
...playMusic.value,
|
||||
isFirstPlay: true,
|
||||
playMusicUrl: isLocalMusic ? playMusic.value.playMusicUrl : undefined
|
||||
},
|
||||
isPlaying
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('重新获取音乐链接失败:', error);
|
||||
play.value = false;
|
||||
isPlay.value = false;
|
||||
playMusic.value = {} as SongResult;
|
||||
playMusicUrl.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
audioService.setPlaybackRate(playbackRate.value);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// ==================== 音频输出设备管理 ====================
|
||||
|
||||
/**
|
||||
@@ -707,12 +187,7 @@ export const usePlayerCoreStore = defineStore(
|
||||
getVolume,
|
||||
increaseVolume,
|
||||
decreaseVolume,
|
||||
handlePlayMusic,
|
||||
playAudio,
|
||||
handlePause,
|
||||
checkPlaybackState,
|
||||
reparseCurrentSong,
|
||||
initializePlayState,
|
||||
refreshAudioDevices,
|
||||
setAudioOutputDevice,
|
||||
initAudioDeviceListener
|
||||
|
||||
@@ -87,7 +87,6 @@ export const usePlaylistStore = defineStore(
|
||||
// 连续失败计数器(用于防止无限循环)
|
||||
const consecutiveFailCount = ref(0);
|
||||
const MAX_CONSECUTIVE_FAILS = 5; // 最大连续失败次数
|
||||
const SINGLE_TRACK_MAX_RETRIES = 3; // 单曲最大重试次数
|
||||
|
||||
// ==================== Computed ====================
|
||||
const currentPlayList = computed(() => playList.value);
|
||||
@@ -416,103 +415,104 @@ export const usePlaylistStore = defineStore(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 下一首
|
||||
* @param singleTrackRetryCount 单曲重试次数(同一首歌的重试)
|
||||
*/
|
||||
const _nextPlay = async (singleTrackRetryCount: number = 0) => {
|
||||
let nextPlayRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const cancelRetryTimer = () => {
|
||||
if (nextPlayRetryTimer) {
|
||||
clearTimeout(nextPlayRetryTimer);
|
||||
nextPlayRetryTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const _nextPlay = async (retryCount: number = 0, autoEnd: boolean = false) => {
|
||||
try {
|
||||
if (playList.value.length === 0) {
|
||||
return;
|
||||
if (playList.value.length === 0) return;
|
||||
|
||||
// User-initiated (retryCount=0): reset state
|
||||
if (retryCount === 0) {
|
||||
cancelRetryTimer();
|
||||
consecutiveFailCount.value = 0;
|
||||
}
|
||||
|
||||
const playerCore = usePlayerCoreStore();
|
||||
const sleepTimerStore = useSleepTimerStore();
|
||||
|
||||
// 检查是否超过最大连续失败次数
|
||||
if (consecutiveFailCount.value >= MAX_CONSECUTIVE_FAILS) {
|
||||
console.error(`[nextPlay] 连续${MAX_CONSECUTIVE_FAILS}首歌曲播放失败,停止播放`);
|
||||
console.error(`[nextPlay] 连续${MAX_CONSECUTIVE_FAILS}首播放失败,停止`);
|
||||
getMessage().warning(i18n.global.t('player.consecutiveFailsError'));
|
||||
consecutiveFailCount.value = 0; // 重置计数器
|
||||
consecutiveFailCount.value = 0;
|
||||
playerCore.setIsPlay(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 顺序播放模式:播放到最后一首后停止
|
||||
// Sequential mode: at the last song
|
||||
if (playMode.value === 0 && playListIndex.value >= playList.value.length - 1) {
|
||||
if (sleepTimerStore.sleepTimer.type === 'end') {
|
||||
sleepTimerStore.stopPlayback();
|
||||
if (autoEnd) {
|
||||
// 歌曲自然播放结束:停止播放
|
||||
console.log('[nextPlay] 顺序播放:最后一首播放完毕,停止');
|
||||
if (sleepTimerStore.sleepTimer.type === 'end') {
|
||||
sleepTimerStore.stopPlayback();
|
||||
}
|
||||
getMessage().info(i18n.global.t('player.playListEnded'));
|
||||
playerCore.setIsPlay(false);
|
||||
const { audioService } = await import('@/services/audioService');
|
||||
audioService.pause();
|
||||
} else {
|
||||
// 用户手动点击下一首:保持当前播放,只提示
|
||||
console.log('[nextPlay] 顺序播放:已是最后一首,保持当前播放');
|
||||
getMessage().info(i18n.global.t('player.playListEnded'));
|
||||
}
|
||||
console.log('[nextPlay] 顺序播放模式:已播放到最后一首,停止播放');
|
||||
playerCore.setIsPlay(false);
|
||||
const { audioService } = await import('@/services/audioService');
|
||||
audioService.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = playListIndex.value;
|
||||
const nowPlayListIndex = (playListIndex.value + 1) % playList.value.length;
|
||||
const nextSong = { ...playList.value[nowPlayListIndex] };
|
||||
|
||||
// 同一首歌重试时强制刷新在线 URL,避免卡在失效链接上
|
||||
if (singleTrackRetryCount > 0 && !nextSong.playMusicUrl?.startsWith('local://')) {
|
||||
// Force refresh URL on retry
|
||||
if (retryCount > 0 && !nextSong.playMusicUrl?.startsWith('local://')) {
|
||||
nextSong.playMusicUrl = undefined;
|
||||
nextSong.expiredAt = undefined;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[nextPlay] 尝试播放: ${nextSong.name}, 索引: ${currentIndex} -> ${nowPlayListIndex}, 单曲重试: ${singleTrackRetryCount}/${SINGLE_TRACK_MAX_RETRIES}, 连续失败: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}`
|
||||
);
|
||||
console.log(
|
||||
'[nextPlay] Current mode:',
|
||||
playMode.value,
|
||||
'Playlist length:',
|
||||
playList.value.length
|
||||
`[nextPlay] ${nextSong.name}, 索引: ${playListIndex.value} -> ${nowPlayListIndex}, 重试: ${retryCount}/1`
|
||||
);
|
||||
|
||||
// 先尝试播放歌曲
|
||||
const success = await playerCore.handlePlayMusic(nextSong, true);
|
||||
const { playTrack } = await import('@/services/playbackController');
|
||||
const success = await playTrack(nextSong, true);
|
||||
|
||||
// Check if we were superseded by a newer operation
|
||||
if (playerCore.playMusic.id !== nextSong.id) {
|
||||
console.log('[nextPlay] 被新操作取代,静默退出');
|
||||
return;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// 播放成功,重置所有计数器并更新索引
|
||||
consecutiveFailCount.value = 0;
|
||||
playListIndex.value = nowPlayListIndex;
|
||||
console.log(`[nextPlay] 播放成功,索引已更新为: ${nowPlayListIndex}`);
|
||||
console.log(
|
||||
'[nextPlay] New current song in list:',
|
||||
playList.value[playListIndex.value]?.name
|
||||
);
|
||||
console.log(`[nextPlay] 播放成功,索引: ${nowPlayListIndex}`);
|
||||
sleepTimerStore.handleSongChange();
|
||||
} else {
|
||||
console.error(`[nextPlay] 播放失败: ${nextSong.name}`);
|
||||
|
||||
// 单曲重试逻辑
|
||||
if (singleTrackRetryCount < SINGLE_TRACK_MAX_RETRIES) {
|
||||
console.log(
|
||||
`[nextPlay] 单曲重试 ${singleTrackRetryCount + 1}/${SINGLE_TRACK_MAX_RETRIES}`
|
||||
);
|
||||
// 不更新索引,重试同一首歌
|
||||
setTimeout(() => {
|
||||
_nextPlay(singleTrackRetryCount + 1);
|
||||
// Retry once, then skip to next
|
||||
if (retryCount < 1) {
|
||||
console.log(`[nextPlay] 播放失败,1秒后重试`);
|
||||
nextPlayRetryTimer = setTimeout(() => {
|
||||
nextPlayRetryTimer = null;
|
||||
_nextPlay(retryCount + 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
// 单曲重试次数用尽,递增连续失败计数,尝试下一首
|
||||
consecutiveFailCount.value++;
|
||||
console.log(
|
||||
`[nextPlay] 单曲重试用尽,连续失败计数: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}`
|
||||
`[nextPlay] 重试用尽,连续失败: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}`
|
||||
);
|
||||
|
||||
if (playList.value.length > 1) {
|
||||
// 更新索引到失败的歌曲位置,这样下次递归调用会继续往下
|
||||
playListIndex.value = nowPlayListIndex;
|
||||
getMessage().warning(i18n.global.t('player.parseFailedPlayNext'));
|
||||
|
||||
// 延迟后尝试下一首(重置单曲重试计数)
|
||||
setTimeout(() => {
|
||||
nextPlayRetryTimer = setTimeout(() => {
|
||||
nextPlayRetryTimer = null;
|
||||
_nextPlay(0);
|
||||
}, 500);
|
||||
} else {
|
||||
// 只有一首歌且失败
|
||||
getMessage().error(i18n.global.t('player.playFailed'));
|
||||
playerCore.setIsPlay(false);
|
||||
}
|
||||
@@ -525,73 +525,33 @@ export const usePlaylistStore = defineStore(
|
||||
|
||||
const nextPlay = useThrottleFn(_nextPlay, 500);
|
||||
|
||||
/**
|
||||
* 上一首
|
||||
*/
|
||||
/** 歌曲自然播放结束时调用,顺序模式最后一首会停止 */
|
||||
const nextPlayOnEnd = () => {
|
||||
_nextPlay(0, true);
|
||||
};
|
||||
|
||||
const _prevPlay = async () => {
|
||||
try {
|
||||
if (playList.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (playList.value.length === 0) return;
|
||||
|
||||
cancelRetryTimer();
|
||||
const playerCore = usePlayerCoreStore();
|
||||
const currentIndex = playListIndex.value;
|
||||
const nowPlayListIndex =
|
||||
(playListIndex.value - 1 + playList.value.length) % playList.value.length;
|
||||
|
||||
const prevSong = { ...playList.value[nowPlayListIndex] };
|
||||
|
||||
console.log(
|
||||
`[prevPlay] 尝试播放上一首: ${prevSong.name}, 索引: ${currentIndex} -> ${nowPlayListIndex}`
|
||||
`[prevPlay] ${prevSong.name}, 索引: ${playListIndex.value} -> ${nowPlayListIndex}`
|
||||
);
|
||||
|
||||
let success = false;
|
||||
let retryCount = 0;
|
||||
const maxRetries = 2;
|
||||
|
||||
// 先尝试播放歌曲,成功后再更新索引
|
||||
while (!success && retryCount < maxRetries) {
|
||||
success = await playerCore.handlePlayMusic(prevSong);
|
||||
|
||||
if (!success) {
|
||||
retryCount++;
|
||||
console.error(`播放上一首失败,尝试 ${retryCount}/${maxRetries}`);
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
console.error('多次尝试播放失败,将从播放列表中移除此歌曲');
|
||||
const newPlayList = [...playList.value];
|
||||
newPlayList.splice(nowPlayListIndex, 1);
|
||||
|
||||
if (newPlayList.length > 0) {
|
||||
const keepCurrentIndexPosition = true;
|
||||
setPlayList(newPlayList, keepCurrentIndexPosition);
|
||||
|
||||
if (newPlayList.length === 1) {
|
||||
playListIndex.value = 0;
|
||||
} else {
|
||||
const newPrevIndex =
|
||||
(playListIndex.value - 1 + newPlayList.length) % newPlayList.length;
|
||||
playListIndex.value = newPrevIndex;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
prevPlay();
|
||||
}, 300);
|
||||
return;
|
||||
} else {
|
||||
console.error('播放列表为空,停止尝试');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const { playTrack } = await import('@/services/playbackController');
|
||||
const success = await playTrack(prevSong);
|
||||
|
||||
if (success) {
|
||||
// 播放成功,更新索引
|
||||
playListIndex.value = nowPlayListIndex;
|
||||
console.log(`[prevPlay] 播放成功,索引已更新为: ${nowPlayListIndex}`);
|
||||
} else {
|
||||
console.error(`[prevPlay] 播放上一首失败,保持当前索引: ${currentIndex}`);
|
||||
console.log(`[prevPlay] 播放成功,索引: ${nowPlayListIndex}`);
|
||||
} else if (playerCore.playMusic.id === prevSong.id) {
|
||||
// Only show error if not superseded
|
||||
playerCore.setIsPlay(false);
|
||||
getMessage().error(i18n.global.t('player.playFailed'));
|
||||
}
|
||||
@@ -609,16 +569,12 @@ export const usePlaylistStore = defineStore(
|
||||
playListDrawerVisible.value = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置播放(兼容旧API)
|
||||
*/
|
||||
const setPlay = async (song: SongResult) => {
|
||||
try {
|
||||
const playerCore = usePlayerCoreStore();
|
||||
|
||||
// 检查URL是否已过期
|
||||
// Check URL expiration
|
||||
if (song.expiredAt && song.expiredAt < Date.now()) {
|
||||
// 本地音乐(local:// 协议)不会过期
|
||||
if (!song.playMusicUrl?.startsWith('local://')) {
|
||||
console.info(`歌曲URL已过期,重新获取: ${song.name}`);
|
||||
song.playMusicUrl = undefined;
|
||||
@@ -626,7 +582,7 @@ export const usePlaylistStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是当前正在播放的音乐,则切换播放/暂停状态
|
||||
// Toggle play/pause for current song
|
||||
if (
|
||||
playerCore.playMusic.id === song.id &&
|
||||
playerCore.playMusic.playMusicUrl === song.playMusicUrl &&
|
||||
@@ -644,10 +600,9 @@ export const usePlaylistStore = defineStore(
|
||||
const sound = audioService.getCurrentSound();
|
||||
if (sound) {
|
||||
sound.play();
|
||||
// 在恢复播放时也进行状态检测,防止URL已过期导致无声
|
||||
playerCore.checkPlaybackState(playerCore.playMusic);
|
||||
} else {
|
||||
console.warn('[PlaylistStore.setPlay] 无可用音频实例,尝试重建播放链路');
|
||||
// No audio instance, rebuild via playTrack
|
||||
const { playTrack } = await import('@/services/playbackController');
|
||||
const recoverSong = {
|
||||
...playerCore.playMusic,
|
||||
isFirstPlay: true,
|
||||
@@ -655,7 +610,7 @@ export const usePlaylistStore = defineStore(
|
||||
? playerCore.playMusic.playMusicUrl
|
||||
: undefined
|
||||
};
|
||||
const recovered = await playerCore.handlePlayMusic(recoverSong, true);
|
||||
const recovered = await playTrack(recoverSong, true);
|
||||
if (!recovered) {
|
||||
playerCore.setIsPlay(false);
|
||||
getMessage().error(i18n.global.t('player.playFailed'));
|
||||
@@ -665,33 +620,24 @@ export const usePlaylistStore = defineStore(
|
||||
return;
|
||||
}
|
||||
|
||||
if (song.isFirstPlay) {
|
||||
song.isFirstPlay = false;
|
||||
}
|
||||
if (song.isFirstPlay) song.isFirstPlay = false;
|
||||
|
||||
// 查找歌曲在播放列表中的索引
|
||||
// Update playlist index
|
||||
const songIndex = playList.value.findIndex(
|
||||
(item: SongResult) => item.id === song.id && item.source === song.source
|
||||
);
|
||||
|
||||
// 更新播放索引
|
||||
if (songIndex !== -1 && songIndex !== playListIndex.value) {
|
||||
console.log('歌曲索引不匹配,更新为:', songIndex);
|
||||
playListIndex.value = songIndex;
|
||||
}
|
||||
|
||||
const success = await playerCore.handlePlayMusic(song);
|
||||
|
||||
// playerCore 的状态由其自己的 store 管理
|
||||
const { playTrack } = await import('@/services/playbackController');
|
||||
const success = await playTrack(song);
|
||||
|
||||
if (success) {
|
||||
playerCore.isPlay = true;
|
||||
|
||||
// 预加载下一首歌曲
|
||||
if (songIndex !== -1) {
|
||||
setTimeout(() => {
|
||||
preloadNextSongs(playListIndex.value);
|
||||
}, 3000);
|
||||
setTimeout(() => preloadNextSongs(playListIndex.value), 3000);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
@@ -740,6 +686,7 @@ export const usePlaylistStore = defineStore(
|
||||
restoreOriginalOrder,
|
||||
preloadNextSongs,
|
||||
nextPlay: nextPlay as unknown as typeof _nextPlay,
|
||||
nextPlayOnEnd,
|
||||
prevPlay: prevPlay as unknown as typeof _prevPlay,
|
||||
setPlayListDrawerVisible,
|
||||
setPlay,
|
||||
|
||||
Reference in New Issue
Block a user