Skip to main content

openipc_video/api/
config.rs

1use std::io::Cursor;
2
3use cros_codecs::codec::{h264, h265};
4
5use crate::{
6    codecs::{H264Config, H265Config},
7    FrameDimensions, VideoError,
8};
9
10/// Encoded video codec accepted by a decoder backend.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum VideoCodec {
14    /// H.264 / AVC.
15    H264,
16    /// H.265 / HEVC.
17    H265,
18}
19
20impl std::fmt::Display for VideoCodec {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.write_str(match self {
23            Self::H264 => "H.264",
24            Self::H265 => "H.265",
25        })
26    }
27}
28
29impl From<openipc_core::Codec> for VideoCodec {
30    fn from(value: openipc_core::Codec) -> Self {
31        match value {
32            openipc_core::Codec::H264 => Self::H264,
33            openipc_core::Codec::H265 => Self::H265,
34        }
35    }
36}
37
38/// Complete parameter-set configuration for an encoded stream.
39#[derive(Debug, Clone, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum CodecConfig {
42    /// H.264 SPS and PPS configuration.
43    H264(H264Config),
44    /// H.265 VPS, SPS and PPS configuration.
45    H265(H265Config),
46}
47
48/// Dimensions, bit depth, and RFC 6381 codec identifier parsed from an SPS.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct CodecStreamInfo {
51    /// Full coded picture dimensions before conformance cropping.
52    pub coded_dimensions: FrameDimensions,
53    /// Visible picture dimensions after SPS cropping is applied.
54    pub visible_dimensions: FrameDimensions,
55    /// Luma bit depth declared by the SPS.
56    pub bit_depth: u8,
57    /// Browser/media-container codec identifier such as `avc1.64001F`.
58    pub codec_string: String,
59}
60
61impl CodecConfig {
62    /// Codec described by this configuration.
63    pub const fn codec(&self) -> VideoCodec {
64        match self {
65            Self::H264(_) => VideoCodec::H264,
66            Self::H265(_) => VideoCodec::H265,
67        }
68    }
69
70    /// Serialize the parameter sets as a standalone Annex-B byte stream.
71    ///
72    /// This is useful when a platform decoder or recorder needs in-band
73    /// configuration before the first random-access picture.
74    pub fn to_annex_b(&self) -> Vec<u8> {
75        let units: Vec<&[u8]> = match self {
76            Self::H264(config) => vec![&config.sps, &config.pps],
77            Self::H265(config) => vec![&config.vps, &config.sps, &config.pps],
78        };
79        let capacity = units.iter().map(|unit| 4 + unit.len()).sum();
80        let mut output = Vec::with_capacity(capacity);
81        for unit in units {
82            output.extend_from_slice(&[0, 0, 0, 1]);
83            output.extend_from_slice(unit);
84        }
85        output
86    }
87
88    /// Parse coded/visible dimensions, bit depth, and the codec identifier.
89    pub fn stream_info(&self) -> Result<CodecStreamInfo, VideoError> {
90        match self {
91            Self::H264(config) => h264_stream_info(config),
92            Self::H265(config) => h265_stream_info(config),
93        }
94    }
95}
96
97fn h264_stream_info(config: &H264Config) -> Result<CodecStreamInfo, VideoError> {
98    let annex_b = annex_b_unit(&config.sps);
99    let mut cursor = Cursor::new(annex_b.as_slice());
100    let nalu = h264::parser::Nalu::next(&mut cursor).map_err(codec_metadata_error)?;
101    let mut parser = h264::parser::Parser::default();
102    let sps = parser.parse_sps(&nalu).map_err(codec_metadata_error)?;
103    let visible = sps.visible_rectangle();
104    let profile = config.sps.get(1).copied().unwrap_or(0x42);
105    let compatibility = config.sps.get(2).copied().unwrap_or(0);
106    let level = config.sps.get(3).copied().unwrap_or(0x1e);
107    Ok(CodecStreamInfo {
108        coded_dimensions: FrameDimensions {
109            width: sps.width(),
110            height: sps.height(),
111        },
112        visible_dimensions: FrameDimensions {
113            width: visible.max.x.saturating_sub(visible.min.x),
114            height: visible.max.y.saturating_sub(visible.min.y),
115        },
116        bit_depth: 8 + sps.bit_depth_luma_minus8,
117        codec_string: format!("avc1.{profile:02X}{compatibility:02X}{level:02X}"),
118    })
119}
120
121fn h265_stream_info(config: &H265Config) -> Result<CodecStreamInfo, VideoError> {
122    let vps = annex_b_unit(&config.vps);
123    let sps_bytes = annex_b_unit(&config.sps);
124    let mut parser = h265::parser::Parser::default();
125    let mut vps_cursor = Cursor::new(vps.as_slice());
126    let vps_nalu = h265::parser::Nalu::next(&mut vps_cursor).map_err(codec_metadata_error)?;
127    parser.parse_vps(&vps_nalu).map_err(codec_metadata_error)?;
128    let mut sps_cursor = Cursor::new(sps_bytes.as_slice());
129    let sps_nalu = h265::parser::Nalu::next(&mut sps_cursor).map_err(codec_metadata_error)?;
130    let sps = parser.parse_sps(&sps_nalu).map_err(codec_metadata_error)?;
131    let visible = sps.visible_rectangle();
132    let tier = if sps.profile_tier_level.general_tier_flag {
133        'H'
134    } else {
135        'L'
136    };
137    let profile_space = match sps.profile_tier_level.general_profile_space {
138        0 => String::new(),
139        value @ 1..=3 => char::from(b'A' + value - 1).to_string(),
140        _ => String::new(),
141    };
142    let compatibility = sps
143        .profile_tier_level
144        .general_profile_compatibility_flag
145        .iter()
146        .enumerate()
147        .fold(0u32, |value, (bit, enabled)| {
148            value | (u32::from(*enabled) << bit)
149        });
150    let constraints = h265_constraint_string(&config.sps);
151    Ok(CodecStreamInfo {
152        coded_dimensions: FrameDimensions {
153            width: u32::from(sps.width()),
154            height: u32::from(sps.height()),
155        },
156        visible_dimensions: FrameDimensions {
157            width: visible.max.x.saturating_sub(visible.min.x),
158            height: visible.max.y.saturating_sub(visible.min.y),
159        },
160        bit_depth: 8 + sps.bit_depth_luma_minus8,
161        codec_string: format!(
162            "hev1.{profile_space}{}.{compatibility:X}.{tier}{}{constraints}",
163            sps.profile_tier_level.general_profile_idc,
164            sps.profile_tier_level.general_level_idc as u8,
165        ),
166    })
167}
168
169fn h265_constraint_string(sps: &[u8]) -> String {
170    // The first SPS byte after the two-byte NAL header precedes the fixed-size
171    // general profile/tier/level fields. Decode emulation-prevention bytes so
172    // the RFC 6381 constraint bytes can be copied exactly from the bitstream.
173    let mut rbsp = Vec::with_capacity(sps.len().saturating_sub(2));
174    let mut zero_count = 0;
175    for &byte in sps.get(2..).unwrap_or_default() {
176        if zero_count >= 2 && byte == 3 {
177            zero_count = 0;
178            continue;
179        }
180        rbsp.push(byte);
181        zero_count = if byte == 0 { zero_count + 1 } else { 0 };
182    }
183    let Some(bytes) = rbsp.get(6..12) else {
184        return String::new();
185    };
186    let end = bytes
187        .iter()
188        .rposition(|byte| *byte != 0)
189        .map_or(0, |index| index + 1);
190    bytes[..end]
191        .iter()
192        .map(|byte| format!(".{byte:02X}"))
193        .collect()
194}
195
196fn annex_b_unit(nalu: &[u8]) -> Vec<u8> {
197    let mut bytes = Vec::with_capacity(4 + nalu.len());
198    bytes.extend_from_slice(&[0, 0, 0, 1]);
199    bytes.extend_from_slice(nalu);
200    bytes
201}
202
203fn codec_metadata_error(message: String) -> VideoError {
204    VideoError::Backend {
205        backend: "codec-metadata",
206        operation: "parse sequence parameter set",
207        message,
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::CodecConfig;
214    use crate::{
215        backends::test_fixtures::{H264_KEYFRAME, H265_KEYFRAME},
216        CodecConfigTracker, ConfigUpdate, H264Config, H265Config, VideoCodec,
217    };
218
219    #[test]
220    fn builds_annex_b_parameter_set_headers() {
221        let h264 = CodecConfig::H264(H264Config::new([0x67, 1], [0x68, 2]).unwrap());
222        assert_eq!(
223            h264.to_annex_b(),
224            [0, 0, 0, 1, 0x67, 1, 0, 0, 0, 1, 0x68, 2]
225        );
226
227        let h265 =
228            CodecConfig::H265(H265Config::new([32 << 1, 1], [33 << 1, 2], [34 << 1, 3]).unwrap());
229        assert_eq!(h265.to_annex_b().len(), 18);
230    }
231
232    #[test]
233    fn parses_h264_stream_metadata() {
234        let mut tracker = CodecConfigTracker::default();
235        let ConfigUpdate::Changed(config) = tracker
236            .observe(VideoCodec::H264, H264_KEYFRAME)
237            .expect("fixture should contain valid Annex-B NAL units")
238        else {
239            panic!("fixture should contain a complete H.264 configuration");
240        };
241        let info = config.stream_info().unwrap();
242        assert_eq!(info.codec_string, "avc1.64000B");
243        assert_eq!(info.visible_dimensions.width, 128);
244        assert_eq!(info.visible_dimensions.height, 128);
245        assert_eq!(info.bit_depth, 8);
246    }
247
248    #[test]
249    fn parses_h265_stream_metadata() {
250        let mut tracker = CodecConfigTracker::default();
251        let ConfigUpdate::Changed(config) = tracker
252            .observe(VideoCodec::H265, H265_KEYFRAME)
253            .expect("fixture should contain valid Annex-B NAL units")
254        else {
255            panic!("fixture should contain a complete H.265 configuration");
256        };
257        let info = config.stream_info().unwrap();
258        assert_eq!(info.codec_string, "hev1.1.6.L60.B0");
259        assert!(info.visible_dimensions.width > 0);
260        assert!(info.visible_dimensions.height > 0);
261        assert!(matches!(info.bit_depth, 8 | 10));
262    }
263}
264
265/// Runtime policy shared by platform decoders.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct DecoderOptions {
268    /// Maximum encoded frames submitted but not yet completed.
269    pub max_frames_in_flight: usize,
270    /// Prefer decoder latency over frame reordering and throughput.
271    pub low_latency: bool,
272    /// Require hardware where the platform can enforce that distinction.
273    ///
274    /// WebCodecs only exposes a hardware preference, and Android API 26 does
275    /// not classify the system-selected codec through the NDK API. Those
276    /// backends request the hardware path but cannot prove which codec the
277    /// operating system selected.
278    pub require_hardware: bool,
279}
280
281impl Default for DecoderOptions {
282    fn default() -> Self {
283        Self {
284            max_frames_in_flight: 3,
285            low_latency: true,
286            require_hardware: true,
287        }
288    }
289}