Skip to main content

encypher_c2pa/
ertmp_wire.rs

1// Copyright 2026 Encypher Corporation
2// SPDX-License-Identifier: Apache-2.0
3
4//! Bounded FLV and RTMP framing for the E-RTMP verifier.
5//!
6//! The packet layouts follow Veovera Enhanced RTMP v2 at revision
7//! `dc10175591d24d4dfd4ff1908d6b4d0215a69496`. This module only frames and
8//! verifies transport. It does not contain signing keys or sign content.
9//! For a type-3 chunk starting a new message after a type-0 header, the
10//! preceding absolute timestamp is also the inherited delta, as specified by
11//! Adobe RTMP 5.3.1.2.4.
12
13mod amf3;
14
15use std::collections::HashMap;
16use std::error::Error as StdError;
17use std::fmt;
18
19use crate::{ErtmpEvent, ErtmpInput, ErtmpMediaType, ErtmpTrack};
20
21const SET_CHUNK_SIZE: u8 = 1;
22const ABORT_MESSAGE: u8 = 2;
23const ACKNOWLEDGEMENT: u8 = 3;
24const WINDOW_ACKNOWLEDGEMENT_SIZE: u8 = 5;
25const SET_PEER_BANDWIDTH: u8 = 6;
26const AUDIO_MESSAGE: u8 = 8;
27const VIDEO_MESSAGE: u8 = 9;
28const AGGREGATE_MESSAGE: u8 = 22;
29const AMF3_SCRIPT_DATA: u8 = 15;
30const AMF0_SCRIPT_DATA: u8 = 18;
31const VIDEO_METADATA: u8 = 4;
32const VIDEO_MULTITRACK: u8 = 6;
33const AUDIO_MULTITRACK: u8 = 5;
34const PROTOCOL_CONTROL_CHUNK_STREAM_ID: u32 = 2;
35const MAX_VERIFIER_EVENTS: usize = 1_000_000;
36
37/// Hard bounds applied before allocating from untrusted wire lengths.
38#[derive(Debug, Clone, Copy)]
39pub struct ErtmpWireLimits {
40    /// Maximum bytes accepted for one connection or FLV file.
41    pub max_wire_bytes: usize,
42    /// Maximum bytes accepted across all supplied RTMP connections.
43    pub max_total_wire_bytes: usize,
44    /// Maximum reassembled RTMP message or FLV tag body.
45    pub max_message_bytes: usize,
46    /// Maximum bytes buffered across incomplete RTMP messages.
47    pub max_buffered_message_bytes: usize,
48    /// Maximum chunk streams retaining compression or reassembly state.
49    pub max_active_chunk_streams: usize,
50    /// Maximum emitted verifier events.
51    pub max_events: usize,
52    /// Maximum aggregate bytes owned by emitted event names, payloads, and
53    /// index-aligned wire contexts.
54    pub max_total_event_payload_bytes: usize,
55    /// Maximum negotiated RTMP chunk size.
56    pub max_chunk_size: usize,
57}
58
59impl Default for ErtmpWireLimits {
60    fn default() -> Self {
61        Self {
62            max_wire_bytes: 100_000_000,
63            max_total_wire_bytes: 100_000_000,
64            max_message_bytes: 16 * 1024 * 1024,
65            max_buffered_message_bytes: 32 * 1024 * 1024,
66            max_active_chunk_streams: 4096,
67            max_events: MAX_VERIFIER_EVENTS,
68            max_total_event_payload_bytes: 100_000_000,
69            max_chunk_size: 1024 * 1024,
70        }
71    }
72}
73
74/// Caller-supplied E-RTMP capability negotiation assertions.
75#[derive(Debug, Clone, Copy)]
76pub struct ErtmpConnectionContext {
77    /// Client `capsEx` value from the connect command.
78    pub client_caps_ex: u64,
79    /// Server `capsEx` value from the connect response.
80    pub server_caps_ex: u64,
81    /// Negotiated AMF object encoding.
82    pub object_encoding: u8,
83}
84
85/// One complete publisher-to-server RTMP connection after its handshake.
86#[derive(Debug, Clone, Copy)]
87pub struct RtmpWireConnection<'a> {
88    /// Complete post-handshake chunk-stream bytes in publisher-to-server wire order.
89    pub bytes: &'a [u8],
90    /// Negotiation assertions for this connection, not an earlier connection.
91    pub connection: ErtmpConnectionContext,
92    /// Chunk size in force at the start of this connection trace.
93    pub initial_chunk_size: usize,
94}
95
96/// Parsed verifier input plus wire identity for every event.
97#[derive(Debug, Clone)]
98pub struct ErtmpWireInput {
99    /// Core events accepted by [`crate::verify_ertmp`].
100    pub input: ErtmpInput,
101    /// Wire identity aligned one-for-one with `input.events`.
102    pub event_contexts: Vec<Option<ErtmpWireEventContext>>,
103}
104
105/// Wire identity for one media-derived verifier event.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct ErtmpWireEventContext {
108    /// RTMP message stream ID. FLV inputs have no such field and use `None`.
109    pub message_stream_id: Option<u32>,
110    /// Enhanced RTMP codec FOURCC, including mapped legacy AVC and AAC.
111    pub codec_fourcc: Option<[u8; 4]>,
112    /// Whether the event came from an Enhanced RTMP Multitrack packet.
113    pub multitrack: bool,
114}
115
116/// FLV has no chunk-stream identity, so callers must provide the IDs from the
117/// RTMP connection that the file/tag trace represents.
118#[derive(Debug, Clone, Copy)]
119pub struct FlvWireOptions {
120    /// Caller-supplied connection capability assertions.
121    pub connection: ErtmpConnectionContext,
122    /// Chunk stream carrying audio tags.
123    pub audio_chunk_stream_id: u32,
124    /// Chunk stream carrying video tags.
125    pub video_chunk_stream_id: u32,
126    /// Resource bounds.
127    pub limits: ErtmpWireLimits,
128}
129
130/// A framing or media-packet defect. Every defect is fatal: malformed input is
131/// never converted into a partial event stream.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct ErtmpWireError {
134    message: String,
135}
136
137impl ErtmpWireError {
138    fn new(message: impl Into<String>) -> Self {
139        Self {
140            message: message.into(),
141        }
142    }
143}
144
145impl fmt::Display for ErtmpWireError {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        f.write_str(&self.message)
148    }
149}
150
151impl StdError for ErtmpWireError {}
152
153/// Parse one complete FLV file into ordered verifier events.
154pub fn ertmp_input_from_flv(
155    bytes: &[u8],
156    options: FlvWireOptions,
157) -> Result<ErtmpWireInput, ErtmpWireError> {
158    validate_connection_context(options.connection)?;
159    if bytes.len() > options.limits.max_wire_bytes
160        || bytes.len() > options.limits.max_total_wire_bytes
161    {
162        return Err(ErtmpWireError::new("FLV input exceeds the wire byte limit"));
163    }
164    if bytes.len() < 13 || &bytes[..3] != b"FLV" || bytes[3] != 1 {
165        return Err(ErtmpWireError::new("invalid or truncated FLV header"));
166    }
167    if bytes[4] & 0xfa != 0 {
168        return Err(ErtmpWireError::new("FLV header reserved bits are nonzero"));
169    }
170    let data_offset = be_u32(&bytes[5..9]) as usize;
171    if data_offset < 9
172        || data_offset
173            .checked_add(4)
174            .is_none_or(|end| end > bytes.len())
175    {
176        return Err(ErtmpWireError::new("invalid FLV data offset"));
177    }
178    if be_u32(&bytes[data_offset..data_offset + 4]) != 0 {
179        return Err(ErtmpWireError::new("FLV PreviousTagSize0 is not zero"));
180    }
181
182    let mut sink = EventSink::new(options.limits);
183    let mut bindings = TrackBindings::default();
184    let mut offset = data_offset + 4;
185    while offset < bytes.len() {
186        let remaining = bytes.len() - offset;
187        if remaining < 15 {
188            return Err(ErtmpWireError::new("truncated FLV tag"));
189        }
190        let tag_byte = bytes[offset];
191        if tag_byte & 0xe0 != 0 {
192            return Err(ErtmpWireError::new(
193                "reserved, encrypted, or filtered FLV tags are unsupported",
194            ));
195        }
196        let tag_type = tag_byte & 0x1f;
197        let data_size = be_u24(&bytes[offset + 1..offset + 4]) as usize;
198        if data_size > options.limits.max_message_bytes {
199            return Err(ErtmpWireError::new(
200                "FLV tag exceeds the message byte limit",
201            ));
202        }
203        let tag_end = offset
204            .checked_add(11)
205            .and_then(|value| value.checked_add(data_size))
206            .and_then(|value| value.checked_add(4))
207            .ok_or_else(|| ErtmpWireError::new("FLV tag length overflow"))?;
208        if tag_end > bytes.len() {
209            return Err(ErtmpWireError::new("truncated FLV tag body"));
210        }
211        if bytes[offset + 8..offset + 11] != [0, 0, 0] {
212            return Err(ErtmpWireError::new("FLV tag stream id is not zero"));
213        }
214        let timestamp = be_u24(&bytes[offset + 4..offset + 7]) | ((bytes[offset + 7] as u32) << 24);
215        let body = &bytes[offset + 11..offset + 11 + data_size];
216        let previous = be_u32(&bytes[offset + 11 + data_size..tag_end]) as usize;
217        if previous != 11 + data_size {
218            return Err(ErtmpWireError::new(
219                "FLV PreviousTagSize does not match its tag",
220            ));
221        }
222        match tag_type {
223            AUDIO_MESSAGE => {
224                if bytes[4] & 0x04 == 0 {
225                    return Err(ErtmpWireError::new(
226                        "FLV audio tag contradicts the header flags",
227                    ));
228                }
229                parse_audio_message(
230                    body,
231                    timestamp,
232                    options.audio_chunk_stream_id,
233                    None,
234                    &mut bindings,
235                    &mut sink,
236                )?;
237            }
238            VIDEO_MESSAGE => {
239                if bytes[4] & 0x01 == 0 {
240                    return Err(ErtmpWireError::new(
241                        "FLV video tag contradicts the header flags",
242                    ));
243                }
244                parse_video_message(
245                    body,
246                    timestamp,
247                    options.video_chunk_stream_id,
248                    None,
249                    &mut bindings,
250                    &mut sink,
251                )?;
252            }
253            AMF3_SCRIPT_DATA | AMF0_SCRIPT_DATA => {}
254            other => {
255                return Err(ErtmpWireError::new(format!(
256                    "unsupported FLV tag type {other}"
257                )))
258            }
259        }
260        offset = tag_end;
261    }
262    Ok(wire_input(options.connection, sink))
263}
264
265/// Parse complete post-handshake publisher-to-server RTMP connection traces. Each
266/// trace carries its own caller-supplied negotiation assertions and initial chunk
267/// size. A reconnect event is inserted between traces; chunk, message, and
268/// track-to-message-stream state resets at that point.
269pub fn ertmp_input_from_rtmp_connections(
270    connections: &[RtmpWireConnection<'_>],
271    limits: ErtmpWireLimits,
272) -> Result<ErtmpWireInput, ErtmpWireError> {
273    let Some(first) = connections.first() else {
274        return Err(ErtmpWireError::new(
275            "at least one RTMP connection is required",
276        ));
277    };
278
279    let mut total_wire_bytes = 0usize;
280    for trace in connections {
281        validate_connection_context(trace.connection)?;
282        if trace.initial_chunk_size == 0 || trace.initial_chunk_size > limits.max_chunk_size {
283            return Err(ErtmpWireError::new(
284                "initial RTMP chunk size is out of bounds",
285            ));
286        }
287        if trace.bytes.len() > limits.max_wire_bytes {
288            return Err(ErtmpWireError::new(
289                "RTMP connection exceeds the wire byte limit",
290            ));
291        }
292        total_wire_bytes = total_wire_bytes
293            .checked_add(trace.bytes.len())
294            .ok_or_else(|| ErtmpWireError::new("total RTMP wire byte count overflow"))?;
295        if total_wire_bytes > limits.max_total_wire_bytes {
296            return Err(ErtmpWireError::new(
297                "RTMP connections exceed the total wire byte limit",
298            ));
299        }
300    }
301
302    let mut sink = EventSink::new(limits);
303    for (index, trace) in connections.iter().enumerate() {
304        if index != 0 {
305            sink.push_reconnect()?;
306        }
307        RtmpDecoder::new(trace.initial_chunk_size, limits).decode(trace.bytes, &mut sink)?;
308    }
309    Ok(wire_input(first.connection, sink))
310}
311
312fn validate_connection_context(context: ErtmpConnectionContext) -> Result<(), ErtmpWireError> {
313    if context.client_caps_ex & 0x10 == 0
314        || context.server_caps_ex & 0x10 == 0
315        || context.object_encoding != 3
316    {
317        return Err(ErtmpWireError::new(
318            "connection did not negotiate C2PA with AMF3 on both endpoints",
319        ));
320    }
321    Ok(())
322}
323
324fn wire_input(connection: ErtmpConnectionContext, sink: EventSink) -> ErtmpWireInput {
325    ErtmpWireInput {
326        input: ErtmpInput {
327            client_caps_ex: connection.client_caps_ex,
328            server_caps_ex: connection.server_caps_ex,
329            object_encoding: connection.object_encoding,
330            events: sink.events,
331        },
332        event_contexts: sink.event_contexts,
333    }
334}
335
336struct EventSink {
337    events: Vec<ErtmpEvent>,
338    event_contexts: Vec<Option<ErtmpWireEventContext>>,
339    payload_bytes: usize,
340    limits: ErtmpWireLimits,
341}
342
343impl EventSink {
344    fn new(limits: ErtmpWireLimits) -> Self {
345        Self {
346            events: Vec::new(),
347            event_contexts: Vec::new(),
348            payload_bytes: 0,
349            limits,
350        }
351    }
352
353    fn remaining_events(&self) -> usize {
354        self.limits
355            .max_events
356            .min(MAX_VERIFIER_EVENTS)
357            .saturating_sub(self.events.len())
358    }
359
360    fn remaining_payload_bytes(&self) -> usize {
361        self.limits
362            .max_total_event_payload_bytes
363            .saturating_sub(self.payload_bytes)
364    }
365
366    fn claim(&mut self, bytes: usize) -> Result<(), ErtmpWireError> {
367        if self.remaining_events() == 0 {
368            return Err(ErtmpWireError::new(
369                "E-RTMP wire event count exceeds its limit",
370            ));
371        }
372        let bytes = bytes
373            .checked_add(std::mem::size_of::<Option<ErtmpWireEventContext>>())
374            .ok_or_else(|| ErtmpWireError::new("event ownership byte count overflow"))?;
375        let next = self
376            .payload_bytes
377            .checked_add(bytes)
378            .ok_or_else(|| ErtmpWireError::new("event payload byte count overflow"))?;
379        if next > self.limits.max_total_event_payload_bytes {
380            return Err(ErtmpWireError::new(
381                "E-RTMP event payload bytes exceed their limit",
382            ));
383        }
384        self.events
385            .try_reserve(1)
386            .map_err(|_| ErtmpWireError::new("unable to allocate E-RTMP event"))?;
387        self.event_contexts
388            .try_reserve(1)
389            .map_err(|_| ErtmpWireError::new("unable to allocate E-RTMP event context"))?;
390        self.payload_bytes = next;
391        Ok(())
392    }
393
394    fn push_reconnect(&mut self) -> Result<(), ErtmpWireError> {
395        self.claim(0)?;
396        self.events.push(ErtmpEvent::Reconnect);
397        self.event_contexts.push(None);
398        Ok(())
399    }
400
401    fn push_codec_config(
402        &mut self,
403        track: ErtmpTrack,
404        chunk_stream_id: u32,
405        payload: &[u8],
406        context: ErtmpWireEventContext,
407    ) -> Result<(), ErtmpWireError> {
408        self.claim(payload.len())?;
409        let payload = owned_bytes(payload)?;
410        self.events.push(ErtmpEvent::CodecConfig {
411            track,
412            chunk_stream_id,
413            payload,
414        });
415        self.event_contexts.push(Some(context));
416        Ok(())
417    }
418
419    fn push_frame(
420        &mut self,
421        track: ErtmpTrack,
422        chunk_stream_id: u32,
423        timestamp: u32,
424        composition_time_offset: i32,
425        coded_data: &[u8],
426        context: ErtmpWireEventContext,
427    ) -> Result<(), ErtmpWireError> {
428        self.claim(coded_data.len())?;
429        let coded_data = owned_bytes(coded_data)?;
430        self.events.push(ErtmpEvent::Frame {
431            track,
432            chunk_stream_id,
433            timestamp,
434            composition_time_offset,
435            coded_data,
436        });
437        self.event_contexts.push(Some(context));
438        Ok(())
439    }
440
441    fn push_metadata(
442        &mut self,
443        track: ErtmpTrack,
444        chunk_stream_id: u32,
445        timestamp: u32,
446        name: &str,
447        value: &[u8],
448        context: ErtmpWireEventContext,
449    ) -> Result<(), ErtmpWireError> {
450        let owned_size = name
451            .len()
452            .checked_add(value.len())
453            .ok_or_else(|| ErtmpWireError::new("metadata event byte count overflow"))?;
454        self.claim(owned_size)?;
455        let value = owned_bytes(value)?;
456        let mut owned_name = String::new();
457        owned_name
458            .try_reserve_exact(name.len())
459            .map_err(|_| ErtmpWireError::new("unable to allocate E-RTMP metadata name"))?;
460        owned_name.push_str(name);
461        self.events.push(ErtmpEvent::Metadata {
462            track,
463            chunk_stream_id,
464            timestamp,
465            name: owned_name,
466            value,
467        });
468        self.event_contexts.push(Some(context));
469        Ok(())
470    }
471}
472
473fn owned_bytes(bytes: &[u8]) -> Result<Vec<u8>, ErtmpWireError> {
474    let mut owned = Vec::new();
475    owned
476        .try_reserve_exact(bytes.len())
477        .map_err(|_| ErtmpWireError::new("unable to allocate E-RTMP event payload"))?;
478    owned.extend_from_slice(bytes);
479    Ok(owned)
480}
481
482#[derive(Clone)]
483struct ChunkHeader {
484    timestamp: u32,
485    timestamp_delta: u32,
486    message_length: usize,
487    message_type: u8,
488    message_stream_id: u32,
489    extended: bool,
490    absolute: bool,
491}
492
493struct MessageAssembly {
494    header: ChunkHeader,
495    payload: Vec<u8>,
496    received: usize,
497    retain_payload: bool,
498}
499
500struct ChunkState {
501    last_header: ChunkHeader,
502    assembly: Option<MessageAssembly>,
503}
504
505struct TrackBindings {
506    video_message_streams: [Option<u32>; 256],
507    audio_message_streams: [Option<u32>; 256],
508}
509
510impl Default for TrackBindings {
511    fn default() -> Self {
512        Self {
513            video_message_streams: [None; 256],
514            audio_message_streams: [None; 256],
515        }
516    }
517}
518
519impl TrackBindings {
520    fn associate(
521        &mut self,
522        track: ErtmpTrack,
523        message_stream_id: Option<u32>,
524    ) -> Result<(), ErtmpWireError> {
525        let Some(message_stream_id) = message_stream_id else {
526            return Ok(());
527        };
528        if message_stream_id == 0 {
529            return Err(ErtmpWireError::new(
530                "RTMP media message uses control message stream zero",
531            ));
532        }
533        let (media_type, slots) = match track.media_type {
534            ErtmpMediaType::Video => ("video", &mut self.video_message_streams),
535            ErtmpMediaType::Audio => ("audio", &mut self.audio_message_streams),
536        };
537        let slot = &mut slots[track.track_id as usize];
538        match *slot {
539            Some(existing) if existing != message_stream_id => Err(ErtmpWireError::new(format!(
540                "{media_type} track {} is used by conflicting RTMP message streams",
541                track.track_id
542            ))),
543            Some(_) => Ok(()),
544            None => {
545                *slot = Some(message_stream_id);
546                Ok(())
547            }
548        }
549    }
550}
551
552struct RtmpDecoder {
553    chunk_size: usize,
554    states: HashMap<u32, ChunkState>,
555    buffered_bytes: usize,
556    bindings: TrackBindings,
557    limits: ErtmpWireLimits,
558}
559
560impl RtmpDecoder {
561    fn new(chunk_size: usize, limits: ErtmpWireLimits) -> Self {
562        Self {
563            chunk_size,
564            states: HashMap::new(),
565            buffered_bytes: 0,
566            bindings: TrackBindings::default(),
567            limits,
568        }
569    }
570
571    fn decode(mut self, bytes: &[u8], sink: &mut EventSink) -> Result<(), ErtmpWireError> {
572        let mut offset = 0;
573        while offset < bytes.len() {
574            let (fmt, csid, basic_len) = parse_basic_header(&bytes[offset..])?;
575            offset += basic_len;
576            let previous = self
577                .states
578                .get(&csid)
579                .map(|state| state.last_header.clone());
580            let continuing = self
581                .states
582                .get(&csid)
583                .is_some_and(|state| state.assembly.is_some());
584            let (mut header, header_len) =
585                parse_message_header(fmt, previous.as_ref(), &bytes[offset..], continuing)?;
586            offset += header_len;
587
588            if header.extended {
589                if bytes.len() - offset < 4 {
590                    return Err(ErtmpWireError::new("truncated RTMP extended timestamp"));
591                }
592                let extended = be_u32(&bytes[offset..offset + 4]);
593                offset += 4;
594                match fmt {
595                    0 => {
596                        header.timestamp = extended;
597                        header.timestamp_delta = extended;
598                    }
599                    1 | 2 => {
600                        header.timestamp_delta = extended;
601                        header.timestamp = previous
602                            .as_ref()
603                            .map_or(extended, |prior| prior.timestamp.wrapping_add(extended));
604                    }
605                    3 => {
606                        let expected = if continuing && header.absolute {
607                            header.timestamp
608                        } else {
609                            header.timestamp_delta
610                        };
611                        if extended != expected {
612                            return Err(ErtmpWireError::new(
613                                "RTMP type-3 extended timestamp changed unexpectedly",
614                            ));
615                        }
616                    }
617                    _ => unreachable!("RTMP basic header format is two bits"),
618                }
619            }
620            if continuing {
621                let active = self
622                    .states
623                    .get(&csid)
624                    .and_then(|state| state.assembly.as_ref())
625                    .ok_or_else(|| ErtmpWireError::new("missing RTMP message assembly"))?;
626                if header.timestamp != active.header.timestamp
627                    || header.message_length != active.header.message_length
628                    || header.message_type != active.header.message_type
629                    || header.message_stream_id != active.header.message_stream_id
630                {
631                    return Err(ErtmpWireError::new(
632                        "RTMP message was replaced before all chunks arrived",
633                    ));
634                }
635                self.states
636                    .get_mut(&csid)
637                    .ok_or_else(|| ErtmpWireError::new("missing RTMP chunk state"))?
638                    .last_header = header.clone();
639            }
640            if header.message_length > self.limits.max_message_bytes {
641                return Err(ErtmpWireError::new(
642                    "RTMP message exceeds the message byte limit",
643                ));
644            }
645            if !matches!(
646                header.message_type,
647                SET_CHUNK_SIZE
648                    | ABORT_MESSAGE
649                    | ACKNOWLEDGEMENT
650                    | 4
651                    | WINDOW_ACKNOWLEDGEMENT_SIZE
652                    | SET_PEER_BANDWIDTH
653                    | AUDIO_MESSAGE
654                    | VIDEO_MESSAGE
655                    | 15..=20 | AGGREGATE_MESSAGE
656            ) {
657                return Err(ErtmpWireError::new(format!(
658                    "undefined RTMP message type {}",
659                    header.message_type
660                )));
661            }
662            if !self.states.contains_key(&csid) {
663                if self.states.len() >= self.limits.max_active_chunk_streams {
664                    return Err(ErtmpWireError::new(
665                        "RTMP active chunk stream count exceeds its limit",
666                    ));
667                }
668                self.states
669                    .try_reserve(1)
670                    .map_err(|_| ErtmpWireError::new("unable to allocate RTMP chunk state"))?;
671            }
672
673            if !continuing {
674                let retain_payload = matches!(
675                    header.message_type,
676                    SET_CHUNK_SIZE
677                        | ABORT_MESSAGE
678                        | ACKNOWLEDGEMENT
679                        | WINDOW_ACKNOWLEDGEMENT_SIZE
680                        | SET_PEER_BANDWIDTH
681                        | AUDIO_MESSAGE
682                        | VIDEO_MESSAGE
683                        | AGGREGATE_MESSAGE
684                );
685                let assembly = MessageAssembly {
686                    header: header.clone(),
687                    payload: Vec::new(),
688                    received: 0,
689                    retain_payload,
690                };
691                self.states.insert(
692                    csid,
693                    ChunkState {
694                        last_header: header,
695                        assembly: Some(assembly),
696                    },
697                );
698            }
699
700            let completed = {
701                let state = self
702                    .states
703                    .get_mut(&csid)
704                    .ok_or_else(|| ErtmpWireError::new("missing RTMP chunk state"))?;
705                let assembly = state
706                    .assembly
707                    .as_mut()
708                    .ok_or_else(|| ErtmpWireError::new("missing RTMP message assembly"))?;
709                let remaining = assembly.header.message_length - assembly.received;
710                let take = remaining.min(self.chunk_size);
711                if bytes.len() - offset < take {
712                    return Err(ErtmpWireError::new("truncated RTMP chunk payload"));
713                }
714                if assembly.retain_payload {
715                    let required = assembly
716                        .payload
717                        .len()
718                        .checked_add(take)
719                        .ok_or_else(|| ErtmpWireError::new("RTMP payload length overflow"))?;
720                    if required > assembly.payload.capacity() {
721                        let old_capacity = assembly.payload.capacity();
722                        let doubled = old_capacity.saturating_mul(2).max(take);
723                        let target_capacity =
724                            required.max(doubled).min(assembly.header.message_length);
725                        let requested_growth = target_capacity - old_capacity;
726                        let next_buffered = self
727                            .buffered_bytes
728                            .checked_add(requested_growth)
729                            .ok_or_else(|| {
730                                ErtmpWireError::new("RTMP buffered byte count overflow")
731                            })?;
732                        if next_buffered > self.limits.max_buffered_message_bytes {
733                            return Err(ErtmpWireError::new(
734                                "incomplete RTMP messages exceed the buffered byte limit",
735                            ));
736                        }
737                        assembly
738                            .payload
739                            .try_reserve_exact(target_capacity - assembly.payload.len())
740                            .map_err(|_| {
741                                ErtmpWireError::new("unable to allocate RTMP chunk payload")
742                            })?;
743                        let actual_growth = assembly.payload.capacity() - old_capacity;
744                        self.buffered_bytes = self
745                            .buffered_bytes
746                            .checked_add(actual_growth)
747                            .ok_or_else(|| {
748                                ErtmpWireError::new("RTMP buffered byte count overflow")
749                            })?;
750                        if self.buffered_bytes > self.limits.max_buffered_message_bytes {
751                            return Err(ErtmpWireError::new(
752                                "RTMP allocation exceeds the buffered byte limit",
753                            ));
754                        }
755                    }
756                    assembly
757                        .payload
758                        .extend_from_slice(&bytes[offset..offset + take]);
759                }
760                assembly.received += take;
761                offset += take;
762                if assembly.received == assembly.header.message_length {
763                    let completed = state
764                        .assembly
765                        .take()
766                        .ok_or_else(|| ErtmpWireError::new("missing completed RTMP message"))?;
767                    self.buffered_bytes = self
768                        .buffered_bytes
769                        .checked_sub(completed.payload.capacity())
770                        .ok_or_else(|| ErtmpWireError::new("RTMP buffered byte count underflow"))?;
771                    Some(completed)
772                } else {
773                    None
774                }
775            };
776
777            if let Some(completed) = completed {
778                if completed.retain_payload {
779                    self.message(csid, &completed.header, &completed.payload, sink)?;
780                }
781            }
782        }
783        if self.states.values().any(|state| state.assembly.is_some()) {
784            return Err(ErtmpWireError::new(
785                "RTMP connection ended during a fragmented message",
786            ));
787        }
788        Ok(())
789    }
790
791    fn message(
792        &mut self,
793        csid: u32,
794        header: &ChunkHeader,
795        payload: &[u8],
796        sink: &mut EventSink,
797    ) -> Result<(), ErtmpWireError> {
798        if matches!(
799            header.message_type,
800            SET_CHUNK_SIZE
801                | ABORT_MESSAGE
802                | ACKNOWLEDGEMENT
803                | WINDOW_ACKNOWLEDGEMENT_SIZE
804                | SET_PEER_BANDWIDTH
805        ) && (csid != PROTOCOL_CONTROL_CHUNK_STREAM_ID || header.message_stream_id != 0)
806        {
807            return Err(ErtmpWireError::new(
808                "RTMP protocol control message is not on CSID 2 and message stream 0",
809            ));
810        }
811        if matches!(
812            header.message_type,
813            AUDIO_MESSAGE | VIDEO_MESSAGE | AGGREGATE_MESSAGE
814        ) && (csid == PROTOCOL_CONTROL_CHUNK_STREAM_ID || header.message_stream_id == 0)
815        {
816            return Err(ErtmpWireError::new(
817                "RTMP media message uses a reserved control stream",
818            ));
819        }
820
821        match header.message_type {
822            SET_CHUNK_SIZE => {
823                if payload.len() != 4 || payload[0] & 0x80 != 0 {
824                    return Err(ErtmpWireError::new("invalid RTMP Set Chunk Size message"));
825                }
826                let size = be_u32(payload) as usize;
827                if size == 0 || size > self.limits.max_chunk_size {
828                    return Err(ErtmpWireError::new(
829                        "negotiated RTMP chunk size is out of bounds",
830                    ));
831                }
832                self.chunk_size = size;
833            }
834            ABORT_MESSAGE => {
835                if payload.len() != 4 {
836                    return Err(ErtmpWireError::new("invalid RTMP Abort message"));
837                }
838                let target = be_u32(payload);
839                if !(2..=65_599).contains(&target) {
840                    return Err(ErtmpWireError::new(
841                        "RTMP Abort names an invalid chunk stream id",
842                    ));
843                }
844                if let Some(state) = self.states.get_mut(&target) {
845                    if let Some(assembly) = state.assembly.take() {
846                        self.buffered_bytes = self
847                            .buffered_bytes
848                            .checked_sub(assembly.payload.capacity())
849                            .ok_or_else(|| {
850                                ErtmpWireError::new("RTMP buffered byte count underflow")
851                            })?;
852                    }
853                }
854            }
855            ACKNOWLEDGEMENT | WINDOW_ACKNOWLEDGEMENT_SIZE => {
856                if payload.len() != 4 {
857                    return Err(ErtmpWireError::new(
858                        "invalid RTMP four-byte protocol control message",
859                    ));
860                }
861            }
862            SET_PEER_BANDWIDTH => {
863                if payload.len() != 5 || payload[4] > 2 {
864                    return Err(ErtmpWireError::new(
865                        "invalid RTMP Set Peer Bandwidth message",
866                    ));
867                }
868            }
869            AUDIO_MESSAGE => parse_audio_message(
870                payload,
871                header.timestamp,
872                csid,
873                Some(header.message_stream_id),
874                &mut self.bindings,
875                sink,
876            )?,
877            VIDEO_MESSAGE => parse_video_message(
878                payload,
879                header.timestamp,
880                csid,
881                Some(header.message_stream_id),
882                &mut self.bindings,
883                sink,
884            )?,
885            AGGREGATE_MESSAGE => parse_aggregate(
886                payload,
887                header.timestamp,
888                csid,
889                header.message_stream_id,
890                &mut self.bindings,
891                sink,
892            )?,
893            _ => {}
894        }
895        Ok(())
896    }
897}
898
899fn parse_basic_header(bytes: &[u8]) -> Result<(u8, u32, usize), ErtmpWireError> {
900    let first = *bytes
901        .first()
902        .ok_or_else(|| ErtmpWireError::new("truncated RTMP basic header"))?;
903    let fmt = first >> 6;
904    match first & 0x3f {
905        0 => {
906            let low = *bytes
907                .get(1)
908                .ok_or_else(|| ErtmpWireError::new("truncated RTMP two-byte basic header"))?;
909            Ok((fmt, 64 + low as u32, 2))
910        }
911        1 => {
912            let low = *bytes
913                .get(1)
914                .ok_or_else(|| ErtmpWireError::new("truncated RTMP three-byte basic header"))?;
915            let high = *bytes
916                .get(2)
917                .ok_or_else(|| ErtmpWireError::new("truncated RTMP three-byte basic header"))?;
918            Ok((fmt, 64 + low as u32 + ((high as u32) << 8), 3))
919        }
920        csid => Ok((fmt, csid as u32, 1)),
921    }
922}
923
924fn parse_message_header(
925    fmt: u8,
926    previous: Option<&ChunkHeader>,
927    bytes: &[u8],
928    continuing: bool,
929) -> Result<(ChunkHeader, usize), ErtmpWireError> {
930    let need = [11, 7, 3, 0][fmt as usize];
931    if bytes.len() < need {
932        return Err(ErtmpWireError::new("truncated RTMP message header"));
933    }
934    let prior = || {
935        previous
936            .cloned()
937            .ok_or_else(|| ErtmpWireError::new("compressed RTMP header has no predecessor"))
938    };
939    match fmt {
940        0 => {
941            let raw = be_u24(&bytes[..3]);
942            Ok((
943                ChunkHeader {
944                    timestamp: raw,
945                    timestamp_delta: raw,
946                    message_length: be_u24(&bytes[3..6]) as usize,
947                    message_type: bytes[6],
948                    message_stream_id: u32::from_le_bytes(
949                        bytes[7..11].try_into().expect("fixed slice"),
950                    ),
951                    extended: raw == 0x00ff_ffff,
952                    absolute: true,
953                },
954                11,
955            ))
956        }
957        1 => {
958            let prior = prior()?;
959            let delta = be_u24(&bytes[..3]);
960            Ok((
961                ChunkHeader {
962                    timestamp: prior.timestamp.wrapping_add(delta),
963                    timestamp_delta: delta,
964                    message_length: be_u24(&bytes[3..6]) as usize,
965                    message_type: bytes[6],
966                    message_stream_id: prior.message_stream_id,
967                    extended: delta == 0x00ff_ffff,
968                    absolute: false,
969                },
970                7,
971            ))
972        }
973        2 => {
974            let prior = prior()?;
975            let delta = be_u24(&bytes[..3]);
976            Ok((
977                ChunkHeader {
978                    timestamp: prior.timestamp.wrapping_add(delta),
979                    timestamp_delta: delta,
980                    extended: delta == 0x00ff_ffff,
981                    absolute: false,
982                    ..prior
983                },
984                3,
985            ))
986        }
987        3 => {
988            let mut prior = prior()?;
989            if !continuing {
990                prior.timestamp = prior.timestamp.wrapping_add(prior.timestamp_delta);
991                prior.absolute = false;
992            }
993            Ok((prior, 0))
994        }
995        _ => Err(ErtmpWireError::new("invalid RTMP chunk format")),
996    }
997}
998
999fn parse_aggregate(
1000    payload: &[u8],
1001    outer_timestamp: u32,
1002    csid: u32,
1003    message_stream_id: u32,
1004    bindings: &mut TrackBindings,
1005    sink: &mut EventSink,
1006) -> Result<(), ErtmpWireError> {
1007    let mut offset = 0;
1008    let mut first_timestamp = None;
1009    while offset < payload.len() {
1010        if payload.len() - offset < 15 {
1011            return Err(ErtmpWireError::new("truncated RTMP Aggregate tag"));
1012        }
1013        let tag_type = payload[offset];
1014        let size = be_u24(&payload[offset + 1..offset + 4]) as usize;
1015        if size > sink.limits.max_message_bytes {
1016            return Err(ErtmpWireError::new(
1017                "RTMP Aggregate tag exceeds the message limit",
1018            ));
1019        }
1020        let end = offset
1021            .checked_add(11)
1022            .and_then(|value| value.checked_add(size))
1023            .and_then(|value| value.checked_add(4))
1024            .filter(|end| *end <= payload.len())
1025            .ok_or_else(|| ErtmpWireError::new("truncated RTMP Aggregate body"))?;
1026        if be_u32(&payload[offset + 11 + size..end]) as usize != 11 + size {
1027            return Err(ErtmpWireError::new("malformed RTMP Aggregate tag"));
1028        }
1029        let timestamp =
1030            be_u24(&payload[offset + 4..offset + 7]) | ((payload[offset + 7] as u32) << 24);
1031        let base = *first_timestamp.get_or_insert(timestamp);
1032        let normalized = outer_timestamp.wrapping_add(timestamp.wrapping_sub(base));
1033        let body = &payload[offset + 11..offset + 11 + size];
1034        match tag_type {
1035            AUDIO_MESSAGE => parse_audio_message(
1036                body,
1037                normalized,
1038                csid,
1039                Some(message_stream_id),
1040                bindings,
1041                sink,
1042            )?,
1043            VIDEO_MESSAGE => parse_video_message(
1044                body,
1045                normalized,
1046                csid,
1047                Some(message_stream_id),
1048                bindings,
1049                sink,
1050            )?,
1051            AMF3_SCRIPT_DATA | AMF0_SCRIPT_DATA => {}
1052            other => {
1053                return Err(ErtmpWireError::new(format!(
1054                    "unsupported Aggregate tag type {other}"
1055                )))
1056            }
1057        }
1058        offset = end;
1059    }
1060    Ok(())
1061}
1062
1063fn parse_video_message(
1064    payload: &[u8],
1065    timestamp: u32,
1066    csid: u32,
1067    message_stream_id: Option<u32>,
1068    bindings: &mut TrackBindings,
1069    sink: &mut EventSink,
1070) -> Result<(), ErtmpWireError> {
1071    let first = *payload
1072        .first()
1073        .ok_or_else(|| ErtmpWireError::new("empty RTMP video message"))?;
1074    let frame_type = (first >> 4) & 0x07;
1075    if first & 0x80 == 0 {
1076        if frame_type == 5 {
1077            return parse_video_command(&payload[1..]);
1078        }
1079        if !(1..=2).contains(&frame_type) {
1080            return Err(ErtmpWireError::new("invalid legacy video frame type"));
1081        }
1082        let codec = first & 0x0f;
1083        if codec != 7 {
1084            return Err(ErtmpWireError::new(format!(
1085                "unsupported legacy video codec id {codec}"
1086            )));
1087        }
1088        if payload.len() < 5 {
1089            return Err(ErtmpWireError::new("truncated legacy AVC packet"));
1090        }
1091        let packet_type = payload[1];
1092        if packet_type > 2 {
1093            return Err(ErtmpWireError::new(format!(
1094                "invalid legacy AVC packet type {packet_type}"
1095            )));
1096        }
1097        let track = video_track(0);
1098        bindings.associate(track, message_stream_id)?;
1099        return emit_video_packet(
1100            packet_type,
1101            VideoCodec::Avc,
1102            &payload[5..],
1103            timestamp,
1104            signed_u24(&payload[2..5]),
1105            csid,
1106            track,
1107            message_stream_id,
1108            false,
1109            sink,
1110        );
1111    }
1112
1113    let mut offset = 1;
1114    let mut packet_type = first & 0x0f;
1115    while packet_type == 7 {
1116        let length_byte = *payload
1117            .get(offset)
1118            .ok_or_else(|| ErtmpWireError::new("truncated video ModEx length"))?;
1119        offset += 1;
1120        let length = if length_byte == 0xff {
1121            if payload.len() - offset < 2 {
1122                return Err(ErtmpWireError::new("truncated video ModEx extended length"));
1123            }
1124            let value = u16::from_be_bytes([payload[offset], payload[offset + 1]]) as usize + 1;
1125            offset += 2;
1126            value
1127        } else {
1128            length_byte as usize + 1
1129        };
1130        let mod_ex_end = offset
1131            .checked_add(length)
1132            .filter(|end| *end < payload.len())
1133            .ok_or_else(|| ErtmpWireError::new("video ModEx data exceeds its message"))?;
1134        if length < 3
1135            || be_u24(&payload[offset..offset + 3]) >= 1_000_000
1136            || payload[mod_ex_end] >> 4 != 0
1137        {
1138            return Err(ErtmpWireError::new(
1139                "invalid video TimestampOffsetNano ModEx",
1140            ));
1141        }
1142        packet_type = payload[mod_ex_end] & 0x0f;
1143        offset = mod_ex_end + 1;
1144    }
1145
1146    if packet_type == VIDEO_MULTITRACK {
1147        return parse_multitrack_video(
1148            &payload[offset..],
1149            frame_type,
1150            timestamp,
1151            csid,
1152            message_stream_id,
1153            bindings,
1154            sink,
1155        );
1156    }
1157    if packet_type != VIDEO_METADATA {
1158        if frame_type == 5 {
1159            return parse_video_command(&payload[offset..]);
1160        }
1161        if !(1..=2).contains(&frame_type) {
1162            return Err(ErtmpWireError::new("invalid enhanced video frame type"));
1163        }
1164    }
1165    if matches!(packet_type, 7..=15) {
1166        return Err(ErtmpWireError::new(format!(
1167            "unsupported enhanced video packet type {packet_type}"
1168        )));
1169    }
1170    if payload.len() - offset < 4 {
1171        return Err(ErtmpWireError::new(
1172            "enhanced video packet is missing FOURCC",
1173        ));
1174    }
1175    let codec = video_codec(payload[offset..offset + 4].try_into().expect("fixed slice"))?;
1176    offset += 4;
1177    let (cto, body) = split_video_body(packet_type, codec, &payload[offset..])?;
1178    let track = video_track(0);
1179    bindings.associate(track, message_stream_id)?;
1180    emit_video_packet(
1181        packet_type,
1182        codec,
1183        body,
1184        timestamp,
1185        cto,
1186        csid,
1187        track,
1188        message_stream_id,
1189        false,
1190        sink,
1191    )
1192}
1193
1194fn parse_video_command(bytes: &[u8]) -> Result<(), ErtmpWireError> {
1195    if bytes.len() != 1 || bytes[0] > 1 {
1196        return Err(ErtmpWireError::new("invalid video command frame"));
1197    }
1198    Ok(())
1199}
1200
1201fn parse_multitrack_video(
1202    payload: &[u8],
1203    frame_type: u8,
1204    timestamp: u32,
1205    csid: u32,
1206    message_stream_id: Option<u32>,
1207    bindings: &mut TrackBindings,
1208    sink: &mut EventSink,
1209) -> Result<(), ErtmpWireError> {
1210    let header = *payload
1211        .first()
1212        .ok_or_else(|| ErtmpWireError::new("truncated video Multitrack header"))?;
1213    let mode = header >> 4;
1214    let packet_type = header & 0x0f;
1215    if mode > 2 || packet_type == VIDEO_MULTITRACK || matches!(packet_type, 7..=15) {
1216        return Err(ErtmpWireError::new(
1217            "unsupported video Multitrack mode or inner packet type",
1218        ));
1219    }
1220    if packet_type != VIDEO_METADATA && !(1..=2).contains(&frame_type) {
1221        return Err(ErtmpWireError::new("invalid enhanced video frame type"));
1222    }
1223    let mut offset = 1;
1224    let shared_codec = if mode != 2 {
1225        if payload.len() - offset < 4 {
1226            return Err(ErtmpWireError::new(
1227                "video Multitrack packet is missing shared FOURCC",
1228            ));
1229        }
1230        let codec = video_codec(payload[offset..offset + 4].try_into().expect("fixed slice"))?;
1231        offset += 4;
1232        Some(codec)
1233    } else {
1234        None
1235    };
1236    let mut seen = [false; 256];
1237    while offset < payload.len() {
1238        let codec = if mode == 2 {
1239            if payload.len() - offset < 4 {
1240                return Err(ErtmpWireError::new(
1241                    "video Multitrack track is missing FOURCC",
1242                ));
1243            }
1244            let codec = video_codec(payload[offset..offset + 4].try_into().expect("fixed slice"))?;
1245            offset += 4;
1246            codec
1247        } else {
1248            shared_codec.ok_or_else(|| ErtmpWireError::new("missing shared video FOURCC"))?
1249        };
1250        let track_id = *payload
1251            .get(offset)
1252            .ok_or_else(|| ErtmpWireError::new("video Multitrack track is missing track id"))?;
1253        offset += 1;
1254        if seen[track_id as usize] {
1255            return Err(ErtmpWireError::new(
1256                "video Multitrack message repeats a track id",
1257            ));
1258        }
1259        seen[track_id as usize] = true;
1260        let size = if mode == 0 {
1261            payload.len() - offset
1262        } else {
1263            if payload.len() - offset < 3 {
1264                return Err(ErtmpWireError::new(
1265                    "video Multitrack track is missing payload size",
1266                ));
1267            }
1268            let value = be_u24(&payload[offset..offset + 3]) as usize;
1269            offset += 3;
1270            value
1271        };
1272        let end = offset
1273            .checked_add(size)
1274            .filter(|end| *end <= payload.len())
1275            .ok_or_else(|| ErtmpWireError::new("video Multitrack payload exceeds its message"))?;
1276        let (cto, body) = split_video_body(packet_type, codec, &payload[offset..end])?;
1277        let track = video_track(track_id);
1278        bindings.associate(track, message_stream_id)?;
1279        emit_video_packet(
1280            packet_type,
1281            codec,
1282            body,
1283            timestamp,
1284            cto,
1285            csid,
1286            track,
1287            message_stream_id,
1288            true,
1289            sink,
1290        )?;
1291        offset = end;
1292    }
1293    if !seen.into_iter().any(|value| value) {
1294        return Err(ErtmpWireError::new(
1295            "video Multitrack message has no tracks",
1296        ));
1297    }
1298    Ok(())
1299}
1300
1301#[derive(Clone, Copy, PartialEq, Eq)]
1302enum VideoCodec {
1303    Vp8,
1304    Vp9,
1305    Av1,
1306    Avc,
1307    Hevc,
1308    Vvc,
1309}
1310
1311impl VideoCodec {
1312    fn fourcc(self) -> [u8; 4] {
1313        match self {
1314            Self::Vp8 => *b"vp08",
1315            Self::Vp9 => *b"vp09",
1316            Self::Av1 => *b"av01",
1317            Self::Avc => *b"avc1",
1318            Self::Hevc => *b"hvc1",
1319            Self::Vvc => *b"vvc1",
1320        }
1321    }
1322}
1323
1324fn video_codec(fourcc: [u8; 4]) -> Result<VideoCodec, ErtmpWireError> {
1325    match &fourcc {
1326        b"vp08" => Ok(VideoCodec::Vp8),
1327        b"vp09" => Ok(VideoCodec::Vp9),
1328        b"av01" => Ok(VideoCodec::Av1),
1329        b"avc1" => Ok(VideoCodec::Avc),
1330        b"hvc1" => Ok(VideoCodec::Hevc),
1331        b"vvc1" => Ok(VideoCodec::Vvc),
1332        _ => Err(ErtmpWireError::new(format!(
1333            "unsupported enhanced video FOURCC {:?}",
1334            String::from_utf8_lossy(&fourcc)
1335        ))),
1336    }
1337}
1338
1339fn split_video_body(
1340    packet_type: u8,
1341    codec: VideoCodec,
1342    bytes: &[u8],
1343) -> Result<(i32, &[u8]), ErtmpWireError> {
1344    if packet_type == 1 && matches!(codec, VideoCodec::Avc | VideoCodec::Hevc | VideoCodec::Vvc) {
1345        if bytes.len() < 3 {
1346            return Err(ErtmpWireError::new(
1347                "coded video packet is missing composition offset",
1348            ));
1349        }
1350        Ok((signed_u24(&bytes[..3]), &bytes[3..]))
1351    } else {
1352        Ok((0, bytes))
1353    }
1354}
1355
1356#[allow(clippy::too_many_arguments)] // Wire header fields remain explicit at the decoding boundary.
1357fn emit_video_packet(
1358    packet_type: u8,
1359    codec: VideoCodec,
1360    body: &[u8],
1361    timestamp: u32,
1362    cto: i32,
1363    csid: u32,
1364    track: ErtmpTrack,
1365    message_stream_id: Option<u32>,
1366    multitrack: bool,
1367    sink: &mut EventSink,
1368) -> Result<(), ErtmpWireError> {
1369    let context = ErtmpWireEventContext {
1370        message_stream_id,
1371        codec_fourcc: Some(codec.fourcc()),
1372        multitrack,
1373    };
1374    match packet_type {
1375        0 => {
1376            if body.is_empty() {
1377                return Err(ErtmpWireError::new("empty video codec configuration"));
1378            }
1379            sink.push_codec_config(track, csid, body, context)
1380        }
1381        1 => {
1382            if body.is_empty() {
1383                return Err(ErtmpWireError::new("empty coded video frame"));
1384            }
1385            sink.push_frame(track, csid, timestamp, cto, body, context)
1386        }
1387        2 => {
1388            if !body.is_empty() {
1389                return Err(ErtmpWireError::new(
1390                    "video SequenceEnd carries unexpected data",
1391                ));
1392            }
1393            Ok(())
1394        }
1395        3 => {
1396            if !matches!(codec, VideoCodec::Avc | VideoCodec::Hevc | VideoCodec::Vvc) {
1397                return Err(ErtmpWireError::new(
1398                    "CodedFramesX is invalid for this video codec",
1399                ));
1400            }
1401            if body.is_empty() {
1402                return Err(ErtmpWireError::new("empty coded video frame"));
1403            }
1404            sink.push_frame(track, csid, timestamp, 0, body, context)
1405        }
1406        VIDEO_METADATA => {
1407            let remaining_events = sink.remaining_events();
1408            let remaining_value_bytes = sink.remaining_payload_bytes();
1409            amf3::for_each_metadata_pair(
1410                body,
1411                remaining_events,
1412                remaining_value_bytes,
1413                |name, value| sink.push_metadata(track, csid, timestamp, name, value, context),
1414            )
1415        }
1416        5 => {
1417            if codec != VideoCodec::Av1 || body.is_empty() {
1418                return Err(ErtmpWireError::new(
1419                    "MPEG2TSSequenceStart requires a nonempty AV1 descriptor",
1420                ));
1421            }
1422            sink.push_codec_config(track, csid, body, context)
1423        }
1424        other => Err(ErtmpWireError::new(format!(
1425            "unsupported video packet type {other}"
1426        ))),
1427    }
1428}
1429
1430fn parse_audio_message(
1431    payload: &[u8],
1432    timestamp: u32,
1433    csid: u32,
1434    message_stream_id: Option<u32>,
1435    bindings: &mut TrackBindings,
1436    sink: &mut EventSink,
1437) -> Result<(), ErtmpWireError> {
1438    if payload.is_empty() {
1439        return Ok(()); // RTMP silence message
1440    }
1441    let first = payload[0];
1442    let format = first >> 4;
1443    if format != 9 {
1444        let track = audio_track(0);
1445        bindings.associate(track, message_stream_id)?;
1446        if format == 10 {
1447            if payload.len() < 2 {
1448                return Err(ErtmpWireError::new("truncated legacy AAC packet"));
1449            }
1450            let packet_type = payload[1];
1451            if packet_type > 1 {
1452                return Err(ErtmpWireError::new(format!(
1453                    "invalid legacy AAC packet type {packet_type}"
1454                )));
1455            }
1456            return emit_audio_packet(
1457                packet_type,
1458                AudioCodec::Aac,
1459                &payload[2..],
1460                timestamp,
1461                csid,
1462                track,
1463                message_stream_id,
1464                false,
1465                sink,
1466            );
1467        }
1468        if matches!(format, 7 | 8 | 9 | 12 | 13 | 14 | 15) {
1469            return Err(ErtmpWireError::new(format!(
1470                "unsupported legacy audio format {format}"
1471            )));
1472        }
1473        if payload.len() == 1 {
1474            return Err(ErtmpWireError::new("legacy audio packet has no coded data"));
1475        }
1476        return sink.push_frame(
1477            track,
1478            csid,
1479            timestamp,
1480            0,
1481            &payload[1..],
1482            ErtmpWireEventContext {
1483                message_stream_id,
1484                codec_fourcc: None,
1485                multitrack: false,
1486            },
1487        );
1488    }
1489
1490    let mut offset = 1;
1491    let mut packet_type = first & 0x0f;
1492    while packet_type == 7 {
1493        let length_byte = *payload
1494            .get(offset)
1495            .ok_or_else(|| ErtmpWireError::new("truncated audio ModEx length"))?;
1496        offset += 1;
1497        let length = if length_byte == 0xff {
1498            if payload.len() - offset < 2 {
1499                return Err(ErtmpWireError::new("truncated audio ModEx extended length"));
1500            }
1501            let value = u16::from_be_bytes([payload[offset], payload[offset + 1]]) as usize + 1;
1502            offset += 2;
1503            value
1504        } else {
1505            length_byte as usize + 1
1506        };
1507        let mod_ex_end = offset
1508            .checked_add(length)
1509            .filter(|end| *end < payload.len())
1510            .ok_or_else(|| ErtmpWireError::new("audio ModEx data exceeds its message"))?;
1511        if length < 3
1512            || be_u24(&payload[offset..offset + 3]) >= 1_000_000
1513            || payload[mod_ex_end] >> 4 != 0
1514        {
1515            return Err(ErtmpWireError::new(
1516                "invalid audio TimestampOffsetNano ModEx",
1517            ));
1518        }
1519        packet_type = payload[mod_ex_end] & 0x0f;
1520        offset = mod_ex_end + 1;
1521    }
1522    if packet_type == AUDIO_MULTITRACK {
1523        return parse_multitrack_audio(
1524            &payload[offset..],
1525            timestamp,
1526            csid,
1527            message_stream_id,
1528            bindings,
1529            sink,
1530        );
1531    }
1532    if matches!(packet_type, 3 | 6 | 7 | 8..=15) {
1533        return Err(ErtmpWireError::new(format!(
1534            "unsupported or reserved audio packet type {packet_type}"
1535        )));
1536    }
1537    if payload.len() - offset < 4 {
1538        return Err(ErtmpWireError::new(
1539            "enhanced audio packet is missing FOURCC",
1540        ));
1541    }
1542    let codec = audio_codec(payload[offset..offset + 4].try_into().expect("fixed slice"))?;
1543    offset += 4;
1544    let track = audio_track(0);
1545    bindings.associate(track, message_stream_id)?;
1546    emit_audio_packet(
1547        packet_type,
1548        codec,
1549        &payload[offset..],
1550        timestamp,
1551        csid,
1552        track,
1553        message_stream_id,
1554        false,
1555        sink,
1556    )
1557}
1558
1559fn parse_multitrack_audio(
1560    payload: &[u8],
1561    timestamp: u32,
1562    csid: u32,
1563    message_stream_id: Option<u32>,
1564    bindings: &mut TrackBindings,
1565    sink: &mut EventSink,
1566) -> Result<(), ErtmpWireError> {
1567    let header = *payload
1568        .first()
1569        .ok_or_else(|| ErtmpWireError::new("truncated audio Multitrack header"))?;
1570    let mode = header >> 4;
1571    let packet_type = header & 0x0f;
1572    if mode > 2 || packet_type == AUDIO_MULTITRACK || matches!(packet_type, 3 | 6 | 7 | 8..=15) {
1573        return Err(ErtmpWireError::new(
1574            "unsupported audio Multitrack mode or inner packet type",
1575        ));
1576    }
1577    let mut offset = 1;
1578    let shared_codec = if mode != 2 {
1579        if payload.len() - offset < 4 {
1580            return Err(ErtmpWireError::new(
1581                "audio Multitrack packet is missing shared FOURCC",
1582            ));
1583        }
1584        let codec = audio_codec(payload[offset..offset + 4].try_into().expect("fixed slice"))?;
1585        offset += 4;
1586        Some(codec)
1587    } else {
1588        None
1589    };
1590    let mut seen = [false; 256];
1591    while offset < payload.len() {
1592        let codec = if mode == 2 {
1593            if payload.len() - offset < 4 {
1594                return Err(ErtmpWireError::new(
1595                    "audio Multitrack track is missing FOURCC",
1596                ));
1597            }
1598            let codec = audio_codec(payload[offset..offset + 4].try_into().expect("fixed slice"))?;
1599            offset += 4;
1600            codec
1601        } else {
1602            shared_codec.ok_or_else(|| ErtmpWireError::new("missing shared audio FOURCC"))?
1603        };
1604        let track_id = *payload
1605            .get(offset)
1606            .ok_or_else(|| ErtmpWireError::new("audio Multitrack track is missing track id"))?;
1607        offset += 1;
1608        if seen[track_id as usize] {
1609            return Err(ErtmpWireError::new(
1610                "audio Multitrack message repeats a track id",
1611            ));
1612        }
1613        seen[track_id as usize] = true;
1614        let size = if mode == 0 {
1615            payload.len() - offset
1616        } else {
1617            if payload.len() - offset < 3 {
1618                return Err(ErtmpWireError::new(
1619                    "audio Multitrack track is missing payload size",
1620                ));
1621            }
1622            let value = be_u24(&payload[offset..offset + 3]) as usize;
1623            offset += 3;
1624            value
1625        };
1626        let end = offset
1627            .checked_add(size)
1628            .filter(|end| *end <= payload.len())
1629            .ok_or_else(|| ErtmpWireError::new("audio Multitrack payload exceeds its message"))?;
1630        let track = audio_track(track_id);
1631        bindings.associate(track, message_stream_id)?;
1632        emit_audio_packet(
1633            packet_type,
1634            codec,
1635            &payload[offset..end],
1636            timestamp,
1637            csid,
1638            track,
1639            message_stream_id,
1640            true,
1641            sink,
1642        )?;
1643        offset = end;
1644    }
1645    if !seen.into_iter().any(|value| value) {
1646        return Err(ErtmpWireError::new(
1647            "audio Multitrack message has no tracks",
1648        ));
1649    }
1650    Ok(())
1651}
1652
1653#[derive(Clone, Copy, PartialEq, Eq)]
1654enum AudioCodec {
1655    Ac3,
1656    Eac3,
1657    Opus,
1658    Mp3,
1659    Flac,
1660    Aac,
1661}
1662
1663impl AudioCodec {
1664    fn fourcc(self) -> [u8; 4] {
1665        match self {
1666            Self::Ac3 => *b"ac-3",
1667            Self::Eac3 => *b"ec-3",
1668            Self::Opus => *b"Opus",
1669            Self::Mp3 => *b".mp3",
1670            Self::Flac => *b"fLaC",
1671            Self::Aac => *b"mp4a",
1672        }
1673    }
1674}
1675
1676fn audio_codec(fourcc: [u8; 4]) -> Result<AudioCodec, ErtmpWireError> {
1677    match &fourcc {
1678        b"ac-3" => Ok(AudioCodec::Ac3),
1679        b"ec-3" => Ok(AudioCodec::Eac3),
1680        b"Opus" => Ok(AudioCodec::Opus),
1681        b".mp3" => Ok(AudioCodec::Mp3),
1682        b"fLaC" => Ok(AudioCodec::Flac),
1683        b"mp4a" => Ok(AudioCodec::Aac),
1684        _ => Err(ErtmpWireError::new(format!(
1685            "unsupported enhanced audio FOURCC {:?}",
1686            String::from_utf8_lossy(&fourcc)
1687        ))),
1688    }
1689}
1690
1691#[allow(clippy::too_many_arguments)] // Keep decoded media identity explicit at dispatch.
1692fn emit_audio_packet(
1693    packet_type: u8,
1694    codec: AudioCodec,
1695    body: &[u8],
1696    timestamp: u32,
1697    csid: u32,
1698    track: ErtmpTrack,
1699    message_stream_id: Option<u32>,
1700    multitrack: bool,
1701    sink: &mut EventSink,
1702) -> Result<(), ErtmpWireError> {
1703    let context = ErtmpWireEventContext {
1704        message_stream_id,
1705        codec_fourcc: Some(codec.fourcc()),
1706        multitrack,
1707    };
1708    match packet_type {
1709        0 => {
1710            if matches!(codec, AudioCodec::Ac3 | AudioCodec::Eac3 | AudioCodec::Mp3) {
1711                return Err(ErtmpWireError::new(
1712                    "SequenceStart is undefined for this enhanced audio codec",
1713                ));
1714            }
1715            if body.is_empty() && codec != AudioCodec::Opus {
1716                return Err(ErtmpWireError::new("empty audio codec configuration"));
1717            }
1718            sink.push_codec_config(track, csid, body, context)
1719        }
1720        1 => {
1721            if body.is_empty() {
1722                return Err(ErtmpWireError::new("empty coded audio frame"));
1723            }
1724            sink.push_frame(track, csid, timestamp, 0, body, context)
1725        }
1726        2 => {
1727            if !body.is_empty() {
1728                return Err(ErtmpWireError::new(
1729                    "audio SequenceEnd carries unexpected data",
1730                ));
1731            }
1732            Ok(())
1733        }
1734        4 => validate_multichannel_config(body),
1735        // There is intentionally no audio Metadata arm. Values 3 and 6 are
1736        // reserved by Enhanced RTMP v2 and must not be appropriated for C2PA.
1737        other => Err(ErtmpWireError::new(format!(
1738            "unsupported audio packet type {other}"
1739        ))),
1740    }
1741}
1742
1743fn validate_multichannel_config(body: &[u8]) -> Result<(), ErtmpWireError> {
1744    if body.len() < 2 {
1745        return Err(ErtmpWireError::new(
1746            "truncated enhanced audio multichannel configuration",
1747        ));
1748    }
1749    let channel_order = body[0];
1750    let channel_count = body[1] as usize;
1751    let expected = match channel_order {
1752        0 => 2,
1753        1 => 6,
1754        2 => 2usize
1755            .checked_add(channel_count)
1756            .ok_or_else(|| ErtmpWireError::new("audio channel count overflow"))?,
1757        _ => return Err(ErtmpWireError::new("reserved enhanced audio channel order")),
1758    };
1759    if body.len() != expected {
1760        return Err(ErtmpWireError::new(
1761            "enhanced audio multichannel configuration has the wrong length",
1762        ));
1763    }
1764    Ok(())
1765}
1766
1767fn video_track(track_id: u8) -> ErtmpTrack {
1768    ErtmpTrack {
1769        media_type: ErtmpMediaType::Video,
1770        track_id,
1771    }
1772}
1773
1774fn audio_track(track_id: u8) -> ErtmpTrack {
1775    ErtmpTrack {
1776        media_type: ErtmpMediaType::Audio,
1777        track_id,
1778    }
1779}
1780
1781fn be_u24(bytes: &[u8]) -> u32 {
1782    ((bytes[0] as u32) << 16) | ((bytes[1] as u32) << 8) | bytes[2] as u32
1783}
1784
1785fn be_u32(bytes: &[u8]) -> u32 {
1786    u32::from_be_bytes(bytes[..4].try_into().expect("fixed slice"))
1787}
1788
1789fn signed_u24(bytes: &[u8]) -> i32 {
1790    let raw = be_u24(bytes) as i32;
1791    if raw & 0x0080_0000 != 0 {
1792        raw | !0x00ff_ffff
1793    } else {
1794        raw
1795    }
1796}
1797
1798#[cfg(test)]
1799mod tests {
1800    use super::*;
1801
1802    fn context() -> ErtmpConnectionContext {
1803        ErtmpConnectionContext {
1804            client_caps_ex: 0x10,
1805            server_caps_ex: 0x10,
1806            object_encoding: 3,
1807        }
1808    }
1809
1810    fn options(_chunk_size: usize) -> ErtmpWireLimits {
1811        ErtmpWireLimits::default()
1812    }
1813
1814    fn trace(bytes: &[u8]) -> RtmpWireConnection<'_> {
1815        chunked_trace(bytes, 128)
1816    }
1817
1818    fn chunked_trace(bytes: &[u8], initial_chunk_size: usize) -> RtmpWireConnection<'_> {
1819        RtmpWireConnection {
1820            bytes,
1821            connection: context(),
1822            initial_chunk_size,
1823        }
1824    }
1825
1826    fn put_u24(value: u32, out: &mut Vec<u8>) {
1827        out.extend_from_slice(&[
1828            ((value >> 16) & 0xff) as u8,
1829            ((value >> 8) & 0xff) as u8,
1830            (value & 0xff) as u8,
1831        ]);
1832    }
1833
1834    fn put_basic_header(fmt: u8, csid: u32, out: &mut Vec<u8>) {
1835        match csid {
1836            2..=63 => out.push((fmt << 6) | csid as u8),
1837            64..=319 => {
1838                out.push(fmt << 6);
1839                out.push((csid - 64) as u8);
1840            }
1841            320..=65_599 => {
1842                let encoded = csid - 64;
1843                out.push((fmt << 6) | 1);
1844                out.push(encoded as u8);
1845                out.push((encoded >> 8) as u8);
1846            }
1847            _ => panic!("invalid test csid"),
1848        }
1849    }
1850
1851    fn rtmp_type0(
1852        csid: u32,
1853        stream_id: u32,
1854        message_type: u8,
1855        timestamp: u32,
1856        payload: &[u8],
1857        chunk_size: usize,
1858    ) -> Vec<u8> {
1859        let mut out = Vec::new();
1860        put_basic_header(0, csid, &mut out);
1861        let extended = timestamp >= 0x00ff_ffff;
1862        put_u24(if extended { 0x00ff_ffff } else { timestamp }, &mut out);
1863        put_u24(payload.len() as u32, &mut out);
1864        out.push(message_type);
1865        out.extend_from_slice(&stream_id.to_le_bytes());
1866        if extended {
1867            out.extend_from_slice(&timestamp.to_be_bytes());
1868        }
1869        let mut offset = 0;
1870        while offset < payload.len() {
1871            let take = chunk_size.min(payload.len() - offset);
1872            out.extend_from_slice(&payload[offset..offset + take]);
1873            offset += take;
1874            if offset < payload.len() {
1875                put_basic_header(3, csid, &mut out);
1876                if extended {
1877                    out.extend_from_slice(&timestamp.to_be_bytes());
1878                }
1879            }
1880        }
1881        out
1882    }
1883
1884    fn rtmp_type0_prefix(
1885        csid: u32,
1886        stream_id: u32,
1887        message_type: u8,
1888        timestamp: u32,
1889        payload: &[u8],
1890        prefix_len: usize,
1891    ) -> Vec<u8> {
1892        assert!(timestamp < 0x00ff_ffff);
1893        assert!(prefix_len < payload.len());
1894        let mut out = Vec::new();
1895        put_basic_header(0, csid, &mut out);
1896        put_u24(timestamp, &mut out);
1897        put_u24(payload.len() as u32, &mut out);
1898        out.push(message_type);
1899        out.extend_from_slice(&stream_id.to_le_bytes());
1900        out.extend_from_slice(&payload[..prefix_len]);
1901        out
1902    }
1903
1904    fn flv_tag(tag_type: u8, timestamp: u32, payload: &[u8]) -> Vec<u8> {
1905        let mut out = vec![tag_type];
1906        put_u24(payload.len() as u32, &mut out);
1907        put_u24(timestamp & 0x00ff_ffff, &mut out);
1908        out.push((timestamp >> 24) as u8);
1909        out.extend_from_slice(&[0, 0, 0]);
1910        out.extend_from_slice(payload);
1911        out.extend_from_slice(&(11u32 + payload.len() as u32).to_be_bytes());
1912        out
1913    }
1914
1915    fn flv(flags: u8, tags: &[Vec<u8>]) -> Vec<u8> {
1916        let mut out = b"FLV\x01".to_vec();
1917        out.push(flags);
1918        out.extend_from_slice(&9u32.to_be_bytes());
1919        out.extend_from_slice(&0u32.to_be_bytes());
1920        for tag in tags {
1921            out.extend_from_slice(tag);
1922        }
1923        out
1924    }
1925
1926    fn amf_pair(name: &str, marker: u8, value: &[u8]) -> Vec<u8> {
1927        let mut out = vec![0x06, ((name.len() * 2 + 1) as u8)];
1928        out.extend_from_slice(name.as_bytes());
1929        out.push(marker);
1930        if marker == 0x0c {
1931            out.push((value.len() * 2 + 1) as u8);
1932        }
1933        out.extend_from_slice(value);
1934        out
1935    }
1936
1937    fn video_metadata(pairs: &[u8]) -> Vec<u8> {
1938        let mut out = vec![0x94];
1939        out.extend_from_slice(b"avc1");
1940        out.extend_from_slice(pairs);
1941        out
1942    }
1943
1944    fn frame_timestamps(input: &ErtmpWireInput) -> Vec<u32> {
1945        input
1946            .input
1947            .events
1948            .iter()
1949            .filter_map(|event| match event {
1950                ErtmpEvent::Frame { timestamp, .. } => Some(*timestamp),
1951                _ => None,
1952            })
1953            .collect()
1954    }
1955
1956    #[test]
1957    fn completed_messages_on_one_csid_are_not_mistaken_for_fragments() {
1958        let config = [0x90, b'a', b'v', b'c', b'1', 1];
1959        let frame = [0x91, b'a', b'v', b'c', b'1', 0, 0, 2, 4, 5, 6];
1960        let mut metadata_pairs = amf_pair("colorInfo", 0x00, &[]);
1961        metadata_pairs.extend(amf_pair("c2paManifest", 0x0c, b"store"));
1962        let metadata = video_metadata(&metadata_pairs);
1963        let mut wire = rtmp_type0(319, 7, VIDEO_MESSAGE, 10, &config, 3);
1964        wire.extend(rtmp_type0(319, 7, VIDEO_MESSAGE, 11, &frame, 3));
1965        wire.extend(rtmp_type0(319, 7, VIDEO_MESSAGE, 11, &metadata, 3));
1966        let input =
1967            ertmp_input_from_rtmp_connections(&[chunked_trace(&wire, 3)], options(3)).unwrap();
1968        assert_eq!(input.input.events.len(), 3);
1969        match &input.input.events[1] {
1970            ErtmpEvent::Frame {
1971                chunk_stream_id,
1972                timestamp,
1973                composition_time_offset,
1974                coded_data,
1975                ..
1976            } => {
1977                assert_eq!(
1978                    (*chunk_stream_id, *timestamp, *composition_time_offset),
1979                    (319, 11, 2)
1980                );
1981                assert_eq!(coded_data, &[4, 5, 6]);
1982            }
1983            _ => panic!("expected frame"),
1984        }
1985    }
1986
1987    #[test]
1988    fn compressed_headers_inherit_deltas_and_extended_timestamps() {
1989        let payload = [0x2f, 0xaa];
1990        let mut wire = rtmp_type0(3, 1, AUDIO_MESSAGE, 10, &payload, 128);
1991        put_basic_header(3, 3, &mut wire);
1992        wire.extend_from_slice(&payload);
1993        put_basic_header(1, 3, &mut wire);
1994        put_u24(5, &mut wire);
1995        put_u24(payload.len() as u32, &mut wire);
1996        wire.push(AUDIO_MESSAGE);
1997        wire.extend_from_slice(&payload);
1998        put_basic_header(2, 3, &mut wire);
1999        put_u24(7, &mut wire);
2000        wire.extend_from_slice(&payload);
2001        assert_eq!(
2002            frame_timestamps(
2003                &ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap()
2004            ),
2005            [10, 20, 25, 32]
2006        );
2007
2008        let extended = 0x0100_0001;
2009        let mut extended_wire = rtmp_type0(4, 1, AUDIO_MESSAGE, extended, &payload, 128);
2010        put_basic_header(3, 4, &mut extended_wire);
2011        extended_wire.extend_from_slice(&extended.to_be_bytes());
2012        extended_wire.extend_from_slice(&payload);
2013        assert_eq!(
2014            frame_timestamps(
2015                &ertmp_input_from_rtmp_connections(&[trace(&extended_wire)], options(128)).unwrap()
2016            ),
2017            [extended, extended.wrapping_mul(2)]
2018        );
2019    }
2020
2021    #[test]
2022    fn type1_and_type2_extended_timestamp_deltas_are_applied() {
2023        let payload = [0x2f, 0xaa];
2024        let delta = 0x0100_0000u32;
2025        let mut wire = rtmp_type0(3, 1, AUDIO_MESSAGE, 1, &payload, 128);
2026        put_basic_header(1, 3, &mut wire);
2027        put_u24(0x00ff_ffff, &mut wire);
2028        put_u24(payload.len() as u32, &mut wire);
2029        wire.push(AUDIO_MESSAGE);
2030        wire.extend_from_slice(&delta.to_be_bytes());
2031        wire.extend_from_slice(&payload);
2032        put_basic_header(2, 3, &mut wire);
2033        put_u24(0x00ff_ffff, &mut wire);
2034        wire.extend_from_slice(&delta.to_be_bytes());
2035        wire.extend_from_slice(&payload);
2036        assert_eq!(
2037            frame_timestamps(
2038                &ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap()
2039            ),
2040            [1, delta + 1, delta.wrapping_mul(2) + 1]
2041        );
2042    }
2043
2044    #[test]
2045    fn type3_continuation_requires_the_inherited_extended_value() {
2046        let payload = [0x2f, 1, 2, 3];
2047        let timestamp = 0x0100_0000;
2048        let mut wire = rtmp_type0(4, 1, AUDIO_MESSAGE, timestamp, &payload, 2);
2049        let first_extended_position = 1 + 11 + 4 + 2 + 1;
2050        wire[first_extended_position..first_extended_position + 4]
2051            .copy_from_slice(&(timestamp + 1).to_be_bytes());
2052        assert!(ertmp_input_from_rtmp_connections(&[chunked_trace(&wire, 2)], options(2)).is_err());
2053    }
2054
2055    #[test]
2056    fn explicitly_headed_continuations_must_match_the_active_message() {
2057        let payload = [0x2f, 1, 2, 3];
2058        let mut wire = rtmp_type0_prefix(7, 1, AUDIO_MESSAGE, 1, &payload, 2);
2059        let mut continuation = rtmp_type0_prefix(7, 1, AUDIO_MESSAGE, 1, &payload, 2);
2060        let body_offset = continuation.len() - 2;
2061        continuation[body_offset..].copy_from_slice(&payload[2..]);
2062        wire.extend(continuation);
2063        let input =
2064            ertmp_input_from_rtmp_connections(&[chunked_trace(&wire, 2)], options(2)).unwrap();
2065        assert_eq!(frame_timestamps(&input), [1]);
2066
2067        let mut changed = rtmp_type0_prefix(7, 1, AUDIO_MESSAGE, 1, &payload, 2);
2068        changed.extend(rtmp_type0(7, 1, AUDIO_MESSAGE, 2, &payload[2..], 2));
2069        let error = ertmp_input_from_rtmp_connections(&[chunked_trace(&changed, 2)], options(2))
2070            .unwrap_err();
2071        assert_eq!(
2072            error.to_string(),
2073            "RTMP message was replaced before all chunks arrived"
2074        );
2075    }
2076
2077    #[test]
2078    fn abort_discards_only_the_named_partial_message() {
2079        let partial_payload = [0x2f, 1, 2, 3];
2080        let mut wire = rtmp_type0(7, 1, AUDIO_MESSAGE, 1, &partial_payload, 2);
2081        wire.truncate(1 + 11 + 2);
2082        wire.extend(rtmp_type0(2, 0, ABORT_MESSAGE, 0, &7u32.to_be_bytes(), 2));
2083        wire.extend(rtmp_type0(7, 1, AUDIO_MESSAGE, 2, &[0x2f, 9], 2));
2084        let input =
2085            ertmp_input_from_rtmp_connections(&[chunked_trace(&wire, 2)], options(2)).unwrap();
2086        assert_eq!(frame_timestamps(&input), [2]);
2087
2088        let misplaced = rtmp_type0(3, 0, ABORT_MESSAGE, 0, &7u32.to_be_bytes(), 128);
2089        assert!(ertmp_input_from_rtmp_connections(&[trace(&misplaced)], options(128)).is_err());
2090    }
2091
2092    #[test]
2093    fn message_stream_track_association_rejects_collisions_and_resets_on_reconnect() {
2094        let config0 = [0x90, b'a', b'v', b'c', b'1', 1];
2095        let mut colliding = rtmp_type0(7, 1, VIDEO_MESSAGE, 0, &config0, 128);
2096        colliding.extend(rtmp_type0(8, 2, VIDEO_MESSAGE, 0, &config0, 128));
2097        assert!(ertmp_input_from_rtmp_connections(&[trace(&colliding)], options(128)).is_err());
2098
2099        let frame = [0x91, b'a', b'v', b'c', b'1', 0, 0, 0, 7];
2100        let metadata = video_metadata(&amf_pair("c2paSegmentInfo", 0x0c, b"segment"));
2101        let mut cross_stream_closure = rtmp_type0(7, 1, VIDEO_MESSAGE, 4, &frame, 128);
2102        cross_stream_closure.extend(rtmp_type0(7, 2, VIDEO_MESSAGE, 4, &metadata, 128));
2103        assert!(
2104            ertmp_input_from_rtmp_connections(&[trace(&cross_stream_closure)], options(128),)
2105                .is_err()
2106        );
2107
2108        let config1 = [0x96, 0x00, b'a', b'v', b'c', b'1', 1, 1];
2109        let mut unique = rtmp_type0(7, 1, VIDEO_MESSAGE, 0, &config0, 128);
2110        unique.extend(rtmp_type0(8, 2, VIDEO_MESSAGE, 0, &config1, 128));
2111        assert_eq!(
2112            ertmp_input_from_rtmp_connections(&[trace(&unique)], options(128))
2113                .unwrap()
2114                .input
2115                .events
2116                .len(),
2117            2
2118        );
2119
2120        let first = rtmp_type0(7, 1, VIDEO_MESSAGE, 0, &config0, 128);
2121        let second = rtmp_type0(8, 2, VIDEO_MESSAGE, 0, &config0, 128);
2122        let input =
2123            ertmp_input_from_rtmp_connections(&[trace(&first), trace(&second)], options(128))
2124                .unwrap();
2125        assert!(matches!(input.input.events[1], ErtmpEvent::Reconnect));
2126        assert_eq!(input.event_contexts.len(), input.input.events.len());
2127        assert!(input.event_contexts[1].is_none());
2128    }
2129
2130    #[test]
2131    fn aggregate_inherits_outer_message_stream_for_track_collisions() {
2132        let config = [0x90, b'a', b'v', b'c', b'1', 1];
2133        let aggregate = flv_tag(VIDEO_MESSAGE, 0, &config);
2134        let mut wire = rtmp_type0(7, 1, AGGREGATE_MESSAGE, 0, &aggregate, 128);
2135        wire.extend(rtmp_type0(8, 2, VIDEO_MESSAGE, 1, &config, 128));
2136        assert!(ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).is_err());
2137    }
2138
2139    #[test]
2140    fn every_reconnect_context_must_negotiate_c2pa_amf3() {
2141        let wire = rtmp_type0(
2142            7,
2143            1,
2144            VIDEO_MESSAGE,
2145            0,
2146            &[0x90, b'a', b'v', b'c', b'1', 1],
2147            128,
2148        );
2149        let bad = ErtmpConnectionContext {
2150            client_caps_ex: 0x10,
2151            server_caps_ex: 0,
2152            object_encoding: 3,
2153        };
2154        assert!(ertmp_input_from_rtmp_connections(
2155            &[
2156                trace(&wire),
2157                RtmpWireConnection {
2158                    bytes: &wire,
2159                    connection: bad,
2160                    initial_chunk_size: 128,
2161                },
2162            ],
2163            options(128),
2164        )
2165        .is_err());
2166    }
2167
2168    #[test]
2169    fn codec_specific_video_layout_preserves_payloads() {
2170        for fourcc in [b"vp08", b"vp09", b"av01"] {
2171            let mut payload = vec![0x91];
2172            payload.extend_from_slice(fourcc);
2173            payload.extend_from_slice(&[1, 2, 3, 4]);
2174            let wire = rtmp_type0(7, 1, VIDEO_MESSAGE, 9, &payload, 128);
2175            let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2176            match &input.input.events[0] {
2177                ErtmpEvent::Frame {
2178                    coded_data,
2179                    composition_time_offset,
2180                    ..
2181                } => {
2182                    assert_eq!(coded_data, &[1, 2, 3, 4]);
2183                    assert_eq!(*composition_time_offset, 0);
2184                }
2185                _ => panic!("expected frame"),
2186            }
2187        }
2188        for fourcc in [b"avc1", b"hvc1", b"vvc1"] {
2189            let mut payload = vec![0x91];
2190            payload.extend_from_slice(fourcc);
2191            payload.extend_from_slice(&[0xff, 0xff, 0xfe, 7, 8]);
2192            let wire = rtmp_type0(7, 1, VIDEO_MESSAGE, 9, &payload, 128);
2193            let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2194            match &input.input.events[0] {
2195                ErtmpEvent::Frame {
2196                    coded_data,
2197                    composition_time_offset,
2198                    ..
2199                } => {
2200                    assert_eq!(coded_data, &[7, 8]);
2201                    assert_eq!(*composition_time_offset, -2);
2202                }
2203                _ => panic!("expected frame"),
2204            }
2205        }
2206    }
2207
2208    #[test]
2209    fn multitrack_video_applies_composition_offsets_per_codec() {
2210        let payload = [
2211            0x96, 0x21, b'v', b'p', b'0', b'9', 1, 0, 0, 2, 8, 9, b'a', b'v', b'c', b'1', 2, 0, 0,
2212            5, 0, 0, 3, 6, 7,
2213        ];
2214        let wire = rtmp_type0(70, 1, VIDEO_MESSAGE, 9, &payload, 128);
2215        let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2216        match (&input.input.events[0], &input.input.events[1]) {
2217            (
2218                ErtmpEvent::Frame {
2219                    coded_data: first,
2220                    composition_time_offset: first_cto,
2221                    ..
2222                },
2223                ErtmpEvent::Frame {
2224                    coded_data: second,
2225                    composition_time_offset: second_cto,
2226                    ..
2227                },
2228            ) => {
2229                assert_eq!(first, &[8, 9]);
2230                assert_eq!(*first_cto, 0);
2231                assert_eq!(second, &[6, 7]);
2232                assert_eq!(*second_cto, 3);
2233            }
2234            _ => panic!("expected two frames"),
2235        }
2236    }
2237
2238    #[test]
2239    fn multitrack_metadata_ignores_the_outer_frame_type() {
2240        let mut payload = vec![0x86, VIDEO_METADATA, b'a', b'v', b'c', b'1', 3];
2241        payload.extend(amf_pair("c2paSegmentInfo", 0x0c, b"segment"));
2242        let wire = rtmp_type0(7, 1, VIDEO_MESSAGE, 9, &payload, 128);
2243        let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2244        assert!(matches!(
2245            &input.input.events[0],
2246            ErtmpEvent::Metadata {
2247                track,
2248                name,
2249                value,
2250                ..
2251            } if track.track_id == 3
2252                && name == "c2paSegmentInfo"
2253                && value == b"segment"
2254        ));
2255        assert_eq!(
2256            input.event_contexts[0],
2257            Some(ErtmpWireEventContext {
2258                message_stream_id: Some(1),
2259                codec_fourcc: Some(*b"avc1"),
2260                multitrack: true,
2261            })
2262        );
2263    }
2264
2265    #[test]
2266    fn enhanced_audio_fourcc_and_sequence_rules_are_fail_closed() {
2267        for fourcc in [b"ac-3", b"ec-3", b".mp3"] {
2268            let mut sequence = vec![0x90];
2269            sequence.extend_from_slice(fourcc);
2270            sequence.push(1);
2271            let wire = rtmp_type0(4, 1, AUDIO_MESSAGE, 0, &sequence, 128);
2272            assert!(ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).is_err());
2273        }
2274        for fourcc in [b"mp4a", b"fLaC"] {
2275            let mut empty = vec![0x90];
2276            empty.extend_from_slice(fourcc);
2277            let wire = rtmp_type0(4, 1, AUDIO_MESSAGE, 0, &empty, 128);
2278            assert!(ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).is_err());
2279        }
2280        let opus = [0x90, b'O', b'p', b'u', b's'];
2281        let wire = rtmp_type0(4, 1, AUDIO_MESSAGE, 0, &opus, 128);
2282        let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2283        assert!(matches!(
2284            &input.input.events[0],
2285            ErtmpEvent::CodecConfig { payload, .. } if payload.is_empty()
2286        ));
2287
2288        let unknown = [0x90, b'b', b'a', b'd', b'!'];
2289        let wire = rtmp_type0(4, 1, AUDIO_MESSAGE, 0, &unknown, 128);
2290        assert!(ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).is_err());
2291        for reserved in [3u8, 6u8] {
2292            let audio = [0x90 | reserved, b'm', b'p', b'4', b'a', 1];
2293            let wire = rtmp_type0(4, 1, AUDIO_MESSAGE, 0, &audio, 128);
2294            assert!(ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).is_err());
2295        }
2296    }
2297
2298    #[test]
2299    fn flv_reserved_filter_script_and_back_pointer_rules_are_enforced() {
2300        let config = [0x90, b'a', b'v', b'0', b'1', 1, 2, 3, 4];
2301        let valid = flv(
2302            0x01,
2303            &[
2304                flv_tag(AMF3_SCRIPT_DATA, 0, b"script"),
2305                flv_tag(VIDEO_MESSAGE, 55, &config),
2306            ],
2307        );
2308        let input = ertmp_input_from_flv(
2309            &valid,
2310            FlvWireOptions {
2311                connection: context(),
2312                audio_chunk_stream_id: 4,
2313                video_chunk_stream_id: 12,
2314                limits: ErtmpWireLimits::default(),
2315            },
2316        )
2317        .unwrap();
2318        assert_eq!(input.input.events.len(), 1);
2319        assert_eq!(
2320            input.event_contexts,
2321            [Some(ErtmpWireEventContext {
2322                message_stream_id: None,
2323                codec_fourcc: Some(*b"av01"),
2324                multitrack: false,
2325            })]
2326        );
2327
2328        let mut reserved_header = valid.clone();
2329        reserved_header[4] |= 0x02;
2330        assert!(ertmp_input_from_flv(
2331            &reserved_header,
2332            FlvWireOptions {
2333                connection: context(),
2334                audio_chunk_stream_id: 4,
2335                video_chunk_stream_id: 12,
2336                limits: ErtmpWireLimits::default(),
2337            },
2338        )
2339        .is_err());
2340        let filtered = flv(0x01, &[flv_tag(0x20 | VIDEO_MESSAGE, 0, &config)]);
2341        assert!(ertmp_input_from_flv(
2342            &filtered,
2343            FlvWireOptions {
2344                connection: context(),
2345                audio_chunk_stream_id: 4,
2346                video_chunk_stream_id: 12,
2347                limits: ErtmpWireLimits::default(),
2348            },
2349        )
2350        .is_err());
2351
2352        let mut bad_back_pointer = valid.clone();
2353        let last = bad_back_pointer.len() - 1;
2354        bad_back_pointer[last] ^= 1;
2355        assert!(ertmp_input_from_flv(
2356            &bad_back_pointer,
2357            FlvWireOptions {
2358                connection: context(),
2359                audio_chunk_stream_id: 4,
2360                video_chunk_stream_id: 12,
2361                limits: ErtmpWireLimits::default(),
2362            },
2363        )
2364        .is_err());
2365    }
2366
2367    #[test]
2368    fn video_commands_and_legacy_avc_codepoints_are_strict() {
2369        let command = rtmp_type0(7, 1, VIDEO_MESSAGE, 0, &[0xd0, 1], 128);
2370        assert!(
2371            ertmp_input_from_rtmp_connections(&[trace(&command)], options(128))
2372                .unwrap()
2373                .input
2374                .events
2375                .is_empty()
2376        );
2377        let invalid_avc = rtmp_type0(7, 1, VIDEO_MESSAGE, 0, &[0x17, 4, 0, 0, 0], 128);
2378        assert!(ertmp_input_from_rtmp_connections(&[trace(&invalid_avc)], options(128)).is_err());
2379        for payload in [
2380            &[0x37, 0, 0, 0, 0, 1][..],
2381            &[0x47, 0, 0, 0, 0, 1][..],
2382            &[0xb1, b'a', b'v', b'c', b'1', 0, 0, 0, 1][..],
2383            &[0xc6, 0x01, b'a', b'v', b'c', b'1', 1, 0, 0, 0, 7][..],
2384        ] {
2385            let wire = rtmp_type0(7, 1, VIDEO_MESSAGE, 0, payload, 128);
2386            assert!(ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).is_err());
2387        }
2388    }
2389
2390    #[test]
2391    fn protocol_control_placement_and_shapes_are_strict() {
2392        let mut valid = rtmp_type0(2, 0, SET_CHUNK_SIZE, 0, &128u32.to_be_bytes(), 128);
2393        valid.extend(rtmp_type0(2, 0, ABORT_MESSAGE, 0, &7u32.to_be_bytes(), 128));
2394        valid.extend(rtmp_type0(
2395            2,
2396            0,
2397            ACKNOWLEDGEMENT,
2398            0,
2399            &1u32.to_be_bytes(),
2400            128,
2401        ));
2402        valid.extend(rtmp_type0(
2403            2,
2404            0,
2405            WINDOW_ACKNOWLEDGEMENT_SIZE,
2406            0,
2407            &1u32.to_be_bytes(),
2408            128,
2409        ));
2410        valid.extend(rtmp_type0(
2411            2,
2412            0,
2413            SET_PEER_BANDWIDTH,
2414            0,
2415            &[0, 0, 0, 1, 2],
2416            128,
2417        ));
2418        assert!(
2419            ertmp_input_from_rtmp_connections(&[trace(&valid)], options(128))
2420                .unwrap()
2421                .input
2422                .events
2423                .is_empty()
2424        );
2425
2426        for (message_type, payload) in [
2427            (SET_CHUNK_SIZE, &[0, 0, 1][..]),
2428            (ABORT_MESSAGE, &[0, 0, 7][..]),
2429            (ACKNOWLEDGEMENT, &[0, 0, 1][..]),
2430            (WINDOW_ACKNOWLEDGEMENT_SIZE, &[0, 0, 1][..]),
2431            (SET_PEER_BANDWIDTH, &[0, 0, 0, 1][..]),
2432        ] {
2433            let invalid = rtmp_type0(2, 0, message_type, 0, payload, 128);
2434            assert!(ertmp_input_from_rtmp_connections(&[trace(&invalid)], options(128)).is_err());
2435        }
2436        let media_on_control = rtmp_type0(2, 1, AUDIO_MESSAGE, 0, &[0x2f, 1], 128);
2437        assert!(
2438            ertmp_input_from_rtmp_connections(&[trace(&media_on_control)], options(128)).is_err()
2439        );
2440        let undefined = rtmp_type0(7, 1, 7, 0, &[], 128);
2441        let error =
2442            ertmp_input_from_rtmp_connections(&[trace(&undefined)], options(128)).unwrap_err();
2443        assert_eq!(error.to_string(), "undefined RTMP message type 7");
2444    }
2445
2446    #[test]
2447    fn aggregate_normalizes_timestamps_and_ignores_submessage_stream_ids() {
2448        let mut first = flv_tag(AUDIO_MESSAGE, 30, &[0x2f, 1]);
2449        first[8..11].copy_from_slice(&[1, 2, 3]);
2450        let mut second = flv_tag(AUDIO_MESSAGE, 35, &[0x2f, 2]);
2451        second[8..11].copy_from_slice(&[4, 5, 6]);
2452        let mut aggregate = first;
2453        aggregate.extend(second);
2454        let wire = rtmp_type0(7, 9, AGGREGATE_MESSAGE, 100, &aggregate, 128);
2455        let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2456        assert_eq!(frame_timestamps(&input), [100, 105]);
2457
2458        let mut malformed = aggregate;
2459        let last = malformed.len() - 1;
2460        malformed[last] ^= 1;
2461        let wire = rtmp_type0(7, 9, AGGREGATE_MESSAGE, 100, &malformed, 128);
2462        assert!(ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).is_err());
2463    }
2464
2465    #[test]
2466    fn audio_multitrack_modes_apply_codec_specific_sequence_rules() {
2467        let one_track = [0x95, 0x00, b'O', b'p', b'u', b's', 3];
2468        let wire = rtmp_type0(7, 1, AUDIO_MESSAGE, 0, &one_track, 128);
2469        let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2470        assert!(matches!(
2471            &input.input.events[0],
2472            ErtmpEvent::CodecConfig { track, payload, .. }
2473                if track.track_id == 3 && payload.is_empty()
2474        ));
2475
2476        let many_tracks = [
2477            0x95, 0x10, b'm', b'p', b'4', b'a', 1, 0, 0, 1, 7, 2, 0, 0, 1, 8,
2478        ];
2479        let wire = rtmp_type0(7, 1, AUDIO_MESSAGE, 0, &many_tracks, 128);
2480        let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2481        assert_eq!(input.input.events.len(), 2);
2482
2483        let many_codecs = [
2484            0x95, 0x20, b'O', b'p', b'u', b's', 1, 0, 0, 0, b'm', b'p', b'4', b'a', 2, 0, 0, 1, 9,
2485        ];
2486        let wire = rtmp_type0(7, 1, AUDIO_MESSAGE, 0, &many_codecs, 128);
2487        let input = ertmp_input_from_rtmp_connections(&[trace(&wire)], options(128)).unwrap();
2488        assert_eq!(input.input.events.len(), 2);
2489    }
2490
2491    #[test]
2492    fn buffered_and_active_chunk_stream_limits_cover_complete_interleaved_messages() {
2493        let first_payload = [0x2f, 1, 2];
2494        let second_payload = [0x2f, 3, 4];
2495        let mut wire = rtmp_type0_prefix(7, 1, AUDIO_MESSAGE, 0, &first_payload, 1);
2496        wire.extend(rtmp_type0_prefix(
2497            8,
2498            1,
2499            AUDIO_MESSAGE,
2500            0,
2501            &second_payload,
2502            1,
2503        ));
2504        for byte in &first_payload[1..] {
2505            put_basic_header(3, 7, &mut wire);
2506            wire.push(*byte);
2507        }
2508        for byte in &second_payload[1..] {
2509            put_basic_header(3, 8, &mut wire);
2510            wire.push(*byte);
2511        }
2512
2513        let exact_buffer_limit = ErtmpWireLimits {
2514            max_buffered_message_bytes: 4,
2515            max_active_chunk_streams: 2,
2516            ..Default::default()
2517        };
2518        let input =
2519            ertmp_input_from_rtmp_connections(&[chunked_trace(&wire, 1)], exact_buffer_limit)
2520                .unwrap();
2521        assert_eq!(input.input.events.len(), 2);
2522
2523        let one_under_buffer_limit = ErtmpWireLimits {
2524            max_buffered_message_bytes: 3,
2525            max_active_chunk_streams: 2,
2526            ..Default::default()
2527        };
2528        let error =
2529            ertmp_input_from_rtmp_connections(&[chunked_trace(&wire, 1)], one_under_buffer_limit)
2530                .unwrap_err();
2531        assert_eq!(
2532            error.to_string(),
2533            "incomplete RTMP messages exceed the buffered byte limit"
2534        );
2535
2536        let one_under_stream_limit = ErtmpWireLimits {
2537            max_buffered_message_bytes: 4,
2538            max_active_chunk_streams: 1,
2539            ..Default::default()
2540        };
2541        let error =
2542            ertmp_input_from_rtmp_connections(&[chunked_trace(&wire, 1)], one_under_stream_limit)
2543                .unwrap_err();
2544        assert_eq!(
2545            error.to_string(),
2546            "RTMP active chunk stream count exceeds its limit"
2547        );
2548    }
2549
2550    #[test]
2551    fn abort_and_completion_release_buffer_capacity_for_reuse() {
2552        let payload = [0x90, b'a', b'v', b'c', b'1', 1, 2, 3];
2553        let mut wire = rtmp_type0_prefix(7, 1, VIDEO_MESSAGE, 0, &payload, 4);
2554        wire.extend(rtmp_type0(2, 0, ABORT_MESSAGE, 0, &7u32.to_be_bytes(), 4));
2555        wire.extend(rtmp_type0(8, 1, VIDEO_MESSAGE, 1, &payload, 4));
2556        wire.extend(rtmp_type0(8, 1, VIDEO_MESSAGE, 2, &payload, 4));
2557        let limits = ErtmpWireLimits {
2558            max_buffered_message_bytes: 8,
2559            ..Default::default()
2560        };
2561        let input = ertmp_input_from_rtmp_connections(&[chunked_trace(&wire, 4)], limits).unwrap();
2562        assert_eq!(input.input.events.len(), 2);
2563    }
2564
2565    #[test]
2566    fn allocation_limits_apply_before_event_and_wire_copies() {
2567        let limits = ErtmpWireLimits {
2568            max_total_event_payload_bytes: 2,
2569            ..Default::default()
2570        };
2571        let config = rtmp_type0(
2572            7,
2573            1,
2574            VIDEO_MESSAGE,
2575            0,
2576            &[0x90, b'a', b'v', b'c', b'1', 1, 2, 3],
2577            128,
2578        );
2579        let error = ertmp_input_from_rtmp_connections(&[trace(&config)], limits).unwrap_err();
2580        assert_eq!(
2581            error.to_string(),
2582            "E-RTMP event payload bytes exceed their limit"
2583        );
2584
2585        let limits = ErtmpWireLimits {
2586            max_total_wire_bytes: config.len(),
2587            ..Default::default()
2588        };
2589        let error = ertmp_input_from_rtmp_connections(&[trace(&config), trace(&config)], limits)
2590            .unwrap_err();
2591        assert_eq!(
2592            error.to_string(),
2593            "RTMP connections exceed the total wire byte limit"
2594        );
2595    }
2596
2597    #[test]
2598    fn byte_array_references_are_charged_again_at_the_wire_callback_boundary() {
2599        let mut pairs = amf_pair("c2paManifest", 0x0c, b"1234");
2600        pairs.extend_from_slice(&[0x06, 31]);
2601        pairs.extend_from_slice(b"c2paSegmentInfo");
2602        pairs.extend_from_slice(&[0x0c, 0x00]);
2603        let metadata = video_metadata(&pairs);
2604        let wire = rtmp_type0(7, 1, VIDEO_MESSAGE, 0, &metadata, 128);
2605        let limits = ErtmpWireLimits {
2606            max_total_event_payload_bytes: std::mem::size_of::<Option<ErtmpWireEventContext>>() * 2
2607                + 34,
2608            ..Default::default()
2609        };
2610        let error = ertmp_input_from_rtmp_connections(&[trace(&wire)], limits).unwrap_err();
2611        assert_eq!(
2612            error.to_string(),
2613            "E-RTMP event payload bytes exceed their limit"
2614        );
2615    }
2616
2617    #[test]
2618    fn event_limit_is_checked_before_metadata_values_are_copied() {
2619        let pairs = amf_pair("c2paManifest", 0x0c, b"store");
2620        let metadata = video_metadata(&pairs);
2621        let wire = rtmp_type0(7, 1, VIDEO_MESSAGE, 0, &metadata, 128);
2622        let limits = ErtmpWireLimits {
2623            max_events: 0,
2624            ..Default::default()
2625        };
2626        assert!(ertmp_input_from_rtmp_connections(&[trace(&wire)], limits,).is_err());
2627    }
2628}