| type AudioQueueEvent = 'stop' | 'empty-queue' | 'id-change'; |
|
|
| interface AudioQueueStopDetail { |
| event: AudioQueueEvent; |
| id: string | null; |
| } |
|
|
| export type OnStoppedCallback = (detail: AudioQueueStopDetail) => void; |
|
|
| export class AudioQueue { |
| private audio: HTMLAudioElement; |
| private queue: string[] = []; |
| private current: string | null = null; |
| private readonly _onEnded = () => this.next(); |
|
|
| id: string | null = null; |
| onStopped: OnStoppedCallback | null = null; |
|
|
| constructor(audioElement: HTMLAudioElement) { |
| this.audio = audioElement; |
| this.audio.addEventListener('ended', this._onEnded); |
| } |
|
|
| setId(newId: string) { |
| if (this.id === newId) return; |
|
|
| this.#halt(); |
| this.id = newId; |
| this.onStopped?.({ event: 'id-change', id: newId }); |
| } |
|
|
| setPlaybackRate(rate: number) { |
| this.audio.playbackRate = rate; |
| } |
|
|
| enqueue(url: string) { |
| this.queue.push(url); |
|
|
| |
| if (this.audio.paused && !this.current) { |
| this.next(); |
| } |
| } |
|
|
| play() { |
| if (!this.current && this.queue.length > 0) { |
| this.next(); |
| } else { |
| this.audio.play(); |
| } |
| } |
|
|
| next() { |
| this.current = this.queue.shift() ?? null; |
|
|
| if (this.current) { |
| this.audio.src = this.current; |
| this.audio.play(); |
| } else { |
| this.#halt(); |
| this.onStopped?.({ event: 'empty-queue', id: this.id }); |
| } |
| } |
|
|
| stop() { |
| this.#halt(); |
| this.onStopped?.({ event: 'stop', id: this.id }); |
| } |
|
|
| destroy() { |
| this.audio.removeEventListener('ended', this._onEnded); |
| this.#halt(); |
| this.onStopped = null; |
| } |
|
|
| |
| |
| |
| |
| #halt() { |
| this.audio.pause(); |
| this.audio.currentTime = 0; |
| this.audio.removeAttribute('src'); |
| this.audio.load(); |
| this.queue = []; |
| this.current = null; |
| } |
| } |
|
|