fix(playback): gate within-track resume on cache, drop slow seek path

Session resume of a Qobuz track no longer forces the streaming start-position path, which buffered the CMAF stream sequentially from byte 0 up to the saved offset (up to ~30s wait, with no buffering indication).

v2_play_track is now always called without a start position. The track resumes at the saved offset only on a cache hit (format_id === null), where the audio is a full in-memory buffer and Seek lands like a local file. On a cache miss it starts from 0:00 instead of blocking on a sequential buffer fill.

No backend or audio-engine change; the start_position_secs streaming path is now unused.

cc
This commit is contained in:
vicrodh committed 2026-05-16 13:53:17 -06:00
1 parent 3d29cf1384
commit 3abb7f66cb
2 files changed
+31 -17

No files matched your search

+4 -4
View File
@@ -116,10 +116,10 @@ export async function cmdPlayTrack(
// start_position_secs). duration_secs is required on the streaming
// path — without it the backend stores duration=0 and
// current_position() clamps to 0, freezing the seekbar (seen on
// session-restore first play). start_position_secs > 0 turns the
// call into a session-resume play (#315): backend buffers enough
// bytes to cover the offset and pre-skips decoder output before
// engaging audio, avoiding the play-then-seek race.
// session-restore first play). start_position_secs is still accepted
// by the backend but the frontend no longer uses it: session resume
// now seeks to the saved offset only on a cache hit, where the audio
// is fully in memory and Seek lands (see playerStore togglePlay).
return invoke('v2_play_track', {
trackId,
quality: quality ?? null,
+27 -13
View File
@@ -559,25 +559,39 @@ export async function togglePlay(): Promise<void> {
void flushPendingSeek();
}
} else {
// Qobuz track — use v2_play_track. Pass duration so the
// Qobuz track session restore (#315 within-track resume).
// Play from the start: v2_play_track serves a cache hit
// instantly or streams from byte 0. Pass duration so the
// streaming backend's current_position() doesn't clamp to 0
// (the value flows into thread_state.duration and seekbar
// progress is capped by .min(duration)). For session resume,
// hand the restore offset directly to v2_play_track via
// startPositionSecs (#315 extension): the streaming path
// waits for enough buffer to cover the offset and pre-skips
// decoder output before engaging audio, instead of the
// play-then-seek dance that silently dropped the seek
// because the audio thread's Seek handler bails when there
// is no `current_audio_data` (streaming uses
// `current_streaming_source` instead).
await cmdPlayTrack(
// progress is capped by .min(duration)).
//
// Resume the saved offset ONLY on a cache hit (format_id ===
// null): the audio is then a full in-memory buffer
// (current_audio_data) so the Seek handler lands, exactly
// like a local file. On a cache miss the track streams via
// CMAF — seeking mid-track would block on the buffer filling
// sequentially from byte 0, far too slow to feel like a
// feature, so we deliberately start fresh from 0:00.
const result = (await cmdPlayTrack(
currentTrack.id,
getStreamingQuality(),
currentTrack.duration ? Math.round(currentTrack.duration) : null,
null,
restorePosition > 1 ? restorePosition : null,
);
null,
)) as { format_id?: number | null } | null | undefined;
const servedFromCache = result != null && result.format_id === null;
if (restorePosition > 1 && servedFromCache) {
pendingSeekPosition = restorePosition;
void flushPendingSeek();
} else if (restorePosition > 1) {
// Cache miss — undo the seekbar pre-anchor (setPendingSessionRestore
// pre-set currentTime to the saved offset) so the bar matches
// the actual start at 0:00 instead of flashing the saved time.
currentTime = 0;
anchorPosition(0);
notifyListeners();
}
}
} else {