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
This commit is contained in:
vicrodh committed 2026-06-01 03:20:09 -06:00
1 parent 92695c222a
commit d865097a4c
4 files changed
+110 -4

No files matched your search

+55 -3
View File
@@ -35,6 +35,26 @@ pub fn decode_queue_server_events(payload: &[u8]) -> Result<Vec<QueueServerEvent
Ok(events)
}
/// Decode the first renderer `PlaybackErrorMessage` from a batch, if any.
///
/// `playback_error` rides at tag 3 of each `QConnectMessage` inside the batch
/// `messages[]` (the official `Message` envelope record: message_type #1,
/// error #2, playback_error #3, oneof payloads 21-105). This is resolved from
/// the actual transport bytes: the decoder receives the batch `QConnectMessages`,
/// so the field lives on the per-message record, not on the batch wrapper.
pub fn decode_playback_error(payload: &[u8]) -> Option<crate::event::PlaybackErrorEvent> {
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<Option<QueueServerEvent>, 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() {
+34
View File
@@ -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<QueueVersion>,
}
+2 -1
View File
@@ -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,
@@ -872,10 +872,29 @@ pub struct QConnectMessages {
pub messages: Vec<QConnectMessage>,
}
/// 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<QueueVersionRef>,
#[prost(int32, optional, tag = "2")]
pub queue_item_id: Option<i32>,
#[prost(int32, optional, tag = "3")]
pub error_type: Option<i32>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct QConnectMessage {
#[prost(int32, optional, tag = "1")]
pub message_type: Option<i32>,
// 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<PlaybackErrorMessage>,
#[prost(message, optional, tag = "21")]
pub rndr_srvr_join_session: Option<JoinSessionMessage>,
#[prost(message, optional, tag = "22")]