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)]
34pub enum HardwareDecodeFallback {
35 ContextUnavailable,
37 NoHardwareConfig,
39 UnsupportedColor,
41 UnsupportedProfile,
44 UnsupportedSurfaceFormat,
46 SourceUnavailable,
48 DecoderOpenFailed,
50 ReverseWindow,
52 UnsupportedCrop,
56 ImportFailed,
58 AlphaChannel,
61}
62
63impl HardwareDecodeFallback {
64 #[must_use]
66 pub const fn as_str(self) -> &'static str {
67 match self {
68 Self::ContextUnavailable => "contextUnavailable",
69 Self::NoHardwareConfig => "noHardwareConfig",
70 Self::UnsupportedColor => "unsupportedColor",
71 Self::UnsupportedProfile => "unsupportedProfile",
72 Self::UnsupportedSurfaceFormat => "unsupportedSurfaceFormat",
73 Self::SourceUnavailable => "sourceUnavailable",
74 Self::DecoderOpenFailed => "decoderOpenFailed",
75 Self::ReverseWindow => "reverseWindow",
76 Self::UnsupportedCrop => "unsupportedCrop",
77 Self::ImportFailed => "importFailed",
78 Self::AlphaChannel => "alphaChannel",
79 }
80 }
81}
82
83#[derive(Debug)]
85pub struct HardwareOpenError {
86 pub reason: HardwareDecodeFallback,
88 pub source: DecodeError,
90}
91
92impl HardwareOpenError {
93 #[must_use]
95 pub const fn new(reason: HardwareDecodeFallback, source: DecodeError) -> Self {
96 Self { reason, source }
97 }
98}
99
100impl std::fmt::Display for HardwareOpenError {
101 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 write!(formatter, "{}: {:?}", self.reason.as_str(), self.source)
103 }
104}
105
106impl std::error::Error for HardwareOpenError {}
107
108#[cfg(all(
110 any(target_os = "linux", target_os = "macos", target_os = "windows"),
111 feature = "hwdecode"
112))]
113pub(crate) struct HardwareDecoderSpec<'a> {
114 pub device_type: ffi::AVHWDeviceType,
116 pub pixel_format: ffi::AVPixelFormat,
118 pub backend: &'static str,
120 pub require_device_context: bool,
122 pub ten_bit: bool,
124 pub keeps_leading_crop: bool,
131 pub hardware_codecs: Option<&'a [&'static str]>,
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,
149 CpuLinearRgbaF16,
151 GpuI420,
153 GpuNv12,
155 GpuP010,
157}
158
159impl NativePlaneLayout {
160 #[must_use]
162 pub const fn bit_depth(self) -> u8 {
163 match self {
164 Self::CpuRgba8 | Self::GpuI420 | Self::GpuNv12 => 8,
165 Self::CpuLinearRgbaF16 => 16,
166 Self::CpuYuv(format) => format.bit_depth,
167 Self::GpuP010 => 10,
168 }
169 }
170
171 #[cfg_attr(
173 not(all(
174 any(target_os = "linux", target_os = "macos", target_os = "windows"),
175 feature = "hwdecode"
176 )),
177 allow(dead_code)
178 )]
179 const fn bytes_per_pixel_numerator(self) -> u64 {
180 match self {
181 Self::CpuRgba8 => 8,
182 Self::CpuLinearRgbaF16 => 16,
183 Self::GpuI420 | Self::GpuNv12 => 3,
184 Self::GpuP010 => 6,
185 Self::CpuYuv(format) => {
188 format.bytes_per_sample() as u64
189 * (2 + (4 >> (format.chroma_shift_x + format.chroma_shift_y)))
190 }
191 }
192 }
193}
194
195#[cfg(all(
198 any(target_os = "linux", target_os = "macos", target_os = "windows"),
199 feature = "hwdecode"
200))]
201pub(crate) const fn surface_plane_layout(format: ffi::AVPixelFormat) -> Option<NativePlaneLayout> {
202 match format {
203 ffi::AV_PIX_FMT_NV12 => Some(NativePlaneLayout::GpuNv12),
204 ffi::AV_PIX_FMT_P010LE => Some(NativePlaneLayout::GpuP010),
205 _ => None,
206 }
207}
208
209#[cfg_attr(
216 not(all(target_os = "windows", feature = "hwdecode")),
217 allow(dead_code)
218)]
219pub(crate) fn visible_rect_within_surface(
220 width: u32,
221 height: u32,
222 [left, top, right, bottom]: [usize; 4],
223 surface_width: u32,
224 surface_height: u32,
225) -> Option<Rect> {
226 let x = u32::try_from(left).ok()?;
227 let y = u32::try_from(top).ok()?;
228 let visible = Rect {
229 x,
230 y,
231 width: width
232 .checked_sub(x)?
233 .checked_sub(u32::try_from(right).ok()?)?,
234 height: height
235 .checked_sub(y)?
236 .checked_sub(u32::try_from(bottom).ok()?)?,
237 };
238 (u64::from(x) + u64::from(visible.width) <= u64::from(surface_width)
239 && u64::from(y) + u64::from(visible.height) <= u64::from(surface_height))
240 .then_some(visible)
241}
242
243#[derive(Clone, Debug, PartialEq)]
248pub struct NativeVideoFrameMetadata {
249 pub pts: MediaTime,
251 pub dts: Option<MediaTime>,
253 pub duration: Option<MediaTime>,
255 pub visible_rect: Rect,
257 pub orientation: Orientation,
259 pub sample_aspect_ratio: SampleAspectRatio,
261 pub color: Option<ColorMetadata>,
263 pub chroma_location: ChromaLocation,
265 pub bit_depth: u8,
267 pub plane_layout: NativePlaneLayout,
269 pub device_identity: u64,
271 pub decoder_generation: u64,
273 pub picture_identity: u64,
275}
276
277pub trait HardwareFrameLease: Debug + Send + Sync + 'static {
279 fn retained_bytes(&self) -> u64;
281
282 fn as_any(&self) -> &dyn Any;
284
285 fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
287}
288
289#[cfg(all(target_os = "linux", feature = "hwdecode"))]
290impl HardwareFrameLease for alpha2_ffmpeg_bridge::VulkanFrameLease {
291 fn retained_bytes(&self) -> u64 {
292 self.info().map_or(0, |info| {
293 let pixels = u64::try_from(info.width)
294 .unwrap_or(0)
295 .saturating_mul(u64::try_from(info.height).unwrap_or(0));
296 let numerator = surface_plane_layout(info.software_format)
299 .unwrap_or(NativePlaneLayout::GpuNv12)
300 .bytes_per_pixel_numerator();
301 pixels.saturating_mul(numerator).div_ceil(2)
302 })
303 }
304
305 fn as_any(&self) -> &dyn Any {
306 self
307 }
308
309 fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
310 self
311 }
312}
313
314#[cfg(all(target_os = "macos", feature = "hwdecode"))]
315impl HardwareFrameLease for alpha2_ffmpeg_bridge::VideoToolboxFrameLease {
316 fn retained_bytes(&self) -> u64 {
317 self.info().map_or(0, |info| {
318 let pixels = u64::try_from(info.width)
319 .unwrap_or(0)
320 .saturating_mul(u64::try_from(info.height).unwrap_or(0));
321 let numerator = surface_plane_layout(info.software_format)
324 .unwrap_or(NativePlaneLayout::GpuNv12)
325 .bytes_per_pixel_numerator();
326 pixels.saturating_mul(numerator).div_ceil(2)
327 })
328 }
329
330 fn as_any(&self) -> &dyn Any {
331 self
332 }
333
334 fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
335 self
336 }
337}
338
339#[cfg(all(target_os = "windows", feature = "hwdecode"))]
340impl HardwareFrameLease for alpha2_ffmpeg_bridge::D3D12FrameLease {
341 fn retained_bytes(&self) -> u64 {
342 self.info().map_or(0, |info| {
343 let pixels = u64::try_from(info.width)
344 .unwrap_or(0)
345 .saturating_mul(u64::try_from(info.height).unwrap_or(0));
346 let numerator = surface_plane_layout(info.software_format)
349 .unwrap_or(NativePlaneLayout::GpuNv12)
350 .bytes_per_pixel_numerator();
351 pixels.saturating_mul(numerator).div_ceil(2)
352 })
353 }
354
355 fn as_any(&self) -> &dyn Any {
356 self
357 }
358
359 fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
360 self
361 }
362}
363
364#[derive(Debug)]
366pub enum NativeVideoFrame {
367 Cpu(DecodedVideoFrame),
369 Hardware {
371 metadata: NativeVideoFrameMetadata,
373 surface: Arc<dyn HardwareFrameLease>,
375 },
376}
377
378impl NativeVideoFrame {
379 #[must_use]
381 pub fn pts(&self) -> MediaTime {
382 match self {
383 Self::Cpu(frame) => frame.pts,
384 Self::Hardware { metadata, .. } => metadata.pts,
385 }
386 }
387
388 #[must_use]
390 pub fn duration(&self) -> Option<MediaTime> {
391 match self {
392 Self::Cpu(frame) => frame.duration,
393 Self::Hardware { metadata, .. } => metadata.duration,
394 }
395 }
396
397 #[must_use]
399 pub fn metadata(&self) -> NativeVideoFrameMetadata {
400 match self {
401 Self::Cpu(frame) => metadata_from_cpu(frame),
402 Self::Hardware { metadata, .. } => metadata.clone(),
403 }
404 }
405
406 #[must_use]
408 pub const fn is_hardware(&self) -> bool {
409 matches!(self, Self::Hardware { .. })
410 }
411
412 #[must_use]
414 pub fn as_cpu(&self) -> Option<&DecodedVideoFrame> {
415 match self {
416 Self::Cpu(frame) => Some(frame),
417 Self::Hardware { .. } => None,
418 }
419 }
420
421 pub fn to_cpu(&self) -> Result<DecodedVideoFrame, DecodeError> {
429 let (metadata, surface) = match self {
430 Self::Cpu(frame) => return Ok(frame.clone()),
431 Self::Hardware { metadata, surface } => (metadata, surface),
432 };
433 let mut software = download_surface(surface.as_ref())?;
434 software.set_pts(metadata.pts.ticks());
436 let mut scaler = crate::convert::FrameScaler::default();
437 let mut frame = crate::convert::convert_video_frame(
439 &software,
440 rsmpeg::ffi::AV_CODEC_ID_NONE,
441 crate::convert::tick_base(),
442 metadata.color.unwrap_or_default(),
443 &mut scaler,
444 )?;
445 frame.pts = metadata.pts;
446 frame.duration = metadata.duration;
447 frame.orientation = metadata.orientation;
448 frame.sample_aspect_ratio = metadata.sample_aspect_ratio;
449 frame.chroma_location = metadata.chroma_location;
450 if metadata.color.is_none() {
453 frame.color = None;
454 }
455 Ok(frame)
456 }
457
458 #[must_use]
460 pub fn retained_bytes(&self) -> u64 {
461 match self {
462 Self::Cpu(frame) => frame.planes.iter().fold(0_u64, |total, plane| {
463 total.saturating_add(u64::try_from(plane.bytes.len()).unwrap_or(u64::MAX))
464 }),
465 Self::Hardware { surface, .. } => surface.retained_bytes(),
466 }
467 }
468}
469
470#[cfg_attr(
472 not(all(
473 any(target_os = "linux", target_os = "macos", target_os = "windows"),
474 feature = "hwdecode"
475 )),
476 allow(clippy::unnecessary_wraps)
477)]
478fn download_surface(
479 surface: &dyn HardwareFrameLease,
480) -> Result<alpha2_ffmpeg_bridge::DecodedFrame, DecodeError> {
481 let any = surface.as_any();
482 let downloaded: Option<
483 Result<alpha2_ffmpeg_bridge::DecodedFrame, alpha2_ffmpeg_bridge::BridgeError>,
484 > = None;
485 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
486 let downloaded = downloaded.or_else(|| {
487 any.downcast_ref::<alpha2_ffmpeg_bridge::VulkanFrameLease>()
488 .map(alpha2_ffmpeg_bridge::VulkanFrameLease::download)
489 });
490 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
491 let downloaded = downloaded.or_else(|| {
492 any.downcast_ref::<alpha2_ffmpeg_bridge::VideoToolboxFrameLease>()
493 .map(alpha2_ffmpeg_bridge::VideoToolboxFrameLease::download)
494 });
495 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
496 let downloaded = downloaded.or_else(|| {
497 any.downcast_ref::<alpha2_ffmpeg_bridge::D3D12FrameLease>()
498 .map(alpha2_ffmpeg_bridge::D3D12FrameLease::download)
499 });
500 let _ = any;
501 downloaded
502 .ok_or_else(|| {
503 DecodeError::Unsupported("this hardware surface cannot be downloaded".into())
504 })?
505 .map_err(|error| DecodeError::Adapter(error.to_string()))
506}
507
508pub struct NativeExportDecoder {
513 backend: ExportDecoderBackend,
514 hardware_frames: u64,
515 hardware_failure: Option<HardwareDecodeFallback>,
517}
518
519enum ExportDecoderBackend {
520 Software(NativeDecoder),
521 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
522 Vulkan {
523 decoder: NativeDecoder,
524 generation: u64,
525 },
526 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
527 VideoToolbox {
528 decoder: NativeDecoder,
529 generation: u64,
530 device_identity: u64,
531 },
532 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
533 D3D12 {
534 decoder: NativeDecoder,
535 generation: u64,
536 device_identity: u64,
537 },
538}
539
540impl NativeExportDecoder {
541 pub fn take_stage_timings(&mut self) -> DecodeStageTimings {
544 match &mut self.backend {
545 ExportDecoderBackend::Software(decoder) => decoder.take_stage_timings(),
546 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
547 ExportDecoderBackend::Vulkan { decoder, .. } => decoder.take_stage_timings(),
548 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
549 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.take_stage_timings(),
550 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
551 ExportDecoderBackend::D3D12 { decoder, .. } => decoder.take_stage_timings(),
552 }
553 }
554
555 #[must_use]
557 pub const fn is_hardware(&self) -> bool {
558 match &self.backend {
559 ExportDecoderBackend::Software(_) => false,
560 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
561 ExportDecoderBackend::Vulkan { .. } => true,
562 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
563 ExportDecoderBackend::VideoToolbox { .. } => true,
564 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
565 ExportDecoderBackend::D3D12 { .. } => true,
566 }
567 }
568
569 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, DecodeError> {
574 NativeDecoder::open(path).map(|decoder| Self {
575 backend: ExportDecoderBackend::Software(decoder),
576 hardware_frames: 0,
577 hardware_failure: None,
578 })
579 }
580
581 pub fn open_with_threads(
586 path: impl AsRef<std::path::Path>,
587 threads: i64,
588 ) -> Result<Self, DecodeError> {
589 NativeDecoder::open_with_threads(path, threads).map(|decoder| Self {
590 backend: ExportDecoderBackend::Software(decoder),
591 hardware_frames: 0,
592 hardware_failure: None,
593 })
594 }
595
596 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
602 pub fn open_vulkan(
603 path: impl AsRef<std::path::Path>,
604 threads: i64,
605 context: &SharedVulkanContext,
606 generation: u64,
607 ) -> Result<Self, HardwareOpenError> {
608 let spec = HardwareDecoderSpec {
609 device_type: ffi::AV_HWDEVICE_TYPE_VULKAN,
610 pixel_format: ffi::AV_PIX_FMT_VULKAN,
611 backend: "Vulkan",
612 require_device_context: true,
613 ten_bit: context.supports_ten_bit(),
614 keeps_leading_crop: true,
615 hardware_codecs: Some(context.decode_codecs()),
618 get_format: alpha2_ffmpeg_bridge::vulkan_get_format_callback(),
619 };
620 NativeDecoder::open_hardware(path, threads, context.ffmpeg_device(), &spec).map(|decoder| {
621 Self {
622 backend: ExportDecoderBackend::Vulkan {
623 decoder,
624 generation,
625 },
626 hardware_frames: 0,
627 hardware_failure: None,
628 }
629 })
630 }
631
632 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
638 pub fn open_videotoolbox(
639 path: impl AsRef<std::path::Path>,
640 threads: i64,
641 context: &SharedMetalContext,
642 generation: u64,
643 ) -> Result<Self, HardwareOpenError> {
644 let mark = alpha2_ffmpeg_bridge::log_mark();
647 let device = rsmpeg::avutil::AVHWDeviceContext::create(
648 ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
649 None,
650 None,
651 0,
652 )
653 .map_err(crate::convert::adapter_error)
654 .map_err(|error| {
655 HardwareOpenError::new(HardwareDecodeFallback::DecoderOpenFailed, error)
656 })?;
657 let spec = HardwareDecoderSpec {
658 device_type: ffi::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
659 pixel_format: ffi::AV_PIX_FMT_VIDEOTOOLBOX,
660 backend: "VideoToolbox",
661 require_device_context: false,
662 ten_bit: context.supports_ten_bit(),
663 keeps_leading_crop: false,
664 hardware_codecs: Some(context.decode_codecs()),
665 get_format: alpha2_ffmpeg_bridge::videotoolbox_get_format_callback(),
666 };
667 NativeDecoder::open_hardware(path, threads, device, &spec)
668 .map(|decoder| Self {
669 backend: ExportDecoderBackend::VideoToolbox {
670 decoder,
671 generation,
672 device_identity: context.identity().device,
673 },
674 hardware_frames: 0,
675 hardware_failure: None,
676 })
677 .map_err(|mut error| {
678 error.source = with_videotoolbox_log(error.source, mark);
679 error
680 })
681 }
682
683 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
689 pub fn open_d3d12(
690 path: impl AsRef<std::path::Path>,
691 threads: i64,
692 context: &SharedD3D12Context,
693 generation: u64,
694 ) -> Result<Self, HardwareOpenError> {
695 let spec = HardwareDecoderSpec {
696 device_type: ffi::AV_HWDEVICE_TYPE_D3D12VA,
697 pixel_format: ffi::AV_PIX_FMT_D3D12,
698 backend: "D3D12VA",
699 require_device_context: true,
700 ten_bit: context.supports_ten_bit(),
701 keeps_leading_crop: true,
702 hardware_codecs: None,
703 get_format: alpha2_ffmpeg_bridge::d3d12_get_format_callback(),
704 };
705 NativeDecoder::open_hardware(path, threads, context.ffmpeg_device(), &spec).map(|decoder| {
706 Self {
707 backend: ExportDecoderBackend::D3D12 {
708 decoder,
709 generation,
710 device_identity: context.identity().device,
711 },
712 hardware_frames: 0,
713 hardware_failure: None,
714 }
715 })
716 }
717
718 #[must_use]
720 pub fn streams(&self) -> &[StreamInfo] {
721 match &self.backend {
722 ExportDecoderBackend::Software(decoder) => decoder.streams(),
723 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
724 ExportDecoderBackend::Vulkan { decoder, .. } => decoder.streams(),
725 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
726 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.streams(),
727 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
728 ExportDecoderBackend::D3D12 { decoder, .. } => decoder.streams(),
729 }
730 }
731
732 #[must_use]
735 pub fn dropped_layer(&self) -> Option<&crate::DroppedLayer> {
736 match &self.backend {
737 ExportDecoderBackend::Software(decoder) => decoder.dropped_layer(),
738 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
739 ExportDecoderBackend::Vulkan { .. } => None,
740 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
741 ExportDecoderBackend::VideoToolbox { .. } => None,
742 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
743 ExportDecoderBackend::D3D12 { .. } => None,
744 }
745 }
746
747 #[must_use]
749 pub fn video_duration(&self) -> Option<MediaTime> {
750 match &self.backend {
751 ExportDecoderBackend::Software(decoder) => decoder.video_duration(),
752 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
753 ExportDecoderBackend::Vulkan { decoder, .. } => decoder.video_duration(),
754 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
755 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.video_duration(),
756 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
757 ExportDecoderBackend::D3D12 { decoder, .. } => decoder.video_duration(),
758 }
759 }
760
761 pub fn flush(&mut self) -> Result<(), DecodeError> {
766 match &mut self.backend {
767 ExportDecoderBackend::Software(decoder) => decoder.flush(),
768 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
769 ExportDecoderBackend::Vulkan { decoder, .. } => decoder.flush(),
770 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
771 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder.flush(),
772 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
773 ExportDecoderBackend::D3D12 { decoder, .. } => decoder.flush(),
774 }
775 }
776
777 pub fn seek_video(
782 &mut self,
783 request: DecodeRequest,
784 cancel: &CancelToken,
785 ) -> Result<NativeVideoFrame, DecodeError> {
786 let result = self.seek_video_on_backend(request, cancel);
787 self.observe(result, |_| true)
788 }
789
790 pub fn decode_video_next(
795 &mut self,
796 cancel: &CancelToken,
797 ) -> Result<Option<NativeVideoFrame>, DecodeError> {
798 let result = self.decode_video_next_on_backend(cancel);
799 self.observe(result, Option::is_some)
800 }
801
802 pub fn peek_video_pts(
810 &mut self,
811 cancel: &CancelToken,
812 ) -> Result<Option<MediaTime>, DecodeError> {
813 let result = self.native_decoder().peek_raw_pts(cancel);
814 self.observe(result, |_| false)
815 }
816
817 pub fn set_decode_policy(&mut self, policy: crate::DecodePolicy) {
819 self.native_decoder().set_decode_policy(policy);
820 }
821
822 pub fn with_declared_color(
828 mut self,
829 color: Option<ColorMetadata>,
830 ) -> Result<Self, HardwareOpenError> {
831 if self.is_hardware() && color.is_some_and(ColorMetadata::is_hdr) {
832 return Err(HardwareOpenError::new(
833 HardwareDecodeFallback::UnsupportedColor,
834 DecodeError::Unsupported("hardware export currently supports SDR only".into()),
835 ));
836 }
837 self.native_decoder().declare_color(color);
838 Ok(self)
839 }
840
841 pub fn set_decode_reporter(
843 &mut self,
844 reporter: impl Fn(crate::DecodeDiagnostic) + Send + Sync + 'static,
845 ) {
846 self.native_decoder().set_decode_reporter(reporter);
847 }
848
849 pub fn decode_diagnostics(&mut self) -> &crate::DecodeDiagnostics {
851 self.native_decoder().decode_diagnostics()
852 }
853
854 #[must_use]
857 pub fn presentation_deadline(&self, time: MediaTime) -> MediaTime {
858 match &self.backend {
859 ExportDecoderBackend::Software(decoder) => decoder,
860 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
861 ExportDecoderBackend::Vulkan { decoder, .. } => decoder,
862 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
863 ExportDecoderBackend::VideoToolbox { decoder, .. } => decoder,
864 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
865 ExportDecoderBackend::D3D12 { decoder, .. } => decoder,
866 }
867 .presentation_deadline(time)
868 }
869
870 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]
884 pub const fn hardware_failure(&self) -> Option<HardwareDecodeFallback> {
885 self.hardware_failure
886 }
887
888 fn observe<T>(
895 &mut self,
896 result: Result<T, DecodeError>,
897 produced: impl Fn(&T) -> bool,
898 ) -> Result<T, DecodeError> {
899 if !self.is_hardware() {
900 return result;
901 }
902 match result {
903 Ok(value) => {
904 if produced(&value) {
905 self.hardware_frames = self.hardware_frames.saturating_add(1);
906 }
907 Ok(value)
908 }
909 Err(DecodeError::Adapter(message)) if self.hardware_frames == 0 => {
910 self.hardware_failure = Some(HardwareDecodeFallback::DecoderOpenFailed);
911 Err(DecodeError::Unsupported(format!(
912 "the hardware decoder failed before its first frame: {message}"
913 )))
914 }
915 Err(DecodeError::Unsupported(message)) => {
916 self.hardware_failure
917 .get_or_insert(HardwareDecodeFallback::UnsupportedSurfaceFormat);
918 Err(DecodeError::Unsupported(message))
919 }
920 Err(error) => Err(error),
921 }
922 }
923
924 fn seek_video_on_backend(
925 &mut self,
926 request: DecodeRequest,
927 cancel: &CancelToken,
928 ) -> Result<NativeVideoFrame, DecodeError> {
929 match &mut self.backend {
930 ExportDecoderBackend::Software(decoder) => match decoder.seek(request, cancel)?.event {
931 DecodeEvent::Video(frame) => Ok(NativeVideoFrame::Cpu(frame)),
932 _ => Err(DecodeError::Adapter(
933 "video decoder returned a non-video event".into(),
934 )),
935 },
936 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
937 ExportDecoderBackend::Vulkan {
938 decoder,
939 generation,
940 } => {
941 let frame = decoder.seek_raw(request.time, cancel)?;
942 hardware_frame(decoder, frame.into_frame(), *generation)
943 }
944 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
945 ExportDecoderBackend::VideoToolbox {
946 decoder,
947 generation,
948 device_identity,
949 } => {
950 let mark = alpha2_ffmpeg_bridge::log_mark();
951 let frame = decoder
952 .seek_raw(request.time, cancel)
953 .map_err(|error| with_videotoolbox_log(error, mark))?;
954 videotoolbox_frame(decoder, frame.into_frame(), *generation, *device_identity)
955 }
956 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
957 ExportDecoderBackend::D3D12 {
958 decoder,
959 generation,
960 device_identity,
961 } => {
962 let frame = decoder.seek_raw(request.time, cancel)?;
963 d3d12_frame(
964 decoder,
965 frame.into_frame(),
966 *generation,
967 *device_identity,
968 &mut self.hardware_failure,
969 )
970 }
971 }
972 }
973
974 fn decode_video_next_on_backend(
975 &mut self,
976 cancel: &CancelToken,
977 ) -> Result<Option<NativeVideoFrame>, DecodeError> {
978 match &mut self.backend {
979 ExportDecoderBackend::Software(decoder) => match decoder.decode_next(cancel)? {
980 DecodeEvent::Video(frame) => Ok(Some(NativeVideoFrame::Cpu(frame))),
981 DecodeEvent::EndOfStream => Ok(None),
982 _ => Err(DecodeError::Adapter(
983 "video decoder returned a non-video event".into(),
984 )),
985 },
986 #[cfg(all(target_os = "linux", feature = "hwdecode"))]
987 ExportDecoderBackend::Vulkan {
988 decoder,
989 generation,
990 } => decoder
991 .decode_raw_next(cancel)?
992 .map(|frame| hardware_frame(decoder, frame.into_frame(), *generation))
993 .transpose(),
994 #[cfg(all(target_os = "macos", feature = "hwdecode"))]
995 ExportDecoderBackend::VideoToolbox {
996 decoder,
997 generation,
998 device_identity,
999 } => {
1000 let mark = alpha2_ffmpeg_bridge::log_mark();
1001 decoder
1002 .decode_raw_next(cancel)
1003 .map_err(|error| with_videotoolbox_log(error, mark))?
1004 .map(|frame| {
1005 videotoolbox_frame(
1006 decoder,
1007 frame.into_frame(),
1008 *generation,
1009 *device_identity,
1010 )
1011 })
1012 .transpose()
1013 }
1014 #[cfg(all(target_os = "windows", feature = "hwdecode"))]
1015 ExportDecoderBackend::D3D12 {
1016 decoder,
1017 generation,
1018 device_identity,
1019 } => decoder
1020 .decode_raw_next(cancel)?
1021 .map(|frame| {
1022 d3d12_frame(
1023 decoder,
1024 frame.into_frame(),
1025 *generation,
1026 *device_identity,
1027 &mut self.hardware_failure,
1028 )
1029 })
1030 .transpose(),
1031 }
1032 }
1033}
1034
1035#[cfg(all(target_os = "windows", feature = "hwdecode"))]
1039fn d3d12_frame(
1040 decoder: &NativeDecoder,
1041 frame: AVFrame,
1042 generation: u64,
1043 device_identity: u64,
1044 failure: &mut Option<HardwareDecodeFallback>,
1045) -> Result<NativeVideoFrame, DecodeError> {
1046 let info = alpha2_ffmpeg_bridge::d3d12_frame_info(&frame)
1047 .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1048 let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1049 DecodeError::Unsupported(format!(
1050 "D3D12VA output {} is not an importable 4:2:0 surface",
1051 info.software_format
1052 ))
1053 })?;
1054 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
1055 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
1056 let crop = [
1057 frame.crop_left,
1058 frame.crop_top,
1059 frame.crop_right,
1060 frame.crop_bottom,
1061 ];
1062 let Some(visible_rect) =
1063 visible_rect_within_surface(width, height, crop, info.surface_width, info.surface_height)
1064 else {
1065 *failure = Some(HardwareDecodeFallback::UnsupportedCrop);
1066 return Err(DecodeError::Unsupported(format!(
1067 "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",
1068 frame.crop_left, frame.crop_top, info.surface_width, info.surface_height
1069 )));
1070 };
1071 let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1072 frame.pts
1073 } else {
1074 frame.best_effort_timestamp
1075 };
1076 let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1077 let metadata = NativeVideoFrameMetadata {
1078 pts,
1079 dts: None,
1080 duration: timestamp(frame.duration, decoder.time_base),
1081 visible_rect,
1082 orientation: decoder.orientation,
1083 sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1084 color: decoder.picture_color(&frame),
1085 chroma_location: decoder.picture_chroma_location(&frame),
1086 bit_depth: plane_layout.bit_depth(),
1087 plane_layout,
1088 device_identity,
1089 decoder_generation: generation,
1090 picture_identity: info.resource
1091 ^ info.fence_value
1092 ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1093 };
1094 Ok(NativeVideoFrame::Hardware {
1095 metadata,
1096 surface: Arc::new(alpha2_ffmpeg_bridge::D3D12FrameLease::new(frame)),
1097 })
1098}
1099
1100#[cfg(all(target_os = "macos", feature = "hwdecode"))]
1103const VIDEOTOOLBOX_DECODE_LOG_CLASSES: &[&str] = &[
1104 "h264",
1105 "hevc",
1106 "vp9",
1107 "av1",
1108 "AVHWDeviceContext",
1109 "AVHWFramesContext",
1110];
1111
1112#[cfg(all(target_os = "macos", feature = "hwdecode"))]
1115fn with_videotoolbox_log(error: DecodeError, mark: alpha2_ffmpeg_bridge::LogMark) -> DecodeError {
1116 let detail = alpha2_ffmpeg_bridge::log_detail(&alpha2_ffmpeg_bridge::log_since(
1117 mark,
1118 VIDEOTOOLBOX_DECODE_LOG_CLASSES,
1119 ));
1120 if detail.is_empty() {
1121 return error;
1122 }
1123 match error {
1124 DecodeError::Adapter(message) => DecodeError::Adapter(format!("{message}{detail}")),
1125 DecodeError::Unsupported(message) => DecodeError::Unsupported(format!("{message}{detail}")),
1126 DecodeError::Gpu { fault, message } => DecodeError::Gpu {
1127 fault,
1128 message: format!("{message}{detail}"),
1129 },
1130 other => other,
1131 }
1132}
1133
1134#[cfg(all(target_os = "macos", feature = "hwdecode"))]
1135fn videotoolbox_frame(
1136 decoder: &NativeDecoder,
1137 frame: AVFrame,
1138 generation: u64,
1139 device_identity: u64,
1140) -> Result<NativeVideoFrame, DecodeError> {
1141 let info = alpha2_ffmpeg_bridge::videotoolbox_frame_info(&frame)
1142 .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1143 let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1144 DecodeError::Unsupported(format!(
1145 "VideoToolbox output {} is not an importable 4:2:0 surface",
1146 info.software_format
1147 ))
1148 })?;
1149 let width = u32::try_from(info.width).map_err(|_| DecodeError::OutOfRange)?;
1150 let height = u32::try_from(info.height).map_err(|_| DecodeError::OutOfRange)?;
1151 let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1152 frame.pts
1153 } else {
1154 frame.best_effort_timestamp
1155 };
1156 let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1157 let metadata = NativeVideoFrameMetadata {
1158 pts,
1159 dts: None,
1160 duration: timestamp(frame.duration, decoder.time_base),
1161 visible_rect: Rect {
1162 x: u32::try_from(frame.crop_left).unwrap_or(0),
1163 y: u32::try_from(frame.crop_top).unwrap_or(0),
1164 width: width.saturating_sub(
1165 u32::try_from(frame.crop_left.saturating_add(frame.crop_right)).unwrap_or(0),
1166 ),
1167 height: height.saturating_sub(
1168 u32::try_from(frame.crop_top.saturating_add(frame.crop_bottom)).unwrap_or(0),
1169 ),
1170 },
1171 orientation: decoder.orientation,
1172 sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1173 color: decoder.picture_color(&frame),
1174 chroma_location: decoder.picture_chroma_location(&frame),
1175 bit_depth: plane_layout.bit_depth(),
1176 plane_layout,
1177 device_identity,
1178 decoder_generation: generation,
1179 picture_identity: info.pixel_buffer ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1180 };
1181 Ok(NativeVideoFrame::Hardware {
1182 metadata,
1183 surface: Arc::new(alpha2_ffmpeg_bridge::VideoToolboxFrameLease::new(frame)),
1184 })
1185}
1186
1187#[cfg(all(target_os = "linux", feature = "hwdecode"))]
1188fn hardware_frame(
1189 decoder: &NativeDecoder,
1190 frame: AVFrame,
1191 generation: u64,
1192) -> Result<NativeVideoFrame, DecodeError> {
1193 let info = alpha2_ffmpeg_bridge::vulkan_frame_info(&frame)
1194 .map_err(|error| DecodeError::Adapter(error.to_string()))?;
1195 let plane_layout = surface_plane_layout(info.software_format).ok_or_else(|| {
1196 DecodeError::Unsupported(format!(
1197 "Vulkan output {} is not an importable 4:2:0 surface",
1198 info.software_format
1199 ))
1200 })?;
1201 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
1204 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
1205 let coded_width = u32::try_from(info.coded_width).map_err(|_| DecodeError::OutOfRange)?;
1206 let coded_height = u32::try_from(info.coded_height).map_err(|_| DecodeError::OutOfRange)?;
1207 if coded_width < width || coded_height < height {
1208 return Err(DecodeError::Adapter(
1209 "Vulkan frame is larger than its allocated surface".into(),
1210 ));
1211 }
1212 let raw_pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1213 frame.pts
1214 } else {
1215 frame.best_effort_timestamp
1216 };
1217 let pts = timestamp(raw_pts, decoder.time_base).ok_or(DecodeError::OutOfRange)?;
1218 let metadata = NativeVideoFrameMetadata {
1219 pts,
1220 dts: None,
1221 duration: timestamp(frame.duration, decoder.time_base),
1222 visible_rect: Rect {
1223 x: u32::try_from(frame.crop_left).unwrap_or(0),
1224 y: u32::try_from(frame.crop_top).unwrap_or(0),
1225 width: width.saturating_sub(
1226 u32::try_from(frame.crop_left.saturating_add(frame.crop_right)).unwrap_or(0),
1227 ),
1228 height: height.saturating_sub(
1229 u32::try_from(frame.crop_top.saturating_add(frame.crop_bottom)).unwrap_or(0),
1230 ),
1231 },
1232 orientation: decoder.orientation,
1233 sample_aspect_ratio: decoder.picture_sample_aspect_ratio(&frame),
1234 color: decoder.picture_color(&frame),
1235 chroma_location: decoder.picture_chroma_location(&frame),
1236 bit_depth: plane_layout.bit_depth(),
1237 plane_layout,
1238 device_identity: info.device,
1239 decoder_generation: generation,
1240 picture_identity: info.images[0] ^ u64::from_ne_bytes(pts.ticks().to_ne_bytes()),
1241 };
1242 Ok(NativeVideoFrame::Hardware {
1243 metadata,
1244 surface: Arc::new(alpha2_ffmpeg_bridge::VulkanFrameLease::new(frame)),
1245 })
1246}
1247
1248fn metadata_from_cpu(frame: &DecodedVideoFrame) -> NativeVideoFrameMetadata {
1249 let plane_layout = match frame.pixel_format {
1250 PixelFormat::Yuv(format) => NativePlaneLayout::CpuYuv(format),
1251 PixelFormat::LinearRgbaF16 => NativePlaneLayout::CpuLinearRgbaF16,
1252 _ => NativePlaneLayout::CpuRgba8,
1253 };
1254 NativeVideoFrameMetadata {
1255 pts: frame.pts,
1256 dts: frame.dts,
1257 duration: frame.duration,
1258 visible_rect: frame.visible_rect,
1259 orientation: frame.orientation,
1260 sample_aspect_ratio: frame.sample_aspect_ratio,
1261 color: frame.color,
1262 chroma_location: frame.chroma_location,
1263 bit_depth: plane_layout.bit_depth(),
1264 plane_layout,
1265 device_identity: 0,
1266 decoder_generation: 0,
1267 picture_identity: u64::from_ne_bytes(frame.pts.ticks().to_ne_bytes()),
1268 }
1269}