Skip to main content

arcly_stream/codec/
h265.rs

1//! H.265 (HEVC) bitstream helpers: NAL classification and SPS resolution
2//! extraction. Shares the Annex-B scanner and conformance-crop math with H.264.
3
4use super::bitreader::BitReader;
5use super::nal::{conformance_dims, iter_nals, unescape_rbsp};
6use super::parser::{CodecParser, VideoParams};
7use crate::CodecId;
8
9/// NAL unit type for a video parameter set.
10pub const NAL_VPS: u8 = 32;
11/// NAL unit type for a sequence parameter set.
12pub const NAL_SPS: u8 = 33;
13/// NAL unit type for a picture parameter set.
14pub const NAL_PPS: u8 = 34;
15
16/// HEVC NAL unit type, read from the 2-byte NAL header.
17fn nal_type(nal: &[u8]) -> Option<u8> {
18    nal.first().map(|b| (b >> 1) & 0x3F)
19}
20
21/// HEVC IRAP (random-access) VCL NAL types are `16..=23`
22/// (BLA/IDR/CRA). A frame containing any such VCL NAL is a random-access point.
23fn is_irap(t: u8) -> bool {
24    (16..=23).contains(&t)
25}
26
27/// Skip the HEVC `profile_tier_level`, capturing `(profile_idc, tier, level_idc)`.
28fn read_profile_tier_level(r: &mut BitReader, max_sub_layers_minus1: u32) -> Option<(u8, u8, u8)> {
29    // general profile portion is 88 bits, then 8-bit general_level_idc.
30    let _general_profile_space = r.read_bits(2)?;
31    let general_tier_flag = r.read_bit()? as u8;
32    let general_profile_idc = r.read_bits(5)? as u8;
33    let _compat = r.read_bits(32)?; // 32 compatibility flags
34    let _constraint_hi = r.read_bits(32)?; // 48 constraint/reserved/inbld bits …
35    let _constraint_lo = r.read_bits(16)?; // … = 48 total
36    let general_level_idc = r.read_bits(8)? as u8;
37
38    // Sub-layer present flags.
39    let mut sub_profile = [false; 8];
40    let mut sub_level = [false; 8];
41    for i in 0..max_sub_layers_minus1 as usize {
42        sub_profile[i] = r.read_bit()? == 1;
43        sub_level[i] = r.read_bit()? == 1;
44    }
45    if max_sub_layers_minus1 > 0 {
46        for _ in max_sub_layers_minus1..8 {
47            let _reserved = r.read_bits(2)?;
48        }
49    }
50    for i in 0..max_sub_layers_minus1 as usize {
51        if sub_profile[i] {
52            // 88-bit sub-layer profile block.
53            r.read_bits(32)?;
54            r.read_bits(32)?;
55            r.read_bits(24)?;
56        }
57        if sub_level[i] {
58            r.read_bits(8)?;
59        }
60    }
61    Some((general_profile_idc, general_tier_flag, general_level_idc))
62}
63
64/// Parse an HEVC SPS NAL (including its 2-byte header) into [`VideoParams`].
65fn parse_sps(nal: &[u8]) -> Option<VideoParams> {
66    if nal.len() < 3 || nal_type(nal)? != NAL_SPS {
67        return None;
68    }
69    let rbsp = unescape_rbsp(&nal[2..]);
70    let mut r = BitReader::new(&rbsp);
71
72    let _sps_video_parameter_set_id = r.read_bits(4)?;
73    let max_sub_layers_minus1 = r.read_bits(3)?;
74    let _sps_temporal_id_nesting = r.read_bit()?;
75
76    let (profile, tier, level) = read_profile_tier_level(&mut r, max_sub_layers_minus1)?;
77
78    let _sps_seq_parameter_set_id = r.read_ue()?;
79    let chroma_format_idc = r.read_ue()?;
80    if chroma_format_idc == 3 {
81        let _separate_colour_plane = r.read_bit()?;
82    }
83    let width_luma = r.read_ue()?;
84    let height_luma = r.read_ue()?;
85
86    let (mut l, mut rr, mut t, mut b) = (0, 0, 0, 0);
87    if r.read_bit()? == 1 {
88        l = r.read_ue()?;
89        rr = r.read_ue()?;
90        t = r.read_ue()?;
91        b = r.read_ue()?;
92    }
93    let (width, height) = conformance_dims(width_luma, height_luma, chroma_format_idc, l, rr, t, b);
94
95    Some(VideoParams {
96        width,
97        height,
98        profile,
99        level,
100        tier,
101        bit_depth: 8,
102    })
103}
104
105/// [`CodecParser`] implementation for H.265 / HEVC.
106pub struct H265;
107
108impl CodecParser for H265 {
109    const CODEC: CodecId = CodecId::H265;
110
111    fn parse_config(data: &[u8]) -> Option<VideoParams> {
112        iter_nals(data).find_map(parse_sps)
113    }
114
115    fn is_random_access_point(data: &[u8]) -> bool {
116        iter_nals(data).any(|n| nal_type(n).is_some_and(|t| t <= 31 && is_irap(t)))
117    }
118
119    fn carries_config(data: &[u8]) -> bool {
120        iter_nals(data)
121            .any(|n| nal_type(n).is_some_and(|t| matches!(t, NAL_VPS | NAL_SPS | NAL_PPS)))
122    }
123
124    fn hls_codec_string(p: &VideoParams) -> String {
125        // hvc1.{profile_space + profile_idc}.{compat}.{tier}{level}.{constraints}
126        let tier = if p.tier == 1 { 'H' } else { 'L' };
127        format!("hvc1.{}.6.{}{}.B0", p.profile, tier, p.level)
128    }
129}
130
131/// Build an `hvcC` (HEVCDecoderConfigurationRecord, ISO/IEC 14496-15 §8.3.3.1)
132/// from an Annex-B VPS/SPS/PPS config access unit, returning it alongside the
133/// parsed [`VideoParams`]. Returns `None` if no SPS is present or its
134/// profile_tier_level can't be located.
135///
136/// The 12-byte general `profile_tier_level` is copied verbatim from the SPS RBSP
137/// (after un-escaping), so the profile/compatibility/constraint/level fields are
138/// exact rather than re-derived. Used by the fMP4/CMAF muxer.
139pub fn hvcc_config_record(data: &[u8]) -> Option<(VideoParams, Vec<u8>)> {
140    let params = H265::parse_config(data)?;
141
142    let mut vps: Vec<&[u8]> = Vec::new();
143    let mut sps: Vec<&[u8]> = Vec::new();
144    let mut pps: Vec<&[u8]> = Vec::new();
145    for nal in iter_nals(data) {
146        match nal_type(nal) {
147            Some(NAL_VPS) => vps.push(nal),
148            Some(NAL_SPS) => sps.push(nal),
149            Some(NAL_PPS) => pps.push(nal),
150            _ => {}
151        }
152    }
153    let first_sps = *sps.first()?;
154
155    // SPS RBSP layout: sps_vps_id(4) + max_sub_layers_minus1(3) + nesting(1) = 1
156    // byte, then the 12-byte general profile_tier_level.
157    let rbsp = unescape_rbsp(first_sps.get(2..)?);
158    let ptl = rbsp.get(1..13)?; // profile(1) | compat(4) | constraint(6) | level(1)
159
160    let chroma_format_idc = 1u8; // 4:2:0 (VideoParams carries no chroma)
161    let bit_depth_minus8 = params.bit_depth.max(8).saturating_sub(8);
162
163    let mut rec = Vec::with_capacity(32 + first_sps.len());
164    rec.push(1); // configurationVersion
165    rec.extend_from_slice(ptl); // 12-byte general profile_tier_level
166    rec.extend_from_slice(&[0xF0, 0x00]); // reserved | min_spatial_segmentation_idc=0
167    rec.push(0xFC); // reserved(6) | parallelismType(2)=0
168    rec.push(0xFC | (chroma_format_idc & 0x03)); // reserved(6) | chromaFormat(2)
169    rec.push(0xF8 | (bit_depth_minus8 & 0x07)); // reserved(5) | bitDepthLumaMinus8(3)
170    rec.push(0xF8 | (bit_depth_minus8 & 0x07)); // reserved(5) | bitDepthChromaMinus8(3)
171    rec.extend_from_slice(&0u16.to_be_bytes()); // avgFrameRate
172
173    // numTemporalLayers=1 | lengthSizeMinusOne=3 (constantFrameRate=0, temporalIdNested=0).
174    rec.push((1 << 3) | 0x03);
175
176    // NAL-unit arrays (VPS/SPS/PPS), each a complete NAL incl. its 2-byte header.
177    let groups = [(NAL_VPS, &vps), (NAL_SPS, &sps), (NAL_PPS, &pps)];
178    let present: Vec<_> = groups.iter().filter(|(_, v)| !v.is_empty()).collect();
179    rec.push(present.len() as u8); // numOfArrays
180    for (t, nals) in present {
181        rec.push(0x80 | (t & 0x3F)); // array_completeness=1 | reserved(1)=0 | type(6)
182        rec.extend_from_slice(&(nals.len() as u16).to_be_bytes());
183        for nal in nals.iter() {
184            rec.extend_from_slice(&(nal.len() as u16).to_be_bytes());
185            rec.extend_from_slice(nal);
186        }
187    }
188    Some((params, rec))
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::codec::testutil::BitWriter;
195
196    /// Synthesize an HEVC SPS NAL for 1920x1080 (Main, level 4.0), 4:2:0 with a
197    /// 4-row conformance crop (coded 1088 → displayed 1080).
198    fn sps_1920x1080() -> Vec<u8> {
199        let mut w = BitWriter::default();
200        w.bits(0, 4); // sps_video_parameter_set_id
201        w.bits(0, 3); // sps_max_sub_layers_minus1 = 0 (no sub-layer loops)
202        w.bit(0); // sps_temporal_id_nesting_flag
203                  // profile_tier_level (96 bits): profile_space(2) tier(1) idc(5) …
204        w.bits(0, 2); // general_profile_space
205        w.bit(0); // general_tier_flag = 0 (Main tier)
206        w.bits(1, 5); // general_profile_idc = 1 (Main)
207        w.bits(0, 32); // compatibility flags
208        w.bits(0, 32); // constraint/reserved (hi)
209        w.bits(0, 16); // constraint/reserved (lo) → 48 total
210        w.bits(120, 8); // general_level_idc = 120 (level 4.0)
211        w.ue(0); // sps_seq_parameter_set_id
212        w.ue(1); // chroma_format_idc = 1 (4:2:0)
213        w.ue(1920); // pic_width_in_luma_samples
214        w.ue(1088); // pic_height_in_luma_samples
215        w.bit(1); // conformance_window_flag
216        w.ue(0); // conf_win_left_offset
217        w.ue(0); // conf_win_right_offset
218        w.ue(0); // conf_win_top_offset
219        w.ue(2); // conf_win_bottom_offset → 1088 - 2*2 = 1084? see below
220        let mut nal = vec![0x42u8, 0x01]; // NAL header: type 33 (SPS), tid+1=1
221        nal.extend_from_slice(&w.bytes());
222        nal
223    }
224
225    #[test]
226    fn parse_sps_extracts_resolution() {
227        let p = parse_sps(&sps_1920x1080()).expect("parse");
228        // 4:2:0 crop unit Y = 2, bottom offset 2 → 1088 - 2*2 = 1084.
229        assert_eq!((p.width, p.height), (1920, 1084));
230        assert_eq!((p.profile, p.tier, p.level), (1, 0, 120));
231    }
232
233    #[test]
234    fn hvcc_config_record_copies_ptl_and_lists_arrays() {
235        let mut au = vec![0, 0, 0, 1, 0x40, 0x01, 0xAA]; // VPS (type 32)
236        au.extend_from_slice(&[0, 0, 0, 1]);
237        au.extend_from_slice(&sps_1920x1080()); // SPS (type 33)
238        au.extend_from_slice(&[0, 0, 0, 1, 0x44, 0x01, 0xBB]); // PPS (type 34)
239
240        let (params, rec) = hvcc_config_record(&au).expect("hvcC built");
241        assert_eq!((params.width, params.profile, params.level), (1920, 1, 120));
242
243        assert_eq!(rec[0], 1, "configurationVersion");
244        assert_eq!(rec[1] & 0x1F, 1, "general_profile_idc copied from SPS");
245        assert_eq!(rec[12], 120, "general_level_idc copied from SPS");
246        // Fixed header is 22 bytes; numOfArrays follows. VPS+SPS+PPS = 3.
247        assert_eq!(rec[22], 3, "three NAL arrays (VPS/SPS/PPS)");
248    }
249
250    #[test]
251    fn hvcc_config_record_requires_an_sps() {
252        // VPS only — no SPS — cannot build a config record.
253        assert!(hvcc_config_record(&[0, 0, 0, 1, 0x40, 0x01, 0xAA]).is_none());
254    }
255
256    #[test]
257    fn classifies_irap_and_config() {
258        let mut au = vec![0, 0, 0, 1];
259        au.extend_from_slice(&sps_1920x1080());
260        // IDR_W_RADL VCL NAL (type 19): byte0 = 19<<1 = 0x26.
261        au.extend_from_slice(&[0, 0, 0, 1, 0x26, 0x01, 0xAA]);
262
263        assert!(H265::is_random_access_point(&au));
264        assert!(H265::carries_config(&au));
265        let p = H265::parse_config(&au).expect("params");
266        assert_eq!(p.width, 1920);
267        assert_eq!(H265::hls_codec_string(&p), "hvc1.1.6.L120.B0");
268
269        // A TRAIL_R slice (type 1) alone is not a RAP.
270        assert!(!H265::is_random_access_point(&[
271            0, 0, 0, 1, 0x02, 0x01, 0xAA
272        ]));
273    }
274}