1use std::net::IpAddr;
35
36pub const DTMF_DEFAULT_PT: u8 = 101;
42
43pub const DTMF_WIDEBAND_DEFAULT_PT: u8 = 110;
49
50pub const OPUS_DEFAULT_PT: u8 = 111;
56
57pub const OPUS_RTP_CLOCK_RATE: u32 = 48_000;
60
61const G711_RTP_CLOCK_RATE: u32 = 8_000;
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum MediaDirection {
67 SendRecv,
69 SendOnly,
71 Inactive,
73}
74
75impl MediaDirection {
76 pub fn attribute(&self) -> &'static str {
78 match self {
79 MediaDirection::SendRecv => "sendrecv",
80 MediaDirection::SendOnly => "sendonly",
81 MediaDirection::Inactive => "inactive",
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum AudioCodec {
93 Pcmu,
95 Pcma,
97 Opus {
102 payload_type: u8,
104 },
105}
106
107impl AudioCodec {
108 pub fn payload_type(&self) -> u8 {
110 match self {
111 AudioCodec::Pcmu => 0,
112 AudioCodec::Pcma => 8,
113 AudioCodec::Opus { payload_type } => *payload_type,
114 }
115 }
116
117 pub fn rtp_clock_rate(&self) -> u32 {
120 match self {
121 AudioCodec::Pcmu | AudioCodec::Pcma => G711_RTP_CLOCK_RATE,
122 AudioCodec::Opus { .. } => OPUS_RTP_CLOCK_RATE,
123 }
124 }
125
126 fn rtpmap_encoding(&self) -> &'static str {
128 match self {
129 AudioCodec::Pcmu => "PCMU/8000",
130 AudioCodec::Pcma => "PCMA/8000",
131 AudioCodec::Opus { .. } => "opus/48000/2",
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct DtmfSpec {
144 pub payload_type: u8,
146 pub clock_rate: u32,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum CodecMenu {
154 Full,
158 Pinned {
164 codec: AudioCodec,
167 dtmf: Option<DtmfSpec>,
169 },
170}
171
172pub fn build_sdp_offer(local_ip: IpAddr, rtp_port: u16) -> Vec<u8> {
178 build_sdp_with(
179 local_ip,
180 rtp_port,
181 CodecMenu::Full,
182 MediaDirection::SendRecv,
183 0,
184 )
185}
186
187pub fn build_sdp_with(
196 local_ip: IpAddr,
197 rtp_port: u16,
198 menu: CodecMenu,
199 direction: MediaDirection,
200 version: u32,
201) -> Vec<u8> {
202 let mut pts: Vec<String> = Vec::new();
204 let mut attrs: Vec<String> = Vec::new();
205
206 match menu {
207 CodecMenu::Full => {
208 pts.extend([
209 OPUS_DEFAULT_PT.to_string(),
210 "0".into(),
211 "8".into(),
212 DTMF_DEFAULT_PT.to_string(),
213 DTMF_WIDEBAND_DEFAULT_PT.to_string(),
214 ]);
215 attrs.push(format!("a=rtpmap:{OPUS_DEFAULT_PT} opus/48000/2"));
216 attrs.push(format!("a=fmtp:{OPUS_DEFAULT_PT} useinbandfec=1"));
217 attrs.push("a=rtpmap:0 PCMU/8000".into());
218 attrs.push("a=rtpmap:8 PCMA/8000".into());
219 attrs.push(format!("a=rtpmap:{DTMF_DEFAULT_PT} telephone-event/8000"));
220 attrs.push(format!("a=fmtp:{DTMF_DEFAULT_PT} 0-15"));
221 attrs.push(format!(
222 "a=rtpmap:{DTMF_WIDEBAND_DEFAULT_PT} telephone-event/{OPUS_RTP_CLOCK_RATE}"
223 ));
224 attrs.push(format!("a=fmtp:{DTMF_WIDEBAND_DEFAULT_PT} 0-15"));
225 }
226 CodecMenu::Pinned { codec, dtmf } => {
227 let pt = codec.payload_type();
228 pts.push(pt.to_string());
229 attrs.push(format!("a=rtpmap:{pt} {}", codec.rtpmap_encoding()));
230 if matches!(codec, AudioCodec::Opus { .. }) {
231 attrs.push(format!("a=fmtp:{pt} useinbandfec=1"));
232 }
233 if let Some(DtmfSpec {
234 payload_type,
235 clock_rate,
236 }) = dtmf
237 {
238 pts.push(payload_type.to_string());
239 attrs.push(format!(
240 "a=rtpmap:{payload_type} telephone-event/{clock_rate}"
241 ));
242 attrs.push(format!("a=fmtp:{payload_type} 0-15"));
243 }
244 }
245 }
246
247 let pt_list = pts.join(" ");
248 let attr_lines = attrs.join("\r\n");
249 let dir = direction.attribute();
250 format!(
251 "v=0\r\n\
252 o=wavekat 0 {version} IN IP4 {local_ip}\r\n\
253 s=wavekat-sip\r\n\
254 c=IN IP4 {local_ip}\r\n\
255 t=0 0\r\n\
256 m=audio {rtp_port} RTP/AVP {pt_list}\r\n\
257 {attr_lines}\r\n\
258 a={dir}\r\n"
259 )
260 .into_bytes()
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct RemoteMedia {
266 pub addr: IpAddr,
268 pub port: u16,
270 pub payload_type: u8,
274 pub codec: Option<AudioCodec>,
286 pub opus_payload_type: Option<u8>,
290 pub dtmf_events: Vec<DtmfSpec>,
294}
295
296impl RemoteMedia {
297 pub fn dtmf(&self) -> Option<DtmfSpec> {
302 match self.codec {
303 Some(codec) => select_dtmf(self, codec),
304 None => self.dtmf_events.first().copied(),
305 }
306 }
307}
308
309pub fn select_codec(offer: &RemoteMedia) -> Option<AudioCodec> {
314 offer
315 .opus_payload_type
316 .map(|payload_type| AudioCodec::Opus { payload_type })
317 .or(offer.codec)
318}
319
320pub fn select_dtmf(offer: &RemoteMedia, codec: AudioCodec) -> Option<DtmfSpec> {
324 let clock = codec.rtp_clock_rate();
325 offer
326 .dtmf_events
327 .iter()
328 .copied()
329 .find(|d| d.clock_rate == clock)
330 .or_else(|| offer.dtmf_events.first().copied())
331}
332
333pub fn parse_sdp(sdp_bytes: &[u8]) -> Result<RemoteMedia, String> {
336 let sdp = std::str::from_utf8(sdp_bytes).map_err(|e| format!("SDP not UTF-8: {e}"))?;
337
338 let mut addr: Option<IpAddr> = None;
339 let mut port: Option<u16> = None;
340 let mut audio_pts: Vec<u8> = Vec::new();
341 let mut rtpmaps: Vec<(u8, String, u32)> = Vec::new();
343
344 for line in sdp.lines() {
345 let line = line.trim();
346
347 if line.starts_with("c=IN IP4 ") || line.starts_with("c=IN IP6 ") {
349 if let Some(ip_str) = line.split_whitespace().nth(2) {
350 addr = ip_str.parse().ok();
351 }
352 }
353
354 if line.starts_with("m=audio ") {
358 let parts: Vec<&str> = line.split_whitespace().collect();
359 if parts.len() >= 2 {
360 port = parts[1].parse().ok();
361 }
362 for pt_str in parts.iter().skip(3) {
364 if let Ok(pt) = pt_str.parse::<u8>() {
365 audio_pts.push(pt);
366 }
367 }
368 }
369
370 if let Some(rest) = line.strip_prefix("a=rtpmap:") {
373 let mut sp = rest.splitn(2, char::is_whitespace);
375 let pt_str = sp.next().unwrap_or("");
376 let mapping = sp.next().unwrap_or("");
377 if let Ok(pt) = pt_str.parse::<u8>() {
378 let name_lc = mapping.split('/').next().unwrap_or("").to_ascii_lowercase();
379 if let Ok(rate) = mapping.split('/').nth(1).unwrap_or("").parse::<u32>() {
380 rtpmaps.push((pt, name_lc, rate));
381 }
382 }
383 }
384 }
385
386 let rtpmap_for = |pt: u8| rtpmaps.iter().find(|(p, _, _)| *p == pt);
387
388 let resolve = |pt: u8| -> Option<AudioCodec> {
392 match rtpmap_for(pt) {
393 Some((_, name, rate)) => match (name.as_str(), *rate) {
394 ("opus", OPUS_RTP_CLOCK_RATE) => Some(AudioCodec::Opus { payload_type: pt }),
395 ("pcmu", G711_RTP_CLOCK_RATE) => Some(AudioCodec::Pcmu),
396 ("pcma", G711_RTP_CLOCK_RATE) => Some(AudioCodec::Pcma),
397 _ => None,
398 },
399 None => match pt {
400 0 => Some(AudioCodec::Pcmu),
401 8 => Some(AudioCodec::Pcma),
402 _ => None,
403 },
404 }
405 };
406
407 let codec = audio_pts.iter().copied().find_map(resolve);
408 let opus_payload_type = audio_pts
409 .iter()
410 .copied()
411 .find(|&pt| matches!(resolve(pt), Some(AudioCodec::Opus { .. })));
412
413 let dtmf_events: Vec<DtmfSpec> = audio_pts
419 .iter()
420 .filter_map(|&pt| match rtpmap_for(pt) {
421 Some((_, name, rate)) if name == "telephone-event" => Some(DtmfSpec {
422 payload_type: pt,
423 clock_rate: *rate,
424 }),
425 _ => None,
426 })
427 .collect();
428
429 let payload_type = audio_pts.first().copied();
430
431 match (addr, port, payload_type) {
432 (Some(addr), Some(port), Some(pt)) => Ok(RemoteMedia {
433 addr,
434 port,
435 payload_type: pt,
436 codec,
437 opus_payload_type,
438 dtmf_events,
439 }),
440 (None, _, _) => Err("No connection address (c=) in SDP".to_string()),
441 (_, None, _) => Err("No audio media line (m=audio) in SDP".to_string()),
442 (_, _, None) => Err("No payload type in m=audio line".to_string()),
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use std::net::Ipv4Addr;
450
451 fn ip() -> IpAddr {
452 IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))
453 }
454
455 #[test]
456 fn offer_contains_required_fields_opus_first() {
457 let sdp = build_sdp_offer(ip(), 5004);
458 let text = String::from_utf8(sdp).unwrap();
459
460 assert!(text.contains("v=0\r\n"));
461 assert!(text.contains("c=IN IP4 10.0.0.1\r\n"));
462 assert!(text.contains("m=audio 5004 RTP/AVP 111 0 8 101 110\r\n"));
465 assert!(text.contains("a=rtpmap:111 opus/48000/2\r\n"));
466 assert!(text.contains("a=fmtp:111 useinbandfec=1\r\n"));
467 assert!(text.contains("a=rtpmap:0 PCMU/8000\r\n"));
468 assert!(text.contains("a=rtpmap:8 PCMA/8000\r\n"));
469 assert!(text.contains("a=rtpmap:101 telephone-event/8000\r\n"));
470 assert!(text.contains("a=fmtp:101 0-15\r\n"));
471 assert!(text.contains("a=rtpmap:110 telephone-event/48000\r\n"));
472 assert!(text.contains("a=fmtp:110 0-15\r\n"));
473 assert!(text.contains("a=sendrecv\r\n"));
474 assert!(text.contains("o=wavekat 0 0 IN IP4 10.0.0.1\r\n"));
475 }
476
477 #[test]
478 fn build_sdp_with_sets_direction_and_version() {
479 let menu = CodecMenu::Pinned {
480 codec: AudioCodec::Pcmu,
481 dtmf: Some(DtmfSpec {
482 payload_type: 101,
483 clock_rate: 8000,
484 }),
485 };
486
487 let hold = String::from_utf8(build_sdp_with(
488 ip(),
489 5004,
490 menu,
491 MediaDirection::SendOnly,
492 1,
493 ))
494 .unwrap();
495 assert!(hold.contains("o=wavekat 0 1 IN IP4 10.0.0.1\r\n"));
496 assert!(hold.contains("a=sendonly\r\n"));
497 assert!(!hold.contains("a=sendrecv\r\n"));
498
499 let resume = String::from_utf8(build_sdp_with(
500 ip(),
501 5004,
502 menu,
503 MediaDirection::SendRecv,
504 2,
505 ))
506 .unwrap();
507 assert!(resume.contains("o=wavekat 0 2 IN IP4 10.0.0.1\r\n"));
508 assert!(resume.contains("a=sendrecv\r\n"));
509
510 let inactive = String::from_utf8(build_sdp_with(
511 ip(),
512 5004,
513 menu,
514 MediaDirection::Inactive,
515 3,
516 ))
517 .unwrap();
518 assert!(inactive.contains("a=inactive\r\n"));
519 }
520
521 #[test]
522 fn pinned_g711_reoffer_contains_only_the_negotiated_codec() {
523 let menu = CodecMenu::Pinned {
526 codec: AudioCodec::Pcma,
527 dtmf: Some(DtmfSpec {
528 payload_type: 101,
529 clock_rate: 8000,
530 }),
531 };
532 let text = String::from_utf8(build_sdp_with(
533 ip(),
534 5004,
535 menu,
536 MediaDirection::SendOnly,
537 1,
538 ))
539 .unwrap();
540 assert!(text.contains("m=audio 5004 RTP/AVP 8 101\r\n"));
541 assert!(text.contains("a=rtpmap:8 PCMA/8000\r\n"));
542 assert!(!text.contains("opus"));
543 assert!(!text.contains("PCMU"));
544 }
545
546 #[test]
547 fn pinned_opus_reoffer_keeps_the_negotiated_pt_and_dtmf_clock() {
548 let menu = CodecMenu::Pinned {
552 codec: AudioCodec::Opus { payload_type: 96 },
553 dtmf: Some(DtmfSpec {
554 payload_type: 97,
555 clock_rate: 48000,
556 }),
557 };
558 let text = String::from_utf8(build_sdp_with(
559 ip(),
560 5004,
561 menu,
562 MediaDirection::SendOnly,
563 1,
564 ))
565 .unwrap();
566 assert!(text.contains("m=audio 5004 RTP/AVP 96 97\r\n"));
567 assert!(text.contains("a=rtpmap:96 opus/48000/2\r\n"));
568 assert!(text.contains("a=fmtp:96 useinbandfec=1\r\n"));
569 assert!(text.contains("a=rtpmap:97 telephone-event/48000\r\n"));
570 assert!(!text.contains("111"));
571 assert!(!text.contains("PCMU"));
572 assert!(!text.contains("PCMA"));
573 }
574
575 #[test]
576 fn pinned_without_dtmf_omits_telephone_event() {
577 let menu = CodecMenu::Pinned {
578 codec: AudioCodec::Pcmu,
579 dtmf: None,
580 };
581 let text = String::from_utf8(build_sdp_with(
582 ip(),
583 5004,
584 menu,
585 MediaDirection::SendRecv,
586 0,
587 ))
588 .unwrap();
589 assert!(text.contains("m=audio 5004 RTP/AVP 0\r\n"));
590 assert!(!text.contains("telephone-event"));
591 }
592
593 #[test]
594 fn parse_sdp_extracts_addr_port_and_codec() {
595 let sdp = b"v=0\r\nc=IN IP4 192.168.1.100\r\nm=audio 20000 RTP/AVP 0\r\n";
596 let media = parse_sdp(sdp).unwrap();
597 assert_eq!(media.addr, Ipv4Addr::new(192, 168, 1, 100));
598 assert_eq!(media.port, 20000);
599 assert_eq!(media.payload_type, 0);
600 assert_eq!(media.codec, Some(AudioCodec::Pcmu));
601 assert_eq!(media.opus_payload_type, None);
602 assert_eq!(media.dtmf(), None);
604 }
605
606 #[test]
607 fn parse_sdp_extracts_first_codec_when_multiple() {
608 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 8 0 101\r\n\
609 a=rtpmap:101 telephone-event/8000\r\n";
610 let media = parse_sdp(sdp).unwrap();
611 assert_eq!(media.payload_type, 8); assert_eq!(media.codec, Some(AudioCodec::Pcma));
616 assert_eq!(
617 media.dtmf(),
618 Some(DtmfSpec {
619 payload_type: 101,
620 clock_rate: 8000
621 })
622 );
623 }
624
625 #[test]
626 fn parse_sdp_resolves_dynamic_opus_pt() {
627 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 96 0 101\r\n\
630 a=rtpmap:96 opus/48000/2\r\n\
631 a=rtpmap:0 PCMU/8000\r\n\
632 a=rtpmap:101 telephone-event/8000\r\n";
633 let media = parse_sdp(sdp).unwrap();
634 assert_eq!(media.payload_type, 96);
635 assert_eq!(media.codec, Some(AudioCodec::Opus { payload_type: 96 }));
636 assert_eq!(media.opus_payload_type, Some(96));
637 }
638
639 #[test]
640 fn parse_sdp_finds_opus_even_when_not_first() {
641 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 8 111\r\n\
645 a=rtpmap:111 opus/48000/2\r\n";
646 let media = parse_sdp(sdp).unwrap();
647 assert_eq!(media.codec, Some(AudioCodec::Pcmu));
648 assert_eq!(media.opus_payload_type, Some(111));
649 }
650
651 #[test]
652 fn parse_sdp_skips_unknown_dynamic_pt_to_next_known_codec() {
653 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 18 8 0\r\n\
657 a=rtpmap:18 G729/8000\r\n";
658 let media = parse_sdp(sdp).unwrap();
659 assert_eq!(media.payload_type, 18);
660 assert_eq!(media.codec, Some(AudioCodec::Pcma));
661 }
662
663 #[test]
664 fn parse_sdp_no_known_codec_yields_none() {
665 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 18\r\n\
666 a=rtpmap:18 G729/8000\r\n";
667 let media = parse_sdp(sdp).unwrap();
668 assert_eq!(media.codec, None);
669 assert_eq!(select_codec(&media), None);
670 }
671
672 #[test]
673 fn round_trip_build_offer_then_parse() {
674 let ip = IpAddr::V4(Ipv4Addr::new(172, 16, 0, 5));
675 let sdp = build_sdp_offer(ip, 8000);
676 let media = parse_sdp(&sdp).unwrap();
677 assert_eq!(media.addr, ip);
678 assert_eq!(media.port, 8000);
679 assert_eq!(media.payload_type, OPUS_DEFAULT_PT);
681 assert_eq!(
682 media.codec,
683 Some(AudioCodec::Opus {
684 payload_type: OPUS_DEFAULT_PT
685 })
686 );
687 assert_eq!(media.dtmf_events.len(), 2);
690 assert_eq!(
691 media.dtmf(),
692 Some(DtmfSpec {
693 payload_type: DTMF_WIDEBAND_DEFAULT_PT,
694 clock_rate: 48000
695 })
696 );
697 }
698
699 #[test]
700 fn round_trip_pinned_answer_then_parse() {
701 let menu = CodecMenu::Pinned {
704 codec: AudioCodec::Opus { payload_type: 111 },
705 dtmf: Some(DtmfSpec {
706 payload_type: 110,
707 clock_rate: 48000,
708 }),
709 };
710 let sdp = build_sdp_with(ip(), 8000, menu, MediaDirection::SendRecv, 0);
711 let media = parse_sdp(&sdp).unwrap();
712 assert_eq!(media.codec, Some(AudioCodec::Opus { payload_type: 111 }));
713 assert_eq!(
714 media.dtmf(),
715 Some(DtmfSpec {
716 payload_type: 110,
717 clock_rate: 48000
718 })
719 );
720 }
721
722 #[test]
723 fn select_codec_prefers_opus_anywhere_in_the_offer() {
724 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 8 102\r\n\
727 a=rtpmap:102 opus/48000/2\r\n";
728 let media = parse_sdp(sdp).unwrap();
729 assert_eq!(
730 select_codec(&media),
731 Some(AudioCodec::Opus { payload_type: 102 })
732 );
733 }
734
735 #[test]
736 fn select_codec_falls_back_to_g711_without_opus() {
737 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 8 0\r\n";
738 let media = parse_sdp(sdp).unwrap();
739 assert_eq!(select_codec(&media), Some(AudioCodec::Pcma));
742 }
743
744 #[test]
745 fn select_dtmf_matches_the_codec_clock() {
746 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 111 0 101 110\r\n\
747 a=rtpmap:111 opus/48000/2\r\n\
748 a=rtpmap:101 telephone-event/8000\r\n\
749 a=rtpmap:110 telephone-event/48000\r\n";
750 let media = parse_sdp(sdp).unwrap();
751 assert_eq!(
754 select_dtmf(&media, AudioCodec::Opus { payload_type: 111 }),
755 Some(DtmfSpec {
756 payload_type: 110,
757 clock_rate: 48000
758 })
759 );
760 assert_eq!(
762 select_dtmf(&media, AudioCodec::Pcmu),
763 Some(DtmfSpec {
764 payload_type: 101,
765 clock_rate: 8000
766 })
767 );
768 }
769
770 #[test]
771 fn select_dtmf_falls_back_to_any_clock() {
772 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 111 101\r\n\
776 a=rtpmap:111 opus/48000/2\r\n\
777 a=rtpmap:101 telephone-event/8000\r\n";
778 let media = parse_sdp(sdp).unwrap();
779 assert_eq!(
780 select_dtmf(&media, AudioCodec::Opus { payload_type: 111 }),
781 Some(DtmfSpec {
782 payload_type: 101,
783 clock_rate: 8000
784 })
785 );
786 }
787
788 #[test]
789 fn parse_sdp_accepts_telephone_event_at_48k() {
790 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 111 110\r\n\
794 a=rtpmap:111 opus/48000/2\r\n\
795 a=rtpmap:110 telephone-event/48000\r\n";
796 let media = parse_sdp(sdp).unwrap();
797 assert_eq!(
798 media.dtmf(),
799 Some(DtmfSpec {
800 payload_type: 110,
801 clock_rate: 48000
802 })
803 );
804 }
805
806 #[test]
807 fn parse_sdp_records_telephone_event_at_any_clock() {
808 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
813 a=rtpmap:0 PCMU/8000\r\n\
814 a=rtpmap:101 telephone-event/16000\r\n";
815 let media = parse_sdp(sdp).unwrap();
816 assert_eq!(
817 media.dtmf(),
818 Some(DtmfSpec {
819 payload_type: 101,
820 clock_rate: 16000
821 })
822 );
823 }
824
825 #[test]
826 fn parse_sdp_picks_dtmf_pt_from_rtpmap_not_constant() {
827 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 96\r\n\
830 a=rtpmap:0 PCMU/8000\r\n\
831 a=rtpmap:96 telephone-event/8000\r\n";
832 let media = parse_sdp(sdp).unwrap();
833 assert_eq!(
834 media.dtmf(),
835 Some(DtmfSpec {
836 payload_type: 96,
837 clock_rate: 8000
838 })
839 );
840 }
841
842 #[test]
843 fn parse_sdp_rejects_telephone_event_not_listed_on_m_line() {
844 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0\r\n\
847 a=rtpmap:0 PCMU/8000\r\n\
848 a=rtpmap:101 telephone-event/8000\r\n";
849 let media = parse_sdp(sdp).unwrap();
850 assert_eq!(media.dtmf(), None);
851 }
852
853 #[test]
854 fn parse_sdp_handles_mixed_case_telephone_event() {
855 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
858 a=rtpmap:0 PCMU/8000\r\n\
859 a=rtpmap:101 Telephone-Event/8000\r\n";
860 let media = parse_sdp(sdp).unwrap();
861 assert_eq!(
862 media.dtmf(),
863 Some(DtmfSpec {
864 payload_type: 101,
865 clock_rate: 8000
866 })
867 );
868 }
869
870 #[test]
871 fn parse_sdp_handles_telephone_event_without_fmtp() {
872 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
875 a=rtpmap:0 PCMU/8000\r\n\
876 a=rtpmap:101 telephone-event/8000\r\n";
877 let media = parse_sdp(sdp).unwrap();
878 assert!(media.dtmf().is_some());
879 }
880
881 #[test]
882 fn parse_sdp_g711_only_body_still_parses_as_before() {
883 let sdp = b"v=0\r\n\
886 o=wavekat 0 0 IN IP4 10.0.0.2\r\n\
887 s=wavekat-sip\r\n\
888 c=IN IP4 10.0.0.2\r\n\
889 t=0 0\r\n\
890 m=audio 5004 RTP/AVP 0 8 101\r\n\
891 a=rtpmap:0 PCMU/8000\r\n\
892 a=rtpmap:8 PCMA/8000\r\n\
893 a=rtpmap:101 telephone-event/8000\r\n\
894 a=fmtp:101 0-15\r\n\
895 a=sendrecv\r\n";
896 let media = parse_sdp(sdp).unwrap();
897 assert_eq!(media.payload_type, 0);
898 assert_eq!(media.codec, Some(AudioCodec::Pcmu));
899 assert_eq!(media.opus_payload_type, None);
900 assert_eq!(
901 media.dtmf(),
902 Some(DtmfSpec {
903 payload_type: 101,
904 clock_rate: 8000
905 })
906 );
907 }
908
909 #[test]
910 fn parse_sdp_missing_connection_line() {
911 let sdp = b"v=0\r\nm=audio 20000 RTP/AVP 0\r\n";
912 let err = parse_sdp(sdp).unwrap_err();
913 assert!(err.contains("connection address"));
914 }
915
916 #[test]
917 fn parse_sdp_missing_media_line() {
918 let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\n";
919 let err = parse_sdp(sdp).unwrap_err();
920 assert!(err.contains("audio media"));
921 }
922
923 #[test]
924 fn parse_sdp_invalid_utf8() {
925 let sdp = &[0xFF, 0xFE, 0xFD];
926 let err = parse_sdp(sdp).unwrap_err();
927 assert!(err.contains("UTF-8"));
928 }
929
930 #[test]
931 fn audio_codec_wire_identities() {
932 assert_eq!(AudioCodec::Pcmu.payload_type(), 0);
933 assert_eq!(AudioCodec::Pcma.payload_type(), 8);
934 assert_eq!(AudioCodec::Opus { payload_type: 96 }.payload_type(), 96);
935 assert_eq!(AudioCodec::Pcmu.rtp_clock_rate(), 8000);
936 assert_eq!(AudioCodec::Pcma.rtp_clock_rate(), 8000);
937 assert_eq!(
939 AudioCodec::Opus { payload_type: 96 }.rtp_clock_rate(),
940 48000
941 );
942 }
943}