1pub const RTP_HEADER_LEN: usize = 12;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct RtpHeader {
17 pub payload_type: u8,
19 pub marker: bool,
21 pub sequence: u16,
23 pub timestamp: u32,
25 pub ssrc: u32,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct RtpParsed {
35 pub header: RtpHeader,
37 pub payload_offset: usize,
39 pub payload_len: usize,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct RtpParsedHeader {
48 pub header: RtpHeader,
50 pub payload_offset: usize,
52}
53
54impl RtpHeader {
55 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 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; }
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 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 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, (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 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, 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 let d: [u8; 26] = [
222 0b1011_0001,
223 96, 0x00,
225 0x01, 0,
227 0,
228 0,
229 0, 0,
231 0,
232 0,
233 0, 9,
235 9,
236 9,
237 9, 0xBE,
239 0xDE,
240 0x00,
241 0x01, 1,
243 2,
244 3,
245 4, 0x55,
247 0x66, ];
249 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 let mut d = [0u8; RTP_HEADER_LEN];
281 d[0] = 0x8F; assert!(
283 RtpHeader::parse(&d).is_none(),
284 "CSRC list overruns the datagram"
285 );
286 let mut pad = [0u8; RTP_HEADER_LEN + 1];
288 pad[0] = 0xA0; pad[RTP_HEADER_LEN] = 0xFF; assert!(RtpHeader::parse(&pad).is_none(), "padding underflows");
291 }
292}