fix(audio): re-attach waveform canvas on re-navigation to same song
When navigating away from SongPage and back to the same song, the container
div is a new DOM element but the URL is unchanged. The previous early-return
(currentUrl === url) would skip initialization entirely, leaving WaveSurfer
pointing at the detached old container — nothing rendered.
Fix: track currentContainer alongside currentUrl. When URL matches but container
has changed, call wavesurfer.setOptions({ container }) which moves the existing
canvas into the new container without reloading audio or interrupting playback.
WaveSurfer v7 renderer.setOptions() supports this: it calls
newParent.appendChild(this.container) to relocate the canvas div.
Three paths in initialize():
1. Same URL + same container → no-op
2. Same URL + new container → setOptions re-attach (no reload)
3. Different URL → full teardown and reload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ class AudioService {
|
|||||||
private static instance: AudioService;
|
private static instance: AudioService;
|
||||||
private wavesurfer: WaveSurfer | null = null;
|
private wavesurfer: WaveSurfer | null = null;
|
||||||
private currentUrl: string | null = null;
|
private currentUrl: string | null = null;
|
||||||
|
private currentContainer: HTMLElement | null = null;
|
||||||
private isReady = false;
|
private isReady = false;
|
||||||
private lastTimeUpdate = 0;
|
private lastTimeUpdate = 0;
|
||||||
// Persistent audio element attached to document.body so playback survives
|
// Persistent audio element attached to document.body so playback survives
|
||||||
@@ -44,10 +45,18 @@ class AudioService {
|
|||||||
if (!container) throw new Error('Container element is required');
|
if (!container) throw new Error('Container element is required');
|
||||||
if (!url) throw new Error('Valid audio URL is required');
|
if (!url) throw new Error('Valid audio URL is required');
|
||||||
|
|
||||||
// Reuse the existing instance when the URL hasn't changed
|
// Same URL and same container — nothing to do
|
||||||
if (this.currentUrl === url && this.wavesurfer) return;
|
if (this.currentUrl === url && this.wavesurfer && this.currentContainer === container) return;
|
||||||
|
|
||||||
// Tear down the previous instance and clear stale store state
|
// Same URL, different container: navigated away and back to the same song.
|
||||||
|
// Move the waveform canvas to the new container without reloading audio.
|
||||||
|
if (this.currentUrl === url && this.wavesurfer) {
|
||||||
|
this.wavesurfer.setOptions({ container });
|
||||||
|
this.currentContainer = container;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different URL — tear down the previous instance and clear stale store state
|
||||||
if (this.wavesurfer) {
|
if (this.wavesurfer) {
|
||||||
this.destroyWaveSurfer();
|
this.destroyWaveSurfer();
|
||||||
usePlayerStore.getState().batchUpdate({ isPlaying: false, currentTime: 0, duration: 0 });
|
usePlayerStore.getState().batchUpdate({ isPlaying: false, currentTime: 0, duration: 0 });
|
||||||
@@ -72,6 +81,7 @@ class AudioService {
|
|||||||
|
|
||||||
this.wavesurfer = ws;
|
this.wavesurfer = ws;
|
||||||
this.currentUrl = url;
|
this.currentUrl = url;
|
||||||
|
this.currentContainer = container;
|
||||||
this.setupEventHandlers(ws);
|
this.setupEventHandlers(ws);
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -162,6 +172,7 @@ class AudioService {
|
|||||||
this.mediaElement = null;
|
this.mediaElement = null;
|
||||||
this.wavesurfer = null;
|
this.wavesurfer = null;
|
||||||
this.currentUrl = null;
|
this.currentUrl = null;
|
||||||
|
this.currentContainer = null;
|
||||||
this.isReady = false;
|
this.isReady = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function createMockWaveSurfer(duration = 120) {
|
|||||||
isPlaying: vi.fn(() => false),
|
isPlaying: vi.fn(() => false),
|
||||||
unAll: vi.fn(),
|
unAll: vi.fn(),
|
||||||
destroy: vi.fn(),
|
destroy: vi.fn(),
|
||||||
|
setOptions: vi.fn(),
|
||||||
// Helper to fire events from tests
|
// Helper to fire events from tests
|
||||||
_emit: (event: EventName, ...args: any[]) => handlers[event]?.(...args),
|
_emit: (event: EventName, ...args: any[]) => handlers[event]?.(...args),
|
||||||
};
|
};
|
||||||
@@ -114,7 +115,24 @@ describe('AudioService', () => {
|
|||||||
await expect(initPromise).rejects.toThrow('network error');
|
await expect(initPromise).rejects.toThrow('network error');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reuses the existing instance for the same URL', async () => {
|
it('does nothing when called with same URL and same container', async () => {
|
||||||
|
const WaveSurfer = (await import('wavesurfer.js')).default;
|
||||||
|
const mockWs = createMockWaveSurfer();
|
||||||
|
vi.mocked(WaveSurfer.create).mockReturnValue(mockWs as any);
|
||||||
|
|
||||||
|
const url = 'http://example.com/same.mp3';
|
||||||
|
const container = makeContainer();
|
||||||
|
const p1 = service.initialize(container, url);
|
||||||
|
mockWs._emit('ready');
|
||||||
|
await p1;
|
||||||
|
|
||||||
|
const createCount = vi.mocked(WaveSurfer.create).mock.calls.length;
|
||||||
|
await service.initialize(container, url); // exact same reference
|
||||||
|
expect(vi.mocked(WaveSurfer.create).mock.calls.length).toBe(createCount);
|
||||||
|
expect(mockWs.setOptions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('re-attaches waveform to new container when same URL but container changed (re-navigation)', async () => {
|
||||||
const WaveSurfer = (await import('wavesurfer.js')).default;
|
const WaveSurfer = (await import('wavesurfer.js')).default;
|
||||||
const mockWs = createMockWaveSurfer();
|
const mockWs = createMockWaveSurfer();
|
||||||
vi.mocked(WaveSurfer.create).mockReturnValue(mockWs as any);
|
vi.mocked(WaveSurfer.create).mockReturnValue(mockWs as any);
|
||||||
@@ -124,9 +142,13 @@ describe('AudioService', () => {
|
|||||||
mockWs._emit('ready');
|
mockWs._emit('ready');
|
||||||
await p1;
|
await p1;
|
||||||
|
|
||||||
const createCallCount = vi.mocked(WaveSurfer.create).mock.calls.length;
|
const newContainer = makeContainer(); // different DOM element, same URL
|
||||||
await service.initialize(makeContainer(), url); // same URL
|
const createCount = vi.mocked(WaveSurfer.create).mock.calls.length;
|
||||||
expect(vi.mocked(WaveSurfer.create).mock.calls.length).toBe(createCallCount); // no new instance
|
await service.initialize(newContainer, url);
|
||||||
|
|
||||||
|
// No new WaveSurfer instance — just a canvas re-attach
|
||||||
|
expect(vi.mocked(WaveSurfer.create).mock.calls.length).toBe(createCount);
|
||||||
|
expect(mockWs.setOptions).toHaveBeenCalledWith({ container: newContainer });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets store state when URL changes', async () => {
|
it('resets store state when URL changes', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user