1use std::fmt;
7
8#[non_exhaustive]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum CodecMedia {
12 Audio,
13 Video,
14}
15
16impl fmt::Display for CodecMedia {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 CodecMedia::Audio => f.pad("audio"),
20 CodecMedia::Video => f.pad("video"),
21 }
22 }
23}
24
25#[non_exhaustive]
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CodecOffer {
32 pub name: String,
33 pub payload_type: u8,
34 pub clock_rate: Option<u32>,
35 pub ptime: Option<u32>,
36 pub bitrate: Option<u32>,
37 pub channels: Option<u8>,
38}
39
40impl fmt::Display for CodecOffer {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "{}:{}", self.name, self.payload_type)?;
43 if let Some(rate) = self.clock_rate {
44 write!(f, " {rate}Hz")?;
45 }
46 if let Some(ptime) = self.ptime {
47 write!(f, " {ptime}ms")?;
48 }
49 if let Some(bitrate) = self.bitrate {
50 write!(f, " {bitrate}b")?;
51 }
52 if let Some(channels) = self.channels {
53 write!(f, " {channels}ch")?;
54 }
55 Ok(())
56 }
57}
58
59#[non_exhaustive]
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum CodecParseError {
63 Arity { media: CodecMedia, fields: usize },
65 NotNumeric { field: &'static str },
67 EmptyName,
69}
70
71impl fmt::Display for CodecParseError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 CodecParseError::Arity { media, fields } => {
75 write!(f, "{fields} fields is not a {media} codec token")
76 }
77 CodecParseError::NotNumeric { field } => write!(f, "non-numeric {field}"),
78 CodecParseError::EmptyName => f.write_str("empty codec name"),
79 }
80 }
81}
82
83impl std::error::Error for CodecParseError {}
84
85impl CodecOffer {
86 pub fn parse(media: CodecMedia, token: &str) -> Result<Self, CodecParseError> {
93 let mut fields = token.split(':');
94 let name = fields.next().unwrap_or_default();
95 if name.is_empty() {
96 return Err(CodecParseError::EmptyName);
97 }
98 let rest: Vec<&str> = fields.collect();
99
100 let num = |raw: &str, field: &'static str| -> Result<u32, CodecParseError> {
101 raw.parse()
102 .map_err(|_| CodecParseError::NotNumeric { field })
103 };
104 let payload_type = |raw: &str| -> Result<u8, CodecParseError> {
105 raw.parse().map_err(|_| CodecParseError::NotNumeric {
106 field: "payload type",
107 })
108 };
109
110 let arity = |fields: usize| CodecParseError::Arity { media, fields };
111
112 match (media, rest.as_slice()) {
113 (CodecMedia::Video, [pt]) => Ok(CodecOffer {
114 name: name.to_string(),
115 payload_type: payload_type(pt)?,
116 clock_rate: None,
117 ptime: None,
118 bitrate: None,
119 channels: None,
120 }),
121 (CodecMedia::Audio, [pt, rate, ptime, bitrate, channels])
122 | (CodecMedia::Audio, [pt, rate, _, ptime, bitrate, channels]) => Ok(CodecOffer {
123 name: name.to_string(),
124 payload_type: payload_type(pt)?,
125 clock_rate: Some(num(rate, "clock rate")?),
126 ptime: Some(num(ptime, "ptime")?),
127 bitrate: Some(num(bitrate, "bitrate")?),
128 channels: Some(
129 num(channels, "channels")?
130 .try_into()
131 .map_err(|_| CodecParseError::NotNumeric { field: "channels" })?,
132 ),
133 }),
134 _ => Err(arity(rest.len() + 1)),
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
144pub(crate) enum CodecTrace {
145 Compared {
146 offered: CodecOffer,
147 local: CodecOffer,
148 },
149 Matched(CodecOffer),
150 NearMatched(CodecOffer),
151 Dropped(CodecOffer),
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158pub(crate) enum CodecTraceError {
159 UnknownShape,
161 BadToken(CodecParseError),
163}
164
165impl From<CodecTraceError> for Option<CodecParseError> {
166 fn from(e: CodecTraceError) -> Self {
167 match e {
168 CodecTraceError::UnknownShape => None,
169 CodecTraceError::BadToken(e) => Some(e),
170 }
171 }
172}
173
174impl CodecTrace {
175 pub(crate) fn parse(media: CodecMedia, msg: &str) -> Result<Self, CodecTraceError> {
182 let rest = msg
183 .strip_prefix("Audio Codec Compare ")
184 .or_else(|| msg.strip_prefix("Video Codec Compare "))
185 .unwrap_or(msg);
186 let rest = rest
190 .strip_prefix('[')
191 .ok_or(CodecTraceError::UnknownShape)?;
192 let token = |t: &str| CodecOffer::parse(media, t).map_err(CodecTraceError::BadToken);
193
194 if let Some(slash) = rest.find("]/[") {
195 return Ok(CodecTrace::Compared {
196 offered: token(&rest[..slash])?,
197 local: token(rest[slash + 3..].trim_end_matches(']'))?,
198 });
199 }
200
201 let end = rest.find(']').ok_or(CodecTraceError::UnknownShape)?;
202 let (codec, verdict) = (&rest[..end], &rest[end + 1..]);
203 if verdict.contains("was not saved") {
204 Ok(CodecTrace::Dropped(token(codec)?))
205 } else if verdict.contains("near-match") {
206 Ok(CodecTrace::NearMatched(token(codec)?))
207 } else if verdict.contains("is saved as a match") {
208 Ok(CodecTrace::Matched(token(codec)?))
209 } else {
210 Err(CodecTraceError::UnknownShape)
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 fn audio(token: &str) -> CodecOffer {
220 CodecOffer::parse(CodecMedia::Audio, token).expect("parsed")
221 }
222
223 #[test]
224 fn audio_six_field_form() {
225 let c = audio("opus:116:16000:20:0:1");
226 assert_eq!(c.name, "opus");
227 assert_eq!(c.payload_type, 116);
228 assert_eq!(c.clock_rate, Some(16000));
229 assert_eq!(c.ptime, Some(20));
230 assert_eq!(c.bitrate, Some(0));
231 assert_eq!(c.channels, Some(1));
232 }
233
234 #[test]
235 fn audio_seven_field_form_skips_the_extra_rate() {
236 let c = audio("PCMU:0:8000:8000:20:64000:1");
239 assert_eq!(c.clock_rate, Some(8000));
240 assert_eq!(c.ptime, Some(20));
241 assert_eq!(c.bitrate, Some(64000));
242 assert_eq!(c.channels, Some(1));
243 }
244
245 #[test]
246 fn video_two_field_form() {
247 let c = CodecOffer::parse(CodecMedia::Video, "H264:109").expect("parsed");
248 assert_eq!(c.name, "H264");
249 assert_eq!(c.payload_type, 109);
250 assert_eq!(c.clock_rate, None);
251 assert_eq!(c.channels, None);
252 }
253
254 #[test]
255 fn media_type_decides_the_arity() {
256 assert!(CodecOffer::parse(CodecMedia::Audio, "H264:109").is_err());
257 assert!(CodecOffer::parse(CodecMedia::Video, "opus:116:16000:20:0:1").is_err());
258 }
259
260 #[test]
261 fn malformed_tokens_do_not_parse_partially() {
262 assert_eq!(
263 CodecOffer::parse(CodecMedia::Audio, ""),
264 Err(CodecParseError::EmptyName)
265 );
266 assert_eq!(
267 CodecOffer::parse(CodecMedia::Video, "H264:notanumber"),
268 Err(CodecParseError::NotNumeric {
269 field: "payload type"
270 })
271 );
272 assert_eq!(
273 CodecOffer::parse(CodecMedia::Audio, "opus:116:sixteen:20:0:1"),
274 Err(CodecParseError::NotNumeric {
275 field: "clock rate"
276 })
277 );
278 assert!(matches!(
279 CodecOffer::parse(CodecMedia::Audio, "opus:116:16000"),
280 Err(CodecParseError::Arity { fields: 3, .. })
281 ));
282 }
283
284 #[test]
285 fn display_omits_absent_fields() {
286 assert_eq!(
287 audio("opus:116:16000:20:0:1").to_string(),
288 "opus:116 16000Hz 20ms 0b 1ch"
289 );
290 assert_eq!(
291 CodecOffer::parse(CodecMedia::Video, "H264:109")
292 .unwrap()
293 .to_string(),
294 "H264:109"
295 );
296 }
297}