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
10 changes: 5 additions & 5 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
"build": "vite build"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^5.1.4",
"typescript": "^5.9.3",
"vite": "^7.3.1"
"@vitejs/plugin-react": "^6.0.1",
"typescript": "^6.0.3",
"vite": "^8.0.11"
}
}
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gapless",
"version": "4.1.2",
"version": "4.2.0",
"description": "Gapless audio playback javascript plugin",
"type": "module",
"main": "dist/index.mjs",
Expand Down Expand Up @@ -37,18 +37,18 @@
"author": "Daniel Saewitz",
"license": "MIT",
"devDependencies": {
"@playwright/test": "^1.58.2",
"@switz/eslint-config": "^12.5.2",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^9.25.1",
"happy-dom": "^20.8.3",
"playwright": "^1.58.2",
"@playwright/test": "^1.59.1",
"@switz/eslint-config": "^13.0.2",
"@vitest/coverage-v8": "^4.1.5",
"eslint": "^10.3.0",
"happy-dom": "^20.9.0",
"playwright": "^1.59.1",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
"typescript": "^6.0.3",
"vitest": "^4.1.5"
},
"packageManager": "pnpm@10.30.2",
"dependencies": {
"xstate": "^5.28.0"
"xstate": "^5.31.0"
}
}
2,060 changes: 994 additions & 1,066 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

52 changes: 44 additions & 8 deletions src/Queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,21 +418,57 @@ export class Queue implements TrackQueueRef {

private _preloadAhead(fromIndex: number): void {
const cur = this._trackAt(fromIndex);
if (cur && cur.playbackType === 'HTML5' && cur.isPlaying) {
const threshold = isNaN(cur.duration) ? 15 : Math.min(cur.duration * 0.2, 15);
if (cur.currentTime < threshold) {
this.onDebug(`_preloadAhead: deferring — HTML5 track ${fromIndex} at ${cur.currentTime.toFixed(1)}s (threshold=${threshold.toFixed(1)}s)`);
return;
}

// Bandwidth-contention gate: while the current track is playing via HTML5
// and its own Web Audio buffer is still being fetched+decoded, holding
// off on every next-track preload prevents two concurrent large MP3
// downloads from delaying the current track's crossover (and therefore
// the moment from which all future gapless transitions are
// sample-accurate). When the current track's BUFFER_READY fires,
// notifyBufferReady → TRACK_LOADED → preloadAhead re-runs, this gate
// is no longer engaged, and next-track fetches proceed sequentially
// as buffers complete.
//
// This lives here (not in the queue machine) because preloadAhead is
// invoked as a transition action from ~7 different events, and the
// gate's data source — the current track's machine state — isn't
// owned by the queue machine. Pushing the check into every caller
// would just duplicate the same read.
if (
cur != null &&
cur.isPlaying &&
cur.playbackType === 'HTML5' &&
cur.webAudioLoadingState === 'LOADING'
) {
this.onDebug(`_preloadAhead(${fromIndex}): deferring all — current track buffer still loading`);
return;
}

// Speculative-load throttle: the immediate next track is always allowed
// (it must be ready for gapless scheduling), but tracks beyond that are
// deferred until the current track has played past min(duration*0.2, 15s)
// — the point at which we have evidence the user is committing to
// listening, not skipping.
const curBelowThreshold =
cur != null &&
cur.isPlaying &&
(() => {
const threshold = isNaN(cur.duration) ? 15 : Math.min(cur.duration * 0.2, 15);
return cur.currentTime < threshold;
})();

const limit = fromIndex + this._preloadNumTracks + 1;
this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length}`);
this.onDebug(`_preloadAhead(${fromIndex}) limit=${limit} trackCount=${this._tracks.length} belowThreshold=${curBelowThreshold}`);
for (let i = fromIndex + 1; i < this._tracks.length && i < limit; i++) {
const t = this._tracks[i];
if (i > fromIndex + 1 && curBelowThreshold) {
this.onDebug(`_preloadAhead: deferring track ${i} (current at ${cur!.currentTime.toFixed(1)}s, below threshold)`);
return;
}
if (!t.isBufferLoaded) {
this.onDebug(`_preloadAhead: starting preload for track ${i}`);
t.preload();
break;
return;
} else {
this.onDebug(`_preloadAhead: track ${i} already loaded`);
}
Expand Down
Loading
Loading