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:
4 files changed
+76
-6
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user