use std::fmt;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CodecMedia {
Audio,
Video,
}
impl fmt::Display for CodecMedia {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CodecMedia::Audio => f.pad("audio"),
CodecMedia::Video => f.pad("video"),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodecOffer {
pub name: String,
pub payload_type: u8,
pub clock_rate: Option<u32>,
pub ptime: Option<u32>,
pub bitrate: Option<u32>,
pub channels: Option<u8>,
}
impl fmt::Display for CodecOffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.name, self.payload_type)?;
if let Some(rate) = self.clock_rate {
write!(f, " {rate}Hz")?;
}
if let Some(ptime) = self.ptime {
write!(f, " {ptime}ms")?;
}
if let Some(bitrate) = self.bitrate {
write!(f, " {bitrate}b")?;
}
if let Some(channels) = self.channels {
write!(f, " {channels}ch")?;
}
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodecParseError {
Arity { media: CodecMedia, fields: usize },
NotNumeric { field: &'static str },
EmptyName,
}
impl fmt::Display for CodecParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CodecParseError::Arity { media, fields } => {
write!(f, "{fields} fields is not a {media} codec token")
}
CodecParseError::NotNumeric { field } => write!(f, "non-numeric {field}"),
CodecParseError::EmptyName => f.write_str("empty codec name"),
}
}
}
impl std::error::Error for CodecParseError {}
impl CodecOffer {
pub fn parse(media: CodecMedia, token: &str) -> Result<Self, CodecParseError> {
let mut fields = token.split(':');
let name = fields.next().unwrap_or_default();
if name.is_empty() {
return Err(CodecParseError::EmptyName);
}
let rest: Vec<&str> = fields.collect();
let num = |raw: &str, field: &'static str| -> Result<u32, CodecParseError> {
raw.parse()
.map_err(|_| CodecParseError::NotNumeric { field })
};
let payload_type = |raw: &str| -> Result<u8, CodecParseError> {
raw.parse().map_err(|_| CodecParseError::NotNumeric {
field: "payload type",
})
};
let arity = |fields: usize| CodecParseError::Arity { media, fields };
match (media, rest.as_slice()) {
(CodecMedia::Video, [pt]) => Ok(CodecOffer {
name: name.to_string(),
payload_type: payload_type(pt)?,
clock_rate: None,
ptime: None,
bitrate: None,
channels: None,
}),
(CodecMedia::Audio, [pt, rate, ptime, bitrate, channels])
| (CodecMedia::Audio, [pt, rate, _, ptime, bitrate, channels]) => Ok(CodecOffer {
name: name.to_string(),
payload_type: payload_type(pt)?,
clock_rate: Some(num(rate, "clock rate")?),
ptime: Some(num(ptime, "ptime")?),
bitrate: Some(num(bitrate, "bitrate")?),
channels: Some(
num(channels, "channels")?
.try_into()
.map_err(|_| CodecParseError::NotNumeric { field: "channels" })?,
),
}),
_ => Err(arity(rest.len() + 1)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CodecTrace {
Compared {
offered: CodecOffer,
local: CodecOffer,
},
Matched(CodecOffer),
NearMatched(CodecOffer),
Dropped(CodecOffer),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CodecTraceError {
UnknownShape,
BadToken(CodecParseError),
}
impl From<CodecTraceError> for Option<CodecParseError> {
fn from(e: CodecTraceError) -> Self {
match e {
CodecTraceError::UnknownShape => None,
CodecTraceError::BadToken(e) => Some(e),
}
}
}
impl CodecTrace {
pub(crate) fn parse(media: CodecMedia, msg: &str) -> Result<Self, CodecTraceError> {
let rest = msg
.strip_prefix("Audio Codec Compare ")
.or_else(|| msg.strip_prefix("Video Codec Compare "))
.unwrap_or(msg);
let rest = rest
.strip_prefix('[')
.ok_or(CodecTraceError::UnknownShape)?;
let token = |t: &str| CodecOffer::parse(media, t).map_err(CodecTraceError::BadToken);
if let Some(slash) = rest.find("]/[") {
return Ok(CodecTrace::Compared {
offered: token(&rest[..slash])?,
local: token(rest[slash + 3..].trim_end_matches(']'))?,
});
}
let end = rest.find(']').ok_or(CodecTraceError::UnknownShape)?;
let (codec, verdict) = (&rest[..end], &rest[end + 1..]);
if verdict.contains("was not saved") {
Ok(CodecTrace::Dropped(token(codec)?))
} else if verdict.contains("near-match") {
Ok(CodecTrace::NearMatched(token(codec)?))
} else if verdict.contains("is saved as a match") {
Ok(CodecTrace::Matched(token(codec)?))
} else {
Err(CodecTraceError::UnknownShape)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn audio(token: &str) -> CodecOffer {
CodecOffer::parse(CodecMedia::Audio, token).expect("parsed")
}
#[test]
fn audio_six_field_form() {
let c = audio("opus:116:16000:20:0:1");
assert_eq!(c.name, "opus");
assert_eq!(c.payload_type, 116);
assert_eq!(c.clock_rate, Some(16000));
assert_eq!(c.ptime, Some(20));
assert_eq!(c.bitrate, Some(0));
assert_eq!(c.channels, Some(1));
}
#[test]
fn audio_seven_field_form_skips_the_extra_rate() {
let c = audio("PCMU:0:8000:8000:20:64000:1");
assert_eq!(c.clock_rate, Some(8000));
assert_eq!(c.ptime, Some(20));
assert_eq!(c.bitrate, Some(64000));
assert_eq!(c.channels, Some(1));
}
#[test]
fn video_two_field_form() {
let c = CodecOffer::parse(CodecMedia::Video, "H264:109").expect("parsed");
assert_eq!(c.name, "H264");
assert_eq!(c.payload_type, 109);
assert_eq!(c.clock_rate, None);
assert_eq!(c.channels, None);
}
#[test]
fn media_type_decides_the_arity() {
assert!(CodecOffer::parse(CodecMedia::Audio, "H264:109").is_err());
assert!(CodecOffer::parse(CodecMedia::Video, "opus:116:16000:20:0:1").is_err());
}
#[test]
fn malformed_tokens_do_not_parse_partially() {
assert_eq!(
CodecOffer::parse(CodecMedia::Audio, ""),
Err(CodecParseError::EmptyName)
);
assert_eq!(
CodecOffer::parse(CodecMedia::Video, "H264:notanumber"),
Err(CodecParseError::NotNumeric {
field: "payload type"
})
);
assert_eq!(
CodecOffer::parse(CodecMedia::Audio, "opus:116:sixteen:20:0:1"),
Err(CodecParseError::NotNumeric {
field: "clock rate"
})
);
assert!(matches!(
CodecOffer::parse(CodecMedia::Audio, "opus:116:16000"),
Err(CodecParseError::Arity { fields: 3, .. })
));
}
#[test]
fn display_omits_absent_fields() {
assert_eq!(
audio("opus:116:16000:20:0:1").to_string(),
"opus:116 16000Hz 20ms 0b 1ch"
);
assert_eq!(
CodecOffer::parse(CodecMedia::Video, "H264:109")
.unwrap()
.to_string(),
"H264:109"
);
}
}