Skip to main content

o_sfu_rfc/
rtp.rs

1//! RFC references covered:
2//! - RTP base protocol: <https://www.rfc-editor.org/rfc/rfc3550>
3//! - RTP A/V profile payload assignments: <https://www.rfc-editor.org/rfc/rfc3551>
4//! - RTP header extension framework: <https://www.rfc-editor.org/rfc/rfc8285>
5//! - RTCP feedback profile: <https://www.rfc-editor.org/rfc/rfc4585>
6//! - RTP retransmission payload format: <https://www.rfc-editor.org/rfc/rfc4588>
7//! - RTP stream identifier SDES items: <https://www.rfc-editor.org/rfc/rfc8852>
8//! - Video frame marking RTP header extension: <https://www.rfc-editor.org/rfc/rfc9626>
9//! - Layer Refresh Request feedback: <https://www.rfc-editor.org/rfc/rfc9627>
10//! - RTP payload format for VP8: <https://www.rfc-editor.org/rfc/rfc7741>
11//! - RTP payload format for H264: <https://www.rfc-editor.org/rfc/rfc6184>
12//!
13//! A complete RTP packet has this outer shape:
14//!
15//! ```text
16//!  0                   1                   2                   3
17//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
18//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19//! |V=2|P|X|  CC   |M|     PT      |       sequence number         |
20//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21//! |                           timestamp                           |
22//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23//! |                             SSRC                              |
24//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25//! |                         CSRC list ...                         |
26//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
27//! |              extension block if X=1 ...                       |
28//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
29//! |                         codec payload ...                     |
30//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
31//! ```
32//!
33//! Generic NACK is RTCP feedback rather than a field in an RTP packet. One
34//! RTPFB packet can carry multiple `PID`/`BLP` entries:
35//!
36//! ```text
37//!  0                   1                   2                   3
38//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
39//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
40//! |V=2|P| FMT=1 |    PT=205     |             length              |
41//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
42//! |                    SSRC of feedback sender                    |
43//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
44//! |                     SSRC of primary media                     |
45//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
46//! |              PID              |              BLP              |
47//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
48//! |                    more PID/BLP entries ...                   |
49//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
50//! ```
51//!
52//! `PID` names one missing primary RTP sequence number. Zero-based BLP bit `i`
53//! reports `(PID + i + 1) mod 2^16` as missing. See
54//! [RFC 4585 section 6.1](https://www.rfc-editor.org/rfc/rfc4585.html#section-6.1)
55//! and
56//! [RFC 4585 section 6.2.1](https://www.rfc-editor.org/rfc/rfc4585.html#section-6.2.1).
57//!
58//! RTX is a new RTP packet in the repair stream. Its RTP header has the repair
59//! payload type, repair SSRC and an independent repair sequence number. The
60//! payload begins with the original sequence number:
61//!
62//! ```text
63//!  0                   1                   2                   3
64//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
65//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
66//! |V=2|P|X|  CC   |M|   RTX PT    |    repair sequence number     |
67//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
68//! |                    original RTP timestamp                     |
69//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
70//! |                          repair SSRC                          |
71//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
72//! |         optional CSRC list and RTP header extension ...       |
73//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
74//! |              OSN              |                               |
75//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
76//! |                 original RTP packet payload ...               |
77//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
78//! ```
79//!
80//! `apt` binds the RTX payload type to the primary payload type in SDP. It is
81//! not carried in either packet. See
82//! [RFC 4588 section 4](https://www.rfc-editor.org/rfc/rfc4588.html#section-4)
83//! and
84//! [RFC 4588 section 8.1](https://www.rfc-editor.org/rfc/rfc4588.html#section-8.1).
85
86use std::{fmt, iter, mem};
87
88use crate::webrtc::sdp;
89
90/// RTP version defined by RFC 3550 section 5.1.
91pub const RTP_VERSION: u8 = 2;
92
93/// RTP version bits in the first octet of RTP and RTCP packets.
94///
95/// References:
96/// - <https://www.rfc-editor.org/rfc/rfc3550.html#section-5.1>
97/// - <https://www.rfc-editor.org/rfc/rfc3550.html#section-6.4.1>
98const RTP_VERSION_HEADER_BITS: u8 = RTP_VERSION << RTP_VERSION_SHIFT;
99
100/// Fixed RTP header length in bytes with no CSRC and no extension.
101///
102/// Reference: RFC 3550 section 5.1.
103pub const RTP_FIXED_HEADER_BYTES: usize = 12;
104
105/// Number of values representable by the 7-bit RTP payload type field.
106///
107/// Reference: <https://www.rfc-editor.org/rfc/rfc3550.html#section-5.1>
108pub const RTP_PAYLOAD_TYPE_COUNT: usize = 1 << RTP_PAYLOAD_TYPE_BITS;
109
110/// Maximum number of CSRC identifiers in the RTP fixed header.
111///
112/// The CC field is 4 bits, so the valid range is 0..=15.
113/// Reference: RFC 3550 section 5.1.
114pub const RTP_MAX_CSRC_COUNT: u8 = 15;
115
116/// Sequence number rollover modulus for the 16-bit RTP sequence number.
117///
118/// Reference: RFC 3550 section 5.1.
119pub const RTP_SEQUENCE_NUMBER_MODULUS: u32 = 1_u32 << 16;
120
121/// Timestamp rollover modulus for the 32-bit RTP timestamp field.
122///
123/// Reference: RFC 3550 section 5.1.
124pub const RTP_TIMESTAMP_MODULUS: u64 = 1_u64 << 32;
125
126/// Maximum value representable by the 7-bit RTP payload type field.
127///
128/// Reference: RFC 3550 section 5.1.
129pub const RTP_PAYLOAD_TYPE_MAX: u8 = 127;
130
131/// Dynamic RTP payload type range in RTP/AVP.
132///
133/// Reference: RFC 3551 section 6.
134pub const RTP_DYNAMIC_PAYLOAD_TYPE_START: u8 = 96;
135pub const RTP_DYNAMIC_PAYLOAD_TYPE_END: u8 = 127;
136
137/// Payload type range disallowed when RTP and RTCP share one port.
138///
139/// RFC 5761 reserves the full 64 through 95 payload type range for muxed
140/// sessions so the second packet octet can unambiguously distinguish RTP from
141/// RTCP.
142///
143/// Reference: RFC 5761 section 4.
144pub const RTP_RTCP_MUX_FORBIDDEN_PAYLOAD_TYPE_START: u8 = 64;
145pub const RTP_RTCP_MUX_FORBIDDEN_PAYLOAD_TYPE_END: u8 = 95;
146
147const RTP_VERSION_SHIFT: u32 = 6;
148const RTP_VERSION_MASK: u8 = 0b1100_0000;
149const RTP_PAYLOAD_TYPE_BITS: u32 = 7;
150const RTP_PAYLOAD_TYPE_MASK: u8 = RTP_PAYLOAD_TYPE_MAX;
151const RTP_PADDING_MASK: u8 = 0b0010_0000;
152const RTP_EXTENSION_MASK: u8 = 0b0001_0000;
153const RTP_CSRC_COUNT_MASK: u8 = 0b0000_1111;
154const RTP_MARKER_MASK: u8 = 0b1000_0000;
155const RTP_SEQUENCE_NUMBER_OFFSET: usize = 2;
156const RTP_TIMESTAMP_OFFSET: usize = 4;
157const RTP_SSRC_OFFSET: usize = 8;
158const RTP_CSRC_BYTES: usize = 4;
159
160/// RTCP common-header length in bytes.
161///
162/// Reference: <https://www.rfc-editor.org/rfc/rfc3550.html#section-6.4.1>
163const RTCP_COMMON_HEADER_BYTES: usize = 4;
164
165/// SSRC field length in an RTCP packet.
166///
167/// Reference: <https://www.rfc-editor.org/rfc/rfc3550.html#section-6.4.1>
168const RTCP_SSRC_BYTES: usize = 4;
169
170/// Length of an RTCP Receiver Report with no report blocks.
171///
172/// Reference: <https://www.rfc-editor.org/rfc/rfc3550.html#section-6.4.2>
173pub const RTCP_RECEIVER_REPORT_WITHOUT_BLOCKS_BYTES: usize =
174    RTCP_COMMON_HEADER_BYTES + RTCP_SSRC_BYTES;
175
176const RTCP_RECEIVER_REPORT_WITHOUT_BLOCKS_LENGTH_WORDS_MINUS_ONE: u16 = 1;
177
178/// Number of octets prepended to an RTX payload for the original sequence
179/// number.
180///
181/// Reference: <https://www.rfc-editor.org/rfc/rfc4588.html#section-4>
182pub const RTX_ORIGINAL_SEQUENCE_NUMBER_BYTES: usize = 2;
183
184/// Number of sequence numbers represented by the Generic NACK BLP field after
185/// its PID.
186///
187/// Reference: <https://www.rfc-editor.org/rfc/rfc4585.html#section-6.2.1>
188const GENERIC_NACK_BITMASK_BITS: u16 = 16;
189
190/// Common static RTP/AVP payload type assignments.
191///
192/// Reference: RFC 3551 section 6, tables 4 and 5.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194#[repr(u8)]
195pub enum AvpStaticPayloadType {
196    Pcmu = 0,
197    Gsm = 3,
198    G723 = 4,
199    Dvi4_8000 = 5,
200    Dvi4_16000 = 6,
201    Lpc = 7,
202    Pcma = 8,
203    G722 = 9,
204    L16Stereo = 10,
205    L16Mono = 11,
206    Qcelp = 12,
207    ComfortNoise = 13,
208    Mpa = 14,
209    G728 = 15,
210    Dvi4_11025 = 16,
211    Dvi4_22050 = 17,
212    G729 = 18,
213    Celb = 25,
214    Jpeg = 26,
215    Nv = 28,
216    H261 = 31,
217    Mpv = 32,
218    Mp2t = 33,
219    H263 = 34,
220}
221
222impl AvpStaticPayloadType {
223    #[must_use]
224    #[expect(
225        clippy::as_conversions,
226        reason = "repr(u8) guarantees safe identity cast"
227    )]
228    pub const fn as_u8(self) -> u8 {
229        self as u8
230    }
231}
232
233/// 7-bit RTP payload type value usable in RTP/RTCP muxed sessions
234#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
235pub struct PayloadType(u8);
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub struct InvalidPayloadType;
239
240impl PayloadType {
241    #[must_use]
242    pub const fn try_new(value: u8) -> Option<Self> {
243        if is_rtcp_mux_payload_type(value) {
244            Some(Self(value))
245        } else {
246            None
247        }
248    }
249
250    /// builds a payload type for the muxed RTP sessions used by `o-sfu`
251    ///
252    /// # Panics
253    ///
254    /// panics when `value` does not fit the RTP payload type field or is in the
255    /// RTP/RTCP mux forbidden range from RFC 5761 section 4
256    #[must_use]
257    pub const fn new(value: u8) -> Self {
258        assert!(is_rtcp_mux_payload_type(value));
259        Self(value)
260    }
261
262    #[must_use]
263    pub const fn value(self) -> u8 {
264        self.0
265    }
266}
267
268impl TryFrom<u8> for PayloadType {
269    type Error = InvalidPayloadType;
270
271    fn try_from(value: u8) -> Result<Self, Self::Error> {
272        Self::try_new(value).ok_or(InvalidPayloadType)
273    }
274}
275
276impl From<PayloadType> for u8 {
277    fn from(value: PayloadType) -> Self {
278        value.value()
279    }
280}
281
282/// RTP or RTCP candidate selected by the RFC 5761 second-octet split.
283///
284/// The result identifies only the mux candidate kind. It does not validate the
285/// complete packet body.
286///
287/// Reference: <https://www.rfc-editor.org/rfc/rfc5761.html#section-4>
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum RtpRtcpMuxPacketKind {
290    Rtp,
291    Rtcp,
292}
293
294/// Parsed fields from an RTP fixed header in an RTP/RTCP muxed session.
295///
296/// Reference: <https://www.rfc-editor.org/rfc/rfc3550.html#section-5.1>
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub struct RtpFixedHeader {
299    payload_type: u8,
300    sequence_number: u16,
301    timestamp: u32,
302    ssrc: Ssrc,
303    marker: bool,
304    has_padding: bool,
305    has_extension: bool,
306    csrc_count: u8,
307}
308
309impl RtpFixedHeader {
310    #[must_use]
311    pub const fn payload_type(self) -> u8 {
312        self.payload_type
313    }
314
315    #[must_use]
316    pub const fn sequence_number(self) -> u16 {
317        self.sequence_number
318    }
319
320    #[must_use]
321    pub const fn timestamp(self) -> u32 {
322        self.timestamp
323    }
324
325    #[must_use]
326    pub const fn ssrc(self) -> Ssrc {
327        self.ssrc
328    }
329
330    #[must_use]
331    pub const fn marker(self) -> bool {
332        self.marker
333    }
334
335    #[must_use]
336    pub const fn has_padding(self) -> bool {
337        self.has_padding
338    }
339
340    #[must_use]
341    const fn has_extension(self) -> bool {
342        self.has_extension
343    }
344
345    fn extension_offset(self) -> usize {
346        RTP_FIXED_HEADER_BYTES + usize::from(self.csrc_count) * RTP_CSRC_BYTES
347    }
348}
349
350/// Synchronization source identifier for a media stream (RFC 3550).
351///
352/// Every distinct stream of packets (e.g. one audio track, one camera layer)
353/// is assigned a random 32-bit SSRC. This allows multiple streams to be
354/// multiplexed over a single transport (e.g. one UDP port).
355#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
356pub struct Ssrc(u32);
357
358impl Ssrc {
359    #[must_use]
360    pub const fn new(value: u32) -> Self {
361        Self(value)
362    }
363
364    #[must_use]
365    pub const fn value(self) -> u32 {
366        self.0
367    }
368}
369
370impl From<u32> for Ssrc {
371    fn from(value: u32) -> Self {
372        Self::new(value)
373    }
374}
375
376impl From<Ssrc> for u32 {
377    fn from(value: Ssrc) -> Self {
378        value.value()
379    }
380}
381
382/// Restriction identifier (RFC 8851).
383///
384/// Used in "Simulcast" to label different encodings of the same source (e.g.
385/// "low" and "high" resolution). Unlike SSRC which is a random number that
386/// can change if a collision occurs, RID is a stable string label.
387#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
388pub struct Rid(String);
389
390impl Rid {
391    #[must_use]
392    pub fn try_new(value: impl Into<String>) -> Option<Self> {
393        let value = value.into();
394        sdp::rid::is_id(value.as_str()).then_some(Self(value))
395    }
396
397    /// Builds a RID using the RFC 8851 `rid-id` grammar corrected by RFC Editor errata 7132.
398    ///
399    /// # Panics
400    ///
401    /// Panics when `value` is empty, too long or contains a non-alphanumeric
402    /// byte.
403    #[must_use]
404    pub fn new(value: impl Into<String>) -> Self {
405        let value = value.into();
406        assert!(sdp::rid::is_id(value.as_str()));
407        Self(value)
408    }
409
410    #[must_use]
411    pub fn as_str(&self) -> &str {
412        self.0.as_str()
413    }
414}
415
416impl From<&str> for Rid {
417    fn from(value: &str) -> Self {
418        Self::new(value)
419    }
420}
421
422impl From<String> for Rid {
423    fn from(value: String) -> Self {
424        Self::new(value)
425    }
426}
427
428/// Media identification (RFC 9143).
429///
430/// Ties an RTP stream to a specific "m=" section in the SDP. This is
431/// critical for "BUNDLE" where multiple media sections share one transport,
432/// as it provides a stable way to route packets to the correct logical track.
433#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
434pub struct Mid(String);
435
436impl Mid {
437    #[must_use]
438    pub fn new(value: impl Into<String>) -> Self {
439        Self(value.into())
440    }
441
442    #[must_use]
443    pub fn as_str(&self) -> &str {
444        self.0.as_str()
445    }
446}
447
448impl From<&str> for Mid {
449    fn from(value: &str) -> Self {
450        Self::new(value)
451    }
452}
453
454impl From<String> for Mid {
455    fn from(value: String) -> Self {
456        Self::new(value)
457    }
458}
459
460/// Local 4-bit identifier for a header extension (RFC 8285).
461#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
462pub struct HeaderExtensionId(u8);
463
464impl HeaderExtensionId {
465    #[must_use]
466    pub const fn try_new(value: u8) -> Option<Self> {
467        if header_extension::is_one_byte_id(value) {
468            Some(Self(value))
469        } else {
470            None
471        }
472    }
473
474    /// Builds a one-byte RTP header-extension id.
475    ///
476    /// # Panics
477    ///
478    /// Panics when `value` is padding, reserved or outside the RFC 8285
479    /// one-byte element id range.
480    #[must_use]
481    pub const fn new(value: u8) -> Self {
482        assert!(header_extension::is_one_byte_id(value));
483        Self(value)
484    }
485
486    #[must_use]
487    pub const fn value(self) -> u8 {
488        self.0
489    }
490}
491
492impl From<u8> for HeaderExtensionId {
493    fn from(value: u8) -> Self {
494        Self::new(value)
495    }
496}
497
498impl From<HeaderExtensionId> for u8 {
499    fn from(value: HeaderExtensionId) -> Self {
500        value.value()
501    }
502}
503
504/// RTP clock rate used by the supported video payload formats
505///
506/// Reference: IANA RTP Payload Format Media Types registry
507pub const RTP_VIDEO_CLOCK_RATE_HZ: u32 = 90_000;
508
509/// RTP payload-format MIME subtype names commonly used by WebRTC endpoints.
510pub mod codec_name {
511    /// PCMU RTP payload-format subtype.
512    ///
513    /// Reference: RFC 3551.
514    pub const PCMU: &str = "PCMU";
515
516    /// PCMA RTP payload-format subtype.
517    ///
518    /// Reference: RFC 3551.
519    pub const PCMA: &str = "PCMA";
520
521    /// Opus RTP payload-format subtype.
522    ///
523    /// Reference: RFC 7587.
524    pub const OPUS: &str = "opus";
525
526    /// VP8 RTP payload-format subtype.
527    ///
528    /// Reference: RFC 7741.
529    pub const VP8: &str = "VP8";
530
531    /// H264 RTP payload-format subtype.
532    ///
533    /// Reference: RFC 6184.
534    pub const H264: &str = "H264";
535
536    /// H265 RTP payload-format subtype.
537    ///
538    /// Reference: RFC 7798.
539    pub const H265: &str = "H265";
540
541    /// VP9 RTP payload-format subtype.
542    ///
543    /// Reference: RFC 9628.
544    pub const VP9: &str = "VP9";
545
546    /// AV1 RTP payload-format subtype.
547    ///
548    /// Reference: IANA Media Types registry.
549    pub const AV1: &str = "AV1";
550
551    /// RTX retransmission RTP payload-format subtype.
552    ///
553    /// Reference: RFC 4588.
554    pub const RTX: &str = "rtx";
555}
556
557/// G.711 RTP payload-format constants
558pub mod g711 {
559    /// RTP clock rate for the static PCMU and PCMA RTP/AVP payload types
560    ///
561    /// Reference: RFC 3551 section 6
562    pub const RTP_CLOCK_RATE_HZ: u32 = 8_000;
563}
564
565/// Opus RTP payload-format constants.
566///
567/// Reference: RFC 7587 for the RTP payload format and RFC 6716 for the
568/// packet table-of-contents layout.
569pub mod opus {
570    /// RTP clock rate for Opus payloads.
571    ///
572    /// Reference: RFC 7587 section 4.1.
573    pub const RTP_CLOCK_RATE_HZ: u32 = 48_000;
574
575    /// Opus channel count signaled in SDP `rtpmap` attributes.
576    ///
577    /// Reference: RFC 7587 section 7.
578    pub const RTPMAP_CHANNEL_COUNT: u16 = 2;
579
580    /// Opus packet frame-count codes from the table-of-contents byte.
581    ///
582    /// Reference: RFC 6716 section 3.1.
583    pub mod frame_count_code {
584        /// One frame in the Opus packet.
585        pub const ONE_FRAME: u8 = 0;
586    }
587
588    /// Opus table-of-contents configuration numbers.
589    ///
590    /// Reference: RFC 6716 section 3.1.
591    pub mod toc_config {
592        /// SILK-only wideband packet with a 20 ms frame duration.
593        pub const SILK_WIDEBAND_20_MS: u8 = 9;
594    }
595}
596
597/// VP8 RTP payload helpers.
598///
599/// These helpers operate on the codec payload slice, not on the full RTP
600/// packet. In runtime forwarding that slice starts after the RTP fixed header,
601/// CSRC list and any RTP header-extension block.
602///
603/// RFC 7741 puts a VP8 payload descriptor in front of the VP8 payload header.
604/// The descriptor is also where simulcast and temporal-layer packet identity is
605/// carried. o-sfu keeps the RFC constants here, while the runtime uses str0m's
606/// descriptor parser and patch support for local egress.
607///
608/// ```text
609/// VP8 payload slice passed to this module
610///
611/// +------------------+----------------------+----------------------+
612/// | descriptor byte  | extension bytes      | VP8 payload header   |
613/// +------------------+----------------------+----------------------+
614/// | X R N S PartID   | I L T K fields       | P bit and VP8 data   |
615/// +------------------+----------------------+----------------------+
616///
617/// Extension bytes when X=1
618///
619/// +-------------+-------------------+-------------+----------------+
620/// | I L T K ... | PictureID if I=1  | TL0 if L=1  | T/K if present |
621/// +-------------+-------------------+-------------+----------------+
622/// |             | 1 or 2 bytes      | 1 byte      | 1 byte         |
623/// +-------------+-------------------+-------------+----------------+
624/// ```
625pub mod vp8 {
626    /// Extended control bits are present in the VP8 payload descriptor.
627    pub const X_BIT: u8 = 0b1000_0000;
628
629    /// Start of VP8 partition bit in the payload descriptor.
630    pub const S_BIT: u8 = 0b0001_0000;
631
632    const PARTITION_ID_MASK: u8 = 0b0000_0111;
633
634    /// `PictureID` present bit in the extended VP8 payload descriptor.
635    pub const I_BIT: u8 = 0b1000_0000;
636
637    /// TL0PICIDX present bit in the extended VP8 payload descriptor.
638    pub const L_BIT: u8 = 0b0100_0000;
639
640    /// TID/Y/KEYIDX present bit in the extended VP8 payload descriptor.
641    pub const T_BIT: u8 = 0b0010_0000;
642
643    const K_BIT: u8 = 0b0001_0000;
644
645    /// Long `PictureID` marker bit in the VP8 `PictureID` field.
646    pub const LONG_PICTURE_ID_BIT: u8 = 0b1000_0000;
647
648    /// VP8 payload-header P bit set for interframes.
649    pub const INTERFRAME_BIT: u8 = 0b0000_0001;
650
651    const VERSION_MASK: u8 = 0b0000_1110;
652    const VERSION_SHIFT: u32 = 1;
653    const MAX_DEFINED_VERSION: u8 = 3;
654    const DIMENSION_MASK: u16 = 0x3fff;
655    const KEYFRAME_SYNC_CODE: [u8; 3] = [0x9d, 0x01, 0x2a];
656
657    /// value mask for the 7-bit VP8 short `PictureID` field
658    pub const SHORT_PICTURE_ID_MASK: u16 = 0x7f;
659
660    /// Value mask for the 15-bit VP8 long `PictureID` field.
661    pub const LONG_PICTURE_ID_MASK: u16 = 0x7fff;
662
663    /// Modulus for the 15-bit VP8 long `PictureID` field.
664    pub const LONG_PICTURE_ID_MODULUS: u16 = 1 << 15;
665
666    /// Value mask for the two-bit VP8 temporal-layer identity.
667    pub const TEMPORAL_LAYER_ID_MASK: u8 = 0b0000_0011;
668
669    /// Detects a complete VP8 keyframe prefix in the first RTP packet.
670    ///
671    /// RFC 7741 section 4.2 defines the VP8 payload descriptor. A decodable
672    /// keyframe starts at partition 0 (`S=1`, `PartID=0`) and the VP8 payload
673    /// header starts with `P=0`. RFC 6386 section 9.1 defines the keyframe sync
674    /// code and dimensions. The input must be the RTP codec payload.
675    ///
676    /// Truncated descriptors, missing payload headers and non-start partitions
677    /// return `false`. The helper performs only the cheap
678    /// keyframe probe needed by packet gates and decoder-refresh detection.
679    #[must_use]
680    pub fn payload_starts_keyframe(payload: &[u8]) -> bool {
681        let Some((&descriptor, rest)) = payload.split_first() else {
682            return false;
683        };
684        if descriptor & S_BIT == 0 || descriptor & PARTITION_ID_MASK != 0 {
685            return false;
686        }
687        let frame = if descriptor & X_BIT == 0 {
688            rest
689        } else {
690            let Some(frame) = extended_frame_payload(rest) else {
691                return false;
692            };
693            frame
694        };
695        valid_keyframe_prefix(frame)
696    }
697
698    /// Locates the VP8 frame bytes after an extended descriptor.
699    ///
700    /// `None` means the extension-bit combination is malformed or an advertised
701    /// field is absent. The keyframe probe treats that as "not a keyframe".
702    fn extended_frame_payload(payload: &[u8]) -> Option<&[u8]> {
703        let (&extension, mut rest) = payload.split_first()?;
704        if extension & I_BIT != 0 {
705            let (&picture_id, remaining) = rest.split_first()?;
706            rest = if picture_id & LONG_PICTURE_ID_BIT != 0 {
707                remaining.get(1..)?
708            } else {
709                remaining
710            };
711        }
712        if extension & L_BIT != 0 {
713            if extension & T_BIT == 0 {
714                return None;
715            }
716            rest = rest.get(1..)?;
717        }
718        if extension & T_BIT != 0 || extension & K_BIT != 0 {
719            rest = rest.get(1..)?;
720        }
721        (!rest.is_empty()).then_some(rest)
722    }
723
724    fn valid_keyframe_prefix(frame: &[u8]) -> bool {
725        let &[
726            tag,
727            _,
728            _,
729            sync_0,
730            sync_1,
731            sync_2,
732            width_low,
733            width_high,
734            height_low,
735            height_high,
736            ..,
737        ] = frame
738        else {
739            return false;
740        };
741        let version = (tag & VERSION_MASK) >> VERSION_SHIFT;
742        let width = u16::from_le_bytes([width_low, width_high]) & DIMENSION_MASK;
743        let height = u16::from_le_bytes([height_low, height_high]) & DIMENSION_MASK;
744        tag & INTERFRAME_BIT == 0
745            && version <= MAX_DEFINED_VERSION
746            && [sync_0, sync_1, sync_2] == KEYFRAME_SYNC_CODE
747            && width != 0
748            && height != 0
749    }
750}
751
752/// VP9 profile identifier defined by RFC 9628 section 6
753#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
754pub struct Vp9ProfileId(u8);
755
756impl Vp9ProfileId {
757    /// VP9 Profile 0
758    pub const PROFILE_0: Self = Self(0);
759
760    /// VP9 Profile 1
761    pub const PROFILE_1: Self = Self(1);
762
763    /// VP9 Profile 2
764    pub const PROFILE_2: Self = Self(2);
765
766    /// VP9 Profile 3
767    pub const PROFILE_3: Self = Self(3);
768
769    #[must_use]
770    pub const fn try_new(value: u8) -> Option<Self> {
771        if value <= Self::PROFILE_3.value() {
772            Some(Self(value))
773        } else {
774            None
775        }
776    }
777
778    #[must_use]
779    pub const fn value(self) -> u8 {
780        self.0
781    }
782}
783
784/// RTP payload-format MIME subtype names commonly used by WebRTC endpoints.
785#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
786pub enum CodecName {
787    Pcmu,
788    Pcma,
789    Opus,
790    Vp8,
791    H264,
792    H265,
793    Vp9,
794    Av1,
795    Rtx,
796    Other(String),
797}
798
799impl CodecName {
800    #[must_use]
801    pub fn as_str(&self) -> &str {
802        match self {
803            Self::Pcmu => codec_name::PCMU,
804            Self::Pcma => codec_name::PCMA,
805            Self::Opus => codec_name::OPUS,
806            Self::Vp8 => codec_name::VP8,
807            Self::H264 => codec_name::H264,
808            Self::H265 => codec_name::H265,
809            Self::Vp9 => codec_name::VP9,
810            Self::Av1 => codec_name::AV1,
811            Self::Rtx => codec_name::RTX,
812            Self::Other(name) => name.as_str(),
813        }
814    }
815
816    #[must_use]
817    pub fn is_rtx(&self) -> bool {
818        matches!(self, Self::Rtx)
819    }
820}
821
822impl From<&str> for CodecName {
823    fn from(value: &str) -> Self {
824        match value {
825            s if s.eq_ignore_ascii_case(codec_name::PCMU) => Self::Pcmu,
826            s if s.eq_ignore_ascii_case(codec_name::PCMA) => Self::Pcma,
827            s if s.eq_ignore_ascii_case(codec_name::OPUS) => Self::Opus,
828            s if s.eq_ignore_ascii_case(codec_name::VP8) => Self::Vp8,
829            s if s.eq_ignore_ascii_case(codec_name::H264) => Self::H264,
830            s if s.eq_ignore_ascii_case(codec_name::H265) => Self::H265,
831            s if s.eq_ignore_ascii_case(codec_name::VP9) => Self::Vp9,
832            s if s.eq_ignore_ascii_case(codec_name::AV1) => Self::Av1,
833            s if s.eq_ignore_ascii_case(codec_name::RTX) => Self::Rtx,
834            _ => Self::Other(value.to_owned()),
835        }
836    }
837}
838
839impl From<String> for CodecName {
840    fn from(value: String) -> Self {
841        Self::from(value.as_str())
842    }
843}
844
845impl AsRef<str> for CodecName {
846    fn as_ref(&self) -> &str {
847        self.as_str()
848    }
849}
850
851impl fmt::Display for CodecName {
852    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
853        f.write_str(self.as_str())
854    }
855}
856
857/// Codec `fmtp` parameter names and canonical valuse
858pub mod fmtp {
859    /// Separator between format-specific parameters used by RTX `fmtp`.
860    ///
861    /// Reference: <https://www.rfc-editor.org/rfc/rfc4588.html#section-8.6>
862    pub const PARAMETER_SEPARATOR: char = ';';
863
864    /// Separator between an RTX `fmtp` parameter name and value.
865    ///
866    /// Reference: <https://www.rfc-editor.org/rfc/rfc4588.html#section-8.6>
867    pub const NAME_VALUE_SEPARATOR: char = '=';
868
869    /// RTX associated payload type parameter
870    ///
871    /// Reference: <https://www.rfc-editor.org/rfc/rfc4588.html#section-8.1>
872    pub const RTX_ASSOCIATION: &str = "apt";
873
874    /// H264 packetization mode parameter.
875    ///
876    /// Reference: RFC 6184 section 8.1.
877    pub const H264_PACKETIZATION_MODE: &str = "packetization-mode";
878
879    /// H264 profile-level-id parameter.
880    ///
881    /// Reference: RFC 6184 section 8.1
882    pub const H264_PROFILE_LEVEL_ID: &str = "profile-level-id";
883
884    /// Default H264 profile-level-id when the parameter is omitted.
885    ///
886    /// Reference: RFC 6184 section 8.1.
887    pub const H264_DEFAULT_PROFILE_LEVEL_ID: &str = "42000a";
888
889    /// Default H264 packetization mode when the parameter is omitted.
890    ///
891    /// Reference: RFC 6184 section 6.2.
892    pub const H264_DEFAULT_PACKETIZATION_MODE: u8 = 0;
893
894    /// VP9 profile-id parameter.
895    ///
896    /// Reference: RFC 9628 section 6.
897    pub const VP9_PROFILE_ID: &str = "profile-id";
898
899    /// Default VP9 profile when `profile-id` is omitted.
900    ///
901    /// Reference: RFC 9628 section 6.
902    pub const VP9_DEFAULT_PROFILE_ID: super::Vp9ProfileId = super::Vp9ProfileId::PROFILE_0;
903
904    /// Opus in-band FEC parameter
905    ///
906    /// Reference: RFC 7587 section 6.1.
907    pub const OPUS_USE_IN_BAND_FEC: &str = "useinbandfec";
908
909    /// Canonical numeric enabled flag used in WebRTC `fmtp` dictionaries.
910    pub const VALUE_ENABLED: &str = "1";
911
912    /// Canonical numeric disabled flag used in WebRTC `fmtp` dictionaries
913    pub const VALUE_DISABLED: &str = "0";
914
915    /// Textual enabled flag accepted by current ORTC/WebRTC capability payloads
916    pub const VALUE_TRUE: &str = "true";
917
918    /// Textual disabled flag accepted by current ORTC/WebRTC capability payloads
919    pub const VALUE_FALSE: &str = "false";
920}
921
922/// H264 SDP and payload-format helpers derived from RFC 6184.
923///
924/// Packet helpers in this module receive the H264 RTP payload slice. The first
925/// byte of that slice identifies the packetization mode used by the packet.
926/// Decoder-refresh detection needs to recognize IDR access units in all packet
927/// shapes that browsers commonly send.
928///
929/// ```text
930/// Single NAL unit packet
931///
932/// +-------------+----------------------+
933/// | NAL header  | NAL payload          |
934/// +-------------+----------------------+
935/// | F NRI Type  | ...                  |
936/// +-------------+----------------------+
937///
938/// STAP-A aggregation packet
939///
940/// +---------------+----------+-------------+----------+-------------+
941/// | STAP-A header | NAL size | NAL bytes   | NAL size | NAL bytes   |
942/// +---------------+----------+-------------+----------+-------------+
943/// | Type=24       | 16 bits  | starts with | 16 bits  | starts with |
944/// |               |          | NAL header  |          | NAL header  |
945/// +---------------+----------+-------------+----------+-------------+
946///
947/// FU-A fragmentation packet
948///
949/// +---------------+-------------+----------------------+
950/// | FU indicator  | FU header   | fragment payload     |
951/// +---------------+-------------+----------------------+
952/// | Type=28       | S E R Type  | first fragment if S=1 |
953/// +---------------+-------------+----------------------+
954/// ```
955///
956/// rfc 6184 maps `profile-level-id` to three bytes:
957///
958/// ```text
959/// profile_idc | profile-iop | level_idc
960///
961/// profile-iop bit layout:
962///  7   6   5   4   3   2   1   0
963/// +---+---+---+---+---+---+---+---+
964/// |c0 |c1 |c2 |c3 |c4 |c5 | r | r |
965/// +---+---+---+---+---+---+---+---+
966///
967/// cN = constraint_setN_flag
968/// r  = reserved_zero bit
969/// ```
970pub mod h264 {
971    /// Mask for the H264 NAL unit type field.
972    pub const NAL_UNIT_TYPE_MASK: u8 = 0x1f;
973
974    /// Highest NRI value for reference NAL units.
975    pub const NAL_REF_IDC_HIGH: u8 = 0b0110_0000;
976
977    /// H264 IDR slice NAL unit type.
978    pub const NAL_UNIT_TYPE_IDR: u8 = 5;
979
980    /// H264 STAP-A aggregation packet type.
981    pub const NAL_UNIT_TYPE_STAP_A: u8 = 24;
982
983    /// H264 FU-A fragmentation packet type.
984    pub const NAL_UNIT_TYPE_FU_A: u8 = 28;
985
986    /// FU-A start bit in the FU header.
987    pub const FU_START_BIT: u8 = 0x80;
988
989    /// FU-A end bit in the FU header.
990    pub const FU_END_BIT: u8 = 0x40;
991
992    /// H264 RTP packetization mode.
993    ///
994    /// Reference: RFC 6184 section 6.
995    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
996    pub enum PacketizationMode {
997        SingleNalUnit,
998        NonInterleaved,
999        Interleaved,
1000    }
1001
1002    impl PacketizationMode {
1003        #[must_use]
1004        pub const fn from_fmtp_value(value: u8) -> Option<Self> {
1005            match value {
1006                0 => Some(Self::SingleNalUnit),
1007                1 => Some(Self::NonInterleaved),
1008                2 => Some(Self::Interleaved),
1009                _ => None,
1010            }
1011        }
1012
1013        #[must_use]
1014        pub const fn fmtp_value(self) -> u8 {
1015            match self {
1016                Self::SingleNalUnit => 0,
1017                Self::NonInterleaved => 1,
1018                Self::Interleaved => 2,
1019            }
1020        }
1021
1022        const fn allows_aggregation_and_fragmentation(self) -> bool {
1023            matches!(self, Self::NonInterleaved)
1024        }
1025    }
1026
1027    /// parsed H264 `profile-level-id` capability
1028    ///
1029    /// `profile_idc` names the broad H264 profile family, `profile-iop`
1030    /// carries constraint bits that narrow that family to a sub-profile and
1031    /// `level_idc` carries the decoder capability level. [`ProfileLevelId`]
1032    /// normalizes equivalent RFC 6184 profile encodings before router
1033    /// negotiation compares [`Profile`] and [`LevelIdc`]
1034    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1035    pub struct ProfileLevelId {
1036        profile: Profile,
1037        level: LevelIdc,
1038    }
1039
1040    impl ProfileLevelId {
1041        #[must_use]
1042        pub const fn new(profile: Profile, level: LevelIdc) -> Self {
1043            Self { profile, level }
1044        }
1045
1046        /// Parse the RFC 6184 `profile-level-id` hex token.
1047        #[must_use]
1048        pub fn parse(value: &str) -> Option<Self> {
1049            Self::parse_ascii_bytes(value.as_bytes())
1050        }
1051
1052        /// Parse the six-byte ASCII hex form of a `profile-level-id` token.
1053        #[must_use]
1054        pub fn parse_ascii_bytes(value: &[u8]) -> Option<Self> {
1055            let [profile_idc, profile_iop, level_idc] = parse_profile_level_id_bytes(value)?;
1056            let level = normalized_level_idc(profile_idc, profile_iop, level_idc)?;
1057            let profile = profile_from_bytes(profile_idc, profile_iop)?;
1058            Some(Self { profile, level })
1059        }
1060
1061        #[must_use]
1062        pub const fn profile(self) -> Profile {
1063            self.profile
1064        }
1065
1066        #[must_use]
1067        pub const fn level(self) -> LevelIdc {
1068            self.level
1069        }
1070
1071        #[must_use]
1072        pub const fn packed_value(self) -> u32 {
1073            let (profile_idc, profile_iop, level_idc) =
1074                profile_level_id_bytes(self.profile, self.level);
1075            pack_profile_level_id(profile_idc, profile_iop, level_idc)
1076        }
1077
1078        #[must_use]
1079        pub fn fmtp_value(self) -> String {
1080            let value = self.packed_value();
1081            format!("{value:06x}")
1082        }
1083    }
1084
1085    /// H264 sub-profile after equivalent RFC 6184 encodings are normalized
1086    ///
1087    /// the same sub-profile may be advertised through different
1088    /// `profile_idc` families when `profile-iop` constraints restrict them to
1089    /// the same coding tools
1090    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1091    pub enum Profile {
1092        Baseline,
1093        ConstrainedBaseline,
1094        Main,
1095        Extended,
1096        High,
1097        High10,
1098        High422,
1099        High444Predictive,
1100        High10Intra,
1101        High422Intra,
1102        High444Intra,
1103        Cavlc444Intra,
1104    }
1105
1106    /// H264 decoder capability level
1107    ///
1108    /// all levels except [`LevelIdc::Level1B`] map directly to `level_idc`
1109    /// level 1b was inserted between level 1 and level 1.1 after those byte
1110    /// values existed, so RFC 6184 gives it profile-dependent encodings
1111    ///
1112    /// the variant order is the negotiation order
1113    /// wire `level_idc` bytes are parsed and rendered explicitly because Level
1114    /// 1b is encoded specially
1115    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1116    pub enum LevelIdc {
1117        Level1,
1118        Level1B,
1119        Level1_1,
1120        Level1_2,
1121        Level1_3,
1122        Level2,
1123        Level2_1,
1124        Level2_2,
1125        Level3,
1126        Level3_1,
1127        Level3_2,
1128        Level4,
1129        Level4_1,
1130        Level4_2,
1131        Level5,
1132        Level5_1,
1133        Level5_2,
1134    }
1135
1136    /// Detects an H264 RTP packet that starts an IDR access unit.
1137    ///
1138    /// RFC 6184 carries IDR frames either as a single NAL unit, inside a STAP-A
1139    /// aggregation packet or as the first fragment of a FU-A packet.
1140    ///
1141    /// The input must be the RTP codec payload, not a complete RTP packet.
1142    /// Truncated aggregation packets and incomplete FU-A packets return
1143    /// `false`. The helper does not parse a full access unit. It only answers
1144    /// whether this packet can refresh a decoder if forwarded.
1145    #[must_use]
1146    pub fn payload_starts_idr(payload: &[u8], packetization_mode: PacketizationMode) -> bool {
1147        let Some((&nal_header, rest)) = payload.split_first() else {
1148            return false;
1149        };
1150        match nal_header & NAL_UNIT_TYPE_MASK {
1151            NAL_UNIT_TYPE_IDR => true,
1152            NAL_UNIT_TYPE_STAP_A if packetization_mode.allows_aggregation_and_fragmentation() => {
1153                stap_a_contains_idr(rest)
1154            }
1155            NAL_UNIT_TYPE_FU_A if packetization_mode.allows_aggregation_and_fragmentation() => {
1156                fu_a_starts_idr(rest)
1157            }
1158            _ => false,
1159        }
1160    }
1161
1162    /// Scans a STAP-A payload for any contained IDR NAL unit.
1163    ///
1164    /// each aggregate entry is length-prefixed and must use a single NAL unit type
1165    /// malformed lengths fail closed because the caller cannot safely trust later
1166    /// bytes as NAL boundaries
1167    fn stap_a_contains_idr(mut payload: &[u8]) -> bool {
1168        let mut contains_idr = false;
1169        while !payload.is_empty() {
1170            let Some((len, rest)) = payload.split_first_chunk::<2>() else {
1171                return false;
1172            };
1173            let nal_len = usize::from(u16::from_be_bytes(*len));
1174            let Some((nal, remaining_payload)) = rest.split_at_checked(nal_len) else {
1175                return false;
1176            };
1177            let Some(&nal_header) = nal.first() else {
1178                return false;
1179            };
1180            let nal_type = nal_header & NAL_UNIT_TYPE_MASK;
1181            if !(1..NAL_UNIT_TYPE_STAP_A).contains(&nal_type) {
1182                return false;
1183            }
1184            contains_idr |= nal_type == NAL_UNIT_TYPE_IDR;
1185            payload = remaining_payload;
1186        }
1187        contains_idr
1188    }
1189
1190    /// Detects whether a FU-A packet is the first fragment of an IDR NAL unit.
1191    fn fu_a_starts_idr(payload: &[u8]) -> bool {
1192        let Some((&fu_header, _fragment)) = payload.split_first() else {
1193            return false;
1194        };
1195        fu_header & FU_START_BIT != 0
1196            && fu_header & FU_END_BIT == 0
1197            && fu_header & NAL_UNIT_TYPE_MASK == NAL_UNIT_TYPE_IDR
1198    }
1199
1200    impl TryFrom<u8> for LevelIdc {
1201        type Error = ();
1202
1203        fn try_from(value: u8) -> Result<Self, Self::Error> {
1204            match value {
1205                9 => Ok(Self::Level1B),
1206                10 => Ok(Self::Level1),
1207                11 => Ok(Self::Level1_1),
1208                12 => Ok(Self::Level1_2),
1209                13 => Ok(Self::Level1_3),
1210                20 => Ok(Self::Level2),
1211                21 => Ok(Self::Level2_1),
1212                22 => Ok(Self::Level2_2),
1213                30 => Ok(Self::Level3),
1214                31 => Ok(Self::Level3_1),
1215                32 => Ok(Self::Level3_2),
1216                40 => Ok(Self::Level4),
1217                41 => Ok(Self::Level4_1),
1218                42 => Ok(Self::Level4_2),
1219                50 => Ok(Self::Level5),
1220                51 => Ok(Self::Level5_1),
1221                52 => Ok(Self::Level5_2),
1222                _ => Err(()),
1223            }
1224        }
1225    }
1226
1227    const BASELINE_IDC: u8 = 0x42;
1228    const MAIN_IDC: u8 = 0x4d;
1229    const EXTENDED_IDC: u8 = 0x58;
1230    const HIGH_IDC: u8 = 0x64;
1231    const HIGH_10_IDC: u8 = 0x6e;
1232    const HIGH_422_IDC: u8 = 0x7a;
1233    const HIGH_444_IDC: u8 = 0xf4;
1234    const CAVLC_444_IDC: u8 = 0x2c;
1235
1236    // `profile-iop` is the middle byte of `profile-level-id`
1237    // constraint flags are H264 compatibility bits that refine `profile_idc`
1238    // into the sub-profile used for offer or answer matching
1239    //
1240    // bit 7 -> constraint_set0_flag -> `IOP_CONSTRAINT_SET0`
1241    // bit 6 -> constraint_set1_flag -> `IOP_CONSTRAINT_SET1`
1242    // bit 5 -> constraint_set2_flag -> `IOP_CONSTRAINT_SET2`
1243    // bit 4 -> constraint_set3_flag -> `IOP_CONSTRAINT_SET3`
1244    // bit 3 -> constraint_set4_flag -> fixed to 0 by profile-equivalence rows
1245    // bit 2 -> constraint_set5_flag -> fixed to 0 by profile-equivalence rows
1246    // bit 1..0 -> reserved_zero_2bits -> fixed to 0 by H264
1247    const IOP_NONE: u8 = 0x00;
1248    const IOP_CONSTRAINT_SET0: u8 = 0x80;
1249    const IOP_CONSTRAINT_SET1: u8 = 0x40;
1250    const IOP_CONSTRAINT_SET2: u8 = 0x20;
1251    const IOP_CONSTRAINT_SET3: u8 = 0x10;
1252    const IOP_PROFILE_PATTERN_LOW_ZERO_MASK: u8 =
1253        !(IOP_CONSTRAINT_SET0 | IOP_CONSTRAINT_SET1 | IOP_CONSTRAINT_SET2 | IOP_CONSTRAINT_SET3);
1254    const IOP_CONSTRAINED_BASELINE: u8 =
1255        IOP_CONSTRAINT_SET0 | IOP_CONSTRAINT_SET1 | IOP_CONSTRAINT_SET2;
1256
1257    const LEVEL_1B_IDCS: [u8; 3] = [BASELINE_IDC, MAIN_IDC, EXTENDED_IDC];
1258    const LEVEL_1B_OTHER_IDC: u8 = 9;
1259    const LEVEL_1_1_IDC: u8 = 11;
1260
1261    /// one accepted RFC 6184 profile-equivalence row
1262    ///
1263    /// a row maps one concrete `profile_idc` family and one wildcarded
1264    /// `profile-iop` bit pattern to the normalized [`Profile`] used by
1265    /// negotiation
1266    #[derive(Clone, Copy)]
1267    struct ProfilePattern {
1268        profile: Profile,
1269        profile_idc: u8,
1270        iop: ProfileIopPattern,
1271    }
1272
1273    impl ProfilePattern {
1274        const fn masked(profile: Profile, profile_idc: u8, iop: ProfileIopPattern) -> Self {
1275            Self {
1276                profile,
1277                profile_idc,
1278                iop,
1279            }
1280        }
1281
1282        const fn exact(profile: Profile, profile_idc: u8, value: u8) -> Self {
1283            Self {
1284                profile,
1285                profile_idc,
1286                iop: ProfileIopPattern::exact(value),
1287            }
1288        }
1289
1290        const fn matches(self, profile_idc: u8, profile_iop: u8) -> bool {
1291            self.profile_idc == profile_idc && self.iop.matches(profile_iop)
1292        }
1293    }
1294
1295    /// masked `profile-iop` matcher for profile-equivalence wildcard bits
1296    ///
1297    /// `mask` selects bits that must equal `value`. unmasked bits are RFC
1298    /// wildcards such as the `x` bits in `x1xx0000`
1299    #[derive(Clone, Copy)]
1300    struct ProfileIopPattern {
1301        mask: u8,
1302        value: u8,
1303    }
1304
1305    impl ProfileIopPattern {
1306        /// starts with the low four bits fixed to zero and high bits wildcarded
1307        const fn wildcarded() -> Self {
1308            Self {
1309                mask: IOP_PROFILE_PATTERN_LOW_ZERO_MASK,
1310                value: IOP_NONE,
1311            }
1312        }
1313
1314        const fn exact(value: u8) -> Self {
1315            Self {
1316                mask: u8::MAX,
1317                value,
1318            }
1319        }
1320
1321        const fn one(self, bit: u8) -> Self {
1322            Self {
1323                mask: self.mask | bit,
1324                value: self.value | bit,
1325            }
1326        }
1327
1328        const fn zero(self, bit: u8) -> Self {
1329            Self {
1330                mask: self.mask | bit,
1331                value: self.value & !bit,
1332            }
1333        }
1334
1335        const fn matches(self, value: u8) -> bool {
1336            value & self.mask == self.value
1337        }
1338    }
1339
1340    // rfc 6184 lists equivalent sub-profile encodings rather than one canonical byte
1341    // example: `42 x1xx0000`, `4d 1xxx0000` and `58 11xx0000` all mean
1342    // constrained baseline, so `x` bits must be ignored during matching
1343    const PROFILE_PATTERNS: &[ProfilePattern] = &[
1344        ProfilePattern::masked(
1345            Profile::ConstrainedBaseline,
1346            BASELINE_IDC,
1347            ProfileIopPattern::wildcarded().one(IOP_CONSTRAINT_SET1),
1348        ),
1349        ProfilePattern::masked(
1350            Profile::ConstrainedBaseline,
1351            MAIN_IDC,
1352            ProfileIopPattern::wildcarded().one(IOP_CONSTRAINT_SET0),
1353        ),
1354        ProfilePattern::masked(
1355            Profile::ConstrainedBaseline,
1356            EXTENDED_IDC,
1357            ProfileIopPattern::wildcarded()
1358                .one(IOP_CONSTRAINT_SET0)
1359                .one(IOP_CONSTRAINT_SET1),
1360        ),
1361        ProfilePattern::masked(
1362            Profile::Baseline,
1363            BASELINE_IDC,
1364            ProfileIopPattern::wildcarded().zero(IOP_CONSTRAINT_SET1),
1365        ),
1366        ProfilePattern::masked(
1367            Profile::Baseline,
1368            EXTENDED_IDC,
1369            ProfileIopPattern::wildcarded()
1370                .one(IOP_CONSTRAINT_SET0)
1371                .zero(IOP_CONSTRAINT_SET1),
1372        ),
1373        ProfilePattern::masked(
1374            Profile::Main,
1375            MAIN_IDC,
1376            ProfileIopPattern::wildcarded()
1377                .zero(IOP_CONSTRAINT_SET0)
1378                .zero(IOP_CONSTRAINT_SET2),
1379        ),
1380        ProfilePattern::masked(
1381            Profile::Extended,
1382            EXTENDED_IDC,
1383            ProfileIopPattern::wildcarded()
1384                .zero(IOP_CONSTRAINT_SET0)
1385                .zero(IOP_CONSTRAINT_SET1),
1386        ),
1387        ProfilePattern::exact(Profile::High, HIGH_IDC, IOP_NONE),
1388        ProfilePattern::exact(Profile::High10, HIGH_10_IDC, IOP_NONE),
1389        ProfilePattern::exact(Profile::High422, HIGH_422_IDC, IOP_NONE),
1390        ProfilePattern::exact(Profile::High444Predictive, HIGH_444_IDC, IOP_NONE),
1391        ProfilePattern::exact(Profile::High10Intra, HIGH_10_IDC, IOP_CONSTRAINT_SET3),
1392        ProfilePattern::exact(Profile::High422Intra, HIGH_422_IDC, IOP_CONSTRAINT_SET3),
1393        ProfilePattern::exact(Profile::High444Intra, HIGH_444_IDC, IOP_CONSTRAINT_SET3),
1394        ProfilePattern::exact(Profile::Cavlc444Intra, CAVLC_444_IDC, IOP_CONSTRAINT_SET3),
1395    ];
1396
1397    const fn profile_level_id_bytes(profile: Profile, level: LevelIdc) -> (u8, u8, u8) {
1398        let (profile_idc, profile_iop) = profile_bytes(profile);
1399        match level {
1400            LevelIdc::Level1B => level_1b_profile_level_id_bytes(profile_idc, profile_iop),
1401            _ => (profile_idc, profile_iop, level_idc_value(level)),
1402        }
1403    }
1404
1405    const fn profile_bytes(profile: Profile) -> (u8, u8) {
1406        match profile {
1407            Profile::Baseline => (BASELINE_IDC, IOP_NONE),
1408            Profile::ConstrainedBaseline => (BASELINE_IDC, IOP_CONSTRAINED_BASELINE),
1409            Profile::Main => (MAIN_IDC, IOP_NONE),
1410            Profile::Extended => (EXTENDED_IDC, IOP_NONE),
1411            Profile::High => (HIGH_IDC, IOP_NONE),
1412            Profile::High10 => (HIGH_10_IDC, IOP_NONE),
1413            Profile::High422 => (HIGH_422_IDC, IOP_NONE),
1414            Profile::High444Predictive => (HIGH_444_IDC, IOP_NONE),
1415            Profile::High10Intra => (HIGH_10_IDC, IOP_CONSTRAINT_SET3),
1416            Profile::High422Intra => (HIGH_422_IDC, IOP_CONSTRAINT_SET3),
1417            Profile::High444Intra => (HIGH_444_IDC, IOP_CONSTRAINT_SET3),
1418            Profile::Cavlc444Intra => (CAVLC_444_IDC, IOP_CONSTRAINT_SET3),
1419        }
1420    }
1421
1422    // level 1b has no single `level_idc` byte
1423    // baseline, main and extended borrow level 1.1's `level_idc=11` and set
1424    // constraint_set3_flag, while other profiles use `level_idc=9`
1425    const fn level_1b_profile_level_id_bytes(profile_idc: u8, profile_iop: u8) -> (u8, u8, u8) {
1426        match profile_idc {
1427            BASELINE_IDC | MAIN_IDC | EXTENDED_IDC => (
1428                profile_idc,
1429                profile_iop | IOP_CONSTRAINT_SET3,
1430                LEVEL_1_1_IDC,
1431            ),
1432            _ => (profile_idc, profile_iop, LEVEL_1B_OTHER_IDC),
1433        }
1434    }
1435
1436    const fn level_idc_value(level: LevelIdc) -> u8 {
1437        match level {
1438            LevelIdc::Level1 => 10,
1439            LevelIdc::Level1B => LEVEL_1B_OTHER_IDC,
1440            LevelIdc::Level1_1 => 11,
1441            LevelIdc::Level1_2 => 12,
1442            LevelIdc::Level1_3 => 13,
1443            LevelIdc::Level2 => 20,
1444            LevelIdc::Level2_1 => 21,
1445            LevelIdc::Level2_2 => 22,
1446            LevelIdc::Level3 => 30,
1447            LevelIdc::Level3_1 => 31,
1448            LevelIdc::Level3_2 => 32,
1449            LevelIdc::Level4 => 40,
1450            LevelIdc::Level4_1 => 41,
1451            LevelIdc::Level4_2 => 42,
1452            LevelIdc::Level5 => 50,
1453            LevelIdc::Level5_1 => 51,
1454            LevelIdc::Level5_2 => 52,
1455        }
1456    }
1457
1458    #[expect(clippy::as_conversions, reason = "u8 to u32 widening is lossless")]
1459    const fn pack_profile_level_id(profile_idc: u8, profile_iop: u8, level_idc: u8) -> u32 {
1460        ((profile_idc as u32) << 16) | ((profile_iop as u32) << 8) | level_idc as u32
1461    }
1462
1463    fn normalized_level_idc(profile_idc: u8, profile_iop: u8, level_idc: u8) -> Option<LevelIdc> {
1464        // reject the non-canonical level 1b form for baseline, main and extended
1465        // those profile families must use `level_idc=11` plus constraint_set3_flag
1466        if LEVEL_1B_IDCS.contains(&profile_idc) {
1467            if level_idc == LEVEL_1B_OTHER_IDC {
1468                return None;
1469            }
1470            if level_idc == LEVEL_1_1_IDC {
1471                return if (profile_iop & IOP_CONSTRAINT_SET3) != 0 {
1472                    Some(LevelIdc::Level1B)
1473                } else {
1474                    Some(LevelIdc::Level1_1)
1475                };
1476            }
1477        }
1478        LevelIdc::try_from(level_idc).ok()
1479    }
1480
1481    fn profile_from_bytes(profile_idc: u8, profile_iop: u8) -> Option<Profile> {
1482        PROFILE_PATTERNS.iter().copied().find_map(|pattern| {
1483            pattern
1484                .matches(profile_idc, profile_iop)
1485                .then_some(pattern.profile)
1486        })
1487    }
1488
1489    fn parse_profile_level_id_bytes(value: &[u8]) -> Option<[u8; 3]> {
1490        let [
1491            first_high,
1492            first_low,
1493            second_high,
1494            second_low,
1495            third_high,
1496            third_low,
1497        ] = value
1498        else {
1499            return None;
1500        };
1501        Some([
1502            decode_hex_byte(*first_high, *first_low)?,
1503            decode_hex_byte(*second_high, *second_low)?,
1504            decode_hex_byte(*third_high, *third_low)?,
1505        ])
1506    }
1507
1508    fn decode_hex_byte(high: u8, low: u8) -> Option<u8> {
1509        Some((decode_hex_nibble(high)? << 4) | decode_hex_nibble(low)?)
1510    }
1511
1512    fn decode_hex_nibble(value: u8) -> Option<u8> {
1513        match value {
1514            b'0'..=b'9' => Some(value - b'0'),
1515            b'a'..=b'f' => Some(value - b'a' + 10),
1516            b'A'..=b'F' => Some(value - b'A' + 10),
1517            _ => None,
1518        }
1519    }
1520}
1521
1522/// Returns `true` if `payload_type` is an RTP/AVP dynamic payload type.
1523///
1524/// Use this for the `PT` field from the RTP fixed header. It does not validate
1525/// whether a signaling layer actually negotiated that payload type.
1526///
1527/// Reference: RFC 3551 section 6.
1528#[must_use]
1529pub const fn is_dynamic_payload_type(payload_type: u8) -> bool {
1530    payload_type >= RTP_DYNAMIC_PAYLOAD_TYPE_START && payload_type <= RTP_DYNAMIC_PAYLOAD_TYPE_END
1531}
1532
1533/// Returns `true` if `payload_type` fits in the RTP fixed-header PT field.
1534///
1535/// Reference: RFC 3550 section 5.1.
1536#[must_use]
1537pub const fn is_payload_type(payload_type: u8) -> bool {
1538    payload_type <= RTP_PAYLOAD_TYPE_MAX
1539}
1540
1541/// Returns `true` if `payload_type` can be used with RTP/RTCP mux.
1542///
1543/// Reference: RFC 5761 section 4.
1544#[must_use]
1545pub const fn is_rtcp_mux_payload_type(payload_type: u8) -> bool {
1546    is_payload_type(payload_type)
1547        && (payload_type < RTP_RTCP_MUX_FORBIDDEN_PAYLOAD_TYPE_START
1548            || payload_type > RTP_RTCP_MUX_FORBIDDEN_PAYLOAD_TYPE_END)
1549}
1550
1551/// Returns whether `payload_type` can be assigned dynamically in an RTP/RTCP
1552/// muxed RTP/AVP session.
1553///
1554/// RFC 3551 reserves 96 through 127 for dynamic assignment and leaves several
1555/// lower ranges unassigned. RFC 5761 permits muxed RTP to use values below 64
1556/// while excluding 64 through 95 from RTP assignment.
1557///
1558/// References:
1559/// - <https://www.rfc-editor.org/rfc/rfc3551.html#section-6>
1560/// - <https://www.rfc-editor.org/rfc/rfc5761.html#section-4>
1561#[must_use]
1562pub const fn is_rtcp_mux_dynamic_payload_type(payload_type: u8) -> bool {
1563    is_dynamic_payload_type(payload_type)
1564        || matches!(payload_type, 20..=24 | 27 | 29..=30 | 35..=63)
1565}
1566
1567/// Classifies an RTP or RTCP candidate in an RTP/RTCP muxed datagram.
1568///
1569/// The packet must contain the first two header octets plus one body octet,
1570/// matching the minimum boundary used before complete RTP or RTCP parsing.
1571///
1572/// Reference: <https://www.rfc-editor.org/rfc/rfc5761.html#section-4>
1573#[must_use]
1574pub fn classify_rtp_rtcp_mux(packet: &[u8]) -> Option<RtpRtcpMuxPacketKind> {
1575    let [first, second, _, ..] = packet else {
1576        return None;
1577    };
1578    if first & RTP_VERSION_MASK != RTP_VERSION_HEADER_BITS {
1579        return None;
1580    }
1581    let payload_type = second & RTP_PAYLOAD_TYPE_MASK;
1582    if (RTP_RTCP_MUX_FORBIDDEN_PAYLOAD_TYPE_START..=RTP_RTCP_MUX_FORBIDDEN_PAYLOAD_TYPE_END)
1583        .contains(&payload_type)
1584    {
1585        Some(RtpRtcpMuxPacketKind::Rtcp)
1586    } else {
1587        Some(RtpRtcpMuxPacketKind::Rtp)
1588    }
1589}
1590
1591/// Parses the fixed RTP header of an RTP candidate in a muxed session.
1592///
1593/// The parser rejects RTCP candidates, non-RTP versions and packets truncated
1594/// within the fixed header or declared CSRC list. Codec payload and padding
1595/// validation belong to the payload parser.
1596///
1597/// References:
1598/// - <https://www.rfc-editor.org/rfc/rfc3550.html#section-5.1>
1599/// - <https://www.rfc-editor.org/rfc/rfc5761.html#section-4>
1600#[must_use]
1601pub fn parse_muxed_rtp_fixed_header(packet: &[u8]) -> Option<RtpFixedHeader> {
1602    if classify_rtp_rtcp_mux(packet) != Some(RtpRtcpMuxPacketKind::Rtp) {
1603        return None;
1604    }
1605    let first = *packet.first()?;
1606    let second = *packet.get(1)?;
1607    let header = RtpFixedHeader {
1608        payload_type: second & RTP_PAYLOAD_TYPE_MASK,
1609        sequence_number: read_u16(packet, RTP_SEQUENCE_NUMBER_OFFSET)?,
1610        timestamp: read_u32(packet, RTP_TIMESTAMP_OFFSET)?,
1611        ssrc: Ssrc::new(read_u32(packet, RTP_SSRC_OFFSET)?),
1612        marker: second & RTP_MARKER_MASK != 0,
1613        has_padding: first & RTP_PADDING_MASK != 0,
1614        has_extension: first & RTP_EXTENSION_MASK != 0,
1615        csrc_count: first & RTP_CSRC_COUNT_MASK,
1616    };
1617    packet.get(..header.extension_offset())?;
1618    Some(header)
1619}
1620
1621/// Expands one Generic NACK PID and bitmask into requested RTP sequence
1622/// numbers.
1623///
1624/// The PID is yielded first. Each set BLP bit yields PID + bit index + 1 with
1625/// 16-bit RTP sequence-number rollover.
1626///
1627/// Reference: <https://www.rfc-editor.org/rfc/rfc4585.html#section-6.2.1>
1628pub fn generic_nack_sequence_numbers(pid: u16, blp: u16) -> impl Iterator<Item = u16> {
1629    iter::once(pid).chain(
1630        (0_u16..GENERIC_NACK_BITMASK_BITS)
1631            .filter(move |bit| blp & (1_u16 << bit) != 0)
1632            .map(move |bit| pid.wrapping_add(bit + 1)),
1633    )
1634}
1635
1636/// Extracts the original RTP sequence number from an RTX payload.
1637///
1638/// Reference: <https://www.rfc-editor.org/rfc/rfc4588.html#section-4>
1639#[must_use]
1640pub fn rtx_original_sequence_number(payload: &[u8]) -> Option<u16> {
1641    read_u16(payload, 0)
1642}
1643
1644/// Builds an RTCP Receiver Report with no reception report blocks.
1645///
1646/// Reference: <https://www.rfc-editor.org/rfc/rfc3550.html#section-6.4.2>
1647#[must_use]
1648pub fn rtcp_receiver_report_without_report_blocks(
1649    sender_ssrc: Ssrc,
1650) -> [u8; RTCP_RECEIVER_REPORT_WITHOUT_BLOCKS_BYTES] {
1651    let [length_high, length_low] =
1652        RTCP_RECEIVER_REPORT_WITHOUT_BLOCKS_LENGTH_WORDS_MINUS_ONE.to_be_bytes();
1653    let [ssrc_0, ssrc_1, ssrc_2, ssrc_3] = sender_ssrc.value().to_be_bytes();
1654    [
1655        RTP_VERSION_HEADER_BITS,
1656        rtcp_packet_type::RR,
1657        length_high,
1658        length_low,
1659        ssrc_0,
1660        ssrc_1,
1661        ssrc_2,
1662        ssrc_3,
1663    ]
1664}
1665
1666fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
1667    bytes
1668        .get(offset..offset.checked_add(mem::size_of::<u16>())?)?
1669        .try_into()
1670        .ok()
1671        .map(u16::from_be_bytes)
1672}
1673
1674fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
1675    bytes
1676        .get(offset..offset.checked_add(mem::size_of::<u32>())?)?
1677        .try_into()
1678        .ok()
1679        .map(u32::from_be_bytes)
1680}
1681
1682/// rtcp common-header `PT` value namespace
1683///
1684/// reference: RFC 3550 section 12.1 and RFC 4585 section 6.1
1685pub mod rtcp_packet_type {
1686    /// sender report packet
1687    pub const SR: u8 = 200;
1688    /// receiver report packet
1689    pub const RR: u8 = 201;
1690    /// source description packet
1691    pub const SDES: u8 = 202;
1692    /// goodbye packet
1693    pub const BYE: u8 = 203;
1694    /// application-defined packet
1695    pub const APP: u8 = 204;
1696
1697    /// transport-layer feedback packet
1698    pub const RTPFB: u8 = 205;
1699    /// payload-specific feedback packet
1700    pub const PSFB: u8 = 206;
1701}
1702
1703/// rtcp SDES item `type` value namespace
1704///
1705/// reference: RFC 3550 section 12.2 and RFC 8852 section 4
1706pub mod rtcp_sdes_item {
1707    /// canonical end-point identifier
1708    pub const CNAME: u8 = 1;
1709    /// user name
1710    pub const NAME: u8 = 2;
1711    /// email address
1712    pub const EMAIL: u8 = 3;
1713    /// phone number
1714    pub const PHONE: u8 = 4;
1715    /// geographic location
1716    pub const LOC: u8 = 5;
1717    /// application or tool name
1718    pub const TOOL: u8 = 6;
1719    /// transient note
1720    pub const NOTE: u8 = 7;
1721    /// private extension item
1722    pub const PRIV: u8 = 8;
1723
1724    /// RTP stream identifier carried in an SDES packet
1725    pub const RTP_STREAM_ID: u8 = 12;
1726    /// RTP stream repaired by a redundancy stream
1727    pub const REPAIRED_RTP_STREAM_ID: u8 = 13;
1728}
1729
1730/// RTCP feedback FMT values.
1731pub mod rtcp_feedback_format {
1732    /// Generic NACK FMT value for RTPFB packets.
1733    ///
1734    /// Reference: RFC 4585 section 6.2.1.
1735    pub const RTPFB_GENERIC_NACK: u8 = 1;
1736
1737    /// Picture Loss Indication FMT value for PSFB packets.
1738    ///
1739    /// Reference: RFC 4585 section 6.3.1.
1740    pub const PSFB_PLI: u8 = 1;
1741
1742    /// Full Intra Request FMT value for PSFB packets.
1743    ///
1744    /// Reference: RFC 5104 section 4.3.1.
1745    pub const PSFB_FIR: u8 = 4;
1746
1747    /// Layer Refresh Request FMT value for PSFB packets.
1748    ///
1749    /// Reference: RFC 9627 section 8.
1750    pub const PSFB_LRR: u8 = 10;
1751}
1752
1753/// RTP header-extension profile IDs from RFC 8285.
1754///
1755/// RTP packets carry an extension block only when the fixed-header `X` bit is
1756/// set. The 16-bit profile ID then decides whether extension elements use the
1757/// one-byte or two-byte element header shape.
1758///
1759/// ```text
1760/// RTP extension block
1761///
1762/// +----------------+----------------+-----------------------------+
1763/// | profile ID     | length in u32  | extension elements          |
1764/// +----------------+----------------+-----------------------------+
1765/// | 16 bits        | 16 bits        | padded to 32-bit boundary   |
1766/// +----------------+----------------+-----------------------------+
1767///
1768/// One-byte element, profile 0xBEDE
1769///
1770/// +---------+---------+----------------------+
1771/// | ID      | len-1   | data                 |
1772/// +---------+---------+----------------------+
1773/// | 4 bits  | 4 bits  | 1 to 16 bytes        |
1774/// +---------+---------+----------------------+
1775///
1776/// Two-byte element, profile 0x1000 through 0x100F
1777///
1778/// +---------+---------+----------------------+
1779/// | ID      | len     | data                 |
1780/// +---------+---------+----------------------+
1781/// | 8 bits  | 8 bits  | 0 to 255 bytes       |
1782/// +---------+---------+----------------------+
1783/// ```
1784pub mod header_extension {
1785    const HEADER_BYTES: usize = 4;
1786    const WORD_BYTES: usize = 4;
1787    const ONE_BYTE_ID_SHIFT: u32 = 4;
1788    const ONE_BYTE_LENGTH_MASK: u8 = 0b0000_1111;
1789    const ONE_BYTE_LENGTH_OFFSET: usize = 1;
1790
1791    /// RFC 8285 one-byte header extension profile ID.
1792    pub const ONE_BYTE_PROFILE_ID: u16 = 0xBEDE;
1793
1794    /// Base value for the RFC 8285 two-byte header profile with appbits set to 0.
1795    pub const TWO_BYTE_PROFILE_ID_BASE: u16 = 0x1000;
1796
1797    /// Mask for the fixed 12-bit prefix (`0x100`) in the two-byte profile ID.
1798    pub const TWO_BYTE_PROFILE_PREFIX_MASK: u16 = 0xFFF0;
1799
1800    /// One-byte header extension identifier values.
1801    pub const ONE_BYTE_ID_PAD: u8 = 0;
1802    pub const ONE_BYTE_ID_MIN: u8 = 1;
1803    pub const ONE_BYTE_ID_MAX: u8 = 14;
1804    pub const ONE_BYTE_ID_RESERVED: u8 = 15;
1805
1806    /// One-byte header extension data size bounds from RFC 8285 section 4.2.
1807    pub const ONE_BYTE_DATA_LEN_MIN: u8 = 1;
1808    pub const ONE_BYTE_DATA_LEN_MAX: u8 = 16;
1809
1810    /// Two-byte header extension data size bounds from RFC 8285 section 4.3.
1811    pub const TWO_BYTE_DATA_LEN_MIN: u8 = 0;
1812    pub const TWO_BYTE_DATA_LEN_MAX: u8 = u8::MAX;
1813
1814    /// Returns whether `id` is usable as an RFC 8285 one-byte element ID.
1815    ///
1816    /// ID 0 is padding and ID 15 is reserved, so only 1 through 14 can identify
1817    /// negotiated extension values.
1818    #[must_use]
1819    pub const fn is_one_byte_id(id: u8) -> bool {
1820        id >= ONE_BYTE_ID_MIN && id <= ONE_BYTE_ID_MAX
1821    }
1822
1823    /// Builds an RFC 8285 two-byte profile ID from the 4-bit appbits value.
1824    ///
1825    /// Returns `None` when `appbits` cannot fit in the low nibble of the
1826    /// profile ID.
1827    #[must_use]
1828    pub fn two_byte_profile_id(appbits: u8) -> Option<u16> {
1829        if appbits > 0x0F {
1830            return None;
1831        }
1832        Some(TWO_BYTE_PROFILE_ID_BASE | u16::from(appbits))
1833    }
1834
1835    /// Finds an RFC 8285 one-byte extension element in a complete muxed RTP
1836    /// packet.
1837    ///
1838    /// Zero octets are padding. A nonzero element with ID 0 is malformed and
1839    /// ID 15 terminates processing, so neither can expose a later element.
1840    /// Packets without the one-byte extension profile return `None`.
1841    ///
1842    /// References:
1843    /// - <https://www.rfc-editor.org/rfc/rfc8285.html#section-4.1.2>
1844    /// - <https://www.rfc-editor.org/rfc/rfc8285.html#section-4.2>
1845    #[must_use]
1846    pub fn find_one_byte_element(packet: &[u8], target_id: u8) -> Option<&[u8]> {
1847        if !is_one_byte_id(target_id) {
1848            return None;
1849        }
1850        let header = super::parse_muxed_rtp_fixed_header(packet)?;
1851        if !header.has_extension() {
1852            return None;
1853        }
1854        let extension_offset = header.extension_offset();
1855        let profile = super::read_u16(packet, extension_offset)?;
1856        if profile != ONE_BYTE_PROFILE_ID {
1857            return None;
1858        }
1859        let word_count = super::read_u16(
1860            packet,
1861            extension_offset.checked_add(super::mem::size_of::<u16>())?,
1862        )?;
1863        let elements_offset = extension_offset.checked_add(HEADER_BYTES)?;
1864        let elements_bytes = usize::from(word_count).checked_mul(WORD_BYTES)?;
1865        let mut elements =
1866            packet.get(elements_offset..elements_offset.checked_add(elements_bytes)?)?;
1867
1868        while let Some((&element_header, rest)) = elements.split_first() {
1869            elements = rest;
1870            if element_header == ONE_BYTE_ID_PAD {
1871                continue;
1872            }
1873            let id = element_header >> ONE_BYTE_ID_SHIFT;
1874            if id == ONE_BYTE_ID_PAD || id == ONE_BYTE_ID_RESERVED {
1875                return None;
1876            }
1877            let length = usize::from(element_header & ONE_BYTE_LENGTH_MASK)
1878                .checked_add(ONE_BYTE_LENGTH_OFFSET)?;
1879            let value = elements.get(..length)?;
1880            elements = elements.get(length..)?;
1881            if id == target_id {
1882                return Some(value);
1883            }
1884        }
1885        None
1886    }
1887}
1888
1889/// Video Frame Marking RTP header-extension payload values.
1890///
1891/// Frame marking is carried as one negotiated RTP header extension
1892///
1893/// These helpers retain temporal-layer fields for future negotiated SVC
1894/// selection while current packet gates match simulcast RIDs
1895///
1896/// ```text
1897/// Short form, non-scalable stream
1898///
1899/// +---+---+---+---+---------------+
1900/// | S | E | I | D | reserved      |
1901/// +---+---+---+---+---------------+
1902///
1903/// Long form, scalable stream
1904///
1905/// +---+---+---+---+---+-----------+---------------+---------------+
1906/// | S | E | I | D | B | TID       | LID           | TL0PICIDX     |
1907/// +---+---+---+---+---+-----------+---------------+---------------+
1908/// | first octet                    | optional      | optional      |
1909/// +---+---+---+---+---+-----------+---------------+---------------+
1910/// ```
1911///
1912/// Reference: RFC 9626 section 3.
1913pub mod frame_marking {
1914    /// Full long-form frame-marking payload length.
1915    pub const LONG_DATA_LEN_WITH_TL0PICIDX: u8 = 3;
1916
1917    /// Long-form payload length when `TL0PICIDX` is omitted.
1918    pub const LONG_DATA_LEN_WITHOUT_TL0PICIDX: u8 = 2;
1919
1920    /// Long-form payload length when both `LID` and `TL0PICIDX` are omitted.
1921    pub const LONG_DATA_LEN_FLAGS_ONLY: u8 = 1;
1922
1923    /// Short-form non-scalable frame-marking payload length.
1924    pub const SHORT_DATA_LEN: u8 = 1;
1925
1926    /// Start-of-frame flag in the first frame-marking octet.
1927    pub const START_OF_FRAME_MASK: u8 = 0b1000_0000;
1928
1929    /// End-of-frame flag in the first frame-marking octet.
1930    pub const END_OF_FRAME_MASK: u8 = 0b0100_0000;
1931
1932    /// Independent-frame flag in the first frame-marking octet.
1933    pub const INDEPENDENT_FRAME_MASK: u8 = 0b0010_0000;
1934
1935    /// Discardable-frame flag in the first frame-marking octet.
1936    pub const DISCARDABLE_FRAME_MASK: u8 = 0b0001_0000;
1937
1938    /// Base-layer-sync flag in the long-form first octet.
1939    pub const BASE_LAYER_SYNC_MASK: u8 = 0b0000_1000;
1940
1941    /// Temporal-layer identifier bits in the long-form first octet.
1942    pub const TEMPORAL_LAYER_ID_MASK: u8 = 0b0000_0111;
1943
1944    /// Maximum temporal-layer identifier representable by the 3-bit TID field.
1945    pub const TEMPORAL_LAYER_ID_MAX: u8 = 7;
1946
1947    /// Base layer identifier used for TID and LID.
1948    pub const BASE_LAYER_ID: u8 = 0;
1949
1950    /// Extracts the temporal-layer ID from the first long-form frame-marking
1951    /// octet.
1952    ///
1953    /// Callers must only use this value as temporal metadata when signaling or
1954    /// negotiated extension state proves the packet carries frame marking.
1955    #[must_use]
1956    pub const fn temporal_layer_id(first_octet: u8) -> u8 {
1957        first_octet & TEMPORAL_LAYER_ID_MASK
1958    }
1959
1960    /// Returns whether `value` fits in the RFC 9626 three-bit temporal-layer
1961    /// field.
1962    #[must_use]
1963    pub const fn is_valid_temporal_layer_id(value: u8) -> bool {
1964        value <= TEMPORAL_LAYER_ID_MAX
1965    }
1966}
1967
1968#[cfg(test)]
1969#[path = "TESTS/rtp.rs"]
1970mod tests;