1use std::net::Ipv4Addr;
26
27use super::rtp::{PT_PCMA, PT_PCMU};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum G711Codec {
32 Pcmu,
34 Pcma,
36}
37
38impl G711Codec {
39 pub fn payload_type(self) -> u8 {
41 match self {
42 G711Codec::Pcmu => PT_PCMU,
43 G711Codec::Pcma => PT_PCMA,
44 }
45 }
46
47 pub fn rtpmap_name(self) -> &'static str {
49 match self {
50 G711Codec::Pcmu => "PCMU",
51 G711Codec::Pcma => "PCMA",
52 }
53 }
54
55 pub fn from_payload_type(pt: u8) -> Option<Self> {
57 match pt {
58 PT_PCMU => Some(G711Codec::Pcmu),
59 PT_PCMA => Some(G711Codec::Pcma),
60 _ => None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct SdpMedia {
68 pub ip: Ipv4Addr,
70 pub port: u16,
72 pub codec: G711Codec,
74}
75
76pub fn build_offer(ip: Ipv4Addr, port: u16) -> String {
80 build_sdp(ip, port, &[G711Codec::Pcmu, G711Codec::Pcma])
82}
83
84pub fn build_answer(ip: Ipv4Addr, port: u16, codec: G711Codec) -> String {
86 build_sdp(ip, port, &[codec])
87}
88
89fn build_sdp(ip: Ipv4Addr, port: u16, codecs: &[G711Codec]) -> String {
92 let pts: Vec<String> = codecs
93 .iter()
94 .map(|c| c.payload_type().to_string())
95 .collect();
96 let mut s = String::new();
97 s.push_str("v=0\r\n");
98 s.push_str(&format!("o=- 0 0 IN IP4 {ip}\r\n"));
99 s.push_str("s=flowcat\r\n");
100 s.push_str(&format!("c=IN IP4 {ip}\r\n"));
101 s.push_str("t=0 0\r\n");
102 s.push_str(&format!("m=audio {port} RTP/AVP {}\r\n", pts.join(" ")));
103 for c in codecs {
104 s.push_str(&format!(
105 "a=rtpmap:{} {}/8000\r\n",
106 c.payload_type(),
107 c.rtpmap_name()
108 ));
109 }
110 s.push_str("a=ptime:20\r\n");
111 s.push_str("a=sendrecv\r\n");
112 s
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum SdpParseError {
118 NoAudioMedia,
120 NoConnection,
122 BadPort,
124 NoSupportedCodec,
126}
127
128impl std::fmt::Display for SdpParseError {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 match self {
131 SdpParseError::NoAudioMedia => write!(f, "SDP has no m=audio line"),
132 SdpParseError::NoConnection => write!(f, "SDP has no c=IN IP4 connection address"),
133 SdpParseError::BadPort => write!(f, "SDP m=audio line has no/invalid port"),
134 SdpParseError::NoSupportedCodec => write!(f, "SDP offers no PCMU/PCMA codec"),
135 }
136 }
137}
138
139pub fn parse(sdp: &str) -> Result<SdpMedia, SdpParseError> {
146 let mut session_ip: Option<Ipv4Addr> = None;
147 let mut media_ip: Option<Ipv4Addr> = None;
148 let mut port: Option<u16> = None;
149 let mut media_pts: Vec<u8> = Vec::new();
150 let mut in_audio = false;
151 let mut seen_audio = false;
152
153 for line in sdp.lines() {
154 let line = line.trim_end_matches('\r');
155 if let Some(rest) = line.strip_prefix("c=") {
156 if let Some(addr) = parse_connection_ipv4(rest) {
159 if in_audio {
160 media_ip = Some(addr);
161 } else {
162 session_ip = Some(addr);
163 }
164 }
165 } else if let Some(rest) = line.strip_prefix("m=") {
166 if let Some(audio) = rest.strip_prefix("audio ") {
167 if seen_audio {
168 in_audio = false;
173 continue;
174 }
175 in_audio = true;
177 seen_audio = true;
178 let mut parts = audio.split_whitespace();
179 port = parts.next().and_then(|p| p.parse::<u16>().ok());
180 let _transport = parts.next();
182 for pt in parts {
183 if let Ok(n) = pt.parse::<u8>() {
184 media_pts.push(n);
185 }
186 }
187 } else {
188 in_audio = false;
190 }
191 }
192 }
193
194 if !seen_audio {
195 return Err(SdpParseError::NoAudioMedia);
196 }
197 let ip = media_ip.or(session_ip).ok_or(SdpParseError::NoConnection)?;
198 let port = port.ok_or(SdpParseError::BadPort)?;
199 if port == 0 {
200 return Err(SdpParseError::BadPort);
202 }
203 let codec = media_pts
205 .iter()
206 .find_map(|&pt| G711Codec::from_payload_type(pt))
207 .ok_or(SdpParseError::NoSupportedCodec)?;
208
209 Ok(SdpMedia { ip, port, codec })
210}
211
212fn parse_connection_ipv4(rest: &str) -> Option<Ipv4Addr> {
214 let mut parts = rest.split_whitespace();
215 match (parts.next(), parts.next(), parts.next()) {
216 (Some("IN"), Some("IP4"), Some(addr)) => {
217 addr.split('/').next()?.parse().ok()
219 }
220 _ => None,
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn ip(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr {
229 Ipv4Addr::new(a, b, c, d)
230 }
231
232 #[test]
234 fn answer_carries_public_ip_port_sendrecv_ptime_and_rtpmap() {
235 let pub_ip = ip(34, 124, 207, 59);
236 let sdp = build_answer(pub_ip, 41234, G711Codec::Pcmu);
237 assert!(
238 sdp.contains("c=IN IP4 34.124.207.59\r\n"),
239 "answer c= must be the public IP"
240 );
241 assert!(
242 sdp.contains("o=- 0 0 IN IP4 34.124.207.59\r\n"),
243 "o= origin must be the public IP"
244 );
245 assert!(
246 sdp.contains("m=audio 41234 RTP/AVP 0\r\n"),
247 "m= must carry the bound RTP port + PCMU only"
248 );
249 assert!(sdp.contains("a=rtpmap:0 PCMU/8000\r\n"));
250 assert!(sdp.contains("a=ptime:20\r\n"));
251 assert!(sdp.contains("a=sendrecv\r\n"));
252 assert!(
253 !sdp.contains("PCMA"),
254 "answer must commit to exactly one codec"
255 );
256 }
257
258 #[test]
260 fn parse_zadarma_style_offer_selects_pcmu() {
261 let sdp = "v=0\r\no=Zadarma 12345 1 IN IP4 185.45.152.42\r\ns=Zadarma\r\n\
262 c=IN IP4 185.45.152.42\r\nt=0 0\r\nm=audio 18722 RTP/AVP 0 101\r\n\
263 a=rtpmap:0 PCMU/8000\r\na=rtpmap:101 telephone-event/8000\r\n\
264 a=fmtp:101 0-16\r\na=ptime:20\r\na=sendrecv\r\n";
265 let m = parse(sdp).unwrap();
266 assert_eq!(m.codec, G711Codec::Pcmu);
267 assert_eq!(m.ip, ip(185, 45, 152, 42));
268 assert_eq!(m.port, 18722);
269 }
270
271 #[test]
273 fn parse_rejects_offer_without_g711() {
274 let sdp = "v=0\r\nc=IN IP4 9.9.9.9\r\nm=audio 5000 RTP/AVP 111 101\r\n\
275 a=rtpmap:111 opus/48000/2\r\na=rtpmap:101 telephone-event/8000\r\n";
276 assert_eq!(parse(sdp), Err(SdpParseError::NoSupportedCodec));
277 }
278
279 #[test]
280 fn parse_rejects_dtmf_only_offer() {
281 let sdp = "v=0\r\nc=IN IP4 1.2.3.4\r\nm=audio 5000 RTP/AVP 101\r\n\
282 a=rtpmap:101 telephone-event/8000\r\n";
283 assert_eq!(parse(sdp), Err(SdpParseError::NoSupportedCodec));
284 }
285
286 #[test]
288 fn parse_tolerates_lf_only_line_endings() {
289 let sdp = "v=0\nc=IN IP4 7.7.7.7\nm=audio 5000 RTP/AVP 0\na=rtpmap:0 PCMU/8000\n";
290 let m = parse(sdp).unwrap();
291 assert_eq!(m.ip, ip(7, 7, 7, 7));
292 assert_eq!(m.port, 5000);
293 assert_eq!(m.codec, G711Codec::Pcmu);
294 }
295
296 #[test]
297 fn parse_ipv6_only_connection_yields_no_connection() {
298 let sdp = "v=0\r\nc=IN IP6 2001:db8::1\r\nm=audio 5000 RTP/AVP 0\r\n";
299 assert_eq!(parse(sdp), Err(SdpParseError::NoConnection));
300 }
301
302 #[test]
303 fn parse_prefers_ipv4_media_connection_over_ipv6_session() {
304 let sdp = "v=0\r\nc=IN IP6 2001:db8::1\r\nt=0 0\r\nm=audio 9000 RTP/AVP 0\r\n\
305 c=IN IP4 172.16.5.5\r\na=rtpmap:0 PCMU/8000\r\n";
306 let m = parse(sdp).unwrap();
307 assert_eq!(m.ip, ip(172, 16, 5, 5));
308 }
309
310 #[test]
312 fn parse_malformed_inputs_do_not_panic() {
313 assert!(parse("").is_err());
314 assert!(parse("this is not sdp at all\r\n\r\n").is_err());
315 assert!(parse("c=IN IP4 1.1.1.1\r\nm=audio\r\n").is_err());
316 assert!(parse("c=IN IP4 1.1.1.1\r\nm=audio abc RTP/AVP 0\r\n").is_err());
317 assert!(parse("c=IN IP4 1.1.1.1\r\nm=audio 5000 RTP/AVP\r\n").is_err());
318 assert!(parse("c=IN IP4 1.1.1.1\r\nm=audio 70000 RTP/AVP 0\r\n").is_err());
319 assert!(parse("m=\r\n").is_err());
320 assert!(parse("c=IN IP4\r\nm=audio 5000 RTP/AVP 0\r\n").is_err());
321 }
322
323 #[test]
325 fn parse_uses_first_audio_section_only() {
326 let sdp = "v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 8000 RTP/AVP 0\r\n\
327 a=rtpmap:0 PCMU/8000\r\nm=audio 9000 RTP/AVP 8\r\n\
328 a=rtpmap:8 PCMA/8000\r\n";
329 let m = parse(sdp).unwrap();
330 assert_eq!(
331 m.port, 8000,
332 "second m=audio must not clobber the first stream's port"
333 );
334 assert_eq!(
335 m.codec,
336 G711Codec::Pcmu,
337 "PTs from the second m=audio must not leak in"
338 );
339 }
340
341 #[test]
342 fn offer_lists_both_codecs_with_ptime_and_sendrecv() {
343 let sdp = build_offer(ip(10, 0, 0, 5), 40000);
344 assert!(sdp.contains("m=audio 40000 RTP/AVP 0 8\r\n"));
345 assert!(sdp.contains("a=rtpmap:0 PCMU/8000\r\n"));
346 assert!(sdp.contains("a=rtpmap:8 PCMA/8000\r\n"));
347 assert!(sdp.contains("c=IN IP4 10.0.0.5\r\n"));
348 assert!(sdp.contains("a=ptime:20\r\n"));
349 assert!(sdp.contains("a=sendrecv\r\n"));
350 }
351
352 #[test]
353 fn answer_commits_to_one_codec() {
354 let sdp = build_answer(ip(192, 168, 1, 9), 5004, G711Codec::Pcma);
355 assert!(sdp.contains("m=audio 5004 RTP/AVP 8\r\n"));
356 assert!(sdp.contains("a=rtpmap:8 PCMA/8000\r\n"));
357 assert!(!sdp.contains("PCMU"));
358 }
359
360 #[test]
361 fn build_then_parse_offer_round_trips_pcmu() {
362 let sdp = build_offer(ip(203, 0, 113, 7), 12345);
364 let m = parse(&sdp).unwrap();
365 assert_eq!(m.ip, ip(203, 0, 113, 7));
366 assert_eq!(m.port, 12345);
367 assert_eq!(m.codec, G711Codec::Pcmu);
368 assert_eq!(m.codec.payload_type(), 0);
369 }
370
371 #[test]
372 fn build_then_parse_answer_round_trips_pcma() {
373 let sdp = build_answer(ip(8, 8, 4, 4), 6000, G711Codec::Pcma);
374 let m = parse(&sdp).unwrap();
375 assert_eq!(m.ip, ip(8, 8, 4, 4));
376 assert_eq!(m.port, 6000);
377 assert_eq!(m.codec, G711Codec::Pcma);
378 }
379
380 #[test]
381 fn parse_picks_first_listed_payload_type_pcma_when_peer_prefers_it() {
382 let sdp = "v=0\r\n\
384 o=- 0 0 IN IP4 1.2.3.4\r\n\
385 s=-\r\n\
386 c=IN IP4 1.2.3.4\r\n\
387 t=0 0\r\n\
388 m=audio 7000 RTP/AVP 8 0\r\n\
389 a=rtpmap:8 PCMA/8000\r\n\
390 a=rtpmap:0 PCMU/8000\r\n";
391 let m = parse(sdp).unwrap();
392 assert_eq!(m.codec, G711Codec::Pcma);
393 assert_eq!(m.port, 7000);
394 }
395
396 #[test]
397 fn parse_skips_unsupported_codecs_and_finds_g711() {
398 let sdp = "v=0\r\n\
400 c=IN IP4 9.9.9.9\r\n\
401 m=audio 5555 RTP/AVP 111 101 0\r\n\
402 a=rtpmap:111 opus/48000/2\r\n\
403 a=rtpmap:101 telephone-event/8000\r\n\
404 a=rtpmap:0 PCMU/8000\r\n";
405 let m = parse(sdp).unwrap();
406 assert_eq!(m.codec, G711Codec::Pcmu);
407 }
408
409 #[test]
410 fn media_level_connection_overrides_session_level() {
411 let sdp = "v=0\r\n\
412 o=- 0 0 IN IP4 10.0.0.1\r\n\
413 c=IN IP4 10.0.0.1\r\n\
414 t=0 0\r\n\
415 m=audio 9000 RTP/AVP 0\r\n\
416 c=IN IP4 172.16.5.5\r\n\
417 a=rtpmap:0 PCMU/8000\r\n";
418 let m = parse(sdp).unwrap();
419 assert_eq!(m.ip, ip(172, 16, 5, 5));
421 }
422
423 #[test]
424 fn parse_errors_on_missing_pieces() {
425 assert_eq!(
427 parse("v=0\r\nc=IN IP4 1.1.1.1\r\n"),
428 Err(SdpParseError::NoAudioMedia)
429 );
430 assert_eq!(
432 parse("v=0\r\nm=audio 5000 RTP/AVP 0\r\n"),
433 Err(SdpParseError::NoConnection)
434 );
435 assert_eq!(
437 parse("v=0\r\nc=IN IP4 1.1.1.1\r\nm=audio 0 RTP/AVP 0\r\n"),
438 Err(SdpParseError::BadPort)
439 );
440 assert_eq!(
442 parse("v=0\r\nc=IN IP4 1.1.1.1\r\nm=audio 5000 RTP/AVP 111\r\n"),
443 Err(SdpParseError::NoSupportedCodec)
444 );
445 }
446}