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