use std::collections::HashMap;
use crate::sdp::error::{SdpCodecError, SdpWarning};
use crate::sdp::num::atoi_prefix;
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: TryFrom<u32>>(&mut self) -> Option<T> {
self.skip_ws();
let (value, rest) = atoi_prefix(self.rest);
let value = value?;
self.rest = rest;
self.skip_ws();
T::try_from(value).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<u32>), 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<u32>)>,
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 = raw
.split_once('.')
.map_or(raw, |(prefix, _)| prefix);
match atoi_prefix(int_str) {
(Some(n), "") if n != 0 => Some(n),
_ => {
warnings.push(SdpWarning::unparseable_numeric_attribute(attr, raw));
None
}
}
}
const DIRECTION_ATTRIBUTES: [&str; 4] = ["sendrecv", "sendonly", "recvonly", "inactive"];
pub(super) fn direction_from_attrs(
attrs: &[sdp_types::Attribute],
warnings: &mut Vec<SdpWarning>,
) -> Option<SdpDirection> {
for attr in attrs
.iter()
.filter(|a| {
a.value
.is_none()
})
{
if !DIRECTION_ATTRIBUTES
.iter()
.any(|d| d.eq_ignore_ascii_case(&attr.attribute))
{
continue;
}
match attr
.attribute
.parse::<SdpDirection>()
{
Ok(direction) => return Some(direction),
Err(_) => warnings.push(SdpWarning::non_canonical_direction_attribute(
&attr.attribute,
)),
}
}
None
}
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 super::super::testutil::{codec_named, rtp_codec, sdp_header};
use crate::sdp::{SdpCodecs, SdpWarning};
#[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 canonical_direction_attribute_is_read() {
let sdp = format!("{}m=audio 5004 RTP/AVP 0\r\na=recvonly\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert!(codecs
.warnings()
.is_empty());
assert_eq!(
codecs.sections()[0].direction(),
crate::sdp::SdpDirection::RecvOnly
);
}
#[test]
fn non_canonical_direction_attribute_warns_instead_of_inheriting_silently() {
let sdp = format!(
"{}a=sendrecv\r\nm=audio 5004 RTP/AVP 0\r\na=SENDONLY\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs.sections()[0].direction(),
crate::sdp::SdpDirection::SendRecv
);
assert!(matches!(
codecs.warnings()[0],
SdpWarning::NonCanonicalDirectionAttribute { .. }
));
}
#[test]
fn an_unrelated_value_less_attribute_is_not_a_direction() {
let sdp = format!("{}m=audio 5004 RTP/AVP 0\r\na=rtcp-mux\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert!(codecs
.warnings()
.is_empty());
}
#[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 { .. }
));
}
}