1use std::marker::PhantomData;
19use std::rc::Rc;
20
21use bytes::Bytes;
22use hang::catalog::{AV1, VideoCodec, VideoConfig};
23use moq_mux::codec::{annexb, h264, h265};
24use moq_net::Timestamp;
25
26use super::backend::{self, Backend, Codec};
27use crate::{Error, Frame, Output, Size, Surface};
28
29#[derive(Clone, Debug, Default, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum Kind {
34 #[default]
36 Auto,
37 Hardware,
39 Software,
41 Named(String),
44}
45
46#[derive(Clone, Debug, Default)]
54#[non_exhaustive]
55pub struct Config {
56 pub kind: Kind,
58 pub output: Output,
71 pub scale_hint: Option<Size>,
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 {
118 backend: Box<dyn Backend>,
119 conversion: Conversion,
120 output: Output,
121 got_keyframe: bool,
122 _thread_bound: PhantomData<Rc<()>>,
124}
125
126impl Decoder {
127 pub fn new(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
130 let (codec, conversion) = match &catalog.codec {
131 VideoCodec::H264(h264) => {
132 let conversion = match (h264.inline, catalog.description.as_ref()) {
133 (true, _) => Conversion::Passthrough,
134 (false, Some(avcc)) => {
135 let params = h264::Avcc::parse(avcc).map_err(moq_mux::Error::from)?;
136 let keyframe_prefix = annexb::build_prefix(params.sps.iter().chain(params.pps.iter()));
137 Conversion::LengthPrefixed {
138 length_size: params.length_size,
139 keyframe_prefix,
140 }
141 }
142 (false, None) => {
143 tracing::warn!("avc1 track has no avcC description; reading it as Annex-B");
144 Conversion::Passthrough
145 }
146 };
147 (Codec::H264, conversion)
148 }
149 VideoCodec::H265(h265) => {
150 let conversion = if h265.in_band {
151 Conversion::Passthrough
152 } else {
153 let hvcc = catalog.description.as_ref().ok_or_else(|| {
154 Error::Codec(anyhow::anyhow!("hvc1 H.265 track is missing its hvcC description"))
155 })?;
156 let params = h265::Hvcc::parse(hvcc).map_err(moq_mux::Error::from)?;
157 let keyframe_prefix =
158 annexb::build_prefix(params.vps.iter().chain(params.sps.iter()).chain(params.pps.iter()));
159 Conversion::LengthPrefixed {
160 length_size: params.length_size,
161 keyframe_prefix,
162 }
163 };
164 (Codec::H265, conversion)
165 }
166 VideoCodec::AV1(av1) if is_supported_av1(av1) => (Codec::Av1, Conversion::Passthrough),
167 other => return Err(Error::UnsupportedCodec(other.to_string())),
168 };
169
170 if let Some(size) = config.scale_hint {
174 size.validate("decoder scale hint")?;
175 }
176
177 let backend = backend::open(codec, config)?;
178 tracing::debug!(decoder = backend.name(), "opened video decoder");
179 Ok(Self {
180 backend,
181 conversion,
182 output: config.output,
183 got_keyframe: false,
184 _thread_bound: PhantomData,
185 })
186 }
187
188 pub fn name(&self) -> &str {
190 self.backend.name()
191 }
192
193 pub fn decode(&mut self, payload: &Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error> {
199 if !self.got_keyframe {
202 if !keyframe {
203 return Ok(Vec::new());
204 }
205 self.got_keyframe = true;
206 }
207
208 let access_unit = match &self.conversion {
209 Conversion::Passthrough => payload.clone(),
211 Conversion::LengthPrefixed {
212 length_size,
213 keyframe_prefix,
214 } => {
215 let prefix = keyframe.then(|| keyframe_prefix.as_ref());
216 annexb::from_length_prefixed(payload, *length_size, prefix).map_err(moq_mux::Error::from)?
217 }
218 };
219
220 let frames = self.backend.decode(access_unit, timestamp, keyframe)?;
221 self.deliver(frames)
222 }
223
224 pub fn flush(&mut self) -> Result<Vec<Frame>, Error> {
230 self.got_keyframe = false;
231 let frames = self.backend.flush()?;
232 self.deliver(frames)
233 }
234
235 fn deliver(&self, frames: Vec<Frame>) -> Result<Vec<Frame>, Error> {
243 match self.output {
244 Output::Native => Ok(frames),
245 Output::Cpu => frames
246 .into_iter()
247 .map(|frame| Ok(Frame::new(Surface::I420(frame.surface.into_i420()?), frame.timestamp)))
248 .collect(),
249 }
250 }
251}
252
253fn is_supported_av1(av1: &AV1) -> bool {
254 av1.bitdepth == 8 && !av1.mono_chrome && av1.chroma_subsampling_x && av1.chroma_subsampling_y
255}
256
257#[cfg(test)]
258mod tests {
259 #![cfg_attr(not(feature = "openh264"), allow(dead_code, unused_imports))]
260
261 use moq_net::Timestamp;
262
263 use super::backend::{self, Codec, probe};
264 use crate::encode::{Config as EncodeConfig, Encoder, Kind as EncodeKind};
265 use crate::frame::I420;
266 use crate::{Frame, Surface};
267
268 fn flat_frame(index: u64, level: u8, size: crate::Size) -> Frame {
271 let rgba = vec![level; size.pixels() as usize * 4];
272 let surface = Surface::rgba(&rgba, size).unwrap();
273 Frame::new(surface, Timestamp::from_micros(index * 33_333).unwrap())
274 }
275
276 fn gray_frame(index: u64) -> Frame {
278 flat_frame(index, 0x80, gray_size())
279 }
280
281 fn assert_gray(i420: &I420, width: u32, height: u32) {
286 assert_eq!(i420.width, width);
287 assert_eq!(i420.height, height);
288 let luma = (width * height) as usize;
289 assert_eq!(i420.data.len(), luma * 3 / 2);
291
292 let avg = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
293 let y = avg(&i420.data[..luma]);
294 let u = avg(&i420.data[luma..luma + luma / 4]);
295 let v = avg(&i420.data[luma + luma / 4..]);
296 assert!((110..=140).contains(&y), "luma {y} off for a gray frame");
297 assert!((118..=138).contains(&u), "u {u} off for a gray frame");
298 assert!((118..=138).contains(&v), "v {v} off for a gray frame");
299 }
300
301 fn round_trip(mut encoder: Encoder, mut decoder: Box<dyn backend::Backend>, expect_name: &str) {
305 assert_eq!(decoder.name(), expect_name);
306
307 let mut decoded = Vec::new();
308 for i in 0..10u64 {
309 let keyframe = i == 0;
310 if keyframe {
311 encoder.cut().unwrap();
312 }
313 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
315 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
316 }
317 }
318 decoded.extend(decoder.flush().unwrap());
319
320 assert!(!decoded.is_empty(), "decoder produced no frames");
321 for out in &decoded {
322 assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
323 }
324
325 let micros: Vec<u128> = decoded.iter().map(|d| d.timestamp.as_micros()).collect();
329 assert!(
330 micros.windows(2).all(|w| w[0] < w[1]),
331 "decoded timestamps not strictly increasing: {micros:?}"
332 );
333 assert!(
334 micros.iter().all(|&t| t % 33_333 == 0 && t < 333_330),
335 "decoded timestamp outside the fed set: {micros:?}"
336 );
337 }
338
339 fn decode_config(kind: super::Kind) -> super::Config {
341 super::Config {
342 kind,
343 ..super::Config::new()
344 }
345 }
346
347 #[cfg(feature = "openh264")]
348 fn h264_software_encoder(size: crate::Size) -> Encoder {
350 Encoder::new(&EncodeConfig {
351 kind: EncodeKind::Software,
352 ..EncodeConfig::new(size.width, size.height, crate::Rate::new(30, 1).unwrap())
353 })
354 .expect("openh264 encoder")
355 }
356
357 fn gray_size() -> crate::Size {
359 crate::Size::new(320, 240)
360 }
361
362 #[test]
363 #[cfg(feature = "openh264")]
364 fn openh264_round_trip() {
365 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
366 round_trip(h264_software_encoder(gray_size()), decoder, "openh264");
367 }
368
369 #[test]
372 #[cfg(feature = "openh264")]
373 fn avc1_without_avcc_decodes_as_annexb() {
374 let h264 = hang::catalog::H264 {
377 inline: false,
378 profile: 0x64,
379 constraints: 0x00,
380 level: 0x28,
381 };
382 let catalog = hang::catalog::VideoConfig::new(h264);
383 assert_eq!(catalog.codec.to_string(), "avc1.640028");
384 assert!(catalog.description.is_none());
385
386 let mut decoder = super::Decoder::new(&catalog, &decode_config(super::Kind::Software))
387 .expect("a description-less avc1 track opens rather than erroring");
388 assert!(
389 matches!(decoder.conversion, super::Conversion::Passthrough),
390 "a description-less avc1 track is read as Annex-B"
391 );
392
393 let mut encoder = h264_software_encoder(gray_size());
396 let mut decoded = Vec::new();
397 for i in 0..5u64 {
398 let keyframe = i == 0;
399 if keyframe {
400 encoder.cut().unwrap();
401 }
402 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
403 assert!(
404 encoded.payload.starts_with(&[0, 0, 0, 1]) || encoded.payload.starts_with(&[0, 0, 1]),
405 "the test feeds Annex-B, not length-prefixed NALs"
406 );
407 decoded.extend(decoder.decode(&encoded.payload, encoded.timestamp, keyframe).unwrap());
408 }
409 }
410
411 assert!(!decoded.is_empty(), "decoder produced no frames");
412 for out in &decoded {
413 assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
414 }
415 }
416
417 fn probe_catalog() -> hang::catalog::VideoConfig {
419 hang::catalog::VideoConfig::new(hang::catalog::H264 {
420 inline: true,
421 profile: 0x42,
422 constraints: 0,
423 level: 30,
424 })
425 }
426
427 fn decode_native(output: crate::Output, scale_hint: Option<crate::Size>) -> Frame {
430 let config = super::Config {
431 kind: super::Kind::Named(probe::NATIVE_NAME.into()),
432 output,
433 scale_hint,
434 };
435 let mut decoder = super::Decoder::new(&probe_catalog(), &config).expect("the native probe opens");
436 let mut frames = decoder
437 .decode(
438 &bytes::Bytes::from_static(b"access unit"),
439 Timestamp::from_micros(0).unwrap(),
440 true,
441 )
442 .unwrap();
443 assert_eq!(frames.len(), 1, "the probe decodes one picture per access unit");
444 frames.pop().unwrap()
445 }
446
447 #[test]
450 fn output_and_scale_hint_reach_the_backend() {
451 let _probe = probe::native_exclusive();
452 let hint = crate::Size::new(160, 120);
453 decode_native(crate::Output::Cpu, Some(hint));
454
455 let opened = probe::native_opened().expect("the backend recorded its config");
456 assert_eq!(opened.output, crate::Output::Cpu);
457 assert_eq!(opened.scale_hint, Some(hint));
458 }
459
460 #[test]
469 fn cpu_output_converts_native_frames() {
470 let _probe = probe::native_exclusive();
471 let cpu = decode_native(crate::Output::Cpu, None);
472 assert!(
473 matches!(cpu.surface, Surface::I420(_)),
474 "CPU output delivered a native surface"
475 );
476 assert_eq!(cpu.size(), probe::SIZE);
477
478 let native = decode_native(crate::Output::Native, None);
479 #[cfg(target_os = "macos")]
480 assert!(
481 matches!(native.surface, Surface::PixelBuffer(_)),
482 "native output downloaded the picture"
483 );
484 #[cfg(not(target_os = "macos"))]
485 assert!(matches!(native.surface, Surface::I420(_)));
486 }
487
488 #[test]
491 fn scale_hint_is_not_enforced() {
492 let _probe = probe::native_exclusive();
493 let target = crate::Size::new(160, 120);
494 let frame = decode_native(crate::Output::Cpu, Some(target));
495 assert_eq!(frame.size(), probe::SIZE, "the front end scaled behind the backend");
496
497 let resized = frame.resize(target, &crate::resize::Config::default()).unwrap();
498 assert_eq!(resized.size(), target);
499 }
500
501 #[test]
504 fn odd_scale_hint_is_refused() {
505 let _probe = probe::native_exclusive();
506 let config = super::Config {
507 kind: super::Kind::Named(probe::NATIVE_NAME.into()),
508 scale_hint: Some(crate::Size::new(161, 121)),
509 ..super::Config::new()
510 };
511 let Err(err) = super::Decoder::new(&probe_catalog(), &config) else {
512 panic!("an odd scale hint opened a decoder");
513 };
514 assert!(
515 !matches!(err, crate::Error::NoDecoder(_)),
516 "refused for the wrong reason: {err}"
517 );
518 assert!(
519 probe::native_opened().is_none(),
520 "the backend was opened before the hint was checked"
521 );
522 }
523
524 #[test]
525 fn av1_is_supported_by_hardware_only() {
526 let catalog = hang::catalog::VideoConfig::new(hang::catalog::AV1::default());
527 let config = decode_config(super::Kind::Software);
528 let Err(err) = super::Decoder::new(&catalog, &config) else {
529 panic!("software AV1 decode unexpectedly opened");
530 };
531 assert!(matches!(err, crate::Error::NoDecoder(_)));
532 }
533
534 #[test]
535 fn av1_rejects_unsupported_catalog_shape() {
536 let av1 = hang::catalog::AV1 {
537 bitdepth: 10,
538 ..hang::catalog::AV1::default()
539 };
540 let catalog = hang::catalog::VideoConfig::new(av1);
541 let config = decode_config(super::Kind::Auto);
542 let Err(err) = super::Decoder::new(&catalog, &config) else {
543 panic!("10-bit AV1 decode unexpectedly opened");
544 };
545 assert!(matches!(err, crate::Error::UnsupportedCodec(_)));
546 }
547
548 #[cfg(all(target_os = "macos", feature = "openh264"))]
549 #[test]
550 fn videotoolbox_round_trip() {
551 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
552 .expect("videotoolbox decoder");
553 round_trip(h264_software_encoder(gray_size()), decoder, "videotoolbox");
554 }
555
556 #[cfg(all(target_os = "macos", feature = "openh264"))]
559 fn decode_gray(count: u64) -> Vec<Frame> {
560 let mut encoder = h264_software_encoder(gray_size());
561 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
562 .expect("videotoolbox decoder");
563
564 let mut decoded = Vec::new();
565 for i in 0..count {
566 let keyframe = i == 0;
567 if keyframe {
568 encoder.cut().unwrap();
569 }
570 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
571 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
572 }
573 }
574
575 assert!(!decoded.is_empty(), "decoder produced no frames");
576 decoded
577 }
578
579 #[cfg(all(target_os = "macos", feature = "openh264"))]
584 #[test]
585 fn videotoolbox_decode_stays_gpu_resident() {
586 for out in &decode_gray(3) {
587 assert!(
588 matches!(out.surface, Surface::PixelBuffer(_)),
589 "VideoToolbox decode downloaded to the CPU instead of keeping its surface"
590 );
591 }
592 }
593
594 #[cfg(all(target_os = "macos", feature = "openh264"))]
598 #[test]
599 fn videotoolbox_resized_surface_reencodes_in_place() {
600 let decoded = decode_gray(3);
601 let resized: Vec<_> = decoded
602 .iter()
603 .map(|frame| {
604 frame
605 .resize(crate::Size::new(160, 120), &crate::resize::Config::default())
606 .unwrap()
607 })
608 .collect();
609 for frame in &resized {
610 assert_eq!(frame.size(), crate::Size::new(160, 120));
611 assert!(
612 matches!(frame.surface, Surface::PixelBuffer(_)),
613 "VideoToolbox resize downloaded to the CPU"
614 );
615 }
616
617 let encoder = Encoder::new(&EncodeConfig {
618 kind: EncodeKind::Named("videotoolbox".into()),
619 ..EncodeConfig::new(160, 120, crate::Rate::new(30, 1).unwrap())
620 });
621 let Ok(mut encoder) = encoder else {
622 eprintln!("skipping: no VideoToolbox H.264 hardware encoder available");
623 return;
624 };
625
626 let mut packets = 0;
627 for (i, out) in resized.iter().enumerate() {
628 if i == 0 {
629 encoder.cut().unwrap();
630 }
631 packets += encoder.encode(out).unwrap().len();
632 }
633 packets += encoder.finish().unwrap().len();
634
635 assert!(packets > 0, "re-encoding decoded surfaces produced no packets");
636 }
637
638 #[cfg(target_os = "macos")]
643 #[test]
644 fn videotoolbox_hevc_round_trip() {
645 let encoder = Encoder::new(&EncodeConfig {
646 kind: EncodeKind::Named("videotoolbox".into()),
647 codec: crate::encode::Codec::H265,
648 ..EncodeConfig::new(320, 240, crate::Rate::new(30, 1).unwrap())
649 });
650 let Ok(encoder) = encoder else {
651 eprintln!("skipping: no VideoToolbox H.265 hardware encoder available");
652 return;
653 };
654 let decoder = backend::open(Codec::H265, &decode_config(super::Kind::Named("videotoolbox".into())))
655 .expect("videotoolbox H.265 decoder");
656 round_trip(encoder, decoder, "videotoolbox");
657 }
658
659 #[cfg(all(target_os = "windows", feature = "openh264"))]
660 #[test]
661 fn mediafoundation_round_trip() {
662 let Ok(decoder) = backend::open(
665 Codec::H264,
666 &decode_config(super::Kind::Named("mediafoundation".into())),
667 ) else {
668 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
669 return;
670 };
671 round_trip(h264_software_encoder(gray_size()), decoder, "mediafoundation");
672 }
673
674 #[cfg(all(target_os = "windows", feature = "openh264"))]
679 fn level(index: u64) -> u8 {
680 u8::try_from(0x20 + index * 0x10).expect("test stream is short enough to keep its levels distinct")
681 }
682
683 #[cfg(all(target_os = "windows", feature = "openh264"))]
685 fn expected_luma(level: u8) -> u32 {
686 16 + (219 * level as u32) / 255
687 }
688
689 #[cfg(all(target_os = "windows", feature = "openh264"))]
693 fn decode_levels(count: u64, size: crate::Size) -> Option<(Vec<Frame>, Box<dyn backend::Backend>)> {
694 let mut encoder = h264_software_encoder(size);
695 let decoder = backend::open(
696 Codec::H264,
697 &decode_config(super::Kind::Named("mediafoundation".into())),
698 );
699 let Ok(mut decoder) = decoder else {
700 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
701 return None;
702 };
703
704 let mut decoded = Vec::new();
705 for i in 0..count {
706 let keyframe = i == 0;
707 if keyframe {
708 encoder.cut().unwrap();
709 }
710 for encoded in encoder.encode(&flat_frame(i, level(i), size)).unwrap() {
711 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
712 }
713 }
714
715 assert!(!decoded.is_empty(), "decoder produced no frames");
716 Some((decoded, decoder))
721 }
722
723 #[cfg(all(target_os = "windows", feature = "openh264"))]
728 fn plane_averages(frame: &Frame) -> (u32, u32, u32) {
729 let i420 = frame.surface.to_i420().unwrap();
730 let average = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
731 (average(i420.y()), average(i420.u()), average(i420.v()))
732 }
733
734 #[cfg(all(target_os = "windows", feature = "openh264"))]
739 #[test]
740 fn mediafoundation_decode_stays_gpu_resident() {
741 let Some((decoded, _decoder)) = decode_levels(3, gray_size()) else {
742 return;
743 };
744 for out in &decoded {
745 assert!(
746 matches!(out.surface, Surface::Texture(_)),
747 "Media Foundation decode downloaded to the CPU instead of keeping its picture on the GPU"
748 );
749 }
750 }
751
752 #[cfg(all(target_os = "windows", feature = "openh264"))]
761 #[test]
762 fn mediafoundation_held_frames_keep_their_pixels() {
763 let Some((decoded, _decoder)) = decode_levels(12, gray_size()) else {
767 return;
768 };
769
770 for (i, out) in decoded.iter().enumerate() {
771 let (luma, _, _) = plane_averages(out);
772 let want = expected_luma(level(i as u64));
773 assert!(
776 luma.abs_diff(want) <= 6,
777 "frame {i} decoded to luma {luma}, expected about {want}: the decoder recycled its picture buffer"
778 );
779 }
780 }
781
782 #[cfg(all(target_os = "windows", feature = "openh264"))]
789 #[test]
790 fn mediafoundation_decode_crops_coded_padding() {
791 let size = crate::Size::new(320, 180);
792 let Some((decoded, _decoder)) = decode_levels(3, size) else {
793 return;
794 };
795
796 for (i, out) in decoded.iter().enumerate() {
797 assert_eq!(out.size(), size, "frame {i} came back at the coded size");
798 let (luma, u, v) = plane_averages(out);
799 assert!(
800 luma.abs_diff(expected_luma(level(i as u64))) <= 6,
801 "frame {i} luma {luma} is not its own picture"
802 );
803 assert!(
805 u.abs_diff(128) <= 4 && v.abs_diff(128) <= 4,
806 "frame {i} chroma ({u}, {v}) is not neutral: the plane split read into the padding"
807 );
808 }
809 }
810
811 #[cfg(all(target_os = "windows", feature = "openh264"))]
820 #[test]
821 fn mediafoundation_decoded_texture_reencodes_in_place() {
822 let size = gray_size();
823 let Some((decoded, _decoder)) = decode_levels(3, size) else {
824 return;
825 };
826 for out in &decoded {
827 assert!(
828 matches!(out.surface, Surface::Texture(_)),
829 "Media Foundation decode downloaded to the CPU"
830 );
831 }
832
833 let encoder = Encoder::new(&EncodeConfig {
834 kind: EncodeKind::Named("mediafoundation".into()),
835 ..EncodeConfig::new(size.width, size.height, crate::Rate::new(30, 1).unwrap())
836 });
837 let Ok(mut encoder) = encoder else {
838 eprintln!("skipping: no Media Foundation H.264 hardware encoder available");
839 return;
840 };
841
842 let mut reencoded = Vec::new();
843 for (i, out) in decoded.iter().enumerate() {
844 if i == 0 {
845 encoder.cut().unwrap();
846 }
847 reencoded.extend(encoder.encode(out).unwrap());
848 }
849 reencoded.extend(encoder.finish().unwrap());
850 assert!(
851 !reencoded.is_empty(),
852 "re-encoding decoded textures produced no packets"
853 );
854
855 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
858 let mut out = Vec::new();
859 for (i, encoded) in reencoded.iter().enumerate() {
860 out.extend(
861 decoder
862 .decode(encoded.payload.clone(), encoded.timestamp, i == 0)
863 .unwrap(),
864 );
865 }
866
867 assert_eq!(out.len(), decoded.len(), "the re-encoded stream lost frames");
871 for (i, frame) in out.iter().enumerate() {
872 assert_eq!(frame.size(), size, "re-encoded frame {i} changed size");
873 let (luma, _, _) = plane_averages(frame);
874 let want = expected_luma(level(i as u64));
875 assert!(
876 luma.abs_diff(want) <= 6,
877 "re-encoded frame {i} came back as luma {luma}, expected about {want}"
878 );
879 }
880 }
881
882 #[cfg(all(target_os = "windows", feature = "openh264"))]
888 #[test]
889 #[ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"]
890 fn mediafoundation_resized_texture_reencodes_in_place() {
891 let target = crate::Size::new(160, 120);
892 let resize = crate::resize::Config::default();
893 let Some((decoded, _decoder)) = decode_levels(3, gray_size()) else {
894 return;
895 };
896 let Some(device) = decoded.iter().find_map(|frame| match &frame.surface {
897 Surface::Texture(texture) => Some(texture.device()),
898 _ => None,
899 }) else {
900 panic!("Media Foundation decode did not return a Direct3D11 texture");
901 };
902 if !crate::frame::d3d11::supports_nv12_render_target(device) {
903 eprintln!("skipping: driver cannot render to NV12");
904 return;
905 }
906 let resized: Vec<_> = decoded
907 .iter()
908 .map(|frame| frame.resize(target, &resize).unwrap())
909 .collect();
910 for frame in &resized {
911 assert_eq!(frame.size(), target);
912 assert!(
913 matches!(frame.surface, Surface::Texture(_)),
914 "Direct3D11 resize downloaded to the CPU"
915 );
916 }
917
918 let encoder = Encoder::new(&EncodeConfig {
919 kind: EncodeKind::Named("mediafoundation".into()),
920 ..EncodeConfig::new(target.width, target.height, crate::Rate::new(30, 1).unwrap())
921 });
922 let Ok(mut encoder) = encoder else {
923 eprintln!("skipping: no Media Foundation H.264 hardware encoder available");
924 return;
925 };
926
927 let mut packets = 0;
928 for (i, out) in resized.iter().enumerate() {
929 if i == 0 {
930 encoder.cut().unwrap();
931 }
932 packets += encoder.encode(out).unwrap().len();
933 }
934 packets += encoder.finish().unwrap().len();
935
936 assert!(packets > 0, "re-encoding resized textures produced no packets");
937 }
938
939 #[cfg(target_os = "windows")]
944 #[test]
945 fn mediafoundation_hevc_round_trip() {
946 let encoder = Encoder::new(&EncodeConfig {
947 kind: EncodeKind::Named("mediafoundation".into()),
948 codec: crate::encode::Codec::H265,
949 ..EncodeConfig::new(320, 240, crate::Rate::new(30, 1).unwrap())
950 });
951 let Ok(encoder) = encoder else {
952 eprintln!("skipping: no Media Foundation H.265 hardware encoder available");
953 return;
954 };
955 let Ok(decoder) = backend::open(
956 Codec::H265,
957 &decode_config(super::Kind::Named("mediafoundation".into())),
958 ) else {
959 eprintln!("skipping: no Media Foundation H.265 hardware decoder available");
960 return;
961 };
962 round_trip(encoder, decoder, "mediafoundation");
963 }
964}