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:
4 files changed
+122
-3
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user