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//! Resolution extraction is implemented for SPS that signal
6//! `sps_ptl_dpb_hrd_params_present_flag == 0` (the profile-tier-level block is
7//! absent, so the dimension fields are at a fixed offset). When the PTL block is
8//! present its variable-length `general_constraints_info` cannot be skipped
9//! reliably without a fuller parser, so [`parse_config`](Vvc::parse_config)
10//! returns `None` there. **Random-access detection — the signal the engine needs
11//! for GOP caching and 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/// Parse a VVC SPS NAL (including its 2-byte header) into [`VideoParams`].
36fn parse_sps(nal: &[u8]) -> Option<VideoParams> {
37    if nal.len() < 3 || nal_type(nal)? != NAL_SPS {
38        return None;
39    }
40    let rbsp = unescape_rbsp(&nal[2..]);
41    let mut r = BitReader::new(&rbsp);
42
43    let _sps_seq_parameter_set_id = r.read_bits(4)?;
44    let _sps_video_parameter_set_id = r.read_bits(4)?;
45    let _sps_max_sublayers_minus1 = r.read_bits(3)?;
46    let chroma_format_idc = r.read_bits(2)?;
47    let _sps_log2_ctu_size_minus5 = r.read_bits(2)?;
48    let ptl_present = r.read_bit()?;
49    if ptl_present == 1 {
50        // profile_tier_level present → variable-length general_constraints_info
51        // precedes the dimension fields; not parsed here.
52        return None;
53    }
54
55    let _sps_gdr_enabled_flag = r.read_bit()?;
56    let sps_ref_pic_resampling = r.read_bit()?;
57    if sps_ref_pic_resampling == 1 {
58        let _sps_res_change_in_clvs_allowed = r.read_bit()?;
59    }
60
61    let width_luma = r.read_ue()?;
62    let height_luma = r.read_ue()?;
63    let (mut l, mut rr, mut t, mut b) = (0, 0, 0, 0);
64    if r.read_bit()? == 1 {
65        l = r.read_ue()?;
66        rr = r.read_ue()?;
67        t = r.read_ue()?;
68        b = r.read_ue()?;
69    }
70    let (width, height) = conformance_dims(width_luma, height_luma, chroma_format_idc, l, rr, t, b);
71
72    Some(VideoParams {
73        width,
74        height,
75        profile: 0,
76        level: 0,
77        tier: 0,
78        bit_depth: 8,
79    })
80}
81
82/// [`CodecParser`] implementation for VVC / H.266.
83pub struct Vvc;
84
85impl CodecParser for Vvc {
86    const CODEC: CodecId = CodecId::VVC;
87
88    fn parse_config(data: &[u8]) -> Option<VideoParams> {
89        iter_nals(data).find_map(parse_sps)
90    }
91
92    fn is_random_access_point(data: &[u8]) -> bool {
93        iter_nals(data).any(|n| nal_type(n).is_some_and(|t| t <= 11 && is_irap(t)))
94    }
95
96    fn carries_config(data: &[u8]) -> bool {
97        iter_nals(data)
98            .any(|n| nal_type(n).is_some_and(|t| matches!(t, NAL_VPS | NAL_SPS | NAL_PPS)))
99    }
100
101    fn hls_codec_string(p: &VideoParams) -> String {
102        format!("vvc1.{}.L{}", p.profile, p.level)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::codec::testutil::BitWriter;
110
111    /// VVC SPS NAL for 1920x1080, 4:2:0, with the PTL block absent.
112    fn sps_1920x1080() -> Vec<u8> {
113        let mut w = BitWriter::default();
114        w.bits(0, 4); // sps_seq_parameter_set_id
115        w.bits(0, 4); // sps_video_parameter_set_id
116        w.bits(0, 3); // sps_max_sublayers_minus1
117        w.bits(1, 2); // sps_chroma_format_idc = 1 (4:2:0)
118        w.bits(0, 2); // sps_log2_ctu_size_minus5
119        w.bit(0); // sps_ptl_dpb_hrd_params_present_flag = 0
120        w.bit(0); // sps_gdr_enabled_flag
121        w.bit(0); // sps_ref_pic_resampling_enabled_flag
122        w.ue(1920); // sps_pic_width_max_in_luma_samples
123        w.ue(1080); // sps_pic_height_max_in_luma_samples
124        w.bit(0); // sps_conformance_window_flag
125        let mut nal = vec![0x00u8, 0x79]; // NAL header: nuh_unit_type 15 (SPS)
126        nal.extend_from_slice(&w.bytes());
127        nal
128    }
129
130    #[test]
131    fn parse_sps_extracts_resolution() {
132        let p = parse_sps(&sps_1920x1080()).expect("parse");
133        assert_eq!((p.width, p.height), (1920, 1080));
134    }
135
136    #[test]
137    fn classifies_irap_and_config() {
138        let mut au = vec![0, 0, 0, 1];
139        au.extend_from_slice(&sps_1920x1080());
140        // IDR_W_RADL (type 7): byte1 = (7<<3)|1 = 0x39.
141        au.extend_from_slice(&[0, 0, 0, 1, 0x00, 0x39, 0xAA]);
142
143        assert!(Vvc::is_random_access_point(&au));
144        assert!(Vvc::carries_config(&au));
145        assert_eq!(Vvc::parse_config(&au).expect("params").width, 1920);
146
147        // TRAIL (type 0): byte1 = 0x01 → not a RAP.
148        assert!(!Vvc::is_random_access_point(&[
149            0, 0, 0, 1, 0x00, 0x01, 0xAA
150        ]));
151    }
152}