mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-24 16:27:23 +08:00
feat: 优化播放逻辑
This commit is contained in:
@@ -125,6 +125,19 @@ onMounted(async () => {
|
|||||||
if (isLyricWindow.value) {
|
if (isLyricWindow.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查网络状态,离线时自动跳转到本地音乐页面
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
console.log('检测到无网络连接,跳转到本地音乐页面');
|
||||||
|
router.push('/local-music');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听网络状态变化,断网时跳转到本地音乐页面
|
||||||
|
window.addEventListener('offline', () => {
|
||||||
|
console.log('网络连接断开,跳转到本地音乐页面');
|
||||||
|
router.push('/local-music');
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化 MusicHook,注入 playerStore
|
// 初始化 MusicHook,注入 playerStore
|
||||||
initMusicHook(playerStore);
|
initMusicHook(playerStore);
|
||||||
// 初始化播放状态
|
// 初始化播放状态
|
||||||
|
|||||||
+108
-56
@@ -397,6 +397,8 @@ const setupMusicWatchers = () => {
|
|||||||
|
|
||||||
const setupAudioListeners = () => {
|
const setupAudioListeners = () => {
|
||||||
let interval: any = null;
|
let interval: any = null;
|
||||||
|
// 播放状态恢复定时器:当 interval 因异常被清除时,自动恢复
|
||||||
|
let recoveryTimer: any = null;
|
||||||
|
|
||||||
const clearInterval = () => {
|
const clearInterval = () => {
|
||||||
if (interval) {
|
if (interval) {
|
||||||
@@ -405,9 +407,91 @@ const setupAudioListeners = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopRecovery = () => {
|
||||||
|
if (recoveryTimer) {
|
||||||
|
window.clearInterval(recoveryTimer);
|
||||||
|
recoveryTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动进度更新 interval
|
||||||
|
* 从 audioService 实时获取 sound 引用,避免闭包中 sound.value 过期
|
||||||
|
*/
|
||||||
|
const startProgressInterval = () => {
|
||||||
|
clearInterval();
|
||||||
|
interval = window.setInterval(() => {
|
||||||
|
try {
|
||||||
|
// 每次从 audioService 获取最新的 sound 引用,而不是依赖闭包中的 sound.value
|
||||||
|
const currentSound = audioService.getCurrentSound();
|
||||||
|
if (!currentSound) {
|
||||||
|
// sound 暂时为空(可能在切歌/重建中),不清除 interval,等待恢复
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof currentSound.seek !== 'function') {
|
||||||
|
// seek 方法不可用,跳过本次更新,不清除 interval
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = currentSound.seek() as number;
|
||||||
|
if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {
|
||||||
|
// 无效时间,跳过本次更新
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步 sound.value 引用(确保外部也能拿到最新的)
|
||||||
|
if (sound.value !== currentSound) {
|
||||||
|
sound.value = currentSound;
|
||||||
|
}
|
||||||
|
|
||||||
|
nowTime.value = currentTime;
|
||||||
|
allTime.value = currentSound.duration() as number;
|
||||||
|
const newIndex = getLrcIndex(nowTime.value);
|
||||||
|
if (newIndex !== nowIndex.value) {
|
||||||
|
nowIndex.value = newIndex;
|
||||||
|
if (isElectron && isLyricWindowOpen.value) {
|
||||||
|
sendLyricToWin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isElectron && isLyricWindowOpen.value) {
|
||||||
|
sendLyricToWin();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('进度更新 interval 出错:', error);
|
||||||
|
// 出错时不清除 interval,让下一次 tick 继续尝试
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动播放状态恢复监控
|
||||||
|
* 每 500ms 检查一次:如果 store 认为在播放但 interval 已丢失,则恢复
|
||||||
|
*/
|
||||||
|
const startRecoveryMonitor = () => {
|
||||||
|
stopRecovery();
|
||||||
|
recoveryTimer = window.setInterval(() => {
|
||||||
|
try {
|
||||||
|
const store = getPlayerStore();
|
||||||
|
if (store.play && !interval) {
|
||||||
|
const currentSound = audioService.getCurrentSound();
|
||||||
|
if (currentSound && currentSound.playing()) {
|
||||||
|
console.warn('[MusicHook] 检测到播放中但 interval 丢失,自动恢复');
|
||||||
|
startProgressInterval();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 静默忽略
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
// 清理所有事件监听器
|
// 清理所有事件监听器
|
||||||
audioService.clearAllListeners();
|
audioService.clearAllListeners();
|
||||||
|
|
||||||
|
// 启动恢复监控
|
||||||
|
startRecoveryMonitor();
|
||||||
|
|
||||||
// 监听seek开始事件,立即更新UI
|
// 监听seek开始事件,立即更新UI
|
||||||
audioService.on('seek_start', (time) => {
|
audioService.on('seek_start', (time) => {
|
||||||
// 直接更新显示位置,不检查拖动状态
|
// 直接更新显示位置,不检查拖动状态
|
||||||
@@ -417,7 +501,7 @@ const setupAudioListeners = () => {
|
|||||||
// 监听seek完成事件
|
// 监听seek完成事件
|
||||||
audioService.on('seek', () => {
|
audioService.on('seek', () => {
|
||||||
try {
|
try {
|
||||||
const currentSound = sound.value;
|
const currentSound = audioService.getCurrentSound();
|
||||||
if (currentSound) {
|
if (currentSound) {
|
||||||
// 立即更新显示时间,不进行任何检查
|
// 立即更新显示时间,不进行任何检查
|
||||||
const currentTime = currentSound.seek() as number;
|
const currentTime = currentSound.seek() as number;
|
||||||
@@ -465,49 +549,8 @@ const setupAudioListeners = () => {
|
|||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
window.api.sendSong(cloneDeep(getPlayerStore().playMusic));
|
window.api.sendSong(cloneDeep(getPlayerStore().playMusic));
|
||||||
}
|
}
|
||||||
clearInterval();
|
// 启动进度更新
|
||||||
interval = window.setInterval(() => {
|
startProgressInterval();
|
||||||
try {
|
|
||||||
const currentSound = sound.value;
|
|
||||||
if (!currentSound) {
|
|
||||||
console.error('Invalid sound object: sound is null or undefined');
|
|
||||||
clearInterval();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保 seek 方法存在且可调用
|
|
||||||
if (typeof currentSound.seek !== 'function') {
|
|
||||||
console.error('Invalid sound object: seek function not available');
|
|
||||||
clearInterval();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTime = currentSound.seek() as number;
|
|
||||||
if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {
|
|
||||||
console.error('Invalid current time:', currentTime);
|
|
||||||
clearInterval();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nowTime.value = currentTime;
|
|
||||||
allTime.value = currentSound.duration() as number;
|
|
||||||
const newIndex = getLrcIndex(nowTime.value);
|
|
||||||
if (newIndex !== nowIndex.value) {
|
|
||||||
nowIndex.value = newIndex;
|
|
||||||
// 注意:我们不在这里设置 currentLrcProgress 为 0
|
|
||||||
// 因为这会与全局进度更新冲突
|
|
||||||
if (isElectron && isLyricWindowOpen.value) {
|
|
||||||
sendLyricToWin();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isElectron && isLyricWindowOpen.value) {
|
|
||||||
sendLyricToWin();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in interval:', error);
|
|
||||||
clearInterval();
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听暂停
|
// 监听暂停
|
||||||
@@ -520,14 +563,16 @@ const setupAudioListeners = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const replayMusic = async () => {
|
const replayMusic = async (retryCount: number = 0) => {
|
||||||
|
const MAX_REPLAY_RETRIES = 3;
|
||||||
try {
|
try {
|
||||||
// 如果当前有音频实例,先停止并销毁
|
// 如果当前有音频实例,先停止并销毁
|
||||||
if (sound.value) {
|
const currentSound = audioService.getCurrentSound();
|
||||||
sound.value.stop();
|
if (currentSound) {
|
||||||
sound.value.unload();
|
currentSound.stop();
|
||||||
sound.value = null;
|
currentSound.unload();
|
||||||
}
|
}
|
||||||
|
sound.value = null;
|
||||||
|
|
||||||
// 重新播放当前歌曲
|
// 重新播放当前歌曲
|
||||||
if (getPlayerStore().playMusicUrl && playMusic.value) {
|
if (getPlayerStore().playMusicUrl && playMusic.value) {
|
||||||
@@ -535,12 +580,18 @@ const setupAudioListeners = () => {
|
|||||||
sound.value = newSound as Howl;
|
sound.value = newSound as Howl;
|
||||||
setupAudioListeners();
|
setupAudioListeners();
|
||||||
} else {
|
} else {
|
||||||
console.error('No music URL or playMusic data available');
|
console.error('单曲循环:无可用 URL 或歌曲数据');
|
||||||
getPlayerStore().nextPlay();
|
getPlayerStore().nextPlay();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error replaying song:', error);
|
console.error('单曲循环重播失败:', error);
|
||||||
getPlayerStore().nextPlay();
|
if (retryCount < MAX_REPLAY_RETRIES) {
|
||||||
|
console.log(`单曲循环重试 ${retryCount + 1}/${MAX_REPLAY_RETRIES}`);
|
||||||
|
setTimeout(() => replayMusic(retryCount + 1), 1000 * (retryCount + 1));
|
||||||
|
} else {
|
||||||
|
console.error('单曲循环重试次数用尽,切换下一首');
|
||||||
|
getPlayerStore().nextPlay();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -551,9 +602,7 @@ const setupAudioListeners = () => {
|
|||||||
|
|
||||||
if (getPlayerStore().playMode === 1) {
|
if (getPlayerStore().playMode === 1) {
|
||||||
// 单曲循环模式
|
// 单曲循环模式
|
||||||
if (sound.value) {
|
replayMusic();
|
||||||
replayMusic();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 顺序播放、列表循环、随机播放模式都使用统一的nextPlay方法
|
// 顺序播放、列表循环、随机播放模式都使用统一的nextPlay方法
|
||||||
getPlayerStore().nextPlay();
|
getPlayerStore().nextPlay();
|
||||||
@@ -568,7 +617,10 @@ const setupAudioListeners = () => {
|
|||||||
getPlayerStore().nextPlay();
|
getPlayerStore().nextPlay();
|
||||||
});
|
});
|
||||||
|
|
||||||
return clearInterval;
|
return () => {
|
||||||
|
clearInterval();
|
||||||
|
stopRecovery();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const play = () => {
|
export const play = () => {
|
||||||
|
|||||||
@@ -315,8 +315,11 @@ export const useSongDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (playMusic.expiredAt && playMusic.expiredAt < Date.now()) {
|
if (playMusic.expiredAt && playMusic.expiredAt < Date.now()) {
|
||||||
console.info(`歌曲已过期,重新获取: ${playMusic.name}`);
|
// 本地音乐(local:// 协议)不会过期,跳过清除
|
||||||
playMusic.playMusicUrl = undefined;
|
if (!playMusic.playMusicUrl?.startsWith('local://')) {
|
||||||
|
console.info(`歌曲已过期,重新获取: ${playMusic.name}`);
|
||||||
|
playMusic.playMusicUrl = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -513,66 +513,27 @@ class AudioService {
|
|||||||
seekTime: number = 0,
|
seekTime: number = 0,
|
||||||
existingSound?: Howl
|
existingSound?: Howl
|
||||||
): Promise<Howl> {
|
): Promise<Howl> {
|
||||||
// 每次调用play方法时,尝试强制重置锁(注意:仅在页面刷新后的第一次播放时应用)
|
// 如果没有提供新的 URL 和 track,且当前有音频实例,则继续播放当前音频
|
||||||
if (!this.currentSound) {
|
|
||||||
console.log('首次播放请求,强制重置操作锁');
|
|
||||||
this.forceResetOperationLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有操作锁,且不是同一个 track 的操作,则等待
|
|
||||||
if (this.operationLock) {
|
|
||||||
console.log('audioService: 操作锁激活中,等待...');
|
|
||||||
return Promise.reject(new Error('操作锁激活中'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.setOperationLock()) {
|
|
||||||
console.log('audioService: 获取操作锁失败');
|
|
||||||
return Promise.reject(new Error('操作锁激活中'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果操作锁已激活,但持续时间超过安全阈值,强制重置
|
|
||||||
if (this.operationLock) {
|
|
||||||
const currentTime = Date.now();
|
|
||||||
const lockDuration = currentTime - this.operationLockStartTime;
|
|
||||||
|
|
||||||
if (lockDuration > 2000) {
|
|
||||||
console.warn(`操作锁已激活 ${lockDuration}ms,超过安全阈值,强制重置`);
|
|
||||||
this.forceResetOperationLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取锁
|
|
||||||
if (!this.setOperationLock()) {
|
|
||||||
console.log('audioService: 操作锁激活,强制执行当前播放请求');
|
|
||||||
|
|
||||||
// 如果只是要继续播放当前音频,直接执行
|
|
||||||
if (this.currentSound && !url && !track) {
|
|
||||||
if (this.seekLock && this.seekDebounceTimer) {
|
|
||||||
clearTimeout(this.seekDebounceTimer);
|
|
||||||
this.seekLock = false;
|
|
||||||
}
|
|
||||||
this.currentSound.play();
|
|
||||||
return Promise.resolve(this.currentSound);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 强制释放锁并继续执行
|
|
||||||
this.forceResetOperationLock();
|
|
||||||
|
|
||||||
// 这里不再返回错误,而是继续执行播放逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有提供新的 URL 和 track,且当前有音频实例,则继续播放
|
|
||||||
if (this.currentSound && !url && !track) {
|
if (this.currentSound && !url && !track) {
|
||||||
// 如果有进行中的seek操作,等待其完成
|
|
||||||
if (this.seekLock && this.seekDebounceTimer) {
|
if (this.seekLock && this.seekDebounceTimer) {
|
||||||
clearTimeout(this.seekDebounceTimer);
|
clearTimeout(this.seekDebounceTimer);
|
||||||
this.seekLock = false;
|
this.seekLock = false;
|
||||||
}
|
}
|
||||||
this.currentSound.play();
|
this.currentSound.play();
|
||||||
this.releaseOperationLock();
|
|
||||||
return Promise.resolve(this.currentSound);
|
return Promise.resolve(this.currentSound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新播放请求:强制重置旧锁,确保不会被遗留锁阻塞
|
||||||
|
this.forceResetOperationLock();
|
||||||
|
|
||||||
|
// 获取操作锁
|
||||||
|
if (!this.setOperationLock()) {
|
||||||
|
// 理论上不会到这里(刚刚 forceReset 过),但作为防御性编程
|
||||||
|
console.warn('audioService: 获取操作锁失败,强制继续');
|
||||||
|
this.forceResetOperationLock();
|
||||||
|
this.setOperationLock();
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有提供必要的参数,返回错误
|
// 如果没有提供必要的参数,返回错误
|
||||||
if (!url || !track) {
|
if (!url || !track) {
|
||||||
this.releaseOperationLock();
|
this.releaseOperationLock();
|
||||||
@@ -649,11 +610,6 @@ class AudioService {
|
|||||||
this.currentTrack = track;
|
this.currentTrack = track;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果不是热切换,立即更新 currentTrack
|
|
||||||
if (!isHotSwap) {
|
|
||||||
this.currentTrack = track;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newSound: Howl;
|
let newSound: Howl;
|
||||||
|
|
||||||
if (existingSound) {
|
if (existingSound) {
|
||||||
@@ -1061,6 +1017,8 @@ class AudioService {
|
|||||||
* 验证音频图是否正确连接
|
* 验证音频图是否正确连接
|
||||||
* 用于检测音频播放前的图状态
|
* 用于检测音频播放前的图状态
|
||||||
*/
|
*/
|
||||||
|
// 检查音频图是否连接(调试用,保留供 EQ 诊断)
|
||||||
|
// @ts-ignore 保留供调试使用
|
||||||
private isAudioGraphConnected(): boolean {
|
private isAudioGraphConnected(): boolean {
|
||||||
if (!this.context || !this.gainNode || !this.source) {
|
if (!this.context || !this.gainNode || !this.source) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1150,18 +1108,14 @@ class AudioService {
|
|||||||
if (!this.currentSound) return false;
|
if (!this.currentSound) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 综合判断:
|
// 核心判断:Howler API 是否报告正在播放 + 音频上下文是否正常
|
||||||
// 1. Howler API是否报告正在播放
|
// 注意:不再检查 isAudioGraphConnected(),因为 EQ 重建期间
|
||||||
// 2. 是否不在加载状态
|
// source/gainNode 会暂时为 null,导致误判为未播放
|
||||||
// 3. 确保音频上下文状态正常
|
|
||||||
// 4. 确保音频图正确连接(在 Electron 环境中)
|
|
||||||
const isPlaying = this.currentSound.playing();
|
const isPlaying = this.currentSound.playing();
|
||||||
const isLoading = this.isLoading();
|
const isLoading = this.isLoading();
|
||||||
const contextRunning = Howler.ctx && Howler.ctx.state === 'running';
|
const contextRunning = Howler.ctx && Howler.ctx.state === 'running';
|
||||||
const graphConnected = isElectron ? this.isAudioGraphConnected() : true;
|
|
||||||
|
|
||||||
// 只有在所有条件都满足时才认为是真正在播放
|
return isPlaying && !isLoading && contextRunning;
|
||||||
return isPlaying && !isLoading && contextRunning && graphConnected;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查播放状态出错:', error);
|
console.error('检查播放状态出错:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -64,12 +64,29 @@ class PreloadService {
|
|||||||
const duration = sound.duration();
|
const duration = sound.duration();
|
||||||
const expectedDuration = (song.dt || 0) / 1000;
|
const expectedDuration = (song.dt || 0) / 1000;
|
||||||
|
|
||||||
// 时长差异只记录警告,不自动触发重新解析
|
if (expectedDuration > 0 && duration > 0) {
|
||||||
// 用户可以通过 ReparsePopover 手动选择正确的音源
|
const durationDiff = Math.abs(duration - expectedDuration);
|
||||||
if (expectedDuration > 0 && Math.abs(duration - expectedDuration) > 5) {
|
// 如果实际时长远小于预期(可能是试听版),记录警告
|
||||||
console.warn(
|
if (duration < expectedDuration * 0.5 && durationDiff > 10) {
|
||||||
`[PreloadService] 时长差异警告:实际 ${duration.toFixed(1)}s, 预期 ${expectedDuration.toFixed(1)}s (${song.name})`
|
console.warn(
|
||||||
);
|
`[PreloadService] 时长严重不足:实际 ${duration.toFixed(1)}s, 预期 ${expectedDuration.toFixed(1)}s (${song.name}),可能是试听版`
|
||||||
|
);
|
||||||
|
// 通过自定义事件通知上层,可用于后续自动切换音源
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('audio-duration-mismatch', {
|
||||||
|
detail: {
|
||||||
|
songId: song.id,
|
||||||
|
songName: song.name,
|
||||||
|
actualDuration: duration,
|
||||||
|
expectedDuration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (durationDiff > 5) {
|
||||||
|
console.warn(
|
||||||
|
`[PreloadService] 时长差异警告:实际 ${duration.toFixed(1)}s, 预期 ${expectedDuration.toFixed(1)}s (${song.name})`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sound;
|
return sound;
|
||||||
|
|||||||
@@ -110,8 +110,9 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 播放状态检测
|
* 播放状态检测
|
||||||
|
* 在播放开始后延迟检查音频是否真正在播放,防止无声播放
|
||||||
*/
|
*/
|
||||||
const checkPlaybackState = (song: SongResult, requestId?: string, timeout: number = 4000) => {
|
const checkPlaybackState = (song: SongResult, requestId?: string, timeout: number = 6000) => {
|
||||||
if (checkPlayTime) {
|
if (checkPlayTime) {
|
||||||
clearTimeout(checkPlayTime);
|
clearTimeout(checkPlayTime);
|
||||||
}
|
}
|
||||||
@@ -125,6 +126,10 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
console.log(`[${actualRequestId}] 播放事件触发,歌曲成功开始播放`);
|
console.log(`[${actualRequestId}] 播放事件触发,歌曲成功开始播放`);
|
||||||
audioService.off('play', onPlayHandler);
|
audioService.off('play', onPlayHandler);
|
||||||
audioService.off('playerror', onPlayErrorHandler);
|
audioService.off('playerror', onPlayErrorHandler);
|
||||||
|
if (checkPlayTime) {
|
||||||
|
clearTimeout(checkPlayTime);
|
||||||
|
checkPlayTime = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPlayErrorHandler = async () => {
|
const onPlayErrorHandler = async () => {
|
||||||
@@ -140,7 +145,10 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
|
|
||||||
if (userPlayIntent.value && play.value) {
|
if (userPlayIntent.value && play.value) {
|
||||||
console.log('播放失败,尝试刷新URL并重新播放');
|
console.log('播放失败,尝试刷新URL并重新播放');
|
||||||
playMusic.value.playMusicUrl = undefined;
|
// 本地音乐不需要刷新 URL
|
||||||
|
if (!playMusic.value.playMusicUrl?.startsWith('local://')) {
|
||||||
|
playMusic.value.playMusicUrl = undefined;
|
||||||
|
}
|
||||||
const refreshedSong = { ...song, isFirstPlay: true };
|
const refreshedSong = { ...song, isFirstPlay: true };
|
||||||
await handlePlayMusic(refreshedSong, true);
|
await handlePlayMusic(refreshedSong, true);
|
||||||
}
|
}
|
||||||
@@ -158,16 +166,46 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
return;
|
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) {
|
if (!audioService.isActuallyPlaying() && userPlayIntent.value && play.value) {
|
||||||
console.log(`${timeout}ms后歌曲未真正播放且用户仍希望播放,尝试重新获取URL`);
|
console.log(`${timeout}ms后歌曲未真正播放且用户仍希望播放,尝试重新获取URL`);
|
||||||
audioService.off('play', onPlayHandler);
|
audioService.off('play', onPlayHandler);
|
||||||
audioService.off('playerror', onPlayErrorHandler);
|
audioService.off('playerror', onPlayErrorHandler);
|
||||||
|
|
||||||
playMusic.value.playMusicUrl = undefined;
|
// 本地音乐不需要刷新 URL
|
||||||
|
if (!playMusic.value.playMusicUrl?.startsWith('local://')) {
|
||||||
|
playMusic.value.playMusicUrl = undefined;
|
||||||
|
}
|
||||||
(async () => {
|
(async () => {
|
||||||
const refreshedSong = { ...song, isFirstPlay: true };
|
const refreshedSong = { ...song, isFirstPlay: true };
|
||||||
await handlePlayMusic(refreshedSong, true);
|
await handlePlayMusic(refreshedSong, true);
|
||||||
})();
|
})();
|
||||||
|
} else {
|
||||||
|
audioService.off('play', onPlayHandler);
|
||||||
|
audioService.off('playerror', onPlayErrorHandler);
|
||||||
}
|
}
|
||||||
}, timeout);
|
}, timeout);
|
||||||
};
|
};
|
||||||
@@ -418,11 +456,10 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
return newSound;
|
return newSound;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('播放音频失败:', error);
|
console.error('播放音频失败:', error);
|
||||||
setPlayMusic(false);
|
|
||||||
|
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
// 操作锁错误处理
|
// 操作锁错误不应该停止播放状态,只需要重试
|
||||||
if (errorMsg.includes('操作锁激活')) {
|
if (errorMsg.includes('操作锁激活')) {
|
||||||
console.log('由于操作锁正在使用,将在1000ms后重试');
|
console.log('由于操作锁正在使用,将在1000ms后重试');
|
||||||
|
|
||||||
@@ -442,14 +479,17 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
if (userPlayIntent.value && play.value) {
|
if (userPlayIntent.value && play.value) {
|
||||||
playAudio(requestId).catch((e) => {
|
playAudio(requestId).catch((e) => {
|
||||||
console.error('重试播放失败:', e);
|
console.error('重试播放失败:', e);
|
||||||
|
setPlayMusic(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
|
// 非操作锁错误,停止播放并通知用户
|
||||||
|
setPlayMusic(false);
|
||||||
console.warn('播放音频失败(非操作锁错误),由调用方处理重试');
|
console.warn('播放音频失败(非操作锁错误),由调用方处理重试');
|
||||||
|
message.error(i18n.global.t('player.playFailed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
message.error(i18n.global.t('player.playFailed'));
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -556,8 +596,15 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
console.log('恢复上次播放的音乐:', playMusic.value.name);
|
console.log('恢复上次播放的音乐:', playMusic.value.name);
|
||||||
const isPlaying = settingStore.setData.autoPlay;
|
const isPlaying = settingStore.setData.autoPlay;
|
||||||
|
|
||||||
|
// 本地音乐(local:// 协议)不需要重新获取 URL,保留原始路径
|
||||||
|
const isLocalMusic = playMusic.value.playMusicUrl?.startsWith('local://');
|
||||||
|
|
||||||
await handlePlayMusic(
|
await handlePlayMusic(
|
||||||
{ ...playMusic.value, isFirstPlay: true, playMusicUrl: undefined },
|
{
|
||||||
|
...playMusic.value,
|
||||||
|
isFirstPlay: true,
|
||||||
|
playMusicUrl: isLocalMusic ? playMusic.value.playMusicUrl : undefined
|
||||||
|
},
|
||||||
isPlaying
|
isPlaying
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -563,9 +563,12 @@ export const usePlaylistStore = defineStore(
|
|||||||
|
|
||||||
// 检查URL是否已过期
|
// 检查URL是否已过期
|
||||||
if (song.expiredAt && song.expiredAt < Date.now()) {
|
if (song.expiredAt && song.expiredAt < Date.now()) {
|
||||||
console.info(`歌曲URL已过期,重新获取: ${song.name}`);
|
// 本地音乐(local:// 协议)不会过期
|
||||||
song.playMusicUrl = undefined;
|
if (!song.playMusicUrl?.startsWith('local://')) {
|
||||||
song.expiredAt = undefined;
|
console.info(`歌曲URL已过期,重新获取: ${song.name}`);
|
||||||
|
song.playMusicUrl = undefined;
|
||||||
|
song.expiredAt = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是当前正在播放的音乐,则切换播放/暂停状态
|
// 如果是当前正在播放的音乐,则切换播放/暂停状态
|
||||||
|
|||||||
Reference in New Issue
Block a user