Skip to main content

arcly_stream/protocol/
rtp.rs

1//! Shared RTP/RTCP parsing and NAL-codec (de)packetization — RFC 3550 framing
2//! plus H.264 (RFC 6184) and H.265 (RFC 7798) payload formats.
3//!
4//! Gated behind the internal `_rtp` marker, pulled in by both [`rtsp`] and
5//! [`webrtc`]. The two transports differ only in how RTP packets reach the
6//! process (TCP-interleaved / UDP for RTSP, DTLS-SRTP for WebRTC); once a packet
7//! is in hand, reassembling NAL units into an Annex-B access unit is identical,
8//! so it lives here once.
9//!
10//! [`rtsp`]: crate::protocol::rtsp
11//! [`webrtc`]: crate::protocol::webrtc
12//!
13//! # What it does
14//!
15//! - [`RtpHeader::parse`] decodes the fixed RTP header (RFC 3550 §5.1), honoring
16//!   the CSRC count and the extension-header flag to locate the payload.
17//! - [`H264Depacketizer`] / [`H265Depacketizer`] turn a sequence of RTP payloads
18//!   into complete access units in Annex-B form. Each handles the three NALU
19//!   packetization modes for its codec — single NAL, aggregation (STAP-A type 24
20//!   / AP type 48), and fragmentation (FU-A type 28 / FU type 49) — emitting an
21//!   access unit when the marker bit is set or the timestamp advances.
22//! - [`RtpPacketizer`] performs the reverse for egress (e.g. WebRTC WHEP),
23//!   selecting the H.264 or H.265 payload format.
24//!
25//! # What it does not do
26//!
27//! Jitter-buffer reordering and loss concealment are the caller's concern — the
28//! depacketizer assumes in-order delivery (true for TCP-interleaved RTSP; for
29//! UDP/SRTP a small reorder buffer should sit in front of it). It reports a
30//! [`DepacketizeError::OutOfOrder`] gap so a handler can request a keyframe
31//! (PLI/FIR) rather than emit a corrupt access unit.
32
33use bytes::Bytes;
34
35/// Annex-B start code prefixed to every reassembled NAL unit.
36const ANNEXB_START: [u8; 4] = [0, 0, 0, 1];
37
38/// A parsed RTP fixed header (RFC 3550 §5.1).
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct RtpHeader {
41    /// Payload type (7 bits) — identifies the codec/format binding from SDP.
42    pub payload_type: u8,
43    /// Marker bit. For H.264 it flags the last packet of an access unit.
44    pub marker: bool,
45    /// 16-bit sequence number, increments by one per packet (wraps).
46    pub sequence: u16,
47    /// 32-bit media timestamp in the payload's clock (90 kHz for H.264 video).
48    pub timestamp: u32,
49    /// Synchronization source identifier.
50    pub ssrc: u32,
51    /// Byte offset at which the payload begins (past CSRCs and any extension).
52    pub payload_offset: usize,
53}
54
55impl RtpHeader {
56    /// Parse the fixed header from the front of `buf`, returning the header and
57    /// the payload offset. Returns `None` if `buf` is too short or the version
58    /// field is not 2.
59    pub fn parse(buf: &[u8]) -> Option<RtpHeader> {
60        use super::byteops::ByteReader;
61        let mut r = ByteReader::new(buf);
62        let b0 = r.u8()?;
63        if b0 >> 6 != 2 {
64            return None; // RTP version must be 2
65        }
66        let has_extension = b0 & 0x10 != 0;
67        let csrc_count = (b0 & 0x0F) as usize;
68        let b1 = r.u8()?;
69        let marker = b1 & 0x80 != 0;
70        let payload_type = b1 & 0x7F;
71        let sequence = r.u16_be()?;
72        let timestamp = r.u32_be()?;
73        let ssrc = r.u32_be()?;
74        r.skip(csrc_count * 4)?; // CSRC list
75
76        if has_extension {
77            // Extension header: 2-byte profile id, 2-byte length (in 32-bit words).
78            r.skip(2)?;
79            let ext_words = r.u16_be()? as usize;
80            r.skip(ext_words * 4)?;
81        }
82        Some(RtpHeader {
83            payload_type,
84            marker,
85            sequence,
86            timestamp,
87            ssrc,
88            payload_offset: r.position(),
89        })
90    }
91}
92
93/// Extract the value of RTP header extension `ext_id` from `buf`, per the
94/// RFC 8285 one-byte (`0xBEDE`) and two-byte (`0x100x`) forms. Returns `None` if
95/// the packet has no extension block or no element with that local id.
96///
97/// Used to read the **RID** (`urn:ietf:params:rtp-hdrext:sdes:rid`) that labels a
98/// simulcast layer — the value is the ASCII rid string.
99pub fn rtp_extension_value(buf: &[u8], ext_id: u8) -> Option<&[u8]> {
100    if buf.len() < 12 || buf[0] >> 6 != 2 || buf[0] & 0x10 == 0 {
101        return None; // not RTPv2 or no extension flag
102    }
103    let csrc = (buf[0] & 0x0F) as usize;
104    let mut p = 12 + csrc * 4;
105    if buf.len() < p + 4 {
106        return None;
107    }
108    let profile = u16::from_be_bytes([buf[p], buf[p + 1]]);
109    let words = u16::from_be_bytes([buf[p + 2], buf[p + 3]]) as usize;
110    p += 4;
111    let end = p.checked_add(words * 4)?;
112    let ext = buf.get(p..end)?;
113
114    if profile == 0xBEDE {
115        // One-byte form: each element is `(id<<4 | len-1)` then `len` bytes.
116        let mut i = 0;
117        while i < ext.len() {
118            let b = ext[i];
119            if b == 0 {
120                i += 1; // padding
121                continue;
122            }
123            let id = b >> 4;
124            let len = (b & 0x0F) as usize + 1;
125            i += 1;
126            let value = ext.get(i..i + len)?;
127            if id == ext_id {
128                return Some(value);
129            }
130            i += len;
131        }
132    } else if profile & 0xFFF0 == 0x1000 {
133        // Two-byte form: each element is `id`, `len`, then `len` bytes.
134        let mut i = 0;
135        while i + 1 < ext.len() {
136            let id = ext[i];
137            let len = ext[i + 1] as usize;
138            i += 2;
139            if id == 0 {
140                continue; // padding
141            }
142            let value = ext.get(i..i + len)?;
143            if id == ext_id {
144                return Some(value);
145            }
146            i += len;
147        }
148    }
149    None
150}
151
152/// Decode the **client-to-mixer audio level** RTP header extension (RFC 6464,
153/// `urn:ietf:params:rtp-hdrext:ssrc-audio-level`) carried under `ext_id`.
154///
155/// Returns `(level_dbov, voice_active)` where `level_dbov` is the audio level in
156/// -dBov (0 = loudest, 127 = silence) and `voice_active` is the voice-activity
157/// flag (the extension's most-significant bit). `None` when the packet carries no
158/// such extension. This is the per-packet input a conference uses to pick the
159/// active (dominant) speaker.
160pub fn audio_level(buf: &[u8], ext_id: u8) -> Option<(u8, bool)> {
161    let b = *rtp_extension_value(buf, ext_id)?.first()?;
162    Some((b & 0x7F, b & 0x80 != 0))
163}
164
165/// Errors surfaced while depacketizing an RTP stream.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167#[non_exhaustive]
168pub enum DepacketizeError {
169    /// The packet was shorter than the format requires.
170    Truncated,
171    /// A sequence-number discontinuity was detected mid-fragment; the partial
172    /// access unit was dropped. The handler should request a keyframe.
173    OutOfOrder,
174    /// An unsupported NAL/aggregation type was encountered.
175    Unsupported(u8),
176}
177
178/// Depacketizes RFC 3640 AAC-hbr RTP payloads into raw AAC access units.
179///
180/// The common RTSP/SDP profile for AAC (`mode=AAC-hbr`, `sizelength=13`,
181/// `indexlength=3`) frames each payload as a 2-byte **AU-headers-length** (in
182/// bits), followed by one 2-byte AU-header per access unit (13-bit size +
183/// 3-bit index), followed by the access units concatenated. One RTP packet may
184/// carry several AAC frames; [`push`](Self::push) returns each as a separate
185/// raw (ADTS-less) [`bytes::Bytes`].
186#[derive(Debug, Clone, Copy, Default)]
187pub struct AacDepacketizer {
188    /// Bits per AU-header `size` field (13 for AAC-hbr).
189    size_length: u8,
190    /// Bits per AU-header `index`/`index-delta` field (3 for AAC-hbr).
191    index_length: u8,
192}
193
194impl AacDepacketizer {
195    /// A depacketizer for the standard AAC-hbr profile (`sizelength=13`,
196    /// `indexlength=3`).
197    pub fn new() -> Self {
198        Self {
199            size_length: 13,
200            index_length: 3,
201        }
202    }
203
204    /// A depacketizer with explicit AU-header field widths from the SDP `fmtp`.
205    pub fn with_lengths(size_length: u8, index_length: u8) -> Self {
206        Self {
207            size_length,
208            index_length,
209        }
210    }
211
212    /// Split one RTP AAC-hbr payload into its constituent access units.
213    pub fn push(&self, payload: &[u8]) -> Result<Vec<Bytes>, DepacketizeError> {
214        if payload.len() < 2 {
215            return Err(DepacketizeError::Truncated);
216        }
217        // Sizes wider than a 16-bit AU-header field are unsupported (and would
218        // otherwise over-shift below). `with_lengths` can supply arbitrary widths.
219        if self.size_length == 0 || self.size_length > 16 {
220            return Err(DepacketizeError::Unsupported(self.size_length));
221        }
222        let header_bits = u16::from_be_bytes([payload[0], payload[1]]) as usize;
223        let au_header_bits = self.size_length as usize + self.index_length as usize;
224        if au_header_bits == 0 {
225            return Err(DepacketizeError::Unsupported(0));
226        }
227        let header_bytes = header_bits.div_ceil(8);
228        let au_count = header_bits / au_header_bits;
229        let headers = payload
230            .get(2..2 + header_bytes)
231            .ok_or(DepacketizeError::Truncated)?;
232        let mut data_off = 2 + header_bytes;
233        let mut out = Vec::with_capacity(au_count);
234        for i in 0..au_count {
235            // Each AU-header is `au_header_bits` wide; for AAC-hbr that is 16
236            // bits, so the size is the top `size_length` bits of a 2-byte field.
237            let bit = i * au_header_bits;
238            let byte = bit / 8;
239            let hdr = headers
240                .get(byte..byte + 2)
241                .ok_or(DepacketizeError::Truncated)?;
242            let size = (u16::from_be_bytes([hdr[0], hdr[1]]) >> (16 - self.size_length)) as usize;
243            let end = data_off + size;
244            let au = payload
245                .get(data_off..end)
246                .ok_or(DepacketizeError::Truncated)?;
247            out.push(Bytes::copy_from_slice(au));
248            data_off = end;
249        }
250        Ok(out)
251    }
252}
253
254/// One RTP packet's payload (everything *after* the 12-byte header) plus its
255/// marker bit, as produced by [`RtpPacketizer::fragment_into`].
256///
257/// A fragment is **viewer-independent**: it depends only on the access-unit bytes
258/// and the codec/MTU, not on a session's SSRC, sequence number, payload type, or
259/// timestamp. So for an N-viewer fan-out (WHEP), an origin computes the fragment
260/// list **once per stream** and each viewer turns each fragment into a wire
261/// packet with [`RtpPacketizer::stamp_into`] (its own header) — sharing the
262/// NAL→FU fragmentation work instead of repeating it per viewer.
263#[derive(Debug, Clone)]
264pub struct RtpFragment {
265    /// The RTP marker bit (set on the last packet of an access unit).
266    pub marker: bool,
267    /// The packet payload: a single NAL, or one FU/FU-A fragment.
268    pub payload: Vec<u8>,
269}
270
271/// Packetizes H.264 Annex-B access units into RFC 6184 RTP packets — the inverse
272/// of [`H264Depacketizer`], used for WebRTC/WHEP egress.
273///
274/// Each NAL unit that fits the MTU is sent as a single-NAL packet; larger NALs
275/// are split into FU-A fragments. The RTP marker bit is set on the last packet
276/// of each access unit so the receiver knows the frame is complete.
277#[derive(Debug, Clone)]
278pub struct RtpPacketizer {
279    payload_type: u8,
280    ssrc: u32,
281    sequence: u16,
282    /// Maximum RTP payload size (excluding the 12-byte header).
283    max_payload: usize,
284    /// Which NAL-based RTP payload format to emit (H.264 RFC 6184 vs H.265
285    /// RFC 7798 — they differ in NAL-header width and FU framing).
286    codec: NalCodec,
287}
288
289/// The NAL-based codecs [`RtpPacketizer`] / the depacketizers understand. Both
290/// are Annex-B start-code framed; they differ in NAL-header width (1 vs 2 bytes)
291/// and fragmentation-unit layout.
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293enum NalCodec {
294    H264,
295    H265,
296}
297
298/// Walk an Annex-B access unit's NALs and invoke `emit(marker, &fill)` once per
299/// RTP packet **payload** (everything after the 12-byte header) — a single NAL,
300/// or one FU-A (H.264 RFC 6184 §5.8) / FU (H.265 RFC 7798 §4.4.3) fragment — in
301/// order. `marker` is set on the last packet of the access unit; `fill` appends
302/// the payload bytes.
303///
304/// The single source of NAL→RTP framing, driven by both
305/// [`RtpPacketizer::packetize_into`] (which stamps a header inline) and
306/// [`RtpPacketizer::fragment_into`] (which collects the payloads) — sequence- and
307/// header-agnostic, so the two cannot diverge.
308fn for_each_nal_packet(
309    codec: NalCodec,
310    max_payload: usize,
311    access_unit: &[u8],
312    mut emit: impl FnMut(bool, &dyn Fn(&mut Vec<u8>)),
313) {
314    // Both codecs are Annex-B start-code framed; the shared scanner yields the
315    // NAL units (without start codes) in order.
316    let nals: Vec<&[u8]> = crate::codec::nal::iter_nals(access_unit)
317        .filter(|n| !n.is_empty())
318        .collect();
319    for (i, nal) in nals.iter().enumerate() {
320        let last_nal = i + 1 == nals.len();
321        if nal.len() <= max_payload {
322            // Single-NAL packet; marker on the final NAL of the AU.
323            emit(last_nal, &|b| b.extend_from_slice(nal));
324            continue;
325        }
326        match codec {
327            NalCodec::H264 => {
328                let fu_indicator = (nal[0] & 0xE0) | 28; // F|NRI from NAL, type 28
329                let nal_type = nal[0] & 0x1F;
330                let body = &nal[1..];
331                // Each fragment carries a 2-byte FU header (indicator + FU header).
332                let chunk = max_payload.saturating_sub(2).max(1);
333                let n = body.len().div_ceil(chunk);
334                for (idx, part) in body.chunks(chunk).enumerate() {
335                    let (start, end) = (idx == 0, idx + 1 == n);
336                    let fu_header =
337                        nal_type | if start { 0x80 } else { 0 } | if end { 0x40 } else { 0 };
338                    // Marker only on the last fragment of the final NAL of the AU.
339                    emit(last_nal && end, &|pkt| {
340                        pkt.push(fu_indicator);
341                        pkt.push(fu_header);
342                        pkt.extend_from_slice(part);
343                    });
344                }
345            }
346            NalCodec::H265 => {
347                // A well-formed H.265 NAL has a 2-byte header; anything shorter
348                // can't be fragmented meaningfully, so emit it as a single packet.
349                if nal.len() < 2 {
350                    emit(last_nal, &|pkt| pkt.extend_from_slice(nal));
351                    continue;
352                }
353                let nal_type = (nal[0] >> 1) & 0x3F;
354                // PayloadHdr: keep F (bit 15) + LayerId + TID, overwrite the 6-bit
355                // type field (bits 9..14) with 49 (FU).
356                let payload_hdr0 = (nal[0] & 0x81) | (49 << 1);
357                let payload_hdr1 = nal[1];
358                let body = &nal[2..];
359                // Each fragment carries the 2-byte PayloadHdr + 1-byte FU header.
360                let chunk = max_payload.saturating_sub(3).max(1);
361                let n = body.len().div_ceil(chunk);
362                for (idx, part) in body.chunks(chunk).enumerate() {
363                    let (start, end) = (idx == 0, idx + 1 == n);
364                    let fu_header =
365                        nal_type | if start { 0x80 } else { 0 } | if end { 0x40 } else { 0 };
366                    emit(last_nal && end, &|pkt| {
367                        pkt.push(payload_hdr0);
368                        pkt.push(payload_hdr1);
369                        pkt.push(fu_header);
370                        pkt.extend_from_slice(part);
371                    });
372                }
373            }
374        }
375    }
376}
377
378impl RtpPacketizer {
379    /// An H.264 packetizer for `payload_type`/`ssrc`. `mtu` is the maximum UDP
380    /// payload (1200 is the WebRTC-safe default); the 12-byte RTP header is
381    /// subtracted.
382    pub fn new(payload_type: u8, ssrc: u32, mtu: usize) -> Self {
383        Self::with_codec(payload_type, ssrc, mtu, NalCodec::H264)
384    }
385
386    /// An H.265 (HEVC) packetizer, emitting the RFC 7798 payload format
387    /// (2-byte NAL header, FU type 49). Counterpart to [`new`](Self::new).
388    pub fn new_h265(payload_type: u8, ssrc: u32, mtu: usize) -> Self {
389        Self::with_codec(payload_type, ssrc, mtu, NalCodec::H265)
390    }
391
392    fn with_codec(payload_type: u8, ssrc: u32, mtu: usize, codec: NalCodec) -> Self {
393        Self {
394            payload_type,
395            ssrc,
396            sequence: 0,
397            max_payload: mtu.saturating_sub(12).max(1),
398            codec,
399        }
400    }
401
402    /// Emit one RTP packet: recycle a buffer from `recycle` (reusing its heap
403    /// allocation), write the 12-byte header, let `fill` append the payload, and
404    /// push it onto `out`. Advances the sequence number.
405    fn emit(
406        &mut self,
407        recycle: &mut Vec<Vec<u8>>,
408        out: &mut Vec<Vec<u8>>,
409        marker: bool,
410        timestamp: u32,
411        fill: &dyn Fn(&mut Vec<u8>),
412    ) {
413        let mut buf = recycle.pop().unwrap_or_default();
414        buf.clear();
415        write_rtp_header(
416            &mut buf,
417            self.payload_type,
418            marker,
419            self.sequence,
420            timestamp,
421            self.ssrc,
422        );
423        self.sequence = self.sequence.wrapping_add(1);
424        fill(&mut buf);
425        out.push(buf);
426    }
427
428    /// Packetize one Annex-B access unit at `timestamp` (90 kHz) into RTP packets.
429    ///
430    /// Each NAL that fits the MTU is sent as a single-NAL packet; larger NALs are
431    /// fragmented (FU-A for H.264, FU for H.265). The marker bit is set on the
432    /// last packet of the access unit.
433    ///
434    /// Allocating variant; prefer [`packetize_into`](Self::packetize_into) on the
435    /// egress hot path to recycle packet buffers across frames.
436    pub fn packetize(&mut self, access_unit: &[u8], timestamp: u32) -> Vec<Vec<u8>> {
437        let mut out = Vec::new();
438        self.packetize_into(access_unit, timestamp, &mut out);
439        out
440    }
441
442    /// Packetize into a caller-owned buffer, recycling the `Vec<u8>` allocations
443    /// it already holds from a previous frame.
444    ///
445    /// The hot-path contract: pass the *same* `out` every frame. On entry the
446    /// previously-produced (already-sent) packet buffers are reclaimed as a free
447    /// pool and refilled in place, so steady-state egress performs no per-packet
448    /// heap allocation.
449    pub fn packetize_into(&mut self, access_unit: &[u8], timestamp: u32, out: &mut Vec<Vec<u8>>) {
450        // Reclaim last frame's buffers (now consumed by the caller) as a pool.
451        let mut recycle = std::mem::take(out);
452        // Walk the AU once (shared with `fragment_into`); stamp each packet's
453        // header inline so the hot path still recycles buffers and writes no
454        // intermediate fragment list.
455        for_each_nal_packet(self.codec, self.max_payload, access_unit, |marker, fill| {
456            self.emit(&mut recycle, out, marker, timestamp, fill)
457        });
458    }
459
460    /// Split an access unit into viewer-independent [`RtpFragment`]s (the packet
461    /// payloads + marker bits), **without** touching the sequence number — so the
462    /// same packetizer (or a throwaway template one) can fragment once and every
463    /// viewer stamps the result with [`stamp_into`](Self::stamp_into).
464    ///
465    /// This produces exactly the payloads `packetize_into` would, in the same
466    /// order (both drive the shared `for_each_nal_packet` walker); only the
467    /// per-session RTP header is deferred to `stamp_into`. The caller passes the
468    /// same `out` each frame to
469    /// recycle the fragment buffers.
470    pub fn fragment_into(&self, access_unit: &[u8], out: &mut Vec<RtpFragment>) {
471        let mut recycle = std::mem::take(out);
472        for_each_nal_packet(self.codec, self.max_payload, access_unit, |marker, fill| {
473            let mut payload = recycle.pop().map(|f| f.payload).unwrap_or_default();
474            payload.clear();
475            fill(&mut payload);
476            out.push(RtpFragment { marker, payload });
477        });
478    }
479
480    /// Turn one shared [`RtpFragment`] into a complete RTP packet on this session's
481    /// timeline: writes the 12-byte header (this packetizer's PT/SSRC + its own
482    /// monotonic sequence) at `timestamp`, then the fragment payload. Advances the
483    /// sequence number. The per-viewer counterpart to [`fragment_into`](Self::fragment_into).
484    pub fn stamp_into(&mut self, frag: &RtpFragment, timestamp: u32, out: &mut Vec<u8>) {
485        out.clear();
486        write_rtp_header(
487            out,
488            self.payload_type,
489            frag.marker,
490            self.sequence,
491            timestamp,
492            self.ssrc,
493        );
494        self.sequence = self.sequence.wrapping_add(1);
495        out.extend_from_slice(&frag.payload);
496    }
497}
498
499/// Reassembles RFC 6184 H.264 RTP payloads into Annex-B access units.
500///
501/// Feed each packet's payload (the bytes after [`RtpHeader::payload_offset`])
502/// with its marker bit and timestamp to [`push`](Self::push). When a complete
503/// access unit is ready the method returns `Ok(Some(au))`, where `au` is the
504/// concatenated NAL units each prefixed with a 4-byte Annex-B start code —
505/// exactly the shape the codec parsers and `annexb_to_avcc` expect.
506#[derive(Debug, Default)]
507pub struct H264Depacketizer {
508    /// Bytes accumulated for the current access unit (Annex-B framed).
509    au: Vec<u8>,
510    /// FU-A reassembly buffer for the NAL currently being defragmented.
511    fua: Vec<u8>,
512    /// `true` while an FU-A fragment is in progress (between Start and End bits).
513    in_fragment: bool,
514    /// Reconstructed NAL header byte for the in-progress FU-A NAL.
515    fua_header: u8,
516    /// Timestamp of the access unit currently being assembled.
517    current_ts: Option<u32>,
518    /// Last sequence number seen (for gap detection during fragmentation).
519    last_seq: Option<u16>,
520}
521
522impl H264Depacketizer {
523    /// A fresh depacketizer with no in-progress access unit.
524    pub fn new() -> Self {
525        Self::default()
526    }
527
528    /// Append one NAL unit (Annex-B framed) to the current access unit.
529    fn append_nal(&mut self, nal: &[u8]) {
530        self.au.extend_from_slice(&ANNEXB_START);
531        self.au.extend_from_slice(nal);
532    }
533
534    /// Whether the pending access unit holds an IDR (type 5) NAL — a keyframe.
535    fn pending_is_keyframe(&self) -> bool {
536        // Scan the assembled Annex-B for a NAL header with type 5.
537        let mut i = 0;
538        while i + 4 < self.au.len() {
539            if self.au[i..i + 4] == ANNEXB_START {
540                let nal_type = self.au[i + 4] & 0x1F;
541                if nal_type == 5 {
542                    return true;
543                }
544            }
545            i += 1;
546        }
547        false
548    }
549
550    /// Emit and reset the pending access unit, if any.
551    fn take_au(&mut self) -> Option<AccessUnit> {
552        if self.au.is_empty() {
553            return None;
554        }
555        let keyframe = self.pending_is_keyframe();
556        let timestamp = self.current_ts.unwrap_or(0);
557        let data = Bytes::from(std::mem::take(&mut self.au));
558        self.current_ts = None;
559        Some(AccessUnit {
560            data,
561            timestamp,
562            keyframe,
563        })
564    }
565
566    /// Push one RTP H.264 payload. Returns a completed [`AccessUnit`] when the
567    /// marker bit closes the frame (or the timestamp advances to a new one).
568    pub fn push(
569        &mut self,
570        payload: &[u8],
571        marker: bool,
572        timestamp: u32,
573        sequence: u16,
574    ) -> Result<Option<AccessUnit>, DepacketizeError> {
575        if payload.is_empty() {
576            return Err(DepacketizeError::Truncated);
577        }
578
579        // A timestamp change flushes the previous access unit before starting the
580        // new one (some encoders omit the marker bit).
581        let mut completed = None;
582        if let Some(ts) = self.current_ts {
583            if ts != timestamp && !self.in_fragment {
584                completed = self.take_au();
585            }
586        }
587        self.current_ts = Some(timestamp);
588
589        let nal_type = payload[0] & 0x1F;
590        match nal_type {
591            1..=23 => {
592                // Single NAL unit packet — the payload *is* the NAL.
593                self.append_nal(payload);
594            }
595            24 => {
596                // STAP-A: one byte type, then [u16 size][nal]… aggregates.
597                let mut i = 1;
598                while i + 2 <= payload.len() {
599                    let size = u16::from_be_bytes([payload[i], payload[i + 1]]) as usize;
600                    i += 2;
601                    if i + size > payload.len() {
602                        return Err(DepacketizeError::Truncated);
603                    }
604                    self.append_nal(&payload[i..i + size]);
605                    i += size;
606                }
607            }
608            28 => {
609                // FU-A: byte0 = FU indicator, byte1 = FU header (S|E|R|type).
610                if payload.len() < 2 {
611                    return Err(DepacketizeError::Truncated);
612                }
613                let fu_header = payload[1];
614                let start = fu_header & 0x80 != 0;
615                let end = fu_header & 0x40 != 0;
616                let frag_type = fu_header & 0x1F;
617
618                if start {
619                    // Reconstruct the original NAL header: F|NRI from the indicator,
620                    // type from the FU header.
621                    self.fua_header = (payload[0] & 0xE0) | frag_type;
622                    self.fua.clear();
623                    self.fua.push(self.fua_header);
624                    self.in_fragment = true;
625                } else if !self.in_fragment {
626                    // Mid/last fragment with no start — lost the head.
627                    return Err(DepacketizeError::OutOfOrder);
628                } else if self.seq_gap(sequence) {
629                    self.in_fragment = false;
630                    self.fua.clear();
631                    return Err(DepacketizeError::OutOfOrder);
632                }
633                self.fua.extend_from_slice(&payload[2..]);
634
635                if end && self.in_fragment {
636                    let nal = std::mem::take(&mut self.fua);
637                    self.append_nal(&nal);
638                    self.in_fragment = false;
639                }
640            }
641            other => return Err(DepacketizeError::Unsupported(other)),
642        }
643
644        self.last_seq = Some(sequence);
645
646        if completed.is_some() {
647            return Ok(completed);
648        }
649        if marker {
650            return Ok(self.take_au());
651        }
652        Ok(None)
653    }
654
655    /// Detect a one-step sequence-number gap relative to the previous packet.
656    fn seq_gap(&self, sequence: u16) -> bool {
657        match self.last_seq {
658            Some(prev) => sequence.wrapping_sub(prev) != 1,
659            None => false,
660        }
661    }
662}
663
664/// Reassembles RFC 7798 H.265 (HEVC) RTP payloads into Annex-B access units.
665///
666/// The H.265 counterpart to [`H264Depacketizer`], used for ingesting HEVC IP
667/// cameras and encoders over RTSP/WebRTC. It handles the three packetization
668/// modes: single NAL units, aggregation packets (AP, type 48), and fragmentation
669/// units (FU, type 49). The output shape — NAL units each prefixed with a 4-byte
670/// Annex-B start code — matches [`H264Depacketizer`] and the codec parsers.
671///
672/// DONL/DOND fields (only present when `sprop-max-don-diff > 0` is negotiated in
673/// SDP) are not consumed; the common single-stream profile does not use them.
674#[derive(Debug, Default)]
675pub struct H265Depacketizer {
676    /// Bytes accumulated for the current access unit (Annex-B framed).
677    au: Vec<u8>,
678    /// FU reassembly buffer for the NAL currently being defragmented.
679    fu: Vec<u8>,
680    /// `true` while an FU is in progress (between Start and End bits).
681    in_fragment: bool,
682    /// Timestamp of the access unit currently being assembled.
683    current_ts: Option<u32>,
684    /// Last sequence number seen (for gap detection during fragmentation).
685    last_seq: Option<u16>,
686}
687
688impl H265Depacketizer {
689    /// A fresh depacketizer with no in-progress access unit.
690    pub fn new() -> Self {
691        Self::default()
692    }
693
694    /// Append one NAL unit (Annex-B framed) to the current access unit.
695    fn append_nal(&mut self, nal: &[u8]) {
696        self.au.extend_from_slice(&ANNEXB_START);
697        self.au.extend_from_slice(nal);
698    }
699
700    /// Whether the pending access unit holds an IRAP (BLA/IDR/CRA, types 16–23)
701    /// VCL NAL — i.e. a random-access point / keyframe.
702    fn pending_is_keyframe(&self) -> bool {
703        let mut i = 0;
704        while i + 4 < self.au.len() {
705            if self.au[i..i + 4] == ANNEXB_START {
706                let nal_type = (self.au[i + 4] >> 1) & 0x3F;
707                if (16..=23).contains(&nal_type) {
708                    return true;
709                }
710            }
711            i += 1;
712        }
713        false
714    }
715
716    /// Emit and reset the pending access unit, if any.
717    fn take_au(&mut self) -> Option<AccessUnit> {
718        if self.au.is_empty() {
719            return None;
720        }
721        let keyframe = self.pending_is_keyframe();
722        let timestamp = self.current_ts.unwrap_or(0);
723        let data = Bytes::from(std::mem::take(&mut self.au));
724        self.current_ts = None;
725        Some(AccessUnit {
726            data,
727            timestamp,
728            keyframe,
729        })
730    }
731
732    /// Detect a one-step sequence-number gap relative to the previous packet.
733    fn seq_gap(&self, sequence: u16) -> bool {
734        match self.last_seq {
735            Some(prev) => sequence.wrapping_sub(prev) != 1,
736            None => false,
737        }
738    }
739
740    /// Push one RTP H.265 payload. Returns a completed [`AccessUnit`] when the
741    /// marker bit closes the frame (or the timestamp advances to a new one).
742    pub fn push(
743        &mut self,
744        payload: &[u8],
745        marker: bool,
746        timestamp: u32,
747        sequence: u16,
748    ) -> Result<Option<AccessUnit>, DepacketizeError> {
749        // The H.265 NAL header is two bytes; a single byte cannot carry a type.
750        if payload.len() < 2 {
751            return Err(DepacketizeError::Truncated);
752        }
753
754        // A timestamp change flushes the previous access unit (some encoders omit
755        // the marker bit).
756        let mut completed = None;
757        if let Some(ts) = self.current_ts {
758            if ts != timestamp && !self.in_fragment {
759                completed = self.take_au();
760            }
761        }
762        self.current_ts = Some(timestamp);
763
764        let nal_type = (payload[0] >> 1) & 0x3F;
765        match nal_type {
766            // Single NAL unit packet — the payload *is* the NAL (header included).
767            0..=47 => self.append_nal(payload),
768            48 => {
769                // AP: 2-byte header, then [u16 size][nal]… aggregates.
770                let mut i = 2;
771                while i + 2 <= payload.len() {
772                    let size = u16::from_be_bytes([payload[i], payload[i + 1]]) as usize;
773                    i += 2;
774                    if i + size > payload.len() {
775                        return Err(DepacketizeError::Truncated);
776                    }
777                    self.append_nal(&payload[i..i + size]);
778                    i += size;
779                }
780            }
781            49 => {
782                // FU: 2-byte PayloadHdr, 1-byte FU header (S|E|FuType), then body.
783                if payload.len() < 3 {
784                    return Err(DepacketizeError::Truncated);
785                }
786                let fu_header = payload[2];
787                let start = fu_header & 0x80 != 0;
788                let end = fu_header & 0x40 != 0;
789                let fu_type = fu_header & 0x3F;
790
791                if start {
792                    // Reconstruct the original 2-byte NAL header: restore the type
793                    // field (bits 9..14) from FuType, keep F/LayerId/TID.
794                    let hdr0 = (payload[0] & 0x81) | (fu_type << 1);
795                    let hdr1 = payload[1];
796                    self.fu.clear();
797                    self.fu.push(hdr0);
798                    self.fu.push(hdr1);
799                    self.in_fragment = true;
800                } else if !self.in_fragment {
801                    return Err(DepacketizeError::OutOfOrder);
802                } else if self.seq_gap(sequence) {
803                    self.in_fragment = false;
804                    self.fu.clear();
805                    return Err(DepacketizeError::OutOfOrder);
806                }
807                self.fu.extend_from_slice(&payload[3..]);
808
809                if end && self.in_fragment {
810                    let nal = std::mem::take(&mut self.fu);
811                    self.append_nal(&nal);
812                    self.in_fragment = false;
813                }
814            }
815            other => return Err(DepacketizeError::Unsupported(other)),
816        }
817
818        self.last_seq = Some(sequence);
819
820        if completed.is_some() {
821            return Ok(completed);
822        }
823        if marker {
824            return Ok(self.take_au());
825        }
826        Ok(None)
827    }
828}
829
830/// A reassembled coded video frame from the RTP bus.
831///
832/// For the NAL codecs (H.264/H.265) `data` is the access unit in Annex-B form
833/// (each NAL prefixed with a 4-byte start code); for VP9/AV1 it is the raw coded
834/// frame / temporal unit. `keyframe` marks a decodable random-access point.
835#[derive(Debug, Clone, PartialEq, Eq)]
836pub struct AccessUnit {
837    /// The coded frame bytes (Annex-B NALs for H.26x; raw frame for VP9/AV1).
838    pub data: Bytes,
839    /// RTP media timestamp (90 kHz) of the frame.
840    pub timestamp: u32,
841    /// Whether this is a keyframe / random-access point.
842    pub keyframe: bool,
843}
844
845/// Write the 12-byte RTP fixed header (V=2, no padding/extension/CSRC) for one
846/// packet. Shared by every packetizer in this module.
847fn write_rtp_header(out: &mut Vec<u8>, pt: u8, marker: bool, seq: u16, ts: u32, ssrc: u32) {
848    out.push(0x80); // V=2, P=0, X=0, CC=0
849    out.push(if marker { 0x80 } else { 0 } | (pt & 0x7F));
850    out.extend_from_slice(&seq.to_be_bytes());
851    out.extend_from_slice(&ts.to_be_bytes());
852    out.extend_from_slice(&ssrc.to_be_bytes());
853}
854
855// ── Opus (RFC 7587) ──────────────────────────────────────────────────────────
856
857/// Packetizes Opus audio into RTP: each Opus packet is carried verbatim as the
858/// payload of one RTP packet (RFC 7587 — Opus is self-delimiting, so there is no
859/// payload descriptor). The 48 kHz media clock means the caller passes a
860/// timestamp already scaled to 48 kHz.
861#[derive(Debug, Clone)]
862pub struct OpusPacketizer {
863    payload_type: u8,
864    ssrc: u32,
865    sequence: u16,
866}
867
868impl OpusPacketizer {
869    /// An Opus packetizer for `payload_type`/`ssrc`.
870    pub fn new(payload_type: u8, ssrc: u32) -> Self {
871        Self {
872            payload_type,
873            ssrc,
874            sequence: 0,
875        }
876    }
877
878    /// Packetize one Opus frame at `timestamp` (48 kHz) into a single RTP packet,
879    /// appended to `out` (recycling a buffer it already holds). The RTP marker is
880    /// left clear — continuous audio is not a talkspurt boundary.
881    pub fn packetize_into(&mut self, opus: &[u8], timestamp: u32, out: &mut Vec<Vec<u8>>) {
882        let mut recycle = std::mem::take(out);
883        let mut pkt = recycle.pop().unwrap_or_default();
884        pkt.clear();
885        write_rtp_header(
886            &mut pkt,
887            self.payload_type,
888            false,
889            self.sequence,
890            timestamp,
891            self.ssrc,
892        );
893        self.sequence = self.sequence.wrapping_add(1);
894        pkt.extend_from_slice(opus);
895        out.push(pkt);
896    }
897}
898
899// ── VP9 (draft-ietf-payload-vp9) ─────────────────────────────────────────────
900
901/// Packetizes VP9 coded frames into RTP, using a flexible-mode-off payload
902/// descriptor for the common single-layer (non-scalable) case.
903///
904/// Each frame is carried verbatim after a VP9 payload descriptor (the bytes are
905/// not transformed — VP9 RTP carries the frame opaquely), split across the MTU
906/// with the B (begin) bit on the first packet and E (end) + RTP marker on the
907/// last. A 15-bit picture ID increments per frame. Spatial/temporal scalability
908/// and flexible mode are out of scope.
909#[derive(Debug, Clone)]
910pub struct Vp9Packetizer {
911    payload_type: u8,
912    ssrc: u32,
913    sequence: u16,
914    max_payload: usize,
915    picture_id: u16,
916}
917
918impl Vp9Packetizer {
919    /// A VP9 packetizer for `payload_type`/`ssrc`. `mtu` is the maximum UDP
920    /// payload; the 12-byte RTP header and a 3-byte descriptor are subtracted.
921    pub fn new(payload_type: u8, ssrc: u32, mtu: usize) -> Self {
922        Self {
923            payload_type,
924            ssrc,
925            sequence: 0,
926            // 12-byte RTP header + up to 3-byte descriptor (1 flags + 2 picture id).
927            max_payload: mtu.saturating_sub(12 + 3).max(1),
928            picture_id: 0,
929        }
930    }
931
932    /// Packetize one VP9 frame at `timestamp` (90 kHz). `keyframe` clears the P
933    /// (inter-predicted) bit so receivers can identify random-access points.
934    pub fn packetize(&mut self, frame: &[u8], timestamp: u32, keyframe: bool) -> Vec<Vec<u8>> {
935        let mut out = Vec::new();
936        self.packetize_into(frame, timestamp, keyframe, &mut out);
937        out
938    }
939
940    /// Recycling variant of [`packetize`](Self::packetize): pass the same `out`
941    /// every frame to reuse the packet-buffer allocations across frames.
942    pub fn packetize_into(
943        &mut self,
944        frame: &[u8],
945        timestamp: u32,
946        keyframe: bool,
947        out: &mut Vec<Vec<u8>>,
948    ) {
949        let pid = self.picture_id & 0x7FFF;
950        self.picture_id = self.picture_id.wrapping_add(1);
951
952        let mut recycle = std::mem::take(out);
953        let chunks: Vec<&[u8]> = if frame.is_empty() {
954            vec![&[]]
955        } else {
956            frame.chunks(self.max_payload).collect()
957        };
958        let n = chunks.len();
959        for (i, chunk) in chunks.into_iter().enumerate() {
960            let begin = i == 0;
961            let end = i + 1 == n;
962            let mut pkt = recycle.pop().unwrap_or_default();
963            pkt.clear();
964            write_rtp_header(
965                &mut pkt,
966                self.payload_type,
967                end,
968                self.sequence,
969                timestamp,
970                self.ssrc,
971            );
972            self.sequence = self.sequence.wrapping_add(1);
973
974            // Descriptor octet: I=1, P=!keyframe, L=0, F=0, B, E, V=0, Z=0.
975            let mut desc0 = 0x80; // I = 1 (picture ID present)
976            if !keyframe {
977                desc0 |= 0x40; // P (inter-predicted)
978            }
979            if begin {
980                desc0 |= 0x08; // B (start of frame)
981            }
982            if end {
983                desc0 |= 0x04; // E (end of frame)
984            }
985            pkt.push(desc0);
986            // 15-bit picture ID (M=1): 0x80|hi, lo.
987            pkt.push(0x80 | (pid >> 8) as u8);
988            pkt.push((pid & 0xFF) as u8);
989            pkt.extend_from_slice(chunk);
990            out.push(pkt);
991        }
992    }
993}
994
995/// Reassembles VP9 RTP payloads (draft-ietf-payload-vp9, non-flexible single
996/// layer) into coded frames. Counterpart to [`Vp9Packetizer`].
997#[derive(Debug, Default)]
998pub struct Vp9Depacketizer {
999    frame: Vec<u8>,
1000    in_frame: bool,
1001    keyframe: bool,
1002    current_ts: Option<u32>,
1003}
1004
1005impl Vp9Depacketizer {
1006    /// A fresh depacketizer with no in-progress frame.
1007    pub fn new() -> Self {
1008        Self::default()
1009    }
1010
1011    /// Push one VP9 RTP payload. Returns a completed frame when the E (end) bit
1012    /// and RTP marker close it.
1013    pub fn push(
1014        &mut self,
1015        payload: &[u8],
1016        marker: bool,
1017        timestamp: u32,
1018    ) -> Result<Option<AccessUnit>, DepacketizeError> {
1019        if payload.is_empty() {
1020            return Err(DepacketizeError::Truncated);
1021        }
1022        let desc0 = payload[0];
1023        let has_pid = desc0 & 0x80 != 0;
1024        let has_layer = desc0 & 0x20 != 0;
1025        let flexible = desc0 & 0x10 != 0;
1026        let begin = desc0 & 0x08 != 0;
1027        let end = desc0 & 0x04 != 0;
1028        let predicted = desc0 & 0x40 != 0;
1029
1030        // Walk past the variable-length descriptor fields we recognize.
1031        let mut off = 1;
1032        if has_pid {
1033            // M bit selects a 1- or 2-byte picture ID.
1034            let m = payload.get(off).ok_or(DepacketizeError::Truncated)? & 0x80 != 0;
1035            off += if m { 2 } else { 1 };
1036        }
1037        if has_layer {
1038            off += 1; // TID/U/SID/D byte
1039            if !flexible {
1040                off += 1; // TL0PICIDX (non-flexible mode)
1041            }
1042        }
1043        if off > payload.len() {
1044            return Err(DepacketizeError::Truncated);
1045        }
1046
1047        if begin {
1048            self.frame.clear();
1049            self.in_frame = true;
1050            self.keyframe = !predicted;
1051            self.current_ts = Some(timestamp);
1052        } else if !self.in_frame {
1053            return Err(DepacketizeError::OutOfOrder);
1054        }
1055        self.frame.extend_from_slice(&payload[off..]);
1056
1057        if end && marker && self.in_frame {
1058            self.in_frame = false;
1059            return Ok(Some(AccessUnit {
1060                data: Bytes::from(std::mem::take(&mut self.frame)),
1061                timestamp: self.current_ts.unwrap_or(timestamp),
1062                keyframe: self.keyframe,
1063            }));
1064        }
1065        Ok(None)
1066    }
1067}
1068
1069// ── AV1 (AOMedia "RTP Payload Format For AV1") ───────────────────────────────
1070
1071/// Encode `v` as unsigned LEB128 into `out`.
1072#[cfg(feature = "codec-av1")]
1073fn leb128_encode(mut v: u64, out: &mut Vec<u8>) {
1074    loop {
1075        let mut byte = (v & 0x7F) as u8;
1076        v >>= 7;
1077        if v != 0 {
1078            byte |= 0x80;
1079        }
1080        out.push(byte);
1081        if v == 0 {
1082            break;
1083        }
1084    }
1085}
1086
1087#[cfg(feature = "codec-av1")]
1088const AV1_OBU_SEQUENCE_HEADER: u8 = 1;
1089#[cfg(feature = "codec-av1")]
1090const AV1_OBU_TEMPORAL_DELIMITER: u8 = 2;
1091
1092/// Packetizes an AV1 temporal unit into RTP using the AOMedia payload format.
1093///
1094/// Each non-temporal-delimiter OBU is re-framed as a length-delimited *OBU
1095/// element* (the W=0 form, with the OBU's `obu_has_size_field` cleared), the
1096/// elements are concatenated, and the resulting stream is split across the MTU
1097/// with a one-byte aggregation header per packet (Z/Y continuation bits, N on a
1098/// new coded video sequence). The temporal delimiter is dropped per the spec;
1099/// frame boundaries are conveyed by the RTP marker. Scalability structures are
1100/// not emitted.
1101#[cfg(feature = "codec-av1")]
1102#[derive(Debug, Clone)]
1103pub struct Av1Packetizer {
1104    payload_type: u8,
1105    ssrc: u32,
1106    sequence: u16,
1107    max_payload: usize,
1108}
1109
1110#[cfg(feature = "codec-av1")]
1111impl Av1Packetizer {
1112    /// An AV1 packetizer for `payload_type`/`ssrc`. `mtu` is the maximum UDP
1113    /// payload; the 12-byte RTP header and 1-byte aggregation header are removed.
1114    pub fn new(payload_type: u8, ssrc: u32, mtu: usize) -> Self {
1115        Self {
1116            payload_type,
1117            ssrc,
1118            sequence: 0,
1119            max_payload: mtu.saturating_sub(12 + 1).max(1),
1120        }
1121    }
1122
1123    /// Packetize one AV1 temporal unit (low-overhead OBUs) at `timestamp`.
1124    pub fn packetize(&mut self, temporal_unit: &[u8], timestamp: u32) -> Vec<Vec<u8>> {
1125        let mut out = Vec::new();
1126        self.packetize_into(temporal_unit, timestamp, &mut out);
1127        out
1128    }
1129
1130    /// Recycling variant of [`packetize`](Self::packetize): pass the same `out`
1131    /// every temporal unit to reuse the packet-buffer allocations.
1132    pub fn packetize_into(&mut self, temporal_unit: &[u8], timestamp: u32, out: &mut Vec<Vec<u8>>) {
1133        // Re-frame each OBU (minus the temporal delimiter) as a length-delimited
1134        // OBU element with obu_has_size_field cleared.
1135        let mut stream = Vec::with_capacity(temporal_unit.len());
1136        let mut new_cvs = false;
1137        for obu in crate::codec::obu::iter_obus(temporal_unit) {
1138            if obu.obu_type == AV1_OBU_TEMPORAL_DELIMITER {
1139                continue;
1140            }
1141            if obu.obu_type == AV1_OBU_SEQUENCE_HEADER {
1142                new_cvs = true;
1143            }
1144            let header_len = 1 + obu.has_extension as usize;
1145            let mut element = Vec::with_capacity(header_len + obu.payload.len());
1146            element.push(obu.raw[0] & !0x02); // clear obu_has_size_field
1147            if obu.has_extension {
1148                element.push(obu.raw[1]);
1149            }
1150            element.extend_from_slice(obu.payload);
1151            leb128_encode(element.len() as u64, &mut stream);
1152            stream.extend_from_slice(&element);
1153        }
1154
1155        let mut recycle = std::mem::take(out);
1156        let chunks: Vec<&[u8]> = if stream.is_empty() {
1157            vec![&[]]
1158        } else {
1159            stream.chunks(self.max_payload).collect()
1160        };
1161        let n = chunks.len();
1162        for (i, chunk) in chunks.into_iter().enumerate() {
1163            let last = i + 1 == n;
1164            let mut pkt = recycle.pop().unwrap_or_default();
1165            pkt.clear();
1166            write_rtp_header(
1167                &mut pkt,
1168                self.payload_type,
1169                last,
1170                self.sequence,
1171                timestamp,
1172                self.ssrc,
1173            );
1174            self.sequence = self.sequence.wrapping_add(1);
1175
1176            // Aggregation header: Z (continues previous packet) | Y (continues in
1177            // next) | W=0 (length-delimited elements) | N (new coded video seq).
1178            let mut agg = 0u8;
1179            if i > 0 {
1180                agg |= 0x80; // Z
1181            }
1182            if !last {
1183                agg |= 0x40; // Y
1184            }
1185            if i == 0 && new_cvs {
1186                agg |= 0x08; // N
1187            }
1188            pkt.push(agg);
1189            pkt.extend_from_slice(chunk);
1190            out.push(pkt);
1191        }
1192    }
1193}
1194
1195/// Reassembles AV1 RTP payloads into temporal units. Counterpart to
1196/// [`Av1Packetizer`]: it concatenates each packet's OBU-element bytes (past the
1197/// aggregation header) and, on the RTP marker, parses the length-delimited
1198/// elements back into low-overhead OBUs (re-adding each `obu_has_size_field`).
1199#[cfg(feature = "codec-av1")]
1200#[derive(Debug, Default)]
1201pub struct Av1Depacketizer {
1202    stream: Vec<u8>,
1203    new_cvs: bool,
1204    current_ts: Option<u32>,
1205}
1206
1207#[cfg(feature = "codec-av1")]
1208impl Av1Depacketizer {
1209    /// A fresh depacketizer with no in-progress temporal unit.
1210    pub fn new() -> Self {
1211        Self::default()
1212    }
1213
1214    /// Push one AV1 RTP payload. Returns a completed temporal unit when the RTP
1215    /// marker closes it.
1216    pub fn push(
1217        &mut self,
1218        payload: &[u8],
1219        marker: bool,
1220        timestamp: u32,
1221    ) -> Result<Option<AccessUnit>, DepacketizeError> {
1222        if payload.is_empty() {
1223            return Err(DepacketizeError::Truncated);
1224        }
1225        let agg = payload[0];
1226        if agg & 0x08 != 0 {
1227            self.new_cvs = true; // N: new coded video sequence
1228        }
1229        if self.current_ts.is_none() {
1230            self.current_ts = Some(timestamp);
1231        }
1232        self.stream.extend_from_slice(&payload[1..]);
1233
1234        if !marker {
1235            return Ok(None);
1236        }
1237
1238        // Marker: rebuild the temporal unit from length-delimited OBU elements.
1239        let stream = std::mem::take(&mut self.stream);
1240        let mut tu = Vec::with_capacity(stream.len() + 8);
1241        let mut pos = 0;
1242        while pos < stream.len() {
1243            let len = leb128_decode(&stream, &mut pos).ok_or(DepacketizeError::Truncated)?;
1244            let end = pos.checked_add(len).ok_or(DepacketizeError::Truncated)?;
1245            let element = stream.get(pos..end).ok_or(DepacketizeError::Truncated)?;
1246            pos = end;
1247            // Element → low-overhead OBU: set obu_has_size_field, insert the size.
1248            let hdr0 = *element.first().ok_or(DepacketizeError::Truncated)?;
1249            let has_ext = (hdr0 >> 2) & 1 == 1;
1250            let header_len = 1 + has_ext as usize;
1251            let obu_payload = element
1252                .get(header_len..)
1253                .ok_or(DepacketizeError::Truncated)?;
1254            tu.push(hdr0 | 0x02);
1255            if has_ext {
1256                tu.push(element[1]);
1257            }
1258            leb128_encode(obu_payload.len() as u64, &mut tu);
1259            tu.extend_from_slice(obu_payload);
1260        }
1261
1262        let keyframe = std::mem::take(&mut self.new_cvs);
1263        let ts = self.current_ts.take().unwrap_or(timestamp);
1264        Ok(Some(AccessUnit {
1265            data: Bytes::from(tu),
1266            timestamp: ts,
1267            keyframe,
1268        }))
1269    }
1270}
1271
1272/// Decode an unsigned LEB128 integer at `*pos` (advancing it), returning `None`
1273/// on truncation or overflow. Mirrors the codec-side decoder for RTP carriage.
1274#[cfg(feature = "codec-av1")]
1275fn leb128_decode(data: &[u8], pos: &mut usize) -> Option<usize> {
1276    let mut value: u64 = 0;
1277    for i in 0..8 {
1278        let byte = *data.get(*pos)?;
1279        *pos += 1;
1280        value |= ((byte & 0x7F) as u64) << (i * 7);
1281        if byte & 0x80 == 0 {
1282            return usize::try_from(value).ok();
1283        }
1284    }
1285    None
1286}
1287
1288#[cfg(test)]
1289mod tests {
1290    use super::*;
1291
1292    /// Build a minimal 12-byte RTP packet with the given fields and payload.
1293    fn rtp(seq: u16, ts: u32, marker: bool, payload: &[u8]) -> Vec<u8> {
1294        let mut p = vec![0x80, if marker { 0x80 | 96 } else { 96 }];
1295        p.extend_from_slice(&seq.to_be_bytes());
1296        p.extend_from_slice(&ts.to_be_bytes());
1297        p.extend_from_slice(&[0, 0, 0, 1]); // ssrc
1298        p.extend_from_slice(payload);
1299        p
1300    }
1301
1302    #[test]
1303    fn parses_fixed_header_and_payload_offset() {
1304        let pkt = rtp(7, 9000, true, &[0x65, 0xAA]);
1305        let h = RtpHeader::parse(&pkt).unwrap();
1306        assert_eq!(h.sequence, 7);
1307        assert_eq!(h.timestamp, 9000);
1308        assert!(h.marker);
1309        assert_eq!(h.payload_type, 96);
1310        assert_eq!(h.payload_offset, 12);
1311        assert_eq!(&pkt[h.payload_offset..], &[0x65, 0xAA]);
1312    }
1313
1314    #[test]
1315    fn rejects_wrong_version_and_short_buffers() {
1316        assert!(RtpHeader::parse(&[0x00; 12]).is_none()); // version 0
1317        assert!(RtpHeader::parse(&[0x80; 4]).is_none()); // too short
1318    }
1319
1320    /// Build an RTP packet carrying a one-byte (RFC 8285) header extension with a
1321    /// single element `(ext_id, value)`.
1322    fn rtp_with_ext(ext_id: u8, value: &[u8], payload: &[u8]) -> Vec<u8> {
1323        let mut p = vec![0x90, 96]; // X bit set (0x10), PT 96
1324        p.extend_from_slice(&1u16.to_be_bytes()); // seq
1325        p.extend_from_slice(&0u32.to_be_bytes()); // ts
1326        p.extend_from_slice(&7u32.to_be_bytes()); // ssrc
1327        p.extend_from_slice(&0xBEDEu16.to_be_bytes()); // one-byte profile
1328                                                       // One element + padding to a 4-byte word boundary.
1329        let mut ext = vec![(ext_id << 4) | (value.len() as u8 - 1)];
1330        ext.extend_from_slice(value);
1331        while ext.len() % 4 != 0 {
1332            ext.push(0);
1333        }
1334        p.extend_from_slice(&((ext.len() / 4) as u16).to_be_bytes());
1335        p.extend_from_slice(&ext);
1336        p.extend_from_slice(payload);
1337        p
1338    }
1339
1340    #[test]
1341    fn extracts_rid_header_extension() {
1342        let pkt = rtp_with_ext(4, b"hi", &[0xAA, 0xBB]);
1343        assert_eq!(rtp_extension_value(&pkt, 4), Some(&b"hi"[..]));
1344        assert_eq!(rtp_extension_value(&pkt, 5), None, "unknown id");
1345        // Payload still locates correctly past the extension.
1346        let h = RtpHeader::parse(&pkt).unwrap();
1347        assert_eq!(&pkt[h.payload_offset..], &[0xAA, 0xBB]);
1348    }
1349
1350    #[test]
1351    fn extension_value_none_without_extension_flag() {
1352        let pkt = rtp(1, 0, false, &[1, 2, 3]);
1353        assert_eq!(rtp_extension_value(&pkt, 4), None);
1354    }
1355
1356    #[test]
1357    fn decodes_rfc6464_audio_level() {
1358        // Voice active (MSB set) at level 20 -dBov.
1359        let pkt = rtp_with_ext(3, &[0x80 | 20], &[0xAA]);
1360        assert_eq!(audio_level(&pkt, 3), Some((20, true)));
1361        // Voice inactive (MSB clear) at level 90 -dBov (quiet).
1362        let pkt = rtp_with_ext(3, &[90], &[0xAA]);
1363        assert_eq!(audio_level(&pkt, 3), Some((90, false)));
1364        // No such extension id.
1365        assert_eq!(audio_level(&pkt, 7), None);
1366    }
1367
1368    #[test]
1369    fn honors_csrc_count_in_payload_offset() {
1370        let mut pkt = rtp(1, 0, false, &[0x41]);
1371        pkt[0] = 0x82; // version 2, CSRC count = 2
1372        let mut with_csrc = pkt[..12].to_vec();
1373        with_csrc.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0]); // 2 CSRCs
1374        with_csrc.push(0x41);
1375        let h = RtpHeader::parse(&with_csrc).unwrap();
1376        assert_eq!(h.payload_offset, 20);
1377    }
1378
1379    #[test]
1380    fn aac_hbr_splits_two_access_units() {
1381        // AU-headers-length = 32 bits → two 16-bit AU-headers.
1382        // AU sizes 3 and 2 (top 13 bits of each 2-byte header).
1383        let mut p = Vec::new();
1384        p.extend_from_slice(&32u16.to_be_bytes()); // header bits
1385        p.extend_from_slice(&((3u16) << 3).to_be_bytes()); // AU-header: size 3
1386        p.extend_from_slice(&((2u16) << 3).to_be_bytes()); // AU-header: size 2
1387        p.extend_from_slice(&[0xA1, 0xA2, 0xA3]); // AU 1
1388        p.extend_from_slice(&[0xB1, 0xB2]); // AU 2
1389        let aus = AacDepacketizer::new().push(&p).unwrap();
1390        assert_eq!(aus.len(), 2);
1391        assert_eq!(&aus[0][..], &[0xA1, 0xA2, 0xA3]);
1392        assert_eq!(&aus[1][..], &[0xB1, 0xB2]);
1393    }
1394
1395    #[test]
1396    fn aac_hbr_single_au() {
1397        let mut p = Vec::new();
1398        p.extend_from_slice(&16u16.to_be_bytes()); // one 16-bit AU-header
1399        p.extend_from_slice(&((4u16) << 3).to_be_bytes()); // size 4
1400        p.extend_from_slice(&[1, 2, 3, 4]);
1401        let aus = AacDepacketizer::new().push(&p).unwrap();
1402        assert_eq!(aus.len(), 1);
1403        assert_eq!(&aus[0][..], &[1, 2, 3, 4]);
1404    }
1405
1406    #[test]
1407    fn aac_truncated_payload_errors() {
1408        assert_eq!(
1409            AacDepacketizer::new().push(&[0x00]),
1410            Err(DepacketizeError::Truncated)
1411        );
1412        // Declares one AU of size 8 but supplies only 2 data bytes.
1413        let mut p = 16u16.to_be_bytes().to_vec();
1414        p.extend_from_slice(&((8u16) << 3).to_be_bytes());
1415        p.extend_from_slice(&[1, 2]);
1416        assert_eq!(
1417            AacDepacketizer::new().push(&p),
1418            Err(DepacketizeError::Truncated)
1419        );
1420    }
1421
1422    #[test]
1423    fn single_nal_packet_emits_annexb_on_marker() {
1424        let mut d = H264Depacketizer::new();
1425        // Type 1 (non-IDR slice), marker set → one access unit.
1426        let out = d.push(&[0x41, 0x9A, 0xBC], true, 3000, 1).unwrap().unwrap();
1427        assert_eq!(&out.data[..], &[0, 0, 0, 1, 0x41, 0x9A, 0xBC]);
1428        assert!(!out.keyframe);
1429        assert_eq!(out.timestamp, 3000);
1430    }
1431
1432    #[test]
1433    fn idr_single_nal_is_flagged_keyframe() {
1434        let mut d = H264Depacketizer::new();
1435        let out = d.push(&[0x65, 0x01], true, 0, 1).unwrap().unwrap();
1436        assert!(out.keyframe);
1437    }
1438
1439    #[test]
1440    fn packetizer_single_nal_round_trips_through_depacketizer() {
1441        // A small AU (two NALs) → single-NAL packets → reassembled identically.
1442        let au = [0, 0, 0, 1, 0x67, 0x42, 0x00, 0, 0, 0, 1, 0x65, 0x88, 0x99];
1443        let mut pkt = RtpPacketizer::new(96, 0xABCD, 1200);
1444        let packets = pkt.packetize(&au, 3000);
1445        assert_eq!(packets.len(), 2, "one packet per NAL");
1446
1447        let mut depack = H264Depacketizer::new();
1448        let mut out = None;
1449        for p in &packets {
1450            let h = RtpHeader::parse(p).unwrap();
1451            if let Some(au) = depack
1452                .push(&p[h.payload_offset..], h.marker, h.timestamp, h.sequence)
1453                .unwrap()
1454            {
1455                out = Some(au);
1456            }
1457        }
1458        let out = out.expect("AU completed on the marker packet");
1459        assert_eq!(&out.data[..], &au);
1460        assert!(out.keyframe);
1461        assert_eq!(out.timestamp, 3000);
1462    }
1463
1464    #[test]
1465    fn packetize_into_recycles_buffers_without_changing_output() {
1466        // The recycling hot-path API must produce byte-identical packets to the
1467        // allocating `packetize`, frame after frame, including correct sequence
1468        // numbers carried across the reused buffer.
1469        let au1 = [0, 0, 0, 1, 0x67, 0x42, 0x00, 0, 0, 0, 1, 0x65, 0x88, 0x99];
1470        let au2 = [0, 0, 0, 1, 0x65, 0x11, 0x22, 0x33];
1471
1472        let mut a = RtpPacketizer::new(96, 0xABCD, 1200);
1473        let mut b = RtpPacketizer::new(96, 0xABCD, 1200);
1474        let mut reused: Vec<Vec<u8>> = Vec::new();
1475
1476        for au in [&au1[..], &au2[..], &au1[..]] {
1477            let expected = a.packetize(au, 3000);
1478            // Capture the backing pointers to prove buffers are reused, not freed.
1479            b.packetize_into(au, 3000, &mut reused);
1480            assert_eq!(
1481                reused, expected,
1482                "recycled output matches allocating output"
1483            );
1484        }
1485    }
1486
1487    #[test]
1488    fn shared_fragment_then_stamp_equals_packetize() {
1489        // The shared-packetization path (`fragment_into` once + per-viewer
1490        // `stamp_into`) must produce byte-for-byte the same wire packets as the
1491        // direct `packetize`, for both single-NAL and FU-fragmented inputs and
1492        // for H.264 and H.265 — so an N-viewer fan-out can fragment once.
1493        let small = [0, 0, 0, 1, 0x67, 0x42, 0x00, 0, 0, 0, 1, 0x65, 0x88, 0x99];
1494        let mut big = vec![0, 0, 0, 1, 0x65];
1495        big.extend((0..600u16).map(|i| i as u8));
1496
1497        for (mk, mtu, au) in [
1498            (
1499                RtpPacketizer::new as fn(u8, u32, usize) -> RtpPacketizer,
1500                1200,
1501                &small[..],
1502            ),
1503            (RtpPacketizer::new, 100, &big[..]), // tiny MTU forces FU-A
1504            (RtpPacketizer::new_h265, 100, &big[..]),
1505        ] {
1506            // Reference: a direct packetizer.
1507            let mut direct = mk(96, 0xABCD, mtu);
1508            let expected = direct.packetize(au, 3000);
1509
1510            // Shared: fragment once (a template packetizer, seq untouched), then a
1511            // separate "viewer" packetizer with the same PT/SSRC stamps each.
1512            let template = mk(96, 0xABCD, mtu);
1513            let mut frags = Vec::new();
1514            template.fragment_into(au, &mut frags);
1515            let mut viewer = mk(96, 0xABCD, mtu);
1516            let mut pkt = Vec::new();
1517            let stamped: Vec<Vec<u8>> = frags
1518                .iter()
1519                .map(|f| {
1520                    viewer.stamp_into(f, 3000, &mut pkt);
1521                    pkt.clone()
1522                })
1523                .collect();
1524
1525            assert_eq!(stamped, expected, "fragment+stamp == packetize (mtu {mtu})");
1526            // The template's sequence number was not advanced by fragmenting.
1527            assert_eq!(template.sequence, 0, "fragment_into is sequence-stateless");
1528        }
1529    }
1530
1531    #[test]
1532    fn packetizer_fragments_oversized_nal_and_round_trips() {
1533        // One NAL larger than the MTU → FU-A fragments → reassembled identically.
1534        let mut nal = vec![0, 0, 0, 1, 0x65]; // start code + IDR NAL header
1535        nal.extend((0..600u16).map(|i| i as u8)); // long payload
1536        let mut pkt = RtpPacketizer::new(96, 1, 100); // tiny MTU forces FU-A
1537        let packets = pkt.packetize(&nal, 90);
1538        assert!(packets.len() > 1, "oversized NAL is fragmented");
1539        // Only the last packet carries the marker bit.
1540        let markers: Vec<bool> = packets
1541            .iter()
1542            .map(|p| RtpHeader::parse(p).unwrap().marker)
1543            .collect();
1544        assert_eq!(markers.iter().filter(|m| **m).count(), 1);
1545        assert!(markers.last().unwrap());
1546
1547        let mut depack = H264Depacketizer::new();
1548        let mut out = None;
1549        for p in &packets {
1550            let h = RtpHeader::parse(p).unwrap();
1551            if let Some(au) = depack
1552                .push(&p[h.payload_offset..], h.marker, h.timestamp, h.sequence)
1553                .unwrap()
1554            {
1555                out = Some(au);
1556            }
1557        }
1558        assert_eq!(&out.unwrap().data[..], &nal[..]);
1559    }
1560
1561    #[test]
1562    fn stap_a_splits_aggregated_nals() {
1563        // STAP-A (24): [24][size=2][AA BB][size=3][CC DD EE]
1564        let payload = [24, 0, 2, 0xAA, 0xBB, 0, 3, 0xCC, 0xDD, 0xEE];
1565        let mut d = H264Depacketizer::new();
1566        let out = d.push(&payload, true, 0, 1).unwrap().unwrap();
1567        assert_eq!(
1568            &out.data[..],
1569            &[0, 0, 0, 1, 0xAA, 0xBB, 0, 0, 0, 1, 0xCC, 0xDD, 0xEE]
1570        );
1571    }
1572
1573    #[test]
1574    fn fu_a_reassembles_fragmented_nal() {
1575        let mut d = H264Depacketizer::new();
1576        // FU indicator 0x7C (F=0,NRI=3,type=28), FU header start 0x85 (S=1,type=5).
1577        assert!(d
1578            .push(&[0x7C, 0x85, 0x11, 0x22], false, 0, 1)
1579            .unwrap()
1580            .is_none());
1581        // Middle fragment (S=0,E=0).
1582        assert!(d.push(&[0x7C, 0x05, 0x33], false, 0, 2).unwrap().is_none());
1583        // End fragment (E=1), marker closes the AU.
1584        let out = d.push(&[0x7C, 0x45, 0x44], true, 0, 3).unwrap().unwrap();
1585        // Reconstructed NAL header: NRI 0x60 | type 5 = 0x65, then payload bytes.
1586        assert_eq!(&out.data[..], &[0, 0, 0, 1, 0x65, 0x11, 0x22, 0x33, 0x44]);
1587        assert!(out.keyframe);
1588    }
1589
1590    #[test]
1591    fn fu_a_sequence_gap_reports_out_of_order() {
1592        let mut d = H264Depacketizer::new();
1593        d.push(&[0x7C, 0x85, 0x11], false, 0, 1).unwrap();
1594        // Jump from seq 1 to seq 5 mid-fragment.
1595        assert_eq!(
1596            d.push(&[0x7C, 0x05, 0x22], false, 0, 5),
1597            Err(DepacketizeError::OutOfOrder)
1598        );
1599    }
1600
1601    #[test]
1602    fn timestamp_change_flushes_previous_au_without_marker() {
1603        let mut d = H264Depacketizer::new();
1604        // First AU, no marker.
1605        assert!(d.push(&[0x41, 0x01], false, 1000, 1).unwrap().is_none());
1606        // New timestamp flushes the first AU.
1607        let out = d.push(&[0x41, 0x02], false, 2000, 2).unwrap().unwrap();
1608        assert_eq!(out.timestamp, 1000);
1609        assert_eq!(&out.data[..], &[0, 0, 0, 1, 0x41, 0x01]);
1610    }
1611
1612    // ── H.265 (RFC 7798) ────────────────────────────────────────────────────
1613    // H.265 NAL headers are two bytes; type = (byte0 >> 1) & 0x3F. Examples used
1614    // below: VPS=32 (0x40,0x01), IDR_W_RADL=19 (0x26,0x01).
1615
1616    #[test]
1617    fn h265_single_nal_round_trips_through_depacketizer() {
1618        // VPS (non-VCL) + IDR (VCL keyframe), each a single-NAL packet.
1619        let au = [
1620            0, 0, 0, 1, 0x40, 0x01, 0xAA, // VPS (type 32)
1621            0, 0, 0, 1, 0x26, 0x01, 0x88, 0x99, // IDR (type 19)
1622        ];
1623        let mut pkt = RtpPacketizer::new_h265(96, 0xABCD, 1200);
1624        let packets = pkt.packetize(&au, 3000);
1625        assert_eq!(packets.len(), 2, "one packet per NAL");
1626
1627        let mut depack = H265Depacketizer::new();
1628        let mut out = None;
1629        for p in &packets {
1630            let h = RtpHeader::parse(p).unwrap();
1631            if let Some(au) = depack
1632                .push(&p[h.payload_offset..], h.marker, h.timestamp, h.sequence)
1633                .unwrap()
1634            {
1635                out = Some(au);
1636            }
1637        }
1638        let out = out.expect("AU completed on the marker packet");
1639        assert_eq!(&out.data[..], &au);
1640        assert!(out.keyframe, "IRAP type 19 is a keyframe");
1641        assert_eq!(out.timestamp, 3000);
1642    }
1643
1644    #[test]
1645    fn h265_fragments_oversized_nal_and_round_trips() {
1646        // One IDR NAL larger than the MTU → FU fragments → reassembled identically.
1647        let mut nal = vec![0, 0, 0, 1, 0x26, 0x01]; // start code + 2-byte IDR header
1648        nal.extend((0..600u16).map(|i| i as u8));
1649        let mut pkt = RtpPacketizer::new_h265(96, 1, 100); // tiny MTU forces FU
1650        let packets = pkt.packetize(&nal, 90);
1651        assert!(packets.len() > 1, "oversized NAL is fragmented");
1652        // Exactly one marker, on the last fragment.
1653        let markers: Vec<bool> = packets
1654            .iter()
1655            .map(|p| RtpHeader::parse(p).unwrap().marker)
1656            .collect();
1657        assert_eq!(markers.iter().filter(|m| **m).count(), 1);
1658        assert!(markers.last().unwrap());
1659        // Each FU packet carries a type-49 PayloadHdr.
1660        for p in &packets {
1661            let h = RtpHeader::parse(p).unwrap();
1662            let pt = (p[h.payload_offset] >> 1) & 0x3F;
1663            assert_eq!(pt, 49, "FU payload type");
1664        }
1665
1666        let mut depack = H265Depacketizer::new();
1667        let mut out = None;
1668        for p in &packets {
1669            let h = RtpHeader::parse(p).unwrap();
1670            if let Some(au) = depack
1671                .push(&p[h.payload_offset..], h.marker, h.timestamp, h.sequence)
1672                .unwrap()
1673            {
1674                out = Some(au);
1675            }
1676        }
1677        assert_eq!(&out.unwrap().data[..], &nal[..]);
1678    }
1679
1680    #[test]
1681    fn h265_ap_splits_aggregated_nals() {
1682        // AP (type 48): [0x60,0x01][size=2][AA BB][size=3][CC DD EE]
1683        let payload = [0x60, 0x01, 0, 2, 0xAA, 0xBB, 0, 3, 0xCC, 0xDD, 0xEE];
1684        let mut d = H265Depacketizer::new();
1685        let out = d.push(&payload, true, 0, 1).unwrap().unwrap();
1686        assert_eq!(
1687            &out.data[..],
1688            &[0, 0, 0, 1, 0xAA, 0xBB, 0, 0, 0, 1, 0xCC, 0xDD, 0xEE]
1689        );
1690    }
1691
1692    #[test]
1693    fn h265_rejects_truncated_and_unsupported() {
1694        let mut d = H265Depacketizer::new();
1695        // One byte cannot hold a 2-byte NAL header.
1696        assert_eq!(
1697            d.push(&[0x26], true, 0, 1),
1698            Err(DepacketizeError::Truncated)
1699        );
1700        // PACI (type 50) is not supported.
1701        assert_eq!(
1702            d.push(&[50 << 1, 0x01, 0x00], true, 0, 2),
1703            Err(DepacketizeError::Unsupported(50))
1704        );
1705    }
1706
1707    // ── VP9 ───────────────────────────────────────────────────────────────────
1708
1709    fn vp9_depacketize(packets: &[Vec<u8>]) -> Option<AccessUnit> {
1710        let mut d = Vp9Depacketizer::new();
1711        let mut out = None;
1712        for p in packets {
1713            let h = RtpHeader::parse(p).unwrap();
1714            if let Some(f) = d
1715                .push(&p[h.payload_offset..], h.marker, h.timestamp)
1716                .unwrap()
1717            {
1718                out = Some(f);
1719            }
1720        }
1721        out
1722    }
1723
1724    #[test]
1725    fn vp9_fragmented_frame_round_trips() {
1726        let frame: Vec<u8> = (0..500u16).map(|i| i as u8).collect();
1727        let mut pkt = Vp9Packetizer::new(98, 0x1234, 100); // small MTU → fragments
1728        let packets = pkt.packetize(&frame, 9000, true);
1729        assert!(packets.len() > 1, "frame fragmented");
1730
1731        // Exactly one marker, on the last packet.
1732        let markers: Vec<bool> = packets
1733            .iter()
1734            .map(|p| RtpHeader::parse(p).unwrap().marker)
1735            .collect();
1736        assert_eq!(markers.iter().filter(|m| **m).count(), 1);
1737        assert!(markers.last().unwrap());
1738
1739        let out = vp9_depacketize(&packets).expect("frame completed");
1740        assert_eq!(&out.data[..], &frame[..]);
1741        assert!(out.keyframe, "keyframe → P bit clear");
1742        assert_eq!(out.timestamp, 9000);
1743    }
1744
1745    #[test]
1746    fn vp9_inter_frame_is_not_a_keyframe() {
1747        let mut pkt = Vp9Packetizer::new(98, 1, 1200);
1748        let packets = pkt.packetize(&[1, 2, 3], 0, false);
1749        assert_eq!(packets.len(), 1);
1750        let out = vp9_depacketize(&packets).expect("frame");
1751        assert_eq!(&out.data[..], &[1, 2, 3]);
1752        assert!(!out.keyframe, "P bit set → inter frame");
1753    }
1754
1755    // ── AV1 ───────────────────────────────────────────────────────────────────
1756
1757    #[cfg(feature = "codec-av1")]
1758    fn av1_depacketize(packets: &[Vec<u8>]) -> Option<AccessUnit> {
1759        let mut d = Av1Depacketizer::new();
1760        let mut out = None;
1761        for p in packets {
1762            let h = RtpHeader::parse(p).unwrap();
1763            if let Some(f) = d
1764                .push(&p[h.payload_offset..], h.marker, h.timestamp)
1765                .unwrap()
1766            {
1767                out = Some(f);
1768            }
1769        }
1770        out
1771    }
1772
1773    #[cfg(feature = "codec-av1")]
1774    #[test]
1775    fn av1_temporal_unit_round_trips_without_delimiter() {
1776        // Low-overhead OBUs: temporal delimiter + sequence header + frame.
1777        let td = [0x12u8, 0x00];
1778        let seq = [0x0Au8, 0x02, 0xAA, 0xBB];
1779        let frame = [0x32u8, 0x03, 0x11, 0x22, 0x33];
1780        let mut tu = Vec::new();
1781        tu.extend_from_slice(&td);
1782        tu.extend_from_slice(&seq);
1783        tu.extend_from_slice(&frame);
1784
1785        let mut pkt = Av1Packetizer::new(99, 7, 1200);
1786        let packets = pkt.packetize(&tu, 1000);
1787        let out = av1_depacketize(&packets).expect("TU completed");
1788
1789        // The temporal delimiter is dropped; seq + frame survive, low-overhead.
1790        let mut expected = Vec::new();
1791        expected.extend_from_slice(&seq);
1792        expected.extend_from_slice(&frame);
1793        assert_eq!(&out.data[..], &expected[..]);
1794        assert!(out.keyframe, "sequence header → new coded video sequence");
1795        assert_eq!(out.timestamp, 1000);
1796    }
1797
1798    #[cfg(feature = "codec-av1")]
1799    #[test]
1800    fn av1_large_temporal_unit_fragments_and_round_trips() {
1801        // A frame OBU with a 300-byte payload (size field leb128(300) = AC 02).
1802        let mut frame = vec![0x32u8, 0xAC, 0x02];
1803        frame.extend((0..300u16).map(|i| i as u8));
1804        let mut tu = vec![0x12u8, 0x00]; // temporal delimiter
1805        tu.extend_from_slice(&frame);
1806
1807        let mut pkt = Av1Packetizer::new(99, 1, 64); // tiny MTU forces fragmentation
1808        let packets = pkt.packetize(&tu, 0);
1809        assert!(packets.len() > 1, "large TU fragmented");
1810        // Z set on every packet but the first; Y on every packet but the last.
1811        for (i, p) in packets.iter().enumerate() {
1812            let agg = p[RtpHeader::parse(p).unwrap().payload_offset];
1813            assert_eq!((agg & 0x80 != 0), i > 0, "Z continuation bit");
1814            assert_eq!(
1815                (agg & 0x40 != 0),
1816                i + 1 < packets.len(),
1817                "Y continuation bit"
1818            );
1819        }
1820
1821        let out = av1_depacketize(&packets).expect("TU completed");
1822        assert_eq!(&out.data[..], &frame[..], "frame OBU reconstructed");
1823    }
1824}