import { reactive } from 'vue' const KEY_VOLUME = 'vp-music-volume' const KEY_PROGRESS = (id: string) => `vp-music-progress-${id}` const KEY_PLAYING = 'vp-music-playing-id' export const globalState = reactive({ audio: null as HTMLAudioElement | null, playingId: '', isPlayed: false, volume: 60, activeInstances: 0, }) // 计算安全音量:0 映射为 0.0001,防止浏览器因“无声”挂起后台 JS 引擎 const getSafeVolume = (vol: number) => { const v = Math.pow(vol / 100, 4) return v <= 0 ? 0.0001 : v } if (typeof window !== 'undefined') { // 恢复持久化音量 const savedVol = localStorage.getItem(KEY_VOLUME) globalState.volume = savedVol !== null ? Number(savedVol) : 60 // 切回标签页校准:若状态为播放但实际被挂起,则唤醒 document.addEventListener('visibilitychange', () => { const audio = globalState.audio if (document.visibilityState === 'visible' && globalState.isPlayed && audio?.paused) { audio.play().catch(() => {}) } }) } /** 保存音量 (LocalStorage) */ export const saveVolume = (vol: number) => { globalState.volume = vol if (globalState.audio) globalState.audio.volume = getSafeVolume(vol) if (typeof window !== 'undefined') localStorage.setItem(KEY_VOLUME, String(vol)) } /** 保存进度 (SessionStorage: 页面关闭即销毁) */ export const saveProgress = (id: string, time: number) => { if (typeof window === 'undefined' || !id) return sessionStorage.setItem(KEY_PROGRESS(id), String(time)) } /** 读取进度 (SessionStorage) */ export const loadProgress = (id: string): number => { if (typeof window === 'undefined') return 0 return Number(sessionStorage.getItem(KEY_PROGRESS(id)) ?? 0) } /** 获取/初始化单例 Audio */ export const getAudio = (): HTMLAudioElement | null => { if (typeof window === 'undefined') return null if (!globalState.audio) { try { const audio = new Audio() audio.onplay = () => { globalState.isPlayed = true } audio.onpause = () => { globalState.isPlayed = false } audio.ontimeupdate = () => { if (globalState.playingId && audio.currentTime > 0) { saveProgress(globalState.playingId, audio.currentTime) } } audio.volume = getSafeVolume(globalState.volume) globalState.audio = audio } catch { return null } } return globalState.audio } /** 更新系统媒体控制中心 (支持后台切歌/暂停) */ const updateMediaSession = (title: string, artist: string, cover?: string) => { if ('mediaSession' in navigator && globalState.audio) { navigator.mediaSession.metadata = new MediaMetadata({ title: title || 'Playing', artist: artist || 'My Blog', artwork: cover ? [{ src: cover, sizes: '512x512', type: 'image/png' }] : [] }) navigator.mediaSession.setActionHandler('play', () => globalState.audio?.play()) navigator.mediaSession.setActionHandler('pause', () => globalState.audio?.pause()) } } /** 执行播放逻辑 */ export const playSong = (id: string, url: string, info: { title?: string, artist?: string, cover?: string }, resume = true) => { const audio = getAudio() if (!audio || !url) return if (globalState.playingId !== id || audio.src !== url) { if (globalState.playingId) saveProgress(globalState.playingId, audio.currentTime) audio.src = url audio.load() audio.currentTime = resume ? loadProgress(id) : 0 } globalState.playingId = id localStorage.setItem(KEY_PLAYING, id) audio.play().then(() => { updateMediaSession(info.title || '', info.artist || '', info.cover) }).catch(e => console.warn('Autoplay blocked:', e)) } /** 立即停止并清理状态 */ export const stopAudioImmediately = () => { if (typeof window === 'undefined') return const audio = globalState.audio if (!audio) return if (globalState.playingId) saveProgress(globalState.playingId, audio.currentTime) audio.pause() globalState.isPlayed = false globalState.playingId = '' if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'none' }