feat: 再重构
手作代码,从零重写,真正的犟人精神
This commit is contained in:
		
							parent
							
								
									197fb4011d
								
							
						
					
					
						commit
						488854f46b
					
				| 
						 | 
				
			
			@ -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() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,275 +1,180 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
			
		||||
import { WebAudioGaplessPlayer } from '../utils/webAudioPlayer'
 | 
			
		||||
import { usePlayState } from '../stores/usePlayState'
 | 
			
		||||
import { debugPlayer } from '../utils/debug'
 | 
			
		||||
import { watch, ref, onMounted, onUnmounted } from 'vue'
 | 
			
		||||
import artistsOrganize from '../utils/artistsOrganize'
 | 
			
		||||
 | 
			
		||||
const playQueue = usePlayQueueStore()
 | 
			
		||||
const player = ref<WebAudioGaplessPlayer | null>(null)
 | 
			
		||||
const isInitialized = ref(false)
 | 
			
		||||
const currentTrackIndex = ref(-1)
 | 
			
		||||
const playState = usePlayState()
 | 
			
		||||
const playerInstance = ref<WebAudioPlayer | null>(null)
 | 
			
		||||
 | 
			
		||||
class WebAudioPlayer {
 | 
			
		||||
	context: AudioContext
 | 
			
		||||
	audioBuffer: { [key: string]: AudioBuffer}
 | 
			
		||||
	dummyAudio: HTMLAudioElement
 | 
			
		||||
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.context = new window.AudioContext()
 | 
			
		||||
		this.audioBuffer = {}
 | 
			
		||||
		
 | 
			
		||||
		// 创建一个隐藏的 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()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 loadResourceAndPlay() {
 | 
			
