feat(player): ephemeral-aware gapless detection + session-restore filter
Two cross-cutting plumbing changes the ephemeral folder pane needs to behave correctly without touching the rest of the playback paths. playerStore: gapless transition detection used to require event.is_playing to be true at the moment the track_id changes. The ephemeral file-data swap (one buffer of bytes replacing another) briefly reports is_playing false during the engine restart, and that transient frame arrives BEFORE the player can flip the flag back. Without a carve-out the event was mis-classified as an external track change, the queue's current_index hadn't advanced (it's a separate state machine), and the UI froze on the previous track. Streaming playback paths don't see this race because their buffer transition keeps is_playing pinned. Carve-out is scoped to event.track_id >= EPHEMERAL_ID_FLOOR so Qobuz/Plex/library are untouched. routes/+page.svelte: session save now strips ephemeral tracks from the queue snapshot and clears current_index if it pointed at one. Ephemeral ids aren't stable across processes (the cache rebuilds from scratch on each launch), and even if they were, the playback path 404s if session restore fires before the folder rehydrates. The folder-path persistence already brings the pane back; the queue can come back via re-clicking. Also: gapless transition handler logs the advance/expected ids and the match flag, plus runs a defensive syncQueueState on success — that's belt-and-suspenders for paths where the queue's view of "current" lags the player's gapless transition by a tick (e.g. ephemeral, where the queue advance and the player swap are independent state machines). cc
This commit is contained in:
2 files changed
+77
-24
No files matched your search
+64
-23
@@ -4377,30 +4377,51 @@
|
||||
const tracks = snapshot.tracks;
|
||||
const currentIndex = snapshot.current_index;
|
||||
|
||||
const allTracks: PersistedQueueTrack[] = tracks.map(track => ({
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
version: track.version ?? null,
|
||||
artist: track.artist,
|
||||
album: track.album,
|
||||
duration_secs: track.duration_secs,
|
||||
artwork_url: track.artwork_url,
|
||||
hires: track.hires,
|
||||
bit_depth: track.bit_depth ?? null,
|
||||
sample_rate: track.sample_rate ?? null,
|
||||
is_local: track.is_local ?? false,
|
||||
album_id: track.album_id ?? null,
|
||||
artist_id: track.artist_id ?? null,
|
||||
// Preserve source (qobuz | local | plex). Dropping this on save meant
|
||||
// local/plex queues came back as Qobuz after session restore, and
|
||||
// auto-advance routed to v2_play_track with library row ids — which
|
||||
// Qobuz then "resolved" to whatever track happened to share the id.
|
||||
source: track.source ?? (track.is_local ? 'local' : null),
|
||||
}));
|
||||
// Ephemeral tracks (id >= 2^48) live in an in-memory cache that's
|
||||
// rebuilt from scratch on every launch. The IDs aren't stable
|
||||
// across processes, and even if they were, the playback path
|
||||
// would 404 if session restore fired before the folder rehydrates.
|
||||
// Strip them from the persisted queue and clear the current-index
|
||||
// pointer if it was sitting on one — the ephemeral pane comes back
|
||||
// via folder-path persistence and the user re-clicks play.
|
||||
const EPHEMERAL_ID_FLOOR = 1 << 48;
|
||||
const persistedTracks: PersistedQueueTrack[] = [];
|
||||
let persistedCurrentIndex: number | null = null;
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
if (Number(track.id) >= EPHEMERAL_ID_FLOOR) {
|
||||
if (currentIndex !== null && i === currentIndex) {
|
||||
persistedCurrentIndex = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (currentIndex !== null && i === currentIndex) {
|
||||
persistedCurrentIndex = persistedTracks.length;
|
||||
}
|
||||
persistedTracks.push({
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
album: track.album,
|
||||
duration_secs: track.duration_secs,
|
||||
artwork_url: track.artwork_url,
|
||||
hires: track.hires,
|
||||
bit_depth: track.bit_depth ?? null,
|
||||
sample_rate: track.sample_rate ?? null,
|
||||
is_local: track.is_local ?? false,
|
||||
album_id: track.album_id ?? null,
|
||||
artist_id: track.artist_id ?? null,
|
||||
// Preserve source (qobuz | local | plex). Dropping this on save meant
|
||||
// local/plex queues came back as Qobuz after session restore, and
|
||||
// auto-advance routed to v2_play_track with library row ids — which
|
||||
// Qobuz then "resolved" to whatever track happened to share the id.
|
||||
source: track.source ?? (track.is_local ? 'local' : null),
|
||||
});
|
||||
}
|
||||
|
||||
await saveSessionState(
|
||||
allTracks,
|
||||
currentIndex,
|
||||
persistedTracks,
|
||||
persistedCurrentIndex,
|
||||
currentTrack ? Math.floor(currentTime) : 0,
|
||||
volume / 100,
|
||||
isShuffle,
|
||||
@@ -5279,12 +5300,32 @@
|
||||
console.log('[Gapless] Handling transition to track', trackId);
|
||||
// Advance the queue to match backend state
|
||||
const advanced = await nextTrackGuarded();
|
||||
console.log(
|
||||
'[Gapless] nextTrackGuarded returned:',
|
||||
advanced?.id,
|
||||
'expected:',
|
||||
trackId,
|
||||
'match:',
|
||||
advanced?.id === trackId
|
||||
);
|
||||
if (advanced && advanced.id === trackId) {
|
||||
// Queue advanced successfully — update UI metadata
|
||||
await playQueueTrack(advanced, undefined, true);
|
||||
// Defensive sync: belt-and-suspenders for paths (e.g. ephemeral
|
||||
// tracks) where the queue's view of "current" can lag the
|
||||
// player's gapless transition. The cost is one cheap getter
|
||||
// call to the backend; in exchange the queue panel highlight
|
||||
// and now-playing slot stay coherent with what's actually
|
||||
// playing.
|
||||
await syncQueueState();
|
||||
} else {
|
||||
// Queue mismatch — sync from backend
|
||||
console.warn('[Gapless] Queue mismatch, syncing state');
|
||||
console.warn(
|
||||
'[Gapless] Queue mismatch, syncing state. advanced=',
|
||||
advanced?.id,
|
||||
'trackId=',
|
||||
trackId
|
||||
);
|
||||
await syncQueueState();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user