import { computed, nextTick, ref } from 'vue' import { useAudioQuality } from '~/composables/useAudioQuality' import { useLyrics } from '~/composables/useLyrics' import { useAudioPlayer } from '~/composables/useAudioPlayer' // 单例状态 const audioPlayer = ref(null) const isPlaying = ref(false) const progress = ref(0) const currentTime = ref(0) const duration = ref(0) const hasError = ref(false) const coverError = ref(false) const showQualitySettings = ref(false) // 拖拽状态 const isDragging = ref(false) const dragStartX = ref(0) const dragStartProgress = ref(0) const dragProgress = ref(0) // 同步状态 const isSyncingFromGlobal = ref(false) const isLoadingNewSong = ref(false) const isLoadingTrack = ref(false) const progressBarRef = ref(null) const hasUserInteracted = ref(false) // 播放模式: 'off' (单曲播放完退出) | 'order' (顺序播放列表) | 'loopOne' (单曲循环) const playMode = ref<'off' | 'order' | 'loopOne'>('order') // 共享歌词实例 const lyrics = useLyrics() export const useAudioPlayerControl = () => { // 音质 (Composable 使用应在 setup/function 内部) const { getQualityLabel, getQuality, getQualityOptions, saveQuality } = useAudioQuality() // 全局音频播放器 const globalAudioPlayer = useAudioPlayer() // 基本播放控制 const play = async (): Promise => { // 等待音频播放器引用设置完成 let retryCount = 0 const maxRetries = 20 while (!audioPlayer.value && retryCount < maxRetries) { await new Promise((resolve) => setTimeout(resolve, 50)) retryCount++ } if (!audioPlayer.value || hasError.value) { return false } try { // 确保音频已经加载 if (audioPlayer.value.readyState < 2) { await waitForCanPlay(audioPlayer.value) } // 设置音频属性以支持自动播放 audioPlayer.value.autoplay = true audioPlayer.value.preload = 'auto' const playPromise = audioPlayer.value.play() // 处理播放 Promise if (playPromise !== undefined) { await playPromise } return true } catch (error) { // 检查是否是自动播放被阻止的错误 if (error.name === 'NotAllowedError') { console.warn('[AudioPlayerControl] ⚠️ 自动播放被浏览器阻止,需要用户交互') // 不设置 hasError,因为这不是真正的错误 return false } else { console.error('[AudioPlayerControl] ❌ 播放失败:', error) hasError.value = true return false } } } const pause = (): boolean => { if (!audioPlayer.value) { console.warn('[AudioPlayerControl] pause called but audioPlayer is null') return false } try { audioPlayer.value.pause() return true } catch (error) { console.error('暂停失败:', error) return false } } const stop = (): boolean => { if (!audioPlayer.value) return false try { audioPlayer.value.pause() audioPlayer.value.removeAttribute('src') audioPlayer.value.load() // 清理歌词状态 lyrics.clearLyrics() resetState() return true } catch (error) { console.error('停止失败:', error) return false } } const seek = async (timeInSeconds: number): Promise => { if (!audioPlayer.value) return false try { // 如果 duration 为 0,可能元数据还未加载,直接使用传入时间 const maxTime = duration.value > 0 ? duration.value : timeInSeconds + 1000 const targetTime = Math.max(0, Math.min(timeInSeconds, maxTime)) audioPlayer.value.currentTime = targetTime currentTime.value = targetTime if (duration.value > 0) { progress.value = (targetTime / duration.value) * 100 } return true } catch (error) { console.error('跳转失败:', error) return false } } // 抽象后的 seekAndPlay 方法 const seekAndPlay = async (timeInSeconds: number) => { if (!audioPlayer.value) return // 1. 跳转 await seek(timeInSeconds) // 2. 如果暂停则播放 try { await audioPlayer.value.play() } catch (e) { console.warn('Play failed:', e) } // 3. 与全局音频播放器同步状态 globalAudioPlayer.setPosition(timeInSeconds) if (!globalAudioPlayer.getPlayingStatus().value) { const current = globalAudioPlayer.getCurrentSong().value if (current) { globalAudioPlayer.playSong(current) } } } // 等待音频可以播放 const waitForCanPlay = (audio: HTMLAudioElement): Promise => { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { audio.removeEventListener('canplay', onCanPlay) audio.removeEventListener('error', onError) reject(new Error('音频加载超时')) }, 10000) // 10秒超时 const onCanPlay = () => { clearTimeout(timeout) audio.removeEventListener('canplay', onCanPlay) audio.removeEventListener('error', onError) resolve() } const onError = (error: Event) => { clearTimeout(timeout) audio.removeEventListener('canplay', onCanPlay) audio.removeEventListener('error', onError) reject(error) } audio.addEventListener('canplay', onCanPlay) audio.addEventListener('error', onError) }) } // 加载新歌曲 const loadSong = async ( songUrlOrSong: string | any, retryCount: number = 0 ): Promise => { if (!audioPlayer.value) return false stop() isLoadingNewSong.value = true isLoadingTrack.value = true // 开始加载时设置加载状态 hasError.value = false // 立即清空之前的歌词,避免显示上一首歌的歌词 lyrics.clearLyrics() try { let songUrl: string let songInfo: any = null const lyricsPromise: Promise | null = null // 如果传入的是歌曲对象,检查是否有音乐平台信息 if (typeof songUrlOrSong === 'object' && songUrlOrSong !== null) { songInfo = songUrlOrSong // 优先检查是否已有直接的播放URL if (songUrlOrSong.musicUrl) { songUrl = songUrlOrSong.musicUrl // 清理URL中的反引号和空格(特别是网易云备用源) songUrl = songUrl.trim().replace(/`/g, '') console.log('使用已有的播放URL:', songUrl) // 如果有音乐平台信息,加载歌词 if (songUrlOrSong.musicPlatform && songUrlOrSong.musicId) { // lyricsPromise = lyrics.fetchLyrics(songUrlOrSong.musicPlatform, songUrlOrSong.musicId) // 迁移到 useLyricManager 统一管理,避免重复请求 } } else if (songUrlOrSong.musicPlatform && songUrlOrSong.musicId) { // 检查是否是网易云备用源,如果是则不应该调用getMusicUrl if (songUrlOrSong.sourceInfo?.source === 'netease-backup') { throw new Error('网易云备用源歌曲缺少播放URL,请重新获取') } console.log('正在获取歌曲URL:', songUrlOrSong.musicPlatform, songUrlOrSong.musicId) // 检查是否为播客内容 const isPodcast = songUrlOrSong.musicPlatform === 'netease-podcast' || songUrlOrSong.sourceInfo?.type === 'voice' || (songUrlOrSong.sourceInfo?.source === 'netease-backup' && songUrlOrSong.sourceInfo?.type === 'voice') const options = isPodcast ? { unblock: false } : {} songUrl = await getMusicUrl( songUrlOrSong.musicPlatform, songUrlOrSong.musicId, songUrlOrSong.playUrl, options ) if (!songUrl) { throw new Error('无法获取歌曲URL') } // 并行加载歌词(不阻塞音频加载) // lyricsPromise = lyrics.fetchLyrics(songUrlOrSong.musicPlatform, songUrlOrSong.musicId) // 迁移到 useLyricManager 统一管理,避免重复请求 } else { throw new Error('歌曲缺少播放信息(音乐平台ID或直接URL)') } } else if (typeof songUrlOrSong === 'string') { songUrl = songUrlOrSong } else { throw new Error('无效的歌曲参数') } console.log('设置音频源:', songUrl) audioPlayer.value.src = songUrl audioPlayer.value.load() // 等待音频可以播放 await waitForCanPlay(audioPlayer.value) // 等待歌词加载完成(如果有的话) if (lyricsPromise) { try { await lyricsPromise // 歌词加载完成 } catch (lyricsError) { console.warn('歌词加载失败,但继续播放音频:', lyricsError) } } console.log('歌曲加载成功:', songInfo?.title || songUrl) isLoadingNewSong.value = false // 自动开始播放 console.log('尝试自动播放音乐') const playResult = await play() if (!playResult) { console.log('自动播放失败,可能被浏览器阻止,等待用户交互') } else { console.log('自动播放成功') } return true } catch (error) { // 重试逻辑(针对网络抖动等临时错误) if ( retryCount < 2 && !hasError.value && typeof songUrlOrSong === 'object' && songUrlOrSong?.musicPlatform ) { await new Promise((resolve) => setTimeout(resolve, 1000)) if (hasError.value) return false return await loadSong(songUrlOrSong, retryCount + 1) } hasError.value = true isLoadingNewSong.value = false return false } } // 切换播放/暂停 const togglePlay = async (): Promise => { // 标记用户已交互 hasUserInteracted.value = true if (isPlaying.value) { return pause() } else { return await play() } } // 音质切换 const switchQuality = async ( platform: string, musicId: string, qualityValue: string ): Promise => { if (!audioPlayer.value) return false // 保存当前播放进度和状态 const currentTimeBackup = audioPlayer.value.currentTime const wasPlaying = isPlaying.value // 立即暂停 if (wasPlaying) { pause() } try { // 保存音质设置 saveQuality(platform, qualityValue) // 获取新音质的URL const newUrl = await getMusicUrl( platform, musicId, globalAudioPlayer.getCurrentSong().value?.playUrl ) if (!newUrl) { throw new Error('获取新音质URL失败') } // 加载新音频 const loadSuccess = await loadSong(newUrl) if (!loadSuccess) { throw new Error('加载新音质失败') } // 恢复播放进度 if (currentTimeBackup > 0) { await seek(currentTimeBackup) } // 如果之前在播放,恢复播放状态 if (wasPlaying) { // 先尝试播放,再设置进度,避免自动播放限制 try { await play() if (currentTimeBackup > 0) { await nextTick() await seek(currentTimeBackup) } } catch (playError) { console.warn('恢复播放失败,可能需要用户手动播放:', playError) } } return true } catch (error) { console.error('切换音质失败:', error) // 如果切换失败,尝试恢复之前的播放状态 if (wasPlaying && !audioPlayer.value.error) { try { await play() } catch (playError) { console.error('恢复播放失败:', playError) } } return false } } // 动态获取音乐URL(委托到统一逻辑) const getMusicUrl = async ( platform: string, musicId: string, playUrl: string | null = null, options?: { unblock?: boolean quality?: number } ): Promise => { try { const { getMusicUrl: coreGetMusicUrl } = await import('~/utils/musicUrl') const url = await coreGetMusicUrl(platform, musicId, playUrl ?? undefined, options) return url } catch (error) { console.error('获取音乐URL错误:', error) if (error instanceof TypeError && (error as any).message?.includes('fetch')) { throw new Error('网络连接失败,请检查网络连接') } else if ((error as any).name === 'AbortError') { throw new Error('请求超时,请稍后重试') } throw error } } // 进度条拖拽 const startDrag = (event: MouseEvent, progressBar: HTMLElement) => { if (event.button !== 0) return if (!audioPlayer.value || !progressBar) { return } isDragging.value = true dragStartX.value = event.clientX dragStartProgress.value = progress.value const onDrag = (e: MouseEvent) => { if (!isDragging.value || !progressBar) return const rect = progressBar.getBoundingClientRect() const newX = e.clientX - rect.left const percentage = Math.max(0, Math.min(100, (newX / rect.width) * 100)) progress.value = percentage // 拖拽时只更新进度条显示,不实时更新音频位置 if (duration.value) { const newTime = (percentage / 100) * duration.value currentTime.value = newTime } } const endDrag = () => { isDragging.value = false document.removeEventListener('mousemove', onDrag) document.removeEventListener('mouseup', endDrag) // 拖拽结束时才更新音频位置 if (audioPlayer.value && duration.value) { const newTime = (progress.value / 100) * duration.value audioPlayer.value.currentTime = newTime currentTime.value = newTime } } document.addEventListener('mousemove', onDrag) document.addEventListener('mouseup', endDrag) event.preventDefault() } // 触摸拖拽 const startTouchDrag = (event: TouchEvent, progressBar: HTMLElement) => { if (event.touches.length !== 1) return if (!audioPlayer.value || !progressBar) { return } // 阻止默认行为,防止页面滚动 event.preventDefault() event.stopPropagation() isDragging.value = true const touch = event.touches[0] dragStartX.value = touch.clientX dragStartProgress.value = progress.value const onTouchDrag = (e: TouchEvent) => { if (!isDragging.value || !progressBar || e.touches.length !== 1) return // 阻止默认行为和事件冒泡 e.preventDefault() e.stopPropagation() const touch = e.touches[0] const rect = progressBar.getBoundingClientRect() const newX = touch.clientX - rect.left const percentage = Math.max(0, Math.min(100, (newX / rect.width) * 100)) progress.value = percentage // 拖拽时只更新进度条显示,不实时更新音频位置 if (duration.value) { const newTime = (percentage / 100) * duration.value currentTime.value = newTime } } const endTouchDrag = (e: TouchEvent) => { e.preventDefault() e.stopPropagation() isDragging.value = false document.removeEventListener('touchmove', onTouchDrag) document.removeEventListener('touchend', endTouchDrag) // 触摸结束时才更新音频位置 if (audioPlayer.value && duration.value) { const newTime = (progress.value / 100) * duration.value audioPlayer.value.currentTime = newTime currentTime.value = newTime } } document.addEventListener('touchmove', onTouchDrag, { passive: false }) document.addEventListener('touchend', endTouchDrag, { passive: false }) } // 进度条点击跳转 const seekToPosition = (event: MouseEvent) => { if (!audioPlayer.value || isDragging.value) return const progressBar = event.currentTarget as HTMLElement if (!progressBar) { return } const clickPosition = event.offsetX const barWidth = progressBar.clientWidth const seekPercentage = clickPosition / barWidth const newTime = seekPercentage * duration.value seek(newTime) } // 音质相关计算属性 const currentQualityText = computed(() => { return (platform: string) => { if (!platform) return '音质' const quality = getQuality(platform) const label = getQualityLabel(platform, quality) return label.replace(/音质|音乐/, '').trim() || '音质' } }) const currentPlatformOptions = computed(() => { return (platform: string) => { if (!platform) return [] return getQualityOptions(platform) } }) const isCurrentQuality = (platform: string, qualityValue: string) => { if (!platform) return false return getQuality(platform) === qualityValue } // 格式化时间 const formatTime = (seconds: number) => { if (!seconds || isNaN(seconds)) return '0:00' const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } // 音频事件处理 let hasPrefetchedForCurrentSong = false const onTimeUpdate = (currentTimeValue: number) => { if (isDragging.value) return currentTime.value = currentTimeValue if (duration.value > 0) { progress.value = (currentTimeValue / duration.value) * 100 // 距离结束还有15秒时,提前预加载下一首 if (!hasPrefetchedForCurrentSong && duration.value - currentTimeValue <= 15) { hasPrefetchedForCurrentSong = true globalAudioPlayer.prefetchNextSong() } } // 更新歌词时间 const timeInMs = currentTimeValue * 1000 lyrics.updateCurrentLyricIndex(timeInMs) // 转换为毫秒 } const onLoaded = (durationValue: number) => { duration.value = durationValue hasError.value = false hasPrefetchedForCurrentSong = false // 重置预加载标志 } const onError = (error: any) => { console.error('音频播放错误:', error) // 获取更详细的错误信息 let errorMessage = '音频播放失败' if (audioPlayer.value && audioPlayer.value.error) { const mediaError = audioPlayer.value.error switch (mediaError.code) { case MediaError.MEDIA_ERR_ABORTED: errorMessage = '音频加载被中止' break case MediaError.MEDIA_ERR_NETWORK: errorMessage = '网络错误,无法加载音频' break case MediaError.MEDIA_ERR_DECODE: errorMessage = '音频解码失败,格式不支持' break case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: errorMessage = '音频源不支持或无效' break default: errorMessage = `音频播放错误 (代码: ${mediaError.code})` } console.error(`详细错误信息: ${errorMessage}`, { code: mediaError.code, message: mediaError.message, src: audioPlayer.value.src }) } hasError.value = true isPlaying.value = false // 显示用户友好的错误提示 if (window.$showNotification) { window.$showNotification(errorMessage, 'error') } } const onPlay = () => { isPlaying.value = true hasError.value = false isLoadingTrack.value = false // 音频开始播放时立即清除加载状态 } const onPause = () => { isPlaying.value = false } const onEnded = () => { // 根据播放模式处理播放结束事件 if (playMode.value === 'loopOne') { // 单曲循环:重新播放当前歌曲 if (audioPlayer.value) { audioPlayer.value.currentTime = 0 // 使用 Promise 处理播放,避免自动播放策略问题 const playPromise = audioPlayer.value.play() if (playPromise !== undefined) { playPromise.catch((error) => { console.error('单曲循环播放失败:', error) // 如果自动播放失败,更新播放状态 isPlaying.value = false }) } } } else if (playMode.value === 'order') { // 顺序播放:播放下一首 // 如果没有下一首(列表结束),useAudioPlayer.playNext 会返回 false,此时自然停止 const hasNext = globalAudioPlayer.hasNext.value if (hasNext) { globalAudioPlayer.playNext() } else { // 列表播放结束,停止播放 isPlaying.value = false progress.value = 0 currentTime.value = 0 } } else { // 默认/播放完退出 (off):停止播放 isPlaying.value = false progress.value = 0 currentTime.value = 0 } } const onLoadStart = () => { hasError.value = false } const onCanPlay = () => { hasError.value = false } // 设置进度条引用 const setProgressBarRef = (element: HTMLElement | null) => { progressBarRef.value = element } // 设置音频播放器引用 const setAudioPlayerRef = (element: HTMLAudioElement | null) => { audioPlayer.value = element } // 清理资源 const cleanup = () => { if (audioPlayer.value) { // 先暂停播放 audioPlayer.value.pause() // 不设置空的 src,避免触发 MEDIA_ERR_SRC_NOT_SUPPORTED 错误 // 只是暂停播放即可,让组件自然销毁 } resetState() } // 重置状态 const resetState = () => { isPlaying.value = false progress.value = 0 currentTime.value = 0 duration.value = 0 hasError.value = false coverError.value = false isDragging.value = false isSyncingFromGlobal.value = false isLoadingNewSong.value = false isLoadingTrack.value = false } // 强制更新位置(用于鸿蒙侧同步) const forceUpdatePosition = (timeInSeconds: number) => { // 如果正在拖拽,不要更新位置 if (isDragging.value) { return false } if (!audioPlayer.value) { return false } try { // 设置同步标志,防止触发其他事件 isSyncingFromGlobal.value = true // 如果有duration,使用它来限制范围;否则直接使用传入的时间 const targetTime = duration.value > 0 ? Math.max(0, Math.min(timeInSeconds, duration.value)) : Math.max(0, timeInSeconds) // 更新音频播放器位置 audioPlayer.value.currentTime = targetTime // 强制更新UI状态 currentTime.value = targetTime // 只有在duration存在时才计算进度百分比 if (duration.value > 0) { progress.value = (targetTime / duration.value) * 100 } // 更新歌词时间 const timeInMs = targetTime * 1000 lyrics.updateCurrentLyricIndex(timeInMs) // 使用nextTick确保DOM更新 nextTick(() => { isSyncingFromGlobal.value = false }) return true } catch (error) { isSyncingFromGlobal.value = false return false } } // 设置播放模式 const setPlayMode = (mode: 'off' | 'order' | 'loopOne') => { if (['off', 'order', 'loopOne'].includes(mode)) { playMode.value = mode return true } return false } return { // 状态 audioPlayer, isPlaying, progress, currentTime, duration, hasError, coverError, showQualitySettings, isDragging, isSyncingFromGlobal, isLoadingNewSong, isLoadingTrack, progressBarRef, hasUserInteracted, playMode, // 暴露播放模式 // 基本控制 play, setPlayMode, // 暴露设置播放模式方法 pause, stop, seek, seekAndPlay, // 新增 togglePlay, loadSong, forceUpdatePosition, // 音频事件处理 onTimeUpdate, onLoaded, onError, onPlay, onPause, onEnded, onLoadStart, onCanPlay, setProgressBarRef, setAudioPlayerRef, // 音质控制 switchQuality, currentQualityText, currentPlatformOptions, isCurrentQuality, // 拖拽控制 startDrag, startTouchDrag, seekToPosition, // 工具函数 formatTime, resetState, waitForCanPlay, cleanup, // 歌词功能 lyrics } }