fix(slint): reconcile now-playing card to the live track on seamless change
The poll loop's seamless-transition branch guessed whether a track-id change while still playing was a gapless hand-off via a peek_upcoming(1) == track_id heuristic. When that heuristic missed (queue pointer lagging the engine), the meta was never refreshed: the seek bar kept advancing while the title/art/album stayed on the previous track (the reported now-playing populate bug). Replace the heuristic with reconciliation to what is ACTUALLY playing. New frontend-agnostic primitive QueueManager::sync_current_to_id(id) moves the queue pointer to the track the engine advanced to (recording history + keeping the shuffle cursor aligned) and reports whether it moved; QbzCore wraps it and emits a queue update only on a real move. The poll loop now syncs to the live track_id and always refreshes the card, doing the record/sidebar/prefetch bookkeeping only on a genuine advance (a manual play already moved the pointer, so it is idempotent). cc
This commit is contained in:
3 files changed
+73
-24
No files matched your search
@@ -606,6 +606,25 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
queue.current()
|
||||
}
|
||||
|
||||
/// Reconcile the queue pointer to the track the audio engine is actually
|
||||
/// playing. A gapless hand-off advances inside the player without going
|
||||
/// through `next_track`, so the core pointer can lag the live track and
|
||||
/// the now-playing card goes stale. This moves the pointer to the track
|
||||
/// with `id` and returns it plus whether the pointer moved; a queue
|
||||
/// update is emitted only when it did. Frontend-agnostic — the playback
|
||||
/// poll loop calls this to keep now-playing in sync (ADR-006).
|
||||
pub async fn sync_current_to_id(&self, id: u64) -> Option<(QueueTrack, bool)> {
|
||||
let queue = self.queue.write().await;
|
||||
let result = queue.sync_current_to_id(id);
|
||||
if matches!(result, Some((_, true))) {
|
||||
self.emit(CoreEvent::QueueUpdated {
|
||||
state: queue.get_state(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// ==================== Search & Catalog ====================
|
||||
|
||||
/// Search for albums
|
||||
|
||||
@@ -659,6 +659,38 @@ impl QueueManager {
|
||||
prev_idx.and_then(|idx| state.tracks.get(idx).cloned())
|
||||
}
|
||||
|
||||
/// Move the current pointer to the track whose id matches `id`, WITHOUT
|
||||
/// starting playback. Used to reconcile the queue pointer to a track the
|
||||
/// audio engine already advanced to on its own (a gapless hand-off
|
||||
/// happens inside the player, not through `next`), so the now-playing
|
||||
/// card never goes stale while the seek bar keeps moving.
|
||||
///
|
||||
/// Returns the matched track plus whether the pointer actually moved
|
||||
/// (`false` = it was already current). Returns `None` when no queue track
|
||||
/// has that id, leaving the pointer untouched.
|
||||
pub fn sync_current_to_id(&self, id: u64) -> Option<(QueueTrack, bool)> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let target = state.tracks.iter().position(|t| t.id == id)?;
|
||||
let moved = state.current_index != Some(target);
|
||||
if moved {
|
||||
// Record the outgoing track so `previous` still walks back.
|
||||
if let Some(curr_idx) = state.current_index {
|
||||
state.history.push_back(curr_idx);
|
||||
while state.history.len() > 50 {
|
||||
state.history.pop_front();
|
||||
}
|
||||
}
|
||||
state.current_index = Some(target);
|
||||
// Keep the shuffle cursor aligned with the new position.
|
||||
if state.shuffle {
|
||||
if let Some(pos) = state.shuffle_order.iter().position(|&x| x == target) {
|
||||
state.shuffle_position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
state.tracks.get(target).cloned().map(|t| (t, moved))
|
||||
}
|
||||
|
||||
/// Jump to a track by its position in the `upcoming` list as returned by
|
||||
/// `get_state`. This is the position the user sees in the Queue sidebar;
|
||||
/// the method resolves it to the correct canonical index even when
|
||||
|
||||
@@ -1932,32 +1932,30 @@ pub fn start_poll_loop(
|
||||
&& is_playing
|
||||
&& was_playing;
|
||||
if seamless_change {
|
||||
// A track-id change while still playing is EITHER a real
|
||||
// gapless hand-off (the engine started the prefetched next
|
||||
// The audio engine advanced to `track_id` on its own — EITHER
|
||||
// a real gapless hand-off (it started the prefetched next
|
||||
// track) OR a manual new-track play that just replaced the
|
||||
// queue. Only the former should advance the core queue
|
||||
// pointer: a real gapless next IS the current upcoming track.
|
||||
// For a manual play the queue is already correct, and calling
|
||||
// next_track() would push the pointer one past what's actually
|
||||
// playing — desyncing now-playing from the queue (the reported
|
||||
// erratic mismatch).
|
||||
let is_gapless_advance = runtime
|
||||
.core()
|
||||
.peek_upcoming(1)
|
||||
.await
|
||||
.first()
|
||||
.map(|t| t.id)
|
||||
== Some(track_id);
|
||||
if is_gapless_advance {
|
||||
log::info!(
|
||||
"[qbz-slint] [GAPLESS] seamless transition {last_track_id} -> {track_id}"
|
||||
);
|
||||
let _ = runtime.core().next_track().await;
|
||||
// queue. Rather than guess which (the old peek-based heuristic
|
||||
// missed cases and left the card stale while the seek bar kept
|
||||
// moving — the reported populate bug), reconcile the queue
|
||||
// pointer + the now-playing card to whatever is ACTUALLY
|
||||
// playing. `sync_current_to_id` moves the pointer only when it
|
||||
// lags (a real advance); a manual play already moved it, so it
|
||||
// reports `moved == false` and we skip the double bookkeeping.
|
||||
if let Some((_, moved)) =
|
||||
runtime.core().sync_current_to_id(track_id).await
|
||||
{
|
||||
// Always refresh so title/art/meta match the live track.
|
||||
refresh_now_playing_meta(&runtime, &weak).await;
|
||||
record_recent(&runtime).await;
|
||||
refresh_sidebar(true);
|
||||
// Prefetch the successors of the now-current track.
|
||||
kick_prefetch(&runtime).await;
|
||||
if moved {
|
||||
log::info!(
|
||||
"[qbz-slint] [GAPLESS] seamless transition {last_track_id} -> {track_id}"
|
||||
);
|
||||
record_recent(&runtime).await;
|
||||
refresh_sidebar(true);
|
||||
// Prefetch the successors of the now-current track.
|
||||
kick_prefetch(&runtime).await;
|
||||
}
|
||||
gapless_requested_for = 0;
|
||||
}
|
||||
// Resync the edge trackers either way so this change is not
|
||||
|
||||
Reference in new issue
Block a user