diff --git a/web/src/services/audioService.ts b/web/src/services/audioService.ts index 2b4fd66..638c7e8 100755 --- a/web/src/services/audioService.ts +++ b/web/src/services/audioService.ts @@ -24,19 +24,20 @@ class AudioService { // For use in tests only public static resetInstance(): void { - if (this.instance?.mediaElement) { - this.instance.mediaElement.remove(); - } + this.instance?.cleanup(); this.instance = undefined as any; } - private getOrCreateMediaElement(): HTMLAudioElement { - if (!this.mediaElement) { - this.mediaElement = document.createElement('audio'); - this.mediaElement.style.display = 'none'; - document.body.appendChild(this.mediaElement); - } - return this.mediaElement; + private createMediaElement(): HTMLAudioElement { + // Always create a fresh element — never reuse the one from a destroyed + // WaveSurfer instance. WaveSurfer.destroy() aborts its internal fetch + // signal, which can poison the same element when the next instance tries + // to load a new URL. A new element has no lingering aborted state. + // The element is appended to document.body so it outlives SongPage unmounts. + const el = document.createElement('audio'); + el.style.display = 'none'; + document.body.appendChild(el); + return el; } public async initialize(container: HTMLElement, url: string): Promise { @@ -52,11 +53,13 @@ class AudioService { usePlayerStore.getState().batchUpdate({ isPlaying: false, currentTime: 0, duration: 0 }); } + this.mediaElement = this.createMediaElement(); + const ws = WaveSurfer.create({ container, - // Provide a persistent audio element so playback continues even when - // the SongPage container div is removed from the DOM on navigation. - media: this.getOrCreateMediaElement(), + // Fresh audio element per song. Lives on document.body so playback + // continues even when the SongPage container is removed from the DOM. + media: this.mediaElement, waveColor: "rgba(255,255,255,0.09)", progressColor: "#c8861a", cursorColor: "#e8a22a", @@ -146,13 +149,17 @@ class AudioService { private destroyWaveSurfer(): void { if (!this.wavesurfer) return; try { - // Don't pause — if audio is playing and we're switching songs, we want - // the pause to happen naturally when the media element src changes. this.wavesurfer.unAll(); this.wavesurfer.destroy(); + // Remove the old media element after WaveSurfer finishes its own cleanup. + if (this.mediaElement) { + this.mediaElement.pause(); + this.mediaElement.remove(); + } } catch (err) { console.error('[AudioService] Error destroying WaveSurfer:', err); } + this.mediaElement = null; this.wavesurfer = null; this.currentUrl = null; this.isReady = false;