mod attrs;
mod media_section;
#[cfg(test)]
mod testutil;
use attrs::{direction_from_attrs, ptime_from_attrs};
use media_section::{is_image, parse_media_section, proto_has_rtp, SessionDefaults};
use crate::sdp::{
codec::{NonCodecPayload, SdpCodec, SdpDirection, SdpMediaType},
codec_string::{CodecString, CodecStringEntry},
error::{CodecStringError, SdpCodecError, SdpWarning, UnmappedPayload},
options::CodecStringOptions,
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SdpCodecEntry {
Rtp(SdpCodec),
T38,
}
#[derive(Debug, Clone)]
pub struct SdpMediaSection {
media_type: SdpMediaType,
port: u16,
proto: String,
formats: String,
direction: SdpDirection,
entries: Vec<SdpCodecEntry>,
unmapped: Vec<UnmappedPayload>,
non_codec: Vec<NonCodecPayload>,
}
impl SdpMediaSection {
pub fn media_type(&self) -> &SdpMediaType {
&self.media_type
}
pub fn port(&self) -> u16 {
self.port
}
pub fn proto(&self) -> &str {
&self.proto
}
pub fn formats(&self) -> &str {
&self.formats
}
pub fn direction(&self) -> SdpDirection {
self.direction
}
pub fn entries(&self) -> &[SdpCodecEntry] {
&self.entries
}
pub fn unmapped(&self) -> &[UnmappedPayload] {
&self.unmapped
}
pub fn non_codec_payloads(&self) -> &[NonCodecPayload] {
&self.non_codec
}
pub fn is_negotiable(&self) -> bool {
let derives_entries = matches!(self.media_type, SdpMediaType::Audio | SdpMediaType::Video)
|| is_image(&self.media_type);
self.port != 0 && derives_entries
}
}
#[derive(Debug, Clone)]
pub struct SdpCodecs {
sections: Vec<SdpMediaSection>,
warnings: Vec<SdpWarning>,
}
impl SdpCodecs {
pub fn parse(sdp: &str) -> Result<Self, SdpCodecError> {
Self::parse_bytes(sdp.as_bytes())
}
pub fn parse_bytes(bytes: &[u8]) -> Result<Self, SdpCodecError> {
let session = sdp_types::Session::parse(bytes)
.map_err(|e| SdpCodecError::parse_failure("failed to parse SDP", e))?;
Self::from_session(&session)
}
fn from_session(session: &sdp_types::Session) -> Result<Self, SdpCodecError> {
let mut result = Self {
sections: Vec::new(),
warnings: Vec::new(),
};
let session_ptime = ptime_from_attrs(&session.attributes, "ptime", &mut result.warnings);
let session_maxptime =
ptime_from_attrs(&session.attributes, "maxptime", &mut result.warnings);
let session_direction = direction_from_attrs(&session.attributes, &mut result.warnings)
.unwrap_or(SdpDirection::SendRecv);
for media in &session.medias {
let media_type: SdpMediaType = match media
.media
.parse()
{
Ok(t) => t,
Err(e) => match e {},
};
let mut section = SdpMediaSection {
media_type: media_type.clone(),
port: media.port,
proto: media
.proto
.clone(),
formats: media
.fmt
.clone(),
direction: direction_from_attrs(&media.attributes, &mut result.warnings)
.unwrap_or(session_direction),
entries: Vec::new(),
unmapped: Vec::new(),
non_codec: Vec::new(),
};
if is_image(&media_type) {
section
.entries
.push(SdpCodecEntry::T38);
} else if proto_has_rtp(&media.proto) {
let defaults = SessionDefaults {
ptime: session_ptime,
maxptime: session_maxptime,
direction: section.direction,
};
match parse_media_section(media, media_type.clone(), &defaults) {
Ok(parsed) => {
section.entries = parsed.entries;
section.unmapped = parsed.unmapped;
section.non_codec = parsed.non_codec;
result
.warnings
.extend(parsed.warnings);
}
Err(e) => {
result
.warnings
.push(SdpWarning::malformed_media_section(
media_type.to_string(),
e.to_string(),
));
}
}
}
result
.sections
.push(section);
}
Ok(result)
}
pub fn sections(&self) -> &[SdpMediaSection] {
&self.sections
}
fn negotiable(&self) -> impl Iterator<Item = &SdpMediaSection> {
self.sections
.iter()
.filter(|s| s.is_negotiable())
}
pub fn entries(&self) -> impl Iterator<Item = &SdpCodecEntry> {
self.negotiable()
.flat_map(|s| {
s.entries
.iter()
})
}
pub fn len(&self) -> usize {
self.entries()
.count()
}
pub fn is_empty(&self) -> bool {
self.entries()
.next()
.is_none()
}
pub fn audio(&self) -> impl Iterator<Item = &SdpCodec> {
self.rtp_of(SdpMediaType::Audio)
}
pub fn video(&self) -> impl Iterator<Item = &SdpCodec> {
self.rtp_of(SdpMediaType::Video)
}
fn rtp_of(&self, media: SdpMediaType) -> impl Iterator<Item = &SdpCodec> {
self.entries()
.filter_map(move |e| match e {
SdpCodecEntry::Rtp(c) if c.media() == &media => Some(c),
_ => None,
})
}
pub fn unmapped(&self) -> impl Iterator<Item = &UnmappedPayload> {
self.negotiable()
.flat_map(|s| {
s.unmapped
.iter()
})
}
pub fn warnings(&self) -> &[SdpWarning] {
&self.warnings
}
pub fn non_codec_payloads(&self) -> impl Iterator<Item = &NonCodecPayload> {
self.negotiable()
.flat_map(|s| {
s.non_codec
.iter()
})
}
pub fn audio_codec_string(
&self,
options: &CodecStringOptions,
mut warnings: Option<&mut Vec<SdpWarning>>,
) -> Result<CodecString, CodecStringError> {
let mut out = CodecString::new();
for entry in self.entries() {
match entry {
SdpCodecEntry::T38 => out.push(CodecStringEntry::new("t38")?)?,
SdpCodecEntry::Rtp(codec) if codec.media() == &SdpMediaType::Audio => {
if let Some(entry) =
codec_to_entry_lenient(codec, options, warnings.as_deref_mut())?
{
out.push(entry)?;
}
}
SdpCodecEntry::Rtp(_) => {}
}
}
Ok(out)
}
pub fn video_codec_string(
&self,
options: &CodecStringOptions,
mut warnings: Option<&mut Vec<SdpWarning>>,
) -> Result<CodecString, CodecStringError> {
let mut out = CodecString::new();
for codec in self.video() {
if let Some(entry) = codec_to_entry_lenient(codec, options, warnings.as_deref_mut())? {
out.push(entry)?;
}
}
Ok(out)
}
pub fn fmtp_for(&self, name: &str, clock_rate: u32) -> Option<&str> {
self.audio()
.filter(|c| {
c.name()
.eq_ignore_ascii_case(name)
&& c.clock_rate() == clock_rate
})
.find_map(|c| c.fmtp())
}
}
fn unrepresentable_reason(e: &CodecStringError) -> &'static str {
match e {
CodecStringError::AtInFmtp(_) => "contains '@', which the qualifier split takes first",
CodecStringError::DotInFmtpWithoutModule(_) => {
"contains '.' with no module prefix, which the modname split takes first"
}
CodecStringError::WireInjection { .. } => "contains a newline or a carriage return",
CodecStringError::InvalidCharInName { .. } => "contains a codec-string grammar delimiter",
CodecStringError::InvalidCodecName(_) => "is empty",
_ => "has no representation in the codec-string grammar",
}
}
fn codec_to_entry_lenient(
codec: &SdpCodec,
options: &CodecStringOptions,
mut warnings: Option<&mut Vec<SdpWarning>>,
) -> Result<Option<CodecStringEntry>, CodecStringError> {
let mut entry = match CodecStringEntry::new(codec.name()) {
Ok(e) => e,
Err(e) => {
return match warnings.as_deref_mut() {
None => Err(e),
Some(acc) => {
acc.push(SdpWarning::codec_name_unrepresentable(
codec.name(),
unrepresentable_reason(&e),
));
Ok(None)
}
};
}
};
if options.emits_fmtp() {
if let Some(fmtp) = codec.fmtp() {
match entry
.clone()
.with_fmtp(fmtp)
{
Ok(with_fmtp) => entry = with_fmtp,
Err(e) => match warnings {
None => return Err(e),
Some(acc) => {
acc.push(SdpWarning::fmtp_unrepresentable(
codec.name(),
fmtp,
unrepresentable_reason(&e),
));
}
},
}
}
}
Ok(Some(options.apply_qualifiers(entry, codec)))
}
#[cfg(test)]
mod tests {
use super::testutil::{codec_named, rtp_codec, sdp_header};
use super::*;
use crate::sdp::NonCodecKind;
#[test]
fn static_only_pcmu_pcma_g729_no_rtpmap() {
let sdp = format!("{}m=audio 5004 RTP/AVP 0 8 18\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
let entries = codecs.entries();
let rtp = rtp_codec(entries);
assert_eq!(rtp.len(), 3);
assert!(codec_named(&rtp, "PCMU").is_some());
assert!(codec_named(&rtp, "PCMA").is_some());
assert!(codec_named(&rtp, "G729").is_some());
for c in &rtp {
assert!(!c.has_rtpmap(), "static types must have has_rtpmap=false");
}
}
#[test]
fn amrwb_no_channel_field_equals_explicit_one() {
let sdp_no_ch = format!(
"{}m=audio 5004 RTP/AVP 100\r\na=rtpmap:100 AMR-WB/16000\r\n",
sdp_header()
);
let sdp_ch1 = format!(
"{}m=audio 5004 RTP/AVP 100\r\na=rtpmap:100 AMR-WB/16000/1\r\n",
sdp_header()
);
let c_no_ch = SdpCodecs::parse(&sdp_no_ch).unwrap();
let c_ch1 = SdpCodecs::parse(&sdp_ch1).unwrap();
let rtp_no_ch = rtp_codec(c_no_ch.entries());
let rtp_ch1 = rtp_codec(c_ch1.entries());
let amrwb_no_ch = codec_named(&rtp_no_ch, "AMR-WB").expect("AMR-WB must be present");
let amrwb_ch1 = codec_named(&rtp_ch1, "AMR-WB").expect("AMR-WB must be present");
assert_eq!(
amrwb_no_ch.channels(),
amrwb_ch1.channels(),
"missing /1 and explicit /1 must normalize to the same channel count"
);
assert_eq!(amrwb_ch1.channels(), Some(1));
}
#[test]
fn a_channel_count_above_a_byte_parses() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 100\r\na=rtpmap:100 L16/48000/300\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).expect("a legal channel count must not fail the parse");
let rtp = rtp_codec(codecs.entries());
let l16 = codec_named(&rtp, "L16").expect("L16 must be present");
assert_eq!(l16.channels(), Some(300));
}
#[test]
fn ptime_opus_fmtp_overrides_session_ptime() {
let sdp = format!(
"{}a=ptime:20\r\nm=audio 5004 RTP/AVP 111\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 ptime=40\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.ptime(), Some(40), "fmtp ptime= must override a=ptime");
}
#[test]
fn ptime_ilbc_no_fmtp_overrides_explicit_ptime() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 97\r\na=rtpmap:97 iLBC/8000\r\na=ptime:20\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let rtp = rtp_codec(codecs.entries());
let ilbc = codec_named(&rtp, "iLBC").expect("iLBC must be present");
assert_eq!(
ilbc.ptime(),
Some(30),
"iLBC without fmtp must always yield ptime=30"
);
}
#[test]
fn ptime_ilbc_fmtp_mode20_yields_20_no_bitrate() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 97\r\na=rtpmap:97 iLBC/8000\r\na=fmtp:97 mode=20\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let rtp = rtp_codec(codecs.entries());
let ilbc = codec_named(&rtp, "iLBC").expect("iLBC must be present");
assert_eq!(ilbc.ptime(), Some(20));
assert_eq!(
ilbc.bitrate(),
None,
"fmtp present — bitrate not set by static table"
);
}
#[test]
fn sections_keep_m_line_order_across_negotiable_and_not() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 0\r\n",
"m=video 0 RTP/AVP 99\r\n",
"a=rtpmap:99 H264/90000\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let sections = codecs.sections();
assert_eq!(sections[0].media_type(), &SdpMediaType::Audio);
assert!(sections[0].is_negotiable());
assert_eq!(sections[1].media_type(), &SdpMediaType::Video);
assert!(!sections[1].is_negotiable());
let rtp = rtp_codec(codecs.entries());
assert_eq!(rtp.len(), 1, "the declined video must not reach entries()");
assert!(codec_named(&rtp, "PCMU").is_some());
}
#[test]
fn ipv6_connection_parses_without_error() {
let sdp = format!(
"{}c=IN IP6 2001:db8::1\r\nm=audio 5004 RTP/AVP 0\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(codecs.len(), 1);
}
#[test]
fn application_m_line_does_not_fail() {
let sdp = format!(
"{}m=application 9 UDP/BFCP *\r\nm=audio 5004 RTP/AVP 0\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(codecs.len(), 1);
let rtp = rtp_codec(codecs.entries());
assert!(codec_named(&rtp, "PCMU").is_some());
}
#[test]
fn telephone_event_retains_payload_type_rate_and_fmtp() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 0 101 102\r\n",
"a=rtpmap:101 telephone-event/8000\r\n",
"a=fmtp:101 0-16\r\n",
"a=rtpmap:102 telephone-event/16000\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let te: Vec<_> = codecs
.non_codec_payloads()
.collect();
assert_eq!(
te.len(),
2,
"both telephone-event payloads must be retained"
);
assert_eq!(te[0].kind, NonCodecKind::TelephoneEvent);
assert_eq!(te[0].payload_type, 101);
assert_eq!(te[0].clock_rate, 8000);
assert_eq!(
te[0]
.fmtp
.as_deref(),
Some("0-16"),
"the offered digit range is what an operator reads on a DTMF fault"
);
assert!(te[0].has_rtpmap);
assert_eq!(te[0].media_type, SdpMediaType::Audio);
assert_eq!(te[1].payload_type, 102);
assert_eq!(te[1].clock_rate, 16000);
assert!(te[1]
.fmtp
.is_none());
let rtp = rtp_codec(codecs.entries());
assert!(codec_named(&rtp, "telephone-event").is_none());
assert_eq!(
codecs
.unmapped()
.count(),
0
);
}
#[test]
fn comfort_noise_from_static_table_has_no_rtpmap() {
let sdp = format!("{}m=audio 5004 RTP/AVP 0 13\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
let cn: Vec<_> = codecs
.non_codec_payloads()
.collect();
assert_eq!(cn.len(), 1);
assert_eq!(cn[0].kind, NonCodecKind::ComfortNoise);
assert_eq!(cn[0].payload_type, 13);
assert_eq!(cn[0].clock_rate, 8000);
assert!(cn[0]
.fmtp
.is_none());
assert!(
!cn[0].has_rtpmap,
"PT 13 resolved from the RFC 3551 table, not declared by the peer"
);
let rtp = rtp_codec(codecs.entries());
assert!(codec_named(&rtp, "CN").is_none());
assert_eq!(
codecs
.unmapped()
.count(),
0
);
}
#[test]
fn comfort_noise_with_explicit_rtpmap_is_distinguishable() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 0 13\r\n",
"a=rtpmap:13 CN/8000\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let cn: Vec<_> = codecs
.non_codec_payloads()
.collect();
assert_eq!(cn.len(), 1);
assert!(cn[0].has_rtpmap);
}
#[test]
fn non_codec_payloads_are_an_inventory_not_a_set() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 0 101\r\n",
"a=rtpmap:101 telephone-event/8000\r\n",
"m=audio 5006 RTP/AVP 8 96\r\n",
"a=rtpmap:96 telephone-event/8000\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let te: Vec<_> = codecs
.non_codec_payloads()
.collect();
assert_eq!(te.len(), 2);
assert_eq!(te[0].payload_type, 101);
assert_eq!(te[1].payload_type, 96);
}
#[test]
fn non_codec_payloads_never_reach_the_codec_string() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 0 13 101\r\n",
"a=rtpmap:101 telephone-event/8000\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs
.non_codec_payloads()
.count(),
2
);
let cs = codecs
.audio_codec_string(&CodecStringOptions::default(), None)
.unwrap();
assert_eq!(cs.len(), 1, "only PCMU may reach the codec string");
let rendered = cs.to_string();
assert!(!rendered.contains("telephone-event"));
assert!(!rendered.contains("CN"));
}
#[test]
fn dynamic_pt_without_rtpmap_is_unmapped_not_error() {
let sdp = format!("{}m=audio 5004 RTP/AVP 0 99\r\n", sdp_header());
let codecs = SdpCodecs::parse(&sdp).unwrap();
let unmapped: Vec<_> = codecs
.unmapped()
.collect();
assert_eq!(unmapped.len(), 1);
assert_eq!(unmapped[0].payload_type, 99);
let rtp = rtp_codec(codecs.entries());
assert_eq!(rtp.len(), 1);
assert!(codec_named(&rtp, "PCMU").is_some());
}
#[test]
fn amr_oa_be_pair_collapses_after_dedup() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 96 97\r\n",
"a=rtpmap:96 AMR/8000\r\n",
"a=fmtp:96 octet-align=1\r\n",
"a=rtpmap:97 AMR/8000\r\n",
"a=fmtp:97 octet-align=0\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let cs = codecs
.audio_codec_string(&CodecStringOptions::default(), None)
.unwrap();
assert_eq!(cs.len(), 2, "both AMR entries must be emitted");
assert_eq!(
cs.entries()[0],
cs.entries()[1],
"entries must be identical under default options (no fmtp)"
);
let mut deduped = cs.clone();
deduped.dedup();
assert_eq!(
deduped.len(),
1,
"duplicate AMR entries must collapse under dedup()"
);
}
#[test]
fn fmtp_for_falls_through_to_later_codec_with_fmtp() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 97 96\r\n",
"a=rtpmap:97 AMR/8000\r\n",
"a=rtpmap:96 AMR/8000\r\n",
"a=fmtp:96 octet-align=1\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs.fmtp_for("AMR", 8000),
Some("octet-align=1"),
"fmtp_for must fall through a fmtp-less match to a later one that has fmtp"
);
}
#[test]
fn amrwb_absent_and_explicit_channel_collapse_after_dedup() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 100 101\r\n",
"a=rtpmap:100 AMR-WB/16000\r\n",
"a=rtpmap:101 AMR-WB/16000/1\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let cs = codecs
.audio_codec_string(&CodecStringOptions::default(), None)
.unwrap();
assert_eq!(cs.len(), 2);
let mut deduped = cs.clone();
deduped.dedup();
assert_eq!(
deduped.len(),
1,
"absent and explicit /1 channel count must normalize identically"
);
}
#[test]
fn evs_dotted_fmtp_unreachable_under_default_options() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 110\r\n",
"a=rtpmap:110 EVS/16000\r\n",
"a=fmtp:110 br=13.2-24.4;bw=nb-swb\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let cs = codecs
.audio_codec_string(&CodecStringOptions::default(), None)
.unwrap();
assert_eq!(cs.len(), 1);
assert!(
cs.entries()[0]
.fmtp()
.is_none(),
"fmtp emission is off by default; the dotted fmtp path is unreachable"
);
}
#[test]
fn evs_dotted_fmtp_lenient_clears_and_warns() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 110\r\n",
"a=rtpmap:110 EVS/16000\r\n",
"a=fmtp:110 br=13.2-24.4;bw=nb-swb\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let opts = CodecStringOptions::default().with_fmtp(true);
let mut warnings = Vec::new();
let cs = codecs
.audio_codec_string(&opts, Some(&mut warnings))
.unwrap();
assert_eq!(cs.len(), 1, "the codec must still be emitted");
assert!(
cs.entries()[0]
.fmtp()
.is_none(),
"unrepresentable fmtp must be cleared, not left dangling"
);
assert_eq!(warnings.len(), 1);
assert!(matches!(
warnings[0],
SdpWarning::FmtpUnrepresentable { .. }
));
}
#[test]
fn evs_dotted_fmtp_strict_is_err() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 110\r\n",
"a=rtpmap:110 EVS/16000\r\n",
"a=fmtp:110 br=13.2-24.4;bw=nb-swb\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let opts = CodecStringOptions::default().with_fmtp(true);
let result = codecs.audio_codec_string(&opts, None);
assert!(
result.is_err(),
"strict mode must fail on an unrepresentable fmtp"
);
}
#[test]
fn g722_rate_is_8000_never_16000() {
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 cs = codecs
.audio_codec_string(&CodecStringOptions::default(), None)
.unwrap();
assert_eq!(cs.len(), 1);
assert_eq!(cs.entries()[0].name(), "G722");
assert_eq!(cs.entries()[0].rate(), Some(8000));
assert_eq!(cs.entries()[0].ptime(), Some(20));
}
#[test]
fn video_codec_string_carries_no_rate_qualifier() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 0\r\n",
"m=video 5006 RTP/AVP 96\r\n",
"a=rtpmap:96 H264/90000\r\n",
"m=image 5008 udptl t38\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let video_cs = codecs
.video_codec_string(&CodecStringOptions::video(), None)
.unwrap();
assert_eq!(video_cs.len(), 1);
assert_eq!(video_cs.entries()[0].name(), "H264");
assert!(
video_cs.entries()[0]
.rate()
.is_none(),
"CodecStringOptions::video() must not emit a rate qualifier"
);
let audio_cs = codecs
.audio_codec_string(&CodecStringOptions::default(), None)
.unwrap();
assert!(
audio_cs
.entries()
.iter()
.any(|e| e.name() == "t38"),
"an offered m=image section must append a literal t38 entry to the audio string"
);
}
#[test]
fn codec_name_with_delimiter_is_skipped_lenient_and_erred_strict() {
let sdp = format!(
"{}m=audio 5004 RTP/AVP 100\r\na=rtpmap:100 bad,name/8000\r\n",
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
let mut warnings = Vec::new();
let cs = codecs
.audio_codec_string(&CodecStringOptions::default(), Some(&mut warnings))
.unwrap();
assert!(cs.is_empty(), "the unrepresentable codec must be skipped");
assert_eq!(warnings.len(), 1);
assert!(matches!(
warnings[0],
SdpWarning::CodecNameUnrepresentable { .. }
));
let result = codecs.audio_codec_string(&CodecStringOptions::default(), None);
assert!(result.is_err(), "strict mode must fail");
}
#[test]
fn fmtp_for_looks_up_by_name_and_rate_case_insensitively() {
let sdp = format!(
concat!(
"{}",
"m=audio 5004 RTP/AVP 100\r\n",
"a=rtpmap:100 AMR-WB/16000\r\n",
"a=fmtp:100 octet-align=1\r\n"
),
sdp_header()
);
let codecs = SdpCodecs::parse(&sdp).unwrap();
assert_eq!(
codecs.fmtp_for("amr-wb", 16000),
Some("octet-align=1"),
"lookup must be case-insensitive on the codec name"
);
assert_eq!(
codecs.fmtp_for("AMR-WB", 8000),
None,
"clock rate must also match"
);
assert_eq!(
codecs.fmtp_for("PCMU", 8000),
None,
"codec not in the offer"
);
}
}