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 flat_frame(index: u64, level: u8, size: crate::Size) -> Frame {
193 let rgba = vec![level; size.pixels() as usize * 4];
194 let surface = Surface::rgba(&rgba, size).unwrap();
195 Frame::new(surface, Timestamp::from_micros(index * 33_333).unwrap())
196 }
197
198 fn gray_frame(index: u64) -> Frame {
200 flat_frame(index, 0x80, gray_size())
201 }
202
203 fn assert_gray(i420: &I420, width: u32, height: u32) {
208 assert_eq!(i420.width, width);
209 assert_eq!(i420.height, height);
210 let luma = (width * height) as usize;
211 assert_eq!(i420.data.len(), luma * 3 / 2);
213
214 let avg = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
215 let y = avg(&i420.data[..luma]);
216 let u = avg(&i420.data[luma..luma + luma / 4]);
217 let v = avg(&i420.data[luma + luma / 4..]);
218 assert!((110..=140).contains(&y), "luma {y} off for a gray frame");
219 assert!((118..=138).contains(&u), "u {u} off for a gray frame");
220 assert!((118..=138).contains(&v), "v {v} off for a gray frame");
221 }
222
223 fn round_trip(mut encoder: Encoder, mut decoder: Box<dyn backend::Backend>, expect_name: &str) {
227 assert_eq!(decoder.name(), expect_name);
228
229 let mut decoded = Vec::new();
230 for i in 0..10u64 {
231 let keyframe = i == 0;
232 if keyframe {
233 encoder.keyframe();
234 }
235 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
237 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
238 }
239 }
240
241 assert!(!decoded.is_empty(), "decoder produced no frames");
242 for out in &decoded {
243 assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
244 }
245
246 let micros: Vec<u128> = decoded.iter().map(|d| d.timestamp.as_micros()).collect();
250 assert!(
251 micros.windows(2).all(|w| w[0] < w[1]),
252 "decoded timestamps not strictly increasing: {micros:?}"
253 );
254 assert!(
255 micros.iter().all(|&t| t % 33_333 == 0 && t < 333_330),
256 "decoded timestamp outside the fed set: {micros:?}"
257 );
258 }
259
260 fn decode_config(kind: super::Kind) -> super::Config {
262 super::Config {
263 kind,
264 ..super::Config::new()
265 }
266 }
267
268 fn h264_software_encoder(size: crate::Size) -> Encoder {
270 Encoder::new(&EncodeConfig {
271 kind: EncodeKind::Software,
272 ..EncodeConfig::new(size.width, size.height, 30)
273 })
274 .expect("openh264 encoder")
275 }
276
277 fn gray_size() -> crate::Size {
279 crate::Size::new(320, 240)
280 }
281
282 #[test]
283 fn openh264_round_trip() {
284 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
285 round_trip(h264_software_encoder(gray_size()), decoder, "openh264");
286 }
287
288 #[test]
289 fn av1_is_supported_by_hardware_only() {
290 let catalog = hang::catalog::VideoConfig::new(hang::catalog::AV1::default());
291 let config = decode_config(super::Kind::Software);
292 let Err(err) = super::Decoder::new(&catalog, &config) else {
293 panic!("software AV1 decode unexpectedly opened");
294 };
295 assert!(matches!(err, crate::Error::NoDecoder(_)));
296 }
297
298 #[test]
299 fn av1_rejects_unsupported_catalog_shape() {
300 let av1 = hang::catalog::AV1 {
301 bitdepth: 10,
302 ..hang::catalog::AV1::default()
303 };
304 let catalog = hang::catalog::VideoConfig::new(av1);
305 let config = decode_config(super::Kind::Auto);
306 let Err(err) = super::Decoder::new(&catalog, &config) else {
307 panic!("10-bit AV1 decode unexpectedly opened");
308 };
309 assert!(matches!(err, crate::Error::UnsupportedCodec(_)));
310 }
311
312 #[cfg(target_os = "macos")]
313 #[test]
314 fn videotoolbox_round_trip() {
315 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
316 .expect("videotoolbox decoder");
317 round_trip(h264_software_encoder(gray_size()), decoder, "videotoolbox");
318 }
319
320 #[cfg(target_os = "macos")]
323 fn decode_gray(count: u64) -> Vec<Frame> {
324 let mut encoder = h264_software_encoder(gray_size());
325 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
326 .expect("videotoolbox decoder");
327
328 let mut decoded = Vec::new();
329 for i in 0..count {
330 let keyframe = i == 0;
331 if keyframe {
332 encoder.keyframe();
333 }
334 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
335 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
336 }
337 }
338
339 assert!(!decoded.is_empty(), "decoder produced no frames");
340 decoded
341 }
342
343 #[cfg(target_os = "macos")]
348 #[test]
349 fn videotoolbox_decode_stays_gpu_resident() {
350 for out in &decode_gray(3) {
351 assert!(
352 matches!(out.surface, Surface::PixelBuffer(_)),
353 "VideoToolbox decode downloaded to the CPU instead of keeping its surface"
354 );
355 }
356 }
357
358 #[cfg(target_os = "macos")]
362 #[test]
363 fn videotoolbox_resized_surface_reencodes_in_place() {
364 let decoded = decode_gray(3);
365 let resized: Vec<_> = decoded
366 .iter()
367 .map(|frame| frame.resize(crate::Size::new(160, 120)).unwrap())
368 .collect();
369 for frame in &resized {
370 assert_eq!(frame.size(), crate::Size::new(160, 120));
371 assert!(
372 matches!(frame.surface, Surface::PixelBuffer(_)),
373 "VideoToolbox resize downloaded to the CPU"
374 );
375 }
376
377 let encoder = Encoder::new(&EncodeConfig {
378 kind: EncodeKind::Named("videotoolbox".into()),
379 ..EncodeConfig::new(160, 120, 30)
380 });
381 let Ok(mut encoder) = encoder else {
382 eprintln!("skipping: no VideoToolbox H.264 hardware encoder available");
383 return;
384 };
385
386 let mut packets = 0;
387 for (i, out) in resized.iter().enumerate() {
388 if i == 0 {
389 encoder.keyframe();
390 }
391 packets += encoder.encode(out).unwrap().len();
392 }
393 packets += encoder.finish().unwrap().len();
394
395 assert!(packets > 0, "re-encoding decoded surfaces produced no packets");
396 }
397
398 #[cfg(target_os = "macos")]
403 #[test]
404 fn videotoolbox_hevc_round_trip() {
405 let encoder = Encoder::new(&EncodeConfig {
406 kind: EncodeKind::Named("videotoolbox".into()),
407 codec: crate::encode::Codec::H265,
408 ..EncodeConfig::new(320, 240, 30)
409 });
410 let Ok(encoder) = encoder else {
411 eprintln!("skipping: no VideoToolbox H.265 hardware encoder available");
412 return;
413 };
414 let decoder = backend::open(Codec::H265, &decode_config(super::Kind::Named("videotoolbox".into())))
415 .expect("videotoolbox H.265 decoder");
416 round_trip(encoder, decoder, "videotoolbox");
417 }
418
419 #[cfg(target_os = "windows")]
420 #[test]
421 fn mediafoundation_round_trip() {
422 let Ok(decoder) = backend::open(
425 Codec::H264,
426 &decode_config(super::Kind::Named("mediafoundation".into())),
427 ) else {
428 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
429 return;
430 };
431 round_trip(h264_software_encoder(gray_size()), decoder, "mediafoundation");
432 }
433
434 #[cfg(target_os = "windows")]
439 fn level(index: u64) -> u8 {
440 u8::try_from(0x20 + index * 0x10).expect("test stream is short enough to keep its levels distinct")
441 }
442
443 #[cfg(target_os = "windows")]
445 fn expected_luma(level: u8) -> u32 {
446 16 + (219 * level as u32) / 255
447 }
448
449 #[cfg(target_os = "windows")]
453 fn decode_levels(count: u64, size: crate::Size) -> Option<(Vec<Frame>, Box<dyn backend::Backend>)> {
454 let mut encoder = h264_software_encoder(size);
455 let decoder = backend::open(
456 Codec::H264,
457 &decode_config(super::Kind::Named("mediafoundation".into())),
458 );
459 let Ok(mut decoder) = decoder else {
460 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
461 return None;
462 };
463
464 let mut decoded = Vec::new();
465 for i in 0..count {
466 let keyframe = i == 0;
467 if keyframe {
468 encoder.keyframe();
469 }
470 for encoded in encoder.encode(&flat_frame(i, level(i), size)).unwrap() {
471 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
472 }
473 }
474
475 assert!(!decoded.is_empty(), "decoder produced no frames");
476 Some((decoded, decoder))
481 }
482
483 #[cfg(target_os = "windows")]
488 fn plane_averages(frame: &Frame) -> (u32, u32, u32) {
489 let i420 = frame.surface.to_i420().unwrap();
490 let average = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
491 (average(i420.y()), average(i420.u()), average(i420.v()))
492 }
493
494 #[cfg(target_os = "windows")]
499 #[test]
500 fn mediafoundation_decode_stays_gpu_resident() {
501 let Some((decoded, _decoder)) = decode_levels(3, gray_size()) else {
502 return;
503 };
504 for out in &decoded {
505 assert!(
506 matches!(out.surface, Surface::Texture(_)),
507 "Media Foundation decode downloaded to the CPU instead of keeping its picture on the GPU"
508 );
509 }
510 }
511
512 #[cfg(target_os = "windows")]
521 #[test]
522 fn mediafoundation_held_frames_keep_their_pixels() {
523 let Some((decoded, _decoder)) = decode_levels(12, gray_size()) else {
527 return;
528 };
529
530 for (i, out) in decoded.iter().enumerate() {
531 let (luma, _, _) = plane_averages(out);
532 let want = expected_luma(level(i as u64));
533 assert!(
536 luma.abs_diff(want) <= 6,
537 "frame {i} decoded to luma {luma}, expected about {want}: the decoder recycled its picture buffer"
538 );
539 }
540 }
541
542 #[cfg(target_os = "windows")]
549 #[test]
550 fn mediafoundation_decode_crops_coded_padding() {
551 let size = crate::Size::new(320, 180);
552 let Some((decoded, _decoder)) = decode_levels(3, size) else {
553 return;
554 };
555
556 for (i, out) in decoded.iter().enumerate() {
557 assert_eq!(out.size(), size, "frame {i} came back at the coded size");
558 let (luma, u, v) = plane_averages(out);
559 assert!(
560 luma.abs_diff(expected_luma(level(i as u64))) <= 6,
561 "frame {i} luma {luma} is not its own picture"
562 );
563 assert!(
565 u.abs_diff(128) <= 4 && v.abs_diff(128) <= 4,
566 "frame {i} chroma ({u}, {v}) is not neutral: the plane split read into the padding"
567 );
568 }
569 }
570
571 #[cfg(target_os = "windows")]
580 #[test]
581 fn mediafoundation_decoded_texture_reencodes_in_place() {
582 let size = gray_size();
583 let Some((decoded, _decoder)) = decode_levels(3, size) else {
584 return;
585 };
586 for out in &decoded {
587 assert!(
588 matches!(out.surface, Surface::Texture(_)),
589 "Media Foundation decode downloaded to the CPU"
590 );
591 }
592
593 let encoder = Encoder::new(&EncodeConfig {
594 kind: EncodeKind::Named("mediafoundation".into()),
595 ..EncodeConfig::new(size.width, size.height, 30)
596 });
597 let Ok(mut encoder) = encoder else {
598 eprintln!("skipping: no Media Foundation H.264 hardware encoder available");
599 return;
600 };
601
602 let mut reencoded = Vec::new();
603 for (i, out) in decoded.iter().enumerate() {
604 if i == 0 {
605 encoder.keyframe();
606 }
607 reencoded.extend(encoder.encode(out).unwrap());
608 }
609 reencoded.extend(encoder.finish().unwrap());
610 assert!(
611 !reencoded.is_empty(),
612 "re-encoding decoded textures produced no packets"
613 );
614
615 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
618 let mut out = Vec::new();
619 for (i, encoded) in reencoded.iter().enumerate() {
620 out.extend(
621 decoder
622 .decode(encoded.payload.clone(), encoded.timestamp, i == 0)
623 .unwrap(),
624 );
625 }
626
627 assert_eq!(out.len(), decoded.len(), "the re-encoded stream lost frames");
631 for (i, frame) in out.iter().enumerate() {
632 assert_eq!(frame.size(), size, "re-encoded frame {i} changed size");
633 let (luma, _, _) = plane_averages(frame);
634 let want = expected_luma(level(i as u64));
635 assert!(
636 luma.abs_diff(want) <= 6,
637 "re-encoded frame {i} came back as luma {luma}, expected about {want}"
638 );
639 }
640 }
641
642 #[cfg(target_os = "windows")]
647 #[test]
648 fn mediafoundation_hevc_round_trip() {
649 let encoder = Encoder::new(&EncodeConfig {
650 kind: EncodeKind::Named("mediafoundation".into()),
651 codec: crate::encode::Codec::H265,
652 ..EncodeConfig::new(320, 240, 30)
653 });
654 let Ok(encoder) = encoder else {
655 eprintln!("skipping: no Media Foundation H.265 hardware encoder available");
656 return;
657 };
658 let Ok(decoder) = backend::open(
659 Codec::H265,
660 &decode_config(super::Kind::Named("mediafoundation".into())),
661 ) else {
662 eprintln!("skipping: no Media Foundation H.265 hardware decoder available");
663 return;
664 };
665 round_trip(encoder, decoder, "mediafoundation");
666 }
667}