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::convert::TryFrom;
73use std::fmt;
74use std::str::FromStr;
75
76#[derive(Debug)]
77#[non_exhaustive]
78pub enum Codec {
79    Avc1(Avc1),
80    Mp4a(Mp4a),
81    Unknown(String),
82}
83impl Codec {
84    pub fn parse_codecs(codecs: &str) -> impl Iterator<Item = Result<Codec, CodecError>> + '_ {
85        codecs.split(',').map(|s| s.trim().parse())
86    }
87
88    pub fn avc1(profile: u8, constraints: u8, level: u8) -> Self {
89        Codec::Avc1(Avc1 {
90            profile,
91            constraints,
92            level,
93        })
94    }
95}
96impl FromStr for Codec {
97    type Err = CodecError;
98
99    fn from_str(codec: &str) -> Result<Codec, Self::Err> {
100        if let Some(pos) = codec.find('.') {
101            let (fourcc, rest) = codec.split_at(pos);
102            if fourcc.len() != 4 {
103                return Ok(Codec::Unknown(codec.to_string()));
104            }
105            let fourcc = mp4ra_rust::FourCC::from(fourcc.as_bytes());
106            let sample_entry = SampleEntryCode::from(fourcc);
107            match sample_entry {
108                SampleEntryCode::MP4A => Ok(Codec::Mp4a(get_rest(rest)?.parse()?)),
109                SampleEntryCode::AVC1 => Ok(Codec::Avc1(get_rest(rest)?.parse()?)),
110                _ => Ok(Codec::Unknown(codec.to_owned())),
111            }
112        } else {
113            Err(CodecError::ExpectedHierarchySeparator(codec.to_string()))
114        }
115    }
116}
117impl fmt::Display for Codec {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
119        match self {
120            Codec::Avc1(Avc1 {
121                profile,
122                constraints,
123                level,
124            }) => write!(f, "avc1.{:02X}{:02X}{:02X}", profile, constraints, level),
125            Codec::Mp4a(mp4a) => write!(f, "mp4a.{}", mp4a),
126            Codec::Unknown(val) => f.write_str(val),
127        }
128    }
129}
130
131fn get_rest(text: &str) -> Result<&str, CodecError> {
132    if text.is_empty() {
133        Ok(text)
134    } else if let Some(rest) = text.strip_prefix('.') {
135        Ok(rest)
136    } else {
137        Err(CodecError::ExpectedHierarchySeparator(text.to_string()))
138    }
139}
140
141#[derive(Debug)]
142pub enum CodecError {
143    /// The given codec-string-component was not valid
144    InvalidComponent(String),
145    /// expected the '.', but instead found the text included in the variant
146    ExpectedHierarchySeparator(String),
147    /// The length of the given string did not match the expected length
148    UnexpectedLength { expected: usize, got: String },
149}
150
151#[derive(Debug)]
152pub struct Avc1 {
153    profile: u8,
154    constraints: u8,
155    level: u8,
156}
157impl Avc1 {
158    pub fn profile(&self) -> u8 {
159        self.profile
160    }
161    pub fn constraints(&self) -> u8 {
162        self.constraints
163    }
164    pub fn level(&self) -> u8 {
165        self.level
166    }
167}
168impl FromStr for Avc1 {
169    type Err = CodecError;
170
171    fn from_str(value: &str) -> Result<Self, Self::Err> {
172        if value.len() != 6 {
173            return Err(CodecError::UnexpectedLength {
174                expected: 6,
175                got: value.to_string(),
176            });
177        }
178
179        let profile = u8::from_str_radix(&value[0..2], 16)
180            .map_err(|_| CodecError::InvalidComponent(value.to_string()))?;
181
182        let constraints = u8::from_str_radix(&value[2..4], 16)
183            .map_err(|_| CodecError::InvalidComponent(value.to_string()))?;
184
185        let level = u8::from_str_radix(&value[4..6], 16)
186            .map_err(|_| CodecError::InvalidComponent(value.to_string()))?;
187
188        Ok(Avc1 {
189            profile,
190            constraints,
191            level,
192        })
193    }
194}
195
196#[derive(Debug)]
197#[non_exhaustive]
198pub enum Mp4a {
199    Mpeg4Audio {
200        audio_object_type: Option<AudioObjectType>,
201    },
202    Unknown {
203        object_type_indication: ObjectTypeIdentifier,
204        audio_object_type_indication: Option<u8>,
205    },
206}
207impl fmt::Display for Mp4a {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        match self {
210            Mp4a::Mpeg4Audio { audio_object_type } => {
211                write!(
212                    f,
213                    "{:02x}",
214                    u8::from(ObjectTypeIdentifier::AUDIO_ISO_IEC_14496_3)
215                )?;
216                if let Some(aoti) = audio_object_type {
217                    write!(f, ".{}", u8::from(*aoti))?;
218                }
219                Ok(())
220            }
221            Mp4a::Unknown {
222                object_type_indication,
223                audio_object_type_indication,
224            } => {
225                write!(f, "{:02x}", u8::from(*object_type_indication))?;
226                if let Some(aoti) = audio_object_type_indication {
227                    write!(f, ".{}", aoti)?;
228                }
229                Ok(())
230            }
231        }
232    }
233}
234
235impl FromStr for Mp4a {
236    type Err = CodecError;
237
238    fn from_str(value: &str) -> Result<Self, Self::Err> {
239        let mut i = value.splitn(2, '.');
240        let s = i.next().unwrap();
241        let oti =
242            u8::from_str_radix(s, 16).map_err(|_| CodecError::InvalidComponent(s.to_string()))?;
243        let oti = ObjectTypeIdentifier::from(oti);
244        let aoti = i
245            .next()
246            .map(u8::from_str)
247            .transpose()
248            .map_err(|e| CodecError::InvalidComponent(e.to_string()))?;
249        match oti {
250            ObjectTypeIdentifier::AUDIO_ISO_IEC_14496_3 => {
251                let aoti = aoti
252                    .map(AudioObjectType::try_from)
253                    .transpose()
254                    .map_err(|_e| CodecError::InvalidComponent(aoti.unwrap().to_string()))?;
255                Ok(Mp4a::Mpeg4Audio {
256                    audio_object_type: aoti,
257                })
258            }
259            _ => Ok(Mp4a::Unknown {
260                object_type_indication: oti,
261                audio_object_type_indication: aoti,
262            }),
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use assert_matches::*;
271
272    fn roundtrip(codec: &str) {
273        assert_eq!(codec, Codec::from_str(codec).unwrap().to_string())
274    }
275
276    #[test]
277    fn mp4a() {
278        assert_matches!(
279            Codec::from_str("mp4a.40.3"),
280            Ok(Codec::Mp4a(Mp4a::Mpeg4Audio {
281                audio_object_type: Some(AudioObjectType::AAC_SSR)
282            }))
283        );
284        roundtrip("mp4a.40.3");
285    }
286
287    #[test]
288    fn unknown_oti() {
289        const RESERVED_X41: ObjectTypeIdentifier = ObjectTypeIdentifier(0x41);
290        assert_matches!(
291            Codec::from_str("mp4a.41"),
292            Ok(Codec::Mp4a(Mp4a::Unknown {
293                object_type_indication: RESERVED_X41,
294                audio_object_type_indication: None
295            }))
296        );
297        roundtrip("mp4a.41");
298    }
299
300    #[test]
301    fn bad_oti_digit() {
302        assert_matches!(Codec::from_str("mp4a.4g"), Err(_));
303    }
304
305    #[test]
306    fn list() {
307        let mut i = Codec::parse_codecs("mp4a.40.2,avc1.4d401e");
308        assert_matches!(
309            i.next().unwrap(),
310            Ok(Codec::Mp4a(Mp4a::Mpeg4Audio {
311                audio_object_type: Some(AudioObjectType::AAC_LC)
312            }))
313        );
314        assert_matches!(
315            i.next().unwrap(),
316            Ok(Codec::Avc1(Avc1 {
317                profile: 0x4d,
318                constraints: 0x40,
319                level: 0x1e
320            }))
321        );
322    }
323
324    #[test]
325    fn avc1() {
326        assert_matches!(
327            Codec::from_str("avc1.4d401e"),
328            Ok(Codec::Avc1(Avc1 {
329                profile: 0x4d,
330                constraints: 0x40,
331                level: 0x1e
332            }))
333        );
334        roundtrip("avc1.4D401E");
335    }
336
337    #[test]
338    fn bad_avc1_lengths() {
339        assert_matches!(Codec::from_str("avc1.41141"), Err(CodecError::UnexpectedLength { expected: 6, got: text }) if text == "41141");
340        assert_matches!(Codec::from_str("avc1.4114134"), Err(CodecError::UnexpectedLength { expected: 6, got: text }) if text == "4114134");
341    }
342
343    #[test]
344    fn unknown_fourcc() {
345        assert_matches!(Codec::from_str("badd.41"), Ok(Codec::Unknown(v)) if v == "badd.41");
346        roundtrip("badd.41");
347    }
348
349    #[test]
350    fn invalid_unicode_boundary() {
351        // byte position 4 is in the middle of a unicode codepoint - if we naively split off the
352        // first 4 bytes this would panic.  We shouldn't panic, we should instead produce an Err.
353        assert!(Codec::from_str("cod👍ec").is_err())
354    }
355}