1use std::any::Any;
4use std::fmt::Debug;
5use std::sync::Arc;
6
7use alpha2_media::{
8 CancelToken, ChromaLocation, ColorMetadata, DecodeError, DecodeEvent, DecodeRequest,
9 DecodedVideoFrame, Decoder, Orientation, PixelFormat, Rect, SampleAspectRatio, StreamInfo,
10};
11use alpha2_time::MediaTime;
12
13use crate::NativeDecoder;
14#[cfg(all(
15 any(target_os = "linux", target_os = "macos", target_os = "windows"),
16 feature = "hwdecode"
17))]
18use crate::convert::timestamp;
19use crate::decode::DecodeStageTimings;
20#[cfg(all(target_os = "windows", feature = "hwdecode"))]
21use alpha2_native_gpu::SharedD3D12Context;
22#[cfg(all(target_os = "macos", feature = "hwdecode"))]
23use alpha2_native_gpu::SharedMetalContext;
24#[cfg(all(target_os = "linux", feature = "hwdecode"))]
25use alpha2_native_gpu::SharedVulkanContext;
26#[cfg(all(
27 any(target_os = "linux", target_os = "macos", target_os = "windows"),
28 feature = "hwdecode"
29))]
30use rsmpeg::{avutil::AVFrame, ffi};
31
32#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub enum HardwareDecodeFallback {
38 ContextUnavailable,
40 NoHardwareConfig,
42 UnsupportedColor,
44 UnsupportedProfile,
47 UnsupportedSurfaceFormat,
49 SourceUnavailable,
51 DecoderOpenFailed,
53 ReverseWindow,
55 UnsupportedCrop,
60 ImportFailed,
63}
64
65impl HardwareDecodeFallback {
66 #[must_use]
68 pub const fn as_str(self) -> &'static str {
69 match self {
70 Self::ContextUnavailable => "contextUnavailable",
71 Self::NoHardwareConfig => "noHardwareConfig",
72 Self::UnsupportedColor => "unsupportedColor",
73 Self::UnsupportedProfile => "unsupportedProfile",
74 Self::UnsupportedSurfaceFormat => "unsupportedSurfaceFormat",
75 Self::SourceUnavailable => "sourceUnavailable",
76 Self::DecoderOpenFailed => "decoderOpenFailed",
77 Self::ReverseWindow => "reverseWindow",
78 Self::UnsupportedCrop => "unsupportedCrop",
79 Self::ImportFailed => "importFailed",
80 }
81 }
82}
83
84#[derive(Debug)]
86pub struct HardwareOpenError {
87 pub reason: HardwareDecodeFallback,
89 pub source: DecodeError,
91}
92
93impl HardwareOpenError {
94 #[must_use]
96 pub const fn new(reason: HardwareDecodeFallback, source: DecodeError) -> Self {
97 Self { reason, source }
98 }
99}
100
101impl std::fmt::Display for HardwareOpenError {
102 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 write!(formatter, "{}: {:?}", self.reason.as_str(), self.source)
104 }
105}
106
107impl std::error::Error for HardwareOpenError {}
108
109#[cfg(all(
111 any(target_os = "linux", target_os = "macos", target_os = "windows"),
112 feature = "hwdecode"
113))]
114pub(crate) struct HardwareDecoderSpec {
115 pub device_type: ffi::AVHWDeviceType,
117 pub pixel_format: ffi::AVPixelFormat,
119 pub backend: &'static str,
121 pub require_device_context: bool,
123 pub ten_bit: bool,
125 pub keeps_leading_crop: bool,
135 pub get_format: unsafe extern "C" fn(
137 *mut ffi::AVCodecContext,
138 *const ffi::AVPixelFormat,
139 ) -> ffi::AVPixelFormat,
140}
141
142#[derive(Copy, Clone, Debug, Eq, PartialEq)]
144pub enum NativePlaneLayout {
145 CpuYuv(alpha2_media::YuvFormat),
147 CpuRgba8,
150 CpuLinearRgbaF16,
153 GpuI420,
155 GpuNv12,
157 GpuP010,
159}
160
161impl NativePlaneLayout {
162 #[must_use]
164 pub const fn bit_depth(self) -> u8 {
165 match self {
166 Self::CpuRgba8 | Self::GpuI420 | Self::GpuNv12 => 8,
167 Self::CpuLinearRgbaF16 => 16,
168 Self::CpuYuv(format) => format.bit_depth,
169 Self::GpuP010 => 10,
170 }
171 }
172
173 #[cfg_attr(
175 not(all(
176 any(target_os = "linux", target_os = "macos", target_os = "windows"),
177 feature = "hwdecode"
178 )),
179 allow(dead_code)
180 )]
181 const fn bytes_per_pixel_numerator(self) -> u64 {
182 match self {
183 Self::CpuRgba8 => 8,
184 Self::CpuLinearRgbaF16 => 16,
185 Self::GpuI420 | Self::GpuNv12 => 3,
186 Self::GpuP010 => 6,
187 Self::CpuYuv(format) => {
190 format.bytes_per_sample() as u64
191 * (2 + (4 >> (format.chroma_shift_x + format.chroma_shift_y)))
192 }
193 }
194 }
195}
196
197#[cfg(all(
202 any(target_os = "linux", target_os = "macos", target_os = "windows"),
203 feature = "hwdecode"
204))]
205pub(crate) const fn surface_plane_layout(format: ffi::AVPixelFormat) -> Option<NativePlaneLayout> {
206 match format {
207 ffi::AV_PIX_FMT_NV12 => Some(NativePlaneLayout::GpuNv12),
208 ffi::AV_PIX_FMT_P010LE => Some(NativePlaneLayout::GpuP010),
209 _ => None,
210 }
211}
212
213#[cfg_attr(
226 not(all(target_os = "windows", feature = "hwdecode")),
227 allow(dead_code)
228)]
229pub(crate) fn visible_rect_within_surface(
230 width: u32,
231 height: u32,
232 [left, top, right, bottom]: [usize; 4],
233 surface_width: u32,
234 surface_height: u32,
235) -> Option<Rect> {
236 let x = u32::try_from(left).ok()?;
237 let y = u32::try_from(top).ok()?;
238 let visible = Rect {
239 x,
240 y,
241 width: width
242 .checked_sub(x)?
243 .checked_sub(u32::try_from(right).ok()?)?,
244 height: height
245 .checked_sub(y)?
246 .checked_sub(u32::try_from(bottom).ok()?)?,
247 };
248 (u64::from(x) + u64::from(visible.width) <= u64::from(surface_width)
249 && u64::from(y) + u64::from(visible.height) <= u64::from(surface_height))
250 .then_some(visible)
251}
252
253#[derive(Clone, Debug, PartialEq)]
260pub struct NativeVideoFrameMetadata {
261 pub pts: MediaTime,
263 pub dts: Option<MediaTime>,
265 pub duration: Option<MediaTime>,
267 pub visible_rect: Rect,
269 pub orientation: Orientation,
271 pub sample_aspect_ratio: SampleAspectRatio,
273 pub color: Option<ColorMetadata>,
275 pub chroma_location: ChromaLocation,
277 pub bit_depth: u8,
279 pub plane_layout: NativePlaneLayout,
281 pub device_identity: u64,
283 pub decoder_generation: u64,
285 pub picture_identity: u64,
287}
288
289pub trait HardwareFrameLease: Debug + Send + Sync + 'static {
291 fn retained_bytes(&self) -> u64;
293
294 fn as_any(&self) -> &dyn Any;
296
297 fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
300}
301
302#[cfg(all(target_os = "linux", feature = "hwdecode"))]
303impl HardwareFrameLease for alpha2_ffmpeg_bridge::VulkanFrameLease {
304 fn retained_bytes(&self) -> u64 {
305 self.info().map_or(0, |info| {
306 let pixels = u64::try_from(info.width)
307 .unwrap_or(0)
308 .saturating_mul(u64::try_from(info.height).unwrap_or(0));
309 let numerator = surface_plane_layout(info.software_format)
312 .unwrap_or(NativePlaneLayout::GpuNv12)
313 .bytes_per_pixel_numerator();
314 pixels.saturating_mul(numerator).div_ceil(2)
315 })
316 }
317
318 fn as_any(&self) -> &dyn Any {
319 self
320 }
321
322 fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
323 self
324 }
325}
326
327#[cfg(all(target_os = "macos", feature = "hwdecode"))]
328impl HardwareFrameLease for alpha2_ffmpeg_bridge::VideoToolboxFrameLease {
329 fn retained_bytes(&self) -> u64 {
330 self.info().map_or(0, |info| {
331 let pixels = u64::try_from(info.width)
332 .unwrap_or(0)
333 .saturating_mul(u64::try_from(info.height).unwrap_or(0));
334 let numerator = surface_plane_layout(info.software_format)
337 .unwrap_or(NativePlaneLayout::GpuNv12)
338 .bytes_per_pixel_numerator();
339 pixels.saturating_mul(numerator).div_ceil(2)
340 })
341 }
342
343 fn as_any(&self) -> &dyn Any {
344 self
345 }
346
347 fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
348 self
349 }
350}
351
352#[cfg(all(target_os = "windows", feature = "hwdecode"))]
353impl HardwareFrameLease for alpha2_ffmpeg_bridge::D3D12FrameLease {
354 fn retained_bytes(&self) -> u64 {
355 self.info().map_or(0, |info| {
356 let pixels = u64::try_from(info.width)
357 .unwrap_or(0)
358 .saturating_mul(u64::try_from(info.height).unwrap_or(0));
359 let numerator = surface_plane_layout(info.software_format)
362 .unwrap_or(NativePlaneLayout::GpuNv12)
363 .bytes_per_pixel_numerator();
364 pixels.saturating_mul(numerator).div_ceil(2)
365 })
366 }
367
368 fn as_any(&self) -> &dyn Any {
369 self
370 }
371
372 fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
373 self
374 }
375}
376
377#[derive(Debug)]
379pub enum NativeVideoFrame {
380 Cpu(DecodedVideoFrame),
382 Hardware {
384 metadata: NativeVideoFrameMetadata,
386 surface: Arc<dyn HardwareFrameLease>,
388 },
389}
390
391impl NativeVideoFrame {
392 #[must_use]
394 pub fn pts(&self) -> MediaTime {
395 match self {
396 Self::Cpu(frame) => frame.pts,
397 Self::Hardware { metadata, .. } => metadata.pts,
398 }
399 }
400
401 #[must_use]
403 pub fn duration(&self) -> Option<MediaTime> {
404 match self {
405 Self::Cpu(frame) => frame.duration,
406 Self::Hardware { metadata, .. } => metadata.duration,
407 }
408 }
409
410 #[must_use]
412 pub fn metadata(&self) -> NativeVideoFrameMetadata {
413 match self {
414 Self::Cpu(frame) => metadata_from_cpu(frame),
415 Self::Hardware { metadata, .. } => metadata.clone(),
416 }
417 }
418
419 #[must_use]
421 pub const fn is_hardware(&self) -> bool {
422 matches!(self, Self::Hardware { .. })
423 }
424
425 #[must_use]
427 pub fn as_cpu(&self) -> Option<&DecodedVideoFrame> {
428 match self {
429 Self::Cpu(frame) => Some(frame),
430 Self::Hardware { .. } => None,
431 }
432 }
433
434 pub fn to_cpu(&self) -> Result<DecodedVideoFrame, DecodeError> {
446 let (metadata, surface) = match self {
447 Self::Cpu(frame) => return Ok(frame.clone()),
448 Self::Hardware { metadata, surface } => (metadata, surface),
449 };
450 let mut software = download_surface(surface.as_ref())?;
451 software.set_pts(metadata.pts.ticks());
453 let mut scaler = crate::convert::FrameScaler::default();
454 let mut frame = crate::convert::convert_video_frame(
456 &software,
457 rsmpeg::ffi::AV_CODEC_ID_NONE,
458 crate::convert::tick_base(),
459 metadata.color.unwrap_or_default(),
460 &mut scaler,
461 )?;
462 frame.pts = metadata.pts;
463 frame.duration = metadata.duration;
464 frame.orientation = metadata.orientation;
465 frame.sample_aspect_ratio = metadata.sample_aspect_ratio;
466 frame.chroma_location = metadata.chroma_location;
467 if metadata.color.is_none() {
470 frame.color = None;
471 }
472 Ok(frame)
473 }
474
475 #[must_use]
477 pub fn retained_bytes(&self) -> u64 {
478 match self {
479 Self::Cpu(frame) => frame.planes.iter().fold(0_u64, |total, plane| {
480 total.saturating_add(u64::try_from(plane.bytes.len()).unwrap_or(u64::MAX))
481 }),
482 Self::Hardware { surface, .. } => surface.retained_bytes(),
483 }
484 }
485}
486
487#[cfg_attr(
489 not(all(
490 any(target_os = "linux", target_os = "macos", target_os = "windows"),
491 feature = "hwdecode"
492 )),
493 allow(clippy::unnecessary_wraps)
494)]
495fn download_surface(
496 surface: &dyn HardwareFrameLease,
497) -> Result<alpha2_ffmpeg_bridge::DecodedFrame, DecodeError> {
498 let any = surface.as_any();
499 let downloaded: Option<
500 Result<alpha2_ffmpeg_bridge::DecodedFrame, alpha2_ffmpeg_bridge::BridgeError>,
501 > = None;
502 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
503 let downloaded = downloaded.or_else(|| {
504 any.downcast_ref::<alpha2_ffmpeg_bridge::VulkanFrameLease>()
505 .map(alpha2_ffmpeg_bridge::VulkanFrameLease::download)
506 });
507 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
508 let downloaded = downloaded.or_else(|| {
509 any.downcast_ref::<alpha2_ffmpeg_bridge::VideoToolboxFrameLease>()
510 .map(alpha2_ffmpeg_bridge::VideoToolboxFrameLease::download)
511 });
512 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
513 let downloaded = downloaded.or_else(|| {
514 any.downcast_ref::<alpha2_ffmpeg_bridge::D3D12FrameLease>()
515 .map(alpha2_ffmpeg_bridge::D3D12FrameLease::download)
516 });
517 let _ = any;
518 downloaded
519 .ok_or_else(|| {
520 DecodeError::Unsupported("this hardware surface cannot be downloaded".into())
521 })?
522 .map_err(|error| DecodeError::Adapter(error.to_string()))
523}
524
525pub struct NativeExportDecoder {
531 backend: ExportDecoderBackend,
532 hardware_frames: u64,
534 hardware_failure: Option<HardwareDecodeFallback>,
536}
537
538enum ExportDecoderBackend {
539 Software(NativeDecoder),
540 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
541 Vulkan {
542 decoder: NativeDecoder,
543 generation: u64,
544 },
545 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
546 VideoToolbox {
547 decoder: NativeDecoder,
548 generation: u64,
549 device_identity: u64,
550 },
551 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
552 D3D12 {
553 decoder: NativeDecoder,
554 generation: u64,
555 device_identity: u64,
556 },
557}
558
559impl NativeExportDecoder {
560 pub fn take_stage_timings(&mut self) -> DecodeStageTimings {
565 match &mut self.backend {
566 ExportDecoderBackend::Software(decoder) => decoder.take_stage_timings(),
567 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
568 ExportDecoderBackend::Vulkan { decoder, .. } => decoder.take_stage_timings(),
569 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
570 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.take_stage_timings(),
571 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
572 ExportDecoderBackend::D3D12 { decoder, .. } => decoder.take_stage_timings(),
573 }
574 }
575
576 #[must_use]
578 pub const fn is_hardware(&self) -> bool {
579 match &self.backend {
580 ExportDecoderBackend::Software(_) => false,
581 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
582 ExportDecoderBackend::Vulkan { .. } => true,
583 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
584 ExportDecoderBackend::VideoToolbox { .. } => true,
585 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
586 ExportDecoderBackend::D3D12 { .. } => true,
587 }
588 }
589
590 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, DecodeError> {
595 NativeDecoder::open(path).map(|decoder| Self {
596 backend: ExportDecoderBackend::Software(decoder),
597 hardware_frames: 0,
598 hardware_failure: None,
599 })
600 }
601
602 pub fn open_with_threads(
607 path: impl AsRef<std::path::Path>,
608 threads: i64,
609 ) -> Result<Self, DecodeError> {
610 NativeDecoder::open_with_threads(path, threads).map(|decoder| Self {
611 backend: ExportDecoderBackend::Software(decoder),
612 hardware_frames: 0,
613 hardware_failure: None,
614 })
615 }
616
617 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
623 pub fn open_vulkan(
624 path: impl AsRef<std::path::Path>,
625 threads: i64,
626 context: &SharedVulkanContext,
627 generation: u64,
628 ) -> Result<Self, HardwareOpenError> {
629 let spec = HardwareDecoderSpec {
630 device_type: ffi::AV_HWDEVICE_TYPE_VULKAN,
631 pixel_format: ffi::AV_PIX_FMT_VULKAN,
632 backend: "Vulkan",
633 require_device_context: true,
634 ten_bit: context.supports_ten_bit(),
635 keeps_leading_crop: true,
636 get_format: alpha2_ffmpeg_bridge::vulkan_get_format_callback(),
637 };
638 NativeDecoder::open_hardware(path, threads, context.ffmpeg_device(), &spec).map(|decoder| {
639 Self {
640 backend: ExportDecoderBackend::Vulkan {
641 decoder,
642 generation,
643 },
644 hardware_frames: 0,
645 hardware_failure: None,
646 }
647 })
648 }
649
650 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
656 pub fn open_videotoolbox(
657 path: impl AsRef<std::path::Path>,
658 threads: i64,
659 context: &SharedMetalContext,
660 generation: u64,
661 ) -> Result<Self, HardwareOpenError> {
662 let device = rsmpeg::avutil::AVHWDeviceContext::create(
665 ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
666 None,
667 None,
668 0,
669 )
670 .map_err(crate::convert::adapter_error)
671 .map_err(|error| {
672 HardwareOpenError::new(HardwareDecodeFallback::DecoderOpenFailed, error)
673 })?;
674 let spec = HardwareDecoderSpec {
675 device_type: ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
676 pixel_format: ffi::AV_PIX_FMT_VIDEOTOOLBOX,
677 backend: "VideoToolbox",
678 require_device_context: false,
679 ten_bit: context.supports_ten_bit(),
680 keeps_leading_crop: false,
681 get_format: alpha2_ffmpeg_bridge::videotoolbox_get_format_callback(),
682 };
683 NativeDecoder::open_hardware(path, threads, device, &spec).map(|decoder| Self {
684 backend: ExportDecoderBackend::VideoToolbox {
685 decoder,
686 generation,
687 device_identity: context.identity().device,
688 },
689 hardware_frames: 0,
690 hardware_failure: None,
691 })
692 }
693
694 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
700 pub fn open_d3d12(
701 path: impl AsRef<std::path::Path>,
702 threads: i64,
703 context: &SharedD3D12Context,
704 generation: u64,
705 ) -> Result<Self, HardwareOpenError> {
706 let spec = HardwareDecoderSpec {
707 device_type: ffi::AV_HWDEVICE_TYPE_D3D12VA,
708 pixel_format: ffi::AV_PIX_FMT_D3D12,
709 backend: "D3D12VA",
710 require_device_context: true,
711 ten_bit: context.supports_ten_bit(),
712 keeps_leading_crop: true,
713 get_format: alpha2_ffmpeg_bridge::d3d12_get_format_callback(),
714 };
715 NativeDecoder::open_hardware(path, threads, context.ffmpeg_device(), &spec).map(|decoder| {
716 Self {
717 backend: ExportDecoderBackend::D3D12 {
718 decoder,
719 generation,
720 device_identity: context.identity().device,
721 },
722 hardware_frames: 0,
723 hardware_failure: None,
724 }
725 })
726 }
727
728 #[must_use]
730 pub fn streams(&self) -> &[StreamInfo] {
731 match &self.backend {
732 ExportDecoderBackend::Software(decoder) => decoder.streams(),
733 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
734 ExportDecoderBackend::Vulkan { decoder, .. } => decoder.streams(),
735 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
736 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.streams(),
737 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
738 ExportDecoderBackend::D3D12 { decoder, .. } => decoder.streams(),
739 }
740 }
741
742 #[must_use]
744 pub fn video_duration(&self) -> Option<MediaTime> {
745 match &self.backend {
746 ExportDecoderBackend::Software(decoder) => decoder.video_duration(),
747 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
748 ExportDecoderBackend::Vulkan { decoder, .. } => decoder.video_duration(),
749 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
750 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.video_duration(),
751 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
752 ExportDecoderBackend::D3D12 { decoder, .. } => decoder.video_duration(),
753 }
754 }
755
756 pub fn flush(&mut self) -> Result<(), DecodeError> {
761 match &mut self.backend {
762 ExportDecoderBackend::Software(decoder) => decoder.flush(),
763 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
764 ExportDecoderBackend::Vulkan { decoder, .. } => decoder.flush(),
765 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
766 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.flush(),
767 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
768 ExportDecoderBackend::D3D12 { decoder, .. } => decoder.flush(),
769 }
770 }
771
772 pub fn seek_video(
777 &mut self,
778 request: DecodeRequest,
779 cancel: &CancelToken,
780 ) -> Result<NativeVideoFrame, DecodeError> {
781 let result = self.seek_video_on_backend(request, cancel);
782 self.observe(result, |_| true)
783 }
784
785 pub fn decode_video_next(
790 &mut self,
791 cancel: &CancelToken,
792 ) -> Result<Option<NativeVideoFrame>, DecodeError> {
793 let result = self.decode_video_next_on_backend(cancel);
794 self.observe(result, Option::is_some)
795 }
796
797 pub fn peek_video_pts(
807 &mut self,
808 cancel: &CancelToken,
809 ) -> Result<Option<MediaTime>, DecodeError> {
810 let result = self.native_decoder().peek_raw_pts(cancel);
811 self.observe(result, |_| false)
812 }
813
814 pub fn set_decode_policy(&mut self, policy: crate::DecodePolicy) {
816 self.native_decoder().set_decode_policy(policy);
817 }
818
819 pub fn with_declared_color(
827 mut self,
828 color: Option<ColorMetadata>,
829 ) -> Result<Self, HardwareOpenError> {
830 if self.is_hardware() && color.is_some_and(ColorMetadata::is_hdr) {
831 return Err(HardwareOpenError::new(
832 HardwareDecodeFallback::UnsupportedColor,
833 DecodeError::Unsupported("hardware export currently supports SDR only".into()),
834 ));
835 }
836 self.native_decoder().declare_color(color);
837 Ok(self)
838 }
839
840 pub fn set_decode_reporter(
842 &mut self,
843 reporter: impl Fn(crate::DecodeDiagnostic) + Send + Sync + 'static,
844 ) {
845 self.native_decoder().set_decode_reporter(reporter);
846 }
847
848 pub fn decode_diagnostics(&mut self) -> &crate::DecodeDiagnostics {
850 self.native_decoder().decode_diagnostics()
851 }
852
853 #[must_use]
856 pub fn presentation_deadline(&self, time: MediaTime) -> MediaTime {
857 match &self.backend {
858 ExportDecoderBackend::Software(decoder) => decoder,
859 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
860 ExportDecoderBackend::Vulkan { decoder, .. } => decoder,
861 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
862 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder,
863 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
864 ExportDecoderBackend::D3D12 { decoder, .. } => decoder,
865 }
866 .presentation_deadline(time)
867 }
868
869 fn native_decoder(&mut self) -> &mut NativeDecoder {
871 match &mut self.backend {
872 ExportDecoderBackend::Software(decoder) => decoder,
873 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
874 ExportDecoderBackend::Vulkan { decoder, .. } => decoder,
875 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
876 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder,
877 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
878 ExportDecoderBackend::D3D12 { decoder, .. } => decoder,
879 }
880 }
881
882 #[must_use]
885 pub const fn hardware_failure(&self) -> Option<HardwareDecodeFallback> {
886 self.hardware_failure
887 }
888
889 fn observe<T>(
900 &mut self,
901 result: Result<T, DecodeError>,
902 produced: impl Fn(&T) -> bool,
903 ) -> Result<T, DecodeError> {
904 if !self.is_hardware() {
905 return result;
906 }
907 match result {
908 Ok(value) => {
909 if produced(&value) {
910 self.hardware_frames = self.hardware_frames.saturating_add(1);
911 }
912 Ok(value)
913 }
914 Err(DecodeError::Adapter(message)) if self.hardware_frames == 0 => {
915 self.hardware_failure = Some(HardwareDecodeFallback::DecoderOpenFailed);
916 Err(DecodeError::Unsupported(format!(
917 "the hardware decoder failed before its first frame: {message}"
918 )))
919 }
920 Err(DecodeError::Unsupported(message)) => {
921 self.hardware_failure
922 .get_or_insert(HardwareDecodeFallback::UnsupportedSurfaceFormat);
923 Err(DecodeError::Unsupported(message))
924 }
925 Err(error) => Err(error),
926 }
927 }
928
929 fn seek_video_on_backend(
930 &mut self,
931 request: DecodeRequest,
932 cancel: &CancelToken,
933 ) -> Result<NativeVideoFrame, DecodeError> {
934 match &mut self.backend {
935 ExportDecoderBackend::Software(decoder) => match decoder.seek(request, cancel)?.event {
936 DecodeEvent::Video(frame) => Ok(NativeVideoFrame::Cpu(frame)),
937 _ => Err(DecodeError::Adapter(
938 "video decoder returned a non-video event".into(),
939 )),
940 },
941 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
942 ExportDecoderBackend::Vulkan {
943 decoder,
944 generation,
945 } => {
946 let frame = decoder.seek_raw(request.time, cancel)?;
947 hardware_frame(decoder, frame.into_frame(), *generation)
948 }
949 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
950 ExportDecoderBackend::VideoToolbox {
951 decoder,
952 generation,
953 device_identity,
954 } => {
955 let frame = decoder.seek_raw(request.time, cancel)?;
956 videotoolbox_frame(decoder, frame.into_frame(), *generation, *device_identity)
957 }
958 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
959 ExportDecoderBackend::D3D12 {
960 decoder,
961 generation,
962 device_identity,
963 } => {
964 let frame = decoder.seek_raw(request.time, cancel)?;
965 d3d12_frame(
966 decoder,
967 frame.into_frame(),
968 *generation,
969 *device_identity,
970 &mut self.hardware_failure,
971 )
972 }
973 }
974 }
975
976 fn decode_video_next_on_backend(
977 &mut self,
978 cancel: &CancelToken,
979 ) -> Result<Option<NativeVideoFrame>, DecodeError> {
980 match &mut self.backend {
981 ExportDecoderBackend::Software(decoder) => match decoder.decode_next(cancel)? {
982 DecodeEvent::Video(frame) => Ok(Some(NativeVideoFrame::Cpu(frame))),
983 DecodeEvent::EndOfStream => Ok(None),
984 _ => Err(DecodeError::Adapter(
985 "video decoder returned a non-video event".into(),
986 )),
987 },
988 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
989 ExportDecoderBackend::Vulkan {
990 decoder,
991 generation,
992 } => decoder
993 .decode_raw_next(cancel)?
994 .map(|frame| hardware_frame(decoder, frame.into_frame(), *generation))
995 .transpose(),
996 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
997 ExportDecoderBackend::VideoToolbox {
998 decoder,
999 generation,
1000 device_identity,
1001 } => decoder
1002 .decode_raw_next(cancel)?
1003 .map(|frame| {
1004 videotoolbox_frame(decoder, frame.into_frame(), *generation, *device_identity)
1005 })
1006 .transpose(),
1007 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
1008 ExportDecoderBackend::D3D12 {
1009 decoder,
1010 generation,
1011 device_identity,
1012 } => decoder
1013 .decode_raw_next(cancel)?
1014 .map(|frame| {
1015 d3d12_frame(
1016 decoder,
1017 frame.into_frame(),
1018 *generation,
1019 *device_identity,
1020 &mut self.hardware_failure,
1021 )
1022 })
1023 .transpose(),
1024 }
1025 }
1026}
1027
1028#[cfg(all(target_os = "windows", feature = "hwdecode"))]
1035fn d3d12_frame(
1036 decoder: &NativeDecoder,
1037 frame: AVFrame,
1038 generation: u64,
1039 device_identity: u64,
1040 failure: &mut Option<HardwareDecodeFallback>,
1041) -> Result<NativeVideoFrame, DecodeError> {
1042 let info = alpha2_ffmpeg_bridge::d3d12_frame_info(&frame)
1043 .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1044 let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1045 DecodeError::Unsupported(format!(
1046 "D3D12VA output {} is not an importable 4:2:0 surface",
1047 info.software_format
1048 ))
1049 })?;
1050 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
1051 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
1052 let crop = [
1053 frame.crop_left,
1054 frame.crop_top,
1055 frame.crop_right,
1056 frame.crop_bottom,
1057 ];
1058 let Some(visible_rect) =
1059 visible_rect_within_surface(width, height, crop, info.surface_width, info.surface_height)
1060 else {
1061 *failure = Some(HardwareDecodeFallback::UnsupportedCrop);
1062 return Err(DecodeError::Unsupported(format!(
1063 "D3D12VA decoded a {width}x{height} picture cropped {} columns from the left and {} rows from the top into a {}x{} surface, which does not hold it",
1064 frame.crop_left, frame.crop_top, info.surface_width, info.surface_height
1065 )));
1066 };
1067 let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1068 frame.pts
1069 } else {
1070 frame.best_effort_timestamp
1071 };
1072 let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1073 let metadata = NativeVideoFrameMetadata {
1074 pts,
1075 dts: None,
1076 duration: timestamp(frame.duration, decoder.time_base),
1077 visible_rect,
1078 orientation: decoder.orientation,
1079 sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1080 color: decoder.picture_color(&frame),
1081 chroma_location: decoder.picture_chroma_location(&frame),
1082 bit_depth: plane_layout.bit_depth(),
1083 plane_layout,
1084 device_identity,
1085 decoder_generation: generation,
1086 picture_identity: info.resource
1087 ^ info.fence_value
1088 ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1089 };
1090 Ok(NativeVideoFrame::Hardware {
1091 metadata,
1092 surface: Arc::new(alpha2_ffmpeg_bridge::D3D12FrameLease::new(frame)),
1093 })
1094}
1095
1096#[cfg(all(target_os = "macos", feature = "hwdecode"))]
1097fn videotoolbox_frame(
1098 decoder: &NativeDecoder,
1099 frame: AVFrame,
1100 generation: u64,
1101 device_identity: u64,
1102) -> Result<NativeVideoFrame, DecodeError> {
1103 let info = alpha2_ffmpeg_bridge::videotoolbox_frame_info(&frame)
1104 .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1105 let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1106 DecodeError::Unsupported(format!(
1107 "VideoToolbox output {} is not an importable 4:2:0 surface",
1108 info.software_format
1109 ))
1110 })?;
1111 let width = u32::try_from(info.width).map_err(|_| DecodeError::OutOfRange)?;
1112 let height = u32::try_from(info.height).map_err(|_| DecodeError::OutOfRange)?;
1113 let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1114 frame.pts
1115 } else {
1116 frame.best_effort_timestamp
1117 };
1118 let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1119 let metadata = NativeVideoFrameMetadata {
1120 pts,
1121 dts: None,
1122 duration: timestamp(frame.duration, decoder.time_base),
1123 visible_rect: Rect {
1124 x: u32::try_from(frame.crop_left).unwrap_or(0),
1125 y: u32::try_from(frame.crop_top).unwrap_or(0),
1126 width: width.saturating_sub(
1127 u32::try_from(frame.crop_left.saturating_add(frame.crop_right)).unwrap_or(0),
1128 ),
1129 height: height.saturating_sub(
1130 u32::try_from(frame.crop_top.saturating_add(frame.crop_bottom)).unwrap_or(0),
1131 ),
1132 },
1133 orientation: decoder.orientation,
1134 sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1135 color: decoder.picture_color(&frame),
1136 chroma_location: decoder.picture_chroma_location(&frame),
1137 bit_depth: plane_layout.bit_depth(),
1138 plane_layout,
1139 device_identity,
1140 decoder_generation: generation,
1141 picture_identity: info.pixel_buffer ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1142 };
1143 Ok(NativeVideoFrame::Hardware {
1144 metadata,
1145 surface: Arc::new(alpha2_ffmpeg_bridge::VideoToolboxFrameLease::new(frame)),
1146 })
1147}
1148
1149#[cfg(all(target_os = "linux", feature = "hwdecode"))]
1150fn hardware_frame(
1151 decoder: &NativeDecoder,
1152 frame: AVFrame,
1153 generation: u64,
1154) -> Result<NativeVideoFrame, DecodeError> {
1155 let info = alpha2_ffmpeg_bridge::vulkan_frame_info(&frame)
1156 .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1157 let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1158 DecodeError::Unsupported(format!(
1159 "Vulkan output {} is not an importable 4:2:0 surface",
1160 info.software_format
1161 ))
1162 })?;
1163 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
1166 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
1167 let coded_width = u32::try_from(info.coded_width).map_err(|_| DecodeError::OutOfRange)?;
1168 let coded_height = u32::try_from(info.coded_height).map_err(|_| DecodeError::OutOfRange)?;
1169 if coded_width < width || coded_height < height {
1170 return Err(DecodeError::Adapter(
1171 "Vulkan frame is larger than its allocated surface".into(),
1172 ));
1173 }
1174 let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1175 frame.pts
1176 } else {
1177 frame.best_effort_timestamp
1178 };
1179 let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1180 let metadata = NativeVideoFrameMetadata {
1181 pts,
1182 dts: None,
1183 duration: timestamp(frame.duration, decoder.time_base),
1184 visible_rect: Rect {
1185 x: u32::try_from(frame.crop_left).unwrap_or(0),
1186 y: u32::try_from(frame.crop_top).unwrap_or(0),
1187 width: width.saturating_sub(
1188 u32::try_from(frame.crop_left.saturating_add(frame.crop_right)).unwrap_or(0),
1189 ),
1190 height: height.saturating_sub(
1191 u32::try_from(frame.crop_top.saturating_add(frame.crop_bottom)).unwrap_or(0),
1192 ),
1193 },
1194 orientation: decoder.orientation,
1195 sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1196 color: decoder.picture_color(&frame),
1197 chroma_location: decoder.picture_chroma_location(&frame),
1198 bit_depth: plane_layout.bit_depth(),
1199 plane_layout,
1200 device_identity: info.device,
1201 decoder_generation: generation,
1202 picture_identity: info.images[0] ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1203 };
1204 Ok(NativeVideoFrame::Hardware {
1205 metadata,
1206 surface: Arc::new(alpha2_ffmpeg_bridge::VulkanFrameLease::new(frame)),
1207 })
1208}
1209
1210fn metadata_from_cpu(frame: &DecodedVideoFrame) -> NativeVideoFrameMetadata {
1211 let plane_layout = match frame.pixel_format {
1212 PixelFormat::Yuv(format) => NativePlaneLayout::CpuYuv(format),
1213 PixelFormat::LinearRgbaF16 => NativePlaneLayout::CpuLinearRgbaF16,
1214 _ => NativePlaneLayout::CpuRgba8,
1215 };
1216 NativeVideoFrameMetadata {
1217 pts: frame.pts,
1218 dts: frame.dts,
1219 duration: frame.duration,
1220 visible_rect: frame.visible_rect,
1221 orientation: frame.orientation,
1222 sample_aspect_ratio: frame.sample_aspect_ratio,
1223 color: frame.color,
1224 chroma_location: frame.chroma_location,
1225 bit_depth: plane_layout.bit_depth(),
1226 plane_layout,
1227 device_identity: 0,
1228 decoder_generation: 0,
1229 picture_identity: u64::from_ne_bytes(frame.pts.ticks().to_ne_bytes()),
1230 }
1231}