1use std::time::Duration;
12
13use bytes::Bytes;
14use hang::catalog::{AV1, VideoCodec, VideoConfig};
15use moq_mux::codec::{annexb, h264, h265};
16use moq_net::Timestamp;
17
18use super::backend::{self, Backend, Codec};
19use crate::{Error, Frame, Size};
20
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum Kind {
26 #[default]
28 Auto,
29 Hardware,
31 Software,
33 Named(String),
36}
37
38#[derive(Clone, Debug, Default)]
43#[non_exhaustive]
44pub struct Config {
45 pub kind: Kind,
47 pub latency_max: Option<Duration>,
51 pub resize: Option<Size>,
57}
58
59impl Config {
60 pub fn new() -> Self {
62 Self::default()
63 }
64}
65
66enum Conversion {
68 Passthrough,
71 LengthPrefixed { length_size: usize, keyframe_prefix: Bytes },
75}
76
77pub struct Decoder {
85 backend: Box<dyn Backend>,
86 conversion: Conversion,
87 got_keyframe: bool,
88}
89
90impl Decoder {
91 pub fn new(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
94 let (codec, conversion) = match &catalog.codec {
95 VideoCodec::H264(h264) => {
96 let conversion = if h264.inline {
97 Conversion::Passthrough
98 } else {
99 let avcc = catalog.description.as_ref().ok_or_else(|| {
100 Error::Codec(anyhow::anyhow!("avc1 H.264 track is missing its avcC description"))
101 })?;
102 let params = h264::Avcc::parse(avcc).map_err(moq_mux::Error::from)?;
103 let keyframe_prefix = annexb::build_prefix(params.sps.iter().chain(params.pps.iter()));
104 Conversion::LengthPrefixed {
105 length_size: params.length_size,
106 keyframe_prefix,
107 }
108 };
109 (Codec::H264, conversion)
110 }
111 VideoCodec::H265(h265) => {
112 let conversion = if h265.in_band {
113 Conversion::Passthrough
114 } else {
115 let hvcc = catalog.description.as_ref().ok_or_else(|| {
116 Error::Codec(anyhow::anyhow!("hvc1 H.265 track is missing its hvcC description"))
117 })?;
118 let params = h265::Hvcc::parse(hvcc).map_err(moq_mux::Error::from)?;
119 let keyframe_prefix =
120 annexb::build_prefix(params.vps.iter().chain(params.sps.iter()).chain(params.pps.iter()));
121 Conversion::LengthPrefixed {
122 length_size: params.length_size,
123 keyframe_prefix,
124 }
125 };
126 (Codec::H265, conversion)
127 }
128 VideoCodec::AV1(av1) if is_supported_av1(av1) => (Codec::Av1, Conversion::Passthrough),
129 other => return Err(Error::UnsupportedCodec(other.to_string())),
130 };
131
132 let backend = backend::open(codec, config)?;
133 tracing::debug!(decoder = backend.name(), "opened video decoder");
134 Ok(Self {
135 backend,
136 conversion,
137 got_keyframe: false,
138 })
139 }
140
141 pub fn name(&self) -> &str {
143 self.backend.name()
144 }
145
146 pub fn decode(&mut self, payload: &Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error> {
152 if !self.got_keyframe {
155 if !keyframe {
156 return Ok(Vec::new());
157 }
158 self.got_keyframe = true;
159 }
160
161 let access_unit = match &self.conversion {
162 Conversion::Passthrough => payload.clone(),
164 Conversion::LengthPrefixed {
165 length_size,
166 keyframe_prefix,
167 } => {
168 let prefix = keyframe.then(|| keyframe_prefix.as_ref());
169 annexb::from_length_prefixed(payload, *length_size, prefix).map_err(moq_mux::Error::from)?
170 }
171 };
172
173 self.backend.decode(access_unit, timestamp, keyframe)
174 }
175}
176
177fn is_supported_av1(av1: &AV1) -> bool {
178 av1.bitdepth == 8 && !av1.mono_chrome && av1.chroma_subsampling_x && av1.chroma_subsampling_y
179}
180
181#[cfg(test)]
182mod tests {
183 use moq_net::Timestamp;
184
185 use super::backend::{self, Codec};
186 use crate::encode::{Config as EncodeConfig, Encoder, Kind as EncodeKind};
187 use crate::frame::I420;
188 use crate::{Frame, Surface};
189
190 fn gray_frame(index: u64) -> Frame {
192 let rgba = vec![0x80u8; 320 * 240 * 4];
193 let surface = Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
194 Frame::new(surface, Timestamp::from_micros(index * 33_333).unwrap())
195 }
196
197 fn assert_gray(i420: &I420, width: u32, height: u32) {
202 assert_eq!(i420.width, width);
203 assert_eq!(i420.height, height);
204 let luma = (width * height) as usize;
205 assert_eq!(i420.data.len(), luma * 3 / 2);
207
208 let avg = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
209 let y = avg(&i420.data[..luma]);
210 let u = avg(&i420.data[luma..luma + luma / 4]);
211 let v = avg(&i420.data[luma + luma / 4..]);
212 assert!((110..=140).contains(&y), "luma {y} off for a gray frame");
213 assert!((118..=138).contains(&u), "u {u} off for a gray frame");
214 assert!((118..=138).contains(&v), "v {v} off for a gray frame");
215 }
216
217 fn round_trip(mut encoder: Encoder, mut decoder: Box<dyn backend::Backend>, expect_name: &str) {
221 assert_eq!(decoder.name(), expect_name);
222
223 let mut decoded = Vec::new();
224 for i in 0..10u64 {
225 let keyframe = i == 0;
226 if keyframe {
227 encoder.keyframe();
228 }
229 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
231 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
232 }
233 }
234
235 assert!(!decoded.is_empty(), "decoder produced no frames");
236 for out in &decoded {
237 assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
238 }
239
240 let micros: Vec<u128> = decoded.iter().map(|d| d.timestamp.as_micros()).collect();
244 assert!(
245 micros.windows(2).all(|w| w[0] < w[1]),
246 "decoded timestamps not strictly increasing: {micros:?}"
247 );
248 assert!(
249 micros.iter().all(|&t| t % 33_333 == 0 && t < 333_330),
250 "decoded timestamp outside the fed set: {micros:?}"
251 );
252 }
253
254 fn decode_config(kind: super::Kind) -> super::Config {
256 super::Config {
257 kind,
258 ..super::Config::new()
259 }
260 }
261
262 fn h264_software_encoder() -> Encoder {
264 Encoder::new(&EncodeConfig {
265 kind: EncodeKind::Software,
266 ..EncodeConfig::new(320, 240, 30)
267 })
268 .expect("openh264 encoder")
269 }
270
271 #[test]
272 fn openh264_round_trip() {
273 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
274 round_trip(h264_software_encoder(), decoder, "openh264");
275 }
276
277 #[test]
278 fn av1_is_supported_by_hardware_only() {
279 let catalog = hang::catalog::VideoConfig::new(hang::catalog::AV1::default());
280 let config = decode_config(super::Kind::Software);
281 let Err(err) = super::Decoder::new(&catalog, &config) else {
282 panic!("software AV1 decode unexpectedly opened");
283 };
284 assert!(matches!(err, crate::Error::NoDecoder(_)));
285 }
286
287 #[test]
288 fn av1_rejects_unsupported_catalog_shape() {
289 let av1 = hang::catalog::AV1 {
290 bitdepth: 10,
291 ..hang::catalog::AV1::default()
292 };
293 let catalog = hang::catalog::VideoConfig::new(av1);
294 let config = decode_config(super::Kind::Auto);
295 let Err(err) = super::Decoder::new(&catalog, &config) else {
296 panic!("10-bit AV1 decode unexpectedly opened");
297 };
298 assert!(matches!(err, crate::Error::UnsupportedCodec(_)));
299 }
300
301 #[cfg(target_os = "macos")]
302 #[test]
303 fn videotoolbox_round_trip() {
304 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
305 .expect("videotoolbox decoder");
306 round_trip(h264_software_encoder(), decoder, "videotoolbox");
307 }
308
309 #[cfg(target_os = "macos")]
312 fn decode_gray(count: u64) -> Vec<Frame> {
313 let mut encoder = h264_software_encoder();
314 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
315 .expect("videotoolbox decoder");
316
317 let mut decoded = Vec::new();
318 for i in 0..count {
319 let keyframe = i == 0;
320 if keyframe {
321 encoder.keyframe();
322 }
323 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
324 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
325 }
326 }
327
328 assert!(!decoded.is_empty(), "decoder produced no frames");
329 decoded
330 }
331
332 #[cfg(target_os = "macos")]
337 #[test]
338 fn videotoolbox_decode_stays_gpu_resident() {
339 for out in &decode_gray(3) {
340 assert!(
341 matches!(out.surface, Surface::PixelBuffer(_)),
342 "VideoToolbox decode downloaded to the CPU instead of keeping its surface"
343 );
344 }
345 }
346
347 #[cfg(target_os = "macos")]
351 #[test]
352 fn videotoolbox_resized_surface_reencodes_in_place() {
353 let decoded = decode_gray(3);
354 let resized: Vec<_> = decoded
355 .iter()
356 .map(|frame| frame.resize(crate::Size::new(160, 120)).unwrap())
357 .collect();
358 for frame in &resized {
359 assert_eq!(frame.size(), crate::Size::new(160, 120));
360 assert!(
361 matches!(frame.surface, Surface::PixelBuffer(_)),
362 "VideoToolbox resize downloaded to the CPU"
363 );
364 }
365
366 let encoder = Encoder::new(&EncodeConfig {
367 kind: EncodeKind::Named("videotoolbox".into()),
368 ..EncodeConfig::new(160, 120, 30)
369 });
370 let Ok(mut encoder) = encoder else {
371 eprintln!("skipping: no VideoToolbox H.264 hardware encoder available");
372 return;
373 };
374
375 let mut packets = 0;
376 for (i, out) in resized.iter().enumerate() {
377 if i == 0 {
378 encoder.keyframe();
379 }
380 packets += encoder.encode(out).unwrap().len();
381 }
382 packets += encoder.finish().unwrap().len();
383
384 assert!(packets > 0, "re-encoding decoded surfaces produced no packets");
385 }
386
387 #[cfg(target_os = "macos")]
392 #[test]
393 fn videotoolbox_hevc_round_trip() {
394 let encoder = Encoder::new(&EncodeConfig {
395 kind: EncodeKind::Named("videotoolbox".into()),
396 codec: crate::encode::Codec::H265,
397 ..EncodeConfig::new(320, 240, 30)
398 });
399 let Ok(encoder) = encoder else {
400 eprintln!("skipping: no VideoToolbox H.265 hardware encoder available");
401 return;
402 };
403 let decoder = backend::open(Codec::H265, &decode_config(super::Kind::Named("videotoolbox".into())))
404 .expect("videotoolbox H.265 decoder");
405 round_trip(encoder, decoder, "videotoolbox");
406 }
407
408 #[cfg(target_os = "windows")]
409 #[test]
410 fn mediafoundation_round_trip() {
411 let Ok(decoder) = backend::open(
414 Codec::H264,
415 &decode_config(super::Kind::Named("mediafoundation".into())),
416 ) else {
417 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
418 return;
419 };
420 round_trip(h264_software_encoder(), decoder, "mediafoundation");
421 }
422
423 #[cfg(target_os = "windows")]
428 #[test]
429 fn mediafoundation_hevc_round_trip() {
430 let encoder = Encoder::new(&EncodeConfig {
431 kind: EncodeKind::Named("mediafoundation".into()),
432 codec: crate::encode::Codec::H265,
433 ..EncodeConfig::new(320, 240, 30)
434 });
435 let Ok(encoder) = encoder else {
436 eprintln!("skipping: no Media Foundation H.265 hardware encoder available");
437 return;
438 };
439 let Ok(decoder) = backend::open(
440 Codec::H265,
441 &decode_config(super::Kind::Named("mediafoundation".into())),
442 ) else {
443 eprintln!("skipping: no Media Foundation H.265 hardware decoder available");
444 return;
445 };
446 round_trip(encoder, decoder, "mediafoundation");
447 }
448}