fix(qconnect): add device capabilities, initial state, and channel subscriptions
- Add DeviceCapabilitiesMessage to protobuf (tag 7 on DeviceInfo) with volume_remote_control=ALLOWED, min/max audio quality - Extend JoinSessionMessage with reason, initial_state, is_active fields matching Android renderer proto (tags 3-5) - Bootstrap sends renderer JoinSession with is_active=true and initial RendererState (STOPPED) so other devices see QBZ immediately - Send initial StateUpdated, VolumeChanged, MaxAudioQualityChanged reports after join for full renderer presence - Default subscribe channels to 0x01,0x02,0x03 instead of empty - Request queue state from server after bootstrap cc
This commit is contained in:
3 files changed
+232
-26
No files matched your search
@@ -11,16 +11,16 @@ use crate::{
|
|||||||
build_outbound_envelope,
|
build_outbound_envelope,
|
||||||
queue_command_proto::{
|
queue_command_proto::{
|
||||||
AskForQueueStateMessage, AskForRendererStateMessage, AutoplayLoadTracksMessage,
|
AskForQueueStateMessage, AskForRendererStateMessage, AutoplayLoadTracksMessage,
|
||||||
AutoplayRemoveTracksMessage, ClearQueueMessage, DeviceInfoMessage, JoinSessionMessage,
|
AutoplayRemoveTracksMessage, ClearQueueMessage, DeviceCapabilitiesMessage,
|
||||||
MuteVolumeMessage, PlaybackPositionMessage, QConnectMessage, QConnectMessageType,
|
DeviceInfoMessage, JoinSessionMessage, MuteVolumeMessage, PlaybackPositionMessage,
|
||||||
QConnectMessages, QueueAddTracksMessage, QueueInsertTracksMessage, QueueLoadTracksMessage,
|
QConnectMessage, QConnectMessageType, QConnectMessages, QueueAddTracksMessage,
|
||||||
QueueRemoveTracksMessage, QueueReorderTracksMessage, QueueVersionRef,
|
QueueInsertTracksMessage, QueueLoadTracksMessage, QueueRemoveTracksMessage,
|
||||||
RendererFileAudioQualityChangedMessage, RendererMaxAudioQualityChangedMessage,
|
QueueReorderTracksMessage, QueueVersionRef, RendererFileAudioQualityChangedMessage,
|
||||||
RendererStateMessage, RendererStateUpdatedMessage, RendererVolumeChangedMessage,
|
RendererMaxAudioQualityChangedMessage, RendererStateMessage, RendererStateUpdatedMessage,
|
||||||
RendererVolumeMutedMessage, SetActiveRendererMessage, SetAutoplayModeMessage,
|
RendererVolumeChangedMessage, RendererVolumeMutedMessage, SetActiveRendererMessage,
|
||||||
SetLoopModeMessage, SetMaxAudioQualityMessage, SetPlayerStateMessage,
|
SetAutoplayModeMessage, SetLoopModeMessage, SetMaxAudioQualityMessage,
|
||||||
SetPlayerStateQueueItemMessage, SetQueueStateMessage, SetQueueTrackWithContext,
|
SetPlayerStateMessage, SetPlayerStateQueueItemMessage, SetQueueStateMessage,
|
||||||
SetShuffleModeMessage, SetVolumeMessage,
|
SetQueueTrackWithContext, SetShuffleModeMessage, SetVolumeMessage,
|
||||||
},
|
},
|
||||||
OutboundEnvelope, ProtocolError, QueueCommand, QueueCommandType, RendererReport,
|
OutboundEnvelope, ProtocolError, QueueCommand, QueueCommandType, RendererReport,
|
||||||
RendererReportType,
|
RendererReportType,
|
||||||
@@ -86,6 +86,10 @@ fn map_queue_command(command: &QueueCommand) -> Result<QConnectMessage, Protocol
|
|||||||
&["session_uuid", "sessionUuid"],
|
&["session_uuid", "sessionUuid"],
|
||||||
)?,
|
)?,
|
||||||
device_info: parse_device_info(&command.payload)?,
|
device_info: parse_device_info(&command.payload)?,
|
||||||
|
// Controller JoinSession doesn't use renderer-specific fields
|
||||||
|
reason: None,
|
||||||
|
initial_state: None,
|
||||||
|
is_active: None,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
@@ -390,14 +394,26 @@ fn map_queue_command(command: &QueueCommand) -> Result<QConnectMessage, Protocol
|
|||||||
|
|
||||||
fn map_renderer_report(report: &RendererReport) -> Result<QConnectMessage, ProtocolError> {
|
fn map_renderer_report(report: &RendererReport) -> Result<QConnectMessage, ProtocolError> {
|
||||||
match report.report_type {
|
match report.report_type {
|
||||||
RendererReportType::RndrSrvrJoinSession => Ok(QConnectMessage {
|
RendererReportType::RndrSrvrJoinSession => {
|
||||||
message_type: Some(QConnectMessageType::MessageTypeRndrSrvrJoinSession as i32),
|
let initial_state = parse_renderer_initial_state(&report.payload)?;
|
||||||
rndr_srvr_join_session: Some(JoinSessionMessage {
|
let is_active = Some(optional_bool(&report.payload, "is_active", false));
|
||||||
session_uuid: optional_uuid_bytes(&report.payload, &["session_uuid", "sessionUuid"])?,
|
let reason = optional_i32(&report.payload, "reason")?;
|
||||||
device_info: parse_device_info(&report.payload)?,
|
|
||||||
}),
|
Ok(QConnectMessage {
|
||||||
..Default::default()
|
message_type: Some(QConnectMessageType::MessageTypeRndrSrvrJoinSession as i32),
|
||||||
}),
|
rndr_srvr_join_session: Some(JoinSessionMessage {
|
||||||
|
session_uuid: optional_uuid_bytes(
|
||||||
|
&report.payload,
|
||||||
|
&["session_uuid", "sessionUuid"],
|
||||||
|
)?,
|
||||||
|
device_info: parse_device_info(&report.payload)?,
|
||||||
|
reason,
|
||||||
|
initial_state,
|
||||||
|
is_active,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
RendererReportType::RndrSrvrDeviceInfoUpdated => Ok(QConnectMessage {
|
RendererReportType::RndrSrvrDeviceInfoUpdated => Ok(QConnectMessage {
|
||||||
message_type: Some(QConnectMessageType::MessageTypeRndrSrvrDeviceInfoUpdated as i32),
|
message_type: Some(QConnectMessageType::MessageTypeRndrSrvrDeviceInfoUpdated as i32),
|
||||||
rndr_srvr_device_info_updated: parse_device_info(&report.payload)?,
|
rndr_srvr_device_info_updated: parse_device_info(&report.payload)?,
|
||||||
@@ -700,6 +716,7 @@ fn parse_device_info(payload: &Value) -> Result<Option<DeviceInfoMessage>, Proto
|
|||||||
let serial_number = optional_string_any(source, &["serial_number", "serialNumber"]);
|
let serial_number = optional_string_any(source, &["serial_number", "serialNumber"]);
|
||||||
let device_type = optional_i32_any(source, &["device_type", "deviceType"])?;
|
let device_type = optional_i32_any(source, &["device_type", "deviceType"])?;
|
||||||
let software_version = optional_string_any(source, &["software_version", "softwareVersion"]);
|
let software_version = optional_string_any(source, &["software_version", "softwareVersion"]);
|
||||||
|
let capabilities = parse_device_capabilities(source)?;
|
||||||
|
|
||||||
if device_uuid.is_none()
|
if device_uuid.is_none()
|
||||||
&& friendly_name.is_none()
|
&& friendly_name.is_none()
|
||||||
@@ -708,6 +725,7 @@ fn parse_device_info(payload: &Value) -> Result<Option<DeviceInfoMessage>, Proto
|
|||||||
&& serial_number.is_none()
|
&& serial_number.is_none()
|
||||||
&& device_type.is_none()
|
&& device_type.is_none()
|
||||||
&& software_version.is_none()
|
&& software_version.is_none()
|
||||||
|
&& capabilities.is_none()
|
||||||
{
|
{
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -719,10 +737,87 @@ fn parse_device_info(payload: &Value) -> Result<Option<DeviceInfoMessage>, Proto
|
|||||||
model,
|
model,
|
||||||
serial_number,
|
serial_number,
|
||||||
device_type,
|
device_type,
|
||||||
|
capabilities,
|
||||||
software_version,
|
software_version,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_device_capabilities(
|
||||||
|
source: &Value,
|
||||||
|
) -> Result<Option<DeviceCapabilitiesMessage>, ProtocolError> {
|
||||||
|
let nested = source
|
||||||
|
.get("capabilities")
|
||||||
|
.or_else(|| source.get("deviceCapabilities"));
|
||||||
|
|
||||||
|
if let Some(caps) = nested {
|
||||||
|
if caps.is_object() {
|
||||||
|
return Ok(Some(DeviceCapabilitiesMessage {
|
||||||
|
min_audio_quality: optional_i32_any(
|
||||||
|
caps,
|
||||||
|
&["min_audio_quality", "minAudioQuality"],
|
||||||
|
)?,
|
||||||
|
max_audio_quality: optional_i32_any(
|
||||||
|
caps,
|
||||||
|
&["max_audio_quality", "maxAudioQuality"],
|
||||||
|
)?,
|
||||||
|
volume_remote_control: optional_i32_any(
|
||||||
|
caps,
|
||||||
|
&["volume_remote_control", "volumeRemoteControl"],
|
||||||
|
)?,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check flat fields (capabilities declared directly on device_info payload)
|
||||||
|
let min_aq = optional_i32_any(source, &["min_audio_quality", "minAudioQuality"])?;
|
||||||
|
let max_aq = optional_i32_any(source, &["max_audio_quality", "maxAudioQuality"])?;
|
||||||
|
let vol_rc = optional_i32_any(source, &["volume_remote_control", "volumeRemoteControl"])?;
|
||||||
|
|
||||||
|
if min_aq.is_some() || max_aq.is_some() || vol_rc.is_some() {
|
||||||
|
return Ok(Some(DeviceCapabilitiesMessage {
|
||||||
|
min_audio_quality: min_aq,
|
||||||
|
max_audio_quality: max_aq,
|
||||||
|
volume_remote_control: vol_rc,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_renderer_initial_state(
|
||||||
|
payload: &Value,
|
||||||
|
) -> Result<Option<RendererStateMessage>, ProtocolError> {
|
||||||
|
let nested = payload
|
||||||
|
.get("initial_state")
|
||||||
|
.or_else(|| payload.get("initialState"));
|
||||||
|
|
||||||
|
let Some(source) = nested else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !source.is_object() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_position = optional_i32(source, "current_position")?;
|
||||||
|
let playback_position = current_position.map(|value| PlaybackPositionMessage {
|
||||||
|
timestamp: Some(now_ms()),
|
||||||
|
value: Some(value),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Some(RendererStateMessage {
|
||||||
|
playing_state: optional_i32(source, "playing_state")?,
|
||||||
|
buffer_state: optional_i32(source, "buffer_state")?,
|
||||||
|
current_position: playback_position,
|
||||||
|
duration: optional_i32(source, "duration")?,
|
||||||
|
queue_version: optional_queue_version(source, "queue_version")?
|
||||||
|
.map(|v| to_proto_queue_version(v))
|
||||||
|
.transpose()?,
|
||||||
|
current_queue_item_id: optional_i32(source, "current_queue_item_id")?,
|
||||||
|
next_queue_item_id: optional_i32(source, "next_queue_item_id")?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_set_player_state_queue_item(
|
fn parse_set_player_state_queue_item(
|
||||||
payload: &Value,
|
payload: &Value,
|
||||||
) -> Result<Option<SetPlayerStateQueueItemMessage>, ProtocolError> {
|
) -> Result<Option<SetPlayerStateQueueItemMessage>, ProtocolError> {
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ pub struct SetQueueTrackWithContext {
|
|||||||
pub context_uuid: Option<Vec<u8>>,
|
pub context_uuid: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct DeviceCapabilitiesMessage {
|
||||||
|
#[prost(int32, optional, tag = "1")]
|
||||||
|
pub min_audio_quality: Option<i32>,
|
||||||
|
#[prost(int32, optional, tag = "2")]
|
||||||
|
pub max_audio_quality: Option<i32>,
|
||||||
|
#[prost(int32, optional, tag = "3")]
|
||||||
|
pub volume_remote_control: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct DeviceInfoMessage {
|
pub struct DeviceInfoMessage {
|
||||||
#[prost(bytes = "vec", optional, tag = "1")]
|
#[prost(bytes = "vec", optional, tag = "1")]
|
||||||
@@ -108,6 +118,8 @@ pub struct DeviceInfoMessage {
|
|||||||
pub serial_number: Option<String>,
|
pub serial_number: Option<String>,
|
||||||
#[prost(int32, optional, tag = "6")]
|
#[prost(int32, optional, tag = "6")]
|
||||||
pub device_type: Option<i32>,
|
pub device_type: Option<i32>,
|
||||||
|
#[prost(message, optional, tag = "7")]
|
||||||
|
pub capabilities: Option<DeviceCapabilitiesMessage>,
|
||||||
#[prost(string, optional, tag = "8")]
|
#[prost(string, optional, tag = "8")]
|
||||||
pub software_version: Option<String>,
|
pub software_version: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -118,6 +130,15 @@ pub struct JoinSessionMessage {
|
|||||||
pub session_uuid: Option<Vec<u8>>,
|
pub session_uuid: Option<Vec<u8>>,
|
||||||
#[prost(message, optional, tag = "2")]
|
#[prost(message, optional, tag = "2")]
|
||||||
pub device_info: Option<DeviceInfoMessage>,
|
pub device_info: Option<DeviceInfoMessage>,
|
||||||
|
/// Renderer-only: reason for joining (0=unknown, 1=controller_request, 2=reconnection)
|
||||||
|
#[prost(int32, optional, tag = "3")]
|
||||||
|
pub reason: Option<i32>,
|
||||||
|
/// Renderer-only: initial playback state sent on join
|
||||||
|
#[prost(message, optional, tag = "4")]
|
||||||
|
pub initial_state: Option<RendererStateMessage>,
|
||||||
|
/// Renderer-only: whether this renderer is active
|
||||||
|
#[prost(bool, optional, tag = "5")]
|
||||||
|
pub is_active: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
|||||||
@@ -28,14 +28,22 @@ const PLAYING_STATE_UNKNOWN: i32 = 0;
|
|||||||
const PLAYING_STATE_STOPPED: i32 = 1;
|
const PLAYING_STATE_STOPPED: i32 = 1;
|
||||||
const PLAYING_STATE_PLAYING: i32 = 2;
|
const PLAYING_STATE_PLAYING: i32 = 2;
|
||||||
const PLAYING_STATE_PAUSED: i32 = 3;
|
const PLAYING_STATE_PAUSED: i32 = 3;
|
||||||
|
const BUFFER_STATE_OK: i32 = 2;
|
||||||
const QCONNECT_QWS_TOKEN_KIND: &str = "jwt_qws";
|
const QCONNECT_QWS_TOKEN_KIND: &str = "jwt_qws";
|
||||||
const QCONNECT_QWS_CREATE_TOKEN_PATH: &str = "/qws/createToken";
|
const QCONNECT_QWS_CREATE_TOKEN_PATH: &str = "/qws/createToken";
|
||||||
const DEFAULT_QCONNECT_DEVICE_NAME: &str = "QBZ Desktop";
|
const DEFAULT_QCONNECT_DEVICE_NAME: &str = "QBZ Desktop";
|
||||||
const DEFAULT_QCONNECT_DEVICE_BRAND: &str = "QBZ";
|
const DEFAULT_QCONNECT_DEVICE_BRAND: &str = "QBZ";
|
||||||
const DEFAULT_QCONNECT_DEVICE_MODEL: &str = "QBZ";
|
const DEFAULT_QCONNECT_DEVICE_MODEL: &str = "QBZ";
|
||||||
const DEFAULT_QCONNECT_DEVICE_TYPE: i32 = 5;
|
const DEFAULT_QCONNECT_DEVICE_TYPE: i32 = 5; // computer
|
||||||
const DEFAULT_QCONNECT_SOFTWARE_PREFIX: &str = "qbz";
|
const DEFAULT_QCONNECT_SOFTWARE_PREFIX: &str = "qbz";
|
||||||
const QCONNECT_REMOTE_QUEUE_SOURCE: &str = "qobuz_connect_remote";
|
const QCONNECT_REMOTE_QUEUE_SOURCE: &str = "qobuz_connect_remote";
|
||||||
|
// AudioQuality enum: 0=unknown, 1=mp3, 2=cd, 3=hires_l1, 4=hires_l2(192k), 5=hires_l3(384k)
|
||||||
|
const AUDIO_QUALITY_MP3: i32 = 1;
|
||||||
|
const AUDIO_QUALITY_HIRES_LEVEL2: i32 = 4;
|
||||||
|
// VolumeRemoteControl enum: 0=unknown, 1=not_allowed, 2=allowed
|
||||||
|
const VOLUME_REMOTE_CONTROL_ALLOWED: i32 = 2;
|
||||||
|
// JoinSessionReason: 0=unknown, 1=controller_request, 2=reconnection
|
||||||
|
const JOIN_SESSION_REASON_CONTROLLER_REQUEST: i32 = 1;
|
||||||
static QCONNECT_DEVICE_UUID: OnceLock<String> = OnceLock::new();
|
static QCONNECT_DEVICE_UUID: OnceLock<String> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
@@ -190,6 +198,13 @@ pub struct QconnectAdmissionBlockedEvent {
|
|||||||
pub handoff_intent: QconnectHandoffIntent,
|
pub handoff_intent: QconnectHandoffIntent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct QconnectDeviceCapabilitiesPayload {
|
||||||
|
pub min_audio_quality: Option<i32>,
|
||||||
|
pub max_audio_quality: Option<i32>,
|
||||||
|
pub volume_remote_control: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct QconnectDeviceInfoPayload {
|
pub struct QconnectDeviceInfoPayload {
|
||||||
pub device_uuid: Option<String>,
|
pub device_uuid: Option<String>,
|
||||||
@@ -198,6 +213,7 @@ pub struct QconnectDeviceInfoPayload {
|
|||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
pub serial_number: Option<String>,
|
pub serial_number: Option<String>,
|
||||||
pub device_type: Option<i32>,
|
pub device_type: Option<i32>,
|
||||||
|
pub capabilities: Option<QconnectDeviceCapabilitiesPayload>,
|
||||||
pub software_version: Option<String>,
|
pub software_version: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,11 +851,23 @@ async fn bootstrap_remote_presence(
|
|||||||
let device_info = default_qconnect_device_info();
|
let device_info = default_qconnect_device_info();
|
||||||
let queue_version_ref = app.queue_state_snapshot().await.version;
|
let queue_version_ref = app.queue_state_snapshot().await.version;
|
||||||
|
|
||||||
let renderer_join_payload = serde_json::to_value(QconnectJoinSessionRequest {
|
// 1. Renderer JoinSession with is_active=true, initial state, and capabilities
|
||||||
session_uuid: None,
|
let renderer_join_payload = serde_json::json!({
|
||||||
device_info: Some(device_info.clone()),
|
"device_info": serde_json::to_value(&device_info)
|
||||||
})
|
.map_err(|err| format!("serialize device_info: {err}"))?,
|
||||||
.map_err(|err| format!("serialize renderer join_session bootstrap payload: {err}"))?;
|
"is_active": true,
|
||||||
|
"reason": JOIN_SESSION_REASON_CONTROLLER_REQUEST,
|
||||||
|
"initial_state": {
|
||||||
|
"playing_state": PLAYING_STATE_STOPPED,
|
||||||
|
"buffer_state": BUFFER_STATE_OK,
|
||||||
|
"current_position": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"queue_version": {
|
||||||
|
"major": queue_version_ref.major,
|
||||||
|
"minor": queue_version_ref.minor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
let renderer_join_report = RendererReport::new(
|
let renderer_join_report = RendererReport::new(
|
||||||
RendererReportType::RndrSrvrJoinSession,
|
RendererReportType::RndrSrvrJoinSession,
|
||||||
Uuid::new_v4().to_string(),
|
Uuid::new_v4().to_string(),
|
||||||
@@ -850,6 +878,49 @@ async fn bootstrap_remote_presence(
|
|||||||
.await
|
.await
|
||||||
.map_err(|err| format!("send bootstrap rndr_srvr_join_session failed: {err}"))?;
|
.map_err(|err| format!("send bootstrap rndr_srvr_join_session failed: {err}"))?;
|
||||||
|
|
||||||
|
// 2. Send initial StateUpdated report so other devices see our state
|
||||||
|
let state_report_payload = serde_json::json!({
|
||||||
|
"playing_state": PLAYING_STATE_STOPPED,
|
||||||
|
"buffer_state": BUFFER_STATE_OK,
|
||||||
|
"current_position": 0,
|
||||||
|
"duration": 0,
|
||||||
|
"queue_version": {
|
||||||
|
"major": queue_version_ref.major,
|
||||||
|
"minor": queue_version_ref.minor
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let state_report = RendererReport::new(
|
||||||
|
RendererReportType::RndrSrvrStateUpdated,
|
||||||
|
Uuid::new_v4().to_string(),
|
||||||
|
queue_version_ref,
|
||||||
|
state_report_payload,
|
||||||
|
);
|
||||||
|
app.send_renderer_report_command(state_report)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("send bootstrap rndr_srvr_state_updated failed: {err}"))?;
|
||||||
|
|
||||||
|
// 3. Report volume and max audio quality
|
||||||
|
let volume_report = RendererReport::new(
|
||||||
|
RendererReportType::RndrSrvrVolumeChanged,
|
||||||
|
Uuid::new_v4().to_string(),
|
||||||
|
queue_version_ref,
|
||||||
|
serde_json::json!({ "volume": 100 }),
|
||||||
|
);
|
||||||
|
app.send_renderer_report_command(volume_report)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("send bootstrap rndr_srvr_volume_changed failed: {err}"))?;
|
||||||
|
|
||||||
|
let max_quality_report = RendererReport::new(
|
||||||
|
RendererReportType::RndrSrvrMaxAudioQualityChanged,
|
||||||
|
Uuid::new_v4().to_string(),
|
||||||
|
queue_version_ref,
|
||||||
|
serde_json::json!({ "max_audio_quality": AUDIO_QUALITY_HIRES_LEVEL2 }),
|
||||||
|
);
|
||||||
|
app.send_renderer_report_command(max_quality_report)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("send bootstrap rndr_srvr_max_audio_quality_changed failed: {err}"))?;
|
||||||
|
|
||||||
|
// 4. Controller JoinSession to also participate as controller
|
||||||
let join_payload = serde_json::to_value(QconnectJoinSessionRequest {
|
let join_payload = serde_json::to_value(QconnectJoinSessionRequest {
|
||||||
session_uuid: None,
|
session_uuid: None,
|
||||||
device_info: Some(device_info),
|
device_info: Some(device_info),
|
||||||
@@ -862,12 +933,25 @@ async fn bootstrap_remote_presence(
|
|||||||
let join_action_uuid = app
|
let join_action_uuid = app
|
||||||
.send_queue_command(join_command)
|
.send_queue_command(join_command)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("send bootstrap join_session failed: {err}"))?;
|
.map_err(|err| format!("send bootstrap ctrl_srvr_join_session failed: {err}"))?;
|
||||||
|
|
||||||
// JoinSession typically responds with session/renderer controller events that are not part of
|
// JoinSession typically responds with session/renderer controller events that are not part of
|
||||||
// queue reducer correlation. Drop pending slot so queue operations are not blocked for 10s.
|
// queue reducer correlation. Drop pending slot so queue operations are not blocked for 10s.
|
||||||
clear_pending_if_matches(app, &join_action_uuid).await;
|
clear_pending_if_matches(app, &join_action_uuid).await;
|
||||||
|
|
||||||
|
// 5. Ask for current queue state from server
|
||||||
|
let ask_queue_payload = serde_json::json!({});
|
||||||
|
let ask_queue_command = app
|
||||||
|
.build_queue_command(QueueCommandType::CtrlSrvrAskForQueueState, ask_queue_payload)
|
||||||
|
.await;
|
||||||
|
let ask_action_uuid = app
|
||||||
|
.send_queue_command(ask_queue_command)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("send bootstrap ask_for_queue_state failed: {err}"))?;
|
||||||
|
clear_pending_if_matches(app, &ask_action_uuid).await;
|
||||||
|
|
||||||
|
log::info!("[QConnect] Bootstrap complete: renderer joined with is_active=true, initial state reported, controller joined, queue state requested");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,6 +1005,11 @@ fn default_qconnect_device_info() -> QconnectDeviceInfoPayload {
|
|||||||
model: Some(model),
|
model: Some(model),
|
||||||
serial_number: None,
|
serial_number: None,
|
||||||
device_type: Some(device_type),
|
device_type: Some(device_type),
|
||||||
|
capabilities: Some(QconnectDeviceCapabilitiesPayload {
|
||||||
|
min_audio_quality: Some(AUDIO_QUALITY_MP3),
|
||||||
|
max_audio_quality: Some(AUDIO_QUALITY_HIRES_LEVEL2),
|
||||||
|
volume_remote_control: Some(VOLUME_REMOTE_CONTROL_ALLOWED),
|
||||||
|
}),
|
||||||
software_version: Some(software_version),
|
software_version: Some(software_version),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1230,7 +1319,8 @@ async fn resolve_transport_config(
|
|||||||
.collect();
|
.collect();
|
||||||
parse_subscribe_channels(channels)?
|
parse_subscribe_channels(channels)?
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
// Default QConnect channels: connectionId(0x01), backend(0x02), controllers(0x03)
|
||||||
|
vec![vec![0x01], vec![0x02], vec![0x03]]
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = WsTransportConfig::default();
|
let mut config = WsTransportConfig::default();
|
||||||
|
|||||||
Reference in new issue
Block a user