Skip to main content

rtmp_runtime/
message.rs

1//! RTMP message assembly from chunks, protocol control messages, and the
2//! message type catalogue (Adobe RTMP 1.0 §5.4, §6, §7.1).
3//!
4//! See [`docs/rtmp.md`](../docs/rtmp.md) §4 (Protocol Control Messages), §5
5//! (RTMP Message Format + User Control, §6.1/§6.2), and §6 (RTMP Message
6//! Types, §7.1) for the wire layout.
7//!
8//! This module adds typed interpretation on top of the [`crate::chunk::Message`]
9//! carrier: the message-type-id catalogue ([`msg_type`]), the Chunk-Stream-layer
10//! protocol control messages ([`ProtocolControl`], §4/§5.4), and the
11//! streaming-layer user control events ([`UserControl`], §5.3/§6.2/§6.7).
12
13use broadcast_common::{Parse, Serialize};
14
15use crate::RtmpError;
16use crate::chunk::Message;
17
18type Result<T> = core::result::Result<T, RtmpError>;
19
20// ── Message type ids (§6 / §7.1) ────────────────────────────────────────
21
22/// RTMP message type id catalogue (§6, RTMP Message Types §7.1). Type ids
23/// `1..=6` are Chunk-Stream-layer protocol control (this doc's §4, spec
24/// §5.4); the rest are streaming-layer message types (§6/§7.1).
25pub mod msg_type {
26    /// Set Chunk Size (§5.4.1).
27    pub const SET_CHUNK_SIZE: u8 = 1;
28    /// Abort Message (§5.4.2).
29    pub const ABORT: u8 = 2;
30    /// Acknowledgement (§5.4.3).
31    pub const ACKNOWLEDGEMENT: u8 = 3;
32    /// User Control Message (§6.2/§7.1.7).
33    pub const USER_CONTROL: u8 = 4;
34    /// Window Acknowledgement Size (§5.4.4).
35    pub const WINDOW_ACK_SIZE: u8 = 5;
36    /// Set Peer Bandwidth (§5.4.5).
37    pub const SET_PEER_BANDWIDTH: u8 = 6;
38    /// Audio Message (§7.1.4).
39    pub const AUDIO: u8 = 8;
40    /// Video Message (§7.1.5).
41    pub const VIDEO: u8 = 9;
42    /// Data Message, AMF3-encoded (§7.1.2).
43    pub const DATA_AMF3: u8 = 15;
44    /// Command Message, AMF3-encoded (§7.1.1).
45    pub const COMMAND_AMF3: u8 = 17;
46    /// Data Message, AMF0-encoded (§7.1.2).
47    pub const DATA_AMF0: u8 = 18;
48    /// Command Message, AMF0-encoded (§7.1.1).
49    pub const COMMAND_AMF0: u8 = 20;
50    /// Aggregate Message (§7.1.6).
51    pub const AGGREGATE: u8 = 22;
52}
53
54/// Chunk stream id protocol control messages and user control messages
55/// MUST/SHOULD be sent on (§4, §5.3).
56pub const CONTROL_CHUNK_STREAM_ID: u32 = 2;
57/// Message stream id protocol control messages MUST use, and user control
58/// messages SHOULD use (§4, §5.3): the control stream.
59pub const CONTROL_MESSAGE_STREAM_ID: u32 = 0;
60
61/// Byte width of a `u32` protocol control field (chunk size, chunk stream
62/// id, sequence number, window ack size).
63const U32_LEN: usize = 4;
64/// Byte width of the Set Peer Bandwidth payload: window size (4) + limit
65/// type (1).
66const SET_PEER_BANDWIDTH_LEN: usize = U32_LEN + 1;
67
68/// Bit mask isolating the reserved top bit of the Set Chunk Size payload
69/// (§5.4.1: 1 reserved bit, MUST be 0, + 31-bit chunk size).
70const SET_CHUNK_SIZE_RESERVED_MASK: u32 = 0x8000_0000;
71/// Bit mask isolating the 31-bit chunk size field of the Set Chunk Size
72/// payload.
73const SET_CHUNK_SIZE_VALUE_MASK: u32 = 0x7FFF_FFFF;
74
75fn read_u32_be(b: &[u8]) -> u32 {
76    u32::from_be_bytes([b[0], b[1], b[2], b[3]])
77}
78
79fn need_u32(bytes: &[u8], what: &'static str) -> Result<u32> {
80    if bytes.len() < U32_LEN {
81        return Err(RtmpError::BufferTooShort {
82            need: U32_LEN,
83            have: bytes.len(),
84            what,
85        });
86    }
87    Ok(read_u32_be(bytes))
88}
89
90// ── Set Peer Bandwidth Limit Type (§5.4.5) ──────────────────────────────
91
92/// Set Peer Bandwidth's `Limit Type` byte (§5.4.5).
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum LimitType {
96    /// Peer SHOULD limit output to exactly the indicated window.
97    Hard,
98    /// Peer SHOULD limit output to the indicated window or its current
99    /// limit, whichever is smaller.
100    Soft,
101    /// If the previous Limit Type was Hard, treat as Hard; otherwise
102    /// ignore this message.
103    Dynamic,
104}
105
106impl LimitType {
107    /// The spec token for this limit type.
108    #[must_use]
109    pub fn name(&self) -> &'static str {
110        match self {
111            LimitType::Hard => "hard",
112            LimitType::Soft => "soft",
113            LimitType::Dynamic => "dynamic",
114        }
115    }
116
117    /// Decode the wire byte (0..=2) into a [`LimitType`].
118    ///
119    /// # Errors
120    /// [`RtmpError::Malformed`] if `v` is not in `0..=2`.
121    pub const fn from_u8(v: u8) -> core::result::Result<Self, RtmpError> {
122        match v {
123            0 => Ok(LimitType::Hard),
124            1 => Ok(LimitType::Soft),
125            2 => Ok(LimitType::Dynamic),
126            _ => Err(RtmpError::Malformed {
127                what: "set peer bandwidth limit type (must be 0..=2)",
128            }),
129        }
130    }
131
132    /// Encode this limit type back to its wire byte (0..=2).
133    #[must_use]
134    pub const fn to_u8(self) -> u8 {
135        match self {
136            LimitType::Hard => 0,
137            LimitType::Soft => 1,
138            LimitType::Dynamic => 2,
139        }
140    }
141}
142
143broadcast_common::impl_spec_display!(LimitType);
144
145// ── Protocol control messages (§4 / §5.4) ───────────────────────────────
146
147/// A Chunk-Stream-layer protocol control message (§4, spec §5.4):
148/// message type ids `1`, `2`, `3`, `5`, `6`. MUST use message stream id 0
149/// and chunk stream id 2 ([`CONTROL_MESSAGE_STREAM_ID`] /
150/// [`CONTROL_CHUNK_STREAM_ID`]); effective immediately on receipt.
151///
152/// `#[non_exhaustive]`: `§4`'s protocol control catalogue is closed today,
153/// but this mirrors [`UserControl`]/[`crate::amf0::Amf0Value`] so a future
154/// addition never breaks an existing `match`.
155#[non_exhaustive]
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum ProtocolControl {
158    /// Set Chunk Size (§5.4.1): the new maximum chunk size (`1..=0x7FFF_FFFF`).
159    SetChunkSize(u32),
160    /// Abort Message (§5.4.2): discard any partially-received message on
161    /// this chunk stream id.
162    Abort {
163        /// The chunk stream id whose in-progress message should be
164        /// discarded.
165        chunk_stream_id: u32,
166    },
167    /// Acknowledgement (§5.4.3): total bytes received so far.
168    Acknowledgement(u32),
169    /// Window Acknowledgement Size (§5.4.4): the sender's advertised
170    /// window size.
171    WindowAckSize(u32),
172    /// Set Peer Bandwidth (§5.4.5): limit the peer's output bandwidth.
173    SetPeerBandwidth {
174        /// The acknowledgement window size to limit the peer to.
175        ack_window_size: u32,
176        /// How strictly the peer should observe the limit.
177        limit_type: LimitType,
178    },
179}
180
181impl ProtocolControl {
182    /// The spec token for this protocol control message.
183    #[must_use]
184    pub fn name(&self) -> &'static str {
185        match self {
186            ProtocolControl::SetChunkSize(_) => "set chunk size",
187            ProtocolControl::Abort { .. } => "abort message",
188            ProtocolControl::Acknowledgement(_) => "acknowledgement",
189            ProtocolControl::WindowAckSize(_) => "window acknowledgement size",
190            ProtocolControl::SetPeerBandwidth { .. } => "set peer bandwidth",
191        }
192    }
193
194    /// This variant's message type id (§6/§7.1).
195    #[must_use]
196    pub fn message_type_id(&self) -> u8 {
197        match self {
198            ProtocolControl::SetChunkSize(_) => msg_type::SET_CHUNK_SIZE,
199            ProtocolControl::Abort { .. } => msg_type::ABORT,
200            ProtocolControl::Acknowledgement(_) => msg_type::ACKNOWLEDGEMENT,
201            ProtocolControl::WindowAckSize(_) => msg_type::WINDOW_ACK_SIZE,
202            ProtocolControl::SetPeerBandwidth { .. } => msg_type::SET_PEER_BANDWIDTH,
203        }
204    }
205
206    /// Interpret an already-reassembled [`Message`] as a protocol control
207    /// message, dispatching on `message.message_type_id`.
208    ///
209    /// Returns `Ok(None)` if `message.message_type_id` is not one of the
210    /// protocol control ids (`1`, `2`, `3`, `5`, `6`) — the caller should
211    /// then dispatch it elsewhere (user control, audio/video, command, …).
212    ///
213    /// # Errors
214    /// [`RtmpError::BufferTooShort`] if the payload is shorter than the
215    /// message type requires; [`RtmpError::Malformed`] if a field violates
216    /// its wire constraint (reserved bit set, out-of-range limit type).
217    pub fn from_message(message: &Message) -> Result<Option<Self>> {
218        Self::from_payload(message.message_type_id, &message.payload)
219    }
220
221    /// Parse a protocol control payload given its message type id. Not a
222    /// [`Parse`] impl: unlike every other wire type in this crate, a
223    /// protocol control payload alone is ambiguous (e.g. a bare 4-byte
224    /// payload is `SetChunkSize`, `Abort`, `Acknowledgement`, or
225    /// `WindowAckSize` depending on the message type id carried alongside
226    /// it in the [`Message`] header) — the type id is required context.
227    ///
228    /// # Errors
229    /// See [`ProtocolControl::from_message`].
230    pub fn from_payload(message_type_id: u8, payload: &[u8]) -> Result<Option<Self>> {
231        match message_type_id {
232            msg_type::SET_CHUNK_SIZE => {
233                let raw = need_u32(payload, "set chunk size payload")?;
234                if raw & SET_CHUNK_SIZE_RESERVED_MASK != 0 {
235                    return Err(RtmpError::Malformed {
236                        what: "set chunk size reserved top bit (must be 0)",
237                    });
238                }
239                let size = raw & SET_CHUNK_SIZE_VALUE_MASK;
240                if size == 0 {
241                    return Err(RtmpError::Malformed {
242                        what: "set chunk size value (must be >= 1)",
243                    });
244                }
245                Ok(Some(ProtocolControl::SetChunkSize(size)))
246            }
247            msg_type::ABORT => {
248                let chunk_stream_id = need_u32(payload, "abort message payload")?;
249                Ok(Some(ProtocolControl::Abort { chunk_stream_id }))
250            }
251            msg_type::ACKNOWLEDGEMENT => {
252                let sequence_number = need_u32(payload, "acknowledgement payload")?;
253                Ok(Some(ProtocolControl::Acknowledgement(sequence_number)))
254            }
255            msg_type::WINDOW_ACK_SIZE => {
256                let window = need_u32(payload, "window acknowledgement size payload")?;
257                Ok(Some(ProtocolControl::WindowAckSize(window)))
258            }
259            msg_type::SET_PEER_BANDWIDTH => {
260                if payload.len() < SET_PEER_BANDWIDTH_LEN {
261                    return Err(RtmpError::BufferTooShort {
262                        need: SET_PEER_BANDWIDTH_LEN,
263                        have: payload.len(),
264                        what: "set peer bandwidth payload",
265                    });
266                }
267                let ack_window_size = read_u32_be(&payload[0..U32_LEN]);
268                let limit_type = LimitType::from_u8(payload[U32_LEN])?;
269                Ok(Some(ProtocolControl::SetPeerBandwidth {
270                    ack_window_size,
271                    limit_type,
272                }))
273            }
274            _ => Ok(None),
275        }
276    }
277
278    /// Wrap this protocol control message in a [`Message`] ready to hand to
279    /// [`crate::chunk::ChunkWriter`] — chunk stream id
280    /// [`CONTROL_CHUNK_STREAM_ID`], message stream id
281    /// [`CONTROL_MESSAGE_STREAM_ID`], timestamp `0` (protocol control
282    /// messages take effect immediately; timestamps are not meaningful).
283    #[must_use]
284    pub fn to_message(&self) -> Message {
285        Message {
286            chunk_stream_id: CONTROL_CHUNK_STREAM_ID,
287            timestamp: 0,
288            message_type_id: self.message_type_id(),
289            message_stream_id: CONTROL_MESSAGE_STREAM_ID,
290            payload: self.to_bytes(),
291        }
292    }
293}
294
295broadcast_common::impl_spec_display!(ProtocolControl);
296
297impl Serialize for ProtocolControl {
298    type Error = RtmpError;
299
300    fn serialized_len(&self) -> usize {
301        match self {
302            ProtocolControl::SetChunkSize(_)
303            | ProtocolControl::Abort { .. }
304            | ProtocolControl::Acknowledgement(_)
305            | ProtocolControl::WindowAckSize(_) => U32_LEN,
306            ProtocolControl::SetPeerBandwidth { .. } => SET_PEER_BANDWIDTH_LEN,
307        }
308    }
309
310    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
311        let written = self.serialized_len();
312        if buf.len() < written {
313            return Err(RtmpError::BufferTooShort {
314                need: written,
315                have: buf.len(),
316                what: "protocol control payload output",
317            });
318        }
319        match *self {
320            ProtocolControl::SetChunkSize(size) => {
321                if size == 0 || size & SET_CHUNK_SIZE_RESERVED_MASK != 0 {
322                    return Err(RtmpError::Malformed {
323                        what: "set chunk size value (must be 1..=0x7FFF_FFFF)",
324                    });
325                }
326                buf[0..U32_LEN].copy_from_slice(&size.to_be_bytes());
327            }
328            ProtocolControl::Abort { chunk_stream_id } => {
329                buf[0..U32_LEN].copy_from_slice(&chunk_stream_id.to_be_bytes());
330            }
331            ProtocolControl::Acknowledgement(sequence_number) => {
332                buf[0..U32_LEN].copy_from_slice(&sequence_number.to_be_bytes());
333            }
334            ProtocolControl::WindowAckSize(window) => {
335                buf[0..U32_LEN].copy_from_slice(&window.to_be_bytes());
336            }
337            ProtocolControl::SetPeerBandwidth {
338                ack_window_size,
339                limit_type,
340            } => {
341                buf[0..U32_LEN].copy_from_slice(&ack_window_size.to_be_bytes());
342                buf[U32_LEN] = limit_type.to_u8();
343            }
344        }
345        Ok(written)
346    }
347}
348
349// ── User control messages (§5.3 / §6.2 / §6.7) ──────────────────────────
350
351/// Byte width of the User Control Message's 16-bit `Event Type` field.
352const EVENT_TYPE_LEN: usize = 2;
353
354/// User Control Message event types (§6.7, spec §7.1.7).
355mod event_type {
356    pub const STREAM_BEGIN: u16 = 0;
357    pub const STREAM_EOF: u16 = 1;
358    pub const STREAM_DRY: u16 = 2;
359    pub const SET_BUFFER_LENGTH: u16 = 3;
360    pub const STREAM_IS_RECORDED: u16 = 4;
361    // Event value 5 is not defined by the spec.
362    pub const PING_REQUEST: u16 = 6;
363    pub const PING_RESPONSE: u16 = 7;
364}
365
366/// A User Control Message event (§5.3, message type id 4; event-data
367/// formats per §6.7, spec §7.1.7). SHOULD use message stream id 0 and,
368/// over the chunk stream, csid 2. Effective on receipt; timestamps
369/// ignored.
370///
371/// `#[non_exhaustive]`: §6.7's event-type catalogue (event value 5 is
372/// already an unassigned gap) can grow; a new event type must not be a
373/// breaking change for existing `match` callers.
374#[non_exhaustive]
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub enum UserControl {
377    /// Stream Begin (event 0, server→client): the stream is now
378    /// functional/usable. By default sent on stream id 0 right after a
379    /// successful `connect`.
380    StreamBegin(u32),
381    /// Stream EOF (event 1, server→client): playback requested on this
382    /// stream has ended.
383    StreamEof(u32),
384    /// StreamDry (event 2, server→client): no more data on the stream
385    /// (server-detected idle).
386    StreamDry(u32),
387    /// SetBufferLength (event 3, client→server): the client's playback
388    /// buffer size, sent before the server starts sending the stream.
389    SetBufferLength {
390        /// The stream this buffer length applies to.
391        stream_id: u32,
392        /// The client's buffer length, in milliseconds.
393        buffer_ms: u32,
394    },
395    /// StreamIsRecorded (event 4, server→client): the stream is a
396    /// recorded (not live) stream.
397    StreamIsRecorded(u32),
398    /// PingRequest (event 6, server→client): test reachability; the
399    /// client MUST reply with PingResponse.
400    PingRequest(u32),
401    /// PingResponse (event 7, client→server): reply to PingRequest,
402    /// echoing its timestamp.
403    PingResponse(u32),
404}
405
406impl UserControl {
407    /// The spec token for this user control event.
408    #[must_use]
409    pub fn name(&self) -> &'static str {
410        match self {
411            UserControl::StreamBegin(_) => "stream begin",
412            UserControl::StreamEof(_) => "stream eof",
413            UserControl::StreamDry(_) => "stream dry",
414            UserControl::SetBufferLength { .. } => "set buffer length",
415            UserControl::StreamIsRecorded(_) => "stream is recorded",
416            UserControl::PingRequest(_) => "ping request",
417            UserControl::PingResponse(_) => "ping response",
418        }
419    }
420
421    /// This event's 16-bit event type value (§6.7).
422    #[must_use]
423    pub fn event_type(&self) -> u16 {
424        match self {
425            UserControl::StreamBegin(_) => event_type::STREAM_BEGIN,
426            UserControl::StreamEof(_) => event_type::STREAM_EOF,
427            UserControl::StreamDry(_) => event_type::STREAM_DRY,
428            UserControl::SetBufferLength { .. } => event_type::SET_BUFFER_LENGTH,
429            UserControl::StreamIsRecorded(_) => event_type::STREAM_IS_RECORDED,
430            UserControl::PingRequest(_) => event_type::PING_REQUEST,
431            UserControl::PingResponse(_) => event_type::PING_RESPONSE,
432        }
433    }
434
435    /// Wrap this user control event in a [`Message`] ready to hand to
436    /// [`crate::chunk::ChunkWriter`] — chunk stream id
437    /// [`CONTROL_CHUNK_STREAM_ID`], message stream id
438    /// [`CONTROL_MESSAGE_STREAM_ID`], timestamp `0` (effective on receipt;
439    /// timestamps are not meaningful).
440    #[must_use]
441    pub fn to_message(&self) -> Message {
442        Message {
443            chunk_stream_id: CONTROL_CHUNK_STREAM_ID,
444            timestamp: 0,
445            message_type_id: msg_type::USER_CONTROL,
446            message_stream_id: CONTROL_MESSAGE_STREAM_ID,
447            payload: self.to_bytes(),
448        }
449    }
450}
451
452broadcast_common::impl_spec_display!(UserControl);
453
454impl<'a> Parse<'a> for UserControl {
455    type Error = RtmpError;
456
457    fn parse(bytes: &'a [u8]) -> Result<Self> {
458        if bytes.len() < EVENT_TYPE_LEN {
459            return Err(RtmpError::BufferTooShort {
460                need: EVENT_TYPE_LEN,
461                have: bytes.len(),
462                what: "user control event type",
463            });
464        }
465        let event = u16::from_be_bytes([bytes[0], bytes[1]]);
466        let data = &bytes[EVENT_TYPE_LEN..];
467        match event {
468            event_type::STREAM_BEGIN => Ok(UserControl::StreamBegin(need_u32(
469                data,
470                "stream begin event data",
471            )?)),
472            event_type::STREAM_EOF => Ok(UserControl::StreamEof(need_u32(
473                data,
474                "stream eof event data",
475            )?)),
476            event_type::STREAM_DRY => Ok(UserControl::StreamDry(need_u32(
477                data,
478                "stream dry event data",
479            )?)),
480            event_type::SET_BUFFER_LENGTH => {
481                if data.len() < 2 * U32_LEN {
482                    return Err(RtmpError::BufferTooShort {
483                        need: 2 * U32_LEN,
484                        have: data.len(),
485                        what: "set buffer length event data",
486                    });
487                }
488                Ok(UserControl::SetBufferLength {
489                    stream_id: read_u32_be(&data[0..U32_LEN]),
490                    buffer_ms: read_u32_be(&data[U32_LEN..2 * U32_LEN]),
491                })
492            }
493            event_type::STREAM_IS_RECORDED => Ok(UserControl::StreamIsRecorded(need_u32(
494                data,
495                "stream is recorded event data",
496            )?)),
497            event_type::PING_REQUEST => Ok(UserControl::PingRequest(need_u32(
498                data,
499                "ping request event data",
500            )?)),
501            event_type::PING_RESPONSE => Ok(UserControl::PingResponse(need_u32(
502                data,
503                "ping response event data",
504            )?)),
505            _ => Err(RtmpError::Unsupported {
506                what: "user control event type (unrecognised)",
507            }),
508        }
509    }
510}
511
512impl Serialize for UserControl {
513    type Error = RtmpError;
514
515    fn serialized_len(&self) -> usize {
516        let data_len = match self {
517            UserControl::SetBufferLength { .. } => 2 * U32_LEN,
518            _ => U32_LEN,
519        };
520        EVENT_TYPE_LEN + data_len
521    }
522
523    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
524        let written = self.serialized_len();
525        if buf.len() < written {
526            return Err(RtmpError::BufferTooShort {
527                need: written,
528                have: buf.len(),
529                what: "user control event output",
530            });
531        }
532        buf[0..EVENT_TYPE_LEN].copy_from_slice(&self.event_type().to_be_bytes());
533        let data = &mut buf[EVENT_TYPE_LEN..written];
534        match *self {
535            UserControl::StreamBegin(stream_id)
536            | UserControl::StreamEof(stream_id)
537            | UserControl::StreamDry(stream_id)
538            | UserControl::StreamIsRecorded(stream_id)
539            | UserControl::PingRequest(stream_id)
540            | UserControl::PingResponse(stream_id) => {
541                data[0..U32_LEN].copy_from_slice(&stream_id.to_be_bytes());
542            }
543            UserControl::SetBufferLength {
544                stream_id,
545                buffer_ms,
546            } => {
547                data[0..U32_LEN].copy_from_slice(&stream_id.to_be_bytes());
548                data[U32_LEN..2 * U32_LEN].copy_from_slice(&buffer_ms.to_be_bytes());
549            }
550        }
551        Ok(written)
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    fn message(message_type_id: u8, payload: Vec<u8>) -> Message {
560        Message {
561            chunk_stream_id: CONTROL_CHUNK_STREAM_ID,
562            timestamp: 0,
563            message_type_id,
564            message_stream_id: CONTROL_MESSAGE_STREAM_ID,
565            payload,
566        }
567    }
568
569    // ── LimitType ────────────────────────────────────────────────────────
570
571    #[test]
572    fn limit_type_round_trip_and_name() {
573        for (byte, lt, name) in [
574            (0u8, LimitType::Hard, "hard"),
575            (1, LimitType::Soft, "soft"),
576            (2, LimitType::Dynamic, "dynamic"),
577        ] {
578            let parsed = LimitType::from_u8(byte).unwrap();
579            assert_eq!(parsed, lt);
580            assert_eq!(parsed.to_u8(), byte);
581            assert_eq!(parsed.name(), name);
582            assert_eq!(parsed.to_string(), name);
583        }
584    }
585
586    #[test]
587    fn limit_type_out_of_range_is_malformed() {
588        assert!(matches!(
589            LimitType::from_u8(3),
590            Err(RtmpError::Malformed { .. })
591        ));
592    }
593
594    // ── ProtocolControl round-trips ──────────────────────────────────────
595
596    fn protocol_control_round_trip(pc: ProtocolControl) {
597        let bytes = pc.to_bytes();
598        let parsed = ProtocolControl::from_payload(pc.message_type_id(), &bytes)
599            .unwrap()
600            .expect("known protocol control type id");
601        assert_eq!(parsed, pc);
602
603        // parse -> serialize -> byte-identical
604        let msg = message(pc.message_type_id(), bytes.clone());
605        let via_message = ProtocolControl::from_message(&msg).unwrap().unwrap();
606        assert_eq!(via_message, pc);
607        assert_eq!(via_message.to_bytes(), bytes);
608    }
609
610    #[test]
611    fn set_chunk_size_round_trips() {
612        protocol_control_round_trip(ProtocolControl::SetChunkSize(4096));
613    }
614
615    #[test]
616    fn abort_round_trips() {
617        protocol_control_round_trip(ProtocolControl::Abort { chunk_stream_id: 7 });
618    }
619
620    #[test]
621    fn acknowledgement_round_trips() {
622        protocol_control_round_trip(ProtocolControl::Acknowledgement(1_048_576));
623    }
624
625    #[test]
626    fn window_ack_size_round_trips() {
627        protocol_control_round_trip(ProtocolControl::WindowAckSize(2_500_000));
628    }
629
630    #[test]
631    fn set_peer_bandwidth_round_trips_every_limit_type() {
632        for limit_type in [LimitType::Hard, LimitType::Soft, LimitType::Dynamic] {
633            protocol_control_round_trip(ProtocolControl::SetPeerBandwidth {
634                ack_window_size: 2_500_000,
635                limit_type,
636            });
637        }
638    }
639
640    #[test]
641    fn set_chunk_size_reserved_top_bit_rejected_on_parse() {
642        let bytes = 0x8000_1000u32.to_be_bytes().to_vec();
643        assert!(matches!(
644            ProtocolControl::from_payload(msg_type::SET_CHUNK_SIZE, &bytes),
645            Err(RtmpError::Malformed { .. })
646        ));
647    }
648
649    #[test]
650    fn set_chunk_size_zero_rejected() {
651        let bytes = 0u32.to_be_bytes().to_vec();
652        assert!(matches!(
653            ProtocolControl::from_payload(msg_type::SET_CHUNK_SIZE, &bytes),
654            Err(RtmpError::Malformed { .. })
655        ));
656        assert!(matches!(
657            ProtocolControl::SetChunkSize(0).serialize_into(&mut [0u8; 4]),
658            Err(RtmpError::Malformed { .. })
659        ));
660    }
661
662    #[test]
663    fn set_chunk_size_serialize_layout_matches_spec() {
664        // §5.4.1: reserved bit 0, 31-bit chunk size, big-endian.
665        let bytes = ProtocolControl::SetChunkSize(1).to_bytes();
666        assert_eq!(bytes, vec![0x00, 0x00, 0x00, 0x01]);
667    }
668
669    #[test]
670    fn set_peer_bandwidth_serialize_layout_matches_spec() {
671        let bytes = ProtocolControl::SetPeerBandwidth {
672            ack_window_size: 0x0002_5000,
673            limit_type: LimitType::Dynamic,
674        }
675        .to_bytes();
676        assert_eq!(bytes, vec![0x00, 0x02, 0x50, 0x00, 0x02]);
677    }
678
679    #[test]
680    fn set_peer_bandwidth_wrong_limit_type_mapping_would_fail() {
681        // Mutation check: swapping Hard/Dynamic's wire values would break this.
682        assert_eq!(LimitType::Hard.to_u8(), 0);
683        assert_eq!(LimitType::Dynamic.to_u8(), 2);
684        assert_ne!(LimitType::Hard.to_u8(), LimitType::Dynamic.to_u8());
685    }
686
687    #[test]
688    fn from_message_none_for_non_control_type_id() {
689        let msg = message(msg_type::AUDIO, vec![0u8; 4]);
690        assert!(ProtocolControl::from_message(&msg).unwrap().is_none());
691    }
692
693    #[test]
694    fn from_message_some_for_control_type_id() {
695        let msg = message(
696            msg_type::WINDOW_ACK_SIZE,
697            1_000_000u32.to_be_bytes().to_vec(),
698        );
699        assert!(ProtocolControl::from_message(&msg).unwrap().is_some());
700    }
701
702    #[test]
703    fn to_message_uses_control_csid_and_stream_id() {
704        let msg = ProtocolControl::SetChunkSize(4096).to_message();
705        assert_eq!(msg.chunk_stream_id, CONTROL_CHUNK_STREAM_ID);
706        assert_eq!(msg.message_stream_id, CONTROL_MESSAGE_STREAM_ID);
707        assert_eq!(msg.message_type_id, msg_type::SET_CHUNK_SIZE);
708    }
709
710    #[test]
711    fn protocol_control_display_matches_name() {
712        assert_eq!(
713            ProtocolControl::Acknowledgement(1).to_string(),
714            ProtocolControl::Acknowledgement(1).name()
715        );
716    }
717
718    // ── UserControl round-trips ──────────────────────────────────────────
719
720    fn user_control_round_trip(uc: UserControl) {
721        let bytes = uc.to_bytes();
722        let parsed = UserControl::parse(&bytes).unwrap();
723        assert_eq!(parsed, uc);
724        assert_eq!(parsed.to_bytes(), bytes);
725    }
726
727    #[test]
728    fn stream_begin_round_trips() {
729        user_control_round_trip(UserControl::StreamBegin(1));
730    }
731
732    #[test]
733    fn stream_begin_serialize_layout_matches_spec() {
734        // §6.7: event type 0x0000 + 4-byte stream id, big-endian.
735        let bytes = UserControl::StreamBegin(1).to_bytes();
736        assert_eq!(bytes, vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x01]);
737    }
738
739    #[test]
740    fn stream_eof_round_trips() {
741        user_control_round_trip(UserControl::StreamEof(1));
742    }
743
744    #[test]
745    fn stream_dry_round_trips() {
746        user_control_round_trip(UserControl::StreamDry(1));
747    }
748
749    #[test]
750    fn set_buffer_length_round_trips() {
751        user_control_round_trip(UserControl::SetBufferLength {
752            stream_id: 1,
753            buffer_ms: 3000,
754        });
755    }
756
757    #[test]
758    fn stream_is_recorded_round_trips() {
759        user_control_round_trip(UserControl::StreamIsRecorded(1));
760    }
761
762    #[test]
763    fn ping_request_round_trips() {
764        user_control_round_trip(UserControl::PingRequest(0x1234_5678));
765    }
766
767    #[test]
768    fn ping_response_round_trips() {
769        user_control_round_trip(UserControl::PingResponse(0x1234_5678));
770    }
771
772    #[test]
773    fn unrecognised_event_type_is_unsupported() {
774        // Event value 5 is not defined by the spec.
775        let bytes = [0x00, 0x05, 0x00, 0x00, 0x00, 0x01];
776        assert!(matches!(
777            UserControl::parse(&bytes),
778            Err(RtmpError::Unsupported { .. })
779        ));
780    }
781
782    #[test]
783    fn user_control_event_type_wrong_mapping_would_fail() {
784        // Mutation check: swapping StreamBegin/StreamEof's event-type
785        // values would break this.
786        assert_eq!(UserControl::StreamBegin(0).event_type(), 0);
787        assert_eq!(UserControl::StreamEof(0).event_type(), 1);
788    }
789
790    #[test]
791    fn user_control_display_matches_name() {
792        assert_eq!(
793            UserControl::StreamBegin(1).to_string(),
794            UserControl::StreamBegin(1).name()
795        );
796    }
797
798    #[test]
799    fn to_message_uses_control_csid_and_user_control_type_id() {
800        let msg = UserControl::StreamBegin(1).to_message();
801        assert_eq!(msg.chunk_stream_id, CONTROL_CHUNK_STREAM_ID);
802        assert_eq!(msg.message_stream_id, CONTROL_MESSAGE_STREAM_ID);
803        assert_eq!(msg.message_type_id, msg_type::USER_CONTROL);
804    }
805}