Skip to main content

arcly_stream/codec/
vp9.rs

1//! VP9 bitstream helpers: uncompressed frame-header resolution extraction and
2//! keyframe detection.
3//!
4//! VP9 carries no parameter sets — metadata lives in each frame's uncompressed
5//! header, and a keyframe is fully self-describing. The leading frame's header
6//! sits at offset 0 even inside a superframe (whose index is a trailer), so the
7//! parsers read from the start of the access unit.
8//!
9//! Note: VP9 is a WebM/DASH codec; **Apple HLS does not carry VP9**. These
10//! parsers exist so VP9 streams get correct GOP caching, eviction, and recording
11//! via the codec-agnostic engine; HLS egress for VP9 is intentionally unsupported.
12
13use super::bitreader::BitReader;
14use super::parser::{CodecParser, VideoParams};
15use crate::CodecId;
16
17const CS_RGB: u32 = 7;
18const VP9_SYNC_CODE: u32 = 0x498342;
19
20/// Parse the uncompressed header. Returns `(params, is_keyframe)`; `params`
21/// dimensions are only populated for keyframes.
22fn parse_uncompressed_header(data: &[u8]) -> Option<(VideoParams, bool)> {
23    let mut r = BitReader::new(data);
24    if r.read_bits(2)? != 2 {
25        return None; // frame_marker
26    }
27    let profile_low = r.read_bit()?;
28    let profile_high = r.read_bit()?;
29    let profile = ((profile_high << 1) | profile_low) as u8;
30    if profile == 3 {
31        let _reserved = r.read_bit()?;
32    }
33    if r.read_bit()? == 1 {
34        return Some((VideoParams::default(), false)); // show_existing_frame
35    }
36    let frame_type = r.read_bit()?; // 0 = KEY_FRAME
37    let _show_frame = r.read_bit()?;
38    let _error_resilient = r.read_bit()?;
39    if frame_type != 0 {
40        return Some((
41            VideoParams {
42                profile,
43                ..VideoParams::default()
44            },
45            false,
46        ));
47    }
48
49    // Key frame: sync code, color config, then frame size.
50    if r.read_bits(24)? != VP9_SYNC_CODE {
51        return None;
52    }
53    let mut bit_depth = 8u8;
54    if profile >= 2 {
55        bit_depth = if r.read_bit()? == 1 { 12 } else { 10 };
56    }
57    let color_space = r.read_bits(3)?;
58    if color_space != CS_RGB {
59        let _color_range = r.read_bit()?;
60        if profile == 1 || profile == 3 {
61            let _subsampling_x = r.read_bit()?;
62            let _subsampling_y = r.read_bit()?;
63            let _reserved = r.read_bit()?;
64        }
65    } else if profile == 1 || profile == 3 {
66        let _reserved = r.read_bit()?;
67    }
68
69    let width = r.read_bits(16)? + 1;
70    let height = r.read_bits(16)? + 1;
71    Some((
72        VideoParams {
73            width,
74            height,
75            profile,
76            level: 0,
77            tier: 0,
78            bit_depth,
79        },
80        true,
81    ))
82}
83
84/// [`CodecParser`] implementation for VP9.
85pub struct Vp9;
86
87impl CodecParser for Vp9 {
88    const CODEC: CodecId = CodecId::VP9;
89
90    fn parse_config(data: &[u8]) -> Option<VideoParams> {
91        match parse_uncompressed_header(data) {
92            Some((params, true)) => Some(params),
93            _ => None,
94        }
95    }
96
97    fn is_random_access_point(data: &[u8]) -> bool {
98        matches!(parse_uncompressed_header(data), Some((_, true)))
99    }
100
101    fn carries_config(data: &[u8]) -> bool {
102        // A VP9 keyframe is self-contained config.
103        Self::is_random_access_point(data)
104    }
105
106    fn hls_codec_string(p: &VideoParams) -> String {
107        // vp09.<profile>.<level>.<bit_depth> (level defaults to 1.0 → "10").
108        let level = if p.level == 0 { 10 } else { p.level };
109        format!("vp09.{:02}.{:02}.{:02}", p.profile, level, p.bit_depth)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::codec::testutil::BitWriter;
117
118    fn keyframe_1920x1080() -> Vec<u8> {
119        let mut w = BitWriter::default();
120        w.bits(2, 2); // frame_marker
121        w.bit(0); // profile_low_bit
122        w.bit(0); // profile_high_bit → profile 0
123        w.bit(0); // show_existing_frame
124        w.bit(0); // frame_type = KEY_FRAME
125        w.bit(1); // show_frame
126        w.bit(0); // error_resilient_mode
127        w.bits(VP9_SYNC_CODE, 24); // frame_sync_code
128        w.bits(1, 3); // color_space = CS_BT_601
129        w.bit(0); // color_range (profile 0 → no subsampling bits)
130        w.bits(1919, 16); // frame_width_minus_1
131        w.bits(1079, 16); // frame_height_minus_1
132        w.bytes()
133    }
134
135    #[test]
136    fn parse_keyframe_extracts_resolution() {
137        let (p, is_key) = parse_uncompressed_header(&keyframe_1920x1080()).expect("parse");
138        assert!(is_key);
139        assert_eq!(
140            (p.width, p.height, p.profile, p.bit_depth),
141            (1920, 1080, 0, 8)
142        );
143    }
144
145    #[test]
146    fn classifies_keyframe_and_codec_string() {
147        let kf = keyframe_1920x1080();
148        assert!(Vp9::is_random_access_point(&kf));
149        assert!(Vp9::carries_config(&kf));
150        let p = Vp9::parse_config(&kf).expect("params");
151        assert_eq!(Vp9::hls_codec_string(&p), "vp09.00.10.08");
152
153        // Inter frame (frame_type = 1) is not a RAP.
154        let mut w = BitWriter::default();
155        w.bits(2, 2); // marker
156        w.bit(0);
157        w.bit(0); // profile 0
158        w.bit(0); // show_existing_frame
159        w.bit(1); // frame_type = NON_KEY
160        let inter = w.bytes();
161        assert!(!Vp9::is_random_access_point(&inter));
162    }
163}