1use std::time::Duration;
19
20use bytes::Bytes;
21use hang::catalog::{AV1, VideoCodec, VideoConfig};
22use moq_mux::codec::{annexb, h264, h265};
23use moq_net::Timestamp;
24
25use super::backend::{self, Backend, Codec};
26use crate::{Error, Frame, Size};
27
28#[derive(Clone, Debug, Default, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Kind {
33 #[default]
35 Auto,
36 Hardware,
38 Software,
40 Named(String),
43}
44
45#[derive(Clone, Debug, Default)]
50#[non_exhaustive]
51pub struct Config {
52 pub kind: Kind,
54 pub latency_max: Option<Duration>,
58 pub resize: Option<Size>,
64 pub gpu_frames: bool,
81}
82
83impl Config {
84 pub fn new() -> Self {
86 Self::default()
87 }
88}
89
90enum Conversion {
92 Passthrough,
95 LengthPrefixed { length_size: usize, keyframe_prefix: Bytes },
99}
100
101pub struct Decoder {
109 backend: Box<dyn Backend>,
110 conversion: Conversion,
111 got_keyframe: bool,
112}
113
114impl Decoder {
115 pub fn new(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
118 let (codec, conversion) = match &catalog.codec {
119 VideoCodec::H264(h264) => {
120 let conversion = match (h264.inline, catalog.description.as_ref()) {
121 (true, _) => Conversion::Passthrough,
122 (false, Some(avcc)) => {
123 let params = h264::Avcc::parse(avcc).map_err(moq_mux::Error::from)?;
124 let keyframe_prefix = annexb::build_prefix(params.sps.iter().chain(params.pps.iter()));
125 Conversion::LengthPrefixed {
126 length_size: params.length_size,
127 keyframe_prefix,
128 }
129 }
130 (false, None) => {
131 tracing::warn!("avc1 track has no avcC description; reading it as Annex-B");
132 Conversion::Passthrough
133 }
134 };
135 (Codec::H264, conversion)
136 }
137 VideoCodec::H265(h265) => {
138 let conversion = if h265.in_band {
139 Conversion::Passthrough
140 } else {
141 let hvcc = catalog.description.as_ref().ok_or_else(|| {
142 Error::Codec(anyhow::anyhow!("hvc1 H.265 track is missing its hvcC description"))
143 })?;
144 let params = h265::Hvcc::parse(hvcc).map_err(moq_mux::Error::from)?;
145 let keyframe_prefix =
146 annexb::build_prefix(params.vps.iter().chain(params.sps.iter()).chain(params.pps.iter()));
147 Conversion::LengthPrefixed {
148 length_size: params.length_size,
149 keyframe_prefix,
150 }
151 };
152 (Codec::H265, conversion)
153 }
154 VideoCodec::AV1(av1) if is_supported_av1(av1) => (Codec::Av1, Conversion::Passthrough),
155 other => return Err(Error::UnsupportedCodec(other.to_string())),
156 };
157
158 let backend = backend::open(codec, config)?;
159 tracing::debug!(decoder = backend.name(), "opened video decoder");
160 Ok(Self {
161 backend,
162 conversion,
163 got_keyframe: false,
164 })
165 }
166
167 pub fn name(&self) -> &str {
169 self.backend.name()
170 }
171
172 pub fn decode(&mut self, payload: &Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error> {
178 if !self.got_keyframe {
181 if !keyframe {
182 return Ok(Vec::new());
183 }
184 self.got_keyframe = true;
185 }
186
187 let access_unit = match &self.conversion {
188 Conversion::Passthrough => payload.clone(),
190 Conversion::LengthPrefixed {
191 length_size,
192 keyframe_prefix,
193 } => {
194 let prefix = keyframe.then(|| keyframe_prefix.as_ref());
195 annexb::from_length_prefixed(payload, *length_size, prefix).map_err(moq_mux::Error::from)?
196 }
197 };
198
199 self.backend.decode(access_unit, timestamp, keyframe)
200 }
201
202 pub fn flush(&mut self) -> Result<Vec<Frame>, Error> {
208 self.got_keyframe = false;
209 self.backend.flush()
210 }
211}
212
213fn is_supported_av1(av1: &AV1) -> bool {
214 av1.bitdepth == 8 && !av1.mono_chrome && av1.chroma_subsampling_x && av1.chroma_subsampling_y
215}
216
217#[cfg(test)]
218mod tests {
219 use moq_net::Timestamp;
220
221 use super::backend::{self, Codec};
222 use crate::encode::{Config as EncodeConfig, Encoder, Kind as EncodeKind};
223 use crate::frame::I420;
224 use crate::{Frame, Surface};
225
226 fn flat_frame(index: u64, level: u8, size: crate::Size) -> Frame {
229 let rgba = vec![level; size.pixels() as usize * 4];
230 let surface = Surface::rgba(&rgba, size).unwrap();
231 Frame::new(surface, Timestamp::from_micros(index * 33_333).unwrap())
232 }
233
234 fn gray_frame(index: u64) -> Frame {
236 flat_frame(index, 0x80, gray_size())
237 }
238
239 fn assert_gray(i420: &I420, width: u32, height: u32) {
244 assert_eq!(i420.width, width);
245 assert_eq!(i420.height, height);
246 let luma = (width * height) as usize;
247 assert_eq!(i420.data.len(), luma * 3 / 2);
249
250 let avg = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
251 let y = avg(&i420.data[..luma]);
252 let u = avg(&i420.data[luma..luma + luma / 4]);
253 let v = avg(&i420.data[luma + luma / 4..]);
254 assert!((110..=140).contains(&y), "luma {y} off for a gray frame");
255 assert!((118..=138).contains(&u), "u {u} off for a gray frame");
256 assert!((118..=138).contains(&v), "v {v} off for a gray frame");
257 }
258
259 fn round_trip(mut encoder: Encoder, mut decoder: Box<dyn backend::Backend>, expect_name: &str) {
263 assert_eq!(decoder.name(), expect_name);
264
265 let mut decoded = Vec::new();
266 for i in 0..10u64 {
267 let keyframe = i == 0;
268 if keyframe {
269 encoder.keyframe();
270 }
271 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
273 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
274 }
275 }
276 decoded.extend(decoder.flush().unwrap());
277
278 assert!(!decoded.is_empty(), "decoder produced no frames");
279 for out in &decoded {
280 assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
281 }
282
283 let micros: Vec<u128> = decoded.iter().map(|d| d.timestamp.as_micros()).collect();
287 assert!(
288 micros.windows(2).all(|w| w[0] < w[1]),
289 "decoded timestamps not strictly increasing: {micros:?}"
290 );
291 assert!(
292 micros.iter().all(|&t| t % 33_333 == 0 && t < 333_330),
293 "decoded timestamp outside the fed set: {micros:?}"
294 );
295 }
296
297 fn decode_config(kind: super::Kind) -> super::Config {
299 super::Config {
300 kind,
301 ..super::Config::new()
302 }
303 }
304
305 fn h264_software_encoder(size: crate::Size) -> Encoder {
307 Encoder::new(&EncodeConfig {
308 kind: EncodeKind::Software,
309 ..EncodeConfig::new(size.width, size.height, 30)
310 })
311 .expect("openh264 encoder")
312 }
313
314 fn gray_size() -> crate::Size {
316 crate::Size::new(320, 240)
317 }
318
319 #[test]
320 fn openh264_round_trip() {
321 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
322 round_trip(h264_software_encoder(gray_size()), decoder, "openh264");
323 }
324
325 #[test]
328 fn avc1_without_avcc_decodes_as_annexb() {
329 let h264 = hang::catalog::H264 {
332 inline: false,
333 profile: 0x64,
334 constraints: 0x00,
335 level: 0x28,
336 };
337 let catalog = hang::catalog::VideoConfig::new(h264);
338 assert_eq!(catalog.codec.to_string(), "avc1.640028");
339 assert!(catalog.description.is_none());
340
341 let mut decoder = super::Decoder::new(&catalog, &decode_config(super::Kind::Software))
342 .expect("a description-less avc1 track opens rather than erroring");
343 assert!(
344 matches!(decoder.conversion, super::Conversion::Passthrough),
345 "a description-less avc1 track is read as Annex-B"
346 );
347
348 let mut encoder = h264_software_encoder(gray_size());
351 let mut decoded = Vec::new();
352 for i in 0..5u64 {
353 let keyframe = i == 0;
354 if keyframe {
355 encoder.keyframe();
356 }
357 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
358 assert!(
359 encoded.payload.starts_with(&[0, 0, 0, 1]) || encoded.payload.starts_with(&[0, 0, 1]),
360 "the test feeds Annex-B, not length-prefixed NALs"
361 );
362 decoded.extend(decoder.decode(&encoded.payload, encoded.timestamp, keyframe).unwrap());
363 }
364 }
365
366 assert!(!decoded.is_empty(), "decoder produced no frames");
367 for out in &decoded {
368 assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
369 }
370 }
371
372 #[test]
373 fn av1_is_supported_by_hardware_only() {
374 let catalog = hang::catalog::VideoConfig::new(hang::catalog::AV1::default());
375 let config = decode_config(super::Kind::Software);
376 let Err(err) = super::Decoder::new(&catalog, &config) else {
377 panic!("software AV1 decode unexpectedly opened");
378 };
379 assert!(matches!(err, crate::Error::NoDecoder(_)));
380 }
381
382 #[test]
383 fn av1_rejects_unsupported_catalog_shape() {
384 let av1 = hang::catalog::AV1 {
385 bitdepth: 10,
386 ..hang::catalog::AV1::default()
387 };
388 let catalog = hang::catalog::VideoConfig::new(av1);
389 let config = decode_config(super::Kind::Auto);
390 let Err(err) = super::Decoder::new(&catalog, &config) else {
391 panic!("10-bit AV1 decode unexpectedly opened");
392 };
393 assert!(matches!(err, crate::Error::UnsupportedCodec(_)));
394 }
395
396 #[cfg(target_os = "macos")]
397 #[test]
398 fn videotoolbox_round_trip() {
399 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
400 .expect("videotoolbox decoder");
401 round_trip(h264_software_encoder(gray_size()), decoder, "videotoolbox");
402 }
403
404 #[cfg(target_os = "macos")]
407 fn decode_gray(count: u64) -> Vec<Frame> {
408 let mut encoder = h264_software_encoder(gray_size());
409 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
410 .expect("videotoolbox decoder");
411
412 let mut decoded = Vec::new();
413 for i in 0..count {
414 let keyframe = i == 0;
415 if keyframe {
416 encoder.keyframe();
417 }
418 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
419 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
420 }
421 }
422
423 assert!(!decoded.is_empty(), "decoder produced no frames");
424 decoded
425 }
426
427 #[cfg(target_os = "macos")]
432 #[test]
433 fn videotoolbox_decode_stays_gpu_resident() {
434 for out in &decode_gray(3) {
435 assert!(
436 matches!(out.surface, Surface::PixelBuffer(_)),
437 "VideoToolbox decode downloaded to the CPU instead of keeping its surface"
438 );
439 }
440 }
441
442 #[cfg(target_os = "macos")]
446 #[test]
447 fn videotoolbox_resized_surface_reencodes_in_place() {
448 let decoded = decode_gray(3);
449 let resized: Vec<_> = decoded
450 .iter()
451 .map(|frame| frame.resize(crate::Size::new(160, 120)).unwrap())
452 .collect();
453 for frame in &resized {
454 assert_eq!(frame.size(), crate::Size::new(160, 120));
455 assert!(
456 matches!(frame.surface, Surface::PixelBuffer(_)),
457 "VideoToolbox resize downloaded to the CPU"
458 );
459 }
460
461 let encoder = Encoder::new(&EncodeConfig {
462 kind: EncodeKind::Named("videotoolbox".into()),
463 ..EncodeConfig::new(160, 120, 30)
464 });
465 let Ok(mut encoder) = encoder else {
466 eprintln!("skipping: no VideoToolbox H.264 hardware encoder available");
467 return;
468 };
469
470 let mut packets = 0;
471 for (i, out) in resized.iter().enumerate() {
472 if i == 0 {
473 encoder.keyframe();
474 }
475 packets += encoder.encode(out).unwrap().len();
476 }
477 packets += encoder.finish().unwrap().len();
478
479 assert!(packets > 0, "re-encoding decoded surfaces produced no packets");
480 }
481
482 #[cfg(target_os = "macos")]
487 #[test]
488 fn videotoolbox_hevc_round_trip() {
489 let encoder = Encoder::new(&EncodeConfig {
490 kind: EncodeKind::Named("videotoolbox".into()),
491 codec: crate::encode::Codec::H265,
492 ..EncodeConfig::new(320, 240, 30)
493 });
494 let Ok(encoder) = encoder else {
495 eprintln!("skipping: no VideoToolbox H.265 hardware encoder available");
496 return;
497 };
498 let decoder = backend::open(Codec::H265, &decode_config(super::Kind::Named("videotoolbox".into())))
499 .expect("videotoolbox H.265 decoder");
500 round_trip(encoder, decoder, "videotoolbox");
501 }
502
503 #[cfg(target_os = "windows")]
504 #[test]
505 fn mediafoundation_round_trip() {
506 let Ok(decoder) = backend::open(
509 Codec::H264,
510 &decode_config(super::Kind::Named("mediafoundation".into())),
511 ) else {
512 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
513 return;
514 };
515 round_trip(h264_software_encoder(gray_size()), decoder, "mediafoundation");
516 }
517
518 #[cfg(target_os = "windows")]
523 fn level(index: u64) -> u8 {
524 u8::try_from(0x20 + index * 0x10).expect("test stream is short enough to keep its levels distinct")
525 }
526
527 #[cfg(target_os = "windows")]
529 fn expected_luma(level: u8) -> u32 {
530 16 + (219 * level as u32) / 255
531 }
532
533 #[cfg(target_os = "windows")]
537 fn decode_levels(count: u64, size: crate::Size) -> Option<(Vec<Frame>, Box<dyn backend::Backend>)> {
538 let mut encoder = h264_software_encoder(size);
539 let decoder = backend::open(
540 Codec::H264,
541 &decode_config(super::Kind::Named("mediafoundation".into())),
542 );
543 let Ok(mut decoder) = decoder else {
544 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
545 return None;
546 };
547
548 let mut decoded = Vec::new();
549 for i in 0..count {
550 let keyframe = i == 0;
551 if keyframe {
552 encoder.keyframe();
553 }
554 for encoded in encoder.encode(&flat_frame(i, level(i), size)).unwrap() {
555 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
556 }
557 }
558
559 assert!(!decoded.is_empty(), "decoder produced no frames");
560 Some((decoded, decoder))
565 }
566
567 #[cfg(target_os = "windows")]
572 fn plane_averages(frame: &Frame) -> (u32, u32, u32) {
573 let i420 = frame.surface.to_i420().unwrap();
574 let average = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
575 (average(i420.y()), average(i420.u()), average(i420.v()))
576 }
577
578 #[cfg(target_os = "windows")]
583 #[test]
584 fn mediafoundation_decode_stays_gpu_resident() {
585 let Some((decoded, _decoder)) = decode_levels(3, gray_size()) else {
586 return;
587 };
588 for out in &decoded {
589 assert!(
590 matches!(out.surface, Surface::Texture(_)),
591 "Media Foundation decode downloaded to the CPU instead of keeping its picture on the GPU"
592 );
593 }
594 }
595
596 #[cfg(target_os = "windows")]
605 #[test]
606 fn mediafoundation_held_frames_keep_their_pixels() {
607 let Some((decoded, _decoder)) = decode_levels(12, gray_size()) else {
611 return;
612 };
613
614 for (i, out) in decoded.iter().enumerate() {
615 let (luma, _, _) = plane_averages(out);
616 let want = expected_luma(level(i as u64));
617 assert!(
620 luma.abs_diff(want) <= 6,
621 "frame {i} decoded to luma {luma}, expected about {want}: the decoder recycled its picture buffer"
622 );
623 }
624 }
625
626 #[cfg(target_os = "windows")]
633 #[test]
634 fn mediafoundation_decode_crops_coded_padding() {
635 let size = crate::Size::new(320, 180);
636 let Some((decoded, _decoder)) = decode_levels(3, size) else {
637 return;
638 };
639
640 for (i, out) in decoded.iter().enumerate() {
641 assert_eq!(out.size(), size, "frame {i} came back at the coded size");
642 let (luma, u, v) = plane_averages(out);
643 assert!(
644 luma.abs_diff(expected_luma(level(i as u64))) <= 6,
645 "frame {i} luma {luma} is not its own picture"
646 );
647 assert!(
649 u.abs_diff(128) <= 4 && v.abs_diff(128) <= 4,
650 "frame {i} chroma ({u}, {v}) is not neutral: the plane split read into the padding"
651 );
652 }
653 }
654
655 #[cfg(target_os = "windows")]
664 #[test]
665 fn mediafoundation_decoded_texture_reencodes_in_place() {
666 let size = gray_size();
667 let Some((decoded, _decoder)) = decode_levels(3, size) else {
668 return;
669 };
670 for out in &decoded {
671 assert!(
672 matches!(out.surface, Surface::Texture(_)),
673 "Media Foundation decode downloaded to the CPU"
674 );
675 }
676
677 let encoder = Encoder::new(&EncodeConfig {
678 kind: EncodeKind::Named("mediafoundation".into()),
679 ..EncodeConfig::new(size.width, size.height, 30)
680 });
681 let Ok(mut encoder) = encoder else {
682 eprintln!("skipping: no Media Foundation H.264 hardware encoder available");
683 return;
684 };
685
686 let mut reencoded = Vec::new();
687 for (i, out) in decoded.iter().enumerate() {
688 if i == 0 {
689 encoder.keyframe();
690 }
691 reencoded.extend(encoder.encode(out).unwrap());
692 }
693 reencoded.extend(encoder.finish().unwrap());
694 assert!(
695 !reencoded.is_empty(),
696 "re-encoding decoded textures produced no packets"
697 );
698
699 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
702 let mut out = Vec::new();
703 for (i, encoded) in reencoded.iter().enumerate() {
704 out.extend(
705 decoder
706 .decode(encoded.payload.clone(), encoded.timestamp, i == 0)
707 .unwrap(),
708 );
709 }
710
711 assert_eq!(out.len(), decoded.len(), "the re-encoded stream lost frames");
715 for (i, frame) in out.iter().enumerate() {
716 assert_eq!(frame.size(), size, "re-encoded frame {i} changed size");
717 let (luma, _, _) = plane_averages(frame);
718 let want = expected_luma(level(i as u64));
719 assert!(
720 luma.abs_diff(want) <= 6,
721 "re-encoded frame {i} came back as luma {luma}, expected about {want}"
722 );
723 }
724 }
725
726 #[cfg(target_os = "windows")]
732 #[test]
733 #[ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"]
734 fn mediafoundation_resized_texture_reencodes_in_place() {
735 let target = crate::Size::new(160, 120);
736 let resize = crate::resize::Config {
737 acceleration: crate::resize::Acceleration::Gpu,
738 ..Default::default()
739 };
740 let Some((decoded, _decoder)) = decode_levels(3, gray_size()) else {
741 return;
742 };
743 let Some(device) = decoded.iter().find_map(|frame| match &frame.surface {
744 Surface::Texture(texture) => Some(texture.device()),
745 _ => None,
746 }) else {
747 panic!("Media Foundation decode did not return a Direct3D11 texture");
748 };
749 if !crate::frame::d3d11::supports_nv12_render_target(device) {
750 eprintln!("skipping: driver cannot render to NV12");
751 return;
752 }
753 let resized: Vec<_> = decoded
754 .iter()
755 .map(|frame| frame.resize_with(target, &resize).unwrap())
756 .collect();
757 for frame in &resized {
758 assert_eq!(frame.size(), target);
759 assert!(
760 matches!(frame.surface, Surface::Texture(_)),
761 "Direct3D11 resize downloaded to the CPU"
762 );
763 }
764
765 let encoder = Encoder::new(&EncodeConfig {
766 kind: EncodeKind::Named("mediafoundation".into()),
767 ..EncodeConfig::new(target.width, target.height, 30)
768 });
769 let Ok(mut encoder) = encoder else {
770 eprintln!("skipping: no Media Foundation H.264 hardware encoder available");
771 return;
772 };
773
774 let mut packets = 0;
775 for (i, out) in resized.iter().enumerate() {
776 if i == 0 {
777 encoder.keyframe();
778 }
779 packets += encoder.encode(out).unwrap().len();
780 }
781 packets += encoder.finish().unwrap().len();
782
783 assert!(packets > 0, "re-encoding resized textures produced no packets");
784 }
785
786 #[cfg(target_os = "windows")]
791 #[test]
792 fn mediafoundation_hevc_round_trip() {
793 let encoder = Encoder::new(&EncodeConfig {
794 kind: EncodeKind::Named("mediafoundation".into()),
795 codec: crate::encode::Codec::H265,
796 ..EncodeConfig::new(320, 240, 30)
797 });
798 let Ok(encoder) = encoder else {
799 eprintln!("skipping: no Media Foundation H.265 hardware encoder available");
800 return;
801 };
802 let Ok(decoder) = backend::open(
803 Codec::H265,
804 &decode_config(super::Kind::Named("mediafoundation".into())),
805 ) else {
806 eprintln!("skipping: no Media Foundation H.265 hardware decoder available");
807 return;
808 };
809 round_trip(encoder, decoder, "mediafoundation");
810 }
811}