diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts index 4c33903..d0ab5d3 100644 --- a/src/renderer/hooks/MusicHook.ts +++ b/src/renderer/hooks/MusicHook.ts @@ -1,5 +1,5 @@ import { createDiscreteApi } from 'naive-ui'; -import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; +import { computed, nextTick, onMounted, ref, watch } from 'vue'; import useIndexedDB from '@/hooks/IndexDBHook'; import { audioService } from '@/services/audioService'; @@ -54,19 +54,205 @@ document.onkeyup = (e) => { const { message } = createDiscreteApi(['message']); +// 全局变量 +let progressAnimationInitialized = false; +let globalAnimationFrameId: number | null = null; + +// 全局停止函数 +const stopProgressAnimation = () => { + if (globalAnimationFrameId) { + cancelAnimationFrame(globalAnimationFrameId); + globalAnimationFrameId = null; + } +}; + +// 全局更新函数 +const updateProgress = () => { + if (!store.state.play) { + stopProgressAnimation(); + return; + } + + const currentSound = sound.value; + if (!currentSound) { + console.log('进度更新:无效的 sound 对象'); + // 不是立即返回,而是设置定时器稍后再次尝试 + globalAnimationFrameId = setTimeout(() => { + requestAnimationFrame(updateProgress); + }, 100) as unknown as number; + return; + } + + if (typeof currentSound.seek !== 'function') { + console.log('进度更新:无效的 seek 函数'); + // 不是立即返回,而是设置定时器稍后再次尝试 + globalAnimationFrameId = setTimeout(() => { + requestAnimationFrame(updateProgress); + }, 100) as unknown as number; + return; + } + + try { + const { start, end } = currentLrcTiming.value; + if (typeof start !== 'number' || typeof end !== 'number' || start === end) { + globalAnimationFrameId = requestAnimationFrame(updateProgress); + return; + } + + let currentTime; + try { + currentTime = currentSound.seek() as number; + + // 保存当前播放进度到 localStorage (每秒保存一次,避免频繁写入) + if (Math.floor(currentTime) % 2 === 0) { + if (store.state.playMusic && store.state.playMusic.id) { + localStorage.setItem( + 'playProgress', + JSON.stringify({ + songId: store.state.playMusic.id, + progress: currentTime + }) + ); + } + } + } catch (seekError) { + console.error('调用 seek() 方法出错:', seekError); + globalAnimationFrameId = requestAnimationFrame(updateProgress); + return; + } + + if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) { + console.error('无效的当前时间:', currentTime); + globalAnimationFrameId = requestAnimationFrame(updateProgress); + return; + } + + const elapsed = currentTime - start; + const duration = end - start; + const progress = (elapsed / duration) * 100; + + // 确保进度在 0-100 之间 + currentLrcProgress.value = Math.min(Math.max(progress, 0), 100); + } catch (error) { + console.error('更新进度出错:', error); + } + + // 继续下一帧更新 + globalAnimationFrameId = requestAnimationFrame(updateProgress); +}; + +// 全局启动函数 +const startProgressAnimation = () => { + stopProgressAnimation(); // 先停止之前的动画 + updateProgress(); +}; + +// 全局初始化函数 +const initProgressAnimation = () => { + if (progressAnimationInitialized) return; + + console.log('初始化进度动画'); + progressAnimationInitialized = true; + + // 监听播放状态变化,这里使用防抖,避免频繁触发 + let debounceTimer: any = null; + + watch( + () => store.state.play, + (newIsPlaying) => { + console.log('播放状态变化:', newIsPlaying); + + // 清除之前的定时器 + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + // 使用防抖,延迟 100ms 再执行 + debounceTimer = setTimeout(() => { + if (newIsPlaying) { + // 确保 sound 对象有效时才启动进度更新 + if (sound.value) { + console.log('sound 对象已存在,立即启动进度更新'); + startProgressAnimation(); + } else { + console.log('等待 sound 对象初始化...'); + // 定时检查 sound 对象是否已初始化 + const checkInterval = setInterval(() => { + if (sound.value) { + clearInterval(checkInterval); + console.log('sound 对象已初始化,开始进度更新'); + startProgressAnimation(); + } + }, 100); + // 设置超时,防止无限等待 + setTimeout(() => { + clearInterval(checkInterval); + console.log('等待 sound 对象超时,已停止等待'); + }, 5000); + } + } else { + stopProgressAnimation(); + } + }, 100); + } + ); + + // 监听当前歌词索引变化 + watch(nowIndex, () => { + currentLrcProgress.value = 0; + if (store.state.play) { + startProgressAnimation(); + } + }); + + // 监听音频对象变化 + watch(sound, (newSound) => { + console.log('sound 对象变化:', !!newSound); + if (newSound && store.state.play) { + startProgressAnimation(); + } + }); +}; + +// 初始化进度动画 +initProgressAnimation(); + +// 简化后的 watch 函数,只保留核心逻辑 watch( () => store.state.playMusicUrl, async (newVal) => { if (newVal && playMusic.value) { try { - const newSound = await audioService.play(newVal, playMusic.value); + // 保存当前播放状态 + const shouldPlay = store.state.play; + + // 检查是否有保存的进度 + let initialPosition = 0; + const savedProgress = JSON.parse(localStorage.getItem('playProgress') || '{}'); + if (savedProgress.songId === playMusic.value.id) { + initialPosition = savedProgress.progress; + } + + // 播放新音频,传递是否应该播放的状态 + 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(store.state.playMusic)); + localStorage.setItem('currentPlayMusicUrl', newVal); } catch (error) { console.error('播放音频失败:', error); store.commit('setPlayMusic', false); message.error('当前歌曲播放失败,播放下一首'); - // 下一首 store.commit('nextPlay'); } } @@ -104,6 +290,26 @@ const setupAudioListeners = () => { // 清理所有事件监听器 audioService.clearAllListeners(); + // 立即更新一次时间和进度(解决初始化时进度条不显示问题) + const updateCurrentTimeAndDuration = () => { + const currentSound = audioService.getCurrentSound(); + if (currentSound) { + try { + // 更新当前时间和总时长 + const currentTime = currentSound.seek() as number; + if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) { + nowTime.value = currentTime; + allTime.value = currentSound.duration() as number; + } + } catch (error) { + console.error('初始化时间和进度失败:', error); + } + } + }; + + // 立即执行一次更新 + updateCurrentTimeAndDuration(); + // 监听播放 audioService.on('play', () => { store.commit('setPlayMusic', true); @@ -111,8 +317,15 @@ const setupAudioListeners = () => { interval = window.setInterval(() => { try { const currentSound = sound.value; - if (!currentSound || typeof currentSound.seek !== 'function') { - console.error('Invalid sound object or seek function'); + 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; } @@ -129,7 +342,8 @@ const setupAudioListeners = () => { const newIndex = getLrcIndex(nowTime.value); if (newIndex !== nowIndex.value) { nowIndex.value = newIndex; - currentLrcProgress.value = 0; + // 注意:我们不在这里设置 currentLrcProgress 为 0 + // 因为这会与全局进度更新冲突 if (isElectron && isLyricWindowOpen.value) { sendLyricToWin(); } @@ -146,6 +360,7 @@ const setupAudioListeners = () => { // 监听暂停 audioService.on('pause', () => { + console.log('音频暂停事件触发'); store.commit('setPlayMusic', false); clearInterval(); if (isElectron && isLyricWindowOpen.value) { @@ -179,6 +394,7 @@ const setupAudioListeners = () => { // 监听结束 audioService.on('end', () => { + console.log('音频播放结束事件触发'); clearInterval(); if (store.state.playMode === 1) { @@ -213,7 +429,26 @@ export const play = () => { }; export const pause = () => { - audioService.getCurrentSound()?.pause(); + const currentSound = audioService.getCurrentSound(); + if (currentSound) { + try { + // 保存当前播放进度 + const currentTime = currentSound.seek() as number; + if (store.state.playMusic && store.state.playMusic.id) { + localStorage.setItem( + 'playProgress', + JSON.stringify({ + songId: store.state.playMusic.id, + progress: currentTime + }) + ); + } + + currentSound.pause(); + } catch (error) { + console.error('暂停播放出错:', error); + } + } }; const isPlaying = computed(() => store.state.play as boolean); @@ -267,103 +502,8 @@ export const getLrcStyle = (index: number) => { // 播放进度 export const useLyricProgress = () => { - let animationFrameId: number | null = null; - - const updateProgress = () => { - if (!isPlaying.value) { - stopProgressAnimation(); - return; - } - - const currentSound = sound.value; - if (!currentSound || typeof currentSound.seek !== 'function') { - console.error('Invalid sound object or seek function'); - stopProgressAnimation(); - return; - } - - try { - const { start, end } = currentLrcTiming.value; - if (typeof start !== 'number' || typeof end !== 'number' || start === end) { - return; - } - - const currentTime = currentSound.seek() as number; - if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) { - console.error('Invalid current time:', currentTime); - return; - } - - const elapsed = currentTime - start; - const duration = end - start; - const progress = (elapsed / duration) * 100; - - // 确保进度在 0-100 之间 - currentLrcProgress.value = Math.min(Math.max(progress, 0), 100); - } catch (error) { - console.error('Error updating progress:', error); - } - - // 继续下一帧更新 - animationFrameId = requestAnimationFrame(updateProgress); - }; - - const startProgressAnimation = () => { - stopProgressAnimation(); // 先停止之前的动画 - if (isPlaying.value) { - updateProgress(); - } - }; - - const stopProgressAnimation = () => { - if (animationFrameId) { - cancelAnimationFrame(animationFrameId); - animationFrameId = null; - } - }; - - // 监听播放状态变化 - watch( - isPlaying, - (newIsPlaying) => { - if (newIsPlaying) { - startProgressAnimation(); - } else { - stopProgressAnimation(); - } - }, - { immediate: true } - ); - - // 监听当前歌词索引变化 - watch(nowIndex, () => { - currentLrcProgress.value = 0; - if (isPlaying.value) { - startProgressAnimation(); - } - }); - - // 监听音频对象变化 - watch(sound, (newSound) => { - if (newSound && isPlaying.value) { - startProgressAnimation(); - } else { - stopProgressAnimation(); - } - }); - - onMounted(() => { - if (isPlaying.value) { - startProgressAnimation(); - } - }); - - onUnmounted(() => { - stopProgressAnimation(); - }); - + // 如果已经在全局更新进度,立即返回 return { - currentLrcProgress, getLrcStyle }; }; @@ -484,6 +624,29 @@ if (isElectron) { // 在组件挂载时设置监听器 onMounted(() => { + // 初始化音频监听器 setupAudioListeners(); - useLyricProgress(); // 直接调用,不需要解构返回值 + + // 检查是否需要初始化 sound 对象 + if (!sound.value && audioService.getCurrentSound()) { + sound.value = audioService.getCurrentSound(); + + // 如果当前处于播放状态,启动进度更新 + if (store.state.play && sound.value) { + // 如果有保存的播放进度,应用它 + if (store.state.savedPlayProgress !== undefined && sound.value) { + try { + // 设置音频位置 + sound.value.seek(store.state.savedPlayProgress); + // 同时更新时间显示,这样进度条也会更新 + nowTime.value = store.state.savedPlayProgress; + console.log('恢复播放进度:', store.state.savedPlayProgress); + } catch (e) { + console.error('恢复播放进度失败:', e); + } + } + + startProgressAnimation(); + } + } }); diff --git a/src/renderer/hooks/MusicListHook.ts b/src/renderer/hooks/MusicListHook.ts index ce9cf71..79c42a3 100644 --- a/src/renderer/hooks/MusicListHook.ts +++ b/src/renderer/hooks/MusicListHook.ts @@ -49,11 +49,19 @@ const getSongDetail = async (playMusic: SongResult) => { // 加载 当前歌曲 歌曲列表数据 下一首mp3预加载 歌词数据 export const useMusicListHook = () => { - const handlePlayMusic = async (state: any, playMusic: SongResult) => { + const handlePlayMusic = async (state: any, playMusic: SongResult, isPlay: boolean = true) => { const updatedPlayMusic = await getSongDetail(playMusic); state.playMusic = updatedPlayMusic; state.playMusicUrl = updatedPlayMusic.playMusicUrl; - state.play = true; + + // 记录当前设置的播放状态 + state.play = isPlay; + + // 每次设置新歌曲时,立即更新 localStorage + localStorage.setItem('currentPlayMusic', JSON.stringify(state.playMusic)); + localStorage.setItem('currentPlayMusicUrl', state.playMusicUrl); + localStorage.setItem('isPlaying', state.play.toString()); + // 设置网页标题 document.title = `${updatedPlayMusic.name} - ${updatedPlayMusic?.song?.artists?.reduce((prev, curr) => `${prev}${curr.name}/`, '')}`; loadLrcAsync(state, updatedPlayMusic.id); diff --git a/src/renderer/services/audioService.ts b/src/renderer/services/audioService.ts index 9531def..17753c3 100644 --- a/src/renderer/services/audioService.ts +++ b/src/renderer/services/audioService.ts @@ -1,12 +1,14 @@ -import { Howl } from 'howler'; +import { Howl, Howler } from 'howler'; import type { SongResult } from '@/type/music'; -interface Window { +// 使用下划线前缀表示允许未使用的变量 +interface _Window { webkitAudioContext: typeof AudioContext; } -interface HowlSound { +// 使用下划线前缀表示允许未使用的变量 +interface _HowlSound { node: HTMLMediaElement & { audioSource?: MediaElementAudioSourceNode; }; @@ -247,6 +249,7 @@ class AudioService { private async setupEQ(sound: Howl) { try { const howl = sound as any; + // eslint-disable-next-line no-underscore-dangle const audioNode = howl._sounds?.[0]?._node; if (!audioNode || !(audioNode instanceof HTMLMediaElement)) { @@ -355,7 +358,7 @@ class AudioService { } // 播放控制相关 - play(url?: string, track?: SongResult): Promise { + play(url?: string, track?: SongResult, isPlay: boolean = true): Promise { // 如果没有提供新的 URL 和 track,且当前有音频实例,则继续播放 if (this.currentSound && !url && !track) { this.currentSound.play(); @@ -373,9 +376,18 @@ class AudioService { const tryPlay = async () => { try { + console.log('audioService: 开始创建音频对象'); + + // 确保 Howler 上下文已初始化 + if (!Howler.ctx) { + console.log('audioService: 初始化 Howler 上下文'); + Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + // 确保使用同一个音频上下文 - if (!Howler.ctx || Howler.ctx.state === 'closed') { - Howler.ctx = new AudioContext(); + if (Howler.ctx.state === 'closed') { + console.log('audioService: 重新创建音频上下文'); + Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); this.context = Howler.ctx; Howler.masterGain = this.context.createGain(); Howler.masterGain.connect(this.context.destination); @@ -383,24 +395,28 @@ class AudioService { // 恢复上下文状态 if (Howler.ctx.state === 'suspended') { + console.log('audioService: 恢复暂停的音频上下文'); await Howler.ctx.resume(); } // 先停止并清理现有的音频实例 if (this.currentSound) { + console.log('audioService: 停止并清理现有的音频实例'); this.currentSound.stop(); this.currentSound.unload(); this.currentSound = null; } // 清理 EQ 但保持上下文 + console.log('audioService: 清理 EQ'); await this.disposeEQ(true); this.currentTrack = track; + console.log('audioService: 创建新的 Howl 对象'); this.currentSound = new Howl({ src: [url], html5: true, - autoplay: true, + autoplay: false, // 修改为 false,不自动播放,等待完全初始化后手动播放 volume: localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1, @@ -429,14 +445,26 @@ class AudioService { // 音频加载成功后设置 EQ 和更新媒体会话 if (this.currentSound) { try { + console.log('audioService: 音频加载成功,设置 EQ'); await this.setupEQ(this.currentSound); this.updateMediaSessionMetadata(track); this.updateMediaSessionPositionState(); this.emit('load'); + + // 此时音频已完全初始化,根据 isPlay 参数决定是否播放 + console.log('audioService: 音频完全初始化,isPlay =', isPlay); + if (isPlay) { + console.log('audioService: 开始播放'); + this.currentSound.play(); + } + resolve(this.currentSound); } catch (error) { console.error('设置 EQ 失败:', error); - // 即使 EQ 设置失败,也继续播放 + // 即使 EQ 设置失败,也继续播放(如果需要) + if (isPlay) { + this.currentSound.play(); + } resolve(this.currentSound); } } diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts index 2fb174a..04f087e 100644 --- a/src/renderer/store/index.ts +++ b/src/renderer/store/index.ts @@ -94,6 +94,7 @@ export interface State { currentArtistId: number | null; systemFonts: { label: string; value: string }[]; showDownloadDrawer: boolean; + savedPlayProgress?: number; } const state: State = { @@ -139,6 +140,14 @@ const mutations = { async setPlayMusic(state: State, play: boolean) { state.play = play; localStorage.setItem('isPlaying', play.toString()); + + // 每次更改播放状态时,确保当前播放歌曲信息也被保存 + if (state.playMusic && Object.keys(state.playMusic).length > 0) { + localStorage.setItem('currentPlayMusic', JSON.stringify(state.playMusic)); + if (state.playMusicUrl) { + localStorage.setItem('currentPlayMusicUrl', state.playMusicUrl); + } + } }, setMusicFull(state: State, musicFull: boolean) { state.musicFull = musicFull; @@ -342,7 +351,8 @@ const actions = { }, async initializePlayState({ state, commit }: { state: State; commit: any }) { const savedPlayList = getLocalStorageItem('playList', []); - const savedPlayMusic = getLocalStorageItem('currentPlayMusic', null); + const savedPlayMusic = getLocalStorageItem('currentPlayMusic', null) as SongResult | null; + const savedPlayProgress = localStorage.getItem('playProgress'); if (savedPlayList.length > 0) { commit('setPlayList', savedPlayList); @@ -355,11 +365,26 @@ const actions = { // 根据自动播放设置决定是否恢复播放状态 const shouldAutoPlay = state.setData.autoPlay; - if (shouldAutoPlay) { - await handlePlayMusic(state, savedPlayMusic); - } + await handlePlayMusic(state, savedPlayMusic, shouldAutoPlay); state.play = shouldAutoPlay; state.isPlay = true; + + // 如果有保存的播放进度,则提供给前端组件使用 + if (savedPlayProgress) { + try { + const progress = JSON.parse(savedPlayProgress); + if (progress && progress.songId === savedPlayMusic.id) { + // 在全局状态中添加播放进度 + state.savedPlayProgress = progress.progress; + } else { + // 如果歌曲ID不匹配,清除保存的进度 + localStorage.removeItem('playProgress'); + } + } catch (e) { + console.error('解析保存的播放进度失败', e); + localStorage.removeItem('playProgress'); + } + } } catch (error) { console.error('重新获取音乐链接失败:', error); // 清除无效的播放状态 @@ -370,6 +395,7 @@ const actions = { localStorage.removeItem('currentPlayMusic'); localStorage.removeItem('currentPlayMusicUrl'); localStorage.removeItem('isPlaying'); + localStorage.removeItem('playProgress'); } } },