use std::collections::HashMap;
use crate::sdp::error::{SdpCodecError, SdpWarning};
use crate::sdp::static_payload;
use crate::sdp::SdpDirection;
struct AttrCursor<'a> {
rest: &'a str,
}
impl<'a> AttrCursor<'a> {
fn new(value: &'a str) -> Self {
Self { rest: value }
}
fn skip_ws(&mut self) {
self.rest = self
.rest
.trim_start_matches([' ', '\t']);
}
fn number<T: std::str::FromStr>(&mut self) -> Option<T> {
self.skip_ws();
let end = self
.rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(
self.rest
.len(),
);
if end == 0 {
return None;
}
let (digits, rest) = self
.rest
.split_at(end);
self.rest = rest;
self.skip_ws();
digits
.parse()
.ok()
}
fn field(&mut self, stop: &[char]) -> &'a str {
self.skip_ws();
let end = self
.rest
.find(|c: char| c == ' ' || c == '\t' || stop.contains(&c))
.unwrap_or(
self.rest
.len(),
);
let (field, rest) = self
.rest
.split_at(end);
self.rest = rest;
field
}
fn eat(&mut self, ch: char) -> bool {
match self
.rest
.strip_prefix(ch)
{
Some(rest) => {
self.rest = rest;
true
}
None => false,
}
}
fn remaining(&self) -> &'a str {
self.rest
}
}
pub(super) fn parse_rtpmap(value: &str) -> Result<(u8, String, u32, Option<u8>), SdpCodecError> {
let mut cursor = AttrCursor::new(value);
let pt = cursor
.number()
.ok_or_else(|| SdpCodecError::MalformedRtpmap(value.to_string()))?;
let name = cursor.field(&['/']);
if name.is_empty() {
return Err(SdpCodecError::MalformedRtpmap(value.to_string()));
}
let name = name.to_string();
if !cursor.eat('/') {
return Err(SdpCodecError::MalformedRtpmap(value.to_string()));
}
let clock_rate = cursor
.number()
.ok_or_else(|| SdpCodecError::MalformedRtpmap(value.to_string()))?;
let channels = if cursor.eat('/') {
cursor.skip_ws();
if cursor
.remaining()
.is_empty()
{
None
} else {
Some(
cursor
.number()
.ok_or_else(|| SdpCodecError::MalformedRtpmap(value.to_string()))?,
)
}
} else {
None
};
if !cursor
.remaining()
.is_empty()
{
return Err(SdpCodecError::MalformedRtpmap(value.to_string()));
}
Ok((pt, name, clock_rate, channels))
}
pub(super) fn parse_fmtp_pt(value: &str) -> Result<(u8, String), SdpCodecError> {
let mut cursor = AttrCursor::new(value);
let pt = cursor
.number()
.ok_or_else(|| SdpCodecError::MalformedFmtp(value.to_string()))?;
Ok((
pt,
cursor
.remaining()
.to_string(),
))
}
pub(super) struct AttrTables {
pub(super) rtpmap: HashMap<u8, (String, u32, Option<u8>)>,
pub(super) fmtp: HashMap<u8, String>,
}
pub(super) fn attribute_tables(media: &sdp_types::Media) -> Result<AttrTables, SdpCodecError> {
let mut tables = AttrTables {
rtpmap: HashMap::new(),
fmtp: HashMap::new(),
};
for attr in &media.attributes {
match attr
.attribute
.as_str()
{
"rtpmap" => {
if let Some(val) = &attr.value {
let (pt, name, rate, channels) = parse_rtpmap(val)?;
tables
.rtpmap
.insert(pt, (name, rate, channels));
}
}
"fmtp" => {
if let Some(val) = &attr.value {
let (pt, params) = parse_fmtp_pt(val)?;
tables
.fmtp
.insert(pt, params);
}
}
_ => {}
}
}
Ok(tables)
}
pub(super) fn ptime_from_attrs(
attrs: &[sdp_types::Attribute],
name: &str,
warnings: &mut Vec<SdpWarning>,
) -> Option<u32> {
attrs
.iter()
.find(|a| a.attribute == name)
.and_then(|a| {
a.value
.as_deref()
})
.and_then(|v| parse_ptime_value(v, warnings, name))
}
pub(super) fn parse_ptime_value(
raw: &str,
warnings: &mut Vec<SdpWarning>,
attr: &str,
) -> Option<u32> {
let int_str = if let Some((prefix, _)) = raw.split_once('.') {
prefix
} else {
raw
};
match int_str.parse::<u32>() {
Ok(0) | Err(_) => {
warnings.push(SdpWarning::unparseable_numeric_attribute(attr, raw));
None
}
Ok(n) => Some(n),
}
}
pub(super) fn direction_from_attrs(attrs: &[sdp_types::Attribute]) -> Option<SdpDirection> {
attrs
.iter()
.find_map(|a| {
if a.value
.is_none()
{
a.attribute
.parse::<SdpDirection>()
.ok()
} else {
None
}
})
}
pub(super) fn default_ptime_ms(canonical_name: &str) -> u32 {
static_payload::default_ptime(canonical_name)
}
pub(super) fn fmtp_param<'a>(fmtp: &'a str, key: &str) -> Option<&'a str> {
fmtp.split(';')
.find_map(|part| {
let part = part.trim();
let (k, v) = part.split_once('=')?;
if k.trim()
.eq_ignore_ascii_case(key)
{
Some(v.trim())
} else {
None
}
})
}
#[cfg(test)]
mod tests {
use crate::sdp::{SdpCodec, SdpCodecEntry, SdpCodecs, SdpWarning};
fn sdp_header() -> String {
"v=0\r\no=- 0 0 IN IP4 192.0.2.1\r\ns=-\r\nt=0 0\r\n".to_string()
}
fn rtp_codec<'a>(entries: impl IntoIterator<Item = &'a SdpCodecEntry>) -> Vec<&'a SdpCodec> {
entries
.into_iter()
.filter_map(|e| {
if let SdpCodecEntry::Rtp(c) = e {
Some(c)
} else {
None
}
})
.collect()
}
fn codec_named<'a>(entries: &[&'a SdpCodec], name: &str) -> Option<&'a SdpCodec> {
entries
.iter()
.find(|c| {
c.name()
.eq_ignore_ascii_case(name)
})
.copied()
}
#[test]
fn g722_rtpmap_clock_rate_is_8000() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 9\r\na=rtpmap:9 G722/8000\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let rtp = rtp_codec(codecs.entries());
let g722 = codec_named(&rtp, "G722").expect("G722 must be present");
assert_eq!(g722.clock_rate(), 8000);
assert!(g722.has_rtpmap());
}
#[test]
fn opus_stereo_channels_2() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 111\r\na=rtpmap:111 opus/48000/2\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let rtp = rtp_codec(codecs.entries());
let opus = codec_named(&rtp, "opus").expect("opus must be present");
assert_eq!(opus.channels(), Some(2));
assert_eq!(opus.clock_rate(), 48000);
}
#[test]
fn media_ptime_overrides_session_ptime() {
let sdp = format!(
"{}a=ptime:10\r\nm=audio 5004 RTP/AVP 0\r\na=ptime:30\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let rtp = rtp_codec(codecs.entries());
let pcmu = codec_named(&rtp, "PCMU").expect("PCMU must be present");
assert_eq!(
pcmu.ptime(),
Some(30),
"media-level a=ptime must override session-level"
);
}
#[test]
fn session_ptime_applies_when_media_has_none() {
let sdp = format!("{}a=ptime:10\r\nm=audio 5004 RTP/AVP 8\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
let rtp = rtp_codec(codecs.entries());
let pcma = codec_named(&rtp, "PCMA").expect("PCMA must be present");
assert_eq!(
pcma.ptime(),
Some(10),
"session-level a=ptime must apply when media has none"
);
}
#[test]
fn ptime_fractional_value_is_truncated() {
let sdp = format!("{}m=audio 5004 RTP/AVP 0\r\na=ptime:20.0\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert!(codecs
.warnings()
.is_empty());
let rtp = rtp_codec(codecs.entries());
let pcmu = codec_named(&rtp, "PCMU").expect("PCMU must be present");
assert_eq!(pcmu.ptime(), Some(20));
}
#[test]
fn ptime_junk_is_unset_with_warning() {
let sdp = format!("{}m=audio 5004 RTP/AVP 0\r\na=ptime:abc\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs
.warnings()
.len(),
1,
"junk ptime must produce exactly one warning"
);
let rtp = rtp_codec(codecs.entries());
let pcmu = codec_named(&rtp, "PCMU").expect("PCMU must be present");
assert_eq!(pcmu.ptime(), Some(20));
}
#[test]
fn ptime_zero_is_unset_with_warning() {
let sdp = format!("{}m=audio 5004 RTP/AVP 0\r\na=ptime:0\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs
.warnings()
.len(),
1,
"ptime=0 must produce exactly one warning"
);
let rtp = rtp_codec(codecs.entries());
let pcmu = codec_named(&rtp, "PCMU").expect("PCMU must be present");
assert_eq!(pcmu.ptime(), Some(20));
}
#[test]
fn malformed_fmtp_non_numeric_pt_skips_section_with_warning() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 0\r\na=fmtp:x minptime=10\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs
.entries()
.count(),
0
);
assert_eq!(
codecs
.warnings()
.len(),
1
);
assert!(matches!(
codecs.warnings()[0],
SdpWarning::MalformedMediaSection { .. }
));
}
#[test]
fn g7221_malformed_bitrate_records_warning() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 100\r\n",
"a=rtpmap:100 G7221/16000\r\n",
"a=fmtp:100 bitrate=abc\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs
.warnings()
.len(),
1,
"malformed g7221 bitrate must produce exactly one warning"
);
let rtp = rtp_codec(codecs.entries());
let g7221 = codec_named(&rtp, "G7221").expect("G7221 must be present");
assert_eq!(
g7221.bitrate(),
None,
"bitrate must be None after parse error"
);
}
#[test]
fn g7221_valid_bitrate_is_set() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 100\r\n",
"a=rtpmap:100 G7221/16000\r\n",
"a=fmtp:100 bitrate=24000\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert!(codecs
.warnings()
.is_empty());
let rtp = rtp_codec(codecs.entries());
let g7221 = codec_named(&rtp, "G7221").expect("G7221 must be present");
assert_eq!(g7221.bitrate(), Some(24000));
}
#[test]
fn rtpmap_double_space_before_name_is_parsed() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 111\r\na=rtpmap:111 opus/48000/2\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let rtp = rtp_codec(codecs.entries());
let opus = codec_named(&rtp, "opus").expect("opus must be present, not \" opus\"");
assert_eq!(opus.clock_rate(), 48000);
assert_eq!(opus.channels(), Some(2));
}
#[test]
fn rtpmap_tab_separator_is_parsed() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 0\r\na=rtpmap:0\tPCMU/8000\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let rtp = rtp_codec(codecs.entries());
let pcmu = codec_named(&rtp, "PCMU").expect("PCMU must be present");
assert_eq!(pcmu.clock_rate(), 8000);
assert_eq!(
pcmu.channels(),
Some(1),
"no channel field normalizes to mono"
);
}
#[test]
fn rtpmap_interior_whitespace_in_name_is_malformed() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 0\r\na=rtpmap:0 foo bar/8000\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs
.entries()
.count(),
0,
"malformed rtpmap must still drop the whole section"
);
assert!(matches!(
codecs.warnings()[0],
SdpWarning::MalformedMediaSection { .. }
));
}
}