Skip to main content

zvidlib_core/
codec_config.rs

1//! The codec configuration records more than one zvidlib crate reads: the VP9
2//! codec configuration record ([`Vp9CodecConfig`]), and the ISO-BMFF box
3//! unwrapping every `*C` record shares.
4
5use crate::{CodecProfile, Error, ErrorKind, Result};
6
7/// Returns the payload of a box given its complete bytes (size + fourcc + payload).
8#[doc(hidden)]
9pub fn box_payload<'a>(whole: &'a [u8], expected_fourcc: &[u8; 4]) -> Result<&'a [u8]> {
10    if whole.len() < 8 {
11        return Err(Error::new(
12            ErrorKind::MalformedMedia,
13            "codec configuration box is too short",
14        ));
15    }
16    if &whole[4..8] != expected_fourcc {
17        return Err(Error::new(
18            ErrorKind::MalformedMedia,
19            "codec configuration box has an unexpected fourcc",
20        ));
21    }
22    Ok(&whole[8..])
23}
24
25// --- VP9 (vpcC and WebM CodecPrivate) -------------------------------------
26
27/// A VP9 stream's codec configuration: what an MP4 `vpcC` box or a WebM
28/// `CodecPrivate` element says about it.
29///
30/// The VP9 bitstream describes itself, so this is a summary for capability
31/// discovery and codec strings rather than something decoding needs. The
32/// `vpcC` box is version 1 of the VP Codec ISO Media File Format Binding
33/// (`VPCodecConfigurationRecord`); version 0, an earlier draft some muxers
34/// still write, is read as well. WebM's `CodecPrivate` is the optional
35/// list of (ID, length, value) features of the WebM VP9 codec mapping, of
36/// which profile (1), level (2), bit depth (3) and chroma subsampling (4)
37/// are defined; any it leaves out take the profile 0 defaults.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct Vp9CodecConfig {
40    pub profile: u8,
41    /// The VP9 level times ten (`31` is level 3.1), or 0 when unspecified.
42    pub level: u8,
43    pub bit_depth: u8,
44    /// 0 and 1 are 4:2:0 (vertical and colocated chroma), 2 is 4:2:2 and
45    /// 3 is 4:4:4.
46    pub chroma_subsampling: u8,
47    pub video_full_range: bool,
48    /// ISO/IEC 23091-2 color description; 2 means unspecified.
49    pub color_primaries: u8,
50    pub transfer_characteristics: u8,
51    pub matrix_coefficients: u8,
52}
53
54impl Default for Vp9CodecConfig {
55    fn default() -> Self {
56        Self {
57            profile: 0,
58            level: 0,
59            bit_depth: 8,
60            chroma_subsampling: 0,
61            video_full_range: false,
62            color_primaries: 2,
63            transfer_characteristics: 2,
64            matrix_coefficients: 2,
65        }
66    }
67}
68
69const VP9_LEVELS: [u8; 14] = [10, 11, 20, 21, 30, 31, 40, 41, 50, 51, 52, 60, 61, 62];
70
71impl Vp9CodecConfig {
72    /// Reads a complete `vpcC` box (size, type and payload) or, when the
73    /// bytes are not one, a WebM `CodecPrivate`. Empty bytes are an empty
74    /// `CodecPrivate`.
75    pub fn parse(bytes: &[u8]) -> Result<Self> {
76        if bytes.len() >= 8 && &bytes[4..8] == b"vpcC" {
77            Self::parse_vpcc(bytes)
78        } else {
79            Self::parse_webm_codec_private(bytes)
80        }
81    }
82
83    /// Reads a complete `vpcC` box.
84    pub fn parse_vpcc(whole: &[u8]) -> Result<Self> {
85        let payload = box_payload(whole, b"vpcC")?;
86        let declared = u32::from_be_bytes(whole[..4].try_into().expect("4 bytes"));
87        if declared as usize != whole.len() {
88            return Err(malformed_config("vpcC box size is inconsistent"));
89        }
90        if payload.len() < 4 {
91            return Err(malformed_config("vpcC box is too short"));
92        }
93        let version = payload[0];
94        let record = &payload[4..];
95        let config = match version {
96            1 => {
97                if record.len() < 8 {
98                    return Err(malformed_config("vpcC record is too short"));
99                }
100                Self {
101                    profile: record[0],
102                    level: record[1],
103                    bit_depth: record[2] >> 4,
104                    chroma_subsampling: (record[2] >> 1) & 0b111,
105                    video_full_range: record[2] & 1 == 1,
106                    color_primaries: record[3],
107                    transfer_characteristics: record[4],
108                    matrix_coefficients: record[5],
109                }
110            }
111            0 => {
112                // bitDepth(4) colorSpace(4) chromaSubsampling(4)
113                // transferFunction(4) videoFullRangeFlag(1) reserved(7)
114                if record.len() < 7 {
115                    return Err(malformed_config("vpcC record is too short"));
116                }
117                Self {
118                    profile: record[0],
119                    level: record[1],
120                    bit_depth: record[2] >> 4,
121                    chroma_subsampling: record[3] >> 4,
122                    video_full_range: record[4] >> 7 == 1,
123                    ..Self::default()
124                }
125            }
126            _ => return Err(malformed_config("unsupported vpcC version")),
127        };
128        config.validate()
129    }
130
131    /// Reads a WebM VP9 `CodecPrivate`.
132    pub fn parse_webm_codec_private(bytes: &[u8]) -> Result<Self> {
133        let mut config = Self::default();
134        let mut rest = bytes;
135        while !rest.is_empty() {
136            if rest.len() < 2 {
137                return Err(malformed_config("VP9 CodecPrivate feature is truncated"));
138            }
139            let (id, length) = (rest[0], usize::from(rest[1]));
140            let value = rest
141                .get(2..2 + length)
142                .ok_or_else(|| malformed_config("VP9 CodecPrivate feature is truncated"))?;
143            if (1..=4).contains(&id) {
144                if length != 1 {
145                    return Err(malformed_config(
146                        "VP9 CodecPrivate feature has an invalid length",
147                    ));
148                }
149                match id {
150                    1 => config.profile = value[0],
151                    2 => config.level = value[0],
152                    3 => config.bit_depth = value[0],
153                    _ => config.chroma_subsampling = value[0],
154                }
155            }
156            rest = &rest[2 + length..];
157        }
158        config.validate()
159    }
160
161    fn validate(self) -> Result<Self> {
162        if self.profile > 3 {
163            return Err(malformed_config("VP9 profile must be 0 to 3"));
164        }
165        if !matches!(self.bit_depth, 8 | 10 | 12) {
166            return Err(malformed_config("VP9 bit depth must be 8, 10 or 12"));
167        }
168        if self.chroma_subsampling > 3 {
169            return Err(malformed_config("VP9 chroma subsampling is invalid"));
170        }
171        Ok(self)
172    }
173
174    /// Whether the stream is 4:2:0.
175    pub fn is_420(&self) -> bool {
176        self.chroma_subsampling <= 1
177    }
178
179    /// The normalized profile.
180    pub fn codec_profile(&self) -> Result<CodecProfile> {
181        Ok(match self.profile {
182            0 => CodecProfile::Vp9Profile0,
183            1 => CodecProfile::Vp9Profile1,
184            2 => CodecProfile::Vp9Profile2,
185            3 => CodecProfile::Vp9Profile3,
186            _ => return Err(malformed_config("VP9 profile must be 0 to 3")),
187        })
188    }
189
190    /// The complete version 1 `vpcC` box describing this configuration,
191    /// with no codec initialization data.
192    pub fn to_vpcc(&self) -> Vec<u8> {
193        let mut bytes = 20u32.to_be_bytes().to_vec();
194        bytes.extend_from_slice(b"vpcC");
195        bytes.extend_from_slice(&[1, 0, 0, 0, self.profile, self.level]);
196        bytes.push(
197            (self.bit_depth << 4)
198                | (self.chroma_subsampling << 1)
199                | u8::from(self.video_full_range),
200        );
201        bytes.extend_from_slice(&[
202            self.color_primaries,
203            self.transfer_characteristics,
204            self.matrix_coefficients,
205            0,
206            0,
207        ]);
208        bytes
209    }
210
211    /// The `vp09.PP.LL.DD` codec string of the VP9 ISO-BMFF binding, which
212    /// WebCodecs and `MediaSource` accept. An unspecified or invalid level
213    /// is written as level 1.0, since the string requires one.
214    pub fn codec_string(&self) -> String {
215        let level = if VP9_LEVELS.contains(&self.level) {
216            self.level
217        } else {
218            10
219        };
220        format!("vp09.{:02}.{level:02}.{:02}", self.profile, self.bit_depth)
221    }
222}
223
224fn malformed_config(message: &str) -> Error {
225    Error::new(ErrorKind::MalformedMedia, message)
226}