1use mp4ra_rust::{ObjectTypeIdentifier, SampleEntryCode};
71use mpeg4_audio_const::AudioObjectType;
72use std::fmt;
73use std::str::FromStr;
74
75#[derive(Debug, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum Codec {
78 Avc1(Avc),
79 Avc3(Avc),
83 Mp4a(Mp4a),
84 Unknown(String),
85}
86impl Codec {
87 pub fn parse_codecs(codecs: &str) -> impl Iterator<Item = Result<Codec, CodecError>> + '_ {
88 codecs.split(',').map(|s| s.trim().parse())
89 }
90
91 pub fn avc1(profile: u8, constraints: u8, level: u8) -> Self {
92 Codec::Avc1(Avc {
93 profile,
94 constraints,
95 level,
96 })
97 }
98
99 pub fn avc3(profile: u8, constraints: u8, level: u8) -> Self {
100 Codec::Avc3(Avc {
101 profile,
102 constraints,
103 level,
104 })
105 }
106}
107impl FromStr for Codec {
108 type Err = CodecError;
109
110 fn from_str(codec: &str) -> Result<Codec, Self::Err> {
111 if let Some(pos) = codec.find('.') {
112 let (fourcc, rest) = codec.split_at(pos);
113 if fourcc.len() != 4 {
114 return Ok(Codec::Unknown(codec.to_string()));
115 }
116 let fourcc = mp4ra_rust::FourCC::from(fourcc.as_bytes());
117 let sample_entry = SampleEntryCode::from(fourcc);
118 match sample_entry {
119 SampleEntryCode::MP4A => Ok(Codec::Mp4a(get_rest(rest)?.parse()?)),
120 SampleEntryCode::AVC1 => Ok(Codec::Avc1(get_rest(rest)?.parse()?)),
121 SampleEntryCode::AVC3 => Ok(Codec::Avc3(get_rest(rest)?.parse()?)),
122 _ => Ok(Codec::Unknown(codec.to_owned())),
123 }
124 } else {
125 Err(CodecError::ExpectedHierarchySeparator(codec.to_string()))
126 }
127 }
128}
129impl fmt::Display for Codec {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
131 match self {
132 Codec::Avc1(Avc {
133 profile,
134 constraints,
135 level,
136 }) => write!(f, "avc1.{:02X}{:02X}{:02X}", profile, constraints, level),
137 Codec::Avc3(Avc {
138 profile,
139 constraints,
140 level,
141 }) => write!(f, "avc3.{:02X}{:02X}{:02X}", profile, constraints, level),
142 Codec::Mp4a(mp4a) => write!(f, "mp4a.{}", mp4a),
143 Codec::Unknown(val) => f.write_str(val),
144 }
145 }
146}
147
148fn get_rest(text: &str) -> Result<&str, CodecError> {
149 if text.is_empty() {
150 Ok(text)
151 } else if let Some(rest) = text.strip_prefix('.') {
152 Ok(rest)
153 } else {
154 Err(CodecError::ExpectedHierarchySeparator(text.to_string()))
155 }
156}
157
158#[derive(Debug)]
159pub enum CodecError {
160 InvalidComponent(String),
162 ExpectedHierarchySeparator(String),
164 UnexpectedLength { expected: usize, got: String },
166}
167
168#[derive(Debug, PartialEq, Eq)]
171pub struct Avc {
172 profile: u8,
173 constraints: u8,
174 level: u8,
175}
176impl Avc {
177 pub fn profile(&self) -> u8 {
178 self.profile
179 }
180 pub fn constraints(&self) -> u8 {
181 self.constraints
182 }
183 pub fn level(&self) -> u8 {
184 self.level
185 }
186}
187impl FromStr for Avc {
188 type Err = CodecError;
189
190 fn from_str(value: &str) -> Result<Self, Self::Err> {
191 if value.len() != 6 {
192 return Err(CodecError::UnexpectedLength {
193 expected: 6,
194 got: value.to_string(),
195 });
196 }
197 if !value.is_ascii() {
198 return Err(CodecError::InvalidComponent(value.to_string()));
199 }
200
201 let profile = u8::from_str_radix(&value[0..2], 16)
202 .map_err(|_| CodecError::InvalidComponent(value.to_string()))?;
203
204 let constraints = u8::from_str_radix(&value[2..4], 16)
205 .map_err(|_| CodecError::InvalidComponent(value.to_string()))?;
206
207 let level = u8::from_str_radix(&value[4..6], 16)
208 .map_err(|_| CodecError::InvalidComponent(value.to_string()))?;
209
210 Ok(Avc {
211 profile,
212 constraints,
213 level,
214 })
215 }
216}
217
218#[doc(hidden)]
219pub type Avc1 = Avc;
220
221#[derive(Debug, PartialEq, Eq)]
222#[non_exhaustive]
223pub enum Mp4a {
224 Mpeg4Audio {
225 audio_object_type: Option<AudioObjectType>,
226 },
227 Unknown {
228 object_type_indication: ObjectTypeIdentifier,
229 audio_object_type_indication: Option<u8>,
230 },
231}
232impl fmt::Display for Mp4a {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 match self {
235 Mp4a::Mpeg4Audio { audio_object_type } => {
236 write!(
237 f,
238 "{:02x}",
239 u8::from(ObjectTypeIdentifier::AUDIO_ISO_IEC_14496_3)
240 )?;
241 if let Some(aoti) = audio_object_type {
242 write!(f, ".{}", u8::from(*aoti))?;
243 }
244 Ok(())
245 }
246 Mp4a::Unknown {
247 object_type_indication,
248 audio_object_type_indication,
249 } => {
250 write!(f, "{:02x}", u8::from(*object_type_indication))?;
251 if let Some(aoti) = audio_object_type_indication {
252 write!(f, ".{}", aoti)?;
253 }
254 Ok(())
255 }
256 }
257 }
258}
259
260impl FromStr for Mp4a {
261 type Err = CodecError;
262
263 fn from_str(value: &str) -> Result<Self, Self::Err> {
264 let mut i = value.splitn(2, '.');
265 let s = i.next().unwrap();
266 let oti =
267 u8::from_str_radix(s, 16).map_err(|_| CodecError::InvalidComponent(s.to_string()))?;
268 let oti = ObjectTypeIdentifier::from(oti);
269 let aoti = i
270 .next()
271 .map(u8::from_str)
272 .transpose()
273 .map_err(|e| CodecError::InvalidComponent(e.to_string()))?;
274 match oti {
275 ObjectTypeIdentifier::AUDIO_ISO_IEC_14496_3 => {
276 let aoti = aoti
277 .map(AudioObjectType::try_from)
278 .transpose()
279 .map_err(|_e| CodecError::InvalidComponent(aoti.unwrap().to_string()))?;
280 Ok(Mp4a::Mpeg4Audio {
281 audio_object_type: aoti,
282 })
283 }
284 _ => Ok(Mp4a::Unknown {
285 object_type_indication: oti,
286 audio_object_type_indication: aoti,
287 }),
288 }
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use assert_matches::*;
296
297 fn roundtrip(codec: &str) {
298 assert_eq!(codec, Codec::from_str(codec).unwrap().to_string())
299 }
300
301 #[test]
302 fn mp4a() {
303 assert_matches!(
304 Codec::from_str("mp4a.40.3"),
305 Ok(Codec::Mp4a(Mp4a::Mpeg4Audio {
306 audio_object_type: Some(AudioObjectType::AAC_SSR)
307 }))
308 );
309 roundtrip("mp4a.40.3");
310 }
311
312 #[test]
313 fn unknown_oti() {
314 const RESERVED_X41: ObjectTypeIdentifier = ObjectTypeIdentifier(0x41);
315 assert_matches!(
316 Codec::from_str("mp4a.41"),
317 Ok(Codec::Mp4a(Mp4a::Unknown {
318 object_type_indication: RESERVED_X41,
319 audio_object_type_indication: None
320 }))
321 );
322 roundtrip("mp4a.41");
323 }
324
325 #[test]
326 fn bad_oti_digit() {
327 assert_matches!(Codec::from_str("mp4a.4g"), Err(_));
328 }
329
330 #[test]
331 fn list() {
332 let mut i = Codec::parse_codecs("mp4a.40.2,avc1.4d401e");
333 assert_matches!(
334 i.next().unwrap(),
335 Ok(Codec::Mp4a(Mp4a::Mpeg4Audio {
336 audio_object_type: Some(AudioObjectType::AAC_LC)
337 }))
338 );
339 assert_matches!(
340 i.next().unwrap(),
341 Ok(Codec::Avc1(Avc {
342 profile: 0x4d,
343 constraints: 0x40,
344 level: 0x1e
345 }))
346 );
347 }
348
349 #[test]
350 fn avc1() {
351 assert_matches!(
352 Codec::from_str("avc1.4d401e"),
353 Ok(Codec::Avc1(Avc {
354 profile: 0x4d,
355 constraints: 0x40,
356 level: 0x1e
357 }))
358 );
359 roundtrip("avc1.4D401E");
360 }
361
362 #[test]
363 fn bad_avc1_lengths() {
364 assert_matches!(Codec::from_str("avc1.41141"), Err(CodecError::UnexpectedLength { expected: 6, got: text }) if text == "41141");
365 assert_matches!(Codec::from_str("avc1.4114134"), Err(CodecError::UnexpectedLength { expected: 6, got: text }) if text == "4114134");
366 }
367
368 #[test]
369 fn unknown_fourcc() {
370 assert_matches!(Codec::from_str("badd.41"), Ok(Codec::Unknown(v)) if v == "badd.41");
371 roundtrip("badd.41");
372 }
373
374 #[test]
375 fn invalid_unicode_boundary() {
376 assert!(Codec::from_str("cod👍ec").is_err())
379 }
380
381 #[test]
382 fn avc1_non_ascii_payload() {
383 assert!(Codec::from_str("avc1.4\u{029e}\u{0}1E").is_err())
386 }
387
388 #[test]
389 fn avc1_factory_and_accessors() {
390 let codec = Codec::avc1(0x4d, 0x40, 0x1e);
391 assert_matches!(
392 &codec,
393 Codec::Avc1(a) if a.profile() == 0x4d && a.constraints() == 0x40 && a.level() == 0x1e
394 );
395 assert_eq!(codec.to_string(), "avc1.4D401E");
396 }
397
398 #[test]
399 fn avc3() {
400 assert_matches!(
401 Codec::from_str("avc3.4d401e"),
402 Ok(Codec::Avc3(Avc {
403 profile: 0x4d,
404 constraints: 0x40,
405 level: 0x1e
406 }))
407 );
408 roundtrip("avc3.4D401E");
409 }
410
411 #[test]
414 fn avc1_alias_still_works() {
415 #[allow(deprecated)]
416 let _: Avc1 = Avc {
417 profile: 0,
418 constraints: 0,
419 level: 0,
420 };
421 }
422
423 #[test]
424 fn avc3_factory_and_accessors() {
425 let codec = Codec::avc3(0x64, 0x00, 0x1f);
426 assert_matches!(
427 &codec,
428 Codec::Avc3(a) if a.profile() == 0x64 && a.constraints() == 0x00 && a.level() == 0x1f
429 );
430 assert_eq!(codec.to_string(), "avc3.64001F");
431 }
432
433 #[test]
434 fn avc3_bad_length() {
435 assert_matches!(
436 Codec::from_str("avc3.4114"),
437 Err(CodecError::UnexpectedLength { expected: 6, got: text }) if text == "4114"
438 );
439 }
440
441 #[test]
442 fn avc1_and_avc3_are_distinct() {
443 assert_ne!(
444 Codec::from_str("avc1.4D401E").unwrap(),
445 Codec::from_str("avc3.4D401E").unwrap()
446 );
447 }
448
449 #[test]
450 fn fourcc_wrong_length() {
451 assert_matches!(Codec::from_str("ab.cd"), Ok(Codec::Unknown(v)) if v == "ab.cd");
453 assert_matches!(Codec::from_str("abcde.12"), Ok(Codec::Unknown(v)) if v == "abcde.12");
454 }
455
456 #[test]
457 fn no_hierarchy_separator() {
458 assert_matches!(
459 Codec::from_str("avc1"),
460 Err(CodecError::ExpectedHierarchySeparator(v)) if v == "avc1"
461 );
462 }
463
464 #[test]
465 fn mp4a_unknown_oti_with_aoti() {
466 roundtrip("mp4a.41.5");
468 }
469}