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,
|
||||
queue_command_proto::{
|
||||
AskForQueueStateMessage, AskForRendererStateMessage, AutoplayLoadTracksMessage,
|
||||
AutoplayRemoveTracksMessage, ClearQueueMessage, DeviceInfoMessage, JoinSessionMessage,
|
||||
MuteVolumeMessage, PlaybackPositionMessage, QConnectMessage, QConnectMessageType,
|
||||
QConnectMessages, QueueAddTracksMessage, QueueInsertTracksMessage, QueueLoadTracksMessage,
|
||||
QueueRemoveTracksMessage, QueueReorderTracksMessage, QueueVersionRef,
|
||||
RendererFileAudioQualityChangedMessage, RendererMaxAudioQualityChangedMessage,
|
||||
RendererStateMessage, RendererStateUpdatedMessage, RendererVolumeChangedMessage,
|
||||
RendererVolumeMutedMessage, SetActiveRendererMessage, SetAutoplayModeMessage,
|
||||
SetLoopModeMessage, SetMaxAudioQualityMessage, SetPlayerStateMessage,
|
||||
SetPlayerStateQueueItemMessage, SetQueueStateMessage, SetQueueTrackWithContext,
|
||||
SetShuffleModeMessage, SetVolumeMessage,
|
||||
AutoplayRemoveTracksMessage, ClearQueueMessage, DeviceCapabilitiesMessage,
|
||||
DeviceInfoMessage, JoinSessionMessage, MuteVolumeMessage, PlaybackPositionMessage,
|
||||
QConnectMessage, QConnectMessageType, QConnectMessages, QueueAddTracksMessage,
|
||||
QueueInsertTracksMessage, QueueLoadTracksMessage, QueueRemoveTracksMessage,
|
||||
QueueReorderTracksMessage, QueueVersionRef, RendererFileAudioQualityChangedMessage,
|
||||
RendererMaxAudioQualityChangedMessage, RendererStateMessage, RendererStateUpdatedMessage,
|
||||
RendererVolumeChangedMessage, RendererVolumeMutedMessage, SetActiveRendererMessage,
|
||||
SetAutoplayModeMessage, SetLoopModeMessage, SetMaxAudioQualityMessage,
|
||||
SetPlayerStateMessage, SetPlayerStateQueueItemMessage, SetQueueStateMessage,
|
||||
SetQueueTrackWithContext, SetShuffleModeMessage, SetVolumeMessage,
|
||||
},
|
||||
OutboundEnvelope, ProtocolError, QueueCommand, QueueCommandType, RendererReport,
|
||||
RendererReportType,
|
||||
@@ -86,6 +86,10 @@ fn map_queue_command(command: &QueueCommand) -> Result<QConnectMessage, Protocol
|
||||
&["session_uuid", "sessionUuid"],
|
||||
)?,
|
||||
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()
|
||||
}),
|
||||
@@ -390,14 +394,26 @@ fn map_queue_command(command: &QueueCommand) -> Result<QConnectMessage, Protocol
|
||||
|
||||
fn map_renderer_report(report: &RendererReport) -> Result<QConnectMessage, ProtocolError> {
|
||||
match report.report_type {
|
||||
RendererReportType::RndrSrvrJoinSession => Ok(QConnectMessage {
|
||||
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)?,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
RendererReportType::RndrSrvrJoinSession => {
|
||||
let initial_state = parse_renderer_initial_state(&report.payload)?;
|
||||
let is_active = Some(optional_bool(&report.payload, "is_active", false));
|
||||
let reason = optional_i32(&report.payload, "reason")?;
|
||||
|
||||
Ok(QConnectMessage {
|
||||
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 {
|
||||
message_type: Some(QConnectMessageType::MessageTypeRndrSrvrDeviceInfoUpdated as i32),
|
||||
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 device_type = optional_i32_any(source, &["device_type", "deviceType"])?;
|
||||
let software_version = optional_string_any(source, &["software_version", "softwareVersion"]);
|
||||
let capabilities = parse_device_capabilities(source)?;
|
||||
|
||||
if device_uuid.is_none()
|
||||
&& friendly_name.is_none()
|
||||
@@ -708,6 +725,7 @@ fn parse_device_info(payload: &Value) -> Result<Option<DeviceInfoMessage>, Proto
|
||||
&& serial_number.is_none()
|
||||
&& device_type.is_none()
|
||||
&& software_version.is_none()
|
||||
&& capabilities.is_none()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -719,10 +737,87 @@ fn parse_device_info(payload: &Value) -> Result<Option<DeviceInfoMessage>, Proto
|
||||
model,
|
||||
serial_number,
|
||||
device_type,
|
||||
capabilities,
|
||||
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(
|
||||
payload: &Value,
|
||||
) -> Result<Option<SetPlayerStateQueueItemMessage>, ProtocolError> {
|
||||
|
||||
@@ -94,6 +94,16 @@ pub struct SetQueueTrackWithContext {
|
||||
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)]
|
||||
pub struct DeviceInfoMessage {
|
||||
#[prost(bytes = "vec", optional, tag = "1")]
|
||||
@@ -108,6 +118,8 @@ pub struct DeviceInfoMessage {
|
||||
pub serial_number: Option<String>,
|
||||
#[prost(int32, optional, tag = "6")]
|
||||
pub device_type: Option<i32>,
|
||||
#[prost(message, optional, tag = "7")]
|
||||
pub capabilities: Option<DeviceCapabilitiesMessage>,
|
||||
#[prost(string, optional, tag = "8")]
|
||||
pub software_version: Option<String>,
|
||||
}
|
||||
@@ -118,6 +130,15 @@ pub struct JoinSessionMessage {
|
||||
pub session_uuid: Option<Vec<u8>>,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
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)]
|
||||
|
||||
Reference in new issue
Block a user