From c5af89e51fbc40709508fb9c27ebb415f3c72638 Mon Sep 17 00:00:00 2001 From: algerkong Date: Fri, 2 May 2025 19:25:12 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E7=9B=91=E5=90=AC=E5=99=A8?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E9=9F=B3=E9=A2=91=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=E9=9F=B3=E9=A2=91?= =?UTF-8?q?=E5=B0=B1=E7=BB=AA=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E6=93=8D=E4=BD=9C=E9=94=81=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E4=BB=A5=E9=98=B2=E6=AD=A2=E5=B9=B6=E5=8F=91=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/hooks/MusicHook.ts | 90 +++---- src/renderer/services/audioService.ts | 65 ++++-- src/renderer/store/modules/player.ts | 323 ++++++++++++++++++++------ 3 files changed, 329 insertions(+), 149 deletions(-) diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts index 869ed4f..f55f014 100644 --- a/src/renderer/hooks/MusicHook.ts +++ b/src/renderer/hooks/MusicHook.ts @@ -235,70 +235,8 @@ const initProgressAnimation = () => { // 初始化进度动画 initProgressAnimation(); -// 简化后的 watch 函数,只保留核心逻辑 -watch( - () => playerStore.playMusicUrl, - async (newVal) => { - if (newVal && playMusic.value) { - try { - // 保存当前播放状态 - const shouldPlay = playerStore.play; - - // 检查是否有保存的进度 - let initialPosition = 0; - const savedProgress = JSON.parse(localStorage.getItem('playProgress') || '{}'); - if (savedProgress.songId === playMusic.value.id) { - initialPosition = savedProgress.progress; - } - - // 对于B站视频,检查URL是否有效 - if (playMusic.value.source === 'bilibili' && (!newVal || newVal === 'undefined')) { - console.log('B站视频URL无效,尝试重新获取'); - - // 需要重新获取B站视频URL - if (playMusic.value.bilibiliData) { - try { - const proxyUrl = await getBilibiliAudioUrl( - playMusic.value.bilibiliData.bvid, - playMusic.value.bilibiliData.cid - ); - - // 设置URL到播放器状态 - (playMusic.value as any).playMusicUrl = proxyUrl; - playerStore.playMusicUrl = proxyUrl; - newVal = proxyUrl; - } catch (error) { - console.error('获取B站音频URL失败:', error); - return; - } - } - } - - // 播放新音频,传递是否应该播放的状态 - const newSound = await audioService.play(newVal, playMusic.value, shouldPlay); - sound.value = newSound as Howl; - - // 如果有保存的进度,设置播放位置 - if (initialPosition > 0) { - newSound.seek(initialPosition); - // 同时更新进度条显示 - nowTime.value = initialPosition; - } - - setupAudioListeners(); - - // 确保状态与 localStorage 同步 - localStorage.setItem('currentPlayMusic', JSON.stringify(playerStore.playMusic)); - localStorage.setItem('currentPlayMusicUrl', newVal); - } catch (error) { - console.error('播放音频失败:', error); - // store.commit('setPlayMusic', false); - playerStore.setPlayMusic(false); - message.error(i18n.global.t('player.playFailed')); - } - } - } -); +// 移除对 playerStore.playMusicUrl 的监听,因为播放逻辑已经在 player.ts 中处理 +// 保留 watch 对 playerStore.playMusic 的监听以更新歌词数据 watch( () => playerStore.playMusic, @@ -1009,3 +947,27 @@ audioService.on('url_expired', async (expiredTrack) => { message.error('恢复播放失败,请手动点击播放'); } }); + +// 添加音频就绪事件监听器 +window.addEventListener('audio-ready', ((event: CustomEvent) => { + try { + const { sound: newSound } = event.detail; + if (newSound) { + // 更新本地 sound 引用 + sound.value = newSound as Howl; + + // 设置音频监听器 + setupAudioListeners(); + + // 获取当前播放位置并更新显示 + const currentPosition = newSound.seek() as number; + if (typeof currentPosition === 'number' && !Number.isNaN(currentPosition)) { + nowTime.value = currentPosition; + } + + console.log('音频就绪,已设置监听器并更新进度显示'); + } + } catch (error) { + console.error('处理音频就绪事件出错:', error); + } +}) as EventListener); diff --git a/src/renderer/services/audioService.ts b/src/renderer/services/audioService.ts index 9a76c6f..53a6a8c 100644 --- a/src/renderer/services/audioService.ts +++ b/src/renderer/services/audioService.ts @@ -43,6 +43,8 @@ class AudioService { // 添加操作锁防止并发操作 private operationLock = false; + private operationLockTimer: NodeJS.Timeout | null = null; + private operationLockTimeout = 5000; // 5秒超时 constructor() { if ('mediaSession' in navigator) { @@ -359,16 +361,47 @@ class AudioService { } } + // 设置操作锁,带超时自动释放 + private setOperationLock(): boolean { + if (this.operationLock) { + return false; + } + + this.operationLock = true; + + // 清除之前的定时器 + if (this.operationLockTimer) { + clearTimeout(this.operationLockTimer); + } + + // 设置超时自动释放锁 + this.operationLockTimer = setTimeout(() => { + console.warn('操作锁超时自动释放'); + this.operationLock = false; + this.operationLockTimer = null; + }, this.operationLockTimeout); + + return true; + } + + // 释放操作锁 + private releaseOperationLock(): void { + this.operationLock = false; + + if (this.operationLockTimer) { + clearTimeout(this.operationLockTimer); + this.operationLockTimer = null; + } + } + // 播放控制相关 play(url?: string, track?: SongResult, isPlay: boolean = true): Promise { // 如果操作锁已激活,说明有操作正在进行中,直接返回 - if (this.operationLock) { + if (!this.setOperationLock()) { console.log('audioService: 操作锁激活,忽略当前播放请求'); return Promise.reject(new Error('操作锁激活,请等待当前操作完成')); } - this.operationLock = true; - // 如果没有提供新的 URL 和 track,且当前有音频实例,则继续播放 if (this.currentSound && !url && !track) { // 如果有进行中的seek操作,等待其完成 @@ -377,13 +410,13 @@ class AudioService { this.seekLock = false; } this.currentSound.play(); - this.operationLock = false; + this.releaseOperationLock(); return Promise.resolve(this.currentSound); } // 如果没有提供必要的参数,返回错误 if (!url || !track) { - this.operationLock = false; + this.releaseOperationLock(); return Promise.reject(new Error('Missing required parameters: url and track')); } @@ -520,7 +553,7 @@ class AudioService { } } catch (error) { console.error('Error creating audio instance:', error); - this.operationLock = false; + this.releaseOperationLock(); reject(error); } }; @@ -528,7 +561,7 @@ class AudioService { tryPlay(); }).finally(() => { // 无论成功或失败都解除操作锁 - this.operationLock = false; + this.releaseOperationLock(); }); } @@ -541,12 +574,10 @@ class AudioService { } stop() { - if (this.operationLock) { + if (!this.setOperationLock()) { console.log('audioService: 操作锁激活,忽略当前停止请求'); return; } - - this.operationLock = true; if (this.currentSound) { try { @@ -569,7 +600,7 @@ class AudioService { } this.disposeEQ(); - this.operationLock = false; + this.releaseOperationLock(); } setVolume(volume: number) { @@ -580,12 +611,10 @@ class AudioService { } seek(time: number) { - if (this.operationLock) { + if (!this.setOperationLock()) { console.log('audioService: 操作锁激活,忽略当前seek请求'); return; } - - this.operationLock = true; if (this.currentSound) { try { @@ -599,16 +628,14 @@ class AudioService { } } - this.operationLock = false; + this.releaseOperationLock(); } pause() { - if (this.operationLock) { + if (!this.setOperationLock()) { console.log('audioService: 操作锁激活,忽略当前暂停请求'); return; } - - this.operationLock = true; if (this.currentSound) { try { @@ -623,7 +650,7 @@ class AudioService { } } - this.operationLock = false; + this.releaseOperationLock(); } clearAllListeners() { diff --git a/src/renderer/store/modules/player.ts b/src/renderer/store/modules/player.ts index 6362a48..3523d9b 100644 --- a/src/renderer/store/modules/player.ts +++ b/src/renderer/store/modules/player.ts @@ -2,17 +2,21 @@ import { cloneDeep } from 'lodash'; import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; +import i18n from '@/../i18n/renderer'; import { getBilibiliAudioUrl } from '@/api/bilibili'; import { getLikedList, getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music'; import { useMusicHistory } from '@/hooks/MusicHistoryHook'; +import { audioService } from '@/services/audioService'; import type { ILyric, ILyricText, SongResult } from '@/type/music'; import { getImgUrl } from '@/utils'; import { getImageLinearBackground } from '@/utils/linearColor'; +import { createDiscreteApi } from 'naive-ui'; import { useSettingsStore } from './settings'; import { useUserStore } from './user'; const musicHistory = useMusicHistory(); +const { message } = createDiscreteApi(['message']); const preloadingSounds = ref([]); @@ -310,42 +314,78 @@ export const usePlayerStore = defineStore('player', () => { } } catch (error) { console.error('获取B站音频URL失败:', error); - throw error; // 向上抛出错误,让调用者处理 + message.error(i18n.global.t('player.playFailed')); + return false; // 返回失败状态 } } - const updatedPlayMusic = await getSongDetail(music); - playMusic.value = updatedPlayMusic; - playMusicUrl.value = updatedPlayMusic.playMusicUrl as string; + try { + const updatedPlayMusic = await getSongDetail(music); + playMusic.value = updatedPlayMusic; + playMusicUrl.value = updatedPlayMusic.playMusicUrl as string; - play.value = isPlay; + play.value = isPlay; - localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value)); - localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value); - localStorage.setItem('isPlaying', play.value.toString()); + localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value)); + localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value); + localStorage.setItem('isPlaying', play.value.toString()); - let title = updatedPlayMusic.name; + let title = updatedPlayMusic.name; - if (updatedPlayMusic.source === 'netease' && updatedPlayMusic?.song?.artists) { - title += ` - ${updatedPlayMusic.song.artists.reduce( - (prev: string, curr: any) => `${prev}${curr.name}/`, - '' - )}`; - } else if (updatedPlayMusic.source === 'bilibili' && updatedPlayMusic?.song?.ar?.[0]) { - title += ` - ${updatedPlayMusic.song.ar[0].name}`; + if (updatedPlayMusic.source === 'netease' && updatedPlayMusic?.song?.artists) { + title += ` - ${updatedPlayMusic.song.artists.reduce( + (prev: string, curr: any) => `${prev}${curr.name}/`, + '' + )}`; + } else if (updatedPlayMusic.source === 'bilibili' && updatedPlayMusic?.song?.ar?.[0]) { + title += ` - ${updatedPlayMusic.song.ar[0].name}`; + } + + document.title = title; + + loadLrcAsync(playMusic.value); + + musicHistory.addMusic(playMusic.value); + + const newIndex = playList.value.findIndex( + (item: SongResult) => item.id === music.id && item.source === music.source + ); + + // 只有在找到歌曲时才更新索引 + if (newIndex !== -1) { + playListIndex.value = newIndex; + fetchSongs(playList.value, playListIndex.value + 1, playListIndex.value + 3); + } else { + console.warn('当前歌曲未在播放列表中找到'); + } + + // 使用标记防止循环调用 + let playInProgress = false; + + // 直接调用 playAudio 方法播放音频,不需要依赖外部监听 + try { + if (playInProgress) { + console.warn('播放操作正在进行中,避免重复调用'); + return true; + } + + playInProgress = true; + + // 因为调用 playAudio 前我们已经设置了 play.value,所以不需要额外传递 shouldPlay 参数 + const result = await playAudio(); + + playInProgress = false; + return !!result; + } catch (error) { + console.error('自动播放音频失败:', error); + playInProgress = false; + return false; + } + } catch (error) { + console.error('处理播放音乐失败:', error); + message.error(i18n.global.t('player.playFailed')); + return false; } - - document.title = title; - - loadLrcAsync(playMusic.value); - - musicHistory.addMusic(playMusic.value); - - playListIndex.value = playList.value.findIndex( - (item: SongResult) => item.id === music.id && item.source === music.source - ); - - fetchSongs(playList.value, playListIndex.value + 1, playListIndex.value + 3); }; const setPlay = async (song: SongResult) => { @@ -405,56 +445,124 @@ export const usePlayerStore = defineStore('player', () => { }; const nextPlay = async () => { - if (playList.value.length === 0) { - play.value = true; + // 静态标志,防止多次调用造成递归 + if ((nextPlay as any).isRunning) { + console.log('下一首播放正在执行中,忽略重复调用'); return; } + + try { + (nextPlay as any).isRunning = true; + + if (playList.value.length === 0) { + play.value = true; + (nextPlay as any).isRunning = false; + return; + } - let nowPlayListIndex: number; + let nowPlayListIndex: number; - if (playMode.value === 2) { - do { - nowPlayListIndex = Math.floor(Math.random() * playList.value.length); - } while (nowPlayListIndex === playListIndex.value && playList.value.length > 1); - } else { - nowPlayListIndex = (playListIndex.value + 1) % playList.value.length; + if (playMode.value === 2) { + do { + nowPlayListIndex = Math.floor(Math.random() * playList.value.length); + } while (nowPlayListIndex === playListIndex.value && playList.value.length > 1); + } else { + nowPlayListIndex = (playListIndex.value + 1) % playList.value.length; + } + + // 获取下一首歌曲 + const nextSong = playList.value[nowPlayListIndex]; + + // 如果是B站视频,确保重新获取URL + if (nextSong.source === 'bilibili' && nextSong.bilibiliData) { + // 清除之前的URL,确保重新获取 + nextSong.playMusicUrl = undefined; + console.log('下一首是B站视频,已清除URL强制重新获取'); + } + + // 尝试播放,如果失败会返回false + const success = await handlePlayMusic(nextSong); + + if (!success) { + console.error('播放下一首失败,将从播放列表中移除此歌曲'); + // 从播放列表中移除失败的歌曲 + const newPlayList = [...playList.value]; + newPlayList.splice(nowPlayListIndex, 1); + + if (newPlayList.length > 0) { + // 更新播放列表后,重新尝试播放下一首 + setPlayList(newPlayList); + // 延迟一点时间再尝试下一首,避免立即触发可能导致的无限循环 + setTimeout(() => { + (nextPlay as any).isRunning = false; + nextPlay(); + }, 300); + return; + } + } + } catch (error) { + console.error('切换下一首出错:', error); + } finally { + (nextPlay as any).isRunning = false; } - - playListIndex.value = nowPlayListIndex; - - // 获取下一首歌曲 - const nextSong = playList.value[playListIndex.value]; - - // 如果是B站视频,确保重新获取URL - if (nextSong.source === 'bilibili' && nextSong.bilibiliData) { - // 清除之前的URL,确保重新获取 - nextSong.playMusicUrl = undefined; - console.log('下一首是B站视频,已清除URL强制重新获取'); - } - - await handlePlayMusic(nextSong); }; const prevPlay = async () => { - if (playList.value.length === 0) { - play.value = true; + // 静态标志,防止多次调用造成递归 + if ((prevPlay as any).isRunning) { + console.log('上一首播放正在执行中,忽略重复调用'); return; } - const nowPlayListIndex = - (playListIndex.value - 1 + playList.value.length) % playList.value.length; + + try { + (prevPlay as any).isRunning = true; + + if (playList.value.length === 0) { + play.value = true; + (prevPlay as any).isRunning = false; + return; + } + + const nowPlayListIndex = + (playListIndex.value - 1 + playList.value.length) % playList.value.length; - // 获取上一首歌曲 - const prevSong = playList.value[nowPlayListIndex]; + // 获取上一首歌曲 + const prevSong = playList.value[nowPlayListIndex]; - // 如果是B站视频,确保重新获取URL - if (prevSong.source === 'bilibili' && prevSong.bilibiliData) { - // 清除之前的URL,确保重新获取 - prevSong.playMusicUrl = undefined; - console.log('上一首是B站视频,已清除URL强制重新获取'); + // 如果是B站视频,确保重新获取URL + if (prevSong.source === 'bilibili' && prevSong.bilibiliData) { + // 清除之前的URL,确保重新获取 + prevSong.playMusicUrl = undefined; + console.log('上一首是B站视频,已清除URL强制重新获取'); + } + + // 尝试播放,如果失败会返回false + const success = await handlePlayMusic(prevSong); + + if (success) { + await fetchSongs(playList.value, playListIndex.value - 3, nowPlayListIndex); + } else { + console.error('播放上一首失败,将从播放列表中移除此歌曲'); + // 从播放列表中移除失败的歌曲 + const newPlayList = [...playList.value]; + newPlayList.splice(nowPlayListIndex, 1); + + if (newPlayList.length > 0) { + // 更新播放列表后,重新尝试播放上一首 + setPlayList(newPlayList); + // 延迟一点时间再尝试上一首,避免立即触发可能导致的无限循环 + setTimeout(() => { + (prevPlay as any).isRunning = false; + prevPlay(); + }, 300); + return; + } + } + } catch (error) { + console.error('切换上一首出错:', error); + } finally { + (prevPlay as any).isRunning = false; } - - await handlePlayMusic(prevSong); - await fetchSongs(playList.value, playListIndex.value - 3, nowPlayListIndex); }; const togglePlayMode = () => { @@ -568,6 +676,88 @@ export const usePlayerStore = defineStore('player', () => { localStorage.setItem('favoriteList', JSON.stringify(favoriteList.value)); }; + // 修改:处理音频播放的方法,使用事件触发机制 + const playAudio = async () => { + if (!playMusicUrl.value || !playMusic.value) return null; + + try { + // 保存当前播放状态 + const shouldPlay = play.value; + + // 检查是否有保存的进度 + let initialPosition = 0; + const savedProgress = JSON.parse(localStorage.getItem('playProgress') || '{}'); + if (savedProgress.songId === playMusic.value.id) { + initialPosition = savedProgress.progress; + } + + // 对于B站视频,检查URL是否有效 + if (playMusic.value.source === 'bilibili' && (!playMusicUrl.value || playMusicUrl.value === 'undefined')) { + console.log('B站视频URL无效,尝试重新获取'); + + // 需要重新获取B站视频URL + if (playMusic.value.bilibiliData) { + try { + const proxyUrl = await getBilibiliAudioUrl( + playMusic.value.bilibiliData.bvid, + playMusic.value.bilibiliData.cid + ); + + // 设置URL到播放器状态 + (playMusic.value as any).playMusicUrl = proxyUrl; + playMusicUrl.value = proxyUrl; + } catch (error) { + console.error('获取B站音频URL失败:', error); + message.error(i18n.global.t('player.playFailed')); + return null; + } + } + } + + // 播放新音频,传递是否应该播放的状态 + const newSound = await audioService.play(playMusicUrl.value, playMusic.value, shouldPlay); + + // 如果有保存的进度,设置播放位置 + if (initialPosition > 0) { + newSound.seek(initialPosition); + } + + // 发布音频就绪事件,让 MusicHook.ts 来处理设置监听器 + window.dispatchEvent(new CustomEvent('audio-ready', { detail: { sound: newSound } })); + + // 确保状态与 localStorage 同步 + localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value)); + localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value); + + return newSound; + } catch (error) { + console.error('播放音频失败:', error); + setPlayMusic(false); + + // 避免直接调用 nextPlay,改用延时避免无限循环 + // 检查错误是否是由于操作锁引起的 + const errorMsg = error instanceof Error ? error.message : String(error); + if (errorMsg.includes('操作锁激活')) { + console.log('由于操作锁正在使用,将在500ms后重试'); + // 操作锁错误,延迟后再尝试 + setTimeout(() => { + // 检查当前播放列表是否有下一首 + if (playList.value.length > 1) { + nextPlay(); + } + }, 500); + } else { + // 其他错误,延迟更短时间后切换 + setTimeout(() => { + nextPlay(); + }, 100); + } + + message.error(i18n.global.t('player.playFailed')); + return null; + } + }; + return { play, isPlay, @@ -598,6 +788,7 @@ export const usePlayerStore = defineStore('player', () => { initializeFavoriteList, addToFavorite, removeFromFavorite, - removeFromPlayList + removeFromPlayList, + playAudio }; });