Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 5 additions & 1 deletion src/Track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/crossover-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof vi.fn>);
const allSources = sourceCreate.mock.results.map((r) => r.value as { start: ReturnType<typeof vi.fn> });
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'] });
Expand Down
Loading