scuffle_mp4/
codec.rs

1use std::fmt;
2use std::str::FromStr;
3
4use scuffle_aac::AudioObjectType;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum VideoCodec {
8    /// <https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter>
9    Avc {
10        profile: u8,
11        constraint_set: u8,
12        level: u8,
13    },
14    /// There is barely any documentation on this.
15    /// <https://hevcvideo.xp3.biz/html5_video.html>
16    Hevc {
17        general_profile_space: u8,
18        profile_compatibility: scuffle_h265::ProfileCompatibilityFlags,
19        profile: u8,
20        level: u8,
21        tier: bool,
22        constraint_indicator: u64,
23    },
24    /// <https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#av1>
25    Av1 {
26        profile: u8,
27        level: u8,
28        tier: bool,
29        depth: u8,
30        monochrome: bool,
31        sub_sampling_x: bool,
32        sub_sampling_y: bool,
33        color_primaries: u8,
34        transfer_characteristics: u8,
35        matrix_coefficients: u8,
36        full_range_flag: bool,
37    },
38}
39
40impl fmt::Display for VideoCodec {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            VideoCodec::Avc {
44                profile,
45                constraint_set,
46                level,
47            } => write!(f, "avc1.{profile:02x}{constraint_set:02x}{level:02x}"),
48            VideoCodec::Hevc {
49                general_profile_space,
50                profile,
51                level,
52                tier,
53                profile_compatibility,
54                constraint_indicator,
55            } => {
56                let profile_compatibility = profile_compatibility
57                    .bits()
58                    .to_be_bytes()
59                    .into_iter()
60                    .filter(|b| *b != 0)
61                    .map(|b| format!("{b:x}"))
62                    .collect::<String>();
63                let constraint_indicator = constraint_indicator
64                    .to_be_bytes()
65                    .into_iter()
66                    .filter(|b| *b != 0)
67                    .map(|b| format!("{b:x}"))
68                    .collect::<Vec<_>>()
69                    .join(".");
70
71                write!(
72                    f,
73                    "hev1.{}{:x}.{}.{}{:x}.{}",
74                    match general_profile_space {
75                        1 => "A",
76                        2 => "B",
77                        3 => "C",
78                        _ => "",
79                    },
80                    profile, // 1 or 2 chars (hex)
81                    profile_compatibility,
82                    if *tier { 'H' } else { 'L' },
83                    level, // 1 or 2 chars (hex)
84                    constraint_indicator,
85                )
86            }
87            VideoCodec::Av1 {
88                profile,
89                level,
90                tier,
91                depth,
92                monochrome,
93                sub_sampling_x,
94                sub_sampling_y,
95                color_primaries,
96                transfer_characteristics,
97                matrix_coefficients,
98                full_range_flag,
99            } => write!(
100                f,
101                "av01.{}.{}{}.{:02}.{}.{}{}{}.{:02}.{:02}.{:02}.{}",
102                profile,
103                level,
104                if *tier { 'H' } else { 'M' },
105                depth,
106                if *monochrome { 1 } else { 0 },
107                if *sub_sampling_x { 1 } else { 0 },
108                if *sub_sampling_y { 1 } else { 0 },
109                if *monochrome { 1 } else { 0 },
110                color_primaries,
111                transfer_characteristics,
112                matrix_coefficients,
113                if *full_range_flag { 1 } else { 0 },
114            ),
115        }
116    }
117}
118
119impl FromStr for VideoCodec {
120    type Err = String;
121
122    fn from_str(s: &str) -> Result<Self, Self::Err> {
123        let splits = s.split('.').collect::<Vec<_>>();
124        if splits.is_empty() {
125            return Err("invalid codec, empty string".into());
126        }
127
128        match splits[0] {
129            "avc1" => {
130                if splits.len() < 2 {
131                    return Err("invalid codec, missing profile".into());
132                }
133
134                let profile = u8::from_str_radix(&splits[1][..2], 16)
135                    .map_err(|e| format!("invalid codec, invalid profile: {}, {}", splits[1], e))?;
136                let constraint_set = u8::from_str_radix(&splits[1][2..4], 16)
137                    .map_err(|e| format!("invalid codec, invalid constraint set: {}, {}", splits[1], e))?;
138                let level = u8::from_str_radix(&splits[1][4..6], 16)
139                    .map_err(|e| format!("invalid codec, invalid level: {}, {}", splits[1], e))?;
140
141                Ok(VideoCodec::Avc {
142                    profile,
143                    constraint_set,
144                    level,
145                })
146            }
147            "hev1" => {
148                if splits.len() < 6 {
149                    return Err("invalid codec, missing profile".into());
150                }
151
152                let general_profile_space = match splits[1] {
153                    "A" => 1,
154                    "B" => 2,
155                    "C" => 3,
156                    _ => {
157                        return Err(format!("invalid codec, invalid general profile space: {}", splits[1]));
158                    }
159                };
160
161                let profile = u8::from_str_radix(splits[2], 16)
162                    .map_err(|e| format!("invalid codec, invalid profile: {}, {}", splits[2], e))?;
163
164                let profile_compatibility = scuffle_h265::ProfileCompatibilityFlags::from_bits_retain(
165                    u32::from_str_radix(splits[3], 16)
166                        .map_err(|e| format!("invalid codec, invalid profile compatibility: {}, {}", splits[3], e))?,
167                );
168
169                let tier = match splits[4] {
170                    "H" => true,
171                    "L" => false,
172                    _ => return Err(format!("invalid codec, invalid tier: {}", splits[4])),
173                };
174
175                let level = u8::from_str_radix(splits[5], 16)
176                    .map_err(|e| format!("invalid codec, invalid level: {}, {}", splits[5], e))?;
177
178                let constraint_indicator = u64::from_str_radix(splits[6], 16)
179                    .map_err(|e| format!("invalid codec, invalid constraint indicator: {}, {}", splits[6], e))?;
180
181                Ok(VideoCodec::Hevc {
182                    general_profile_space,
183                    profile,
184                    level,
185                    tier,
186                    profile_compatibility,
187                    constraint_indicator,
188                })
189            }
190            "av01" => {
191                if splits.len() < 12 {
192                    return Err("invalid codec, missing profile".into());
193                }
194
195                let profile = u8::from_str_radix(splits[1], 16)
196                    .map_err(|e| format!("invalid codec, invalid profile: {}, {}", splits[1], e))?;
197
198                let level = u8::from_str_radix(splits[2], 16)
199                    .map_err(|e| format!("invalid codec, invalid level: {}, {}", splits[2], e))?;
200
201                let tier = match splits[3] {
202                    "H" => true,
203                    "M" => false,
204                    _ => return Err(format!("invalid codec, invalid tier: {}", splits[3])),
205                };
206
207                let depth = splits[4]
208                    .parse::<u8>()
209                    .map_err(|e| format!("invalid codec, invalid depth: {}, {}", splits[4], e))?;
210
211                let monochrome = match splits[5] {
212                    "1" => true,
213                    "0" => false,
214                    _ => return Err(format!("invalid codec, invalid monochrome: {}", splits[5])),
215                };
216
217                let sub_sampling_x = match splits[6] {
218                    "1" => true,
219                    "0" => false,
220                    _ => {
221                        return Err(format!("invalid codec, invalid sub_sampling_x: {}", splits[6]));
222                    }
223                };
224
225                let sub_sampling_y = match splits[7] {
226                    "1" => true,
227                    "0" => false,
228                    _ => {
229                        return Err(format!("invalid codec, invalid sub_sampling_y: {}", splits[7]));
230                    }
231                };
232
233                let color_primaries = splits[8]
234                    .parse::<u8>()
235                    .map_err(|e| format!("invalid codec, invalid color_primaries: {}, {}", splits[8], e))?;
236
237                let transfer_characteristics = splits[9]
238                    .parse::<u8>()
239                    .map_err(|e| format!("invalid codec, invalid transfer_characteristics: {}, {}", splits[9], e))?;
240
241                let matrix_coefficients = splits[10]
242                    .parse::<u8>()
243                    .map_err(|e| format!("invalid codec, invalid matrix_coefficients: {}, {}", splits[10], e))?;
244
245                let full_range_flag = splits[11]
246                    .parse::<u8>()
247                    .map_err(|e| format!("invalid codec, invalid full_range_flag: {}, {}", splits[11], e))?
248                    == 1;
249
250                Ok(VideoCodec::Av1 {
251                    profile,
252                    level,
253                    tier,
254                    depth,
255                    monochrome,
256                    sub_sampling_x,
257                    sub_sampling_y,
258                    color_primaries,
259                    transfer_characteristics,
260                    matrix_coefficients,
261                    full_range_flag,
262                })
263            }
264            r => Err(format!("invalid codec, unknown type: {r}")),
265        }
266    }
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub enum AudioCodec {
271    Aac {
272        object_type: AudioObjectType,
273    },
274    Opus,
275}
276
277impl fmt::Display for AudioCodec {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        match self {
280            AudioCodec::Aac { object_type } => write!(f, "mp4a.40.{}", u16::from(*object_type)),
281            AudioCodec::Opus => write!(f, "opus"),
282        }
283    }
284}
285
286impl FromStr for AudioCodec {
287    type Err = String;
288
289    fn from_str(s: &str) -> Result<Self, Self::Err> {
290        let splits = s.split('.').collect::<Vec<_>>();
291        if splits.is_empty() {
292            return Err("invalid codec, empty string".into());
293        }
294
295        match splits[0] {
296            "mp4a" => {
297                if splits.len() < 3 {
298                    return Err("invalid codec, missing object type".into());
299                }
300
301                let object_type = splits[2]
302                    .parse::<u16>()
303                    .map_err(|e| format!("invalid codec, invalid object type: {}, {}", splits[2], e))?;
304
305                Ok(AudioCodec::Aac {
306                    object_type: AudioObjectType::from(object_type),
307                })
308            }
309            "opus" => Ok(AudioCodec::Opus),
310            r => Err(format!("invalid codec, unknown type: {r}")),
311        }
312    }
313}