Skip to main content

arcly_stream/codec/
av1.rs

1//! AV1 bitstream helpers: OBU sequence-header resolution extraction and
2//! random-access detection.
3
4use super::bitreader::BitReader;
5use super::obu::iter_obus;
6use super::parser::{CodecParser, VideoParams};
7use crate::CodecId;
8
9/// OBU type for a sequence header.
10pub const OBU_SEQUENCE_HEADER: u8 = 1;
11
12/// Parse an AV1 sequence-header OBU payload into [`VideoParams`].
13///
14/// Handles the common carriage cases (reduced still-picture headers and
15/// timing-info-absent sequence headers). Returns `None` for sequence headers
16/// carrying timing/decoder-model info, which require fuller parsing.
17fn parse_sequence_header(payload: &[u8]) -> Option<VideoParams> {
18    let mut r = BitReader::new(payload);
19    let seq_profile = r.read_bits(3)? as u8;
20    let _still_picture = r.read_bit()?;
21    let reduced_still_picture_header = r.read_bit()?;
22
23    let level;
24    if reduced_still_picture_header == 1 {
25        level = r.read_bits(5)? as u8; // seq_level_idx[0]
26    } else {
27        let timing_info_present_flag = r.read_bit()?;
28        if timing_info_present_flag == 1 {
29            return None; // timing_info()/decoder_model not parsed here
30        }
31        let initial_display_delay_present_flag = r.read_bit()?;
32        let operating_points_cnt_minus1 = r.read_bits(5)?;
33        let mut level0 = 0u8;
34        for i in 0..=operating_points_cnt_minus1 {
35            let _operating_point_idc = r.read_bits(12)?;
36            let seq_level_idx = r.read_bits(5)?;
37            if seq_level_idx > 7 {
38                let _seq_tier = r.read_bit()?;
39            }
40            if initial_display_delay_present_flag == 1 {
41                let present = r.read_bit()?;
42                if present == 1 {
43                    let _initial_display_delay_minus_1 = r.read_bits(4)?;
44                }
45            }
46            if i == 0 {
47                level0 = seq_level_idx as u8;
48            }
49        }
50        level = level0;
51    }
52
53    let frame_width_bits_minus_1 = r.read_bits(4)?;
54    let frame_height_bits_minus_1 = r.read_bits(4)?;
55    let max_frame_width_minus_1 = r.read_bits(frame_width_bits_minus_1 + 1)?;
56    let max_frame_height_minus_1 = r.read_bits(frame_height_bits_minus_1 + 1)?;
57
58    Some(VideoParams {
59        width: max_frame_width_minus_1 + 1,
60        height: max_frame_height_minus_1 + 1,
61        profile: seq_profile,
62        level,
63        tier: 0,
64        bit_depth: 8,
65    })
66}
67
68/// Encode an unsigned LEB128 integer onto `out`.
69fn leb128_encode(mut v: u64, out: &mut Vec<u8>) {
70    loop {
71        let mut byte = (v & 0x7f) as u8;
72        v >>= 7;
73        if v != 0 {
74            byte |= 0x80;
75        }
76        out.push(byte);
77        if v == 0 {
78            break;
79        }
80    }
81}
82
83/// Build the **AV1CodecConfigurationRecord** (`av1C` box body) from a config
84/// access unit — the temporal unit carrying the sequence-header OBU — alongside
85/// the parsed [`VideoParams`]. This is what the fMP4 muxer wraps in an `av1C`
86/// box inside the `av01` sample entry.
87///
88/// Color configuration (bit depth, chroma subsampling) is not parsed from the
89/// sequence header here; the record uses the **8-bit 4:2:0** defaults that match
90/// `seq_profile` 0 streams. Returns `None` if no sequence header is present.
91pub fn av1c_config_record(data: &[u8]) -> Option<(VideoParams, Vec<u8>)> {
92    let seq_payload = iter_obus(data)
93        .find(|o| o.obu_type == OBU_SEQUENCE_HEADER)?
94        .payload;
95    let params = parse_sequence_header(seq_payload)?;
96
97    let mut rec = Vec::with_capacity(4 + 2 + seq_payload.len());
98    rec.push(0x81); // marker(1) | version(7) = 1
99    rec.push(((params.profile & 0x07) << 5) | (params.level & 0x1f)); // seq_profile | seq_level_idx_0
100                                                                      // seq_tier_0 | high_bitdepth(0) | twelve_bit(0) | monochrome(0)
101                                                                      //   | chroma_subsampling_x(1) | chroma_subsampling_y(1) | chroma_sample_position(0)
102    rec.push(((params.tier & 1) << 7) | 0x0C);
103    rec.push(0x00); // reserved(3) | initial_presentation_delay_present(0) | reserved(4)
104                    // configOBUs: the sequence-header OBU, reframed in low-overhead (sized) form.
105    rec.push(0x0A); // obu_type = 1 (seq header), obu_has_size_field = 1
106    leb128_encode(seq_payload.len() as u64, &mut rec);
107    rec.extend_from_slice(seq_payload);
108    Some((params, rec))
109}
110
111/// [`CodecParser`] implementation for AV1.
112pub struct Av1;
113
114impl CodecParser for Av1 {
115    const CODEC: CodecId = CodecId::AV1;
116
117    fn parse_config(data: &[u8]) -> Option<VideoParams> {
118        iter_obus(data)
119            .find(|o| o.obu_type == OBU_SEQUENCE_HEADER)
120            .and_then(|o| parse_sequence_header(o.payload))
121    }
122
123    fn is_random_access_point(data: &[u8]) -> bool {
124        // A sequence-header OBU is only sent at a random-access point; its
125        // presence marks a clean decode start (and the keyframe that follows).
126        iter_obus(data).any(|o| o.obu_type == OBU_SEQUENCE_HEADER)
127    }
128
129    fn carries_config(data: &[u8]) -> bool {
130        Self::is_random_access_point(data)
131    }
132
133    fn hls_codec_string(p: &VideoParams) -> String {
134        // av01.<profile>.<level><tier=M>.<bit_depth>
135        format!("av01.{}.{:02}M.{:02}", p.profile, p.level, p.bit_depth)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::codec::testutil::BitWriter;
143
144    /// Build a low-overhead sequence-header OBU for 1920x1080, profile 0.
145    fn seq_header_obu_1920x1080() -> Vec<u8> {
146        let mut w = BitWriter::default();
147        w.bits(0, 3); // seq_profile
148        w.bit(0); // still_picture
149        w.bit(0); // reduced_still_picture_header
150        w.bit(0); // timing_info_present_flag
151        w.bit(0); // initial_display_delay_present_flag
152        w.bits(0, 5); // operating_points_cnt_minus1 = 0
153        w.bits(0, 12); // operating_point_idc[0]
154        w.bits(1, 5); // seq_level_idx[0] = 1 (<= 7 → no seq_tier)
155        w.bits(11, 4); // frame_width_bits_minus_1 = 11 → 12-bit width field
156        w.bits(11, 4); // frame_height_bits_minus_1 = 11 → 12-bit height field
157        w.bits(1919, 12); // max_frame_width_minus_1
158        w.bits(1079, 12); // max_frame_height_minus_1
159        w.align();
160        let payload = w.bytes();
161
162        let mut obu = vec![0x0A]; // header: type=1 (seq header), has_size_field=1
163        obu.push(payload.len() as u8); // LEB128 size (< 128)
164        obu.extend_from_slice(&payload);
165        obu
166    }
167
168    #[test]
169    fn parse_sequence_header_extracts_resolution() {
170        let p = parse_sequence_header(&seq_header_obu_1920x1080()[2..]).expect("parse");
171        assert_eq!((p.width, p.height), (1920, 1080));
172        assert_eq!((p.profile, p.level), (0, 1));
173    }
174
175    #[test]
176    fn classifies_and_extracts_via_obus() {
177        // Temporal delimiter + sequence header + a frame OBU.
178        let mut tu = vec![0x12, 0x00]; // OBU_TEMPORAL_DELIMITER (type 2), size 0
179        tu.extend_from_slice(&seq_header_obu_1920x1080());
180        tu.extend_from_slice(&[0x32, 0x02, 0xAA, 0xBB]); // OBU_FRAME (type 6), size 2
181
182        assert!(Av1::is_random_access_point(&tu));
183        assert!(Av1::carries_config(&tu));
184        let p = Av1::parse_config(&tu).expect("params");
185        assert_eq!((p.width, p.height), (1920, 1080));
186        assert_eq!(Av1::hls_codec_string(&p), "av01.0.01M.08");
187
188        // A temporal unit with no sequence header is not a RAP.
189        let no_seq = [0x12, 0x00, 0x32, 0x02, 0xAA, 0xBB];
190        assert!(!Av1::is_random_access_point(&no_seq));
191    }
192
193    #[test]
194    fn av1c_record_carries_profile_level_and_seq_header() {
195        let mut tu = vec![0x12, 0x00]; // temporal delimiter
196        let seq = seq_header_obu_1920x1080();
197        tu.extend_from_slice(&seq);
198        let (params, rec) = av1c_config_record(&tu).expect("record");
199        assert_eq!((params.width, params.height), (1920, 1080));
200        assert_eq!(rec[0], 0x81); // marker | version
201        assert_eq!(rec[1], 0x01); // seq_profile 0 (<<5) | seq_level_idx 1
202        assert_eq!(rec[2], 0x0C); // 8-bit 4:2:0 defaults
203                                  // configOBUs: the reframed sequence-header OBU follows the 4-byte header.
204        assert_eq!(rec[4], 0x0A); // seq-header OBU header, has_size_field
205        assert!(rec.ends_with(&seq[2..])); // the seq header payload
206    }
207}