Skip to main content

arcly_stream/codec/
h264.rs

1//! H.264 (AVC) bitstream helpers: NAL iteration, Annex-B ↔ AVCC conversion,
2//! SPS resolution extraction, and the `avcC` decoder-configuration record
3//! ([`AvcConfig`]).
4
5use super::bitreader::BitReader;
6use super::nal::{iter_nals, unescape_rbsp};
7use super::parser::{CodecParser, VideoParams};
8use crate::CodecId;
9use bytes::{BufMut, Bytes, BytesMut};
10
11/// NAL unit type for a sequence parameter set.
12pub const NAL_SPS: u8 = 7;
13/// NAL unit type for a picture parameter set.
14pub const NAL_PPS: u8 = 8;
15/// NAL unit type for a coded slice of an IDR picture (keyframe).
16pub const NAL_IDR: u8 = 5;
17
18/// Iterate the NAL units of an Annex-B (`00 00 01` / `00 00 00 01`) bytestream,
19/// yielding each NAL payload **without** its start code.
20pub fn iter_nals_annexb(data: &[u8]) -> impl Iterator<Item = &[u8]> {
21    iter_nals(data)
22}
23
24/// Convert an Annex-B bytestream to AVCC (length-prefixed) with a 4-byte length.
25pub fn annexb_to_avcc(data: &[u8]) -> Bytes {
26    let mut out = BytesMut::with_capacity(data.len());
27    for nal in iter_nals(data) {
28        out.put_u32(nal.len() as u32);
29        out.put_slice(nal);
30    }
31    out.freeze()
32}
33
34/// Convert an AVCC (length-prefixed) bytestream to Annex-B (`00 00 00 01`).
35///
36/// `nal_length_size` is the prefix width in bytes (1–4), from the
37/// AVCDecoderConfigurationRecord (`lengthSizeMinusOne + 1`).
38pub fn avcc_to_annexb(data: &[u8], nal_length_size: usize) -> Option<Bytes> {
39    if !(1..=4).contains(&nal_length_size) {
40        return None;
41    }
42    let mut out = BytesMut::with_capacity(data.len() + 16);
43    let mut i = 0;
44    while i + nal_length_size <= data.len() {
45        let mut len = 0usize;
46        for _ in 0..nal_length_size {
47            len = (len << 8) | data[i] as usize;
48            i += 1;
49        }
50        if i + len > data.len() {
51            return None; // truncated NAL
52        }
53        out.put_slice(&[0, 0, 0, 1]);
54        out.put_slice(&data[i..i + len]);
55        i += len;
56    }
57    Some(out.freeze())
58}
59
60/// Parsed AVC decoder configuration (the `avcC` record from a video sequence
61/// header / `AVCDecoderConfigurationRecord`), retaining the parameter sets needed
62/// to (a) build Annex-B config for a decoder and (b) self-contain each keyframe.
63///
64/// Lives in the codec layer (not a protocol module) so every consumer — the RTMP
65/// FLV path, the transcode pipeline, and any host — shares one parser.
66#[derive(Debug, Clone, Default, PartialEq, Eq)]
67pub struct AvcConfig {
68    /// NAL length prefix width in bytes (`lengthSizeMinusOne + 1`).
69    pub nal_length_size: usize,
70    /// Sequence parameter sets.
71    pub sps: Vec<Vec<u8>>,
72    /// Picture parameter sets.
73    pub pps: Vec<Vec<u8>>,
74}
75
76impl AvcConfig {
77    /// Parse an `AVCDecoderConfigurationRecord` (`avcC`). Returns `None` if the
78    /// buffer is too short or its `configurationVersion` is not 1.
79    pub fn parse(rec: &[u8]) -> Option<Self> {
80        if rec.len() < 7 || rec[0] != 1 {
81            return None;
82        }
83        let nal_length_size = (rec[4] & 0x03) as usize + 1;
84        let mut pos = 5;
85        let num_sps = (rec.get(pos)? & 0x1F) as usize;
86        pos += 1;
87        let mut sps = Vec::with_capacity(num_sps);
88        for _ in 0..num_sps {
89            let len = u16::from_be_bytes(rec.get(pos..pos + 2)?.try_into().ok()?) as usize;
90            pos += 2;
91            sps.push(rec.get(pos..pos + len)?.to_vec());
92            pos += len;
93        }
94        let num_pps = *rec.get(pos)? as usize;
95        pos += 1;
96        let mut pps = Vec::with_capacity(num_pps);
97        for _ in 0..num_pps {
98            let len = u16::from_be_bytes(rec.get(pos..pos + 2)?.try_into().ok()?) as usize;
99            pos += 2;
100            pps.push(rec.get(pos..pos + len)?.to_vec());
101            pos += len;
102        }
103        Some(Self {
104            nal_length_size,
105            sps,
106            pps,
107        })
108    }
109
110    /// The SPS/PPS as an Annex-B access unit (each NAL prefixed with `00 00 00 01`).
111    pub fn to_annexb(&self) -> Bytes {
112        let mut out = BytesMut::new();
113        for nal in self.sps.iter().chain(self.pps.iter()) {
114            out.put_slice(&[0, 0, 0, 1]);
115            out.put_slice(nal);
116        }
117        out.freeze()
118    }
119
120    /// Rebuild an `AVCDecoderConfigurationRecord` from the stored parameter sets
121    /// (used when serving a `play` request).
122    pub fn to_avc_record(&self) -> Bytes {
123        let sps0 = self
124            .sps
125            .first()
126            .map(|s| s.as_slice())
127            .unwrap_or(&[0, 0, 0, 0]);
128        let mut out = BytesMut::new();
129        out.put_u8(1); // configurationVersion
130        out.put_u8(*sps0.get(1).unwrap_or(&0)); // AVCProfileIndication
131        out.put_u8(*sps0.get(2).unwrap_or(&0)); // profile_compatibility
132        out.put_u8(*sps0.get(3).unwrap_or(&0)); // AVCLevelIndication
133        out.put_u8(0xFF); // 6 reserved bits + lengthSizeMinusOne = 3
134        out.put_u8(0xE0 | (self.sps.len() as u8 & 0x1F));
135        for s in &self.sps {
136            out.put_u16(s.len() as u16);
137            out.put_slice(s);
138        }
139        out.put_u8(self.pps.len() as u8);
140        for p in &self.pps {
141            out.put_u16(p.len() as u16);
142            out.put_slice(p);
143        }
144        out.freeze()
145    }
146
147    /// Build an Annex-B access unit from length-prefixed (AVCC) NAL `data`,
148    /// optionally prepending SPS/PPS so a keyframe is self-decodable.
149    pub fn avcc_to_annexb(&self, avcc: &[u8], prepend_params: bool) -> Option<Bytes> {
150        let body = avcc_to_annexb(avcc, self.nal_length_size)?;
151        if prepend_params {
152            let mut out = BytesMut::with_capacity(body.len() + 64);
153            out.put(self.to_annexb());
154            out.put(body);
155            Some(out.freeze())
156        } else {
157            Some(body)
158        }
159    }
160}
161
162/// Decoded video dimensions and profile from a sequence parameter set.
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub struct SpsInfo {
165    /// Luma width in pixels (after cropping).
166    pub width: u32,
167    /// Luma height in pixels (after cropping).
168    pub height: u32,
169    /// `profile_idc` (66 = baseline, 77 = main, 100 = high, …).
170    pub profile_idc: u8,
171    /// `level_idc` × 10 (e.g. 31 = level 3.1).
172    pub level_idc: u8,
173}
174
175/// Parse a sequence parameter set NAL (including its 1-byte NAL header) and
176/// extract the coded resolution. Returns `None` on a malformed SPS or one using
177/// a scaling matrix (unsupported here).
178pub fn parse_sps(nal: &[u8]) -> Option<SpsInfo> {
179    if nal.is_empty() || nal[0] & 0x1f != NAL_SPS {
180        return None;
181    }
182    let rbsp = unescape_rbsp(&nal[1..]);
183    let mut r = BitReader::new(&rbsp);
184    if r.remaining() < 24 {
185        return None; // too short to hold profile/constraints/level
186    }
187
188    let profile_idc = r.read_bits(8)? as u8;
189    let _constraints = r.read_bits(8)?;
190    let level_idc = r.read_bits(8)? as u8;
191    let _sps_id = r.read_ue()?;
192
193    let mut chroma_format_idc = 1u32; // 4:2:0 default
194    if matches!(
195        profile_idc,
196        100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135
197    ) {
198        chroma_format_idc = r.read_ue()?;
199        if chroma_format_idc == 3 {
200            let _separate_colour_plane = r.read_bit()?;
201        }
202        let _bit_depth_luma = r.read_ue()?;
203        let _bit_depth_chroma = r.read_ue()?;
204        let _qpprime = r.read_bit()?;
205        let scaling_matrix_present = r.read_bit()?;
206        if scaling_matrix_present == 1 {
207            return None; // scaling lists not decoded
208        }
209    }
210
211    let _log2_max_frame_num = r.read_ue()?;
212    let pic_order_cnt_type = r.read_ue()?;
213    if pic_order_cnt_type == 0 {
214        let _log2_max_poc_lsb = r.read_ue()?;
215    } else if pic_order_cnt_type == 1 {
216        let _delta_pic_order_always_zero = r.read_bit()?;
217        let _offset_for_non_ref = r.read_se()?;
218        let _offset_for_top_to_bottom = r.read_se()?;
219        let cycle = r.read_ue()?;
220        for _ in 0..cycle {
221            let _ = r.read_se()?;
222        }
223    }
224
225    let _max_num_ref_frames = r.read_ue()?;
226    let _gaps_allowed = r.read_bit()?;
227    let pic_width_in_mbs_minus1 = r.read_ue()?;
228    let pic_height_in_map_units_minus1 = r.read_ue()?;
229    let frame_mbs_only_flag = r.read_bit()?;
230    if frame_mbs_only_flag == 0 {
231        let _mb_adaptive = r.read_bit()?;
232    }
233    let _direct_8x8 = r.read_bit()?;
234
235    let frame_cropping_flag = r.read_bit()?;
236    let (mut crop_left, mut crop_right, mut crop_top, mut crop_bottom) = (0u32, 0u32, 0u32, 0u32);
237    if frame_cropping_flag == 1 {
238        crop_left = r.read_ue()?;
239        crop_right = r.read_ue()?;
240        crop_top = r.read_ue()?;
241        crop_bottom = r.read_ue()?;
242    }
243
244    let width_mbs = pic_width_in_mbs_minus1 + 1;
245    let height_map = pic_height_in_map_units_minus1 + 1;
246    let frame_height_mbs = (2 - frame_mbs_only_flag) * height_map;
247
248    // Chroma sub-sampling factors for the crop unit.
249    let (sub_w, sub_h) = match chroma_format_idc {
250        0 => (1, 1), // monochrome
251        1 => (2, 2), // 4:2:0
252        2 => (2, 1), // 4:2:2
253        _ => (1, 1), // 4:4:4
254    };
255    let crop_unit_x = if chroma_format_idc == 0 { 1 } else { sub_w };
256    let crop_unit_y = (if chroma_format_idc == 0 { 1 } else { sub_h }) * (2 - frame_mbs_only_flag);
257
258    let width = width_mbs * 16 - (crop_left + crop_right) * crop_unit_x;
259    let height = frame_height_mbs * 16 - (crop_top + crop_bottom) * crop_unit_y;
260
261    Some(SpsInfo {
262        width,
263        height,
264        profile_idc,
265        level_idc,
266    })
267}
268
269/// The HLS `CODECS` attribute for H.264, e.g. `"avc1.4d401f"` (profile/level
270/// encoded as `avc1.PPCCLL`). The middle byte is the constraint-set flags (0).
271pub fn hls_codec_string(p: &VideoParams) -> String {
272    format!("avc1.{:02x}00{:02x}", p.profile, p.level)
273}
274
275/// [`CodecParser`] implementation for H.264 / AVC.
276pub struct H264;
277
278impl CodecParser for H264 {
279    const CODEC: CodecId = CodecId::H264;
280
281    fn parse_config(data: &[u8]) -> Option<VideoParams> {
282        for nal in iter_nals(data) {
283            if !nal.is_empty() && nal[0] & 0x1f == NAL_SPS {
284                let sps = parse_sps(nal)?;
285                return Some(VideoParams {
286                    width: sps.width,
287                    height: sps.height,
288                    profile: sps.profile_idc,
289                    level: sps.level_idc,
290                    tier: 0,
291                    bit_depth: 8,
292                });
293            }
294        }
295        None
296    }
297
298    fn is_random_access_point(data: &[u8]) -> bool {
299        iter_nals(data).any(|n| !n.is_empty() && n[0] & 0x1f == NAL_IDR)
300    }
301
302    fn carries_config(data: &[u8]) -> bool {
303        iter_nals(data).any(|n| !n.is_empty() && matches!(n[0] & 0x1f, NAL_SPS | NAL_PPS))
304    }
305
306    fn hls_codec_string(params: &VideoParams) -> String {
307        hls_codec_string(params)
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::codec::testutil::BitWriter;
315
316    #[test]
317    fn iterates_and_converts_nals() {
318        // Two NALs: [9 0xF0] and [7 0x42], with mixed 4- and 3-byte start codes.
319        let annexb = [0, 0, 0, 1, 9, 0xF0, 0, 0, 1, 7, 0x42];
320        let nals: Vec<&[u8]> = iter_nals_annexb(&annexb).collect();
321        assert_eq!(nals, vec![&[9u8, 0xF0][..], &[7u8, 0x42][..]]);
322
323        // Round-trip Annex-B → AVCC → Annex-B (normalizes to 4-byte codes).
324        let avcc = annexb_to_avcc(&annexb);
325        assert_eq!(&avcc[..], &[0, 0, 0, 2, 9, 0xF0, 0, 0, 0, 2, 7, 0x42]);
326        let back = avcc_to_annexb(&avcc, 4).unwrap();
327        assert_eq!(&back[..], &[0, 0, 0, 1, 9, 0xF0, 0, 0, 0, 1, 7, 0x42]);
328    }
329
330    #[test]
331    fn avc_config_parses_and_round_trips() {
332        // avcC: version 1, profile/compat/level bytes, lengthSizeMinusOne=3,
333        // one SPS [0x67,0x42,0x00,0x1F], one PPS [0x68,0xCE].
334        let avcc = [
335            0x01, 0x42, 0x00, 0x1F, 0xFF, 0xE1, 0x00, 0x04, 0x67, 0x42, 0x00, 0x1F, 0x01, 0x00,
336            0x02, 0x68, 0xCE,
337        ];
338        let cfg = AvcConfig::parse(&avcc).expect("parse avcC");
339        assert_eq!(cfg.nal_length_size, 4);
340        assert_eq!(cfg.sps, vec![vec![0x67, 0x42, 0x00, 0x1F]]);
341        assert_eq!(cfg.pps, vec![vec![0x68, 0xCE]]);
342
343        // to_annexb emits each param set with a 4-byte start code.
344        assert_eq!(
345            &cfg.to_annexb()[..],
346            &[0, 0, 0, 1, 0x67, 0x42, 0x00, 0x1F, 0, 0, 0, 1, 0x68, 0xCE]
347        );
348
349        // A length-prefixed (AVCC) IDR converts to Annex-B, prepending params.
350        let idr_avcc = [0, 0, 0, 2, 0x65, 0xAA];
351        let plain = cfg.avcc_to_annexb(&idr_avcc, false).unwrap();
352        assert_eq!(&plain[..], &[0, 0, 0, 1, 0x65, 0xAA]);
353        let with_params = cfg.avcc_to_annexb(&idr_avcc, true).unwrap();
354        assert!(with_params.ends_with(&[0, 0, 0, 1, 0x65, 0xAA]));
355        assert!(with_params.len() > plain.len());
356
357        // Rebuilding the record preserves the parameter sets.
358        assert_eq!(AvcConfig::parse(&cfg.to_avc_record()), Some(cfg));
359
360        // Not an avcC record (Annex-B start code) → None.
361        assert_eq!(AvcConfig::parse(&[0, 0, 0, 1, 0x67]), None);
362    }
363
364    /// Synthesize a baseline (profile 66) SPS for 1280x720, no cropping.
365    fn sps_1280x720() -> Vec<u8> {
366        let mut w = BitWriter::default();
367        w.bits(66, 8); // profile_idc (baseline → no chroma block)
368        w.bits(0, 8); // constraint flags
369        w.bits(31, 8); // level 3.1
370        w.ue(0); // seq_parameter_set_id
371        w.ue(0); // log2_max_frame_num_minus4
372        w.ue(0); // pic_order_cnt_type = 0
373        w.ue(0); // log2_max_pic_order_cnt_lsb_minus4
374        w.ue(1); // max_num_ref_frames
375        w.bit(0); // gaps_in_frame_num_value_allowed
376        w.ue(79); // pic_width_in_mbs_minus1 → 80*16 = 1280
377        w.ue(44); // pic_height_in_map_units_minus1 → 45*16 = 720
378        w.bit(1); // frame_mbs_only_flag
379        w.bit(0); // direct_8x8_inference_flag
380        w.bit(0); // frame_cropping_flag
381        w.bit(0); // vui_parameters_present_flag
382        let mut nal = vec![0x67u8]; // NAL header: SPS (type 7), ref_idc 3
383        nal.extend_from_slice(&w.bytes());
384        nal
385    }
386
387    #[test]
388    fn parse_sps_extracts_resolution() {
389        let sps = parse_sps(&sps_1280x720()).expect("parse");
390        assert_eq!((sps.width, sps.height), (1280, 720));
391        assert_eq!(sps.profile_idc, 66);
392        assert_eq!(sps.level_idc, 31);
393    }
394
395    #[test]
396    fn codec_parser_classifies_and_extracts() {
397        // Annex-B AU: SPS + an IDR slice (type 5).
398        let mut au = vec![0, 0, 0, 1];
399        au.extend_from_slice(&sps_1280x720());
400        au.extend_from_slice(&[0, 0, 0, 1, 0x65, 0xAA]); // IDR NAL (type 5)
401
402        assert!(H264::is_random_access_point(&au));
403        assert!(H264::carries_config(&au));
404        let p = H264::parse_config(&au).expect("params");
405        assert_eq!((p.width, p.height, p.profile, p.level), (1280, 720, 66, 31));
406        assert_eq!(H264::hls_codec_string(&p), "avc1.42001f");
407
408        // A lone non-IDR slice (type 1) is not a RAP.
409        let delta = [0, 0, 0, 1, 0x41, 0xAA];
410        assert!(!H264::is_random_access_point(&delta));
411    }
412}