diff --git a/package.json b/package.json index 05195af..62c0934 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gapless", - "version": "4.4.1", + "version": "4.4.2", "description": "Gapless audio playback javascript plugin", "type": "module", "main": "dist/index.mjs", diff --git a/src/Track.ts b/src/Track.ts index 7d9699b..2e1c61f 100644 --- a/src/Track.ts +++ b/src/Track.ts @@ -637,7 +637,11 @@ export class Track { private _crossoverHtml5ToWebAudio(wasPlaying: boolean): void { if (!this.ctx || !this.audioBuffer || !this.gainNode) return; - const offset = this.audio.currentTime; + const htmlTime = this.audio.currentTime; + const offset = + isFinite(this.pausedAtTrackTime) && this.pausedAtTrackTime > htmlTime + ? this.pausedAtTrackTime + : htmlTime; this.pausedAtTrackTime = isFinite(offset) ? offset : 0; if (!wasPlaying) { diff --git a/tests/unit/crossover-flow.test.ts b/tests/unit/crossover-flow.test.ts index c0c01f7..d27e550 100644 --- a/tests/unit/crossover-flow.test.ts +++ b/tests/unit/crossover-flow.test.ts @@ -202,6 +202,36 @@ describe('crossover end-to-end flow', () => { expect(internal._scheduledNextIndex).toBe(1); }); + it('crossover preserves pausedAtTrackTime when HTML5 element has not caught up to a prior seek', async () => { + mockFetchSuccess(); + const q = new Queue({ tracks: ['a.mp3', 'b.mp3'] }); + + // Simulate gotoTrack with a startTime — this sets pausedAtTrackTime on the + // track to 31.5, but the HTML5 element's currentTime stays low because + // the element hasn't loaded/seeked yet. + q.gotoTrack(0, false, 31.5); + for (let i = 0; i < 5; i++) await new Promise(r => setTimeout(r, 0)); + + const tracks = (q as any)._tracks; + // HTML5 element's currentTime is still near 0 + (tracks[0].audio as MockAudioElement).currentTime = 1.3; + + q.play(); + for (let i = 0; i < 15; i++) await new Promise(r => setTimeout(r, 0)); + await new Promise(r => setTimeout(r, 150)); + + expect(tracks[0].playbackType).toBe('WEBAUDIO'); + + // The WebAudio source should have started near 31.5, not at 1.3 + const sourceCreate = (tracks[0].ctx.createBufferSource as ReturnType); + const allSources = sourceCreate.mock.results.map((r) => r.value as { start: ReturnType }); + const startedSource = allSources.find(s => s.start.mock.calls.length > 0); + expect(startedSource).toBeDefined(); + const [, offsetArg] = startedSource!.start.mock.calls[0]; + expect(offsetArg).toBeGreaterThanOrEqual(31.5); + expect(offsetArg).toBeLessThan(33); + }); + it('after crossover, gapless scheduling targets the next track via WebAudio→WebAudio', async () => { mockFetchSuccess(); const q = new Queue({ tracks: ['a.mp3', 'b.mp3'] });