Skip to main content

dvb_ci/ci_plus/
cicam_player.rs

1//! CICAM Player resource objects — ETSI TS 103 205 V1.4.1 §8.8, Tables 48-71
2//! (PDF pp. 86-96). See `docs/ts_103_205/cicam-player.md`.
3//!
4//! Resource ID `0x00930041` (Class 147, Type 1, Version 1). The CICAM Player
5//! resource lets the Host request the CICAM to initiate and play a service on its
6//! behalf (IP-delivery CICAM player mode). Tags live in the CI Plus `0x9FA0xx`
7//! namespace; the full 16-APDU set `9FA000`-`9FA00F` is implemented.
8//!
9//! - `CICAM_player_verify_req` (`9F A0 00`, Table 49) — Host → CICAM.
10//! - `CICAM_player_verify_reply` (`9F A0 01`, Table 50) — CICAM → Host.
11//! - `CICAM_player_capabilities_req` (`9F A0 02`, Table 52) — CICAM → Host, header-only.
12//! - `CICAM_player_capabilities_reply` (`9F A0 03`, Table 53) — Host → CICAM.
13//! - `CICAM_player_start_req` (`9F A0 04`, Table 54) — CICAM → Host.
14//! - `CICAM_player_start_reply` (`9F A0 05`, Table 55) — Host → CICAM.
15//! - `CICAM_player_play_req` (`9F A0 06`, Table 57) — Host → CICAM.
16//! - `CICAM_player_status_error` (`9F A0 07`, Table 58) — CICAM → Host.
17//! - `CICAM_player_control_req` (`9F A0 08`, Table 60) — Host → CICAM.
18//! - `CICAM_player_info_req` (`9F A0 09`, Table 63) — Host → CICAM.
19//! - `CICAM_player_info_reply` (`9F A0 0A`, Table 64) — CICAM → Host.
20//! - `CICAM_player_stop` (`9F A0 0B`, Table 65) — Host → CICAM.
21//! - `CICAM_player_end` (`9F A0 0C`, Table 66) — CICAM → Host.
22//! - `CICAM_player_asset_end` (`9F A0 0D`, Table 67) — CICAM → Host.
23//! - `CICAM_player_update_req` (`9F A0 0E`, Table 68) — CICAM → Host.
24//! - `CICAM_player_update_reply` (`9F A0 0F`, Table 69) — Host → CICAM.
25//!
26//! ## Table 69 tag slip
27//!
28//! Table 69 (PDF p. 95) literally prints the first syntax row as
29//! `CICAM_player_start_reply_tag` — an evident copy/paste slip from Table 55. The
30//! field-list text gives the authoritative tag `0x9FA00F`
31//! (`CICAM_player_update_reply_tag`); this module uses `0x9FA00F`.
32//!
33//! The `service_location_byte` bodies (`verify_req` / `play_req`) and the
34//! `PMT_byte` bodies (`start_req` / `update_req`) are opaque to the wire parser —
35//! carried as borrowed `&[u8]`.
36
37use crate::error::{Error, Result};
38use crate::objects;
39use crate::tag::ApduTag;
40use alloc::vec::Vec;
41use dvb_common::{Parse, Serialize};
42
43/// Resource-scoped `apdu_tag`s for the CICAM Player resource (Table 71).
44pub mod tag {
45    use crate::tag::ApduTag;
46    /// `CICAM_player_verify_req_tag` = `9F A0 00`.
47    pub const VERIFY_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x00);
48    /// `CICAM_player_verify_reply_tag` = `9F A0 01`.
49    pub const VERIFY_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x01);
50    /// `CICAM_player_capabilities_req_tag` = `9F A0 02`.
51    pub const CAPABILITIES_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x02);
52    /// `CICAM_player_capabilities_reply_tag` = `9F A0 03`.
53    pub const CAPABILITIES_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x03);
54    /// `CICAM_player_start_req_tag` = `9F A0 04`.
55    pub const START_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x04);
56    /// `CICAM_player_start_reply_tag` = `9F A0 05`.
57    pub const START_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x05);
58    /// `CICAM_player_play_req_tag` = `9F A0 06`.
59    pub const PLAY_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x06);
60    /// `CICAM_player_status_error_tag` = `9F A0 07`.
61    pub const STATUS_ERROR: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x07);
62    /// `CICAM_player_control_req_tag` = `9F A0 08`.
63    pub const CONTROL_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x08);
64    /// `CICAM_player_info_req_tag` = `9F A0 09`.
65    pub const INFO_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x09);
66    /// `CICAM_player_info_reply_tag` = `9F A0 0A`.
67    pub const INFO_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x0A);
68    /// `CICAM_player_stop_tag` = `9F A0 0B`.
69    pub const STOP: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x0B);
70    /// `CICAM_player_end_tag` = `9F A0 0C`.
71    pub const END: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x0C);
72    /// `CICAM_player_asset_end_tag` = `9F A0 0D`.
73    pub const ASSET_END: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x0D);
74    /// `CICAM_player_update_req_tag` = `9F A0 0E`.
75    pub const UPDATE_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x0E);
76    /// `CICAM_player_update_reply_tag` = `9F A0 0F`. (Table 69 prints a
77    /// copy/paste `CICAM_player_start_reply_tag` slip; the authoritative tag is
78    /// `0x9FA00F`.)
79    pub const UPDATE_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0xA0, 0x0F);
80}
81
82// --- player_verify_status (Table 51) ---
83
84/// `player_verify_status` values (Table 51).
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize))]
87#[non_exhaustive]
88pub enum PlayerVerifyStatus {
89    /// `0x00` — OK, service playback is possible.
90    Ok,
91    /// `0x01` — Error, service playback is not possible.
92    Error,
93    /// Reserved (`0x02`–`0xFF`).
94    Reserved(u8),
95}
96impl PlayerVerifyStatus {
97    /// Decode a `player_verify_status` byte.
98    #[must_use]
99    pub fn from_u8(v: u8) -> Self {
100        match v {
101            0x00 => Self::Ok,
102            0x01 => Self::Error,
103            other => Self::Reserved(other),
104        }
105    }
106    /// Wire byte.
107    #[must_use]
108    pub const fn to_u8(self) -> u8 {
109        match self {
110            Self::Ok => 0x00,
111            Self::Error => 0x01,
112            Self::Reserved(v) => v,
113        }
114    }
115    /// Spec token, or `"reserved"`.
116    #[must_use]
117    pub fn name(&self) -> &'static str {
118        match self {
119            Self::Ok => "ok",
120            Self::Error => "error",
121            Self::Reserved(_) => "reserved",
122        }
123    }
124}
125dvb_common::impl_spec_display!(PlayerVerifyStatus, Reserved);
126
127// --- input_status (Table 56) ---
128
129/// `input_status` values (Table 56).
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize))]
132#[non_exhaustive]
133pub enum InputStatus {
134    /// `0x00` — OK, a Local TS has switched to Input Mode.
135    Ok,
136    /// `0x01` — Request refused.
137    RequestRefused,
138    /// `0x02` — Insufficient bitrate available.
139    InsufficientBitrate,
140    /// `0x03` — No remaining player sessions available.
141    NoSessionsAvailable,
142    /// Reserved (`0x04`–`0xFF`).
143    Reserved(u8),
144}
145impl InputStatus {
146    /// Decode an `input_status` byte.
147    #[must_use]
148    pub fn from_u8(v: u8) -> Self {
149        match v {
150            0x00 => Self::Ok,
151            0x01 => Self::RequestRefused,
152            0x02 => Self::InsufficientBitrate,
153            0x03 => Self::NoSessionsAvailable,
154            other => Self::Reserved(other),
155        }
156    }
157    /// Wire byte.
158    #[must_use]
159    pub const fn to_u8(self) -> u8 {
160        match self {
161            Self::Ok => 0x00,
162            Self::RequestRefused => 0x01,
163            Self::InsufficientBitrate => 0x02,
164            Self::NoSessionsAvailable => 0x03,
165            Self::Reserved(v) => v,
166        }
167    }
168    /// Spec token, or `"reserved"`.
169    #[must_use]
170    pub fn name(&self) -> &'static str {
171        match self {
172            Self::Ok => "ok",
173            Self::RequestRefused => "request_refused",
174            Self::InsufficientBitrate => "insufficient_bitrate",
175            Self::NoSessionsAvailable => "no_sessions_available",
176            Self::Reserved(_) => "reserved",
177        }
178    }
179}
180dvb_common::impl_spec_display!(InputStatus, Reserved);
181
182// --- play_status (Table 59) ---
183
184/// `play_status` values (Table 59), carried in `CICAM_player_status_error`.
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize))]
187#[non_exhaustive]
188pub enum PlayStatus {
189    /// `0x01` — Error, content play is not possible (unsupported format/protocol).
190    PlayNotPossible,
191    /// `0x02` — Error, unrecoverable error.
192    Unrecoverable,
193    /// `0x03` — Error, content blocked (no content license available).
194    ContentBlocked,
195    /// Reserved (`0x00`, `0x04`–`0xFF`).
196    Reserved(u8),
197}
198impl PlayStatus {
199    /// Decode a `play_status` byte.
200    #[must_use]
201    pub fn from_u8(v: u8) -> Self {
202        match v {
203            0x01 => Self::PlayNotPossible,
204            0x02 => Self::Unrecoverable,
205            0x03 => Self::ContentBlocked,
206            other => Self::Reserved(other),
207        }
208    }
209    /// Wire byte.
210    #[must_use]
211    pub const fn to_u8(self) -> u8 {
212        match self {
213            Self::PlayNotPossible => 0x01,
214            Self::Unrecoverable => 0x02,
215            Self::ContentBlocked => 0x03,
216            Self::Reserved(v) => v,
217        }
218    }
219    /// Spec token, or `"reserved"`.
220    #[must_use]
221    pub fn name(&self) -> &'static str {
222        match self {
223            Self::PlayNotPossible => "play_not_possible",
224            Self::Unrecoverable => "unrecoverable_error",
225            Self::ContentBlocked => "content_blocked",
226            Self::Reserved(_) => "reserved",
227        }
228    }
229}
230dvb_common::impl_spec_display!(PlayStatus, Reserved);
231
232// --- Command (Table 61) / seek_mode (Table 62) ---
233
234/// `seek_mode` values (Table 62).
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236#[cfg_attr(feature = "serde", derive(serde::Serialize))]
237#[non_exhaustive]
238pub enum SeekMode {
239    /// `0x00` — Absolute.
240    Absolute,
241    /// `0x01` — Relative to current position.
242    Relative,
243    /// Reserved (`0x02`–`0xFF`).
244    Reserved(u8),
245}
246impl SeekMode {
247    /// Decode a `seek_mode` byte.
248    #[must_use]
249    pub fn from_u8(v: u8) -> Self {
250        match v {
251            0x00 => Self::Absolute,
252            0x01 => Self::Relative,
253            other => Self::Reserved(other),
254        }
255    }
256    /// Wire byte.
257    #[must_use]
258    pub const fn to_u8(self) -> u8 {
259        match self {
260            Self::Absolute => 0x00,
261            Self::Relative => 0x01,
262            Self::Reserved(v) => v,
263        }
264    }
265    /// Spec token, or `"reserved"`.
266    #[must_use]
267    pub fn name(&self) -> &'static str {
268        match self {
269            Self::Absolute => "absolute",
270            Self::Relative => "relative",
271            Self::Reserved(_) => "reserved",
272        }
273    }
274}
275dvb_common::impl_spec_display!(SeekMode, Reserved);
276
277// --- update_status (Table 70) ---
278
279/// `update_status` values (Table 70).
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281#[cfg_attr(feature = "serde", derive(serde::Serialize))]
282#[non_exhaustive]
283pub enum UpdateStatus {
284    /// `0x00` — OK, Host processed the updated PMT and is ready to receive the Local TS.
285    Ok,
286    /// `0x01` — Request refused.
287    RequestRefused,
288    /// Reserved (`0x02`–`0xFF`).
289    Reserved(u8),
290}
291impl UpdateStatus {
292    /// Decode an `update_status` byte.
293    #[must_use]
294    pub fn from_u8(v: u8) -> Self {
295        match v {
296            0x00 => Self::Ok,
297            0x01 => Self::RequestRefused,
298            other => Self::Reserved(other),
299        }
300    }
301    /// Wire byte.
302    #[must_use]
303    pub const fn to_u8(self) -> u8 {
304        match self {
305            Self::Ok => 0x00,
306            Self::RequestRefused => 0x01,
307            Self::Reserved(v) => v,
308        }
309    }
310    /// Spec token, or `"reserved"`.
311    #[must_use]
312    pub fn name(&self) -> &'static str {
313        match self {
314            Self::Ok => "ok",
315            Self::RequestRefused => "request_refused",
316            Self::Reserved(_) => "reserved",
317        }
318    }
319}
320dvb_common::impl_spec_display!(UpdateStatus, Reserved);
321
322// ---------------------------------------------------------------------------
323// CICAM_player_verify_req (Table 49)
324// ---------------------------------------------------------------------------
325
326/// `CICAM_player_verify_req()` (Table 49): Host → CICAM. A length-prefixed
327/// `service_location` XML blob (annex D schema).
328#[derive(Debug, Clone, PartialEq, Eq)]
329#[cfg_attr(feature = "serde", derive(serde::Serialize))]
330pub struct PlayerVerifyReq<'a> {
331    /// `service_location_byte` body (`service_location_length` bytes) — opaque XML.
332    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
333    pub service_location: &'a [u8],
334}
335
336// service_location_length(2) + service_location bytes.
337const SERVICE_LOCATION_PREFIX: usize = 2;
338
339impl<'a> Parse<'a> for PlayerVerifyReq<'a> {
340    type Error = Error;
341    fn parse(bytes: &'a [u8]) -> Result<Self> {
342        let body = objects::parse_apdu_header(bytes, tag::VERIFY_REQ, "CICAM_player_verify_req")?;
343        let service_location = parse_service_location(body, "CICAM_player_verify_req")?;
344        Ok(Self { service_location })
345    }
346}
347impl Serialize for PlayerVerifyReq<'_> {
348    type Error = Error;
349    fn serialized_len(&self) -> usize {
350        objects::apdu_len(SERVICE_LOCATION_PREFIX + self.service_location.len())
351    }
352    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
353        let body_len = SERVICE_LOCATION_PREFIX + self.service_location.len();
354        let pos = objects::write_apdu_header(tag::VERIFY_REQ, body_len, buf)?;
355        write_service_location(self.service_location, &mut buf[pos..]);
356        Ok(pos + body_len)
357    }
358}
359
360fn parse_service_location<'a>(body: &'a [u8], what: &'static str) -> Result<&'a [u8]> {
361    if body.len() < SERVICE_LOCATION_PREFIX {
362        return Err(Error::BufferTooShort {
363            need: SERVICE_LOCATION_PREFIX,
364            have: body.len(),
365            what,
366        });
367    }
368    let len = u16::from_be_bytes([body[0], body[1]]) as usize;
369    let end = SERVICE_LOCATION_PREFIX + len;
370    if body.len() < end {
371        return Err(Error::LengthMismatch {
372            what,
373            declared: len,
374            actual: body.len().saturating_sub(SERVICE_LOCATION_PREFIX),
375        });
376    }
377    Ok(&body[SERVICE_LOCATION_PREFIX..end])
378}
379
380fn write_service_location(loc: &[u8], buf: &mut [u8]) {
381    buf[0..2].copy_from_slice(&(loc.len() as u16).to_be_bytes());
382    buf[2..2 + loc.len()].copy_from_slice(loc);
383}
384
385// ---------------------------------------------------------------------------
386// CICAM_player_verify_reply (Table 50)
387// ---------------------------------------------------------------------------
388
389/// `CICAM_player_verify_reply()` (Table 50): CICAM → Host.
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391#[cfg_attr(feature = "serde", derive(serde::Serialize))]
392pub struct PlayerVerifyReply {
393    /// `player_verify_status` (8) — Table 51.
394    pub player_verify_status: PlayerVerifyStatus,
395}
396
397const VERIFY_REPLY_BODY: usize = 1;
398
399impl<'a> Parse<'a> for PlayerVerifyReply {
400    type Error = Error;
401    fn parse(bytes: &'a [u8]) -> Result<Self> {
402        let body =
403            objects::parse_apdu_header(bytes, tag::VERIFY_REPLY, "CICAM_player_verify_reply")?;
404        if body.len() < VERIFY_REPLY_BODY {
405            return Err(Error::BufferTooShort {
406                need: VERIFY_REPLY_BODY,
407                have: body.len(),
408                what: "CICAM_player_verify_reply",
409            });
410        }
411        Ok(Self {
412            player_verify_status: PlayerVerifyStatus::from_u8(body[0]),
413        })
414    }
415}
416impl Serialize for PlayerVerifyReply {
417    type Error = Error;
418    fn serialized_len(&self) -> usize {
419        objects::apdu_len(VERIFY_REPLY_BODY)
420    }
421    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
422        let pos = objects::write_apdu_header(tag::VERIFY_REPLY, VERIFY_REPLY_BODY, buf)?;
423        buf[pos] = self.player_verify_status.to_u8();
424        Ok(pos + VERIFY_REPLY_BODY)
425    }
426}
427
428// ---------------------------------------------------------------------------
429// CICAM_player_capabilities_req (Table 52)
430// ---------------------------------------------------------------------------
431
432/// `CICAM_player_capabilities_req()` (Table 52): CICAM → Host, header-only.
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
434#[cfg_attr(feature = "serde", derive(serde::Serialize))]
435pub struct PlayerCapabilitiesReq;
436
437impl<'a> Parse<'a> for PlayerCapabilitiesReq {
438    type Error = Error;
439    fn parse(bytes: &'a [u8]) -> Result<Self> {
440        objects::parse_empty_apdu(
441            bytes,
442            tag::CAPABILITIES_REQ,
443            "CICAM_player_capabilities_req",
444        )?;
445        Ok(Self)
446    }
447}
448impl Serialize for PlayerCapabilitiesReq {
449    type Error = Error;
450    fn serialized_len(&self) -> usize {
451        objects::empty_apdu_len()
452    }
453    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
454        objects::serialize_empty_apdu(tag::CAPABILITIES_REQ, buf)
455    }
456}
457
458// ---------------------------------------------------------------------------
459// CICAM_player_capabilities_reply (Table 53)
460// ---------------------------------------------------------------------------
461
462/// One component-type entry of `CICAM_player_capabilities_reply` (Table 53).
463/// `stream_content` / `component_type` are coded as in the EN 300 468 Component
464/// descriptor \[10\].
465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466#[cfg_attr(feature = "serde", derive(serde::Serialize))]
467pub struct ComponentType {
468    /// `stream_content` (4).
469    pub stream_content: u8,
470    /// `component_type` (8).
471    pub component_type: u8,
472}
473
474/// `CICAM_player_capabilities_reply()` (Table 53): Host → CICAM.
475#[derive(Debug, Clone, PartialEq, Eq)]
476#[cfg_attr(feature = "serde", derive(serde::Serialize))]
477pub struct PlayerCapabilitiesReply {
478    /// `component_type` list (loop count `number_of_component_types`).
479    pub component_types: Vec<ComponentType>,
480}
481
482// number_of_component_types(2), then per-entry stream_content/reserved(1)+component_type(1).
483const CAPABILITIES_PREFIX: usize = 2;
484const COMPONENT_TYPE_LEN: usize = 2;
485const STREAM_CONTENT_MASK: u8 = 0x0F;
486
487impl<'a> Parse<'a> for PlayerCapabilitiesReply {
488    type Error = Error;
489    fn parse(bytes: &'a [u8]) -> Result<Self> {
490        let body = objects::parse_apdu_header(
491            bytes,
492            tag::CAPABILITIES_REPLY,
493            "CICAM_player_capabilities_reply",
494        )?;
495        if body.len() < CAPABILITIES_PREFIX {
496            return Err(Error::BufferTooShort {
497                need: CAPABILITIES_PREFIX,
498                have: body.len(),
499                what: "CICAM_player_capabilities_reply",
500            });
501        }
502        let n = u16::from_be_bytes([body[0], body[1]]) as usize;
503        let mut pos = CAPABILITIES_PREFIX;
504        let mut component_types = Vec::with_capacity(n);
505        for _ in 0..n {
506            if pos + COMPONENT_TYPE_LEN > body.len() {
507                return Err(Error::BufferTooShort {
508                    need: pos + COMPONENT_TYPE_LEN,
509                    have: body.len(),
510                    what: "CICAM_player_capabilities_reply entry",
511                });
512            }
513            component_types.push(ComponentType {
514                // stream_content(4) + reserved(4).
515                stream_content: (body[pos] >> 4) & STREAM_CONTENT_MASK,
516                component_type: body[pos + 1],
517            });
518            pos += COMPONENT_TYPE_LEN;
519        }
520        Ok(Self { component_types })
521    }
522}
523impl Serialize for PlayerCapabilitiesReply {
524    type Error = Error;
525    fn serialized_len(&self) -> usize {
526        objects::apdu_len(CAPABILITIES_PREFIX + self.component_types.len() * COMPONENT_TYPE_LEN)
527    }
528    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
529        let body_len = CAPABILITIES_PREFIX + self.component_types.len() * COMPONENT_TYPE_LEN;
530        let mut pos = objects::write_apdu_header(tag::CAPABILITIES_REPLY, body_len, buf)?;
531        buf[pos..pos + 2].copy_from_slice(&(self.component_types.len() as u16).to_be_bytes());
532        pos += CAPABILITIES_PREFIX;
533        for c in &self.component_types {
534            // stream_content(4) << 4 + reserved(4)='1111'.
535            buf[pos] = ((c.stream_content & STREAM_CONTENT_MASK) << 4) | 0x0F;
536            buf[pos + 1] = c.component_type;
537            pos += COMPONENT_TYPE_LEN;
538        }
539        Ok(pos)
540    }
541}
542
543// ---------------------------------------------------------------------------
544// CICAM_player_start_req (Table 54)
545// ---------------------------------------------------------------------------
546
547/// `CICAM_player_start_req()` (Table 54): CICAM → Host.
548#[derive(Debug, Clone, PartialEq, Eq)]
549#[cfg_attr(feature = "serde", derive(serde::Serialize))]
550pub struct PlayerStartReq<'a> {
551    /// `input_max_bitrate` (16) — Host→CICAM delivery, units of 10 kbps (rounded up).
552    pub input_max_bitrate: u16,
553    /// `output_max_bitrate` (16) — CICAM→Host delivery, units of 10 kbps.
554    pub output_max_bitrate: u16,
555    /// `linearChannel` (1) — set = linear channel with no timeshift.
556    pub linear_channel: bool,
557    /// `PMT_byte` body (`PMT_length` bytes) — opaque PMT (first byte = `table_id`).
558    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
559    pub pmt: &'a [u8],
560}
561
562// input_max_bitrate(2)+output_max_bitrate(2)+linearChannel/reserved(1)+PMT_length(2).
563const START_REQ_PREFIX: usize = 2 + 2 + 1 + 2;
564const LINEAR_CHANNEL_BIT: u8 = 0x80;
565
566impl<'a> Parse<'a> for PlayerStartReq<'a> {
567    type Error = Error;
568    fn parse(bytes: &'a [u8]) -> Result<Self> {
569        let body = objects::parse_apdu_header(bytes, tag::START_REQ, "CICAM_player_start_req")?;
570        if body.len() < START_REQ_PREFIX {
571            return Err(Error::BufferTooShort {
572                need: START_REQ_PREFIX,
573                have: body.len(),
574                what: "CICAM_player_start_req",
575            });
576        }
577        let input_max_bitrate = u16::from_be_bytes([body[0], body[1]]);
578        let output_max_bitrate = u16::from_be_bytes([body[2], body[3]]);
579        let linear_channel = body[4] & LINEAR_CHANNEL_BIT != 0;
580        let pmt_length = u16::from_be_bytes([body[5], body[6]]) as usize;
581        let end = START_REQ_PREFIX + pmt_length;
582        if body.len() < end {
583            return Err(Error::LengthMismatch {
584                what: "CICAM_player_start_req PMT",
585                declared: pmt_length,
586                actual: body.len().saturating_sub(START_REQ_PREFIX),
587            });
588        }
589        Ok(Self {
590            input_max_bitrate,
591            output_max_bitrate,
592            linear_channel,
593            pmt: &body[START_REQ_PREFIX..end],
594        })
595    }
596}
597impl Serialize for PlayerStartReq<'_> {
598    type Error = Error;
599    fn serialized_len(&self) -> usize {
600        objects::apdu_len(START_REQ_PREFIX + self.pmt.len())
601    }
602    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
603        let body_len = START_REQ_PREFIX + self.pmt.len();
604        let pos = objects::write_apdu_header(tag::START_REQ, body_len, buf)?;
605        buf[pos..pos + 2].copy_from_slice(&self.input_max_bitrate.to_be_bytes());
606        buf[pos + 2..pos + 4].copy_from_slice(&self.output_max_bitrate.to_be_bytes());
607        // linearChannel(1) + reserved(7)='0000000'.
608        buf[pos + 4] = if self.linear_channel {
609            LINEAR_CHANNEL_BIT
610        } else {
611            0
612        };
613        buf[pos + 5..pos + 7].copy_from_slice(&(self.pmt.len() as u16).to_be_bytes());
614        buf[pos + START_REQ_PREFIX..pos + body_len].copy_from_slice(self.pmt);
615        Ok(pos + body_len)
616    }
617}
618
619// ---------------------------------------------------------------------------
620// CICAM_player_start_reply (Table 55)
621// ---------------------------------------------------------------------------
622
623/// `CICAM_player_start_reply()` (Table 55): Host → CICAM.
624#[derive(Debug, Clone, Copy, PartialEq, Eq)]
625#[cfg_attr(feature = "serde", derive(serde::Serialize))]
626pub struct PlayerStartReply {
627    /// `LTS_id` (8) — Local TS allocated; uniquely identifies the player session.
628    /// Ignored if `input_status` is non-zero.
629    pub lts_id: u8,
630    /// `input_status` (8) — Table 56.
631    pub input_status: InputStatus,
632}
633
634const START_REPLY_BODY: usize = 2;
635
636impl<'a> Parse<'a> for PlayerStartReply {
637    type Error = Error;
638    fn parse(bytes: &'a [u8]) -> Result<Self> {
639        let body = objects::parse_apdu_header(bytes, tag::START_REPLY, "CICAM_player_start_reply")?;
640        if body.len() < START_REPLY_BODY {
641            return Err(Error::BufferTooShort {
642                need: START_REPLY_BODY,
643                have: body.len(),
644                what: "CICAM_player_start_reply",
645            });
646        }
647        Ok(Self {
648            lts_id: body[0],
649            input_status: InputStatus::from_u8(body[1]),
650        })
651    }
652}
653impl Serialize for PlayerStartReply {
654    type Error = Error;
655    fn serialized_len(&self) -> usize {
656        objects::apdu_len(START_REPLY_BODY)
657    }
658    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
659        let pos = objects::write_apdu_header(tag::START_REPLY, START_REPLY_BODY, buf)?;
660        buf[pos] = self.lts_id;
661        buf[pos + 1] = self.input_status.to_u8();
662        Ok(pos + START_REPLY_BODY)
663    }
664}
665
666// ---------------------------------------------------------------------------
667// CICAM_player_play_req (Table 57)
668// ---------------------------------------------------------------------------
669
670/// `CICAM_player_play_req()` (Table 57): Host → CICAM. A length-prefixed
671/// `service_location` XML blob (annex D schema).
672#[derive(Debug, Clone, PartialEq, Eq)]
673#[cfg_attr(feature = "serde", derive(serde::Serialize))]
674pub struct PlayerPlayReq<'a> {
675    /// `service_location_byte` body (`service_location_length` bytes) — opaque XML.
676    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
677    pub service_location: &'a [u8],
678}
679
680impl<'a> Parse<'a> for PlayerPlayReq<'a> {
681    type Error = Error;
682    fn parse(bytes: &'a [u8]) -> Result<Self> {
683        let body = objects::parse_apdu_header(bytes, tag::PLAY_REQ, "CICAM_player_play_req")?;
684        let service_location = parse_service_location(body, "CICAM_player_play_req")?;
685        Ok(Self { service_location })
686    }
687}
688impl Serialize for PlayerPlayReq<'_> {
689    type Error = Error;
690    fn serialized_len(&self) -> usize {
691        objects::apdu_len(SERVICE_LOCATION_PREFIX + self.service_location.len())
692    }
693    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
694        let body_len = SERVICE_LOCATION_PREFIX + self.service_location.len();
695        let pos = objects::write_apdu_header(tag::PLAY_REQ, body_len, buf)?;
696        write_service_location(self.service_location, &mut buf[pos..]);
697        Ok(pos + body_len)
698    }
699}
700
701// ---------------------------------------------------------------------------
702// CICAM_player_status_error (Table 58)
703// ---------------------------------------------------------------------------
704
705/// `CICAM_player_status_error()` (Table 58): CICAM → Host.
706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707#[cfg_attr(feature = "serde", derive(serde::Serialize))]
708pub struct PlayerStatusError {
709    /// `valid_LTS_id` (1) — `true` when `lts_id` relates to an established session.
710    pub valid_lts_id: bool,
711    /// `LTS_id` (8) — undefined when `valid_lts_id` is `false`.
712    pub lts_id: u8,
713    /// `player_status` (8) — Table 59.
714    pub player_status: PlayStatus,
715}
716
717// reserved(7)+valid_LTS_id(1) + LTS_id(8) + player_status(8).
718const STATUS_ERROR_BODY: usize = 3;
719const VALID_LTS_ID_BIT: u8 = 0x01;
720
721impl<'a> Parse<'a> for PlayerStatusError {
722    type Error = Error;
723    fn parse(bytes: &'a [u8]) -> Result<Self> {
724        let body =
725            objects::parse_apdu_header(bytes, tag::STATUS_ERROR, "CICAM_player_status_error")?;
726        if body.len() < STATUS_ERROR_BODY {
727            return Err(Error::BufferTooShort {
728                need: STATUS_ERROR_BODY,
729                have: body.len(),
730                what: "CICAM_player_status_error",
731            });
732        }
733        Ok(Self {
734            valid_lts_id: body[0] & VALID_LTS_ID_BIT != 0,
735            lts_id: body[1],
736            player_status: PlayStatus::from_u8(body[2]),
737        })
738    }
739}
740impl Serialize for PlayerStatusError {
741    type Error = Error;
742    fn serialized_len(&self) -> usize {
743        objects::apdu_len(STATUS_ERROR_BODY)
744    }
745    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
746        let pos = objects::write_apdu_header(tag::STATUS_ERROR, STATUS_ERROR_BODY, buf)?;
747        // reserved(7)='0000000' + valid_LTS_id(1).
748        buf[pos] = if self.valid_lts_id {
749            VALID_LTS_ID_BIT
750        } else {
751            0
752        };
753        buf[pos + 1] = self.lts_id;
754        buf[pos + 2] = self.player_status.to_u8();
755        Ok(pos + STATUS_ERROR_BODY)
756    }
757}
758
759// ---------------------------------------------------------------------------
760// CICAM_player_control_req (Table 60)
761// ---------------------------------------------------------------------------
762
763/// The `Command`-selected payload of `CICAM_player_control_req` (Tables 60/61).
764#[derive(Debug, Clone, Copy, PartialEq, Eq)]
765#[cfg_attr(feature = "serde", derive(serde::Serialize))]
766#[non_exhaustive]
767pub enum ControlCommand {
768    /// `command == 0x01` — Set position.
769    SetPosition {
770        /// `seek_mode` (8) — Table 62.
771        seek_mode: SeekMode,
772        /// `seek_position` (32, signed) — milliseconds; `0xFFFFFFFF` = jump to live / end.
773        seek_position: i32,
774    },
775    /// `command == 0x02` — Set speed.
776    SetSpeed {
777        /// `Speed` (16, signed) — hundredths of nominal (100 = nominal, 0 = pause).
778        speed: i16,
779    },
780    /// Reserved/unknown `command` value with no further payload (e.g. `0x00`).
781    Reserved(u8),
782}
783impl ControlCommand {
784    /// The `Command` byte (Table 61) this payload encodes.
785    #[must_use]
786    pub fn command(&self) -> u8 {
787        match self {
788            Self::SetPosition { .. } => 0x01,
789            Self::SetSpeed { .. } => 0x02,
790            Self::Reserved(v) => *v,
791        }
792    }
793}
794
795/// `CICAM_player_control_req()` (Table 60): Host → CICAM.
796#[derive(Debug, Clone, Copy, PartialEq, Eq)]
797#[cfg_attr(feature = "serde", derive(serde::Serialize))]
798pub struct PlayerControlReq {
799    /// `LTS_id` (8).
800    pub lts_id: u8,
801    /// The `Command`-selected payload.
802    pub command: ControlCommand,
803}
804
805// LTS_id(1) + Command(1) [+ command-specific].
806const CONTROL_PREFIX: usize = 2;
807const CONTROL_CMD_SET_POSITION: u8 = 0x01;
808const CONTROL_CMD_SET_SPEED: u8 = 0x02;
809// SetPosition: seek_mode(1) + seek_position(4); SetSpeed: Speed(2).
810const SET_POSITION_EXTRA: usize = 1 + 4;
811const SET_SPEED_EXTRA: usize = 2;
812
813impl PlayerControlReq {
814    fn body_len(&self) -> usize {
815        CONTROL_PREFIX
816            + match self.command {
817                ControlCommand::SetPosition { .. } => SET_POSITION_EXTRA,
818                ControlCommand::SetSpeed { .. } => SET_SPEED_EXTRA,
819                ControlCommand::Reserved(_) => 0,
820            }
821    }
822}
823
824impl<'a> Parse<'a> for PlayerControlReq {
825    type Error = Error;
826    fn parse(bytes: &'a [u8]) -> Result<Self> {
827        let body = objects::parse_apdu_header(bytes, tag::CONTROL_REQ, "CICAM_player_control_req")?;
828        if body.len() < CONTROL_PREFIX {
829            return Err(Error::BufferTooShort {
830                need: CONTROL_PREFIX,
831                have: body.len(),
832                what: "CICAM_player_control_req",
833            });
834        }
835        let lts_id = body[0];
836        let command = match body[1] {
837            CONTROL_CMD_SET_POSITION => {
838                if body.len() < CONTROL_PREFIX + SET_POSITION_EXTRA {
839                    return Err(Error::BufferTooShort {
840                        need: CONTROL_PREFIX + SET_POSITION_EXTRA,
841                        have: body.len(),
842                        what: "CICAM_player_control_req set_position",
843                    });
844                }
845                ControlCommand::SetPosition {
846                    seek_mode: SeekMode::from_u8(body[2]),
847                    seek_position: i32::from_be_bytes([body[3], body[4], body[5], body[6]]),
848                }
849            }
850            CONTROL_CMD_SET_SPEED => {
851                if body.len() < CONTROL_PREFIX + SET_SPEED_EXTRA {
852                    return Err(Error::BufferTooShort {
853                        need: CONTROL_PREFIX + SET_SPEED_EXTRA,
854                        have: body.len(),
855                        what: "CICAM_player_control_req set_speed",
856                    });
857                }
858                ControlCommand::SetSpeed {
859                    speed: i16::from_be_bytes([body[2], body[3]]),
860                }
861            }
862            other => ControlCommand::Reserved(other),
863        };
864        Ok(Self { lts_id, command })
865    }
866}
867impl Serialize for PlayerControlReq {
868    type Error = Error;
869    fn serialized_len(&self) -> usize {
870        objects::apdu_len(self.body_len())
871    }
872    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
873        let body_len = self.body_len();
874        let pos = objects::write_apdu_header(tag::CONTROL_REQ, body_len, buf)?;
875        buf[pos] = self.lts_id;
876        buf[pos + 1] = self.command.command();
877        match self.command {
878            ControlCommand::SetPosition {
879                seek_mode,
880                seek_position,
881            } => {
882                buf[pos + 2] = seek_mode.to_u8();
883                buf[pos + 3..pos + 7].copy_from_slice(&seek_position.to_be_bytes());
884            }
885            ControlCommand::SetSpeed { speed } => {
886                buf[pos + 2..pos + 4].copy_from_slice(&speed.to_be_bytes());
887            }
888            ControlCommand::Reserved(_) => {}
889        }
890        Ok(pos + body_len)
891    }
892}
893
894// ---------------------------------------------------------------------------
895// CICAM_player_info_req (Table 63)
896// ---------------------------------------------------------------------------
897
898/// `CICAM_player_info_req()` (Table 63): Host → CICAM.
899#[derive(Debug, Clone, Copy, PartialEq, Eq)]
900#[cfg_attr(feature = "serde", derive(serde::Serialize))]
901pub struct PlayerInfoReq {
902    /// `LTS_id` (8).
903    pub lts_id: u8,
904}
905
906const LTS_ID_BODY: usize = 1;
907
908impl<'a> Parse<'a> for PlayerInfoReq {
909    type Error = Error;
910    fn parse(bytes: &'a [u8]) -> Result<Self> {
911        Ok(Self {
912            lts_id: parse_lts_id_body(bytes, tag::INFO_REQ, "CICAM_player_info_req")?,
913        })
914    }
915}
916impl Serialize for PlayerInfoReq {
917    type Error = Error;
918    fn serialized_len(&self) -> usize {
919        objects::apdu_len(LTS_ID_BODY)
920    }
921    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
922        serialize_lts_id_body(self.lts_id, tag::INFO_REQ, buf)
923    }
924}
925
926fn parse_lts_id_body(bytes: &[u8], expected: ApduTag, what: &'static str) -> Result<u8> {
927    let body = objects::parse_apdu_header(bytes, expected, what)?;
928    if body.len() < LTS_ID_BODY {
929        return Err(Error::BufferTooShort {
930            need: LTS_ID_BODY,
931            have: body.len(),
932            what,
933        });
934    }
935    Ok(body[0])
936}
937
938fn serialize_lts_id_body(lts_id: u8, tag: ApduTag, buf: &mut [u8]) -> Result<usize> {
939    let pos = objects::write_apdu_header(tag, LTS_ID_BODY, buf)?;
940    buf[pos] = lts_id;
941    Ok(pos + LTS_ID_BODY)
942}
943
944// ---------------------------------------------------------------------------
945// CICAM_player_info_reply (Table 64)
946// ---------------------------------------------------------------------------
947
948/// `CICAM_player_info_reply()` (Table 64): CICAM → Host.
949#[derive(Debug, Clone, Copy, PartialEq, Eq)]
950#[cfg_attr(feature = "serde", derive(serde::Serialize))]
951pub struct PlayerInfoReply {
952    /// `LTS_id` (8).
953    pub lts_id: u8,
954    /// `duration` (32) — total content duration in seconds; `0xFFFFFFFF` if unknown.
955    pub duration: u32,
956    /// `position` (32) — current play position in seconds; `0xFFFFFFFF` if unknown.
957    pub position: u32,
958    /// `speed` (16, signed) — current playout speed in hundredths of nominal.
959    pub speed: i16,
960}
961
962// LTS_id(1) + duration(4) + position(4) + speed(2).
963const INFO_REPLY_BODY: usize = 1 + 4 + 4 + 2;
964
965impl<'a> Parse<'a> for PlayerInfoReply {
966    type Error = Error;
967    fn parse(bytes: &'a [u8]) -> Result<Self> {
968        let body = objects::parse_apdu_header(bytes, tag::INFO_REPLY, "CICAM_player_info_reply")?;
969        if body.len() < INFO_REPLY_BODY {
970            return Err(Error::BufferTooShort {
971                need: INFO_REPLY_BODY,
972                have: body.len(),
973                what: "CICAM_player_info_reply",
974            });
975        }
976        Ok(Self {
977            lts_id: body[0],
978            duration: u32::from_be_bytes([body[1], body[2], body[3], body[4]]),
979            position: u32::from_be_bytes([body[5], body[6], body[7], body[8]]),
980            speed: i16::from_be_bytes([body[9], body[10]]),
981        })
982    }
983}
984impl Serialize for PlayerInfoReply {
985    type Error = Error;
986    fn serialized_len(&self) -> usize {
987        objects::apdu_len(INFO_REPLY_BODY)
988    }
989    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
990        let pos = objects::write_apdu_header(tag::INFO_REPLY, INFO_REPLY_BODY, buf)?;
991        buf[pos] = self.lts_id;
992        buf[pos + 1..pos + 5].copy_from_slice(&self.duration.to_be_bytes());
993        buf[pos + 5..pos + 9].copy_from_slice(&self.position.to_be_bytes());
994        buf[pos + 9..pos + 11].copy_from_slice(&self.speed.to_be_bytes());
995        Ok(pos + INFO_REPLY_BODY)
996    }
997}
998
999// ---------------------------------------------------------------------------
1000// CICAM_player_stop (Table 65) / CICAM_player_end (Table 66)
1001// ---------------------------------------------------------------------------
1002
1003/// `CICAM_player_stop()` (Table 65): Host → CICAM.
1004#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1005#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1006pub struct PlayerStop {
1007    /// `LTS_id` (8).
1008    pub lts_id: u8,
1009}
1010
1011impl<'a> Parse<'a> for PlayerStop {
1012    type Error = Error;
1013    fn parse(bytes: &'a [u8]) -> Result<Self> {
1014        Ok(Self {
1015            lts_id: parse_lts_id_body(bytes, tag::STOP, "CICAM_player_stop")?,
1016        })
1017    }
1018}
1019impl Serialize for PlayerStop {
1020    type Error = Error;
1021    fn serialized_len(&self) -> usize {
1022        objects::apdu_len(LTS_ID_BODY)
1023    }
1024    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1025        serialize_lts_id_body(self.lts_id, tag::STOP, buf)
1026    }
1027}
1028
1029/// `CICAM_player_end()` (Table 66): CICAM → Host.
1030#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1031#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1032pub struct PlayerEnd {
1033    /// `LTS_id` (8).
1034    pub lts_id: u8,
1035}
1036
1037impl<'a> Parse<'a> for PlayerEnd {
1038    type Error = Error;
1039    fn parse(bytes: &'a [u8]) -> Result<Self> {
1040        Ok(Self {
1041            lts_id: parse_lts_id_body(bytes, tag::END, "CICAM_player_end")?,
1042        })
1043    }
1044}
1045impl Serialize for PlayerEnd {
1046    type Error = Error;
1047    fn serialized_len(&self) -> usize {
1048        objects::apdu_len(LTS_ID_BODY)
1049    }
1050    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1051        serialize_lts_id_body(self.lts_id, tag::END, buf)
1052    }
1053}
1054
1055// ---------------------------------------------------------------------------
1056// CICAM_player_asset_end (Table 67)
1057// ---------------------------------------------------------------------------
1058
1059/// `CICAM_player_asset_end()` (Table 67): CICAM → Host.
1060#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1061#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1062pub struct PlayerAssetEnd {
1063    /// `LTS_id` (8).
1064    pub lts_id: u8,
1065    /// `beginning` (1) — `true` = start of asset reached; otherwise end reached.
1066    pub beginning: bool,
1067}
1068
1069// LTS_id(1) + reserved(7)+beginning(1).
1070const ASSET_END_BODY: usize = 2;
1071const BEGINNING_BIT: u8 = 0x01;
1072// reserved field shall be 0x7F per §8.8.16.
1073const ASSET_END_RESERVED: u8 = 0x7F << 1;
1074
1075impl<'a> Parse<'a> for PlayerAssetEnd {
1076    type Error = Error;
1077    fn parse(bytes: &'a [u8]) -> Result<Self> {
1078        let body = objects::parse_apdu_header(bytes, tag::ASSET_END, "CICAM_player_asset_end")?;
1079        if body.len() < ASSET_END_BODY {
1080            return Err(Error::BufferTooShort {
1081                need: ASSET_END_BODY,
1082                have: body.len(),
1083                what: "CICAM_player_asset_end",
1084            });
1085        }
1086        Ok(Self {
1087            lts_id: body[0],
1088            beginning: body[1] & BEGINNING_BIT != 0,
1089        })
1090    }
1091}
1092impl Serialize for PlayerAssetEnd {
1093    type Error = Error;
1094    fn serialized_len(&self) -> usize {
1095        objects::apdu_len(ASSET_END_BODY)
1096    }
1097    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1098        let pos = objects::write_apdu_header(tag::ASSET_END, ASSET_END_BODY, buf)?;
1099        buf[pos] = self.lts_id;
1100        // reserved(7) shall be 0x7F + beginning(1).
1101        buf[pos + 1] = ASSET_END_RESERVED | u8::from(self.beginning);
1102        Ok(pos + ASSET_END_BODY)
1103    }
1104}
1105
1106// ---------------------------------------------------------------------------
1107// CICAM_player_update_req (Table 68)
1108// ---------------------------------------------------------------------------
1109
1110/// `CICAM_player_update_req()` (Table 68): CICAM → Host.
1111#[derive(Debug, Clone, PartialEq, Eq)]
1112#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1113pub struct PlayerUpdateReq<'a> {
1114    /// `LTS_id` (8).
1115    pub lts_id: u8,
1116    /// `PMT_byte` body (`PMT_length` bytes, shall not be zero) — opaque PMT.
1117    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
1118    pub pmt: &'a [u8],
1119}
1120
1121// LTS_id(1) + PMT_length(2) + PMT bytes.
1122const UPDATE_REQ_PREFIX: usize = 1 + 2;
1123
1124impl<'a> Parse<'a> for PlayerUpdateReq<'a> {
1125    type Error = Error;
1126    fn parse(bytes: &'a [u8]) -> Result<Self> {
1127        let body = objects::parse_apdu_header(bytes, tag::UPDATE_REQ, "CICAM_player_update_req")?;
1128        if body.len() < UPDATE_REQ_PREFIX {
1129            return Err(Error::BufferTooShort {
1130                need: UPDATE_REQ_PREFIX,
1131                have: body.len(),
1132                what: "CICAM_player_update_req",
1133            });
1134        }
1135        let lts_id = body[0];
1136        let pmt_length = u16::from_be_bytes([body[1], body[2]]) as usize;
1137        let end = UPDATE_REQ_PREFIX + pmt_length;
1138        if body.len() < end {
1139            return Err(Error::LengthMismatch {
1140                what: "CICAM_player_update_req PMT",
1141                declared: pmt_length,
1142                actual: body.len().saturating_sub(UPDATE_REQ_PREFIX),
1143            });
1144        }
1145        Ok(Self {
1146            lts_id,
1147            pmt: &body[UPDATE_REQ_PREFIX..end],
1148        })
1149    }
1150}
1151impl Serialize for PlayerUpdateReq<'_> {
1152    type Error = Error;
1153    fn serialized_len(&self) -> usize {
1154        objects::apdu_len(UPDATE_REQ_PREFIX + self.pmt.len())
1155    }
1156    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1157        let body_len = UPDATE_REQ_PREFIX + self.pmt.len();
1158        let pos = objects::write_apdu_header(tag::UPDATE_REQ, body_len, buf)?;
1159        buf[pos] = self.lts_id;
1160        buf[pos + 1..pos + 3].copy_from_slice(&(self.pmt.len() as u16).to_be_bytes());
1161        buf[pos + UPDATE_REQ_PREFIX..pos + body_len].copy_from_slice(self.pmt);
1162        Ok(pos + body_len)
1163    }
1164}
1165
1166// ---------------------------------------------------------------------------
1167// CICAM_player_update_reply (Table 69)
1168// ---------------------------------------------------------------------------
1169
1170/// `CICAM_player_update_reply()` (Table 69): Host → CICAM.
1171///
1172/// NB: Table 69 prints a copy/paste `CICAM_player_start_reply_tag` slip; the
1173/// authoritative tag is `0x9FA00F` ([`tag::UPDATE_REPLY`]).
1174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1175#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1176pub struct PlayerUpdateReply {
1177    /// `LTS_id` (8).
1178    pub lts_id: u8,
1179    /// `update_status` (8) — Table 70.
1180    pub update_status: UpdateStatus,
1181}
1182
1183const UPDATE_REPLY_BODY: usize = 2;
1184
1185impl<'a> Parse<'a> for PlayerUpdateReply {
1186    type Error = Error;
1187    fn parse(bytes: &'a [u8]) -> Result<Self> {
1188        let body =
1189            objects::parse_apdu_header(bytes, tag::UPDATE_REPLY, "CICAM_player_update_reply")?;
1190        if body.len() < UPDATE_REPLY_BODY {
1191            return Err(Error::BufferTooShort {
1192                need: UPDATE_REPLY_BODY,
1193                have: body.len(),
1194                what: "CICAM_player_update_reply",
1195            });
1196        }
1197        Ok(Self {
1198            lts_id: body[0],
1199            update_status: UpdateStatus::from_u8(body[1]),
1200        })
1201    }
1202}
1203impl Serialize for PlayerUpdateReply {
1204    type Error = Error;
1205    fn serialized_len(&self) -> usize {
1206        objects::apdu_len(UPDATE_REPLY_BODY)
1207    }
1208    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1209        let pos = objects::write_apdu_header(tag::UPDATE_REPLY, UPDATE_REPLY_BODY, buf)?;
1210        buf[pos] = self.lts_id;
1211        buf[pos + 1] = self.update_status.to_u8();
1212        Ok(pos + UPDATE_REPLY_BODY)
1213    }
1214}
1215
1216// ---------------------------------------------------------------------------
1217// Resource-scoped dispatch
1218// ---------------------------------------------------------------------------
1219
1220/// Resource-scoped dispatch over the CICAM Player resource objects (Table 71).
1221#[derive(Debug, Clone, PartialEq, Eq)]
1222#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1223#[non_exhaustive]
1224pub enum CicamPlayerApdu<'a> {
1225    /// `CICAM_player_verify_req` (`9F A0 00`).
1226    VerifyReq(#[cfg_attr(feature = "serde", serde(borrow))] PlayerVerifyReq<'a>),
1227    /// `CICAM_player_verify_reply` (`9F A0 01`).
1228    VerifyReply(PlayerVerifyReply),
1229    /// `CICAM_player_capabilities_req` (`9F A0 02`).
1230    CapabilitiesReq(PlayerCapabilitiesReq),
1231    /// `CICAM_player_capabilities_reply` (`9F A0 03`).
1232    CapabilitiesReply(PlayerCapabilitiesReply),
1233    /// `CICAM_player_start_req` (`9F A0 04`).
1234    StartReq(#[cfg_attr(feature = "serde", serde(borrow))] PlayerStartReq<'a>),
1235    /// `CICAM_player_start_reply` (`9F A0 05`).
1236    StartReply(PlayerStartReply),
1237    /// `CICAM_player_play_req` (`9F A0 06`).
1238    PlayReq(#[cfg_attr(feature = "serde", serde(borrow))] PlayerPlayReq<'a>),
1239    /// `CICAM_player_status_error` (`9F A0 07`).
1240    StatusError(PlayerStatusError),
1241    /// `CICAM_player_control_req` (`9F A0 08`).
1242    ControlReq(PlayerControlReq),
1243    /// `CICAM_player_info_req` (`9F A0 09`).
1244    InfoReq(PlayerInfoReq),
1245    /// `CICAM_player_info_reply` (`9F A0 0A`).
1246    InfoReply(PlayerInfoReply),
1247    /// `CICAM_player_stop` (`9F A0 0B`).
1248    Stop(PlayerStop),
1249    /// `CICAM_player_end` (`9F A0 0C`).
1250    End(PlayerEnd),
1251    /// `CICAM_player_asset_end` (`9F A0 0D`).
1252    AssetEnd(PlayerAssetEnd),
1253    /// `CICAM_player_update_req` (`9F A0 0E`).
1254    UpdateReq(#[cfg_attr(feature = "serde", serde(borrow))] PlayerUpdateReq<'a>),
1255    /// `CICAM_player_update_reply` (`9F A0 0F`).
1256    UpdateReply(PlayerUpdateReply),
1257}
1258
1259impl<'a> CicamPlayerApdu<'a> {
1260    /// Parse a CICAM Player APDU, dispatching on the leading `apdu_tag`.
1261    pub fn parse(body: &'a [u8]) -> Result<Self> {
1262        if body.len() < 3 {
1263            return Err(Error::BufferTooShort {
1264                need: 3,
1265                have: body.len(),
1266                what: "cicam_player apdu_tag",
1267            });
1268        }
1269        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
1270        match t {
1271            tag::VERIFY_REQ => Ok(Self::VerifyReq(PlayerVerifyReq::parse(body)?)),
1272            tag::VERIFY_REPLY => Ok(Self::VerifyReply(PlayerVerifyReply::parse(body)?)),
1273            tag::CAPABILITIES_REQ => Ok(Self::CapabilitiesReq(PlayerCapabilitiesReq::parse(body)?)),
1274            tag::CAPABILITIES_REPLY => Ok(Self::CapabilitiesReply(PlayerCapabilitiesReply::parse(
1275                body,
1276            )?)),
1277            tag::START_REQ => Ok(Self::StartReq(PlayerStartReq::parse(body)?)),
1278            tag::START_REPLY => Ok(Self::StartReply(PlayerStartReply::parse(body)?)),
1279            tag::PLAY_REQ => Ok(Self::PlayReq(PlayerPlayReq::parse(body)?)),
1280            tag::STATUS_ERROR => Ok(Self::StatusError(PlayerStatusError::parse(body)?)),
1281            tag::CONTROL_REQ => Ok(Self::ControlReq(PlayerControlReq::parse(body)?)),
1282            tag::INFO_REQ => Ok(Self::InfoReq(PlayerInfoReq::parse(body)?)),
1283            tag::INFO_REPLY => Ok(Self::InfoReply(PlayerInfoReply::parse(body)?)),
1284            tag::STOP => Ok(Self::Stop(PlayerStop::parse(body)?)),
1285            tag::END => Ok(Self::End(PlayerEnd::parse(body)?)),
1286            tag::ASSET_END => Ok(Self::AssetEnd(PlayerAssetEnd::parse(body)?)),
1287            tag::UPDATE_REQ => Ok(Self::UpdateReq(PlayerUpdateReq::parse(body)?)),
1288            tag::UPDATE_REPLY => Ok(Self::UpdateReply(PlayerUpdateReply::parse(body)?)),
1289            _ => Err(Error::UnexpectedApduTag {
1290                got: t.as_u24(),
1291                expected: tag::VERIFY_REQ.as_u24(),
1292                what: "cicam_player",
1293            }),
1294        }
1295    }
1296}
1297
1298impl Serialize for CicamPlayerApdu<'_> {
1299    type Error = Error;
1300    fn serialized_len(&self) -> usize {
1301        match self {
1302            Self::VerifyReq(o) => o.serialized_len(),
1303            Self::VerifyReply(o) => o.serialized_len(),
1304            Self::CapabilitiesReq(o) => o.serialized_len(),
1305            Self::CapabilitiesReply(o) => o.serialized_len(),
1306            Self::StartReq(o) => o.serialized_len(),
1307            Self::StartReply(o) => o.serialized_len(),
1308            Self::PlayReq(o) => o.serialized_len(),
1309            Self::StatusError(o) => o.serialized_len(),
1310            Self::ControlReq(o) => o.serialized_len(),
1311            Self::InfoReq(o) => o.serialized_len(),
1312            Self::InfoReply(o) => o.serialized_len(),
1313            Self::Stop(o) => o.serialized_len(),
1314            Self::End(o) => o.serialized_len(),
1315            Self::AssetEnd(o) => o.serialized_len(),
1316            Self::UpdateReq(o) => o.serialized_len(),
1317            Self::UpdateReply(o) => o.serialized_len(),
1318        }
1319    }
1320    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1321        match self {
1322            Self::VerifyReq(o) => o.serialize_into(buf),
1323            Self::VerifyReply(o) => o.serialize_into(buf),
1324            Self::CapabilitiesReq(o) => o.serialize_into(buf),
1325            Self::CapabilitiesReply(o) => o.serialize_into(buf),
1326            Self::StartReq(o) => o.serialize_into(buf),
1327            Self::StartReply(o) => o.serialize_into(buf),
1328            Self::PlayReq(o) => o.serialize_into(buf),
1329            Self::StatusError(o) => o.serialize_into(buf),
1330            Self::ControlReq(o) => o.serialize_into(buf),
1331            Self::InfoReq(o) => o.serialize_into(buf),
1332            Self::InfoReply(o) => o.serialize_into(buf),
1333            Self::Stop(o) => o.serialize_into(buf),
1334            Self::End(o) => o.serialize_into(buf),
1335            Self::AssetEnd(o) => o.serialize_into(buf),
1336            Self::UpdateReq(o) => o.serialize_into(buf),
1337            Self::UpdateReply(o) => o.serialize_into(buf),
1338        }
1339    }
1340}
1341
1342#[cfg(test)]
1343mod tests {
1344    use super::*;
1345
1346    #[test]
1347    fn verify_req_round_trips_and_bites() {
1348        let req = PlayerVerifyReq {
1349            service_location: &[0x3C, 0x53, 0x4C, 0x3E], // "<SL>"
1350        };
1351        let bytes = req.to_bytes();
1352        // tag(9F A0 00) len(06) svc_len(00 04) 3C 53 4C 3E.
1353        assert_eq!(
1354            bytes,
1355            [0x9F, 0xA0, 0x00, 0x06, 0x00, 0x04, 0x3C, 0x53, 0x4C, 0x3E]
1356        );
1357        assert_eq!(PlayerVerifyReq::parse(&bytes).unwrap(), req);
1358        let other = PlayerVerifyReq {
1359            service_location: &[0x3C, 0x53, 0x4C, 0x00],
1360        };
1361        assert_ne!(bytes, other.to_bytes());
1362    }
1363
1364    #[test]
1365    fn verify_reply_round_trips() {
1366        let r = PlayerVerifyReply {
1367            player_verify_status: PlayerVerifyStatus::Error,
1368        };
1369        let bytes = r.to_bytes();
1370        assert_eq!(bytes, [0x9F, 0xA0, 0x01, 0x01, 0x01]);
1371        assert_eq!(PlayerVerifyReply::parse(&bytes).unwrap(), r);
1372    }
1373
1374    #[test]
1375    fn capabilities_req_round_trips() {
1376        let bytes = PlayerCapabilitiesReq.to_bytes();
1377        assert_eq!(bytes, [0x9F, 0xA0, 0x02, 0x00]);
1378        assert_eq!(
1379            PlayerCapabilitiesReq::parse(&bytes).unwrap(),
1380            PlayerCapabilitiesReq
1381        );
1382    }
1383
1384    #[test]
1385    fn capabilities_reply_two_entries() {
1386        let r = PlayerCapabilitiesReply {
1387            component_types: alloc::vec![
1388                ComponentType {
1389                    stream_content: 0x01,
1390                    component_type: 0x03,
1391                },
1392                ComponentType {
1393                    stream_content: 0x02,
1394                    component_type: 0x05,
1395                },
1396            ],
1397        };
1398        let bytes = r.to_bytes();
1399        // len = 2 + 2*2 = 6. count(00 02) then 0x1F 03 / 0x2F 05.
1400        assert_eq!(bytes[0..4], [0x9F, 0xA0, 0x03, 0x06]);
1401        assert_eq!(&bytes[4..6], &[0x00, 0x02]);
1402        assert_eq!(&bytes[6..10], &[0x1F, 0x03, 0x2F, 0x05]);
1403        assert_eq!(PlayerCapabilitiesReply::parse(&bytes).unwrap(), r);
1404        let mut other = r.clone();
1405        other.component_types[0].component_type = 0x04;
1406        assert_ne!(bytes, other.to_bytes());
1407    }
1408
1409    #[test]
1410    fn start_req_round_trips_and_bites() {
1411        // 512 kbps = 0x0034 input; output 0x0040; linear; 3-byte PMT.
1412        let req = PlayerStartReq {
1413            input_max_bitrate: 0x0034,
1414            output_max_bitrate: 0x0040,
1415            linear_channel: true,
1416            pmt: &[0x02, 0xB0, 0x12],
1417        };
1418        let bytes = req.to_bytes();
1419        // body = 7 + 3 = 10 = 0x0A.
1420        assert_eq!(bytes[0..4], [0x9F, 0xA0, 0x04, 0x0A]);
1421        assert_eq!(&bytes[4..6], &[0x00, 0x34]); // input_max_bitrate
1422        assert_eq!(&bytes[6..8], &[0x00, 0x40]); // output_max_bitrate
1423        assert_eq!(bytes[8], 0x80); // linearChannel set
1424        assert_eq!(&bytes[9..11], &[0x00, 0x03]); // PMT_length
1425        assert_eq!(&bytes[11..14], &[0x02, 0xB0, 0x12]);
1426        assert_eq!(PlayerStartReq::parse(&bytes).unwrap(), req);
1427        let mut other = req.clone();
1428        other.linear_channel = false;
1429        assert_eq!(other.to_bytes()[8], 0x00);
1430        assert_ne!(bytes, other.to_bytes());
1431    }
1432
1433    #[test]
1434    fn start_reply_round_trips() {
1435        let r = PlayerStartReply {
1436            lts_id: 0x07,
1437            input_status: InputStatus::InsufficientBitrate,
1438        };
1439        let bytes = r.to_bytes();
1440        assert_eq!(bytes, [0x9F, 0xA0, 0x05, 0x02, 0x07, 0x02]);
1441        assert_eq!(PlayerStartReply::parse(&bytes).unwrap(), r);
1442    }
1443
1444    #[test]
1445    fn play_req_round_trips() {
1446        let req = PlayerPlayReq {
1447            service_location: &[0xAA, 0xBB],
1448        };
1449        let bytes = req.to_bytes();
1450        assert_eq!(bytes, [0x9F, 0xA0, 0x06, 0x04, 0x00, 0x02, 0xAA, 0xBB]);
1451        assert_eq!(PlayerPlayReq::parse(&bytes).unwrap(), req);
1452    }
1453
1454    #[test]
1455    fn status_error_round_trips_and_bites() {
1456        let e = PlayerStatusError {
1457            valid_lts_id: true,
1458            lts_id: 0x09,
1459            player_status: PlayStatus::ContentBlocked,
1460        };
1461        let bytes = e.to_bytes();
1462        // body=3: reserved+valid(01) LTS_id(09) play_status(03).
1463        assert_eq!(bytes, [0x9F, 0xA0, 0x07, 0x03, 0x01, 0x09, 0x03]);
1464        assert_eq!(PlayerStatusError::parse(&bytes).unwrap(), e);
1465        let mut other = e;
1466        other.valid_lts_id = false;
1467        assert_eq!(other.to_bytes()[4], 0x00);
1468        assert_ne!(bytes, other.to_bytes());
1469    }
1470
1471    #[test]
1472    fn control_req_set_position_round_trips_and_bites() {
1473        let c = PlayerControlReq {
1474            lts_id: 0x03,
1475            command: ControlCommand::SetPosition {
1476                seek_mode: SeekMode::Relative,
1477                seek_position: -1000,
1478            },
1479        };
1480        let bytes = c.to_bytes();
1481        // body = LTS_id(03) cmd(01) seek_mode(01) seek_position(-1000 = FF FF FC 18).
1482        assert_eq!(bytes[0..4], [0x9F, 0xA0, 0x08, 0x07]);
1483        assert_eq!(bytes[4], 0x03);
1484        assert_eq!(bytes[5], 0x01); // Command set_position
1485        assert_eq!(bytes[6], 0x01); // seek_mode relative
1486        assert_eq!(&bytes[7..11], &(-1000i32).to_be_bytes());
1487        assert_eq!(PlayerControlReq::parse(&bytes).unwrap(), c);
1488        let mut other = c;
1489        other.command = ControlCommand::SetPosition {
1490            seek_mode: SeekMode::Absolute,
1491            seek_position: -1000,
1492        };
1493        assert_ne!(bytes, other.to_bytes());
1494    }
1495
1496    #[test]
1497    fn control_req_set_speed_round_trips() {
1498        let c = PlayerControlReq {
1499            lts_id: 0x01,
1500            command: ControlCommand::SetSpeed { speed: -100 },
1501        };
1502        let bytes = c.to_bytes();
1503        // body = LTS_id(01) cmd(02) speed(-100 = FF 9C).
1504        assert_eq!(bytes[0..4], [0x9F, 0xA0, 0x08, 0x04]);
1505        assert_eq!(bytes[4], 0x01);
1506        assert_eq!(bytes[5], 0x02);
1507        assert_eq!(&bytes[6..8], &(-100i16).to_be_bytes());
1508        assert_eq!(PlayerControlReq::parse(&bytes).unwrap(), c);
1509    }
1510
1511    #[test]
1512    fn info_req_reply_round_trip() {
1513        let req = PlayerInfoReq { lts_id: 0x05 };
1514        let rb = req.to_bytes();
1515        assert_eq!(rb, [0x9F, 0xA0, 0x09, 0x01, 0x05]);
1516        assert_eq!(PlayerInfoReq::parse(&rb).unwrap(), req);
1517
1518        let reply = PlayerInfoReply {
1519            lts_id: 0x05,
1520            duration: 0xFFFF_FFFF,
1521            position: 0x0000_003C,
1522            speed: 100,
1523        };
1524        let bytes = reply.to_bytes();
1525        // body = 11 = 0x0B.
1526        assert_eq!(bytes[0..4], [0x9F, 0xA0, 0x0A, 0x0B]);
1527        assert_eq!(bytes[4], 0x05);
1528        assert_eq!(&bytes[5..9], &[0xFF, 0xFF, 0xFF, 0xFF]);
1529        assert_eq!(&bytes[9..13], &[0x00, 0x00, 0x00, 0x3C]);
1530        assert_eq!(&bytes[13..15], &100i16.to_be_bytes());
1531        assert_eq!(PlayerInfoReply::parse(&bytes).unwrap(), reply);
1532    }
1533
1534    #[test]
1535    fn stop_end_round_trip() {
1536        let stop = PlayerStop { lts_id: 0x02 };
1537        assert_eq!(stop.to_bytes(), [0x9F, 0xA0, 0x0B, 0x01, 0x02]);
1538        assert_eq!(PlayerStop::parse(&stop.to_bytes()).unwrap(), stop);
1539
1540        let end = PlayerEnd { lts_id: 0x02 };
1541        assert_eq!(end.to_bytes(), [0x9F, 0xA0, 0x0C, 0x01, 0x02]);
1542        assert_eq!(PlayerEnd::parse(&end.to_bytes()).unwrap(), end);
1543    }
1544
1545    #[test]
1546    fn asset_end_round_trips_and_reserved_is_7f() {
1547        let e = PlayerAssetEnd {
1548            lts_id: 0x04,
1549            beginning: true,
1550        };
1551        let bytes = e.to_bytes();
1552        // reserved(7)=0x7F, beginning=1 -> 0xFE | 0x01 = 0xFF.
1553        assert_eq!(bytes, [0x9F, 0xA0, 0x0D, 0x02, 0x04, 0xFF]);
1554        assert_eq!(PlayerAssetEnd::parse(&bytes).unwrap(), e);
1555        let other = PlayerAssetEnd {
1556            lts_id: 0x04,
1557            beginning: false,
1558        };
1559        // reserved still 0x7F<<1 = 0xFE, beginning=0.
1560        assert_eq!(other.to_bytes(), [0x9F, 0xA0, 0x0D, 0x02, 0x04, 0xFE]);
1561    }
1562
1563    #[test]
1564    fn update_req_round_trips_two_byte_pmt() {
1565        let req = PlayerUpdateReq {
1566            lts_id: 0x08,
1567            pmt: &[0x02, 0xB0],
1568        };
1569        let bytes = req.to_bytes();
1570        // body = 3 + 2 = 5. LTS_id(08) PMT_length(00 02) 02 B0.
1571        assert_eq!(
1572            bytes,
1573            [0x9F, 0xA0, 0x0E, 0x05, 0x08, 0x00, 0x02, 0x02, 0xB0]
1574        );
1575        assert_eq!(PlayerUpdateReq::parse(&bytes).unwrap(), req);
1576    }
1577
1578    #[test]
1579    fn update_reply_uses_9fa00f_not_start_reply_slip() {
1580        let r = PlayerUpdateReply {
1581            lts_id: 0x06,
1582            update_status: UpdateStatus::RequestRefused,
1583        };
1584        let bytes = r.to_bytes();
1585        // The authoritative tag is 0x9FA00F, NOT the Table-69 0x9FA005 slip.
1586        assert_eq!(&bytes[0..3], &[0x9F, 0xA0, 0x0F]);
1587        assert_ne!(&bytes[0..3], &[0x9F, 0xA0, 0x05]); // not the start_reply slip tag
1588        assert_eq!(bytes, [0x9F, 0xA0, 0x0F, 0x02, 0x06, 0x01]);
1589        assert_eq!(PlayerUpdateReply::parse(&bytes).unwrap(), r);
1590        assert_eq!(tag::UPDATE_REPLY.as_u24(), 0x009F_A00F);
1591    }
1592
1593    #[test]
1594    fn dispatch_routes_every_tag() {
1595        let cases: alloc::vec::Vec<alloc::vec::Vec<u8>> = alloc::vec![
1596            PlayerVerifyReq {
1597                service_location: &[]
1598            }
1599            .to_bytes(),
1600            PlayerVerifyReply {
1601                player_verify_status: PlayerVerifyStatus::Ok
1602            }
1603            .to_bytes(),
1604            PlayerCapabilitiesReq.to_bytes(),
1605            PlayerCapabilitiesReply {
1606                component_types: alloc::vec![]
1607            }
1608            .to_bytes(),
1609            PlayerStartReq {
1610                input_max_bitrate: 0,
1611                output_max_bitrate: 0,
1612                linear_channel: false,
1613                pmt: &[]
1614            }
1615            .to_bytes(),
1616            PlayerStartReply {
1617                lts_id: 0,
1618                input_status: InputStatus::Ok
1619            }
1620            .to_bytes(),
1621            PlayerPlayReq {
1622                service_location: &[]
1623            }
1624            .to_bytes(),
1625            PlayerStatusError {
1626                valid_lts_id: false,
1627                lts_id: 0,
1628                player_status: PlayStatus::Unrecoverable
1629            }
1630            .to_bytes(),
1631            PlayerControlReq {
1632                lts_id: 0,
1633                command: ControlCommand::SetSpeed { speed: 0 }
1634            }
1635            .to_bytes(),
1636            PlayerInfoReq { lts_id: 0 }.to_bytes(),
1637            PlayerInfoReply {
1638                lts_id: 0,
1639                duration: 0,
1640                position: 0,
1641                speed: 0
1642            }
1643            .to_bytes(),
1644            PlayerStop { lts_id: 0 }.to_bytes(),
1645            PlayerEnd { lts_id: 0 }.to_bytes(),
1646            PlayerAssetEnd {
1647                lts_id: 0,
1648                beginning: false
1649            }
1650            .to_bytes(),
1651            PlayerUpdateReq {
1652                lts_id: 0,
1653                pmt: &[0x00]
1654            }
1655            .to_bytes(),
1656            PlayerUpdateReply {
1657                lts_id: 0,
1658                update_status: UpdateStatus::Ok
1659            }
1660            .to_bytes(),
1661        ];
1662        for c in &cases {
1663            let parsed = CicamPlayerApdu::parse(c).unwrap();
1664            assert_eq!(&parsed.to_bytes(), c);
1665        }
1666        // Unknown tag in the 0x9FA0xx space.
1667        assert!(matches!(
1668            CicamPlayerApdu::parse(&[0x9F, 0xA0, 0x7E, 0x00]),
1669            Err(Error::UnexpectedApduTag { .. })
1670        ));
1671    }
1672}