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