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, Copy, Debug, Default, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum Start {
56 #[default]
62 Oldest,
63 Latest,
69}
70
71#[derive(Clone, Debug, Default)]
76#[non_exhaustive]
77pub struct Config {
78 pub kind: Kind,
80 pub latency_max: Option<Duration>,
84 pub start: Start,
86 pub resize: Option<Size>,
92 pub gpu_frames: bool,
109}
110
111impl Config {
112 pub fn new() -> Self {
114 Self::default()
115 }
116}
117
118enum Conversion {
120 Passthrough,
123 LengthPrefixed { length_size: usize, keyframe_prefix: Bytes },
127}
128
129pub struct Decoder {
137 backend: Box<dyn Backend>,
138 conversion: Conversion,
139 got_keyframe: bool,
140}
141
142impl Decoder {
143 pub fn new(catalog: &VideoConfig, config: &Config) -> Result<Self, Error> {
146 let (codec, conversion) = match &catalog.codec {
147 VideoCodec::H264(h264) => {
148 let conversion = match (h264.inline, catalog.description.as_ref()) {
149 (true, _) => Conversion::Passthrough,
150 (false, Some(avcc)) => {
151 let params = h264::Avcc::parse(avcc).map_err(moq_mux::Error::from)?;
152 let keyframe_prefix = annexb::build_prefix(params.sps.iter().chain(params.pps.iter()));
153 Conversion::LengthPrefixed {
154 length_size: params.length_size,
155 keyframe_prefix,
156 }
157 }
158 (false, None) => {
159 tracing::warn!("avc1 track has no avcC description; reading it as Annex-B");
160 Conversion::Passthrough
161 }
162 };
163 (Codec::H264, conversion)
164 }
165 VideoCodec::H265(h265) => {
166 let conversion = if h265.in_band {
167 Conversion::Passthrough
168 } else {
169 let hvcc = catalog.description.as_ref().ok_or_else(|| {
170 Error::Codec(anyhow::anyhow!("hvc1 H.265 track is missing its hvcC description"))
171 })?;
172 let params = h265::Hvcc::parse(hvcc).map_err(moq_mux::Error::from)?;
173 let keyframe_prefix =
174 annexb::build_prefix(params.vps.iter().chain(params.sps.iter()).chain(params.pps.iter()));
175 Conversion::LengthPrefixed {
176 length_size: params.length_size,
177 keyframe_prefix,
178 }
179 };
180 (Codec::H265, conversion)
181 }
182 VideoCodec::AV1(av1) if is_supported_av1(av1) => (Codec::Av1, Conversion::Passthrough),
183 other => return Err(Error::UnsupportedCodec(other.to_string())),
184 };
185
186 let backend = backend::open(codec, config)?;
187 tracing::debug!(decoder = backend.name(), "opened video decoder");
188 Ok(Self {
189 backend,
190 conversion,
191 got_keyframe: false,
192 })
193 }
194
195 pub fn name(&self) -> &str {
197 self.backend.name()
198 }
199
200 pub fn decode(&mut self, payload: &Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error> {
206 if !self.got_keyframe {
209 if !keyframe {
210 return Ok(Vec::new());
211 }
212 self.got_keyframe = true;
213 }
214
215 let access_unit = match &self.conversion {
216 Conversion::Passthrough => payload.clone(),
218 Conversion::LengthPrefixed {
219 length_size,
220 keyframe_prefix,
221 } => {
222 let prefix = keyframe.then(|| keyframe_prefix.as_ref());
223 annexb::from_length_prefixed(payload, *length_size, prefix).map_err(moq_mux::Error::from)?
224 }
225 };
226
227 self.backend.decode(access_unit, timestamp, keyframe)
228 }
229
230 pub fn flush(&mut self) -> Result<Vec<Frame>, Error> {
236 self.got_keyframe = false;
237 self.backend.flush()
238 }
239}
240
241fn is_supported_av1(av1: &AV1) -> bool {
242 av1.bitdepth == 8 && !av1.mono_chrome && av1.chroma_subsampling_x && av1.chroma_subsampling_y
243}
244
245#[cfg(test)]
246mod tests {
247 use moq_net::Timestamp;
248
249 use super::backend::{self, Codec};
250 use crate::encode::{Config as EncodeConfig, Encoder, Kind as EncodeKind};
251 use crate::frame::I420;
252 use crate::{Frame, Surface};
253
254 fn flat_frame(index: u64, level: u8, size: crate::Size) -> Frame {
257 let rgba = vec![level; size.pixels() as usize * 4];
258 let surface = Surface::rgba(&rgba, size).unwrap();
259 Frame::new(surface, Timestamp::from_micros(index * 33_333).unwrap())
260 }
261
262 fn gray_frame(index: u64) -> Frame {
264 flat_frame(index, 0x80, gray_size())
265 }
266
267 fn assert_gray(i420: &I420, width: u32, height: u32) {
272 assert_eq!(i420.width, width);
273 assert_eq!(i420.height, height);
274 let luma = (width * height) as usize;
275 assert_eq!(i420.data.len(), luma * 3 / 2);
277
278 let avg = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
279 let y = avg(&i420.data[..luma]);
280 let u = avg(&i420.data[luma..luma + luma / 4]);
281 let v = avg(&i420.data[luma + luma / 4..]);
282 assert!((110..=140).contains(&y), "luma {y} off for a gray frame");
283 assert!((118..=138).contains(&u), "u {u} off for a gray frame");
284 assert!((118..=138).contains(&v), "v {v} off for a gray frame");
285 }
286
287 fn round_trip(mut encoder: Encoder, mut decoder: Box<dyn backend::Backend>, expect_name: &str) {
291 assert_eq!(decoder.name(), expect_name);
292
293 let mut decoded = Vec::new();
294 for i in 0..10u64 {
295 let keyframe = i == 0;
296 if keyframe {
297 encoder.keyframe();
298 }
299 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
301 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
302 }
303 }
304 decoded.extend(decoder.flush().unwrap());
305
306 assert!(!decoded.is_empty(), "decoder produced no frames");
307 for out in &decoded {
308 assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
309 }
310
311 let micros: Vec<u128> = decoded.iter().map(|d| d.timestamp.as_micros()).collect();
315 assert!(
316 micros.windows(2).all(|w| w[0] < w[1]),
317 "decoded timestamps not strictly increasing: {micros:?}"
318 );
319 assert!(
320 micros.iter().all(|&t| t % 33_333 == 0 && t < 333_330),
321 "decoded timestamp outside the fed set: {micros:?}"
322 );
323 }
324
325 fn decode_config(kind: super::Kind) -> super::Config {
327 super::Config {
328 kind,
329 ..super::Config::new()
330 }
331 }
332
333 fn h264_software_encoder(size: crate::Size) -> Encoder {
335 Encoder::new(&EncodeConfig {
336 kind: EncodeKind::Software,
337 ..EncodeConfig::new(size.width, size.height, 30)
338 })
339 .expect("openh264 encoder")
340 }
341
342 fn gray_size() -> crate::Size {
344 crate::Size::new(320, 240)
345 }
346
347 #[test]
348 fn openh264_round_trip() {
349 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
350 round_trip(h264_software_encoder(gray_size()), decoder, "openh264");
351 }
352
353 #[test]
356 fn avc1_without_avcc_decodes_as_annexb() {
357 let h264 = hang::catalog::H264 {
360 inline: false,
361 profile: 0x64,
362 constraints: 0x00,
363 level: 0x28,
364 };
365 let catalog = hang::catalog::VideoConfig::new(h264);
366 assert_eq!(catalog.codec.to_string(), "avc1.640028");
367 assert!(catalog.description.is_none());
368
369 let mut decoder = super::Decoder::new(&catalog, &decode_config(super::Kind::Software))
370 .expect("a description-less avc1 track opens rather than erroring");
371 assert!(
372 matches!(decoder.conversion, super::Conversion::Passthrough),
373 "a description-less avc1 track is read as Annex-B"
374 );
375
376 let mut encoder = h264_software_encoder(gray_size());
379 let mut decoded = Vec::new();
380 for i in 0..5u64 {
381 let keyframe = i == 0;
382 if keyframe {
383 encoder.keyframe();
384 }
385 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
386 assert!(
387 encoded.payload.starts_with(&[0, 0, 0, 1]) || encoded.payload.starts_with(&[0, 0, 1]),
388 "the test feeds Annex-B, not length-prefixed NALs"
389 );
390 decoded.extend(decoder.decode(&encoded.payload, encoded.timestamp, keyframe).unwrap());
391 }
392 }
393
394 assert!(!decoded.is_empty(), "decoder produced no frames");
395 for out in &decoded {
396 assert_gray(&out.surface.to_i420().unwrap(), 320, 240);
397 }
398 }
399
400 #[test]
401 fn av1_is_supported_by_hardware_only() {
402 let catalog = hang::catalog::VideoConfig::new(hang::catalog::AV1::default());
403 let config = decode_config(super::Kind::Software);
404 let Err(err) = super::Decoder::new(&catalog, &config) else {
405 panic!("software AV1 decode unexpectedly opened");
406 };
407 assert!(matches!(err, crate::Error::NoDecoder(_)));
408 }
409
410 #[test]
411 fn av1_rejects_unsupported_catalog_shape() {
412 let av1 = hang::catalog::AV1 {
413 bitdepth: 10,
414 ..hang::catalog::AV1::default()
415 };
416 let catalog = hang::catalog::VideoConfig::new(av1);
417 let config = decode_config(super::Kind::Auto);
418 let Err(err) = super::Decoder::new(&catalog, &config) else {
419 panic!("10-bit AV1 decode unexpectedly opened");
420 };
421 assert!(matches!(err, crate::Error::UnsupportedCodec(_)));
422 }
423
424 #[cfg(target_os = "macos")]
425 #[test]
426 fn videotoolbox_round_trip() {
427 let decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
428 .expect("videotoolbox decoder");
429 round_trip(h264_software_encoder(gray_size()), decoder, "videotoolbox");
430 }
431
432 #[cfg(target_os = "macos")]
435 fn decode_gray(count: u64) -> Vec<Frame> {
436 let mut encoder = h264_software_encoder(gray_size());
437 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Named("videotoolbox".into())))
438 .expect("videotoolbox decoder");
439
440 let mut decoded = Vec::new();
441 for i in 0..count {
442 let keyframe = i == 0;
443 if keyframe {
444 encoder.keyframe();
445 }
446 for encoded in encoder.encode(&gray_frame(i)).unwrap() {
447 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
448 }
449 }
450
451 assert!(!decoded.is_empty(), "decoder produced no frames");
452 decoded
453 }
454
455 #[cfg(target_os = "macos")]
460 #[test]
461 fn videotoolbox_decode_stays_gpu_resident() {
462 for out in &decode_gray(3) {
463 assert!(
464 matches!(out.surface, Surface::PixelBuffer(_)),
465 "VideoToolbox decode downloaded to the CPU instead of keeping its surface"
466 );
467 }
468 }
469
470 #[cfg(target_os = "macos")]
474 #[test]
475 fn videotoolbox_resized_surface_reencodes_in_place() {
476 let decoded = decode_gray(3);
477 let resized: Vec<_> = decoded
478 .iter()
479 .map(|frame| frame.resize(crate::Size::new(160, 120)).unwrap())
480 .collect();
481 for frame in &resized {
482 assert_eq!(frame.size(), crate::Size::new(160, 120));
483 assert!(
484 matches!(frame.surface, Surface::PixelBuffer(_)),
485 "VideoToolbox resize downloaded to the CPU"
486 );
487 }
488
489 let encoder = Encoder::new(&EncodeConfig {
490 kind: EncodeKind::Named("videotoolbox".into()),
491 ..EncodeConfig::new(160, 120, 30)
492 });
493 let Ok(mut encoder) = encoder else {
494 eprintln!("skipping: no VideoToolbox H.264 hardware encoder available");
495 return;
496 };
497
498 let mut packets = 0;
499 for (i, out) in resized.iter().enumerate() {
500 if i == 0 {
501 encoder.keyframe();
502 }
503 packets += encoder.encode(out).unwrap().len();
504 }
505 packets += encoder.finish().unwrap().len();
506
507 assert!(packets > 0, "re-encoding decoded surfaces produced no packets");
508 }
509
510 #[cfg(target_os = "macos")]
515 #[test]
516 fn videotoolbox_hevc_round_trip() {
517 let encoder = Encoder::new(&EncodeConfig {
518 kind: EncodeKind::Named("videotoolbox".into()),
519 codec: crate::encode::Codec::H265,
520 ..EncodeConfig::new(320, 240, 30)
521 });
522 let Ok(encoder) = encoder else {
523 eprintln!("skipping: no VideoToolbox H.265 hardware encoder available");
524 return;
525 };
526 let decoder = backend::open(Codec::H265, &decode_config(super::Kind::Named("videotoolbox".into())))
527 .expect("videotoolbox H.265 decoder");
528 round_trip(encoder, decoder, "videotoolbox");
529 }
530
531 #[cfg(target_os = "windows")]
532 #[test]
533 fn mediafoundation_round_trip() {
534 let Ok(decoder) = backend::open(
537 Codec::H264,
538 &decode_config(super::Kind::Named("mediafoundation".into())),
539 ) else {
540 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
541 return;
542 };
543 round_trip(h264_software_encoder(gray_size()), decoder, "mediafoundation");
544 }
545
546 #[cfg(target_os = "windows")]
551 fn level(index: u64) -> u8 {
552 u8::try_from(0x20 + index * 0x10).expect("test stream is short enough to keep its levels distinct")
553 }
554
555 #[cfg(target_os = "windows")]
557 fn expected_luma(level: u8) -> u32 {
558 16 + (219 * level as u32) / 255
559 }
560
561 #[cfg(target_os = "windows")]
565 fn decode_levels(count: u64, size: crate::Size) -> Option<(Vec<Frame>, Box<dyn backend::Backend>)> {
566 let mut encoder = h264_software_encoder(size);
567 let decoder = backend::open(
568 Codec::H264,
569 &decode_config(super::Kind::Named("mediafoundation".into())),
570 );
571 let Ok(mut decoder) = decoder else {
572 eprintln!("skipping: no Media Foundation H.264 hardware decoder available");
573 return None;
574 };
575
576 let mut decoded = Vec::new();
577 for i in 0..count {
578 let keyframe = i == 0;
579 if keyframe {
580 encoder.keyframe();
581 }
582 for encoded in encoder.encode(&flat_frame(i, level(i), size)).unwrap() {
583 decoded.extend(decoder.decode(encoded.payload, encoded.timestamp, keyframe).unwrap());
584 }
585 }
586
587 assert!(!decoded.is_empty(), "decoder produced no frames");
588 Some((decoded, decoder))
593 }
594
595 #[cfg(target_os = "windows")]
600 fn plane_averages(frame: &Frame) -> (u32, u32, u32) {
601 let i420 = frame.surface.to_i420().unwrap();
602 let average = |plane: &[u8]| plane.iter().map(|&b| b as u32).sum::<u32>() / plane.len() as u32;
603 (average(i420.y()), average(i420.u()), average(i420.v()))
604 }
605
606 #[cfg(target_os = "windows")]
611 #[test]
612 fn mediafoundation_decode_stays_gpu_resident() {
613 let Some((decoded, _decoder)) = decode_levels(3, gray_size()) else {
614 return;
615 };
616 for out in &decoded {
617 assert!(
618 matches!(out.surface, Surface::Texture(_)),
619 "Media Foundation decode downloaded to the CPU instead of keeping its picture on the GPU"
620 );
621 }
622 }
623
624 #[cfg(target_os = "windows")]
633 #[test]
634 fn mediafoundation_held_frames_keep_their_pixels() {
635 let Some((decoded, _decoder)) = decode_levels(12, gray_size()) else {
639 return;
640 };
641
642 for (i, out) in decoded.iter().enumerate() {
643 let (luma, _, _) = plane_averages(out);
644 let want = expected_luma(level(i as u64));
645 assert!(
648 luma.abs_diff(want) <= 6,
649 "frame {i} decoded to luma {luma}, expected about {want}: the decoder recycled its picture buffer"
650 );
651 }
652 }
653
654 #[cfg(target_os = "windows")]
661 #[test]
662 fn mediafoundation_decode_crops_coded_padding() {
663 let size = crate::Size::new(320, 180);
664 let Some((decoded, _decoder)) = decode_levels(3, size) else {
665 return;
666 };
667
668 for (i, out) in decoded.iter().enumerate() {
669 assert_eq!(out.size(), size, "frame {i} came back at the coded size");
670 let (luma, u, v) = plane_averages(out);
671 assert!(
672 luma.abs_diff(expected_luma(level(i as u64))) <= 6,
673 "frame {i} luma {luma} is not its own picture"
674 );
675 assert!(
677 u.abs_diff(128) <= 4 && v.abs_diff(128) <= 4,
678 "frame {i} chroma ({u}, {v}) is not neutral: the plane split read into the padding"
679 );
680 }
681 }
682
683 #[cfg(target_os = "windows")]
692 #[test]
693 fn mediafoundation_decoded_texture_reencodes_in_place() {
694 let size = gray_size();
695 let Some((decoded, _decoder)) = decode_levels(3, size) else {
696 return;
697 };
698 for out in &decoded {
699 assert!(
700 matches!(out.surface, Surface::Texture(_)),
701 "Media Foundation decode downloaded to the CPU"
702 );
703 }
704
705 let encoder = Encoder::new(&EncodeConfig {
706 kind: EncodeKind::Named("mediafoundation".into()),
707 ..EncodeConfig::new(size.width, size.height, 30)
708 });
709 let Ok(mut encoder) = encoder else {
710 eprintln!("skipping: no Media Foundation H.264 hardware encoder available");
711 return;
712 };
713
714 let mut reencoded = Vec::new();
715 for (i, out) in decoded.iter().enumerate() {
716 if i == 0 {
717 encoder.keyframe();
718 }
719 reencoded.extend(encoder.encode(out).unwrap());
720 }
721 reencoded.extend(encoder.finish().unwrap());
722 assert!(
723 !reencoded.is_empty(),
724 "re-encoding decoded textures produced no packets"
725 );
726
727 let mut decoder = backend::open(Codec::H264, &decode_config(super::Kind::Software)).expect("openh264 decoder");
730 let mut out = Vec::new();
731 for (i, encoded) in reencoded.iter().enumerate() {
732 out.extend(
733 decoder
734 .decode(encoded.payload.clone(), encoded.timestamp, i == 0)
735 .unwrap(),
736 );
737 }
738
739 assert_eq!(out.len(), decoded.len(), "the re-encoded stream lost frames");
743 for (i, frame) in out.iter().enumerate() {
744 assert_eq!(frame.size(), size, "re-encoded frame {i} changed size");
745 let (luma, _, _) = plane_averages(frame);
746 let want = expected_luma(level(i as u64));
747 assert!(
748 luma.abs_diff(want) <= 6,
749 "re-encoded frame {i} came back as luma {luma}, expected about {want}"
750 );
751 }
752 }
753
754 #[cfg(target_os = "windows")]
760 #[test]
761 #[ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"]
762 fn mediafoundation_resized_texture_reencodes_in_place() {
763 let target = crate::Size::new(160, 120);
764 let resize = crate::resize::Config {
765 acceleration: crate::resize::Acceleration::Gpu,
766 ..Default::default()
767 };
768 let Some((decoded, _decoder)) = decode_levels(3, gray_size()) else {
769 return;
770 };
771 let Some(device) = decoded.iter().find_map(|frame| match &frame.surface {
772 Surface::Texture(texture) => Some(texture.device()),
773 _ => None,
774 }) else {
775 panic!("Media Foundation decode did not return a Direct3D11 texture");
776 };
777 if !crate::frame::d3d11::supports_nv12_render_target(device) {
778 eprintln!("skipping: driver cannot render to NV12");
779 return;
780 }
781 let resized: Vec<_> = decoded
782 .iter()
783 .map(|frame| frame.resize_with(target, &resize).unwrap())
784 .collect();
785 for frame in &resized {
786 assert_eq!(frame.size(), target);
787 assert!(
788 matches!(frame.surface, Surface::Texture(_)),
789 "Direct3D11 resize downloaded to the CPU"
790 );
791 }
792
793 let encoder = Encoder::new(&EncodeConfig {
794 kind: EncodeKind::Named("mediafoundation".into()),
795 ..EncodeConfig::new(target.width, target.height, 30)
796 });
797 let Ok(mut encoder) = encoder else {
798 eprintln!("skipping: no Media Foundation H.264 hardware encoder available");
799 return;
800 };
801
802 let mut packets = 0;
803 for (i, out) in resized.iter().enumerate() {
804 if i == 0 {
805 encoder.keyframe();
806 }
807 packets += encoder.encode(out).unwrap().len();
808 }
809 packets += encoder.finish().unwrap().len();
810
811 assert!(packets > 0, "re-encoding resized textures produced no packets");
812 }
813
814 #[cfg(target_os = "windows")]
819 #[test]
820 fn mediafoundation_hevc_round_trip() {
821 let encoder = Encoder::new(&EncodeConfig {
822 kind: EncodeKind::Named("mediafoundation".into()),
823 codec: crate::encode::Codec::H265,
824 ..EncodeConfig::new(320, 240, 30)
825 });
826 let Ok(encoder) = encoder else {
827 eprintln!("skipping: no Media Foundation H.265 hardware encoder available");
828 return;
829 };
830 let Ok(decoder) = backend::open(
831 Codec::H265,
832 &decode_config(super::Kind::Named("mediafoundation".into())),
833 ) else {
834 eprintln!("skipping: no Media Foundation H.265 hardware decoder available");
835 return;
836 };
837 round_trip(encoder, decoder, "mediafoundation");
838 }
839}