feat(qconnect-app): apply renderer commands from WS transport events

-cx
This commit is contained in:
vicrodh committed 2026-02-21 12:35:20 -06:00
1 parent 0124728aa9
commit fc0135246a
4 files changed
+142 -8

No files matched your search

+125 -4
View File
@@ -4,12 +4,12 @@ use std::{
};
use qconnect_core::{
apply_event, telemetry, PendingCorrelation, PendingQueueAction, QConnectQueueState, QueueEvent,
QueueItem,
apply_event, apply_renderer_command, telemetry, PendingCorrelation, PendingQueueAction,
QConnectQueueState, QConnectRendererState, QueueEvent, QueueItem, RendererCommand,
};
use qconnect_protocol::{
build_qconnect_outbound_envelope, parse_inbound_event, InboundEnvelope, QueueCommand,
QueueCommandType, QueueEventType, QueueServerEvent,
QueueCommandType, QueueEventType, QueueServerEvent, RendererCommandType, RendererServerCommand,
};
use qconnect_transport_ws::{TransportEvent, WsTransport, WsTransportConfig};
use serde_json::Value;
@@ -94,6 +94,10 @@ where
self.state.lock().await.queue.clone()
}
pub async fn renderer_state_snapshot(&self) -> QConnectRendererState {
self.state.lock().await.renderer.clone()
}
pub async fn build_queue_command(
&self,
command_type: QueueCommandType,
@@ -171,6 +175,9 @@ where
TransportEvent::InboundQueueServerEvent(event) => {
self.apply_server_event(event).await?;
}
TransportEvent::InboundRendererServerCommand(command) => {
self.apply_renderer_server_command(command).await;
}
TransportEvent::Authenticated
| TransportEvent::Subscribed
| TransportEvent::ReconnectScheduled { .. }
@@ -269,6 +276,22 @@ where
Ok(())
}
async fn apply_renderer_server_command(&self, command: RendererServerCommand) {
let Some(renderer_command) = map_renderer_server_command(&command) else {
return;
};
let snapshot = {
let mut state = self.state.lock().await;
apply_renderer_command(&mut state.renderer, &renderer_command, now_ms());
state.renderer.clone()
};
self.sink
.on_event(QconnectAppEvent::RendererUpdated(snapshot))
.await;
}
fn spawn_pending_timeout_watch(&self, action_uuid: String) {
let app = self.clone();
tokio::spawn(async move {
@@ -472,6 +495,40 @@ fn map_server_event(event: &QueueServerEvent, current: &QConnectQueueState) -> Q
}
}
fn map_renderer_server_command(command: &RendererServerCommand) -> Option<RendererCommand> {
match command.command_type {
RendererCommandType::SrvrRndrSetState => Some(RendererCommand::SetState {
playing_state: parse_i32(&command.payload, "playing_state"),
current_position_ms: parse_u64(&command.payload, "current_position"),
current_track: parse_renderer_track(&command.payload, "current_track"),
next_track: parse_renderer_track(&command.payload, "next_track"),
}),
RendererCommandType::SrvrRndrSetVolume => Some(RendererCommand::SetVolume {
volume: parse_i32(&command.payload, "volume"),
volume_delta: parse_i32(&command.payload, "volume_delta"),
}),
RendererCommandType::SrvrRndrSetActive => Some(RendererCommand::SetActive {
active: parse_bool(&command.payload, "active", false),
}),
RendererCommandType::SrvrRndrSetMaxAudioQuality => {
parse_i32(&command.payload, "max_audio_quality")
.map(|max_audio_quality| RendererCommand::SetMaxAudioQuality { max_audio_quality })
}
RendererCommandType::SrvrRndrSetLoopMode => parse_i32(&command.payload, "loop_mode")
.map(|loop_mode| RendererCommand::SetLoopMode { loop_mode }),
RendererCommandType::SrvrRndrSetShuffleMode => Some(RendererCommand::SetShuffleMode {
shuffle_mode: parse_bool(&command.payload, "shuffle_mode", false),
}),
RendererCommandType::SrvrRndrMuteVolume => Some(RendererCommand::MuteVolume {
value: parse_bool(&command.payload, "value", false),
}),
}
}
fn parse_renderer_track(payload: &Value, field: &str) -> Option<QueueItem> {
payload.get(field).and_then(parse_queue_item)
}
fn parse_queue_items(payload: &Value, field: &str) -> Vec<QueueItem> {
payload
.get(field)
@@ -530,6 +587,10 @@ fn parse_optional_bool(payload: &Value, field: &str) -> Option<bool> {
payload.get(field).and_then(Value::as_bool)
}
fn parse_i32(payload: &Value, field: &str) -> Option<i32> {
payload.get(field).and_then(value_as_i32)
}
fn parse_usize_list(payload: &Value, field: &str) -> Vec<usize> {
payload
.get(field)
@@ -550,6 +611,13 @@ fn value_as_u64(value: &Value) -> Option<u64> {
.or_else(|| value.as_i64().and_then(|entry| u64::try_from(entry).ok()))
}
fn value_as_i32(value: &Value) -> Option<i32> {
value
.as_i64()
.and_then(|entry| i32::try_from(entry).ok())
.or_else(|| value.as_u64().and_then(|entry| i32::try_from(entry).ok()))
}
fn value_to_string(value: &Value) -> String {
value
.as_str()
@@ -577,7 +645,10 @@ mod tests {
use crate::{QconnectAppEvent, QconnectEventSink};
use super::QconnectApp;
use qconnect_protocol::{QueueCommandType, QueueEventType, QueueServerEvent};
use qconnect_protocol::{
QueueCommandType, QueueEventType, QueueServerEvent, RendererCommandType,
RendererServerCommand,
};
use qconnect_transport_ws::WsTransportConfig;
#[derive(Debug, Default, Clone)]
@@ -737,4 +808,54 @@ mod tests {
"ignored late queue error should not mutate queue"
);
}
#[tokio::test]
async fn inbound_renderer_command_updates_renderer_state() {
let (app, sink, _, _events_rx) = build_connected_app().await;
app.apply_renderer_server_command(RendererServerCommand {
command_type: RendererCommandType::SrvrRndrSetState,
payload: json!({
"playing_state": 2,
"current_position": 65321,
"current_track": {
"track_context_uuid": "ctx-remote",
"track_id": 777001,
"queue_item_id": 991
}
}),
})
.await;
app.apply_renderer_server_command(RendererServerCommand {
command_type: RendererCommandType::SrvrRndrSetVolume,
payload: json!({
"volume": 52,
"volume_delta": 3
}),
})
.await;
let renderer = app.renderer_state_snapshot().await;
assert_eq!(renderer.playing_state, Some(2));
assert_eq!(renderer.current_position_ms, Some(65_321));
assert_eq!(renderer.volume, Some(55));
assert_eq!(renderer.volume_delta, Some(3));
assert_eq!(
renderer
.current_track
.as_ref()
.map(|item| item.queue_item_id),
Some(991)
);
let events = sink.snapshot().await;
assert!(
events
.iter()
.filter(|event| matches!(event, QconnectAppEvent::RendererUpdated(_)))
.count()
>= 2
);
}
}
+2 -1
View File
@@ -1,5 +1,5 @@
use async_trait::async_trait;
use qconnect_core::QConnectQueueState;
use qconnect_core::{QConnectQueueState, QConnectRendererState};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -7,6 +7,7 @@ pub enum QconnectAppEvent {
TransportConnected,
TransportDisconnected,
QueueUpdated(QConnectQueueState),
RendererUpdated(QConnectRendererState),
PendingActionStarted {
uuid: String,
},
+2 -1
View File
@@ -1,8 +1,9 @@
use qconnect_core::{PendingActionSlot, QConnectQueueState};
use qconnect_core::{PendingActionSlot, QConnectQueueState, QConnectRendererState};
#[derive(Debug, Default)]
pub struct QconnectRuntimeState {
pub queue: QConnectQueueState,
pub renderer: QConnectRendererState,
pub pending: PendingActionSlot,
pub transport_connected: bool,
pub concurrency_canceled_action_uuid: Option<String>,