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#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn audio(token: &str) -> CodecOffer {
144 CodecOffer::parse(CodecMedia::Audio, token).expect("parsed")
145 }
146
147 #[test]
148 fn audio_six_field_form() {
149 let c = audio("opus:116:16000:20:0:1");
150 assert_eq!(c.name, "opus");
151 assert_eq!(c.payload_type, 116);
152 assert_eq!(c.clock_rate, Some(16000));
153 assert_eq!(c.ptime, Some(20));
154 assert_eq!(c.bitrate, Some(0));
155 assert_eq!(c.channels, Some(1));
156 }
157
158 #[test]
159 fn audio_seven_field_form_skips_the_extra_rate() {
160 let c = audio("PCMU:0:8000:8000:20:64000:1");
163 assert_eq!(c.clock_rate, Some(8000));
164 assert_eq!(c.ptime, Some(20));
165 assert_eq!(c.bitrate, Some(64000));
166 assert_eq!(c.channels, Some(1));
167 }
168
169 #[test]
170 fn video_two_field_form() {
171 let c = CodecOffer::parse(CodecMedia::Video, "H264:109").expect("parsed");
172 assert_eq!(c.name, "H264");
173 assert_eq!(c.payload_type, 109);
174 assert_eq!(c.clock_rate, None);
175 assert_eq!(c.channels, None);
176 }
177
178 #[test]
179 fn media_type_decides_the_arity() {
180 assert!(CodecOffer::parse(CodecMedia::Audio, "H264:109").is_err());
181 assert!(CodecOffer::parse(CodecMedia::Video, "opus:116:16000:20:0:1").is_err());
182 }
183
184 #[test]
185 fn malformed_tokens_do_not_parse_partially() {
186 assert_eq!(
187 CodecOffer::parse(CodecMedia::Audio, ""),
188 Err(CodecParseError::EmptyName)
189 );
190 assert_eq!(
191 CodecOffer::parse(CodecMedia::Video, "H264:notanumber"),
192 Err(CodecParseError::NotNumeric {
193 field: "payload type"
194 })
195 );
196 assert_eq!(
197 CodecOffer::parse(CodecMedia::Audio, "opus:116:sixteen:20:0:1"),
198 Err(CodecParseError::NotNumeric {
199 field: "clock rate"
200 })
201 );
202 assert!(matches!(
203 CodecOffer::parse(CodecMedia::Audio, "opus:116:16000"),
204 Err(CodecParseError::Arity { fields: 3, .. })
205 ));
206 }
207
208 #[test]
209 fn display_omits_absent_fields() {
210 assert_eq!(
211 audio("opus:116:16000:20:0:1").to_string(),
212 "opus:116 16000Hz 20ms 0b 1ch"
213 );
214 assert_eq!(
215 CodecOffer::parse(CodecMedia::Video, "H264:109")
216 .unwrap()
217 .to_string(),
218 "H264:109"
219 );
220 }
221}