use crate::picture::SourceFormat;
pub const ENCODING_NAME: &str = "H261";
pub const CLOCK_RATE: u32 = 90_000;
pub const MEDIA_NAME: &str = "video";
pub const FULL_RATE_NUM: u32 = 2997;
pub const FULL_RATE_DEN: u32 = 100;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SdpError {
MpiOutOfRange {
param: &'static str,
value: u32,
},
BadAnnexD {
value: String,
},
NotAnInteger {
param: String,
value: String,
},
MalformedToken {
token: String,
},
DuplicateParam {
param: String,
},
NoPictureSize,
}
impl core::fmt::Display for SdpError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SdpError::MpiOutOfRange { param, value } => {
write!(f, "h261 sdp: {param} MPI {value} outside 1..=4")
}
SdpError::BadAnnexD { value } => {
write!(f, "h261 sdp: D (Annex D) value {value:?} is not 0 or 1")
}
SdpError::NotAnInteger { param, value } => {
write!(f, "h261 sdp: {param} value {value:?} is not an integer")
}
SdpError::MalformedToken { token } => {
write!(f, "h261 sdp: fmtp token {token:?} is missing '='")
}
SdpError::DuplicateParam { param } => {
write!(f, "h261 sdp: parameter {param} appears more than once")
}
SdpError::NoPictureSize => {
write!(f, "h261 sdp: no CIF or QCIF picture size specified")
}
}
}
}
impl std::error::Error for SdpError {}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct H261FmtpParams {
pub cif: Option<u8>,
pub qcif: Option<u8>,
pub d: Option<bool>,
}
impl H261FmtpParams {
pub fn rfc2032_fallback() -> Self {
H261FmtpParams {
cif: None,
qcif: Some(1),
d: None,
}
}
pub fn supports(&self, fmt: SourceFormat) -> bool {
match fmt {
SourceFormat::Cif => self.cif.is_some(),
SourceFormat::Qcif => self.qcif.is_some(),
}
}
pub fn mpi(&self, fmt: SourceFormat) -> Option<u8> {
match fmt {
SourceFormat::Cif => self.cif,
SourceFormat::Qcif => self.qcif,
}
}
pub fn max_frame_rate(&self, fmt: SourceFormat) -> Option<(u32, u32)> {
self.mpi(fmt)
.map(|mpi| (FULL_RATE_NUM, FULL_RATE_DEN * u32::from(mpi)))
}
pub fn validate(&self) -> Result<(), SdpError> {
if self.cif.is_none() && self.qcif.is_none() {
return Err(SdpError::NoPictureSize);
}
Ok(())
}
pub fn preferred_picture_size(&self) -> Option<SourceFormat> {
if self.cif.is_some() {
Some(SourceFormat::Cif)
} else if self.qcif.is_some() {
Some(SourceFormat::Qcif)
} else {
None
}
}
pub fn format_value(&self) -> String {
self.format_value_preferred(SourceFormat::Cif)
}
pub fn format_value_preferred(&self, preferred: SourceFormat) -> String {
let cif = self.cif.map(|mpi| format!("CIF={mpi}"));
let qcif = self.qcif.map(|mpi| format!("QCIF={mpi}"));
let (first, second) = match preferred {
SourceFormat::Qcif => (qcif, cif),
SourceFormat::Cif => (cif, qcif),
};
let mut parts: Vec<String> = Vec::new();
parts.extend(first);
parts.extend(second);
if let Some(d) = self.d {
parts.push(format!("D={}", u8::from(d)));
}
parts.join(";")
}
pub fn parse_preference_order(s: &str) -> Vec<SourceFormat> {
let mut order: Vec<SourceFormat> = Vec::new();
let mut seen_cif = false;
let mut seen_qcif = false;
for raw in s.split(';') {
let token = raw.trim();
if token.is_empty() {
continue;
}
let Some((key, _value)) = token.split_once('=') else {
continue;
};
match key.trim().to_ascii_uppercase().as_str() {
"CIF" if !seen_cif => {
seen_cif = true;
order.push(SourceFormat::Cif);
}
"QCIF" if !seen_qcif => {
seen_qcif = true;
order.push(SourceFormat::Qcif);
}
_ => {}
}
}
order
}
pub fn parse_value(s: &str) -> Result<Self, SdpError> {
let mut out = H261FmtpParams::default();
let mut seen_cif = false;
let mut seen_qcif = false;
for raw in s.split(';') {
let token = raw.trim();
if token.is_empty() {
continue;
}
let (key, value) = token
.split_once('=')
.ok_or_else(|| SdpError::MalformedToken {
token: token.to_string(),
})?;
let key = key.trim();
let value = value.trim();
let key_upper = key.to_ascii_uppercase();
match key_upper.as_str() {
"CIF" => {
if seen_cif {
return Err(SdpError::DuplicateParam {
param: "CIF".to_string(),
});
}
seen_cif = true;
out.cif = Some(parse_mpi("CIF", value)?);
}
"QCIF" => {
if seen_qcif {
return Err(SdpError::DuplicateParam {
param: "QCIF".to_string(),
});
}
seen_qcif = true;
out.qcif = Some(parse_mpi("QCIF", value)?);
}
"D" => {
out.d = Some(parse_annex_d(value)?);
}
_ => {}
}
}
Ok(out)
}
}
pub fn negotiate_answer(
offer: &H261FmtpParams,
our_capability: &H261FmtpParams,
) -> Result<H261FmtpParams, SdpError> {
let effective_offer = if offer.cif.is_none() && offer.qcif.is_none() {
H261FmtpParams::rfc2032_fallback()
} else {
*offer
};
let cif = match (effective_offer.cif, our_capability.cif) {
(Some(a), Some(b)) => Some(a.max(b)),
_ => None,
};
let qcif = match (effective_offer.qcif, our_capability.qcif) {
(Some(a), Some(b)) => Some(a.max(b)),
_ => None,
};
if cif.is_none() && qcif.is_none() {
return Err(SdpError::NoPictureSize);
}
let d = match (effective_offer.d, our_capability.d) {
(Some(true), Some(true)) => Some(true),
_ => None,
};
Ok(H261FmtpParams { cif, qcif, d })
}
fn parse_mpi(param: &'static str, value: &str) -> Result<u8, SdpError> {
let n: u32 = value.parse().map_err(|_| SdpError::NotAnInteger {
param: param.to_string(),
value: value.to_string(),
})?;
if !(1..=4).contains(&n) {
return Err(SdpError::MpiOutOfRange { param, value: n });
}
Ok(n as u8)
}
fn parse_annex_d(value: &str) -> Result<bool, SdpError> {
match value {
"1" => Ok(true),
"0" => Ok(false),
other => Err(SdpError::BadAnnexD {
value: other.to_string(),
}),
}
}
pub fn format_rtpmap(payload_type: u8) -> String {
format!("a=rtpmap:{payload_type} {ENCODING_NAME}/{CLOCK_RATE}")
}
pub fn format_fmtp(payload_type: u8, params: &H261FmtpParams) -> Option<String> {
let value = params.format_value();
if value.is_empty() {
None
} else {
Some(format!("a=fmtp:{payload_type} {value}"))
}
}
pub fn format_fmtp_preferred(
payload_type: u8,
params: &H261FmtpParams,
preferred: SourceFormat,
) -> Option<String> {
let value = params.format_value_preferred(preferred);
if value.is_empty() {
None
} else {
Some(format!("a=fmtp:{payload_type} {value}"))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RtpMap {
pub payload_type: u8,
pub clock_rate: u32,
}
impl RtpMap {
pub fn is_rfc4587_compliant(self) -> bool {
self.clock_rate == CLOCK_RATE
}
}
pub fn parse_rtpmap(line: &str) -> Option<RtpMap> {
let line = line.trim();
let body = line.strip_prefix("a=").unwrap_or(line);
let rest = body.strip_prefix("rtpmap:")?;
let (pt_str, enc_clock) = rest.split_once(char::is_whitespace)?;
let payload_type: u8 = pt_str.trim().parse().ok()?;
let enc_clock = enc_clock.trim();
let mut fields = enc_clock.split('/');
let encoding = fields.next()?.trim();
if !encoding.eq_ignore_ascii_case(ENCODING_NAME) {
return None;
}
let clock_rate: u32 = fields.next()?.trim().parse().ok()?;
Some(RtpMap {
payload_type,
clock_rate,
})
}
pub fn parse_rtpmap_strict(line: &str) -> Option<RtpMap> {
let m = parse_rtpmap(line)?;
if m.is_rfc4587_compliant() {
Some(m)
} else {
None
}
}
pub fn parse_fmtp(line: &str, payload_type: u8) -> Option<Result<H261FmtpParams, SdpError>> {
let line = line.trim();
let body = line.strip_prefix("a=").unwrap_or(line);
let rest = body.strip_prefix("fmtp:")?;
let (pt_str, value) = rest.split_once(char::is_whitespace)?;
let pt: u8 = pt_str.trim().parse().ok()?;
if pt != payload_type {
return None;
}
Some(H261FmtpParams::parse_value(value.trim()))
}
pub fn parse_fmtp_strict(line: &str, payload_type: u8) -> Option<Result<H261FmtpParams, SdpError>> {
match parse_fmtp(line, payload_type)? {
Ok(params) => match params.validate() {
Ok(()) => Some(Ok(params)),
Err(_) => None,
},
Err(e) => Some(Err(e)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rtpmap_constants_match_spec() {
assert_eq!(ENCODING_NAME, "H261");
assert_eq!(CLOCK_RATE, 90_000);
assert_eq!(MEDIA_NAME, "video");
}
#[test]
fn format_rtpmap_matches_spec_example() {
assert_eq!(format_rtpmap(31), "a=rtpmap:31 H261/90000");
assert_eq!(format_rtpmap(96), "a=rtpmap:96 H261/90000");
}
#[test]
fn parse_rtpmap_accepts_spec_example() {
let m = parse_rtpmap("a=rtpmap:31 H261/90000").unwrap();
assert_eq!(m.payload_type, 31);
assert_eq!(m.clock_rate, 90_000);
}
#[test]
fn parse_rtpmap_without_a_prefix() {
let m = parse_rtpmap("rtpmap:96 H261/90000").unwrap();
assert_eq!(m.payload_type, 96);
assert_eq!(m.clock_rate, 90_000);
}
#[test]
fn parse_rtpmap_is_case_insensitive_on_encoding() {
assert!(parse_rtpmap("a=rtpmap:31 h261/90000").is_some());
assert!(parse_rtpmap("a=rtpmap:31 H261/90000").is_some());
}
#[test]
fn parse_rtpmap_rejects_other_codecs() {
assert!(parse_rtpmap("a=rtpmap:34 H263/90000").is_none());
assert!(parse_rtpmap("a=rtpmap:0 PCMU/8000").is_none());
}
#[test]
fn parse_rtpmap_rejects_non_rtpmap_line() {
assert!(parse_rtpmap("a=fmtp:31 CIF=2").is_none());
assert!(parse_rtpmap("m=video 49170 RTP/AVP 31").is_none());
}
#[test]
fn parse_rtpmap_tolerates_trailing_channel_field() {
let m = parse_rtpmap("a=rtpmap:31 H261/90000/1").unwrap();
assert_eq!(m.clock_rate, 90_000);
}
#[test]
fn rtpmap_is_rfc4587_compliant_only_at_90000() {
let ok = parse_rtpmap("a=rtpmap:31 H261/90000").unwrap();
assert!(ok.is_rfc4587_compliant());
let too_low = parse_rtpmap("a=rtpmap:31 H261/8000").unwrap();
assert_eq!(too_low.clock_rate, 8_000);
assert!(!too_low.is_rfc4587_compliant());
let too_high = parse_rtpmap("a=rtpmap:96 H261/180000").unwrap();
assert_eq!(too_high.clock_rate, 180_000);
assert!(!too_high.is_rfc4587_compliant());
let legacy = parse_rtpmap("rtpmap:31 H261/90000").unwrap();
assert!(legacy.is_rfc4587_compliant());
}
#[test]
fn parse_rtpmap_strict_drops_non_compliant_clock_rate() {
assert_eq!(
parse_rtpmap_strict("a=rtpmap:31 H261/90000"),
Some(RtpMap {
payload_type: 31,
clock_rate: 90_000,
})
);
assert_eq!(parse_rtpmap_strict("a=rtpmap:31 H261/8000"), None);
assert_eq!(parse_rtpmap_strict("a=rtpmap:96 H261/45000"), None);
assert_eq!(parse_rtpmap_strict("a=rtpmap:34 H263/90000"), None);
assert_eq!(parse_rtpmap_strict("a=fmtp:31 CIF=2"), None);
let m = parse_rtpmap_strict("a=rtpmap:31 H261/90000/1").unwrap();
assert_eq!(m.payload_type, 31);
assert!(m.is_rfc4587_compliant());
}
#[test]
fn format_value_matches_spec_example() {
let p = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
assert_eq!(p.format_value(), "CIF=2;QCIF=1;D=1");
}
#[test]
fn format_fmtp_matches_spec_example() {
let p = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
assert_eq!(
format_fmtp(31, &p).as_deref(),
Some("a=fmtp:31 CIF=2;QCIF=1;D=1")
);
}
#[test]
fn format_fmtp_empty_params_yields_no_line() {
let p = H261FmtpParams::default();
assert_eq!(format_fmtp(31, &p), None);
assert_eq!(p.format_value(), "");
}
#[test]
fn format_value_cif_before_qcif() {
let p = H261FmtpParams {
cif: Some(4),
qcif: Some(2),
d: None,
};
assert_eq!(p.format_value(), "CIF=4;QCIF=2");
}
#[test]
fn format_value_d_zero_is_explicit() {
let p = H261FmtpParams {
cif: None,
qcif: Some(1),
d: Some(false),
};
assert_eq!(p.format_value(), "QCIF=1;D=0");
}
#[test]
fn format_value_preferred_cif_matches_canonical_formatter() {
let shapes = [
H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
},
H261FmtpParams {
cif: Some(4),
qcif: None,
d: None,
},
H261FmtpParams {
cif: None,
qcif: Some(3),
d: Some(false),
},
H261FmtpParams {
cif: None,
qcif: None,
d: Some(true),
},
H261FmtpParams::default(),
];
for p in &shapes {
assert_eq!(
p.format_value_preferred(SourceFormat::Cif),
p.format_value(),
"CIF preference must equal the canonical order for {p:?}",
);
}
}
#[test]
fn format_value_preferred_qcif_leads_with_qcif() {
let p = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
let out = p.format_value_preferred(SourceFormat::Qcif);
assert_eq!(out, "QCIF=1;CIF=2;D=1");
assert_eq!(
H261FmtpParams::parse_preference_order(&out),
vec![SourceFormat::Qcif, SourceFormat::Cif],
);
}
#[test]
fn format_value_preferred_unadvertised_preference_falls_back() {
let cif_only = H261FmtpParams {
cif: Some(3),
qcif: None,
d: Some(true),
};
assert_eq!(
cif_only.format_value_preferred(SourceFormat::Qcif),
"CIF=3;D=1",
);
let qcif_only = H261FmtpParams {
cif: None,
qcif: Some(2),
d: None,
};
assert_eq!(
qcif_only.format_value_preferred(SourceFormat::Cif),
"QCIF=2",
);
}
#[test]
fn format_value_preferred_round_trips_parse_value() {
let p = H261FmtpParams {
cif: Some(2),
qcif: Some(4),
d: Some(false),
};
for fmt in [SourceFormat::Cif, SourceFormat::Qcif] {
let out = p.format_value_preferred(fmt);
let reparsed = H261FmtpParams::parse_value(&out).expect("formatter output reparses");
assert_eq!(reparsed, p, "round trip with preference {fmt:?}");
assert_eq!(
H261FmtpParams::parse_preference_order(&out).first(),
Some(&fmt),
);
}
}
#[test]
fn format_fmtp_preferred_builds_full_line() {
let p = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
assert_eq!(
format_fmtp_preferred(31, &p, SourceFormat::Qcif).unwrap(),
"a=fmtp:31 QCIF=1;CIF=2;D=1",
);
assert_eq!(
format_fmtp_preferred(31, &p, SourceFormat::Cif),
format_fmtp(31, &p),
);
let empty = H261FmtpParams::default();
assert_eq!(format_fmtp_preferred(31, &empty, SourceFormat::Qcif), None);
let line = format_fmtp_preferred(96, &p, SourceFormat::Qcif).unwrap();
let reparsed = parse_fmtp(&line, 96).unwrap().unwrap();
assert_eq!(reparsed, p);
}
#[test]
fn parse_value_round_trips_spec_example() {
let p = H261FmtpParams::parse_value("CIF=2;QCIF=1;D=1").unwrap();
assert_eq!(p.cif, Some(2));
assert_eq!(p.qcif, Some(1));
assert_eq!(p.d, Some(true));
assert_eq!(p.format_value(), "CIF=2;QCIF=1;D=1");
}
#[test]
fn parse_value_tolerates_whitespace() {
let p = H261FmtpParams::parse_value(" CIF = 2 ; QCIF=1 ; D = 1 ").unwrap();
assert_eq!(p.cif, Some(2));
assert_eq!(p.qcif, Some(1));
assert_eq!(p.d, Some(true));
}
#[test]
fn parse_value_case_insensitive_names() {
let p = H261FmtpParams::parse_value("cif=3;qcif=4;d=1").unwrap();
assert_eq!(p.cif, Some(3));
assert_eq!(p.qcif, Some(4));
assert_eq!(p.d, Some(true));
}
#[test]
fn parse_value_empty_yields_default() {
assert_eq!(
H261FmtpParams::parse_value("").unwrap(),
H261FmtpParams::default()
);
assert_eq!(
H261FmtpParams::parse_value(" ").unwrap(),
H261FmtpParams::default()
);
let p = H261FmtpParams::parse_value(";CIF=1;").unwrap();
assert_eq!(p.cif, Some(1));
}
#[test]
fn parse_value_ignores_unknown_params() {
let p = H261FmtpParams::parse_value("CIF=2;MAXBR=256;QCIF=1").unwrap();
assert_eq!(p.cif, Some(2));
assert_eq!(p.qcif, Some(1));
}
#[test]
fn parse_value_rejects_mpi_out_of_range() {
for bad in [0u32, 5, 100] {
let err = H261FmtpParams::parse_value(&format!("CIF={bad}")).unwrap_err();
assert_eq!(
err,
SdpError::MpiOutOfRange {
param: "CIF",
value: bad
}
);
}
let err = H261FmtpParams::parse_value("QCIF=9").unwrap_err();
assert_eq!(
err,
SdpError::MpiOutOfRange {
param: "QCIF",
value: 9
}
);
}
#[test]
fn parse_value_accepts_full_mpi_range() {
for mpi in 1u8..=4 {
let p = H261FmtpParams::parse_value(&format!("CIF={mpi}")).unwrap();
assert_eq!(p.cif, Some(mpi));
}
}
#[test]
fn parse_value_rejects_non_integer_mpi() {
let err = H261FmtpParams::parse_value("CIF=two").unwrap_err();
assert_eq!(
err,
SdpError::NotAnInteger {
param: "CIF".to_string(),
value: "two".to_string()
}
);
}
#[test]
fn parse_value_rejects_bad_annex_d() {
let err = H261FmtpParams::parse_value("QCIF=1;D=2").unwrap_err();
assert_eq!(
err,
SdpError::BadAnnexD {
value: "2".to_string()
}
);
let err = H261FmtpParams::parse_value("QCIF=1;D=yes").unwrap_err();
assert_eq!(
err,
SdpError::BadAnnexD {
value: "yes".to_string()
}
);
}
#[test]
fn parse_value_accepts_d_zero() {
let p = H261FmtpParams::parse_value("QCIF=1;D=0").unwrap();
assert_eq!(p.d, Some(false));
}
#[test]
fn parse_value_rejects_malformed_token() {
let err = H261FmtpParams::parse_value("CIF").unwrap_err();
assert_eq!(
err,
SdpError::MalformedToken {
token: "CIF".to_string()
}
);
}
#[test]
fn parse_value_rejects_duplicate_param() {
let err = H261FmtpParams::parse_value("CIF=2;CIF=3").unwrap_err();
assert_eq!(
err,
SdpError::DuplicateParam {
param: "CIF".to_string()
}
);
let err = H261FmtpParams::parse_value("QCIF=1;qcif=2").unwrap_err();
assert_eq!(
err,
SdpError::DuplicateParam {
param: "QCIF".to_string()
}
);
}
#[test]
fn validate_requires_a_picture_size() {
let none = H261FmtpParams {
cif: None,
qcif: None,
d: Some(true),
};
assert_eq!(none.validate(), Err(SdpError::NoPictureSize));
let cif_only = H261FmtpParams {
cif: Some(1),
..Default::default()
};
assert_eq!(cif_only.validate(), Ok(()));
let qcif_only = H261FmtpParams {
qcif: Some(1),
..Default::default()
};
assert_eq!(qcif_only.validate(), Ok(()));
}
#[test]
fn supports_and_mpi_reflect_fields() {
let p = H261FmtpParams {
cif: Some(2),
qcif: None,
d: None,
};
assert!(p.supports(SourceFormat::Cif));
assert!(!p.supports(SourceFormat::Qcif));
assert_eq!(p.mpi(SourceFormat::Cif), Some(2));
assert_eq!(p.mpi(SourceFormat::Qcif), None);
}
#[test]
fn max_frame_rate_is_29_97_over_mpi() {
let p = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: None,
};
assert_eq!(p.max_frame_rate(SourceFormat::Cif), Some((2997, 200)));
assert_eq!(p.max_frame_rate(SourceFormat::Qcif), Some((2997, 100)));
let q = H261FmtpParams {
cif: None,
qcif: Some(4),
d: None,
};
assert_eq!(q.max_frame_rate(SourceFormat::Cif), None);
assert_eq!(q.max_frame_rate(SourceFormat::Qcif), Some((2997, 400)));
}
#[test]
fn rfc2032_fallback_is_qcif_mpi_1() {
let p = H261FmtpParams::rfc2032_fallback();
assert_eq!(p.qcif, Some(1));
assert_eq!(p.cif, None);
assert_eq!(p.d, None);
assert!(p.supports(SourceFormat::Qcif));
assert!(!p.supports(SourceFormat::Cif));
}
#[test]
fn parse_fmtp_line_round_trips_spec_example() {
let parsed = parse_fmtp("a=fmtp:31 CIF=2;QCIF=1;D=1", 31)
.unwrap()
.unwrap();
assert_eq!(parsed.cif, Some(2));
assert_eq!(parsed.qcif, Some(1));
assert_eq!(parsed.d, Some(true));
assert_eq!(
format_fmtp(31, &parsed).as_deref(),
Some("a=fmtp:31 CIF=2;QCIF=1;D=1")
);
}
#[test]
fn parse_fmtp_without_a_prefix() {
let parsed = parse_fmtp("fmtp:96 QCIF=1", 96).unwrap().unwrap();
assert_eq!(parsed.qcif, Some(1));
}
#[test]
fn parse_fmtp_rejects_payload_type_mismatch() {
assert!(parse_fmtp("a=fmtp:31 CIF=2", 96).is_none());
}
#[test]
fn parse_fmtp_rejects_non_fmtp_line() {
assert!(parse_fmtp("a=rtpmap:31 H261/90000", 31).is_none());
}
#[test]
fn parse_fmtp_surfaces_value_errors() {
let res = parse_fmtp("a=fmtp:31 CIF=9", 31).unwrap();
assert_eq!(
res,
Err(SdpError::MpiOutOfRange {
param: "CIF",
value: 9
})
);
}
#[test]
fn full_session_description_lines_round_trip() {
let params = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
let rtpmap = format_rtpmap(31);
let fmtp = format_fmtp(31, ¶ms).unwrap();
assert_eq!(rtpmap, "a=rtpmap:31 H261/90000");
assert_eq!(fmtp, "a=fmtp:31 CIF=2;QCIF=1;D=1");
let map = parse_rtpmap(&rtpmap).unwrap();
assert_eq!(map.payload_type, 31);
assert_eq!(map.clock_rate, CLOCK_RATE);
let back = parse_fmtp(&fmtp, map.payload_type).unwrap().unwrap();
assert_eq!(back, params);
}
#[test]
fn preferred_picture_size_picks_cif_when_advertised() {
let both = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: None,
};
assert_eq!(both.preferred_picture_size(), Some(SourceFormat::Cif));
let qcif_only = H261FmtpParams {
cif: None,
qcif: Some(1),
d: None,
};
assert_eq!(qcif_only.preferred_picture_size(), Some(SourceFormat::Qcif));
let cif_only = H261FmtpParams {
cif: Some(3),
qcif: None,
d: None,
};
assert_eq!(cif_only.preferred_picture_size(), Some(SourceFormat::Cif));
let neither = H261FmtpParams::default();
assert_eq!(neither.preferred_picture_size(), None);
}
#[test]
fn negotiate_answer_intersects_picture_sizes() {
let offer = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: None,
};
let ours = H261FmtpParams {
cif: None,
qcif: Some(1),
d: None,
};
let ans = negotiate_answer(&offer, &ours).unwrap();
assert_eq!(ans.cif, None);
assert_eq!(ans.qcif, Some(1));
assert_eq!(ans.d, None);
}
#[test]
fn negotiate_answer_picks_max_mpi_per_size() {
let offer = H261FmtpParams {
cif: Some(1),
qcif: Some(1),
d: None,
};
let ours = H261FmtpParams {
cif: Some(3),
qcif: Some(2),
d: None,
};
let ans = negotiate_answer(&offer, &ours).unwrap();
assert_eq!(ans.cif, Some(3));
assert_eq!(ans.qcif, Some(2));
}
#[test]
fn negotiate_answer_disjoint_sizes_errors() {
let offer = H261FmtpParams {
cif: Some(2),
qcif: None,
d: None,
};
let ours = H261FmtpParams {
cif: None,
qcif: Some(1),
d: None,
};
assert_eq!(
negotiate_answer(&offer, &ours),
Err(SdpError::NoPictureSize)
);
}
#[test]
fn negotiate_answer_annex_d_needs_both_sides() {
let off_d = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
let our_d = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
let our_no_d = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: None,
};
let our_d_zero = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(false),
};
assert_eq!(negotiate_answer(&off_d, &our_d).unwrap().d, Some(true));
assert_eq!(negotiate_answer(&off_d, &our_no_d).unwrap().d, None);
assert_eq!(negotiate_answer(&off_d, &our_d_zero).unwrap().d, None);
let off_no_d = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: None,
};
assert_eq!(negotiate_answer(&off_no_d, &our_d).unwrap().d, None);
}
#[test]
fn negotiate_answer_rfc2032_fallback_for_offerless_picture_size() {
let offer = H261FmtpParams::default(); let ours = H261FmtpParams {
cif: Some(2),
qcif: Some(2),
d: Some(true),
};
let ans = negotiate_answer(&offer, &ours).unwrap();
assert_eq!(ans.cif, None);
assert_eq!(ans.qcif, Some(2));
assert_eq!(ans.d, None);
}
#[test]
fn negotiate_answer_round_trips_through_format_value() {
let offer = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
let ours = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: Some(true),
};
let ans = negotiate_answer(&offer, &ours).unwrap();
assert_eq!(ans.format_value(), "CIF=2;QCIF=1;D=1");
}
#[test]
fn negotiate_answer_validate_passes() {
let offer = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: None,
};
let ours = H261FmtpParams {
cif: Some(2),
qcif: None,
d: None,
};
let ans = negotiate_answer(&offer, &ours).unwrap();
assert!(ans.validate().is_ok());
}
#[test]
fn negotiate_answer_full_offer_answer_max_frame_rate_satisfies_both_peers() {
let offer = H261FmtpParams {
cif: Some(1),
qcif: Some(4),
d: None,
};
let ours = H261FmtpParams {
cif: Some(2),
qcif: Some(1),
d: None,
};
let ans = negotiate_answer(&offer, &ours).unwrap();
assert_eq!(ans.cif, Some(2));
assert_eq!(ans.qcif, Some(4));
let (num_cif, den_cif) = ans.max_frame_rate(SourceFormat::Cif).unwrap();
assert_eq!((num_cif, den_cif), (2997, 200));
let (num_q, den_q) = ans.max_frame_rate(SourceFormat::Qcif).unwrap();
assert_eq!((num_q, den_q), (2997, 400));
}
#[test]
fn parse_preference_order_spec_worked_example() {
assert_eq!(
H261FmtpParams::parse_preference_order("CIF=2;QCIF=1;D=1"),
vec![SourceFormat::Cif, SourceFormat::Qcif],
);
}
#[test]
fn parse_preference_order_qcif_first() {
assert_eq!(
H261FmtpParams::parse_preference_order("QCIF=1;CIF=2"),
vec![SourceFormat::Qcif, SourceFormat::Cif],
);
assert_eq!(
H261FmtpParams::parse_preference_order("QCIF=1;CIF=2;D=1"),
vec![SourceFormat::Qcif, SourceFormat::Cif],
);
}
#[test]
fn parse_preference_order_single_size() {
assert_eq!(
H261FmtpParams::parse_preference_order("CIF=2"),
vec![SourceFormat::Cif],
);
assert_eq!(
H261FmtpParams::parse_preference_order("QCIF=1"),
vec![SourceFormat::Qcif],
);
assert_eq!(
H261FmtpParams::parse_preference_order("D=1;CIF=2;MAXBR=256"),
vec![SourceFormat::Cif],
);
}
#[test]
fn parse_preference_order_no_picture_size_is_empty() {
assert!(H261FmtpParams::parse_preference_order("").is_empty());
assert!(H261FmtpParams::parse_preference_order("D=1").is_empty());
assert!(H261FmtpParams::parse_preference_order("D=0;MAXBR=256").is_empty());
}
#[test]
fn parse_preference_order_is_case_insensitive() {
assert_eq!(
H261FmtpParams::parse_preference_order("qcif=1;cif=2"),
vec![SourceFormat::Qcif, SourceFormat::Cif],
);
assert_eq!(
H261FmtpParams::parse_preference_order("Cif=2;Qcif=1"),
vec![SourceFormat::Cif, SourceFormat::Qcif],
);
}
#[test]
fn parse_preference_order_tolerates_whitespace_and_empty_tokens() {
assert_eq!(
H261FmtpParams::parse_preference_order(" CIF = 2 ; QCIF=1 "),
vec![SourceFormat::Cif, SourceFormat::Qcif],
);
assert_eq!(
H261FmtpParams::parse_preference_order(";CIF=2;;QCIF=1;"),
vec![SourceFormat::Cif, SourceFormat::Qcif],
);
}
#[test]
fn parse_preference_order_dedupes_repeated_tokens() {
assert_eq!(
H261FmtpParams::parse_preference_order("CIF=2;CIF=3;QCIF=1"),
vec![SourceFormat::Cif, SourceFormat::Qcif],
);
assert_eq!(
H261FmtpParams::parse_preference_order("QCIF=1;CIF=2;QCIF=3"),
vec![SourceFormat::Qcif, SourceFormat::Cif],
);
}
#[test]
fn parse_preference_order_skips_malformed_tokens() {
assert_eq!(
H261FmtpParams::parse_preference_order("CIF;QCIF=9;CIF=2"),
vec![SourceFormat::Qcif, SourceFormat::Cif],
);
assert_eq!(
H261FmtpParams::parse_preference_order("QCIF;CIF=9;QCIF=1"),
vec![SourceFormat::Cif, SourceFormat::Qcif],
);
}
#[test]
fn parse_preference_order_agrees_with_parse_value_on_spec_example() {
let p = H261FmtpParams::parse_value("CIF=2;QCIF=1;D=1").unwrap();
let order = H261FmtpParams::parse_preference_order("CIF=2;QCIF=1;D=1");
assert_eq!(p.preferred_picture_size(), order.first().copied());
}
#[test]
fn parse_preference_order_diverges_from_preferred_picture_size_on_qcif_first() {
let value = "QCIF=1;CIF=2;D=1";
let p = H261FmtpParams::parse_value(value).unwrap();
let order = H261FmtpParams::parse_preference_order(value);
assert_eq!(p.preferred_picture_size(), Some(SourceFormat::Cif));
assert_eq!(order.first().copied(), Some(SourceFormat::Qcif));
}
#[test]
fn parse_fmtp_strict_accepts_spec_example() {
let lenient = parse_fmtp("a=fmtp:31 CIF=2;QCIF=1;D=1", 31)
.unwrap()
.unwrap();
let strict = parse_fmtp_strict("a=fmtp:31 CIF=2;QCIF=1;D=1", 31)
.unwrap()
.unwrap();
assert_eq!(lenient, strict);
assert!(strict.validate().is_ok());
}
#[test]
fn parse_fmtp_strict_rejects_no_picture_size() {
let lenient = parse_fmtp("a=fmtp:31 D=1", 31).unwrap().unwrap();
assert_eq!(lenient.cif, None);
assert_eq!(lenient.qcif, None);
assert_eq!(lenient.d, Some(true));
assert!(matches!(lenient.validate(), Err(SdpError::NoPictureSize)));
assert!(parse_fmtp_strict("a=fmtp:31 D=1", 31).is_none());
}
#[test]
fn parse_fmtp_strict_rejects_only_unknown_parameter() {
let lenient = parse_fmtp("a=fmtp:31 FUTUREPARAM=hello", 31)
.unwrap()
.unwrap();
assert_eq!(lenient, H261FmtpParams::default());
assert!(parse_fmtp_strict("a=fmtp:31 FUTUREPARAM=hello", 31).is_none());
}
#[test]
fn parse_fmtp_strict_propagates_malformed_token_error() {
let strict = parse_fmtp_strict("a=fmtp:31 CIF", 31);
assert!(matches!(strict, Some(Err(SdpError::MalformedToken { .. }))));
}
#[test]
fn parse_fmtp_strict_payload_type_mismatch_returns_none() {
assert!(parse_fmtp_strict("a=fmtp:31 CIF=2", 96).is_none());
}
#[test]
fn parse_fmtp_strict_accepts_qcif_only_line() {
let strict = parse_fmtp_strict("a=fmtp:31 QCIF=1", 31).unwrap().unwrap();
assert_eq!(strict.cif, None);
assert_eq!(strict.qcif, Some(1));
assert_eq!(strict.d, None);
}
#[test]
fn parse_fmtp_strict_accepts_cif_only_line() {
let strict = parse_fmtp_strict("a=fmtp:31 CIF=4", 31).unwrap().unwrap();
assert_eq!(strict.cif, Some(4));
assert_eq!(strict.qcif, None);
assert_eq!(strict.d, None);
}
#[test]
fn parse_fmtp_strict_does_not_apply_rfc2032_fallback() {
assert!(parse_fmtp_strict("a=fmtp:31 D=1", 31).is_none());
assert!(parse_fmtp_strict("a=fmtp:31 FUTUREPARAM=hello", 31).is_none());
let fallback = H261FmtpParams::rfc2032_fallback();
assert_eq!(fallback.qcif, Some(1));
}
#[test]
fn parse_fmtp_strict_implies_lenient() {
for line in [
"a=fmtp:31 CIF=2;QCIF=1;D=1",
"a=fmtp:31 CIF=4",
"a=fmtp:31 QCIF=2",
"a=fmtp:31 QCIF=1;CIF=2",
"a=fmtp:96 CIF=1;D=0",
] {
let pt: u8 = line["a=fmtp:".len()..line.find(' ').unwrap()]
.parse()
.unwrap();
let lenient = parse_fmtp(line, pt).unwrap().unwrap();
let strict = parse_fmtp_strict(line, pt).unwrap().unwrap();
assert_eq!(lenient, strict, "strict diverges from lenient: {line}");
assert!(
strict.validate().is_ok(),
"strict accepted §6.2.1-noncompliant line: {line}"
);
}
}
#[test]
fn display_covers_all_error_variants() {
let cases: [SdpError; 6] = [
SdpError::MpiOutOfRange {
param: "CIF",
value: 5,
},
SdpError::BadAnnexD {
value: "2".to_string(),
},
SdpError::NotAnInteger {
param: "QCIF".to_string(),
value: "x".to_string(),
},
SdpError::MalformedToken {
token: "CIF".to_string(),
},
SdpError::DuplicateParam {
param: "CIF".to_string(),
},
SdpError::NoPictureSize,
];
for e in &cases {
let s = e.to_string();
assert!(s.starts_with("h261 sdp:"), "unexpected Display: {s}");
}
}
}