Skip to main content

zvidlib_container/
codec_config.rs

1//! Portable derivation of WebCodecs codec strings and [`CodecProfile`] values
2//! from raw MP4 `hvcC`/`av1C`/`vpcC` codec configuration boxes and for VP8,
3//! which has none, and the VP9 codec configuration record
4//! ([`Vp9CodecConfig`]).
5//!
6//! This module contains no browser (`web_sys`) types so it can be unit
7//! tested natively. It is consumed by the browser WebCodecs decoder factory
8//! to build the `codec` string WebCodecs' `isConfigSupported`/`VideoDecoder`
9//! APIs require, and to select a normalized [`CodecProfile`].
10
11use crate::codec::CodecProfile;
12use crate::media::Codec;
13use crate::{Av1CodecConfigurationRecord, Error, ErrorKind, Limits, Result};
14#[doc(inline)]
15pub use zvidlib_core::codec_config::Vp9CodecConfig;
16use zvidlib_core::codec_config::box_payload;
17
18/// A codec string plus the normalized profile it was derived from.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct DerivedCodecString {
21    pub codec_string: String,
22    pub profile: CodecProfile,
23}
24
25/// Derives a WebCodecs `codec` string and [`CodecProfile`] from a track's
26/// complete raw codec configuration box (including its size/fourcc header):
27/// `hvcC`, `av1C` or Opus's `dOps`. Vorbis has no MP4 box, so its
28/// configuration is the Xiph-laced `CodecPrivate` [`crate::VorbisConfig`]
29/// reads.
30pub fn derive_codec_string(codec: Codec, decoder_config: &[u8]) -> Result<DerivedCodecString> {
31    match codec {
32        Codec::Hevc => derive_hevc(decoder_config),
33        Codec::Av1 => derive_av1(decoder_config),
34        // VP8 has one profile and no configuration record, so its WebCodecs
35        // string is the bare codec name.
36        Codec::Vp8 => Ok(DerivedCodecString {
37            codec_string: "vp8".to_owned(),
38            profile: CodecProfile::Vp8,
39        }),
40        // Opus and Vorbis each have a single codec string; the configuration
41        // is still checked so a track the browser is handed is one zvidlib
42        // could also decode itself.
43        Codec::Opus => {
44            crate::OpusHead::from_dops(decoder_config)?;
45            Ok(DerivedCodecString {
46                codec_string: "opus".into(),
47                profile: CodecProfile::Opus,
48            })
49        }
50        Codec::Vorbis => {
51            crate::VorbisConfig::from_codec_private(decoder_config)?;
52            Ok(DerivedCodecString {
53                codec_string: "vorbis".into(),
54                profile: CodecProfile::Vorbis,
55            })
56        }
57        Codec::Vp9 => {
58            let config = Vp9CodecConfig::parse(decoder_config)?;
59            Ok(DerivedCodecString {
60                codec_string: config.codec_string(),
61                profile: config.codec_profile()?,
62            })
63        }
64        Codec::UncompressedVideo | Codec::H264 | Codec::Aac => Err(Error::new(
65            ErrorKind::Unsupported,
66            "codec string derivation only supports HEVC, AV1, VP8, VP9, Opus and Vorbis",
67        )),
68        _ => Err(Error::new(
69            ErrorKind::Unsupported,
70            "codec string derivation only supports HEVC, AV1, VP8, VP9, Opus and Vorbis",
71        )),
72    }
73}
74
75// --- HEVC (hvcC) -----------------------------------------------------------
76//
77// ISO/IEC 14496-15 HEVCDecoderConfigurationRecord layout (relevant prefix):
78// unsigned int(8) configurationVersion;
79// unsigned int(2) general_profile_space;
80// unsigned int(1) general_tier_flag;
81// unsigned int(5) general_profile_idc;
82// unsigned int(32) general_profile_compatibility_flags;
83// unsigned int(48) general_constraint_indicator_flags;
84// unsigned int(8) general_level_idc;
85
86fn derive_hevc(decoder_config: &[u8]) -> Result<DerivedCodecString> {
87    let payload = box_payload(decoder_config, b"hvcC")?;
88    if payload.len() < 13 {
89        return Err(Error::new(
90            ErrorKind::MalformedMedia,
91            "hvcC configuration is too short",
92        ));
93    }
94    let profile_space = (payload[1] >> 6) & 0b11;
95    let tier_flag = (payload[1] >> 5) & 0b1;
96    let profile_idc = payload[1] & 0b0001_1111;
97    let compatibility_flags = u32::from_be_bytes(payload[2..6].try_into().expect("4 bytes"));
98    let constraint_flags = &payload[6..12];
99    let level_idc = payload[12];
100
101    // General profile/tier/level string per ISO/IEC 14496-15 Annex E and the
102    // WebCodecs HEVC registration ("hev1"/"hvc1" fourcc followed by a dot,
103    // then the general_profile_space letter (if any) plus profile_idc, tier
104    // ('L' or 'H'), level_idc, then '.' plus each constraint byte in hex,
105    // trailing zero bytes omitted).
106    let profile_space_letter = match profile_space {
107        0 => String::new(),
108        1 => "A".to_owned(),
109        2 => "B".to_owned(),
110        3 => "C".to_owned(),
111        _ => unreachable!("profile_space is masked to 2 bits"),
112    };
113    let tier_letter = if tier_flag == 1 { 'H' } else { 'L' };
114
115    let mut constraint_hex = String::new();
116    let mut last_nonzero = None;
117    for (index, byte) in constraint_flags.iter().enumerate() {
118        if *byte != 0 {
119            last_nonzero = Some(index);
120        }
121    }
122    if let Some(last) = last_nonzero {
123        for byte in &constraint_flags[..=last] {
124            constraint_hex.push('.');
125            constraint_hex.push_str(&format!("{byte:02X}"));
126        }
127    }
128
129    let codec_string = format!(
130        "hev1.{profile_space_letter}{profile_idc}.{compatibility_flags:X}.{tier_letter}{level_idc}{constraint_hex}"
131    );
132
133    let profile = match profile_idc {
134        2 => CodecProfile::HevcMain10,
135        _ => CodecProfile::HevcMain,
136    };
137
138    Ok(DerivedCodecString {
139        codec_string,
140        profile,
141    })
142}
143
144// --- AV1 (av1C) --------------------------------------------------------
145//
146// AV1 Codec ISO Media File Format Binding, AV1CodecConfigurationRecord:
147// unsigned int (1) marker = 1;
148// unsigned int (7) version = 1;
149// unsigned int (3) seq_profile;
150// unsigned int (5) seq_level_idx_0;
151// unsigned int (1) seq_tier_0;
152// unsigned int (1) high_bitdepth;
153// unsigned int (1) twelve_bit;
154// unsigned int (1) monochrome;
155// unsigned int (1) chroma_subsampling_x;
156// unsigned int (1) chroma_subsampling_y;
157// unsigned int (2) chroma_sample_position;
158// ...
159
160fn derive_av1(decoder_config: &[u8]) -> Result<DerivedCodecString> {
161    let record = Av1CodecConfigurationRecord::parse(decoder_config, &Limits::default())?;
162    let seq_profile = record.seq_profile;
163    let seq_level_idx_0 = record.seq_level_idx_0;
164
165    let bit_depth: u32 = if record.high_bitdepth {
166        if seq_profile == 2 && record.twelve_bit {
167            12
168        } else {
169            10
170        }
171    } else {
172        8
173    };
174
175    let tier_letter = if record.seq_tier_0 { 'H' } else { 'M' };
176    let mono = u8::from(record.monochrome);
177    let chroma_subsampling_x = u8::from(record.chroma_subsampling_x);
178    let chroma_subsampling_y = u8::from(record.chroma_subsampling_y);
179    let chroma_sample_position = record.chroma_sample_position;
180    let codec_string = format!(
181        "av01.{seq_profile}.{seq_level_idx_0:02}{tier_letter}.{bit_depth:02}.{mono}.{chroma_subsampling_x}{chroma_subsampling_y}{chroma_sample_position}"
182    );
183
184    Ok(DerivedCodecString {
185        codec_string,
186        profile: match seq_profile {
187            0 => CodecProfile::Av1Main,
188            1 => CodecProfile::Av1High,
189            2 => CodecProfile::Av1Professional,
190            _ => unreachable!("validated AV1 profile"),
191        },
192    })
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
200        let mut bytes = (8 + payload.len() as u32).to_be_bytes().to_vec();
201        bytes.extend_from_slice(kind);
202        bytes.extend_from_slice(payload);
203        bytes
204    }
205
206    #[test]
207    fn derives_hevc_main_profile_codec_string() {
208        // configurationVersion=1, profile_space=0, tier=0, profile_idc=1 (Main)
209        let mut payload = vec![1_u8, 0b0000_0001];
210        payload.extend_from_slice(&0x6000_0000_u32.to_be_bytes()); // compatibility flags
211        payload.extend_from_slice(&[0, 0, 0, 0, 0, 0]); // constraint flags (all zero)
212        payload.push(93); // general_level_idc
213        payload.extend_from_slice(&[0; 12]); // remaining fixed fields (not parsed)
214        let hvcc = boxed(b"hvcC", &payload);
215
216        let derived = derive_codec_string(Codec::Hevc, &hvcc).unwrap();
217        assert_eq!(derived.codec_string, "hev1.1.60000000.L93");
218        assert_eq!(derived.profile, CodecProfile::HevcMain);
219    }
220
221    #[test]
222    fn derives_hevc_main10_profile_from_profile_idc_two() {
223        let mut payload = vec![1_u8, 0b0000_0010];
224        payload.extend_from_slice(&0x6000_0000_u32.to_be_bytes());
225        payload.extend_from_slice(&[0xB0, 0, 0, 0, 0, 0]);
226        payload.push(120);
227        payload.extend_from_slice(&[0; 12]);
228        let hvcc = boxed(b"hvcC", &payload);
229
230        let derived = derive_codec_string(Codec::Hevc, &hvcc).unwrap();
231        assert_eq!(derived.profile, CodecProfile::HevcMain10);
232        assert_eq!(derived.codec_string, "hev1.2.60000000.L120.B0");
233    }
234
235    #[test]
236    fn derives_av1_main_profile_codec_string() {
237        // marker=1, version=1, seq_profile=0, seq_level_idx_0=4 (matching the
238        // repo's existing av1C test fixture bytes: [0x81, 0, 0, 0]).
239        let av1c = boxed(b"av1C", &[0x81, 0x04, 0, 0]);
240        let derived = derive_codec_string(Codec::Av1, &av1c).unwrap();
241        assert_eq!(derived.profile, CodecProfile::Av1Main);
242        assert_eq!(derived.codec_string, "av01.0.04M.08.0.000");
243    }
244
245    #[test]
246    fn av1_codec_string_carries_every_chroma_sample_position() {
247        // 4:2:0 color with each `chroma_sample_position`; the value is the last
248        // digit of the `av01.` string.
249        for pos in 0..4u8 {
250            let av1c = boxed(b"av1C", &[0x81, 0x04, 0x0c | pos, 0]);
251            let derived = derive_codec_string(Codec::Av1, &av1c).unwrap();
252            assert_eq!(derived.profile, CodecProfile::Av1Main);
253            assert_eq!(derived.codec_string, format!("av01.0.04M.08.0.11{pos}"));
254        }
255    }
256
257    #[test]
258    fn distinguishes_av1_high_and_professional_profiles() {
259        let high = boxed(b"av1C", &[0x81, 0x24, 0, 0]);
260        assert_eq!(
261            derive_codec_string(Codec::Av1, &high).unwrap().profile,
262            CodecProfile::Av1High
263        );
264        let professional = boxed(b"av1C", &[0x81, 0x44, 0x68, 0]);
265        assert_eq!(
266            derive_codec_string(Codec::Av1, &professional)
267                .unwrap()
268                .profile,
269            CodecProfile::Av1Professional
270        );
271    }
272
273    #[test]
274    fn rejects_wrong_fourcc() {
275        let bogus = boxed(b"esds", &[0, 0, 0, 0]);
276        let error = derive_codec_string(Codec::Hevc, &bogus).unwrap_err();
277        assert_eq!(error.kind(), ErrorKind::MalformedMedia);
278    }
279
280    #[test]
281    fn rejects_truncated_configuration() {
282        let short = boxed(b"hvcC", &[1, 2]);
283        let error = derive_codec_string(Codec::Hevc, &short).unwrap_err();
284        assert_eq!(error.kind(), ErrorKind::MalformedMedia);
285
286        let short_av1 = boxed(b"av1C", &[1]);
287        let error = derive_codec_string(Codec::Av1, &short_av1).unwrap_err();
288        assert_eq!(error.kind(), ErrorKind::MalformedMedia);
289    }
290
291    #[test]
292    fn derives_the_vp8_codec_string_without_a_configuration_record() {
293        let derived = derive_codec_string(Codec::Vp8, &[]).unwrap();
294        assert_eq!(derived.codec_string, "vp8");
295        assert_eq!(derived.profile, CodecProfile::Vp8);
296    }
297
298    #[test]
299    fn rejects_unsupported_codecs() {
300        let error = derive_codec_string(Codec::Aac, &boxed(b"esds", &[0; 4])).unwrap_err();
301        assert_eq!(error.kind(), ErrorKind::Unsupported);
302        let error =
303            derive_codec_string(Codec::UncompressedVideo, &boxed(b"hvcC", &[0; 13])).unwrap_err();
304        assert_eq!(error.kind(), ErrorKind::Unsupported);
305    }
306
307    fn vpcc(version: u8, record: &[u8]) -> Vec<u8> {
308        let mut payload = vec![version, 0, 0, 0];
309        payload.extend_from_slice(record);
310        boxed(b"vpcC", &payload)
311    }
312
313    #[test]
314    fn reads_a_version_1_vpcc_box_and_derives_its_codec_string() {
315        // Profile 0, level 3.1, 8-bit, colocated 4:2:0, limited range,
316        // BT.709 color description, no initialization data.
317        let bytes = vpcc(1, &[0, 31, 0x82, 1, 1, 1, 0, 0]);
318        let config = Vp9CodecConfig::parse(&bytes).unwrap();
319        assert_eq!(
320            config,
321            Vp9CodecConfig {
322                profile: 0,
323                level: 31,
324                bit_depth: 8,
325                chroma_subsampling: 1,
326                video_full_range: false,
327                color_primaries: 1,
328                transfer_characteristics: 1,
329                matrix_coefficients: 1,
330            }
331        );
332        assert!(config.is_420());
333        let derived = derive_codec_string(Codec::Vp9, &bytes).unwrap();
334        assert_eq!(derived.codec_string, "vp09.00.31.08");
335        assert_eq!(derived.profile, CodecProfile::Vp9Profile0);
336
337        let ten_bit = vpcc(1, &[2, 0, 0xa1, 9, 16, 9, 0, 0]);
338        let config = Vp9CodecConfig::parse(&ten_bit).unwrap();
339        assert!(config.video_full_range);
340        assert_eq!(config.codec_profile().unwrap(), CodecProfile::Vp9Profile2);
341        // Level 0 is unspecified, which the codec string cannot say.
342        assert_eq!(config.codec_string(), "vp09.02.10.10");
343    }
344
345    #[test]
346    fn reads_a_version_0_vpcc_box() {
347        let bytes = vpcc(0, &[1, 20, 0x80, 0x30, 0x80, 0, 0]);
348        let config = Vp9CodecConfig::parse(&bytes).unwrap();
349        assert_eq!(config.profile, 1);
350        assert_eq!(config.level, 20);
351        assert_eq!(config.bit_depth, 8);
352        assert_eq!(config.chroma_subsampling, 3);
353        assert!(config.video_full_range);
354    }
355
356    #[test]
357    fn reads_webm_codec_private_features() {
358        assert_eq!(
359            Vp9CodecConfig::parse(&[]).unwrap(),
360            Vp9CodecConfig::default()
361        );
362        let config = Vp9CodecConfig::parse(&[1, 1, 0, 2, 1, 40, 3, 1, 8, 4, 1, 1]).unwrap();
363        assert_eq!((config.profile, config.level, config.bit_depth), (0, 40, 8));
364        assert_eq!(config.chroma_subsampling, 1);
365        // Unknown features are skipped.
366        assert_eq!(
367            Vp9CodecConfig::parse(&[9, 2, 7, 7, 1, 1, 2])
368                .unwrap()
369                .profile,
370            2
371        );
372    }
373
374    #[test]
375    fn rejects_malformed_vp9_configurations() {
376        for bytes in [
377            vec![1, 1],
378            vec![1, 2, 0, 0],
379            vec![3, 1, 9],
380            vpcc(1, &[0, 10]),
381            vpcc(2, &[0, 10, 0x80, 1, 1, 1, 0, 0]),
382            vpcc(1, &[4, 10, 0x80, 1, 1, 1, 0, 0]),
383            vpcc(1, &[0, 10, 0x90, 1, 1, 1, 0, 0]),
384        ] {
385            let error = Vp9CodecConfig::parse(&bytes).unwrap_err();
386            assert_eq!(error.kind(), ErrorKind::MalformedMedia, "{bytes:?}");
387        }
388        let config = Vp9CodecConfig::parse(&[1, 1, 0, 2, 1, 41, 4, 1, 1]).unwrap();
389        assert_eq!(Vp9CodecConfig::parse(&config.to_vpcc()).unwrap(), config);
390        let mut inconsistent = vpcc(1, &[0, 10, 0x80, 1, 1, 1, 0, 0]);
391        inconsistent[3] += 1;
392        assert!(Vp9CodecConfig::parse(&inconsistent).is_err());
393    }
394}