Skip to main content

rfc6381_codec/
lib.rs

1//! Support for codec parameter values
2//!
3//! See also,
4//!  - [MDN: The "codecs" parameter in common media types](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter)
5//!
6//! ## Basic usage
7//!
8//! Parse a codec string,
9//! ```rust
10//! # use rfc6381_codec::Codec;
11//! # use std::str::FromStr;
12//! let codec = Codec::from_str("avc1.4D401E");
13//! if let Ok(Codec::Avc1(avc1)) = codec {
14//!     assert_eq!(avc1.profile(), 0x4d);
15//! } else {
16//!     panic!("unexpected codec type");
17//! }
18//! ```
19//!
20//! Generate a codec string,
21//!
22//! ```rust
23//! # use rfc6381_codec::Codec;
24//! let codec = Codec::avc1(0x4d, 0x40, 0x1e);
25//! assert_eq!(codec.to_string(), "avc1.4D401E")
26//! ```
27//!
28//! ## No support for 'fancy' syntax
29//!
30//! RFC 6381 specifies the following BNF grammar for general syntax, which this crate does not
31//! yet fully support:
32//!
33//! ```text
34//!   codecs      := cod-simple / cod-fancy
35//!   cod-simple  := "codecs" "=" unencodedv
36//!   unencodedv  := id-simple / simp-list
37//!   simp-list   := DQUOTE id-simple *( "," id-simple ) DQUOTE
38//!   id-simple   := element
39//!               ; "." reserved as hierarchy delimiter
40//!   element     := 1*octet-sim
41//!   octet-sim   := <any TOKEN character>
42//!
43//!               ; Within a 'codecs' parameter value, "." is reserved
44//!               ; as a hierarchy delimiter
45//!   cod-fancy   := "codecs*" "=" encodedv
46//!   encodedv    := fancy-sing / fancy-list
47//!   fancy-sing  := [charset] "'" [language] "'" id-encoded
48//!               ; Parsers MAY ignore <language>
49//!               ; Parsers MAY support only US-ASCII and UTF-8
50//!   fancy-list  := DQUOTE [charset] "'" [language] "'" id-list DQUOTE
51//!               ; Parsers MAY ignore <language>
52//!               ; Parsers MAY support only US-ASCII and UTF-8
53//!   id-list     := id-encoded *( "," id-encoded )
54//!   id-encoded  := encoded-elm *( "." encoded-elm )
55//!               ; "." reserved as hierarchy delimiter
56//!   encoded-elm := 1*octet-fancy
57//!   octet-fancy := ext-octet / attribute-char
58//!
59//!   DQUOTE      := %x22 ; " (double quote)
60//! ```
61//!
62//! In particular note the following productions:
63//!
64//!  - `cod-simple` - specifies the attribute name+value structure `codec=".."` — this crate only
65//!    supports dealing with the value of this attribute (the bit inside quotes).
66//!  - `cod-fancy` (and related productions `fancy-sing` / `fancy-list` etc.) — show extended
67//!    structures that can optionally specify a charset for the data like `en-gb'UTF-8'%25%20xz` or `''%25%20xz` — this crate does not support values
68//!    using these structures.
69
70use 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    /// AVC with in-band parameter sets. Codec-string grammar is identical to
80    /// `avc1`; only the fourcc (and the implied parameter-set location in the
81    /// bitstream) differs. Defined in ISO/IEC 14496-15, not RFC 6381 itself.
82    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    /// The given codec-string-component was not valid
161    InvalidComponent(String),
162    /// expected the '.', but instead found the text included in the variant
163    ExpectedHierarchySeparator(String),
164    /// The length of the given string did not match the expected length
165    UnexpectedLength { expected: usize, got: String },
166}
167
168/// AVC profile/constraints/level triple (the `PPCCLL` grammar from RFC 6381
169/// §3.3). Shared between the `avc1` and `avc3` codec-string forms.
170#[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        // byte position 4 is in the middle of a unicode codepoint - if we naively split off the
377        // first 4 bytes this would panic.  We shouldn't panic, we should instead produce an Err.
378        assert!(Codec::from_str("cod👍ec").is_err())
379    }
380
381    #[test]
382    fn avc1_non_ascii_payload() {
383        // payload is 6 bytes but contains a 2-byte UTF-8 codepoint, so byte-indexing into
384        // it would land mid-codepoint.  We must Err rather than panic.
385        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    // Verifies the doc-hidden `Avc1` type alias still resolves so that
412    // pre-rename downstream code keeps compiling.
413    #[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        // the prefix before '.' is not 4 bytes, so the whole string is returned as Unknown
452        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        // exercises the Mp4a::Unknown Display path where audio_object_type_indication is Some
467        roundtrip("mp4a.41.5");
468    }
469}