From cfe197c80561cd79d862a4fb2d0480392337c73a Mon Sep 17 00:00:00 2001 From: alger Date: Mon, 31 Mar 2025 22:57:00 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E4=BC=98=E5=8C=96=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E6=92=AD=E6=94=BE=E8=BF=9B=E5=BA=A6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=8B=96=E5=8A=A8?= =?UTF-8?q?=E6=BB=91=E5=9D=97=E6=97=B6=E7=9A=84=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=92=8C=E8=8A=82=E6=B5=81=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/hooks/MusicHook.ts | 51 ++++++++++++++++++++-- src/renderer/layout/components/PlayBar.vue | 44 ++++++++++++++++--- src/renderer/services/audioService.ts | 42 +++++++++++++++--- src/renderer/store/modules/player.ts | 26 +++++++++-- 4 files changed, 145 insertions(+), 18 deletions(-) diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts index 952f8ab..90b4b96 100644 --- a/src/renderer/hooks/MusicHook.ts +++ b/src/renderer/hooks/MusicHook.ts @@ -60,6 +60,7 @@ const { message } = createDiscreteApi(['message']); // 全局变量 let progressAnimationInitialized = false; let globalAnimationFrameId: number | null = null; +const lastSavedTime = ref(0); // 全局停止函数 const stopProgressAnimation = () => { @@ -104,10 +105,21 @@ const updateProgress = () => { let currentTime; try { + // 获取当前播放位置 currentTime = currentSound.seek() as number; + // 减少更新频率,避免频繁更新UI + const timeDiff = Math.abs(currentTime - nowTime.value); + if (timeDiff > 0.2 || Math.floor(currentTime) !== Math.floor(nowTime.value)) { + nowTime.value = currentTime; + } + // 保存当前播放进度到 localStorage (每秒保存一次,避免频繁写入) - if (Math.floor(currentTime) % 2 === 0) { + if ( + Math.floor(currentTime) % 2 === 0 && + Math.floor(currentTime) !== Math.floor(lastSavedTime.value) + ) { + lastSavedTime.value = currentTime; if (playerStore.playMusic && playerStore.playMusic.id) { localStorage.setItem( 'playProgress', @@ -140,8 +152,10 @@ const updateProgress = () => { console.error('更新进度出错:', error); } - // 继续下一帧更新 - globalAnimationFrameId = requestAnimationFrame(updateProgress); + // 继续下一帧更新,但降低更新频率为60帧中更新10帧 + globalAnimationFrameId = setTimeout(() => { + requestAnimationFrame(updateProgress); + }, 100) as unknown as number; }; // 全局启动函数 @@ -326,6 +340,37 @@ const setupAudioListeners = () => { // 清理所有事件监听器 audioService.clearAllListeners(); + // 监听seek开始事件,立即更新UI + audioService.on('seek_start', (time) => { + // 直接更新显示位置,不检查拖动状态 + nowTime.value = time; + }); + + // 监听seek完成事件 + audioService.on('seek', () => { + try { + const currentSound = sound.value; + if (currentSound) { + // 立即更新显示时间,不进行任何检查 + const currentTime = currentSound.seek() as number; + if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) { + nowTime.value = currentTime; + + // 检查是否需要更新歌词 + const newIndex = getLrcIndex(nowTime.value); + if (newIndex !== nowIndex.value) { + nowIndex.value = newIndex; + if (isElectron && isLyricWindowOpen.value) { + sendLyricToWin(); + } + } + } + } + } catch (error) { + console.error('处理seek事件出错:', error); + } + }); + // 立即更新一次时间和进度(解决初始化时进度条不显示问题) const updateCurrentTimeAndDuration = () => { const currentSound = audioService.getCurrentSound(); diff --git a/src/renderer/layout/components/PlayBar.vue b/src/renderer/layout/components/PlayBar.vue index 79f8ddf..6bed763 100644 --- a/src/renderer/layout/components/PlayBar.vue +++ b/src/renderer/layout/components/PlayBar.vue @@ -28,6 +28,8 @@ :show-tooltip="showSliderTooltip" @mouseenter="showSliderTooltip = true" @mouseleave="showSliderTooltip = false" + @dragstart="handleSliderDragStart" + @dragend="handleSliderDragEnd" >
@@ -199,7 +201,6 @@ import { nowTime, openLyric, playMusic, - sound, textColors } from '@/hooks/MusicHook'; import { useArtist } from '@/hooks/useArtist'; @@ -233,17 +234,47 @@ watch( // 节流版本的 seek 函数 const throttledSeek = useThrottleFn((value: number) => { - if (!sound.value) return; - sound.value.seek(value); + audioService.seek(value); nowTime.value = value; }, 50); // 50ms 的节流延迟 +// 拖动时的临时值,避免频繁更新 nowTime 触发重渲染 +const dragValue = ref(0); + +// 为滑块拖动添加状态跟踪 +const isDragging = ref(false); + // 修改 timeSlider 计算属性 const timeSlider = computed({ - get: () => nowTime.value, - set: throttledSeek + get: () => (isDragging.value ? dragValue.value : nowTime.value), + set: (value) => { + if (isDragging.value) { + // 拖动中只更新临时值,不触发 nowTime 更新和 seek 操作 + dragValue.value = value; + return; + } + + // 点击操作 (非拖动),可以直接 seek + throttledSeek(value); + } }); +// 添加滑块拖动开始和结束事件处理 +const handleSliderDragStart = () => { + isDragging.value = true; + // 初始化拖动值为当前时间 + dragValue.value = nowTime.value; +}; + +const handleSliderDragEnd = () => { + isDragging.value = false; + + // 直接应用最终的拖动值 + audioService.seek(dragValue.value); + nowTime.value = dragValue.value; +}; + +// 格式化提示文本,根据拖动状态显示不同的时间 const formatTooltip = (value: number) => { return `${secondToMinute(value)} / ${secondToMinute(allTime.value)}`; }; @@ -266,9 +297,8 @@ const getVolumeIcon = computed(() => { const volumeSlider = computed({ get: () => audioVolume.value * 100, set: (value) => { - if (!sound.value) return; localStorage.setItem('volume', (value / 100).toString()); - sound.value.volume(value / 100); + audioService.setVolume(value / 100); audioVolume.value = value / 100; } }); diff --git a/src/renderer/services/audioService.ts b/src/renderer/services/audioService.ts index 45aaa77..917d5c7 100644 --- a/src/renderer/services/audioService.ts +++ b/src/renderer/services/audioService.ts @@ -37,6 +37,10 @@ class AudioService { private retryCount = 0; + private seekLock = false; + + private seekDebounceTimer: NodeJS.Timeout | null = null; + constructor() { if ('mediaSession' in navigator) { this.initMediaSession(); @@ -61,21 +65,22 @@ class AudioService { navigator.mediaSession.setActionHandler('seekto', (event) => { if (event.seekTime && this.currentSound) { - this.currentSound.seek(event.seekTime); + // this.currentSound.seek(event.seekTime); + this.seek(event.seekTime); } }); navigator.mediaSession.setActionHandler('seekbackward', (event) => { if (this.currentSound) { const currentTime = this.currentSound.seek() as number; - this.currentSound.seek(currentTime - (event.seekOffset || 10)); + this.seek(currentTime - (event.seekOffset || 10)); } }); navigator.mediaSession.setActionHandler('seekforward', (event) => { if (this.currentSound) { const currentTime = this.currentSound.seek() as number; - this.currentSound.seek(currentTime + (event.seekOffset || 10)); + this.seek(currentTime + (event.seekOffset || 10)); } }); @@ -355,6 +360,11 @@ class AudioService { play(url?: string, track?: SongResult, isPlay: boolean = true): Promise { // 如果没有提供新的 URL 和 track,且当前有音频实例,则继续播放 if (this.currentSound && !url && !track) { + // 如果有进行中的seek操作,等待其完成 + if (this.seekLock && this.seekDebounceTimer) { + clearTimeout(this.seekDebounceTimer); + this.seekLock = false; + } this.currentSound.play(); return Promise.resolve(this.currentSound); } @@ -396,6 +406,11 @@ class AudioService { // 先停止并清理现有的音频实例 if (this.currentSound) { console.log('audioService: 停止并清理现有的音频实例'); + // 确保任何进行中的seek操作被取消 + if (this.seekLock && this.seekDebounceTimer) { + clearTimeout(this.seekDebounceTimer); + this.seekLock = false; + } this.currentSound.stop(); this.currentSound.unload(); this.currentSound = null; @@ -511,6 +526,11 @@ class AudioService { stop() { if (this.currentSound) { try { + // 确保任何进行中的seek操作被取消 + if (this.seekLock && this.seekDebounceTimer) { + clearTimeout(this.seekDebounceTimer); + this.seekLock = false; + } this.currentSound.stop(); this.currentSound.unload(); } catch (error) { @@ -534,14 +554,26 @@ class AudioService { seek(time: number) { if (this.currentSound) { - this.currentSound.seek(time); - this.updateMediaSessionPositionState(); + try { + // 直接执行seek操作,避免任何过滤或判断 + this.currentSound.seek(time); + // 触发seek事件 + this.updateMediaSessionPositionState(); + this.emit('seek', time); + } catch (error) { + console.error('Seek操作失败:', error); + } } } pause() { if (this.currentSound) { try { + // 如果有进行中的seek操作,等待其完成 + if (this.seekLock && this.seekDebounceTimer) { + clearTimeout(this.seekDebounceTimer); + this.seekLock = false; + } this.currentSound.pause(); } catch (error) { console.error('Error pausing audio:', error); diff --git a/src/renderer/store/modules/player.ts b/src/renderer/store/modules/player.ts index 0f75bdd..a72f1f2 100644 --- a/src/renderer/store/modules/player.ts +++ b/src/renderer/store/modules/player.ts @@ -165,13 +165,28 @@ const getSongDetail = async (playMusic: SongResult) => { const preloadNextSong = (nextSongUrl: string) => { try { - if (preloadingSounds.value.length >= 2) { + // 清理多余的预加载实例,确保最多只有2个预加载音频 + while (preloadingSounds.value.length >= 2) { const oldestSound = preloadingSounds.value.shift(); if (oldestSound) { - oldestSound.unload(); + try { + oldestSound.stop(); + oldestSound.unload(); + } catch (e) { + console.error('清理预加载音频实例失败:', e); + } } } + // 检查这个URL是否已经在预加载列表中 + const existingPreload = preloadingSounds.value.find( + (sound) => (sound as any)._src === nextSongUrl + ); + if (existingPreload) { + console.log('该音频已在预加载列表中,跳过:', nextSongUrl); + return existingPreload; + } + const sound = new Howl({ src: [nextSongUrl], html5: true, @@ -187,7 +202,12 @@ const preloadNextSong = (nextSongUrl: string) => { if (index > -1) { preloadingSounds.value.splice(index, 1); } - sound.unload(); + try { + sound.stop(); + sound.unload(); + } catch (e) { + console.error('卸载预加载音频失败:', e); + } }); return sound;