Skip to main content

g2g_core/
rtp.rs

1//! RFC 3550 RTP fixed header (M643): the 12-byte wire header every RTP
2//! packetizer in the workspace emits, defined once. Pure `const fn`
3//! byte-building, part of the heap-free subset, so the MCU packet sink and
4//! the std packetizers in `g2g-plugins` (H.264, the ST 2110 essences) share
5//! one implementation instead of five hand-rolled copies.
6//!
7//! Only the fixed header lives here: V=2, no padding, no extension, no CSRC
8//! list, which is the shape every g2g payload format uses. Payload-format
9//! headers (FU-A, RFC 4175 SRDs, RFC 8331 ANC) stay with their packetizers.
10
11/// RTP fixed header length: V=2 with no CSRC list and no extension.
12pub const RTP_HEADER_LEN: usize = 12;
13
14/// The RFC 3550 fixed header fields a packetizer chooses per packet.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct RtpHeader {
17    /// 7-bit payload type (a static PT like 0 = PCMU, or a dynamic 96..=127).
18    pub payload_type: u8,
19    /// Frame/talkspurt boundary marker; payload-format-defined semantics.
20    pub marker: bool,
21    /// Per-packet sequence number (the packetizer increments, wrapping).
22    pub sequence: u16,
23    /// Media-clock timestamp of the payload's sampling instant.
24    pub timestamp: u32,
25    /// Synchronization source identifier.
26    pub ssrc: u32,
27}
28
29/// A parsed RTP packet: the fixed-header fields plus where the payload sits in
30/// the datagram, after any CSRC list / extension header and before any padding.
31/// [`RtpHeader::parse`] returns this; a depacketizer reads
32/// `buf[payload_offset..payload_offset + payload_len]`.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct RtpParsed {
35    /// The fixed-header fields (`RtpHeader::to_bytes` round-trips these).
36    pub header: RtpHeader,
37    /// Byte offset of the payload within the parsed datagram.
38    pub payload_offset: usize,
39    /// Payload length in bytes (padding already stripped).
40    pub payload_len: usize,
41}
42
43/// The fields and byte length of an RTP header. Unlike [`RtpParsed`], this
44/// does not inspect the payload or its padding count, so SRTP can parse the
45/// authenticated header before decrypting the payload and padding.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct RtpParsedHeader {
48    /// The fixed-header fields (`RtpHeader::to_bytes` round-trips these).
49    pub header: RtpHeader,
50    /// Byte offset immediately after the CSRC list and extension header.
51    pub payload_offset: usize,
52}
53
54impl RtpHeader {
55    /// Parse an RTP datagram received from an arbitrary peer, returning the
56    /// fixed-header fields and the payload's byte range. The inverse of
57    /// [`to_bytes`](Self::to_bytes), but tolerant of the header variations a
58    /// real sender may set that `to_bytes` never emits: a CSRC list (`CC`), a
59    /// profile extension header (`X`), and payload padding (`P`).
60    ///
61    /// Parser discipline (never trust the wire, per the demuxer rule): every
62    /// offset and length is folded with checked arithmetic and bounds-checked
63    /// against `buf`, so a malformed or truncated datagram returns `None`
64    /// rather than panicking or reading out of bounds. A non-RTP-v2 packet is
65    /// rejected. Heap-free (part of the no-alloc subset), so an MCU RTP source
66    /// uses it directly.
67    pub fn parse(buf: &[u8]) -> Option<RtpParsed> {
68        let parsed_header = Self::parse_header(buf)?;
69        let b0 = *buf.first()?;
70        let has_padding = b0 & 0x20 != 0;
71
72        // Padding, if present, is counted by the datagram's last byte.
73        let mut end = buf.len();
74        if has_padding {
75            let pad = *buf.get(end.checked_sub(1)?)? as usize;
76            end = end.checked_sub(pad)?;
77        }
78        if parsed_header.payload_offset > end {
79            return None; // header (and padding) overrun the datagram
80        }
81        Some(RtpParsed {
82            header: parsed_header.header,
83            payload_offset: parsed_header.payload_offset,
84            payload_len: end - parsed_header.payload_offset,
85        })
86    }
87
88    /// Parse the authenticated RTP header without reading the payload. This
89    /// accepts CSRC identifiers and an RTP extension and rejects any header
90    /// whose declared fields exceed `buf`.
91    pub fn parse_header(buf: &[u8]) -> Option<RtpParsedHeader> {
92        let b0 = *buf.first()?;
93        if b0 >> 6 != 2 {
94            return None;
95        }
96        let has_extension = b0 & 0x10 != 0;
97        let csrc_count = (b0 & 0x0F) as usize;
98        let b1 = *buf.get(1)?;
99        let marker = b1 & 0x80 != 0;
100        let payload_type = b1 & 0x7F;
101        let sequence = u16::from_be_bytes([*buf.get(2)?, *buf.get(3)?]);
102        let timestamp =
103            u32::from_be_bytes([*buf.get(4)?, *buf.get(5)?, *buf.get(6)?, *buf.get(7)?]);
104        let ssrc = u32::from_be_bytes([*buf.get(8)?, *buf.get(9)?, *buf.get(10)?, *buf.get(11)?]);
105
106        let mut payload_offset = RTP_HEADER_LEN.checked_add(csrc_count.checked_mul(4)?)?;
107        if has_extension {
108            let extension_length_high = *buf.get(payload_offset.checked_add(2)?)?;
109            let extension_length_low = *buf.get(payload_offset.checked_add(3)?)?;
110            let extension_words =
111                u16::from_be_bytes([extension_length_high, extension_length_low]) as usize;
112            payload_offset = payload_offset
113                .checked_add(4)?
114                .checked_add(extension_words.checked_mul(4)?)?;
115        }
116        if payload_offset > buf.len() {
117            return None;
118        }
119
120        Some(RtpParsedHeader {
121            header: RtpHeader {
122                payload_type,
123                marker,
124                sequence,
125                timestamp,
126                ssrc,
127            },
128            payload_offset,
129        })
130    }
131
132    /// The header as it goes on the wire. Pure and heap-free; a packetizer
133    /// prepends this to its payload.
134    pub const fn to_bytes(self) -> [u8; RTP_HEADER_LEN] {
135        let seq = self.sequence.to_be_bytes();
136        let ts = self.timestamp.to_be_bytes();
137        let ssrc = self.ssrc.to_be_bytes();
138        [
139            0x80, // V=2, P=0, X=0, CC=0
140            (if self.marker { 0x80 } else { 0 }) | (self.payload_type & 0x7F),
141            seq[0],
142            seq[1],
143            ts[0],
144            ts[1],
145            ts[2],
146            ts[3],
147            ssrc[0],
148            ssrc[1],
149            ssrc[2],
150            ssrc[3],
151        ]
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn header_bytes_match_the_wire_layout() {
161        let h = RtpHeader {
162            payload_type: 96,
163            marker: true,
164            sequence: 0x0102,
165            timestamp: 0x0304_0506,
166            ssrc: 0x0708_090A,
167        };
168        assert_eq!(
169            h.to_bytes(),
170            [0x80, 0x80 | 96, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0A],
171            "V=2 | M+PT | seq | timestamp | ssrc, all big-endian"
172        );
173        let unmarked = RtpHeader { marker: false, ..h };
174        assert_eq!(
175            unmarked.to_bytes()[1],
176            96,
177            "marker bit clear leaves the bare PT"
178        );
179        // PT is 7 bits: bit 7 of an oversized PT must not leak into marker.
180        let overwide = RtpHeader {
181            payload_type: 0xFF,
182            marker: false,
183            ..h
184        };
185        assert_eq!(
186            overwide.to_bytes()[1],
187            0x7F,
188            "payload type masked to 7 bits"
189        );
190    }
191
192    #[test]
193    fn parse_round_trips_to_bytes_and_finds_the_payload() {
194        let h = RtpHeader {
195            payload_type: 0, // PCMU
196            marker: true,
197            sequence: 0x1234,
198            timestamp: 0xAABB_CCDD,
199            ssrc: 0x0011_2233,
200        };
201        let mut dgram = [0u8; RTP_HEADER_LEN + 4];
202        dgram[..RTP_HEADER_LEN].copy_from_slice(&h.to_bytes());
203        dgram[RTP_HEADER_LEN..].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
204        let p = RtpHeader::parse(&dgram).expect("valid RTP");
205        assert_eq!(
206            p.header, h,
207            "fixed fields round-trip through to_bytes/parse"
208        );
209        assert_eq!(p.payload_offset, RTP_HEADER_LEN);
210        assert_eq!(p.payload_len, 4);
211        assert_eq!(
212            &dgram[p.payload_offset..p.payload_offset + p.payload_len],
213            &[0xDE, 0xAD, 0xBE, 0xEF]
214        );
215    }
216
217    #[test]
218    fn parse_handles_csrc_extension_and_padding() {
219        // V=2, P=1, X=1, CC=1; PT=96, no marker; then 1 CSRC word, a 1-word
220        // extension, 2 payload bytes, and 2 padding bytes (last byte = 2).
221        let d: [u8; 26] = [
222            0b1011_0001,
223            96, // V/P/X/CC, M/PT
224            0x00,
225            0x01, // sequence
226            0,
227            0,
228            0,
229            0, // timestamp
230            0,
231            0,
232            0,
233            0, // ssrc
234            9,
235            9,
236            9,
237            9, // 1 CSRC identifier
238            0xBE,
239            0xDE,
240            0x00,
241            0x01, // ext profile + len = 1 word
242            1,
243            2,
244            3,
245            4, // 1 word of extension data
246            0x55,
247            0x66, // payload (padding count byte follows in the next 2)
248        ];
249        // Append the 2 padding bytes (0x00, count=2) to a 28-byte datagram.
250        let mut dg = [0u8; 28];
251        dg[..26].copy_from_slice(&d);
252        dg[26] = 0;
253        dg[27] = 2;
254        let p = RtpHeader::parse(&dg).expect("valid RTP with CC/X/P");
255        assert_eq!(p.header.payload_type, 96);
256        assert_eq!(p.payload_len, 2, "CSRC, extension and padding all excluded");
257        assert_eq!(
258            &dg[p.payload_offset..p.payload_offset + p.payload_len],
259            &[0x55, 0x66]
260        );
261
262        dg[27] = 0xFF;
263        let header = RtpHeader::parse_header(&dg).expect("payload is opaque to header parsing");
264        assert_eq!(header.payload_offset, 24);
265        assert!(
266            RtpHeader::parse(&dg).is_none(),
267            "full parsing checks padding"
268        );
269    }
270
271    #[test]
272    fn parse_rejects_malformed_input_without_panicking() {
273        assert!(RtpHeader::parse(&[]).is_none(), "empty");
274        assert!(
275            RtpHeader::parse(&[0x80, 0]).is_none(),
276            "truncated fixed header"
277        );
278        assert!(RtpHeader::parse(&[0x00; 12]).is_none(), "version != 2");
279        // CC=15 claims 60 CSRC bytes a 12-byte datagram does not have.
280        let mut d = [0u8; RTP_HEADER_LEN];
281        d[0] = 0x8F; // V=2, CC=15
282        assert!(
283            RtpHeader::parse(&d).is_none(),
284            "CSRC list overruns the datagram"
285        );
286        // Padding count larger than the datagram.
287        let mut pad = [0u8; RTP_HEADER_LEN + 1];
288        pad[0] = 0xA0; // V=2, P=1
289        pad[RTP_HEADER_LEN] = 0xFF; // pad count 255 > available
290        assert!(RtpHeader::parse(&pad).is_none(), "padding underflows");
291    }
292}