		||||
		try {
 | 
			
		||||
			debugPlayer("从播放器实例内部获取播放项目:")
 | 
			
		||||
			debugPlayer(`目前播放:${playQueue.currentTrack?.song.cid ?? "空"}`)
 | 
			
		||||
			debugPlayer(`上一首:${playQueue.previousTrack?.song.cid ?? "空"}`)
 | 
			
		||||
			debugPlayer(`下一首:${playQueue.nextTrack?.song.cid ?? "空"}`)
 | 
			
		||||
 | 
			
		||||
			if (playQueue.queue.length === 0) {
 | 
			
		||||
				// TODO: 如果当前正在播放,则可能需要停止播放
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 将音频 buffer 加载到缓存空间
 | 
			
		||||
			const loadBuffer = async (track: QueueItem) => {
 | 
			
		||||
				if (this.audioBuffer[track.song.cid]) return // 已经缓存了,直接跳
 | 
			
		||||
				const response = await fetch(track.sourceUrl ?? "")
 | 
			
		||||
				const arrayBuffer = await response.arrayBuffer()
 | 
			
		||||
				const audioBuffer = await this.context.decodeAudioData(arrayBuffer)
 | 
			
		||||
				this.audioBuffer[track.song.cid] = audioBuffer
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (playQueue.currentTrack){
 | 
			
		||||
				await loadBuffer(playQueue.currentTrack)
 | 
			
		||||
				this.play()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (playQueue.nextTrack)
 | 
			
		||||
				await loadBuffer(playQueue.nextTrack)
 | 
			
		||||
 | 
			
		||||
			if (playQueue.previousTrack)
 | 
			
		||||
				await loadBuffer(playQueue.previousTrack)
 | 
			
		||||
 | 
			
		||||
			debugPlayer("缓存完成")
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error('播放失败:', error)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 从头播放
 | 
			
		||||
	async play() {
 | 
			
		||||
		debugPlayer("开始播放")
 | 
			
		||||
		const source = this.context.createBufferSource()
 | 
			
		||||
		source.buffer = this.audioBuffer[playQueue.currentTrack.song.cid]
 | 
			
		||||
		source.connect(this.context.destination)
 | 
			
		||||
		source.start()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pause() {}
 | 
			
		||||
 | 
			
		||||
	resume() {}
 | 
			
		||||
 | 
			
		||||
	stop() {
 | 
			
		||||
		
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	togglePlay() {
 | 
			
		||||
		
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getCurrentTime() {
 | 
			
		||||
		
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updateMediaSessionState() {
 | 
			
		||||
		
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 定期更新播放位置
 | 
			
		||||
	startPositionUpdates() {
 | 
			
		||||
		
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 清理资源
 | 
			
		||||
	destroy() {
 | 
			
		||||
		
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 初始化 Web Audio 播放器
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	player.value = new WebAudioGaplessPlayer()
 | 
			
		||||
	
 | 
			
		||||
	// 注册事件回调
 | 
			
		||||
	player.value.onTrackStart((index) => {
 | 
			
		||||
		debugPlayer(`Track ${index} started`)
 | 
			
		||||
		currentTrackIndex.value = index
 | 
			
		||||
		
 | 
			
		||||
		// 更新 store 中的当前播放索引
 | 
			
		||||
		playQueue.toggleQueuePlay(index)
 | 
			
		||||
		
 | 
			
		||||
		// 更新媒体会话
 | 
			
		||||
		const track = playQueue.queue[index]
 | 
			
		||||
		if (track) {
 | 
			
		||||
			updateMediaSession(track)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	
 | 
			
		||||
	player.value.onTrackEnd((index) => {
 | 
			
		||||
		debugPlayer(`Track ${index} ended`)
 | 
			
		||||
		
 | 
			
		||||
		// 根据循环模式处理
 | 
			
		||||
		if (playQueue.loopMode === 'single') {
 | 
			
		||||
			// 单曲循环:重新播放当前曲目
 | 
			
		||||
			player.value?.seekToTrack(index)
 | 
			
		||||
		} else if (index === playQueue.queue.length - 1) {
 | 
			
		||||
			// 最后一首歌
 | 
			
		||||
			if (playQueue.loopMode === 'all') {
 | 
			
		||||
				// 列表循环:从头开始
 | 
			
		||||
				player.value?.seekToTrack(0)
 | 
			
		||||
			} else {
 | 
			
		||||
				// 停止播放
 | 
			
		||||
				playQueue.togglePlay(false)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// 立即播放下一首歌
 | 
			
		||||
			player.value?.playNext()
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	
 | 
			
		||||
	isInitialized.value = true
 | 
			
		||||
	
 | 
			
		||||
	// 设置媒体会话处理器
 | 
			
		||||
	setupMediaSessionHandlers()
 | 
			
		||||
	playerInstance.value = new WebAudioPlayer()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 销毁播放器
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
	if (player.value) {
 | 
			
		||||
		player.value.destroy()
 | 
			
		||||
		player.value = null
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 监听播放队列变化
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueue.queue,
 | 
			
		||||
	async (newQueue) => {
 | 
			
		||||
		if (!player.value || !isInitialized.value) return
 | 
			
		||||
		
 | 
			
		||||
		debugPlayer('Queue changed, rebuilding Web Audio queue')
 | 
			
		||||
		
 | 
			
		||||
		// 清空当前队列
 | 
			
		||||
		player.value.clearQueue()
 | 
			
		||||
		currentTrackIndex.value = -1
 | 
			
		||||
		
 | 
			
		||||
		// 添加所有曲目到 Web Audio 队列
 | 
			
		||||
		for (const track of newQueue) {
 | 
			
		||||
			if (track.song.sourceUrl) {
 | 
			
		||||
				await player.value.addTrack(track.song.sourceUrl, {
 | 
			
		||||
					cid: track.song.cid,
 | 
			
		||||
					name: track.song.name,
 | 
			
		||||
					artists: track.song.artists,
 | 
			
		||||
					album: track.album,
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		debugPlayer(`Added ${newQueue.length} tracks to Web Audio queue`)
 | 
			
		||||
	},
 | 
			
		||||
	{ deep: true },
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 监听播放状态变化
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueue.isPlaying,
 | 
			
		||||
	async (isPlaying) => {
 | 
			
		||||
		if (!player.value || !isInitialized.value) return
 | 
			
		||||
		
 | 
			
		||||
		if (isPlaying) {
 | 
			
		||||
			debugPlayer('Starting playback')
 | 
			
		||||
			// 如果是从暂停恢复
 | 
			
		||||
			const position = player.value.getCurrentPosition()
 | 
			
		||||
			if (position && position.trackTime > 0) {
 | 
			
		||||
				await player.value.resume()
 | 
			
		||||
			} else {
 | 
			
		||||
				// 从当前索引开始播放
 | 
			
		||||
				await player.value.play(playQueue.currentIndex)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			debugPlayer('Pausing playback')
 | 
			
		||||
			player.value.pause()
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 监听当前曲目变化(用户手动切换)
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueue.currentIndex,
 | 
			
		||||
	async (newIndex, oldIndex) => {
 | 
			
		||||
		if (!player.value || !isInitialized.value) return
 | 
			
		||||
		
 | 
			
		||||
		// 如果是 Web Audio 触发的变化,跳过
 | 
			
		||||
		if (newIndex === currentTrackIndex.value) return
 | 
			
		||||
		
 | 
			
		||||
		debugPlayer(`User requested track change: ${oldIndex} -> ${newIndex}`)
 | 
			
		||||
		
 | 
			
		||||
		// 跳转到指定曲目
 | 
			
		||||
		await player.value.seekToTrack(newIndex)
 | 
			
		||||
		currentTrackIndex.value = newIndex
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 监听音量变化(如果有音量控制)
 | 
			
		||||
// Note: volume control not implemented in current store, default to 1
 | 
			
		||||
watch(
 | 
			
		||||
	() => isInitialized.value,
 | 
			
		||||
	(initialized) => {
 | 
			
		||||
		if (initialized && player.value) {
 | 
			
		||||
			player.value.setVolume(1)
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	{ immediate: true },
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 更新媒体会话信息
 | 
			
		||||
function updateMediaSession(track: QueueItem) {
 | 
			
		||||
	if (!('mediaSession' in navigator)) return
 | 
			
		||||
	
 | 
			
		||||
	console.log('Updating media session for:', track.song.name)
 | 
			
		||||
	
 | 
			
		||||
	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',
 | 
			
		||||
			},
 | 
			
		||||
		],
 | 
			
		||||
	})
 | 
			
		||||
	
 | 
			
		||||
	// 设置播放状态
 | 
			
		||||
	navigator.mediaSession.playbackState = playQueue.isPlaying ? 'playing' : 'paused'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 初始化媒体会话处理器(只需要设置一次)
 | 
			
		||||
function setupMediaSessionHandlers() {
 | 
			
		||||
	if (!('mediaSession' in navigator)) return
 | 
			
		||||
	
 | 
			
		||||
	console.log('Setting up media session handlers')
 | 
			
		||||
	
 | 
			
		||||
	navigator.mediaSession.setActionHandler('play', () => {
 | 
			
		||||
		console.log('Media session: play')
 | 
			
		||||
		playQueue.togglePlay(true)
 | 
			
		||||
	})
 | 
			
		||||
	
 | 
			
		||||
	navigator.mediaSession.setActionHandler('pause', () => {
 | 
			
		||||
		console.log('Media session: pause')
 | 
			
		||||
		playQueue.togglePlay(false)
 | 
			
		||||
	})
 | 
			
		||||
	
 | 
			
		||||
	navigator.mediaSession.setActionHandler('previoustrack', () => {
 | 
			
		||||
		console.log('Media session: previous')
 | 
			
		||||
		const prevIndex = Math.max(0, playQueue.currentIndex - 1)
 | 
			
		||||
		playQueue.toggleQueuePlay(prevIndex)
 | 
			
		||||
	})
 | 
			
		||||
	
 | 
			
		||||
	navigator.mediaSession.setActionHandler('nexttrack', () => {
 | 
			
		||||
		console.log('Media session: next')
 | 
			
		||||
		playQueue.skipToNext()
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定期报告播放进度
 | 
			
		||||
let progressInterval: number | null = null
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueue.isPlaying,
 | 
			
		||||
	(isPlaying) => {
 | 
			
		||||
		// 同步媒体会话播放状态
 | 
			
		||||
		if ('mediaSession' in navigator) {
 | 
			
		||||
			navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused'
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		if (isPlaying) {
 | 
			
		||||
			// 开始定期报告进度
 | 
			
		||||
			progressInterval = window.setInterval(() => {
 | 
			
		||||
				if (!player.value) return
 | 
			
		||||
				
 | 
			
		||||
				const position = player.value.getCurrentPosition()
 | 
			
		||||
				if (position) {
 | 
			
		||||
					playQueue.reportPlayProgress(position.trackTime)
 | 
			
		||||
					
 | 
			
		||||
					// 更新媒体会话位置信息
 | 
			
		||||
					if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
 | 
			
		||||
						const currentTrack = playQueue.queue[position.trackIndex]
 | 
			
		||||
						if (currentTrack) {
 | 
			
		||||
							try {
 | 
			
		||||
								navigator.mediaSession.setPositionState({
 | 
			
		||||
									duration: currentTrack.song.duration || 0,
 | 
			
		||||
									playbackRate: 1.0,
 | 
			
		||||
									position: position.trackTime,
 | 
			
		||||
								})
 | 
			
		||||
							} catch (error) {
 | 
			
		||||
								// 某些浏览器可能不支持 setPositionState
 | 
			
		||||
								console.debug('Media session setPositionState not supported:', error)
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}, 100) // 每100ms更新一次
 | 
			
		||||
		} else {
 | 
			
		||||
			// 停止报告进度
 | 
			
		||||
			if (progressInterval !== null) {
 | 
			
		||||
				clearInterval(progressInterval)
 | 
			
		||||
				progressInterval = null
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 清理定时器
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
	if (progressInterval !== null) {
 | 
			
		||||
		clearInterval(progressInterval)
 | 
			
		||||
	}
 | 
			
		||||
watch(() => playQueue.currentTrack, () => {
 | 
			
		||||
	debugPlayer(`检测到当前播放曲目更新`)
 | 
			
		||||
	playerInstance.value?.loadResourceAndPlay()
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<!-- Web Audio Player 不需要 DOM 元素 -->
 | 
			
		||||
	<div class="web-audio-player" v-show="false">
 | 
			
		||||
		<div v-if="!isInitialized" class="text-white">
 | 
			
		||||
			Initializing Web Audio Player...
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-else class="text-white">
 | 
			
		||||
			Web Audio Player Ready ({{ playQueue.queue.length }} tracks)
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.web-audio-player {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	z-index: -1;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -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<number[]>([]) // 播放队列顺序
 | 
			
		||||
	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,
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										203
									
								
								src/stores/usePlayState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/stores/usePlayState.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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<QueueItem | null>(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,
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -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<string, AudioBuffer> = 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<AudioBuffer> {
 | 
			
		||||
		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<number> {
 | 
			
		||||
		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<void> {
 | 
			
		||||
		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<void> {
 | 
			
		||||
		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<void> {
 | 
			
		||||
		await this.playTrack(startIndex)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Play next track
 | 
			
		||||
	 */
 | 
			
		||||
	async playNext(): Promise<void> {
 | 
			
		||||
		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<void> {
 | 
			
		||||
		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<void> {
 | 
			
		||||
		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()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SimpleWebAudioPlayer
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user