use anyhow::{Result, anyhow};
use audio_codec::CodecType;
use rustrtc::{MediaKind, SdpType, SessionDescription};
use std::collections::{BTreeSet, HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct CodecInfo {
pub payload_type: u8,
pub codec: CodecType,
pub clock_rate: u32,
pub channels: u16,
}
impl CodecInfo {
pub fn to_params(&self) -> rustrtc::RtpCodecParameters {
rustrtc::RtpCodecParameters {
payload_type: self.payload_type,
clock_rate: self.clock_rate,
channels: if self.channels > 255 {
255
} else {
self.channels as u8
},
}
}
pub fn is_dtmf(&self) -> bool {
self.codec == CodecType::TelephoneEvent
}
pub fn to_audio_capability(&self) -> Option<rustrtc::config::AudioCapability> {
use rustrtc::config::AudioCapability;
let (codec_name, default_fmtp) = match self.codec {
CodecType::PCMU => ("PCMU".to_string(), None),
CodecType::PCMA => ("PCMA".to_string(), None),
CodecType::G722 => ("G722".to_string(), None),
CodecType::G729 => ("G729".to_string(), None),
#[cfg(feature = "opus")]
CodecType::Opus => (
"opus".to_string(),
Some("minptime=10;useinbandfec=1".to_string()),
),
CodecType::TelephoneEvent => ("telephone-event".to_string(), Some("0-16".to_string())),
#[allow(unreachable_patterns)]
_ => return None,
};
Some(AudioCapability {
payload_type: self.payload_type,
codec_name,
clock_rate: self.clock_rate,
channels: if self.channels > u8::MAX as u16 {
u8::MAX
} else {
self.channels as u8
},
fmtp: default_fmtp,
rtcp_fbs: vec![],
})
}
}
#[derive(Debug, Clone, Default)]
pub struct ExtractedCodecs {
pub audio: Vec<CodecInfo>,
pub video: Vec<CodecInfo>,
pub dtmf: Vec<CodecInfo>,
}
#[derive(Debug, Clone)]
pub struct NegotiationResult {
pub codec: CodecType,
pub params: rustrtc::RtpCodecParameters,
pub dtmf_pt: Option<u8>,
}
#[derive(Debug, Clone)]
pub struct NegotiatedCodec {
pub codec: CodecType,
pub payload_type: u8,
pub clock_rate: u32,
pub channels: u16,
}
#[derive(Debug, Clone, Default)]
pub struct NegotiatedLegProfile {
pub audio: Option<NegotiatedCodec>,
pub video: Option<NegotiatedCodec>,
pub dtmf: Option<NegotiatedCodec>,
}
pub struct MediaNegotiator;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CodecSelectionStrategy {
#[default]
Performance,
Quality,
}
fn quality_codec_order() -> Vec<CodecType> {
vec![
#[cfg(feature = "opus")]
CodecType::Opus,
CodecType::G729,
CodecType::G722,
CodecType::PCMU,
CodecType::PCMA,
]
}
#[derive(Debug, Clone)]
pub struct BridgeCodecLists {
pub caller_side: Vec<CodecInfo>,
pub callee_side: Vec<CodecInfo>,
pub common: Vec<CodecInfo>,
}
impl MediaNegotiator {
fn parse_audio_section(sdp_str: &str) -> Option<rustrtc::MediaSection> {
SessionDescription::parse(SdpType::Answer, sdp_str)
.or_else(|_| SessionDescription::parse(SdpType::Offer, sdp_str))
.ok()?
.media_sections
.into_iter()
.find(|m| m.kind == MediaKind::Audio)
}
fn parse_video_section(sdp_str: &str) -> Option<rustrtc::MediaSection> {
SessionDescription::parse(SdpType::Answer, sdp_str)
.or_else(|_| SessionDescription::parse(SdpType::Offer, sdp_str))
.ok()?
.media_sections
.into_iter()
.find(|m| m.kind == MediaKind::Video)
}
fn parse_rtpmap_attributes(
section: &rustrtc::MediaSection,
) -> (HashMap<u8, CodecInfo>, HashSet<u8>) {
let mut codec_by_pt = HashMap::new();
let mut unrecognized_pts = HashSet::new();
for attr in §ion.attributes {
if attr.key == "rtpmap"
&& let Some(ref value) = attr.value
&& let Some((pt_str, codec_str)) = value.split_once(' ')
&& let Ok(pt) = pt_str.parse::<u8>()
{
let parts: Vec<&str> = codec_str.split('/').collect();
if parts.len() >= 2 {
let codec_name = parts[0];
let clock_rate = parts[1].parse::<u32>().unwrap_or(8000);
let channels = if parts.len() >= 3 {
parts[2].parse::<u16>().unwrap_or(1)
} else {
1
};
let codec_type = match CodecType::try_from(codec_name) {
Ok(c) => c,
Err(_) => {
unrecognized_pts.insert(pt);
continue;
}
};
codec_by_pt.insert(
pt,
CodecInfo {
payload_type: pt,
codec: codec_type,
clock_rate,
channels,
},
);
}
}
}
(codec_by_pt, unrecognized_pts)
}
fn static_codec_for_payload(section: &rustrtc::MediaSection, pt: u8) -> Option<CodecInfo> {
let static_codec = if let Ok(codec) = CodecType::try_from(pt) {
let (rate, chans) = match codec {
CodecType::PCMU | CodecType::PCMA | CodecType::G722 | CodecType::G729 => (8000, 1),
#[cfg(feature = "opus")]
CodecType::Opus => (48000, 2),
_ => return None,
};
Some((codec, rate, chans))
} else {
#[cfg(feature = "opus")]
if (pt == 96 || pt == 111) && section.kind == MediaKind::Audio {
Some((CodecType::Opus, 48000, 2))
} else {
None
}
#[cfg(not(feature = "opus"))]
None
};
static_codec.map(|(codec, rate, chans)| CodecInfo {
payload_type: pt,
codec,
clock_rate: rate,
channels: chans,
})
}
fn extract_ordered_codecs_from_section(section: &rustrtc::MediaSection) -> Vec<CodecInfo> {
let (mut codec_by_pt, unrecognized_pts) = Self::parse_rtpmap_attributes(section);
let mut ordered_codecs = Vec::new();
let mut seen_pts = HashSet::new();
for format in §ion.formats {
let Ok(pt) = format.parse::<u8>() else {
continue;
};
if !seen_pts.insert(pt) {
continue;
}
if unrecognized_pts.contains(&pt) {
continue;
}
let codec = codec_by_pt
.remove(&pt)
.or_else(|| Self::static_codec_for_payload(section, pt));
if let Some(codec) = codec {
ordered_codecs.push(codec);
}
}
ordered_codecs
}
pub fn parse_rtp_map_from_section(
section: &rustrtc::MediaSection,
) -> Vec<(u8, (CodecType, u32, u16))> {
Self::extract_ordered_codecs_from_section(section)
.into_iter()
.map(|codec| {
(
codec.payload_type,
(codec.codec, codec.clock_rate, codec.channels),
)
})
.collect()
}
pub fn extract_codec_params(sdp_str: &str) -> ExtractedCodecs {
let mut extracted = ExtractedCodecs::default();
if let Some(section) = Self::parse_audio_section(sdp_str) {
for codec in Self::extract_ordered_codecs_from_section(§ion) {
if codec.is_dtmf() {
extracted.dtmf.push(codec);
} else {
extracted.audio.push(codec);
}
}
}
if let Some(section) = Self::parse_video_section(sdp_str) {
for codec in Self::extract_ordered_codecs_from_section(§ion) {
extracted.video.push(codec);
}
}
extracted
}
pub fn extract_dtmf_codecs(sdp_str: &str) -> Vec<CodecInfo> {
Self::extract_codec_params(sdp_str).dtmf
}
pub fn select_best_codec(
remote_codecs: &[CodecInfo],
allowed_codecs: &[CodecType],
) -> Option<CodecInfo> {
remote_codecs
.iter()
.filter(|c| c.codec != CodecType::TelephoneEvent)
.find(|c| allowed_codecs.is_empty() || allowed_codecs.contains(&c.codec))
.cloned()
}
pub fn extract_all_codecs(sdp_str: &str) -> Vec<CodecInfo> {
let extracted = Self::extract_codec_params(sdp_str);
extracted.audio.into_iter().chain(extracted.dtmf).collect()
}
pub fn negotiate_codec(
local_codecs: &[CodecType],
remote_sdp: &str,
) -> Result<NegotiationResult> {
let remote_codecs = Self::extract_all_codecs(remote_sdp);
for local_codec in local_codecs {
if let Some(remote) = remote_codecs
.iter()
.find(|r| r.codec == *local_codec && r.codec != CodecType::TelephoneEvent)
{
let params = rustrtc::RtpCodecParameters {
payload_type: remote.payload_type,
clock_rate: remote.clock_rate,
channels: if remote.channels > 255 {
255
} else {
remote.channels as u8
},
};
let remote_dtmf_codecs: Vec<_> = remote_codecs
.iter()
.filter(|r| r.codec == CodecType::TelephoneEvent)
.cloned()
.collect();
return Ok(NegotiationResult {
codec: remote.codec,
params,
dtmf_pt: remote_dtmf_codecs.first().map(|codec| codec.payload_type),
});
}
}
Err(anyhow!("No compatible codec found"))
}
pub fn default_rtp_codecs() -> Vec<CodecType> {
vec![
#[cfg(feature = "opus")]
CodecType::Opus,
CodecType::G729,
CodecType::G722,
CodecType::PCMU,
CodecType::PCMA,
CodecType::TelephoneEvent,
]
}
pub fn default_webrtc_codecs() -> Vec<CodecType> {
vec![
#[cfg(feature = "opus")]
CodecType::Opus,
CodecType::G722,
CodecType::PCMU,
CodecType::PCMA,
CodecType::TelephoneEvent,
]
}
fn supported_codecs_for_transport(is_webrtc: bool) -> Vec<CodecType> {
if is_webrtc {
Self::default_webrtc_codecs()
} else {
Self::default_rtp_codecs()
}
}
fn allowed_supported_codecs(is_webrtc: bool, allow_codecs: &[CodecType]) -> Vec<CodecType> {
Self::supported_codecs_for_transport(is_webrtc)
.into_iter()
.filter(|codec| allow_codecs.is_empty() || allow_codecs.contains(codec))
.collect()
}
pub(crate) fn codec_info_for_type(codec_type: CodecType) -> CodecInfo {
CodecInfo {
payload_type: codec_type.payload_type(),
codec: codec_type,
clock_rate: codec_type.clock_rate(),
channels: codec_type.channels(),
}
}
pub fn get_preferred_codec(codecs: &[CodecType]) -> Option<CodecType> {
codecs
.iter()
.find(|c| **c != CodecType::TelephoneEvent)
.copied()
}
pub fn extract_leg_profile(sdp: &str) -> NegotiatedLegProfile {
let extracted = Self::extract_codec_params(sdp);
let audio = extracted.audio.first().map(|c| NegotiatedCodec {
codec: c.codec,
payload_type: c.payload_type,
clock_rate: c.clock_rate,
channels: c.channels,
});
let video = extracted.video.first().map(|c| NegotiatedCodec {
codec: c.codec,
payload_type: c.payload_type,
clock_rate: c.clock_rate,
channels: c.channels,
});
let dtmf = match extracted.dtmf.len() {
0 => None,
1 => extracted.dtmf.first().map(|c| NegotiatedCodec {
codec: c.codec,
payload_type: c.payload_type,
clock_rate: c.clock_rate,
channels: c.channels,
}),
_ => {
let preferred_rate = match audio.as_ref().map(|codec| codec.codec) {
#[cfg(feature = "opus")]
Some(CodecType::Opus) => 48000,
_ => 8000,
};
extracted
.dtmf
.iter()
.find(|codec| codec.clock_rate == preferred_rate)
.or(extracted.dtmf.first())
.map(|c| NegotiatedCodec {
codec: c.codec,
payload_type: c.payload_type,
clock_rate: c.clock_rate,
channels: c.channels,
})
}
};
NegotiatedLegProfile { audio, video, dtmf }
}
fn attr_payload_type(attr: &rustrtc::sdp::Attribute) -> Option<u8> {
let value = attr.value.as_ref()?;
value.split_whitespace().next()?.parse::<u8>().ok()
}
fn codec_signature(codec: &CodecInfo) -> String {
format!("{:?}/{}/{}", codec.codec, codec.clock_rate, codec.channels)
}
pub fn restrict_answer_to_callee_accepted_codecs(
answer_sdp: &str,
callee_answer_sdp: &str,
) -> Option<String> {
let caller_answer_codecs = Self::extract_codec_params(answer_sdp);
let callee_answer_codecs = Self::extract_codec_params(callee_answer_sdp);
let accepted_by_callee: HashSet<String> = callee_answer_codecs
.audio
.iter()
.chain(callee_answer_codecs.dtmf.iter())
.map(Self::codec_signature)
.collect();
if accepted_by_callee.is_empty() {
return None;
}
let mut allowed_pts = Vec::new();
let mut seen_pts = HashSet::new();
for codec in caller_answer_codecs
.audio
.iter()
.chain(caller_answer_codecs.dtmf.iter())
{
let signature = Self::codec_signature(codec);
if accepted_by_callee.contains(&signature) && seen_pts.insert(codec.payload_type) {
allowed_pts.push(codec.payload_type);
}
}
if allowed_pts.is_empty() {
return None;
}
let mut desc = SessionDescription::parse(SdpType::Answer, answer_sdp).ok()?;
let audio_section = desc
.media_sections
.iter_mut()
.find(|section| section.kind == MediaKind::Audio)?;
audio_section.formats = allowed_pts.iter().map(|pt| pt.to_string()).collect();
audio_section
.attributes
.retain(|attr| match attr.key.as_str() {
"rtpmap" | "fmtp" | "rtcp-fb" => {
Self::attr_payload_type(attr).is_none_or(|pt| allowed_pts.contains(&pt))
}
_ => true,
});
Some(desc.to_sdp_string())
}
pub fn build_callee_codec_offer(caller_sdp: &str, is_webrtc: bool) -> Vec<CodecInfo> {
Self::build_callee_codec_offer_with_allow(
caller_sdp,
is_webrtc,
&[],
CodecSelectionStrategy::default(),
)
}
pub fn build_callee_codec_offer_with_allow(
caller_sdp: &str,
is_webrtc: bool,
allow_codecs: &[CodecType],
strategy: CodecSelectionStrategy,
) -> Vec<CodecInfo> {
let extracted = Self::extract_codec_params(caller_sdp);
let supported = Self::allowed_supported_codecs(is_webrtc, allow_codecs);
let mut result: Vec<CodecInfo> = Vec::new();
let mut seen_codecs: BTreeSet<CodecType> = BTreeSet::new();
for codec in &extracted.audio {
if supported.contains(&codec.codec) {
result.push(codec.clone());
seen_codecs.insert(codec.codec);
}
}
if strategy == CodecSelectionStrategy::Quality {
for codec_type in &supported {
if *codec_type == CodecType::TelephoneEvent {
continue; }
if !seen_codecs.contains(codec_type) {
result.push(Self::codec_info_for_type(*codec_type));
seen_codecs.insert(*codec_type);
}
}
let quality_order = quality_codec_order();
let mut audio: Vec<CodecInfo> = Vec::new();
let mut dtmf: Vec<CodecInfo> = Vec::new();
for c in result.drain(..) {
if c.is_dtmf() {
dtmf.push(c);
} else {
audio.push(c);
}
}
audio.sort_by(|a, b| {
let pa = quality_order
.iter()
.position(|c| *c == a.codec)
.unwrap_or(usize::MAX);
let pb = quality_order
.iter()
.position(|c| *c == b.codec)
.unwrap_or(usize::MAX);
pa.cmp(&pb)
});
result = audio;
result.extend(dtmf);
}
if supported.contains(&CodecType::TelephoneEvent) && !extracted.dtmf.is_empty() {
let mut seen_dtmf_rates: HashSet<u32> = HashSet::new();
for dtmf in &extracted.dtmf {
result.push(dtmf.clone());
seen_dtmf_rates.insert(dtmf.clock_rate);
}
let has_opus = result.iter().any(|c| c.codec == CodecType::Opus);
let has_narrowband = result
.iter()
.any(|c| c.codec.is_audio() && c.codec != CodecType::Opus);
let mut needed_rates: Vec<u32> = Vec::new();
if has_narrowband && !seen_dtmf_rates.contains(&8000) {
needed_rates.push(8000);
}
if has_opus && !seen_dtmf_rates.contains(&48000) {
needed_rates.push(48000);
}
let mut used_pts: HashSet<u8> = result.iter().map(|c| c.payload_type).collect();
for rate in needed_rates {
let default_pt = CodecType::TelephoneEvent.payload_type();
let pt = if !used_pts.contains(&default_pt) {
default_pt
} else {
(96..=127)
.find(|p| !used_pts.contains(p))
.unwrap_or(default_pt)
};
used_pts.insert(pt);
result.push(CodecInfo {
payload_type: pt,
clock_rate: rate,
channels: 1,
codec: CodecType::TelephoneEvent,
});
}
}
result
}
pub fn build_caller_answer_codec_list(caller_sdp: &str, is_webrtc: bool) -> Vec<CodecInfo> {
Self::build_caller_answer_codec_list_with_allow(caller_sdp, is_webrtc, &[])
}
pub fn build_caller_answer_codec_list_with_allow(
caller_sdp: &str,
is_webrtc: bool,
allow_codecs: &[CodecType],
) -> Vec<CodecInfo> {
let extracted = Self::extract_codec_params(caller_sdp);
let supported = Self::allowed_supported_codecs(is_webrtc, allow_codecs);
let mut result = Vec::new();
for codec in extracted.audio.into_iter().chain(extracted.dtmf) {
if supported.contains(&codec.codec) {
result.push(codec);
}
}
result
}
pub fn build_bridge_codec_lists(
caller_sdp: &str,
caller_is_webrtc: bool,
callee_is_webrtc: bool,
allow_codecs: &[CodecType],
strategy: CodecSelectionStrategy,
) -> BridgeCodecLists {
let caller_side = Self::build_caller_answer_codec_list_with_allow(
caller_sdp,
caller_is_webrtc,
allow_codecs,
);
let callee_side = Self::build_callee_codec_offer_with_allow(
caller_sdp,
callee_is_webrtc,
allow_codecs,
strategy,
);
let common: Vec<CodecInfo> = caller_side
.iter()
.filter(|c| callee_side.iter().any(|cc| cc.codec == c.codec))
.cloned()
.collect();
BridgeCodecLists {
caller_side,
callee_side,
common,
}
}
pub fn extract_ssrc(sdp: &str) -> Option<u32> {
let session = SessionDescription::parse(SdpType::Answer, sdp)
.or_else(|_| SessionDescription::parse(SdpType::Offer, sdp))
.ok()?;
for section in session.media_sections {
if section.kind == MediaKind::Audio {
for attr in section.attributes {
if attr.key == "ssrc"
&& let Some(value) = attr.value
{
let ssrc_str = value.split_whitespace().next()?;
if let Ok(ssrc) = ssrc_str.parse::<u32>() {
return Some(ssrc);
}
}
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_rtp_map() {
let sdp = "v=0\r\n\
o=- 1234 1234 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 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";
let desc = SessionDescription::parse(SdpType::Offer, sdp).unwrap();
let section = desc
.media_sections
.iter()
.find(|m| m.kind == MediaKind::Audio)
.unwrap();
let rtp_map = MediaNegotiator::parse_rtp_map_from_section(section);
assert_eq!(rtp_map.len(), 3);
assert!(
rtp_map
.iter()
.any(|(pt, (c, _, _))| *pt == 0 && *c == CodecType::PCMU)
);
assert!(
rtp_map
.iter()
.any(|(pt, (c, _, _))| *pt == 8 && *c == CodecType::PCMA)
);
assert!(
rtp_map
.iter()
.any(|(pt, (c, _, _))| *pt == 101 && *c == CodecType::TelephoneEvent)
);
}
#[test]
fn test_extract_codec_params() {
let sdp = "v=0\r\n\
o=- 1234 1234 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let codecs = MediaNegotiator::extract_codec_params(sdp);
let first = &codecs.audio[0];
let params = first.to_params();
assert_eq!(first.codec, CodecType::PCMU);
assert_eq!(params.payload_type, 0);
assert_eq!(params.clock_rate, 8000);
assert_eq!(
codecs
.dtmf
.iter()
.map(|codec| codec.payload_type)
.collect::<Vec<_>>(),
vec![101]
);
}
#[test]
fn test_extract_codec_params_preserves_dtmf_offer_order() {
let sdp = "v=0\r\n\
o=- 1234 1234 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 96 110 126\r\n\
a=rtpmap:96 opus/48000/2\r\n\
a=rtpmap:110 telephone-event/48000\r\n\
a=rtpmap:126 telephone-event/8000\r\n";
let codecs = MediaNegotiator::extract_codec_params(sdp);
assert_eq!(
codecs
.dtmf
.iter()
.map(|codec| (codec.payload_type, codec.clock_rate))
.collect::<Vec<_>>(),
vec![(110, 48000), (126, 8000)]
);
}
#[test]
fn test_negotiate_codec() {
let local_codecs = vec![CodecType::PCMU, CodecType::PCMA];
let remote_sdp = "v=0\r\n\
o=- 1234 1234 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 8 101\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let result = MediaNegotiator::negotiate_codec(&local_codecs, remote_sdp).unwrap();
assert_eq!(result.codec, CodecType::PCMA);
assert_eq!(result.params.payload_type, 8);
assert_eq!(result.dtmf_pt, Some(101));
}
#[test]
fn test_negotiate_codec_no_match() {
let local_codecs = vec![CodecType::Opus];
let remote_sdp = "v=0\r\n\
o=- 1234 1234 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 0\r\n\
a=rtpmap:0 PCMU/8000\r\n";
let result = MediaNegotiator::negotiate_codec(&local_codecs, remote_sdp);
assert!(result.is_err());
}
#[test]
fn test_default_codecs() {
let rtp_codecs = MediaNegotiator::default_rtp_codecs();
assert!(rtp_codecs.contains(&CodecType::PCMU));
assert!(rtp_codecs.contains(&CodecType::PCMA));
let webrtc_codecs = MediaNegotiator::default_webrtc_codecs();
assert!(webrtc_codecs.contains(&CodecType::PCMU));
}
#[test]
fn test_parse_static_payload_types() {
let sdp = "v=0\r\n\
o=- 1234 1234 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 0 8 101\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let desc = SessionDescription::parse(SdpType::Offer, sdp).unwrap();
let section = desc
.media_sections
.iter()
.find(|m| m.kind == MediaKind::Audio)
.unwrap();
let rtp_map = MediaNegotiator::parse_rtp_map_from_section(section);
println!("RTP MAP: {:?}", rtp_map);
assert!(
rtp_map
.iter()
.any(|(pt, (codec, _, _))| *pt == 0 && *codec == CodecType::PCMU),
"Missing PCMU (0)"
);
assert!(
rtp_map
.iter()
.any(|(pt, (codec, _, _))| *pt == 8 && *codec == CodecType::PCMA),
"Missing PCMA (8)"
);
}
#[test]
fn test_parse_dynamic_payload_type_fallback() {
let sdp = "v=0\r\n\
o=- 1234 1234 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 96\r\n";
let desc = SessionDescription::parse(SdpType::Offer, sdp).unwrap();
let section = desc
.media_sections
.iter()
.find(|m| m.kind == MediaKind::Audio)
.unwrap();
let rtp_map = MediaNegotiator::parse_rtp_map_from_section(section);
assert!(
rtp_map.iter().any(|(pt, (codec, rate, chans))| *pt == 96
&& *codec == CodecType::Opus
&& *rate == 48000
&& *chans == 2),
"Missing fallback for Opus (96)"
);
}
#[test]
fn test_parse_dynamic_payload_type_fallback_111() {
let sdp = "v=0\r\n\
o=- 1234 1234 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 111\r\n";
let desc = SessionDescription::parse(SdpType::Offer, sdp).unwrap();
let section = desc
.media_sections
.iter()
.find(|m| m.kind == MediaKind::Audio)
.unwrap();
let rtp_map = MediaNegotiator::parse_rtp_map_from_section(section);
assert!(
rtp_map.iter().any(|(pt, (codec, rate, chans))| *pt == 111
&& *codec == CodecType::Opus
&& *rate == 48000
&& *chans == 2),
"Missing fallback for Opus (111)"
);
}
#[test]
fn test_extract_codec_params_order_preference() {
let sdp = "v=0\r\no=- 123456 123456 IN IP4 127.0.0.1\r\ns=-\r\nc=IN IP4 127.0.0.1\r\nt=0 0\r\nm=audio 4000 RTP/AVP 0 101 8 9\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:101 telephone-event/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:9 G722/8000\r\n";
let codecs = MediaNegotiator::extract_codec_params(sdp);
assert_eq!(
codecs.audio[0].codec,
CodecType::PCMU,
"Should have picked PCMU (the first codec)"
);
}
#[test]
fn test_select_best_codec_with_preference() {
let codecs = vec![
CodecInfo {
payload_type: 9,
codec: CodecType::G722,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 0,
codec: CodecType::PCMU,
clock_rate: 8000,
channels: 1,
},
];
let allowed = vec![CodecType::PCMU, CodecType::G722];
let best = MediaNegotiator::select_best_codec(&codecs, &allowed).unwrap();
assert_eq!(best.codec, CodecType::G722);
let allowed = vec![CodecType::G722, CodecType::PCMU];
let best = MediaNegotiator::select_best_codec(&codecs, &allowed).unwrap();
assert_eq!(best.codec, CodecType::G722);
let allowed = vec![CodecType::PCMU];
let best = MediaNegotiator::select_best_codec(&codecs, &allowed).unwrap();
assert_eq!(best.codec, CodecType::PCMU);
let allowed = vec![];
let best = MediaNegotiator::select_best_codec(&codecs, &allowed).unwrap();
assert_eq!(best.codec, CodecType::G722);
}
#[test]
fn test_select_best_codec_skips_telephone_event() {
let codecs = vec![
CodecInfo {
payload_type: 101,
codec: CodecType::TelephoneEvent,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 0,
codec: CodecType::PCMU,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 8,
codec: CodecType::PCMA,
clock_rate: 8000,
channels: 1,
},
];
let allowed = vec![CodecType::PCMU, CodecType::PCMA];
let best = MediaNegotiator::select_best_codec(&codecs, &allowed).unwrap();
assert_eq!(best.codec, CodecType::PCMU);
assert_ne!(best.codec, CodecType::TelephoneEvent);
let allowed = vec![];
let best = MediaNegotiator::select_best_codec(&codecs, &allowed).unwrap();
assert_eq!(best.codec, CodecType::PCMU);
assert_ne!(best.codec, CodecType::TelephoneEvent);
}
#[test]
fn test_g722_clock_rate_preserves_sdp_value() {
let sdp = "v=0\r\n\
o=- 1769236545 1769236546 IN IP4 192.168.3.211\r\n\
s=-\r\n\
c=IN IP4 192.168.3.211\r\n\
t=0 0\r\n\
m=audio 51624 RTP/AVP 0 8 9 18 111\r\n\
a=mid:0\r\n\
a=sendrecv\r\n\
a=rtcp-mux\r\n\
a=rtpmap:0 PCMU/8000/1\r\n\
a=rtpmap:8 PCMA/8000/1\r\n\
a=rtpmap:9 G722/16000/1\r\n\
a=rtpmap:18 G729/8000/1\r\n\
a=rtpmap:111 opus/48000/2\r\n";
let codecs = MediaNegotiator::extract_codec_params(sdp);
let g722_info = codecs.audio.iter().find(|c| c.codec == CodecType::G722);
assert!(g722_info.is_some(), "G722 should be parsed");
let g722_info = g722_info.unwrap();
assert_eq!(
g722_info.clock_rate, 16000,
"G722 clock rate should now follow the SDP value as offered"
);
assert_eq!(g722_info.payload_type, 9);
assert_eq!(g722_info.channels, 1);
let g729_info = codecs.audio.iter().find(|c| c.codec == CodecType::G729);
assert!(g729_info.is_some());
assert_eq!(g729_info.unwrap().clock_rate, 8000);
}
#[test]
fn test_answer_codec_selection_respects_answerer_preference() {
let answer_codecs = vec![
CodecInfo {
payload_type: 0,
codec: CodecType::PCMU,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 8,
codec: CodecType::PCMA,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 9,
codec: CodecType::G722,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 18,
codec: CodecType::G729,
clock_rate: 8000,
channels: 1,
},
];
let our_offer_order = vec![
CodecType::G729,
CodecType::G722,
CodecType::PCMU,
CodecType::PCMA,
];
let selected = MediaNegotiator::select_best_codec(&answer_codecs, &our_offer_order);
assert!(selected.is_some(), "Should find a matching codec");
let selected = selected.unwrap();
assert_eq!(
selected.codec,
CodecType::PCMU,
"Must use PCMU (answerer's first choice), not G729 (offerer's first choice)"
);
assert_eq!(selected.payload_type, 0);
}
#[test]
fn test_bridge_codecs_webrtc_caller_rtp_callee_pcmu_only() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 12345 UDP/TLS/RTP/SAVPF 111 0 101\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
true, false, &[CodecType::PCMU, CodecType::TelephoneEvent],
CodecSelectionStrategy::default(),
);
assert!(lists.caller_side.iter().any(|c| c.codec == CodecType::PCMU));
assert!(
!lists.caller_side.iter().any(|c| c.codec == CodecType::Opus),
"Opus not in allow_codecs"
);
assert!(
lists
.caller_side
.iter()
.any(|c| c.codec == CodecType::TelephoneEvent)
);
assert!(lists.callee_side.iter().any(|c| c.codec == CodecType::PCMU));
assert!(!lists.callee_side.iter().any(|c| c.codec == CodecType::Opus));
assert!(
lists
.callee_side
.iter()
.any(|c| c.codec == CodecType::TelephoneEvent)
);
}
#[test]
fn test_bridge_codecs_prefer_no_transcode_opus() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 12345 UDP/TLS/RTP/SAVPF 111 0 101\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
true, false, &[CodecType::Opus, CodecType::PCMU, CodecType::TelephoneEvent],
CodecSelectionStrategy::default(),
);
let caller_audio: Vec<_> = lists.caller_side.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(
caller_audio[0].codec,
CodecType::Opus,
"Opus should be first on caller side"
);
assert_eq!(
caller_audio[1].codec,
CodecType::PCMU,
"PCMU should be second"
);
let callee_audio: Vec<_> = lists.callee_side.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(
callee_audio[0].codec,
CodecType::Opus,
"Opus should be first on callee side"
);
assert_eq!(callee_audio[1].codec, CodecType::PCMU);
}
#[test]
fn test_bridge_codecs_g729_dropped_for_webrtc_side() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 18 0 101\r\n\
a=rtpmap:18 G729/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, true, &[CodecType::G729, CodecType::PCMU, CodecType::TelephoneEvent],
CodecSelectionStrategy::default(),
);
assert!(
lists.caller_side.iter().any(|c| c.codec == CodecType::G729),
"G729 OK on RTP caller side"
);
assert!(lists.caller_side.iter().any(|c| c.codec == CodecType::PCMU));
assert!(
!lists.callee_side.iter().any(|c| c.codec == CodecType::G729),
"G729 dropped on WebRTC callee side"
);
assert!(
lists.callee_side.iter().any(|c| c.codec == CodecType::PCMU),
"PCMU kept on WebRTC callee side"
);
}
#[test]
fn test_bridge_codecs_empty_allow_codecs_fallback() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, true, &[], CodecSelectionStrategy::Quality, );
let caller_audio: Vec<_> = lists.caller_side.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(caller_audio.len(), 1);
assert_eq!(caller_audio[0].codec, CodecType::PCMU);
let callee_audio: Vec<_> = lists.callee_side.iter().filter(|c| !c.is_dtmf()).collect();
assert!(
callee_audio
.iter()
.any(|codec| codec.codec == CodecType::PCMU),
"Callee side should include supported PCMU"
);
assert!(
callee_audio
.iter()
.any(|codec| codec.codec == CodecType::G722),
"Callee side should include generated WebRTC-supported codecs"
);
}
#[test]
fn test_bridge_codecs_preserves_caller_payload_type() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, false, &[CodecType::PCMU, CodecType::TelephoneEvent],
CodecSelectionStrategy::default(),
);
let caller_pcmu = lists
.caller_side
.iter()
.find(|c| c.codec == CodecType::PCMU)
.unwrap();
assert_eq!(
caller_pcmu.payload_type, 0,
"Caller side should preserve caller PT 0"
);
let callee_pcmu = lists
.callee_side
.iter()
.find(|c| c.codec == CodecType::PCMU)
.unwrap();
assert_eq!(
callee_pcmu.payload_type, 0,
"Callee side should preserve caller PT 0"
);
}
#[test]
fn test_bridge_codecs_preserves_caller_dtmf_payload_types_and_rates() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 12345 UDP/TLS/RTP/SAVPF 111 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 lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
true, false, &[CodecType::Opus, CodecType::TelephoneEvent],
CodecSelectionStrategy::default(),
);
let caller_dtmf: Vec<_> = lists
.caller_side
.iter()
.filter(|c| c.codec == CodecType::TelephoneEvent)
.collect();
assert_eq!(caller_dtmf.len(), 2);
assert_eq!(caller_dtmf[0].payload_type, 101);
assert_eq!(caller_dtmf[0].clock_rate, 8000);
assert_eq!(caller_dtmf[1].payload_type, 110);
assert_eq!(caller_dtmf[1].clock_rate, 48000);
let callee_dtmf: Vec<_> = lists
.callee_side
.iter()
.filter(|c| c.codec == CodecType::TelephoneEvent)
.collect();
assert_eq!(callee_dtmf.len(), 2);
assert_eq!(callee_dtmf[0].clock_rate, 8000);
assert_eq!(callee_dtmf[1].clock_rate, 48000);
}
#[test]
fn test_bridge_codecs_rtp_caller_webrtc_callee() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 8 0 101\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, true, &[
CodecType::Opus,
CodecType::PCMU,
CodecType::PCMA,
CodecType::TelephoneEvent,
],
CodecSelectionStrategy::Quality, );
let caller_audio: Vec<_> = lists.caller_side.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(
caller_audio[0].codec,
CodecType::PCMA,
"Caller side preserves PCMA first from caller SDP"
);
assert_eq!(caller_audio[1].codec, CodecType::PCMU);
assert_eq!(caller_audio.len(), 2);
assert!(!lists.caller_side.iter().any(|c| c.codec == CodecType::Opus));
let callee_audio: Vec<_> = lists.callee_side.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(callee_audio[0].codec, CodecType::Opus);
assert_eq!(callee_audio[1].codec, CodecType::PCMU);
assert_eq!(callee_audio[2].codec, CodecType::PCMA);
assert_eq!(callee_audio.len(), 3);
}
#[test]
fn test_restrict_answer_to_callee_accepted_codecs_preserves_caller_payload_types() {
let answer_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 9 UDP/TLS/RTP/SAVPF 96 0 110 126\r\n\
c=IN IP4 0.0.0.0\r\n\
a=mid:0\r\n\
a=sendrecv\r\n\
a=rtcp-mux\r\n\
a=rtpmap:96 opus/48000/2\r\n\
a=fmtp:96 minptime=10;useinbandfec=1\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:110 telephone-event/48000\r\n\
a=fmtp:110 0-16\r\n\
a=rtpmap:126 telephone-event/8000\r\n\
a=fmtp:126 0-16\r\n";
let callee_answer = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 9 RTP/AVP 111 0 101\r\n\
c=IN IP4 0.0.0.0\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=fmtp:111 useinbandfec=1\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n\
a=fmtp:101 0-16\r\n";
let filtered =
MediaNegotiator::restrict_answer_to_callee_accepted_codecs(answer_sdp, callee_answer)
.unwrap();
assert!(filtered.contains("m=audio 9 UDP/TLS/RTP/SAVPF 96 0 126"));
assert!(filtered.contains("a=rtpmap:96 opus/48000/2"));
assert!(filtered.contains("a=rtpmap:0 PCMU/8000"));
assert!(filtered.contains("a=rtpmap:126 telephone-event/8000"));
assert!(!filtered.contains("a=rtpmap:110 telephone-event/48000"));
assert!(!filtered.contains("a=rtpmap:101 telephone-event/8000"));
}
#[test]
fn test_codec_info_to_audio_capability() {
let codecs = vec![
CodecInfo {
payload_type: 0,
codec: CodecType::PCMU,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 8,
codec: CodecType::PCMA,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 9,
codec: CodecType::G722,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 18,
codec: CodecType::G729,
clock_rate: 8000,
channels: 1,
},
CodecInfo {
payload_type: 101,
codec: CodecType::TelephoneEvent,
clock_rate: 8000,
channels: 1,
},
];
for ci in &codecs {
assert!(
ci.to_audio_capability().is_some(),
"{:?} should convert to AudioCapability",
ci.codec
);
}
}
#[test]
fn test_bridge_codecs_common_excludes_transport_unsupported() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 18 0 101\r\n\
a=rtpmap:18 G729/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, true, &[],
CodecSelectionStrategy::default(),
);
assert!(
!lists.common.iter().any(|c| c.codec == CodecType::G729),
"G729 must NOT be in common (WebRTC does not support it)"
);
assert!(
lists.common.iter().any(|c| c.codec == CodecType::PCMU),
"PCMU must be in common (both sides support it)"
);
assert!(
lists
.common
.iter()
.any(|c| c.codec == CodecType::TelephoneEvent),
"TelephoneEvent must be in common"
);
let first_audio = lists.common.iter().find(|c| !c.is_dtmf());
assert!(
first_audio.is_some(),
"Common must have at least one audio codec"
);
assert_eq!(
first_audio.unwrap().codec,
CodecType::PCMU,
"First common audio codec should be PCMU (G729 excluded)"
);
}
#[test]
fn test_bridge_codecs_common_preserves_caller_ordering() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 18 8 0 101\r\n\
a=rtpmap:18 G729/8000\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, true, &[],
CodecSelectionStrategy::default(),
);
let common_audio: Vec<_> = lists.common.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(common_audio.len(), 2, "2 common audio codecs: PCMA + PCMU");
assert_eq!(
common_audio[0].codec,
CodecType::PCMA,
"PCMA must be first in common (caller ordered PCMA before PCMU)"
);
assert_eq!(
common_audio[1].codec,
CodecType::PCMU,
"PCMU must be second in common"
);
assert!(
!lists.common.iter().any(|c| c.codec == CodecType::G729),
"G729 excluded from common"
);
}
#[test]
fn test_bridge_codecs_common_identical_when_same_transport() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 9 UDP/TLS/RTP/SAVPF 111 0 101\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
true, true, &[],
CodecSelectionStrategy::default(),
);
let common_audio: Vec<_> = lists.common.iter().filter(|c| !c.is_dtmf()).collect();
let caller_audio: Vec<_> = lists.caller_side.iter().filter(|c| !c.is_dtmf()).collect();
let callee_audio: Vec<_> = lists.callee_side.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(common_audio.len(), caller_audio.len());
for (c, a) in common_audio.iter().zip(caller_audio.iter()) {
assert_eq!(c.codec, a.codec, "common and caller_side must match");
}
assert!(
callee_audio.len() >= common_audio.len(),
"callee_side can include additional generated codecs"
);
}
#[test]
fn test_bridge_codecs_common_respects_allow_codecs() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 18 8 0 101\r\n\
a=rtpmap:18 G729/8000\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, true, &[CodecType::PCMU, CodecType::TelephoneEvent],
CodecSelectionStrategy::default(),
);
let common_audio: Vec<_> = lists.common.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(
common_audio.len(),
1,
"Only PCMU allowed, so only PCMU in common"
);
assert_eq!(common_audio[0].codec, CodecType::PCMU);
assert!(!lists.common.iter().any(|c| c.codec == CodecType::G729));
assert!(!lists.common.iter().any(|c| c.codec == CodecType::PCMA));
}
#[test]
fn test_bridge_codecs_ignores_unrecognized_rtpmap_entries() {
let caller_sdp = "v=0\r\n\
o=- 1777370486 1777370486 IN IP4 58.246.19.74\r\n\
s=-\r\n\
c=IN IP4 58.246.19.74\r\n\
t=0 0\r\n\
m=audio 16844 RTP/AVP 98 96 111 106 18 8 0 100\r\n\
a=rtpmap:98 AMR-WB/16000/1\r\n\
a=rtpmap:96 AMR/8000/1\r\n\
a=rtpmap:111 EVS/16000\r\n\
a=rtpmap:106 EVS/16000\r\n\
a=rtpmap:18 G729/8000\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:100 telephone-event/8000\r\n";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, true, &[],
CodecSelectionStrategy::default(),
);
assert!(
!lists.caller_side.iter().any(|c| c.payload_type == 96),
"PT 96 must NOT appear (it was AMR, not Opus)"
);
assert!(
!lists.caller_side.iter().any(|c| c.payload_type == 111),
"PT 111 must NOT appear (it was EVS, not Opus)"
);
assert!(
!lists.caller_side.iter().any(|c| c.payload_type == 98),
"PT 98 must NOT appear (it was AMR-WB)"
);
assert!(
!lists.caller_side.iter().any(|c| c.payload_type == 106),
"PT 106 must NOT appear (it was EVS)"
);
assert!(
!lists.common.iter().any(|c| c.payload_type == 96),
"Common must NOT include PT 96"
);
assert!(
!lists.common.iter().any(|c| c.payload_type == 111),
"Common must NOT include PT 111"
);
let has_g729 = lists
.caller_side
.iter()
.any(|c| c.codec == CodecType::G729 && c.payload_type == 18);
assert!(has_g729, "G729 at PT 18 must appear in caller_side");
let has_pcma = lists
.caller_side
.iter()
.any(|c| c.codec == CodecType::PCMA && c.payload_type == 8);
assert!(has_pcma, "PCMA at PT 8 must appear in caller_side");
let has_pcmu = lists
.caller_side
.iter()
.any(|c| c.codec == CodecType::PCMU && c.payload_type == 0);
assert!(has_pcmu, "PCMU at PT 0 must appear in caller_side");
let common_g729 = lists.common.iter().any(|c| c.codec == CodecType::G729);
assert!(!common_g729, "G729 must NOT be in common (WebRTC callee)");
let common_pcma = lists.common.iter().any(|c| c.codec == CodecType::PCMA);
assert!(common_pcma, "PCMA must be in common");
let common_pcmu = lists.common.iter().any(|c| c.codec == CodecType::PCMU);
assert!(common_pcmu, "PCMU must be in common");
let common_dtmf = lists
.common
.iter()
.any(|c| c.codec == CodecType::TelephoneEvent);
assert!(common_dtmf, "TelephoneEvent must be in common");
assert!(
!lists.caller_side.iter().any(|c| c.codec == CodecType::Opus),
"Opus must NOT appear in caller_side (PSTN didn't offer Opus)"
);
assert!(
!lists.common.iter().any(|c| c.codec == CodecType::Opus),
"Opus must NOT appear in common (PSTN didn't offer Opus)"
);
}
#[test]
fn test_codec_selection_strategy_serde() {
let perf = CodecSelectionStrategy::Performance;
let json = serde_json::to_string(&perf).unwrap();
assert_eq!(json, "\"performance\"");
let back: CodecSelectionStrategy = serde_json::from_str(&json).unwrap();
assert_eq!(back, CodecSelectionStrategy::Performance);
let qual = CodecSelectionStrategy::Quality;
let json = serde_json::to_string(&qual).unwrap();
assert_eq!(json, "\"quality\"");
let back: CodecSelectionStrategy = serde_json::from_str(&json).unwrap();
assert_eq!(back, CodecSelectionStrategy::Quality);
}
#[test]
fn test_performance_strategy_keeps_only_caller_codecs() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 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";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, false, &[CodecType::PCMU, CodecType::PCMA, CodecType::TelephoneEvent],
CodecSelectionStrategy::Performance,
);
let callee_audio: Vec<_> = lists.callee_side.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(callee_audio.len(), 2, "Performance: only caller's codecs");
assert_eq!(
callee_audio[0].codec,
CodecType::PCMU,
"PCMU first (caller order)"
);
assert_eq!(
callee_audio[1].codec,
CodecType::PCMA,
"PCMA second (caller order)"
);
assert!(!callee_audio.iter().any(|c| c.codec == CodecType::G722));
assert!(!callee_audio.iter().any(|c| c.codec == CodecType::G729));
}
#[test]
fn test_quality_strategy_appends_and_orders() {
let caller_sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 10000 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";
let lists = MediaNegotiator::build_bridge_codec_lists(
caller_sdp,
false, false, &[
CodecType::PCMU,
CodecType::PCMA,
CodecType::G722,
CodecType::TelephoneEvent,
],
CodecSelectionStrategy::Quality,
);
let callee_audio: Vec<_> = lists.callee_side.iter().filter(|c| !c.is_dtmf()).collect();
assert_eq!(
callee_audio.len(),
3,
"Quality: caller codecs + appended G722"
);
assert_eq!(
callee_audio[0].codec,
CodecType::G722,
"G722 first (quality order)"
);
assert_eq!(callee_audio[1].codec, CodecType::PCMU, "PCMU second");
assert_eq!(callee_audio[2].codec, CodecType::PCMA, "PCMA third");
}
}