fix(player): resume playback-state tracking when gapless lands late

When a track ends before its gapless prefetch completes, 'track
finished (engine empty)' sets thread_state.is_playing=false, and the
late PlayNext still plays (the engine's append re-opens the writer
gate) but nothing restores the player-level flag: the position monitor
freezes, no next gapless arm, no QConnect reports, the queue cursor
stays on the finished track, and playback dies silently at the next
end. The PlayNext handler now captures engine.empty() before appending
and restores is_playing when the append landed on an empty engine —
a user-paused engine still holds its source (not empty), so manual
pause is never resumed by this.

km
This commit is contained in:
vicrodh committed 2026-07-22 09:04:05 -06:00
1 parent 9fad1dc123
commit da7964f6c3
1 file changed
+23 -1
+23 -1
View File
@@ -3630,7 +3630,21 @@ impl Player {
&analyzer_enabled,
);
// Append to existing Sink (gapless queue)
// Append to existing Sink (gapless queue).
// Late-gapless race: this append can arrive AFTER
// the engine went empty at natural end ("track
// finished" already set thread_state.is_playing =
// false). The engine's own append re-opens its
// writer gate and the track PLAYS, but the
// player-level flag stays false — freezing the
// position monitor, the next gapless arm, the
// QConnect reports and the queue cursor on the
// finished track while the next one plays (the
// "invisible track" desync). Capture emptiness
// BEFORE the append: a user-paused engine still
// holds its current source (NOT empty), so a
// paused session is never resumed by this.
let engine_was_empty = engine.empty();
if let Err(e) = engine.append(source) {
log::error!(
"Gapless: failed to append track {} to engine: {}",
@@ -3640,6 +3654,14 @@ impl Player {
thread_state.set_gapless_ready(false);
return;
}
if engine_was_empty
&& !thread_state.is_playing.load(Ordering::SeqCst)
{
log::info!(
"Gapless: PlayNext landed after track finished — resuming playback-state tracking"
);
thread_state.is_playing.store(true, Ordering::SeqCst);
}
// Store pending gapless data for transition detection
*gapless_pending = Some(GaplessPending {