From d865097a4ca457a50ef360d111e8b269a3fa0174 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 1 Jun 2026 03:20:09 -0600 Subject: [PATCH] feat(qconnect-protocol): decode PlaybackErrorMessage + ErrorType Models the renderer per-track failure (TRACK_NOT_STREAMABLE/NETWORK_ERROR/...). Envelope-vs-batch placement verified against transport bytes: the decoder receives the batch QConnectMessages (CloudPayload.payload tag 7), and the official Message record carries playback_error at tag 3, so the field is attached to QConnectMessage inside messages[], NOT to the batch wrapper. cc --- crates/qconnect-protocol/src/decoder.rs | 58 ++++++++++++++++++- crates/qconnect-protocol/src/event.rs | 34 +++++++++++ crates/qconnect-protocol/src/lib.rs | 3 +- .../src/queue_command_proto.rs | 19 ++++++ 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/crates/qconnect-protocol/src/decoder.rs b/crates/qconnect-protocol/src/decoder.rs index 05b933efb..c99b918e2 100644 --- a/crates/qconnect-protocol/src/decoder.rs +++ b/crates/qconnect-protocol/src/decoder.rs @@ -35,6 +35,26 @@ pub fn decode_queue_server_events(payload: &[u8]) -> Result Option { + let batch = QConnectMessages::decode(payload).ok()?; + let msg = batch + .messages + .into_iter() + .find_map(|message| message.playback_error)?; + Some(crate::event::PlaybackErrorEvent { + queue_item_id: i32_to_u64(msg.queue_item_id.unwrap_or(0)).ok()?, + error_type: crate::event::ErrorType::from_wire(msg.error_type.unwrap_or(0)), + queue_version: queue_version_opt(msg.queue_version).ok().flatten(), + }) +} + fn decode_queue_server_event( message: QConnectMessage, ) -> Result, ProtocolError> { @@ -1065,12 +1085,44 @@ mod tests { use prost::Message; use crate::queue_command_proto::{ - QConnectMessage, QConnectMessageType, QConnectMessages, QueueTrack, QueueTrackWithContext, - QueueTracksAddedMessage, QueueTracksLoadedMessage, QueueVersionRef, + PlaybackErrorMessage, QConnectMessage, QConnectMessageType, QConnectMessages, QueueTrack, + QueueTrackWithContext, QueueTracksAddedMessage, QueueTracksLoadedMessage, QueueVersionRef, RendererMuteVolumeMessage, RendererSetStateMessage, }; + use crate::ErrorType; - use super::{decode_queue_server_events, decode_renderer_server_commands}; + use super::{ + decode_playback_error, decode_queue_server_events, decode_renderer_server_commands, + }; + + #[test] + fn decodes_playback_error_track_not_streamable() { + // playback_error rides at tag 3 of a QConnectMessage inside the batch + // messages[], not on the QConnectMessages wrapper (verified from the + // transport bytes: decoder receives the batch). + let message = QConnectMessage { + // message_type = PLAYBACK_ERROR(2) per Common.MessageType. + message_type: Some(2), + playback_error: Some(PlaybackErrorMessage { + queue_version: Some(QueueVersionRef { + major: Some(2), + minor: Some(0), + }), + queue_item_id: Some(55), + error_type: Some(2), // TRACK_NOT_STREAMABLE + }), + ..Default::default() + }; + let batch = QConnectMessages { + messages_time: None, + messages_id: None, + messages: vec![message], + }; + let bytes = batch.encode_to_vec(); + let parsed = decode_playback_error(&bytes).expect("playback error decoded"); + assert_eq!(parsed.queue_item_id, 55); + assert_eq!(parsed.error_type, ErrorType::TrackNotStreamable); + } #[test] fn decodes_tracks_added_server_event_batch() { diff --git a/crates/qconnect-protocol/src/event.rs b/crates/qconnect-protocol/src/event.rs index 934a07f74..bb3aaea39 100644 --- a/crates/qconnect-protocol/src/event.rs +++ b/crates/qconnect-protocol/src/event.rs @@ -108,3 +108,37 @@ impl QueueServerEvent { self.event_type.as_message_type() } } + +/// Common.ErrorType classification for a renderer per-track playback failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ErrorType { + Unknown, + TrackNotFound, + TrackNotStreamable, + TrackMusicDataInvalid, + ServiceError, + NetworkError, + OtherErrors, +} + +impl ErrorType { + pub fn from_wire(value: i32) -> Self { + match value { + 1 => Self::TrackNotFound, + 2 => Self::TrackNotStreamable, + 3 => Self::TrackMusicDataInvalid, + 4 => Self::ServiceError, + 5 => Self::NetworkError, + 100 => Self::OtherErrors, + _ => Self::Unknown, + } + } +} + +/// Renderer per-track failure surfaced from `PlaybackErrorMessage`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PlaybackErrorEvent { + pub queue_item_id: u64, + pub error_type: ErrorType, + pub queue_version: Option, +} diff --git a/crates/qconnect-protocol/src/lib.rs b/crates/qconnect-protocol/src/lib.rs index a29f0a69a..bc53df0cc 100644 --- a/crates/qconnect-protocol/src/lib.rs +++ b/crates/qconnect-protocol/src/lib.rs @@ -12,10 +12,11 @@ mod renderer; mod wire; pub use command::{QueueCommand, QueueCommandType}; +pub use decoder::decode_playback_error; pub use decoder::decode_queue_server_events; pub use decoder::decode_renderer_server_commands; pub use error::ProtocolError; -pub use event::{QueueEventType, QueueServerEvent}; +pub use event::{ErrorType, PlaybackErrorEvent, QueueEventType, QueueServerEvent}; pub use mapper::{ build_qconnect_outbound_envelope, build_qconnect_renderer_outbound_envelope, encode_queue_command_batch, encode_renderer_report_batch, diff --git a/crates/qconnect-protocol/src/queue_command_proto.rs b/crates/qconnect-protocol/src/queue_command_proto.rs index f1f851a19..640703b6a 100644 --- a/crates/qconnect-protocol/src/queue_command_proto.rs +++ b/crates/qconnect-protocol/src/queue_command_proto.rs @@ -872,10 +872,29 @@ pub struct QConnectMessages { pub messages: Vec, } +/// Renderer per-track failure (unstreamable, geo-block, network, ...). Rides at +/// tag 3 of each `QConnectMessage` (`message_type = PLAYBACK_ERROR(2)`). +/// Shape: queue_version + queue_item_id + error_type (Common.ErrorType). +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PlaybackErrorMessage { + #[prost(message, optional, tag = "1")] + pub queue_version: Option, + #[prost(int32, optional, tag = "2")] + pub queue_item_id: Option, + #[prost(int32, optional, tag = "3")] + pub error_type: Option, +} + #[derive(Clone, PartialEq, ::prost::Message)] pub struct QConnectMessage { #[prost(int32, optional, tag = "1")] pub message_type: Option, + // Tag 3 of the official `Message` envelope record (message_type #1; error #2; + // playback_error #3; oneof payloads 21-105). Resolved from the actual transport + // bytes: the decoder receives the batch `QConnectMessages` and `playback_error` + // rides inside each `messages[]` entry, NOT on the batch wrapper. + #[prost(message, optional, tag = "3")] + pub playback_error: Option, #[prost(message, optional, tag = "21")] pub rndr_srvr_join_session: Option, #[prost(message, optional, tag = "22")]