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_set_volume_if_remote,
|
||||||
qconnect::v2_qconnect_mute_if_remote,
|
qconnect::v2_qconnect_mute_if_remote,
|
||||||
qconnect::v2_qconnect_stop_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_toggle_shuffle_if_remote,
|
||||||
qconnect::v2_qconnect_cycle_repeat_if_remote,
|
qconnect::v2_qconnect_cycle_repeat_if_remote,
|
||||||
qconnect::v2_qconnect_set_autoplay_mode_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)
|
.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]
|
#[tauri::command]
|
||||||
pub async fn v2_qconnect_toggle_shuffle_if_remote(
|
pub async fn v2_qconnect_toggle_shuffle_if_remote(
|
||||||
app_handle: AppHandle,
|
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())
|
|| (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.
|
/// Report volume change to QConnect server.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn v2_qconnect_report_volume(
|
pub async fn v2_qconnect_report_volume(
|
||||||
|
|||||||
@@ -1510,6 +1510,53 @@ impl QconnectServiceState {
|
|||||||
|
|
||||||
Ok(true)
|
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::{
|
use super::commands::{
|
||||||
build_qconnect_file_audio_quality_snapshot, classify_qconnect_audio_quality,
|
build_qconnect_file_audio_quality_snapshot, build_set_position_player_state_request,
|
||||||
determine_queue_lookup_report_strategy,
|
classify_qconnect_audio_quality, determine_queue_lookup_report_strategy,
|
||||||
should_skip_renderer_report_due_to_stale_snapshot,
|
should_skip_renderer_report_due_to_stale_snapshot,
|
||||||
};
|
};
|
||||||
use super::queue_resolution::{
|
use super::queue_resolution::{
|
||||||
@@ -15,7 +15,8 @@ use super::transport::{
|
|||||||
};
|
};
|
||||||
use super::{
|
use super::{
|
||||||
normalize_volume_to_fraction, QconnectHandoffIntent, QconnectOutboundCommandType,
|
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 qbz_models::RepeatMode;
|
||||||
use qconnect_app::{
|
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).
|
// Do not arm when not an active peer (e.g. local renderer is active).
|
||||||
assert!(!should_arm_renderer_watchdog(Some(PLAYING_STATE_PLAYING), false));
|
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