diff --git a/src/components/AlbumDetailDialog.vue b/src/components/AlbumDetailDialog.vue index 7fc7d9e..61a6fcf 100644 --- a/src/components/AlbumDetailDialog.vue +++ b/src/components/AlbumDetailDialog.vue @@ -9,6 +9,7 @@ import { gsap } from 'gsap' import apis from '../apis' import { artistsOrganize } from '../utils' import { usePlayQueueStore } from '../stores/usePlayQueueStore' +import { usePlayState } from '../stores/usePlayState' import TrackItem from './TrackItem.vue' import LoadingIndicator from '../assets/icons/loadingindicator.vue' import { debugUI } from '../utils/debug' @@ -138,6 +139,7 @@ watch( ) const playQueue = usePlayQueueStore() +const playState = usePlayState() async function playTheAlbum(from: number = 0) { let newQueue = [] @@ -148,7 +150,7 @@ async function playTheAlbum(from: number = 0) { }) } await playQueue.replaceQueue(newQueue) - await playQueue.togglePlay(true) + await playState.togglePlay(true) } function shuffle() { diff --git a/src/components/PlayerWebAudio.vue b/src/components/PlayerWebAudio.vue index fb9845a..129264a 100644 --- a/src/components/PlayerWebAudio.vue +++ b/src/components/PlayerWebAudio.vue @@ -1,275 +1,180 @@ - \ No newline at end of file diff --git a/src/stores/usePlayQueueStore.ts b/src/stores/usePlayQueueStore.ts index dd55055..bbef410 100644 --- a/src/stores/usePlayQueueStore.ts +++ b/src/stores/usePlayQueueStore.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { debugStore } from '../utils/debug' +import apis from '../apis' export const usePlayQueueStore = defineStore('queue', () => { // 内部状态 @@ -10,8 +11,6 @@ export const usePlayQueueStore = defineStore('queue', () => { const queueReplaceLock = ref(false) const currentPlaying = ref(0) // 当前播放指针,指针在 queueOrder 中寻址(无论是否开启了随机播放) const queueOrder = ref([]) // 播放队列顺序 - const isPlaying = ref(false) - const playProgress = ref(0) // 当前曲目的播放时间指针 // 暴露给外部的响应式只读引用 const queueState = computed(() => @@ -29,18 +28,27 @@ export const usePlayQueueStore = defineStore('queue', () => { return queue.value[actualIndex] || null }) - // 获取当前播放时间 - const playProgressState = computed(() => playProgress.value) + // 获取上一曲目 + const previousTrack = computed(() => { + const actualIndex = queueOrder.value[currentPlaying.value - 1] + return queue.value[actualIndex] || null + }) - // 获取当前是否正在播放 - const playingState = computed(() => isPlaying.value) + // 获取下一曲目 + const nextTrack = computed(() => { + const actualIndex = queueOrder.value[currentPlaying.value + 1] + return queue.value[actualIndex] || null + }) /************ * 播放队列相关 ***********/ // 使用新队列替换老队列 // 队列替换锁开启时启用确认,确认后重置该锁 - async function replaceQueue(newQueue: QueueItem[]) { + async function replaceQueue(newQueue: { + song: Song, + album?: Album + }[]) { if (queueReplaceLock.value) { if ( !confirm( @@ -53,13 +61,32 @@ export const usePlayQueueStore = defineStore('queue', () => { queueReplaceLock.value = false } - // 将新队列替换已有队列 - queue.value = newQueue - - // 初始化播放顺序 - queueOrder.value = Array.from({ length: newQueue.length }, (_, i) => i) + // 以空队列向外部监听器回报队列已被修改 + queue.value = [] + queueOrder.value = [] currentPlaying.value = 0 + // 获取最新资源地址 + let newQueueWithUrl: QueueItem[] = [] + + for (const track of newQueue) { + const res = await apis.getSong(track.song.cid) + newQueueWithUrl[newQueueWithUrl.length] = { + song: track.song, + album: track.album, + sourceUrl: res.sourceUrl ?? "", + lyricUrl: res.lyricUrl + } + } + + debugStore(newQueueWithUrl) + + // 将新队列替换已有队列 + queue.value = newQueueWithUrl + + // 正式初始化播放顺序 + queueOrder.value = Array.from({ length: newQueue.length }, (_, i) => i) + // 关闭随机播放和循环(外部可在此方法执行完毕后再更新播放模式) isShuffle.value = false loopingMode.value = 'off' @@ -68,12 +95,6 @@ export const usePlayQueueStore = defineStore('queue', () => { /*********** * 播放控制相关 **********/ - // 控制播放 - const togglePlay = (turnTo?: boolean) => { - const newPlayState = turnTo ?? !isPlaying.value - if (newPlayState === isPlaying.value) return - isPlaying.value = newPlayState - } // 跳转至队列的某首歌曲 const toggleQueuePlay = (turnTo: number) => { @@ -90,15 +111,10 @@ export const usePlayQueueStore = defineStore('queue', () => { // 通常为当前曲目播放完毕,需要通过循环模式判断应该重置进度或队列指针 +1 const continueToNext = () => { debugStore(loopingMode.value) - // TODO: 需要留意 progress seeking 相关 - if (loopingMode.value === 'single') playProgress.value = 0 - else currentPlaying.value = currentPlaying.value + 1 - } - - // 回报播放进度 - const reportPlayProgress = (progress: number) => { - debugStore(`进度更新回报: ${progress}`) - playProgress.value = progress + // 注意:单曲循环时的进度重置需要在播放状态管理中处理 + if (loopingMode.value !== 'single') { + currentPlaying.value = currentPlaying.value + 1 + } } /************ @@ -178,17 +194,15 @@ export const usePlayQueueStore = defineStore('queue', () => { loopMode: loopModeState, currentTrack, currentIndex: currentPlaying, - isPlaying: playingState, - playProgress: playProgressState, + previousTrack, + nextTrack, // 修改方法 replaceQueue, toggleShuffle, toggleLoop, - togglePlay, toggleQueuePlay, skipToNext, continueToNext, - reportPlayProgress, } }) diff --git a/src/stores/usePlayState.ts b/src/stores/usePlayState.ts new file mode 100644 index 0000000..10446eb --- /dev/null +++ b/src/stores/usePlayState.ts @@ -0,0 +1,203 @@ +import { defineStore } from 'pinia' +import { ref, computed, watch } from 'vue' +import { debugStore } from '../utils/debug' +import artistsOrganize from '../utils/artistsOrganize' + +export const usePlayState = defineStore('playState', () => { + // 播放状态 + const isPlaying = ref(false) + const playProgress = ref(0) // 播放进度 + const currentTrackDuration = ref(0) // 曲目总时长 + const currentTrack = ref(null) // 当前播放的曲目 + const mediaSessionInitialized = ref(false) + + // 外显播放状态方法 + const playingState = computed(() => isPlaying.value) + const playProgressState = computed(() => playProgress.value) + const trackDurationState = computed(() => currentTrackDuration.value) + + // 回报目前播放进度百分比 + const playProgressPercent = computed(() => { + if (currentTrackDuration.value === 0) return 0 + return Math.min(playProgress.value / currentTrackDuration.value, 1) + }) + + // 回报剩余时间 + const remainingTime = computed(() => { + return Math.max(currentTrackDuration.value - playProgress.value, 0) + }) + + /*********** + * 修改状态 + **********/ + // 触发播放 + const togglePlay = (turnTo?: boolean) => { + const newPlayState = turnTo ?? !isPlaying.value + if (newPlayState === isPlaying.value) return + isPlaying.value = newPlayState + debugStore(`播放状态更新: ${newPlayState}`) + } + + // 回报播放位置 + const reportPlayProgress = (progress: number) => { + debugStore(`播放位置回报: ${progress}`) + playProgress.value = progress + } + + // 回报曲目进度 + const setCurrentTrackDuration = (duration: number) => { + debugStore(`曲目进度回报: ${duration}`) + currentTrackDuration.value = duration + } + + // 重置播放进度 + const resetProgress = () => { + debugStore('重置播放进度') + playProgress.value = 0 + } + + // 用户触发进度条跳转 + const seekTo = (time: number) => { + const clampedTime = Math.max(0, Math.min(time, currentTrackDuration.value)) + debugStore(`进度条跳转: ${clampedTime}`) + playProgress.value = clampedTime + } + + /*********** + * 媒体会话管理 + **********/ + // 设置当前播放曲目 + const setCurrentTrack = (track: QueueItem | null) => { + debugStore('设置当前曲目:', track?.song.name || 'null') + currentTrack.value = track + if (track) { + updateMediaSession(track) + } + } + + // 初始化媒体会话处理器 + const setupMediaSessionHandlers = () => { + if (!('mediaSession' in navigator) || mediaSessionInitialized.value) return + + debugStore('设置媒体会话处理器') + + navigator.mediaSession.setActionHandler('play', () => { + debugStore('媒体会话: 播放') + togglePlay(true) + }) + + navigator.mediaSession.setActionHandler('pause', () => { + debugStore('媒体会话: 暂停') + togglePlay(false) + }) + + // 上一首和下一首需要从外部传入回调 + mediaSessionInitialized.value = true + } + + // 设置上一首/下一首处理器 + const setTrackNavigationHandlers = ( + previousHandler: () => void, + nextHandler: () => void, + ) => { + if (!('mediaSession' in navigator)) return + + navigator.mediaSession.setActionHandler('previoustrack', () => { + debugStore('媒体会话: 上一首') + previousHandler() + }) + + navigator.mediaSession.setActionHandler('nexttrack', () => { + debugStore('媒体会话: 下一首') + nextHandler() + }) + } + + // 更新媒体会话信息 + const updateMediaSession = (track: QueueItem) => { + if (!('mediaSession' in navigator)) return + + debugStore('更新媒体会话:', track.song.name) + + try { + navigator.mediaSession.metadata = new MediaMetadata({ + title: track.song.name, + artist: artistsOrganize(track.song.artists ?? []), + album: track.album?.name, + artwork: [ + { + src: track.album?.coverUrl ?? '', + sizes: '500x500', + type: 'image/png', + }, + ], + }) + } catch (error) { + console.error('更新媒体会话元数据失败:', error) + } + } + + // 更新媒体会话播放状态 + const updateMediaSessionPlaybackState = () => { + if (!('mediaSession' in navigator)) return + + navigator.mediaSession.playbackState = isPlaying.value + ? 'playing' + : 'paused' + debugStore('媒体会话状态更新:', navigator.mediaSession.playbackState) + } + + // 更新媒体会话位置信息 + const updateMediaSessionPosition = () => { + if ( + !('mediaSession' in navigator) || + !('setPositionState' in navigator.mediaSession) + ) + return + + try { + navigator.mediaSession.setPositionState({ + duration: currentTrackDuration.value || 0, + playbackRate: 1.0, + position: playProgress.value, + }) + } catch (error) { + debugStore('媒体会话位置更新失败:', error) + } + } + + // 监听播放状态变化,自动更新媒体会话 + watch(isPlaying, () => { + updateMediaSessionPlaybackState() + }) + + // 监听播放进度变化,定期更新位置信息 + watch(playProgress, () => { + updateMediaSessionPosition() + }) + + return { + // 状态读取 + isPlaying: playingState, + playProgress: playProgressState, + trackDuration: trackDurationState, + playProgressPercent, + remainingTime, + currentTrack: computed(() => currentTrack.value), + + // 修改方法 + togglePlay, + reportPlayProgress, + setCurrentTrackDuration, + resetProgress, + seekTo, + + // 媒体会话方法 + setCurrentTrack, + setupMediaSessionHandlers, + setTrackNavigationHandlers, + updateMediaSession, + updateMediaSessionPlaybackState, + updateMediaSessionPosition, + } +}) diff --git a/src/utils/webAudioPlayer.ts b/src/utils/webAudioPlayer.ts index ee6ac2e..e3039ba 100644 --- a/src/utils/webAudioPlayer.ts +++ b/src/utils/webAudioPlayer.ts @@ -1,415 +1,298 @@ -interface AudioTrack { - url: string - buffer: AudioBuffer | null - source: AudioBufferSourceNode | null - gainNode: GainNode | null +class SimpleAudioPlayer { + context: AudioContext + currentSource: AudioBufferSourceNode | null + audioBuffer: AudioBuffer | null + playing: boolean + startTime: number + pauseTime: number duration: number - metadata?: any -} - -interface PlaybackState { - isPlaying: boolean - currentIndex: number - pausedAt: number - pausedOffset: number -} - -export class WebAudioGaplessPlayer { - private context: AudioContext - private masterGain: GainNode - private tracks: AudioTrack[] = [] - private state: PlaybackState = { - isPlaying: false, - currentIndex: 0, - pausedAt: 0, - pausedOffset: 0, - } - private bufferCache: Map = new Map() - private onTrackEndCallbacks: ((index: number) => void)[] = [] - private onTrackStartCallbacks: ((index: number) => void)[] = [] - private preloadAhead = 2 - private maxCacheSize = 5 + dummyAudio: HTMLAudioElement constructor() { - this.context = new ( - window.AudioContext || (window as any).webkitAudioContext - )() - this.masterGain = this.context.createGain() - this.masterGain.connect(this.context.destination) + this.context = new window.AudioContext() + this.currentSource = null + this.audioBuffer = null + this.playing = false + this.startTime = 0 + this.pauseTime = 0 + this.duration = 0 + + // 创建一个隐藏的 HTML Audio 元素来帮助同步媒体会话状态 + this.dummyAudio = new Audio() + this.dummyAudio.style.display = 'none' + this.dummyAudio.loop = true + this.dummyAudio.volume = 0.001 // 极小音量 + // 使用一个很短的静音音频文件,或者生成一个 + this.createSilentAudioBlob() + + document.body.appendChild(this.dummyAudio) + + this.initMediaSession() } - /** - * Load and decode audio from URL - */ - private async loadAudio(url: string): Promise { - if (this.bufferCache.has(url)) { - return this.bufferCache.get(url)! + createSilentAudioBlob() { + // 创建一个1秒的静音WAV文件 + const sampleRate = 44100 + const channels = 1 + const length = sampleRate * 1 // 1秒 + + const arrayBuffer = new ArrayBuffer(44 + length * 2) + const view = new DataView(arrayBuffer) + + // WAV 文件头 + const writeString = (offset: number, string: string) => { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)) + } } + + writeString(0, 'RIFF') + view.setUint32(4, 36 + length * 2, true) + writeString(8, 'WAVE') + writeString(12, 'fmt ') + view.setUint32(16, 16, true) + view.setUint16(20, 1, true) + view.setUint16(22, channels, true) + view.setUint32(24, sampleRate, true) + view.setUint32(28, sampleRate * 2, true) + view.setUint16(32, 2, true) + view.setUint16(34, 16, true) + writeString(36, 'data') + view.setUint32(40, length * 2, true) + + // 静音数据(全零) + for (let i = 0; i < length; i++) { + view.setInt16(44 + i * 2, 0, true) + } + + const blob = new Blob([arrayBuffer], { type: 'audio/wav' }) + this.dummyAudio.src = URL.createObjectURL(blob) + } + initMediaSession() { + if ('mediaSession' in navigator) { + navigator.mediaSession.setActionHandler('play', () => { + console.log('Media session: play requested') + this.play() + }) + navigator.mediaSession.setActionHandler('pause', () => { + console.log('Media session: pause requested') + this.pause() + }) + navigator.mediaSession.setActionHandler('stop', () => { + console.log('Media session: stop requested') + this.stop() + }) + } + } + + async loadResource() { try { - const response = await fetch(url) - if (!response.ok) { - throw new Error(`Failed to fetch audio: ${response.status}`) + // 如果已经加载过,直接播放 + if (this.audioBuffer) { + this.play() + return } + // 加载音频 + const response = await fetch( + 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/858/outfoxing.mp3' + ) const arrayBuffer = await response.arrayBuffer() - const audioBuffer = await this.context.decodeAudioData(arrayBuffer) + this.audioBuffer = await this.context.decodeAudioData(arrayBuffer) + this.duration = this.audioBuffer.duration - this.bufferCache.set(url, audioBuffer) - this.cleanupCache() - - return audioBuffer - } catch (error) { - console.error('Failed to load audio:', error) - throw error - } - } - - /** - * Clean up cache - */ - private cleanupCache(): void { - if (this.bufferCache.size <= this.maxCacheSize) return - - const currentUrls = new Set( - this.tracks - .slice( - Math.max(0, this.state.currentIndex - 1), - this.state.currentIndex + this.preloadAhead + 1, - ) - .map((t) => t.url), - ) - - for (const [url] of this.bufferCache) { - if (!currentUrls.has(url) && this.bufferCache.size > this.maxCacheSize) { - this.bufferCache.delete(url) + // 设置媒体元数据 + if ('mediaSession' in navigator) { + navigator.mediaSession.metadata = new MediaMetadata({ + title: 'Outfoxing the Fox', + artist: 'Kevin MacLeod', + album: 'YouTube Audio Library', + }) } - } - } - /** - * Add a track to the queue - */ - async addTrack(url: string, metadata?: any): Promise { - const track: AudioTrack = { - url, - buffer: null, - source: null, - gainNode: null, - duration: 0, - metadata, - } - - this.tracks.push(track) - const index = this.tracks.length - 1 - - // Preload if within range - if (this.shouldPreload(index)) { - await this.preloadTrack(index) - } - - return index - } - - /** - * Check if a track should be preloaded - */ - private shouldPreload(index: number): boolean { - const distance = index - this.state.currentIndex - return distance >= 0 && distance <= this.preloadAhead - } - - /** - * Preload a track - */ - private async preloadTrack(index: number): Promise { - const track = this.tracks[index] - if (!track || track.buffer) return - - try { - track.buffer = await this.loadAudio(track.url) - track.duration = track.buffer.duration + // 开始播放 + this.play() } catch (error) { - console.error(`Failed to preload track ${index}:`, error) + console.error('播放失败:', error) } } - /** - * Stop current track if playing - */ - private stopCurrentTrack(): void { - const currentTrack = this.tracks[this.state.currentIndex] - if (currentTrack?.source) { - try { - currentTrack.source.stop() - } catch (e) { - // Source might have already ended - } - currentTrack.source = null - currentTrack.gainNode = null + async play() { + if (!this.audioBuffer) { + this.loadResource() + return } - } - /** - * Play a specific track - */ - async playTrack(index: number): Promise { - if (index < 0 || index >= this.tracks.length) return + if (this.playing) { + console.log('Already playing, ignoring play request') + return + } - // Stop any currently playing track - this.stopCurrentTrack() + console.log('Starting playback from position:', this.pauseTime) - // Resume context if suspended + // 恢复 AudioContext(如果被暂停) if (this.context.state === 'suspended') { await this.context.resume() } - // Ensure track is loaded - await this.preloadTrack(index) + // 开始播放隐藏的 audio 元素 + try { + await this.dummyAudio.play() + } catch (e) { + console.log('Dummy audio play failed (expected):', e) + } - const track = this.tracks[index] - if (!track?.buffer) { - console.error(`Track ${index} not loaded`) + // 创建新的源节点 + this.currentSource = this.context.createBufferSource() + this.currentSource.buffer = this.audioBuffer + this.currentSource.connect(this.context.destination) + + // 从暂停位置开始播放 + const offset = this.pauseTime + this.currentSource.start(0, offset) + + this.startTime = this.context.currentTime - offset + this.playing = true + + // 播放结束处理 - 只在自然结束时触发 + this.currentSource.onended = () => { + console.log('Audio naturally ended') + // 检查是否真的播放到了结尾 + const currentTime = this.getCurrentTime() + if (currentTime >= this.duration - 0.1) { // 允许小误差 + console.log('Natural end of track') + this.stop() + } else { + console.log('Audio ended prematurely (likely paused), current time:', currentTime) + // 这是由于暂停导致的结束,不做任何处理 + } + } + + // 更新媒体会话状态 + this.updateMediaSessionState() + } + + pause() { + console.log('Pause requested, current state - playing:', this.playing, 'hasSource:', !!this.currentSource) + + // 暂停隐藏的 audio 元素 + this.dummyAudio.pause() + + if (!this.playing) { + console.log('Already paused, but updating media session state') + // 即使已经暂停,也要确保媒体会话状态正确 + this.updateMediaSessionState() return } - // Update state - this.state.currentIndex = index - this.state.isPlaying = true - this.state.pausedOffset = 0 - - // Create audio nodes - const source = this.context.createBufferSource() - source.buffer = track.buffer - - const gainNode = this.context.createGain() - source.connect(gainNode) - gainNode.connect(this.masterGain) - - // Set up callbacks - source.onended = () => { - this.handleTrackEnded(index) + if (!this.currentSource) { + console.log('No current source, but updating media session state') + this.updateMediaSessionState() + return } - // Store references - track.source = source - track.gainNode = gainNode + console.log('Pausing playback at position:', this.getCurrentTime()) - // Start playing immediately - source.start(this.context.currentTime) + // 计算当前播放位置 + this.pauseTime = this.getCurrentTime() + + // 移除 onended 事件处理器,避免干扰 + this.currentSource.onended = null + + // 停止当前源 + this.currentSource.stop() + this.currentSource = null + this.playing = false - // Notify track started - this.onTrackStartCallbacks.forEach((cb) => cb(index)) + // 更新媒体会话状态 + this.updateMediaSessionState() + } - // Preload next tracks - for (let i = 1; i <= this.preloadAhead; i++) { - const nextIndex = index + i - if (nextIndex < this.tracks.length) { - this.preloadTrack(nextIndex).catch(console.error) + stop() { + console.log('Stopping playback') + + // 停止隐藏的 audio 元素 + this.dummyAudio.pause() + this.dummyAudio.currentTime = 0 + + if (this.currentSource) { + this.currentSource.stop() + this.currentSource = null + } + + this.playing = false + this.pauseTime = 0 + this.startTime = 0 + + // 更新媒体会话状态 + this.updateMediaSessionState() + } + + togglePlay() { + if (this.playing) { + this.pause() + } else { + this.play() + } + } + + getCurrentTime(): number { + if (this.playing && this.currentSource) { + return Math.min(this.context.currentTime - this.startTime, this.duration) + } + return this.pauseTime + } + + updateMediaSessionState() { + if ('mediaSession' in navigator) { + let state = 'none' + if (this.playing) { + state = 'playing' + } else if (this.audioBuffer) { + // 只要有音频缓冲区就应该是暂停状态 + state = 'paused' + } + + console.log('Updating media session state to:', state, '(playing:', this.playing, ', hasBuffer:', !!this.audioBuffer, ')') + + // 强制设置状态 + try { + navigator.mediaSession.playbackState = state as any + + // 更新位置信息 + if ('setPositionState' in navigator.mediaSession && this.duration > 0) { + navigator.mediaSession.setPositionState({ + duration: this.duration, + playbackRate: 1.0, + position: this.getCurrentTime() + }) + } + } catch (error) { + console.error('Error updating media session:', error) } } } - /** - * Handle track ended event - */ - private handleTrackEnded(index: number): void { - const track = this.tracks[index] - if (track) { - track.source = null - track.gainNode = null - } - - // Only notify if this is still the current track - if (index === this.state.currentIndex) { - this.onTrackEndCallbacks.forEach((cb) => cb(index)) - } - } - - /** - * Start playing from a specific index - */ - async play(startIndex = 0): Promise { - await this.playTrack(startIndex) - } - - /** - * Play next track - */ - async playNext(): Promise { - const nextIndex = this.state.currentIndex + 1 - if (nextIndex < this.tracks.length) { - await this.playTrack(nextIndex) - } - } - - /** - * Pause playback - */ - pause(): void { - if (!this.state.isPlaying) return - - this.state.pausedAt = this.context.currentTime - this.state.isPlaying = false - - // Calculate paused offset - const position = this.getCurrentPosition() - if (position) { - this.state.pausedOffset = position.trackTime - } - - // Stop current track - this.stopCurrentTrack() - - // Suspend context to save resources - this.context.suspend() - } - - /** - * Resume playback - */ - async resume(): Promise { - if (this.state.isPlaying) return - - await this.context.resume() - - const currentTrack = this.tracks[this.state.currentIndex] - if (!currentTrack?.buffer) return - - this.state.isPlaying = true - - // Create new source for resume - const source = this.context.createBufferSource() - source.buffer = currentTrack.buffer - - const gainNode = this.context.createGain() - source.connect(gainNode) - gainNode.connect(this.masterGain) - - // Calculate remaining duration - const remainingDuration = currentTrack.duration - this.state.pausedOffset - - // Resume from offset - source.start(this.context.currentTime, this.state.pausedOffset, remainingDuration) - - source.onended = () => this.handleTrackEnded(this.state.currentIndex) - - // Store references - currentTrack.source = source - currentTrack.gainNode = gainNode - - this.state.pausedOffset = 0 - } - - /** - * Stop playback - */ - stop(): void { - this.pause() - this.state.currentIndex = 0 - this.state.pausedOffset = 0 - } - - /** - * Seek to a specific track - */ - async seekToTrack(index: number): Promise { - if (index < 0 || index >= this.tracks.length) return - await this.playTrack(index) - } - - /** - * Get current playback position - */ - getCurrentPosition(): { - trackIndex: number - trackTime: number - totalTime: number - } | null { - if (!this.state.isPlaying) { - return { - trackIndex: this.state.currentIndex, - trackTime: this.state.pausedOffset, - totalTime: 0, + // 定期更新播放位置 + startPositionUpdates() { + setInterval(() => { + if (this.audioBuffer) { + this.updateMediaSessionState() } - } - - const currentTrack = this.tracks[this.state.currentIndex] - if (!currentTrack?.source) return null - - // Estimate current time (not perfectly accurate but good enough) - const elapsed = this.context.currentTime - (this.state.pausedAt || 0) - const trackTime = Math.min(elapsed + this.state.pausedOffset, currentTrack.duration) - - return { - trackIndex: this.state.currentIndex, - trackTime, - totalTime: trackTime, - } + }, 1000) } - /** - * Set volume (0.0 to 1.0) - */ - setVolume(volume: number): void { - this.masterGain.gain.value = Math.max(0, Math.min(1, volume)) - } - - /** - * Get volume - */ - getVolume(): number { - return this.masterGain.gain.value - } - - /** - * Clear all tracks - */ - clearQueue(): void { + // 清理资源 + destroy() { this.stop() - this.tracks = [] - this.bufferCache.clear() - } - - /** - * Register callback for track end event - */ - onTrackEnd(callback: (index: number) => void): void { - this.onTrackEndCallbacks.push(callback) - } - - /** - * Register callback for track start event - */ - onTrackStart(callback: (index: number) => void): void { - this.onTrackStartCallbacks.push(callback) - } - - /** - * Get audio context - */ - getContext(): AudioContext { - return this.context - } - - /** - * Get master gain node - */ - getMasterGain(): GainNode { - return this.masterGain - } - - /** - * Destroy the player - */ - destroy(): void { - this.stop() - this.clearQueue() - this.onTrackEndCallbacks = [] - this.onTrackStartCallbacks = [] - + if (this.dummyAudio) { + document.body.removeChild(this.dummyAudio) + } if (this.context.state !== 'closed') { this.context.close() } } -} \ No newline at end of file +} + +export default SimpleWebAudioPlayer \ No newline at end of file