use std::net::IpAddr;
pub const DTMF_DEFAULT_PT: u8 = 101;
pub const DTMF_WIDEBAND_DEFAULT_PT: u8 = 110;
pub const OPUS_DEFAULT_PT: u8 = 111;
pub const OPUS_RTP_CLOCK_RATE: u32 = 48_000;
const G711_RTP_CLOCK_RATE: u32 = 8_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaDirection {
SendRecv,
SendOnly,
Inactive,
}
impl MediaDirection {
pub fn attribute(&self) -> &'static str {
match self {
MediaDirection::SendRecv => "sendrecv",
MediaDirection::SendOnly => "sendonly",
MediaDirection::Inactive => "inactive",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioCodec {
Pcmu,
Pcma,
Opus {
payload_type: u8,
},
}
impl AudioCodec {
pub fn payload_type(&self) -> u8 {
match self {
AudioCodec::Pcmu => 0,
AudioCodec::Pcma => 8,
AudioCodec::Opus { payload_type } => *payload_type,
}
}
pub fn rtp_clock_rate(&self) -> u32 {
match self {
AudioCodec::Pcmu | AudioCodec::Pcma => G711_RTP_CLOCK_RATE,
AudioCodec::Opus { .. } => OPUS_RTP_CLOCK_RATE,
}
}
fn rtpmap_encoding(&self) -> &'static str {
match self {
AudioCodec::Pcmu => "PCMU/8000",
AudioCodec::Pcma => "PCMA/8000",
AudioCodec::Opus { .. } => "opus/48000/2",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DtmfSpec {
pub payload_type: u8,
pub clock_rate: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodecMenu {
Full,
Pinned {
codec: AudioCodec,
dtmf: Option<DtmfSpec>,
},
}
pub fn build_sdp_offer(local_ip: IpAddr, rtp_port: u16) -> Vec<u8> {
build_sdp_with(
local_ip,
rtp_port,
CodecMenu::Full,
MediaDirection::SendRecv,
0,
)
}
pub fn build_sdp_with(
local_ip: IpAddr,
rtp_port: u16,
menu: CodecMenu,
direction: MediaDirection,
version: u32,
) -> Vec<u8> {
let mut pts: Vec<String> = Vec::new();
let mut attrs: Vec<String> = Vec::new();
match menu {
CodecMenu::Full => {
pts.extend([
OPUS_DEFAULT_PT.to_string(),
"0".into(),
"8".into(),
DTMF_DEFAULT_PT.to_string(),
DTMF_WIDEBAND_DEFAULT_PT.to_string(),
]);
attrs.push(format!("a=rtpmap:{OPUS_DEFAULT_PT} opus/48000/2"));
attrs.push(format!("a=fmtp:{OPUS_DEFAULT_PT} useinbandfec=1"));
attrs.push("a=rtpmap:0 PCMU/8000".into());
attrs.push("a=rtpmap:8 PCMA/8000".into());
attrs.push(format!("a=rtpmap:{DTMF_DEFAULT_PT} telephone-event/8000"));
attrs.push(format!("a=fmtp:{DTMF_DEFAULT_PT} 0-15"));
attrs.push(format!(
"a=rtpmap:{DTMF_WIDEBAND_DEFAULT_PT} telephone-event/{OPUS_RTP_CLOCK_RATE}"
));
attrs.push(format!("a=fmtp:{DTMF_WIDEBAND_DEFAULT_PT} 0-15"));
}
CodecMenu::Pinned { codec, dtmf } => {
let pt = codec.payload_type();
pts.push(pt.to_string());
attrs.push(format!("a=rtpmap:{pt} {}", codec.rtpmap_encoding()));
if matches!(codec, AudioCodec::Opus { .. }) {
attrs.push(format!("a=fmtp:{pt} useinbandfec=1"));
}
if let Some(DtmfSpec {
payload_type,
clock_rate,
}) = dtmf
{
pts.push(payload_type.to_string());
attrs.push(format!(
"a=rtpmap:{payload_type} telephone-event/{clock_rate}"
));
attrs.push(format!("a=fmtp:{payload_type} 0-15"));
}
}
}
let pt_list = pts.join(" ");
let attr_lines = attrs.join("\r\n");
let dir = direction.attribute();
format!(
"v=0\r\n\
o=wavekat 0 {version} IN IP4 {local_ip}\r\n\
s=wavekat-sip\r\n\
c=IN IP4 {local_ip}\r\n\
t=0 0\r\n\
m=audio {rtp_port} RTP/AVP {pt_list}\r\n\
{attr_lines}\r\n\
a={dir}\r\n"
)
.into_bytes()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteMedia {
pub addr: IpAddr,
pub port: u16,
pub payload_type: u8,
pub codec: Option<AudioCodec>,
pub opus_payload_type: Option<u8>,
pub dtmf_events: Vec<DtmfSpec>,
}
impl RemoteMedia {
pub fn dtmf(&self) -> Option<DtmfSpec> {
match self.codec {
Some(codec) => select_dtmf(self, codec),
None => self.dtmf_events.first().copied(),
}
}
}
pub fn select_codec(offer: &RemoteMedia) -> Option<AudioCodec> {
offer
.opus_payload_type
.map(|payload_type| AudioCodec::Opus { payload_type })
.or(offer.codec)
}
pub fn select_dtmf(offer: &RemoteMedia, codec: AudioCodec) -> Option<DtmfSpec> {
let clock = codec.rtp_clock_rate();
offer
.dtmf_events
.iter()
.copied()
.find(|d| d.clock_rate == clock)
.or_else(|| offer.dtmf_events.first().copied())
}
pub fn parse_sdp(sdp_bytes: &[u8]) -> Result<RemoteMedia, String> {
let sdp = std::str::from_utf8(sdp_bytes).map_err(|e| format!("SDP not UTF-8: {e}"))?;
let mut addr: Option<IpAddr> = None;
let mut port: Option<u16> = None;
let mut audio_pts: Vec<u8> = Vec::new();
let mut rtpmaps: Vec<(u8, String, u32)> = Vec::new();
for line in sdp.lines() {
let line = line.trim();
if line.starts_with("c=IN IP4 ") || line.starts_with("c=IN IP6 ") {
if let Some(ip_str) = line.split_whitespace().nth(2) {
addr = ip_str.parse().ok();
}
}
if line.starts_with("m=audio ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
port = parts[1].parse().ok();
}
for pt_str in parts.iter().skip(3) {
if let Ok(pt) = pt_str.parse::<u8>() {
audio_pts.push(pt);
}
}
}
if let Some(rest) = line.strip_prefix("a=rtpmap:") {
let mut sp = rest.splitn(2, char::is_whitespace);
let pt_str = sp.next().unwrap_or("");
let mapping = sp.next().unwrap_or("");
if let Ok(pt) = pt_str.parse::<u8>() {
let name_lc = mapping.split('/').next().unwrap_or("").to_ascii_lowercase();
if let Ok(rate) = mapping.split('/').nth(1).unwrap_or("").parse::<u32>() {
rtpmaps.push((pt, name_lc, rate));
}
}
}
}
let rtpmap_for = |pt: u8| rtpmaps.iter().find(|(p, _, _)| *p == pt);
let resolve = |pt: u8| -> Option<AudioCodec> {
match rtpmap_for(pt) {
Some((_, name, rate)) => match (name.as_str(), *rate) {
("opus", OPUS_RTP_CLOCK_RATE) => Some(AudioCodec::Opus { payload_type: pt }),
("pcmu", G711_RTP_CLOCK_RATE) => Some(AudioCodec::Pcmu),
("pcma", G711_RTP_CLOCK_RATE) => Some(AudioCodec::Pcma),
_ => None,
},
None => match pt {
0 => Some(AudioCodec::Pcmu),
8 => Some(AudioCodec::Pcma),
_ => None,
},
}
};
let codec = audio_pts.iter().copied().find_map(resolve);
let opus_payload_type = audio_pts
.iter()
.copied()
.find(|&pt| matches!(resolve(pt), Some(AudioCodec::Opus { .. })));
let dtmf_events: Vec<DtmfSpec> = audio_pts
.iter()
.filter_map(|&pt| match rtpmap_for(pt) {
Some((_, name, rate)) if name == "telephone-event" => Some(DtmfSpec {
payload_type: pt,
clock_rate: *rate,
}),
_ => None,
})
.collect();
let payload_type = audio_pts.first().copied();
match (addr, port, payload_type) {
(Some(addr), Some(port), Some(pt)) => Ok(RemoteMedia {
addr,
port,
payload_type: pt,
codec,
opus_payload_type,
dtmf_events,
}),
(None, _, _) => Err("No connection address (c=) in SDP".to_string()),
(_, None, _) => Err("No audio media line (m=audio) in SDP".to_string()),
(_, _, None) => Err("No payload type in m=audio line".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
fn ip() -> IpAddr {
IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))
}
#[test]
fn offer_contains_required_fields_opus_first() {
let sdp = build_sdp_offer(ip(), 5004);
let text = String::from_utf8(sdp).unwrap();
assert!(text.contains("v=0\r\n"));
assert!(text.contains("c=IN IP4 10.0.0.1\r\n"));
assert!(text.contains("m=audio 5004 RTP/AVP 111 0 8 101 110\r\n"));
assert!(text.contains("a=rtpmap:111 opus/48000/2\r\n"));
assert!(text.contains("a=fmtp:111 useinbandfec=1\r\n"));
assert!(text.contains("a=rtpmap:0 PCMU/8000\r\n"));
assert!(text.contains("a=rtpmap:8 PCMA/8000\r\n"));
assert!(text.contains("a=rtpmap:101 telephone-event/8000\r\n"));
assert!(text.contains("a=fmtp:101 0-15\r\n"));
assert!(text.contains("a=rtpmap:110 telephone-event/48000\r\n"));
assert!(text.contains("a=fmtp:110 0-15\r\n"));
assert!(text.contains("a=sendrecv\r\n"));
assert!(text.contains("o=wavekat 0 0 IN IP4 10.0.0.1\r\n"));
}
#[test]
fn build_sdp_with_sets_direction_and_version() {
let menu = CodecMenu::Pinned {
codec: AudioCodec::Pcmu,
dtmf: Some(DtmfSpec {
payload_type: 101,
clock_rate: 8000,
}),
};
let hold = String::from_utf8(build_sdp_with(
ip(),
5004,
menu,
MediaDirection::SendOnly,
1,
))
.unwrap();
assert!(hold.contains("o=wavekat 0 1 IN IP4 10.0.0.1\r\n"));
assert!(hold.contains("a=sendonly\r\n"));
assert!(!hold.contains("a=sendrecv\r\n"));
let resume = String::from_utf8(build_sdp_with(
ip(),
5004,
menu,
MediaDirection::SendRecv,
2,
))
.unwrap();
assert!(resume.contains("o=wavekat 0 2 IN IP4 10.0.0.1\r\n"));
assert!(resume.contains("a=sendrecv\r\n"));
let inactive = String::from_utf8(build_sdp_with(
ip(),
5004,
menu,
MediaDirection::Inactive,
3,
))
.unwrap();
assert!(inactive.contains("a=inactive\r\n"));
}
#[test]
fn pinned_g711_reoffer_contains_only_the_negotiated_codec() {
let menu = CodecMenu::Pinned {
codec: AudioCodec::Pcma,
dtmf: Some(DtmfSpec {
payload_type: 101,
clock_rate: 8000,
}),
};
let text = String::from_utf8(build_sdp_with(
ip(),
5004,
menu,
MediaDirection::SendOnly,
1,
))
.unwrap();
assert!(text.contains("m=audio 5004 RTP/AVP 8 101\r\n"));
assert!(text.contains("a=rtpmap:8 PCMA/8000\r\n"));
assert!(!text.contains("opus"));
assert!(!text.contains("PCMU"));
}
#[test]
fn pinned_opus_reoffer_keeps_the_negotiated_pt_and_dtmf_clock() {
let menu = CodecMenu::Pinned {
codec: AudioCodec::Opus { payload_type: 96 },
dtmf: Some(DtmfSpec {
payload_type: 97,
clock_rate: 48000,
}),
};
let text = String::from_utf8(build_sdp_with(
ip(),
5004,
menu,
MediaDirection::SendOnly,
1,
))
.unwrap();
assert!(text.contains("m=audio 5004 RTP/AVP 96 97\r\n"));
assert!(text.contains("a=rtpmap:96 opus/48000/2\r\n"));
assert!(text.contains("a=fmtp:96 useinbandfec=1\r\n"));
assert!(text.contains("a=rtpmap:97 telephone-event/48000\r\n"));
assert!(!text.contains("111"));
assert!(!text.contains("PCMU"));
assert!(!text.contains("PCMA"));
}
#[test]
fn pinned_without_dtmf_omits_telephone_event() {
let menu = CodecMenu::Pinned {
codec: AudioCodec::Pcmu,
dtmf: None,
};
let text = String::from_utf8(build_sdp_with(
ip(),
5004,
menu,
MediaDirection::SendRecv,
0,
))
.unwrap();
assert!(text.contains("m=audio 5004 RTP/AVP 0\r\n"));
assert!(!text.contains("telephone-event"));
}
#[test]
fn parse_sdp_extracts_addr_port_and_codec() {
let sdp = b"v=0\r\nc=IN IP4 192.168.1.100\r\nm=audio 20000 RTP/AVP 0\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(media.addr, Ipv4Addr::new(192, 168, 1, 100));
assert_eq!(media.port, 20000);
assert_eq!(media.payload_type, 0);
assert_eq!(media.codec, Some(AudioCodec::Pcmu));
assert_eq!(media.opus_payload_type, None);
assert_eq!(media.dtmf(), None);
}
#[test]
fn parse_sdp_extracts_first_codec_when_multiple() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 8 0 101\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(media.payload_type, 8); assert_eq!(media.codec, Some(AudioCodec::Pcma));
assert_eq!(
media.dtmf(),
Some(DtmfSpec {
payload_type: 101,
clock_rate: 8000
})
);
}
#[test]
fn parse_sdp_resolves_dynamic_opus_pt() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 96 0 101\r\n\
a=rtpmap:96 opus/48000/2\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(media.payload_type, 96);
assert_eq!(media.codec, Some(AudioCodec::Opus { payload_type: 96 }));
assert_eq!(media.opus_payload_type, Some(96));
}
#[test]
fn parse_sdp_finds_opus_even_when_not_first() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 8 111\r\n\
a=rtpmap:111 opus/48000/2\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(media.codec, Some(AudioCodec::Pcmu));
assert_eq!(media.opus_payload_type, Some(111));
}
#[test]
fn parse_sdp_skips_unknown_dynamic_pt_to_next_known_codec() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 18 8 0\r\n\
a=rtpmap:18 G729/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(media.payload_type, 18);
assert_eq!(media.codec, Some(AudioCodec::Pcma));
}
#[test]
fn parse_sdp_no_known_codec_yields_none() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 18\r\n\
a=rtpmap:18 G729/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(media.codec, None);
assert_eq!(select_codec(&media), None);
}
#[test]
fn round_trip_build_offer_then_parse() {
let ip = IpAddr::V4(Ipv4Addr::new(172, 16, 0, 5));
let sdp = build_sdp_offer(ip, 8000);
let media = parse_sdp(&sdp).unwrap();
assert_eq!(media.addr, ip);
assert_eq!(media.port, 8000);
assert_eq!(media.payload_type, OPUS_DEFAULT_PT);
assert_eq!(
media.codec,
Some(AudioCodec::Opus {
payload_type: OPUS_DEFAULT_PT
})
);
assert_eq!(media.dtmf_events.len(), 2);
assert_eq!(
media.dtmf(),
Some(DtmfSpec {
payload_type: DTMF_WIDEBAND_DEFAULT_PT,
clock_rate: 48000
})
);
}
#[test]
fn round_trip_pinned_answer_then_parse() {
let menu = CodecMenu::Pinned {
codec: AudioCodec::Opus { payload_type: 111 },
dtmf: Some(DtmfSpec {
payload_type: 110,
clock_rate: 48000,
}),
};
let sdp = build_sdp_with(ip(), 8000, menu, MediaDirection::SendRecv, 0);
let media = parse_sdp(&sdp).unwrap();
assert_eq!(media.codec, Some(AudioCodec::Opus { payload_type: 111 }));
assert_eq!(
media.dtmf(),
Some(DtmfSpec {
payload_type: 110,
clock_rate: 48000
})
);
}
#[test]
fn select_codec_prefers_opus_anywhere_in_the_offer() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 8 102\r\n\
a=rtpmap:102 opus/48000/2\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(
select_codec(&media),
Some(AudioCodec::Opus { payload_type: 102 })
);
}
#[test]
fn select_codec_falls_back_to_g711_without_opus() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 8 0\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(select_codec(&media), Some(AudioCodec::Pcma));
}
#[test]
fn select_dtmf_matches_the_codec_clock() {
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\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:101 telephone-event/8000\r\n\
a=rtpmap:110 telephone-event/48000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(
select_dtmf(&media, AudioCodec::Opus { payload_type: 111 }),
Some(DtmfSpec {
payload_type: 110,
clock_rate: 48000
})
);
assert_eq!(
select_dtmf(&media, AudioCodec::Pcmu),
Some(DtmfSpec {
payload_type: 101,
clock_rate: 8000
})
);
}
#[test]
fn select_dtmf_falls_back_to_any_clock() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 111 101\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(
select_dtmf(&media, AudioCodec::Opus { payload_type: 111 }),
Some(DtmfSpec {
payload_type: 101,
clock_rate: 8000
})
);
}
#[test]
fn parse_sdp_accepts_telephone_event_at_48k() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 111 110\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:110 telephone-event/48000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(
media.dtmf(),
Some(DtmfSpec {
payload_type: 110,
clock_rate: 48000
})
);
}
#[test]
fn parse_sdp_records_telephone_event_at_any_clock() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/16000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(
media.dtmf(),
Some(DtmfSpec {
payload_type: 101,
clock_rate: 16000
})
);
}
#[test]
fn parse_sdp_picks_dtmf_pt_from_rtpmap_not_constant() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 96\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:96 telephone-event/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(
media.dtmf(),
Some(DtmfSpec {
payload_type: 96,
clock_rate: 8000
})
);
}
#[test]
fn parse_sdp_rejects_telephone_event_not_listed_on_m_line() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(media.dtmf(), None);
}
#[test]
fn parse_sdp_handles_mixed_case_telephone_event() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 Telephone-Event/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(
media.dtmf(),
Some(DtmfSpec {
payload_type: 101,
clock_rate: 8000
})
);
}
#[test]
fn parse_sdp_handles_telephone_event_without_fmtp() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\nm=audio 5000 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let media = parse_sdp(sdp).unwrap();
assert!(media.dtmf().is_some());
}
#[test]
fn parse_sdp_g711_only_body_still_parses_as_before() {
let sdp = b"v=0\r\n\
o=wavekat 0 0 IN IP4 10.0.0.2\r\n\
s=wavekat-sip\r\n\
c=IN IP4 10.0.0.2\r\n\
t=0 0\r\n\
m=audio 5004 RTP/AVP 0 8 101\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n\
a=fmtp:101 0-15\r\n\
a=sendrecv\r\n";
let media = parse_sdp(sdp).unwrap();
assert_eq!(media.payload_type, 0);
assert_eq!(media.codec, Some(AudioCodec::Pcmu));
assert_eq!(media.opus_payload_type, None);
assert_eq!(
media.dtmf(),
Some(DtmfSpec {
payload_type: 101,
clock_rate: 8000
})
);
}
#[test]
fn parse_sdp_missing_connection_line() {
let sdp = b"v=0\r\nm=audio 20000 RTP/AVP 0\r\n";
let err = parse_sdp(sdp).unwrap_err();
assert!(err.contains("connection address"));
}
#[test]
fn parse_sdp_missing_media_line() {
let sdp = b"v=0\r\nc=IN IP4 10.0.0.1\r\n";
let err = parse_sdp(sdp).unwrap_err();
assert!(err.contains("audio media"));
}
#[test]
fn parse_sdp_invalid_utf8() {
let sdp = &[0xFF, 0xFE, 0xFD];
let err = parse_sdp(sdp).unwrap_err();
assert!(err.contains("UTF-8"));
}
#[test]
fn audio_codec_wire_identities() {
assert_eq!(AudioCodec::Pcmu.payload_type(), 0);
assert_eq!(AudioCodec::Pcma.payload_type(), 8);
assert_eq!(AudioCodec::Opus { payload_type: 96 }.payload_type(), 96);
assert_eq!(AudioCodec::Pcmu.rtp_clock_rate(), 8000);
assert_eq!(AudioCodec::Pcma.rtp_clock_rate(), 8000);
assert_eq!(
AudioCodec::Opus { payload_type: 96 }.rtp_clock_rate(),
48000
);
}
}