feat(qconnect): add set_position_if_remote seek command

CtrlSrvrSetPlayerState with current_position + current_queue_item,
playing_state omitted so seek does not toggle play/pause.

cc
This commit is contained in:
vicrodh committed 2026-06-01 03:13:21 -06:00
1 parent 452f961977
commit b6bf833ba9
4 files changed
+122 -3

No files matched your search

+1
View File
@@ -1568,6 +1568,7 @@ pub fn run(qconnect_cli_override: Option<bool>) {
qconnect::v2_qconnect_set_volume_if_remote,
qconnect::v2_qconnect_mute_if_remote,
qconnect::v2_qconnect_stop_if_remote,
qconnect::v2_qconnect_set_position_if_remote,
qconnect::v2_qconnect_toggle_shuffle_if_remote,
qconnect::v2_qconnect_cycle_repeat_if_remote,
qconnect::v2_qconnect_set_autoplay_mode_if_remote,
+41
View File
@@ -413,6 +413,19 @@ pub async fn v2_qconnect_stop_if_remote(
.map_err(RuntimeError::Internal)
}
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_qconnect_set_position_if_remote(
positionMs: i64,
app_handle: AppHandle,
service: State<'_, QconnectServiceState>,
) -> Result<bool, RuntimeError> {
service
.set_position_if_remote(positionMs, &app_handle)
.await
.map_err(RuntimeError::Internal)
}
#[tauri::command]
pub async fn v2_qconnect_toggle_shuffle_if_remote(
app_handle: AppHandle,
@@ -838,6 +851,34 @@ pub(super) fn should_report_queue_item_ids_for_renderer_state(
|| (local_renderer_active && resolved_current_qid.is_some())
}
/// Build a `CtrlSrvrSetPlayerState` request that seeks the remote renderer to
/// `position_ms` for `current_queue_item_id` under optimistic concurrency.
///
/// `playing_state` is intentionally `None`: a seek must not toggle play/pause.
pub(super) fn build_set_position_player_state_request(
position_ms: i64,
current_queue_item_id: Option<u64>,
queue_version: super::QconnectQueueVersionPayload,
) -> super::QconnectSetPlayerStateRequest {
let current_position = i32::try_from(position_ms.max(0)).ok();
let current_queue_item = current_queue_item_id.and_then(|qid| {
i32::try_from(qid)
.ok()
.map(|id| super::QconnectSetPlayerStateQueueItemPayload {
queue_version: Some(super::QconnectQueueVersionPayload {
major: queue_version.major,
minor: queue_version.minor,
}),
id: Some(id),
})
});
super::QconnectSetPlayerStateRequest {
playing_state: None,
current_position,
current_queue_item,
}
}
/// Report volume change to QConnect server.
#[tauri::command]
pub async fn v2_qconnect_report_volume(
+47
View File
@@ -1510,6 +1510,53 @@ impl QconnectServiceState {
Ok(true)
}
pub(super) async fn set_position_if_remote(
&self,
position_ms: i64,
app_handle: &AppHandle,
) -> Result<bool, String> {
let remote_context = self.effective_remote_renderer_snapshot().await?;
let Some((renderer, queue, session)) = remote_context else {
return Ok(false);
};
let current_queue_item_id = renderer
.current_track
.as_ref()
.map(|item| item.queue_item_id);
let request = super::commands::build_set_position_player_state_request(
position_ms,
current_queue_item_id,
QconnectQueueVersionPayload {
major: queue.version.major,
minor: queue.version.minor,
},
);
let payload = serde_json::to_value(request)
.map_err(|err| format!("serialize set_position request: {err}"))?;
self.send_command(QueueCommandType::CtrlSrvrSetPlayerState, payload)
.await?;
if position_ms >= 0 {
self.update_renderer_position(position_ms as u64).await;
}
emit_qconnect_diagnostic(
app_handle,
"qconnect:set_position_handoff",
"info",
json!({
"active_renderer_id": session.active_renderer_id,
"local_renderer_id": session.local_renderer_id,
"position_ms": position_ms,
}),
);
Ok(true)
}
}
+33 -3
View File
@@ -1,6 +1,6 @@
use super::commands::{
build_qconnect_file_audio_quality_snapshot, classify_qconnect_audio_quality,
determine_queue_lookup_report_strategy,
build_qconnect_file_audio_quality_snapshot, build_set_position_player_state_request,
classify_qconnect_audio_quality, determine_queue_lookup_report_strategy,
should_skip_renderer_report_due_to_stale_snapshot,
};
use super::queue_resolution::{
@@ -15,7 +15,8 @@ use super::transport::{
};
use super::{
normalize_volume_to_fraction, QconnectHandoffIntent, QconnectOutboundCommandType,
QconnectRendererInfo, QconnectSessionState, QconnectTrackOrigin, AUDIO_QUALITY_HIRES_LEVEL1,
QconnectQueueVersionPayload, QconnectRendererInfo, QconnectSessionState, QconnectTrackOrigin,
AUDIO_QUALITY_HIRES_LEVEL1,
};
use qbz_models::RepeatMode;
use qconnect_app::{
@@ -1113,3 +1114,32 @@ fn watchdog_arms_only_for_playing_active_peer() {
// Do not arm when not an active peer (e.g. local renderer is active).
assert!(!should_arm_renderer_watchdog(Some(PLAYING_STATE_PLAYING), false));
}
#[test]
fn build_set_position_request_carries_position_and_queue_item_without_changing_play_state() {
let req = build_set_position_player_state_request(
42_000,
Some(7),
QconnectQueueVersionPayload { major: 3, minor: 1 },
);
assert_eq!(
req.playing_state, None,
"seek must not change play/pause state"
);
assert_eq!(req.current_position, Some(42_000));
let item = req.current_queue_item.expect("queue item present");
assert_eq!(item.id, Some(7));
let ver = item.queue_version.expect("queue version present");
assert_eq!((ver.major, ver.minor), (3, 1));
}
#[test]
fn build_set_position_request_omits_queue_item_when_unknown() {
let req = build_set_position_player_state_request(
1_000,
None,
QconnectQueueVersionPayload { major: 1, minor: 0 },
);
assert!(req.current_queue_item.is_none());
assert_eq!(req.current_position, Some(1_000));
}