1mod 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#[derive(Debug, Clone, Copy)]
39pub struct ErtmpWireLimits {
40 pub max_wire_bytes: usize,
42 pub max_total_wire_bytes: usize,
44 pub max_message_bytes: usize,
46 pub max_buffered_message_bytes: usize,
48 pub max_active_chunk_streams: usize,
50 pub max_events: usize,
52 pub max_total_event_payload_bytes: usize,
55 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#[derive(Debug, Clone, Copy)]
76pub struct ErtmpConnectionContext {
77 pub client_caps_ex: u64,
79 pub server_caps_ex: u64,
81 pub object_encoding: u8,
83}
84
85#[derive(Debug, Clone, Copy)]
87pub struct RtmpWireConnection<'a> {
88 pub bytes: &'a [u8],
90 pub connection: ErtmpConnectionContext,
92 pub initial_chunk_size: usize,
94}
95
96#[derive(Debug, Clone)]
98pub struct ErtmpWireInput {
99 pub input: ErtmpInput,
101 pub event_contexts: Vec<Option<ErtmpWireEventContext>>,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct ErtmpWireEventContext {
108 pub message_stream_id: Option<u32>,
110 pub codec_fourcc: Option<[u8; 4]>,
112 pub multitrack: bool,
114}
115
116#[derive(Debug, Clone, Copy)]
119pub struct FlvWireOptions {
120 pub connection: ErtmpConnectionContext,
122 pub audio_chunk_stream_id: u32,
124 pub video_chunk_stream_id: u32,
126 pub limits: ErtmpWireLimits,
128}
129
130#[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
153pub 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
265pub 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)] fn 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(()); }
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)] fn 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 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(×tamp.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(×tamp.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}