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:
4 files changed
+110
-4
No files matched your search
@@ -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() {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
Reference in new issue
Block a user