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/// [`CodecParser`] implementation for AV1.
69pub struct Av1;
70
71impl CodecParser for Av1 {
72    const CODEC: CodecId = CodecId::AV1;
73
74    fn parse_config(data: &[u8]) -> Option<VideoParams> {
75        iter_obus(data)
76            .find(|o| o.obu_type == OBU_SEQUENCE_HEADER)
77            .and_then(|o| parse_sequence_header(o.payload))
78    }
79
80    fn is_random_access_point(data: &[u8]) -> bool {
81        // A sequence-header OBU is only sent at a random-access point; its
82        // presence marks a clean decode start (and the keyframe that follows).
83        iter_obus(data).any(|o| o.obu_type == OBU_SEQUENCE_HEADER)
84    }
85
86    fn carries_config(data: &[u8]) -> bool {
87        Self::is_random_access_point(data)
88    }
89
90    fn hls_codec_string(p: &VideoParams) -> String {
91        // av01.<profile>.<level><tier=M>.<bit_depth>
92        format!("av01.{}.{:02}M.{:02}", p.profile, p.level, p.bit_depth)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::codec::testutil::BitWriter;
100
101    /// Build a low-overhead sequence-header OBU for 1920x1080, profile 0.
102    fn seq_header_obu_1920x1080() -> Vec<u8> {
103        let mut w = BitWriter::default();
104        w.bits(0, 3); // seq_profile
105        w.bit(0); // still_picture
106        w.bit(0); // reduced_still_picture_header
107        w.bit(0); // timing_info_present_flag
108        w.bit(0); // initial_display_delay_present_flag
109        w.bits(0, 5); // operating_points_cnt_minus1 = 0
110        w.bits(0, 12); // operating_point_idc[0]
111        w.bits(1, 5); // seq_level_idx[0] = 1 (<= 7 → no seq_tier)
112        w.bits(11, 4); // frame_width_bits_minus_1 = 11 → 12-bit width field
113        w.bits(11, 4); // frame_height_bits_minus_1 = 11 → 12-bit height field
114        w.bits(1919, 12); // max_frame_width_minus_1
115        w.bits(1079, 12); // max_frame_height_minus_1
116        w.align();
117        let payload = w.bytes();
118
119        let mut obu = vec![0x0A]; // header: type=1 (seq header), has_size_field=1
120        obu.push(payload.len() as u8); // LEB128 size (< 128)
121        obu.extend_from_slice(&payload);
122        obu
123    }
124
125    #[test]
126    fn parse_sequence_header_extracts_resolution() {
127        let p = parse_sequence_header(&seq_header_obu_1920x1080()[2..]).expect("parse");
128        assert_eq!((p.width, p.height), (1920, 1080));
129        assert_eq!((p.profile, p.level), (0, 1));
130    }
131
132    #[test]
133    fn classifies_and_extracts_via_obus() {
134        // Temporal delimiter + sequence header + a frame OBU.
135        let mut tu = vec![0x12, 0x00]; // OBU_TEMPORAL_DELIMITER (type 2), size 0
136        tu.extend_from_slice(&seq_header_obu_1920x1080());
137        tu.extend_from_slice(&[0x32, 0x02, 0xAA, 0xBB]); // OBU_FRAME (type 6), size 2
138
139        assert!(Av1::is_random_access_point(&tu));
140        assert!(Av1::carries_config(&tu));
141        let p = Av1::parse_config(&tu).expect("params");
142        assert_eq!((p.width, p.height), (1920, 1080));
143        assert_eq!(Av1::hls_codec_string(&p), "av01.0.01M.08");
144
145        // A temporal unit with no sequence header is not a RAP.
146        let no_seq = [0x12, 0x00, 0x32, 0x02, 0xAA, 0xBB];
147        assert!(!Av1::is_random_access_point(&no_seq));
148    }
149}