From da7964f6c35314595d0f84ea4a767e2001256bf3 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:04:05 -0600 Subject: [PATCH] fix(player): resume playback-state tracking when gapless lands late MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/qbz-player/src/player/mod.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/qbz-player/src/player/mod.rs b/crates/qbz-player/src/player/mod.rs index 876ab5692..ed45e9442 100644 --- a/crates/qbz-player/src/player/mod.rs +++ b/crates/qbz-player/src/player/mod.rs @@ -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 {