fix(qconnect): fix seekbar position sync with remote controllers

Root cause: server pause/resume commands send None for position and
tracks, which the renderer state blindly overwrote. Then renderer
reports sent null position to the server, making controllers show the
track as finished.

Three-part fix:
1. renderer.rs: only overwrite position/tracks when server provides
   a non-None value (retain last known values through pause/resume)
2. app.rs + qconnect_service.rs: update renderer state position from
   frontend periodic reports so the internal state always has the
   real playback position
3. +page.svelte: increase position report frequency from 5s to 2s
   for smoother seekbar updates on controllers
This commit is contained in:
vicrodh committed 2026-02-22 12:57:50 -06:00
1 parent e240c12897
commit 9444269fd6
4 files changed
+76 -6

No files matched your search

+8
View File
@@ -154,6 +154,14 @@ where
self.send_renderer_report(report).await
}
/// Update the renderer state's position from the frontend's playback position.
/// This keeps the internal state in sync with the actual audio playback, so that
/// renderer reports triggered by server commands (pause/resume) include the real position.
pub async fn update_renderer_position(&self, position_ms: u64) {
let mut state = self.state.lock().await;
state.renderer.current_position_ms = Some(position_ms);
}
pub async fn handle_transport_event(
&self,
event: TransportEvent,
+47 -4
View File
@@ -78,10 +78,22 @@ pub fn apply_renderer_command(
current_track,
next_track,
} => {
state.playing_state = *playing_state;
state.current_position_ms = *current_position_ms;
state.current_track = current_track.clone();
state.next_track = next_track.clone();
// Only overwrite fields when the server provides a value.
// Server pause/resume commands send None for position and tracks,
// but the renderer should retain the last known values so that
// subsequent renderer reports include them correctly.
if playing_state.is_some() {
state.playing_state = *playing_state;
}
if current_position_ms.is_some() {
state.current_position_ms = *current_position_ms;
}
if current_track.is_some() {
state.current_track = current_track.clone();
}
if next_track.is_some() {
state.next_track = next_track.clone();
}
}
RendererCommand::SetVolume {
volume,
@@ -173,4 +185,35 @@ mod tests {
assert_eq!(state.volume_delta, Some(8));
assert_eq!(state.updated_at_ms, 1000);
}
#[test]
fn set_state_none_does_not_overwrite_existing_values() {
let current_track = QueueItem {
track_context_uuid: "ctx-a".to_string(),
track_id: 11,
queue_item_id: 101,
};
let mut state = QConnectRendererState {
playing_state: Some(2),
current_position_ms: Some(30_000),
current_track: Some(current_track.clone()),
..Default::default()
};
// Simulate a server pause command that sends None for position/tracks
apply_renderer_command(
&mut state,
&RendererCommand::SetState {
playing_state: Some(3),
current_position_ms: None,
current_track: None,
next_track: None,
},
1500,
);
assert_eq!(state.playing_state, Some(3)); // updated
assert_eq!(state.current_position_ms, Some(30_000)); // retained
assert_eq!(state.current_track, Some(current_track)); // retained
}
}