Skip to main content

arcly_stream/codec/
vvc.rs

1//! VVC (H.266) bitstream helpers: NAL classification and SPS resolution
2//! extraction. Shares the Annex-B scanner and conformance-crop math with the
3//! other NAL codecs.
4//!
5//! The SPS parser handles both carriages of the profile-tier-level block: when
6//! `sps_ptl_dpb_hrd_params_present_flag == 1` it parses `profile_tier_level()`
7//! (extracting `general_profile_idc` / `general_tier_flag` / `general_level_idc`
8//! and stepping over the variable `general_constraint_info()` by bit count) to
9//! reach the dimension fields; when absent, `profile`/`level`/`tier` report `0`.
10//! Random-access detection — the signal the engine needs for GOP caching and
11//! segmentation — works for all VVC streams.
12
13use super::bitreader::BitReader;
14use super::nal::{conformance_dims, iter_nals, unescape_rbsp};
15use super::parser::{CodecParser, VideoParams};
16use crate::CodecId;
17
18/// NAL unit type for a video parameter set.
19pub const NAL_VPS: u8 = 14;
20/// NAL unit type for a sequence parameter set.
21pub const NAL_SPS: u8 = 15;
22/// NAL unit type for a picture parameter set.
23pub const NAL_PPS: u8 = 16;
24
25/// VVC NAL unit type, from `nuh_unit_type` in the 2-byte NAL header.
26fn nal_type(nal: &[u8]) -> Option<u8> {
27    nal.get(1).map(|b| (b >> 3) & 0x1F)
28}
29
30/// VVC IRAP VCL NAL types: IDR_W_RADL=7, IDR_N_LP=8, CRA_NUT=9.
31fn is_irap(t: u8) -> bool {
32    (7..=9).contains(&t)
33}
34
35/// Number of fixed `general_constraint_info()` flag bits when `gci_present_flag`
36/// is set — the constraint-flag block from `gci_intra_only_constraint_flag`
37/// through `gci_no_virtual_boundaries_constraint_flag` (ITU-T H.266 §7.3.3.2),
38/// preceding `gci_num_additional_bits`.
39const GCI_FIXED_FLAG_BITS: usize = 71;
40
41/// Parse `profile_tier_level(profileTierPresentFlag = 1, max_sublayers_minus1)`
42/// (ITU-T H.266 §7.3.3.1), returning `(profile, tier, level)` and leaving the
43/// reader positioned at the field that follows. The variable
44/// `general_constraint_info()` is stepped over by bit count, not interpreted.
45fn parse_profile_tier_level(r: &mut BitReader, max_sublayers_minus1: u32) -> Option<(u8, u8, u8)> {
46    let general_profile_idc = r.read_bits(7)? as u8;
47    let general_tier_flag = r.read_bit()? as u8;
48    let general_level_idc = r.read_bits(8)? as u8;
49    let _ptl_frame_only_constraint_flag = r.read_bit()?;
50    let _ptl_multilayer_enabled_flag = r.read_bit()?;
51
52    // general_constraint_info() (profileTierPresentFlag == 1).
53    if r.read_bit()? == 1 {
54        // gci_present_flag: a fixed flag block, then num_additional_bits more.
55        r.skip_bits(GCI_FIXED_FLAG_BITS)?;
56        let gci_num_additional_bits = r.read_bits(8)? as usize;
57        r.skip_bits(gci_num_additional_bits)?;
58    }
59    r.align_to_byte(); // gci_alignment_zero_bit(s)
60
61    // ptl_sublayer_level_present_flag[ MaxNumSubLayersMinus1-1 .. 0 ].
62    let mut sublayer_present = vec![false; max_sublayers_minus1 as usize];
63    for i in (0..max_sublayers_minus1 as usize).rev() {
64        sublayer_present[i] = r.read_bit()? == 1;
65    }
66    r.align_to_byte(); // ptl_reserved_zero_bit(s)
67    for present in sublayer_present.iter().rev() {
68        if *present {
69            let _sublayer_level_idc = r.read_bits(8)?;
70        }
71    }
72
73    // profileTierPresentFlag == 1 → sub-profiles.
74    let num_sub_profiles = r.read_bits(8)?;
75    for _ in 0..num_sub_profiles {
76        let _general_sub_profile_idc = r.read_bits(32)?;
77    }
78
79    Some((general_profile_idc, general_tier_flag, general_level_idc))
80}
81
82/// Parse a VVC SPS NAL (including its 2-byte header) into [`VideoParams`].
83fn parse_sps(nal: &[u8]) -> Option<VideoParams> {
84    if nal.len() < 3 || nal_type(nal)? != NAL_SPS {
85        return None;
86    }
87    let rbsp = unescape_rbsp(&nal[2..]);
88    let mut r = BitReader::new(&rbsp);
89
90    let _sps_seq_parameter_set_id = r.read_bits(4)?;
91    let _sps_video_parameter_set_id = r.read_bits(4)?;
92    let sps_max_sublayers_minus1 = r.read_bits(3)?;
93    let chroma_format_idc = r.read_bits(2)?;
94    let _sps_log2_ctu_size_minus5 = r.read_bits(2)?;
95    let ptl_present = r.read_bit()?;
96    let (profile, tier, level) = if ptl_present == 1 {
97        parse_profile_tier_level(&mut r, sps_max_sublayers_minus1)?
98    } else {
99        (0, 0, 0)
100    };
101
102    let _sps_gdr_enabled_flag = r.read_bit()?;
103    let sps_ref_pic_resampling = r.read_bit()?;
104    if sps_ref_pic_resampling == 1 {
105        let _sps_res_change_in_clvs_allowed = r.read_bit()?;
106    }
107
108    let width_luma = r.read_ue()?;
109    let height_luma = r.read_ue()?;
110    let (mut l, mut rr, mut t, mut b) = (0, 0, 0, 0);
111    if r.read_bit()? == 1 {
112        l = r.read_ue()?;
113        rr = r.read_ue()?;
114        t = r.read_ue()?;
115        b = r.read_ue()?;
116    }
117    let (width, height) = conformance_dims(width_luma, height_luma, chroma_format_idc, l, rr, t, b);
118
119    Some(VideoParams {
120        width,
121        height,
122        profile,
123        level,
124        tier,
125        bit_depth: 8,
126    })
127}
128
129/// Build the **VvcDecoderConfigurationRecord** (`vvcC` box body) from an Annex-B
130/// config access unit carrying the VVC parameter sets, alongside the parsed
131/// [`VideoParams`]. This is what the fMP4 muxer wraps in a `vvcC` box inside the
132/// `vvc1` sample entry.
133///
134/// Profile/tier/level come from [`Vvc::parse_config`]: real values when the SPS
135/// carries a PTL block, or `0` when it does not. Chroma is assumed 8-bit 4:2:0
136/// (the SPS color-config fields are read later in the SPS than the muxer needs).
137/// The record embeds the actual VPS/SPS/PPS NAL units and fixes `num_sublayers
138/// = 1` with a 1-byte constraint-info field so every field stays byte-aligned.
139/// Returns `None` when no SPS is present.
140pub fn vvcc_config_record(data: &[u8]) -> Option<(VideoParams, Vec<u8>)> {
141    let params = Vvc::parse_config(data)?;
142
143    // Collect parameter sets, preserving order within each type.
144    let mut vps: Vec<&[u8]> = Vec::new();
145    let mut sps: Vec<&[u8]> = Vec::new();
146    let mut pps: Vec<&[u8]> = Vec::new();
147    for nal in iter_nals(data) {
148        match nal_type(nal) {
149            Some(NAL_VPS) => vps.push(nal),
150            Some(NAL_SPS) => sps.push(nal),
151            Some(NAL_PPS) => pps.push(nal),
152            _ => {}
153        }
154    }
155    if sps.is_empty() {
156        return None;
157    }
158
159    let chroma_format_idc = 1u8; // 4:2:0 (VideoParams carries no chroma)
160    let bit_depth_minus8 = params.bit_depth.saturating_sub(8);
161
162    // Fixed prefix (ptl_present): config flags + the byte-aligned VvcPTLRecord(1).
163    let mut rec = vec![
164        0xFF,                              // reserved(5) | LengthSizeMinusOne=3 | ptl_present_flag=1
165        0x00,                              // ols_idx(9)=0 | num_sublayers(3)=1 | … (high bits)
166        0x10 | (chroma_format_idc & 0x03), // … | constant_frame_rate(2)=0 | chroma(2)
167        (bit_depth_minus8 << 5) | 0x1F,    // bit_depth_minus8(3) | reserved(5)
168        0x01, // VvcPTLRecord: reserved(2) | num_bytes_constraint_info = 1
169        ((params.profile & 0x7F) << 1) | (params.tier & 1), // profile(7) | tier(1)
170        params.level, // general_level_idc
171        0x00, // frame_only(1) | multilayer(1) | general_constraint_info(6)
172        0x00, // num_ptl_sub_profiles = 0
173    ];
174    rec.extend_from_slice(&(params.width as u16).to_be_bytes()); // max_picture_width
175    rec.extend_from_slice(&(params.height as u16).to_be_bytes()); // max_picture_height
176    rec.extend_from_slice(&0u16.to_be_bytes()); // avg_frame_rate
177
178    // NAL-unit arrays (VPS/SPS/PPS present), each a complete NAL incl. header.
179    let groups = [(NAL_VPS, &vps), (NAL_SPS, &sps), (NAL_PPS, &pps)];
180    let present: Vec<_> = groups.iter().filter(|(_, v)| !v.is_empty()).collect();
181    rec.push(present.len() as u8); // num_of_arrays
182    for (t, nals) in present {
183        rec.push(0x80 | (t & 0x1F)); // array_completeness=1 | reserved(2) | NAL_unit_type(5)
184        rec.extend_from_slice(&(nals.len() as u16).to_be_bytes());
185        for nal in nals.iter() {
186            rec.extend_from_slice(&(nal.len() as u16).to_be_bytes());
187            rec.extend_from_slice(nal);
188        }
189    }
190    Some((params, rec))
191}
192
193/// [`CodecParser`] implementation for VVC / H.266.
194pub struct Vvc;
195
196impl CodecParser for Vvc {
197    const CODEC: CodecId = CodecId::VVC;
198
199    fn parse_config(data: &[u8]) -> Option<VideoParams> {
200        iter_nals(data).find_map(parse_sps)
201    }
202
203    fn is_random_access_point(data: &[u8]) -> bool {
204        iter_nals(data).any(|n| nal_type(n).is_some_and(|t| t <= 11 && is_irap(t)))
205    }
206
207    fn carries_config(data: &[u8]) -> bool {
208        iter_nals(data)
209            .any(|n| nal_type(n).is_some_and(|t| matches!(t, NAL_VPS | NAL_SPS | NAL_PPS)))
210    }
211
212    fn hls_codec_string(p: &VideoParams) -> String {
213        format!("vvc1.{}.L{}", p.profile, p.level)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::codec::testutil::BitWriter;
221
222    /// VVC SPS NAL for 1920x1080, 4:2:0, with the PTL block absent.
223    fn sps_1920x1080() -> Vec<u8> {
224        let mut w = BitWriter::default();
225        w.bits(0, 4); // sps_seq_parameter_set_id
226        w.bits(0, 4); // sps_video_parameter_set_id
227        w.bits(0, 3); // sps_max_sublayers_minus1
228        w.bits(1, 2); // sps_chroma_format_idc = 1 (4:2:0)
229        w.bits(0, 2); // sps_log2_ctu_size_minus5
230        w.bit(0); // sps_ptl_dpb_hrd_params_present_flag = 0
231        w.bit(0); // sps_gdr_enabled_flag
232        w.bit(0); // sps_ref_pic_resampling_enabled_flag
233        w.ue(1920); // sps_pic_width_max_in_luma_samples
234        w.ue(1080); // sps_pic_height_max_in_luma_samples
235        w.bit(0); // sps_conformance_window_flag
236        let mut nal = vec![0x00u8, 0x79]; // NAL header: nuh_unit_type 15 (SPS)
237        nal.extend_from_slice(&w.bytes());
238        nal
239    }
240
241    /// VVC SPS for 1280x720 with the profile-tier-level block **present**.
242    /// `gci_present` selects whether `general_constraint_info` carries its fixed
243    /// flag block (true) or is empty (false).
244    fn sps_with_ptl(profile: u32, tier: u8, level: u32, gci_present: bool) -> Vec<u8> {
245        let mut w = BitWriter::default();
246        w.bits(0, 4); // sps_seq_parameter_set_id
247        w.bits(0, 4); // sps_video_parameter_set_id
248        w.bits(0, 3); // sps_max_sublayers_minus1 = 0
249        w.bits(1, 2); // chroma_format_idc = 4:2:0
250        w.bits(0, 2); // sps_log2_ctu_size_minus5
251        w.bit(1); // sps_ptl_dpb_hrd_params_present_flag = 1
252                  // profile_tier_level(1, 0)
253        w.bits(profile, 7); // general_profile_idc
254        w.bit(tier); // general_tier_flag
255        w.bits(level, 8); // general_level_idc
256        w.bit(0); // ptl_frame_only_constraint_flag
257        w.bit(0); // ptl_multilayer_enabled_flag
258        w.bit(gci_present as u8); // gci_present_flag
259        if gci_present {
260            for _ in 0..71 {
261                w.bit(0); // fixed general_constraint_info flag block
262            }
263            w.bits(0, 8); // gci_num_additional_bits = 0
264        }
265        w.align(); // gci_alignment_zero_bit(s)
266                   // no sublayer flags (max_sublayers_minus1 = 0); align is a no-op
267        w.align();
268        w.bits(0, 8); // ptl_num_sub_profiles = 0
269                      // back in the SPS
270        w.bit(0); // sps_gdr_enabled_flag
271        w.bit(0); // sps_ref_pic_resampling_enabled_flag
272        w.ue(1280); // sps_pic_width_max_in_luma_samples
273        w.ue(720); // sps_pic_height_max_in_luma_samples
274        w.bit(0); // sps_conformance_window_flag
275        let mut nal = vec![0x00u8, 0x79]; // NAL header: nuh_unit_type 15 (SPS)
276        nal.extend_from_slice(&w.bytes());
277        nal
278    }
279
280    #[test]
281    fn parse_sps_extracts_resolution() {
282        let p = parse_sps(&sps_1920x1080()).expect("parse");
283        assert_eq!((p.width, p.height), (1920, 1080));
284        // PTL absent → profile/level/tier report 0.
285        assert_eq!((p.profile, p.level, p.tier), (0, 0, 0));
286    }
287
288    #[test]
289    fn parse_sps_with_ptl_extracts_profile_level_and_dims() {
290        // gci_present_flag = 0 (the common case).
291        let p = parse_sps(&sps_with_ptl(1, 0, 51, false)).expect("parse no-gci");
292        assert_eq!((p.width, p.height), (1280, 720));
293        assert_eq!((p.profile, p.level, p.tier), (1, 51, 0));
294
295        // gci_present_flag = 1 with a full fixed constraint block + high tier.
296        let p = parse_sps(&sps_with_ptl(1, 1, 51, true)).expect("parse gci");
297        assert_eq!((p.width, p.height), (1280, 720));
298        assert_eq!((p.profile, p.level, p.tier), (1, 51, 1));
299        assert_eq!(Vvc::hls_codec_string(&p), "vvc1.1.L51");
300    }
301
302    #[test]
303    fn classifies_irap_and_config() {
304        let mut au = vec![0, 0, 0, 1];
305        au.extend_from_slice(&sps_1920x1080());
306        // IDR_W_RADL (type 7): byte1 = (7<<3)|1 = 0x39.
307        au.extend_from_slice(&[0, 0, 0, 1, 0x00, 0x39, 0xAA]);
308
309        assert!(Vvc::is_random_access_point(&au));
310        assert!(Vvc::carries_config(&au));
311        assert_eq!(Vvc::parse_config(&au).expect("params").width, 1920);
312
313        // TRAIL (type 0): byte1 = 0x01 → not a RAP.
314        assert!(!Vvc::is_random_access_point(&[
315            0, 0, 0, 1, 0x00, 0x01, 0xAA
316        ]));
317    }
318
319    #[test]
320    fn vvcc_record_embeds_param_sets_and_dims() {
321        // A VPS (type 14, byte1 = (14<<3)|1 = 0x71) plus the SPS.
322        let vps = [0x00u8, 0x71, 0xAB, 0xCD];
323        let sps = sps_1920x1080();
324        let mut au = vec![0, 0, 0, 1];
325        au.extend_from_slice(&vps);
326        au.extend_from_slice(&[0, 0, 0, 1]);
327        au.extend_from_slice(&sps);
328
329        let (params, rec) = vvcc_config_record(&au).expect("record");
330        assert_eq!((params.width, params.height), (1920, 1080));
331        assert_eq!(rec[0], 0xFF); // LengthSizeMinusOne=3 | ptl_present=1
332        assert_eq!(rec[2] & 0x03, 1); // chroma_format_idc = 4:2:0
333
334        // num_of_arrays = 2 (VPS + SPS). It sits after the fixed 18-byte prefix
335        // (1 + 2 + 1 + 5 PTL + 6 dims/fps = 15)… assert structurally via search:
336        assert!(rec.windows(vps.len()).any(|w| w == vps), "VPS embedded");
337        assert!(rec.windows(sps.len()).any(|w| w == sps), "SPS embedded");
338        // The SPS array marker: array_completeness=1 | NAL_SPS(15) = 0x8F.
339        assert!(rec.contains(&0x8F));
340    }
341}