1use std::io::Cursor;
2
3use cros_codecs::codec::{h264, h265};
4
5use crate::{
6 codecs::{H264Config, H265Config},
7 FrameDimensions, VideoError,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum VideoCodec {
14 H264,
16 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#[derive(Debug, Clone, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum CodecConfig {
42 H264(H264Config),
44 H265(H265Config),
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct CodecStreamInfo {
51 pub coded_dimensions: FrameDimensions,
53 pub visible_dimensions: FrameDimensions,
55 pub bit_depth: u8,
57 pub codec_string: String,
59}
60
61impl CodecConfig {
62 pub const fn codec(&self) -> VideoCodec {
64 match self {
65 Self::H264(_) => VideoCodec::H264,
66 Self::H265(_) => VideoCodec::H265,
67 }
68 }
69
70 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct DecoderOptions {
268 pub max_frames_in_flight: usize,
270 pub low_latency: bool,
272 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}