1#![allow(unsafe_code)]
8#![allow(clippy::similar_names)]
10#![allow(clippy::too_many_lines)]
11#![allow(clippy::cast_sign_loss)]
12#![allow(clippy::cast_possible_truncation)]
13#![allow(clippy::cast_possible_wrap)]
14#![allow(clippy::module_name_repetitions)]
15#![allow(clippy::match_same_arms)]
16#![allow(clippy::ptr_as_ptr)]
17#![allow(clippy::doc_markdown)]
18#![allow(clippy::unnecessary_cast)]
19#![allow(clippy::if_not_else)]
20#![allow(clippy::unnecessary_wraps)]
21#![allow(clippy::cast_precision_loss)]
22#![allow(clippy::if_same_then_else)]
23#![allow(clippy::cast_lossless)]
24
25use std::ffi::CStr;
26use std::path::Path;
27use std::ptr;
28use std::sync::Arc;
29use std::time::Duration;
30
31use ff_format::NetworkOptions;
32
33use ff_format::PooledBuffer;
34use ff_format::codec::VideoCodec;
35use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
36use ff_format::container::ContainerInfo;
37use ff_format::time::{Rational, Timestamp};
38use ff_format::{PixelFormat, VideoFrame, VideoStreamInfo};
39use ff_sys::{
40 AVBufferRef, AVCodecContext, AVCodecID, AVColorPrimaries, AVColorRange, AVColorSpace,
41 AVFormatContext, AVFrame, AVHWDeviceType, AVMediaType_AVMEDIA_TYPE_VIDEO, AVPacket,
42 AVPixelFormat, SwsContext,
43};
44
45use crate::HardwareAccel;
46use crate::error::DecodeError;
47use crate::video::builder::OutputScale;
48use ff_common::FramePool;
49
50const KEYFRAME_SEEK_TOLERANCE_SECS: u64 = 1;
56
57mod context;
58mod decoding;
59mod format_convert;
60mod hardware;
61mod seeking;
62
63struct AvFormatContextGuard(*mut AVFormatContext);
65
66impl AvFormatContextGuard {
67 unsafe fn new(path: &Path) -> Result<Self, DecodeError> {
73 let format_ctx = unsafe {
75 ff_sys::avformat::open_input(path).map_err(|e| DecodeError::Ffmpeg {
76 code: e,
77 message: format!("Failed to open file: {}", ff_sys::av_error_string(e)),
78 })?
79 };
80 Ok(Self(format_ctx))
81 }
82
83 const fn as_ptr(&self) -> *mut AVFormatContext {
85 self.0
86 }
87
88 fn into_raw(self) -> *mut AVFormatContext {
90 let ptr = self.0;
91 std::mem::forget(self);
92 ptr
93 }
94
95 unsafe fn new_image_sequence(path: &Path, framerate: u32) -> Result<Self, DecodeError> {
101 let format_ctx = unsafe {
103 ff_sys::avformat::open_input_image_sequence(path, framerate).map_err(|e| {
104 DecodeError::Ffmpeg {
105 code: e,
106 message: format!(
107 "Failed to open image sequence: {}",
108 ff_sys::av_error_string(e)
109 ),
110 }
111 })?
112 };
113 Ok(Self(format_ctx))
114 }
115
116 unsafe fn new_url(url: &str, network: &NetworkOptions) -> Result<Self, DecodeError> {
122 let format_ctx = unsafe {
124 ff_sys::avformat::open_input_url(url, network.connect_timeout, network.read_timeout)
125 .map_err(|e| {
126 crate::network::map_network_error(e, crate::network::sanitize_url(url))
127 })?
128 };
129 Ok(Self(format_ctx))
130 }
131}
132
133impl Drop for AvFormatContextGuard {
134 fn drop(&mut self) {
135 if !self.0.is_null() {
136 unsafe {
138 ff_sys::avformat::close_input(&mut (self.0 as *mut _));
139 }
140 }
141 }
142}
143
144struct AvCodecContextGuard(*mut AVCodecContext);
146
147impl AvCodecContextGuard {
148 unsafe fn new(codec: *const ff_sys::AVCodec) -> Result<Self, DecodeError> {
154 let codec_ctx = unsafe {
156 ff_sys::avcodec::alloc_context3(codec).map_err(|e| DecodeError::Ffmpeg {
157 code: e,
158 message: format!("Failed to allocate codec context: {e}"),
159 })?
160 };
161 Ok(Self(codec_ctx))
162 }
163
164 const fn as_ptr(&self) -> *mut AVCodecContext {
166 self.0
167 }
168
169 fn into_raw(self) -> *mut AVCodecContext {
171 let ptr = self.0;
172 std::mem::forget(self);
173 ptr
174 }
175}
176
177impl Drop for AvCodecContextGuard {
178 fn drop(&mut self) {
179 if !self.0.is_null() {
180 unsafe {
182 ff_sys::avcodec::free_context(&mut (self.0 as *mut _));
183 }
184 }
185 }
186}
187
188struct AvPacketGuard(*mut AVPacket);
190
191impl AvPacketGuard {
192 unsafe fn new() -> Result<Self, DecodeError> {
198 let packet = unsafe { ff_sys::av_packet_alloc() };
200 if packet.is_null() {
201 return Err(DecodeError::Ffmpeg {
202 code: 0,
203 message: "Failed to allocate packet".to_string(),
204 });
205 }
206 Ok(Self(packet))
207 }
208
209 fn into_raw(self) -> *mut AVPacket {
211 let ptr = self.0;
212 std::mem::forget(self);
213 ptr
214 }
215}
216
217impl Drop for AvPacketGuard {
218 fn drop(&mut self) {
219 if !self.0.is_null() {
220 unsafe {
222 ff_sys::av_packet_free(&mut (self.0 as *mut _));
223 }
224 }
225 }
226}
227
228struct AvFrameGuard(*mut AVFrame);
230
231impl AvFrameGuard {
232 unsafe fn new() -> Result<Self, DecodeError> {
238 let frame = unsafe { ff_sys::av_frame_alloc() };
240 if frame.is_null() {
241 return Err(DecodeError::Ffmpeg {
242 code: 0,
243 message: "Failed to allocate frame".to_string(),
244 });
245 }
246 Ok(Self(frame))
247 }
248
249 const fn as_ptr(&self) -> *mut AVFrame {
251 self.0
252 }
253
254 fn into_raw(self) -> *mut AVFrame {
256 let ptr = self.0;
257 std::mem::forget(self);
258 ptr
259 }
260}
261
262impl Drop for AvFrameGuard {
263 fn drop(&mut self) {
264 if !self.0.is_null() {
265 unsafe {
267 ff_sys::av_frame_free(&mut (self.0 as *mut _));
268 }
269 }
270 }
271}
272
273pub(crate) struct VideoDecoderInner {
278 pub(super) format_ctx: *mut AVFormatContext,
280 pub(super) codec_ctx: *mut AVCodecContext,
282 pub(super) stream_index: i32,
284 pub(super) sws_ctx: Option<*mut SwsContext>,
286 pub(super) sws_cache_key: Option<(u32, u32, i32, u32, u32, i32)>,
288 pub(super) output_format: Option<PixelFormat>,
290 pub(super) output_scale: Option<OutputScale>,
292 pub(super) is_live: bool,
294 pub(super) eof: bool,
296 pub(super) position: Duration,
298 pub(super) packet: *mut AVPacket,
300 pub(super) frame: *mut AVFrame,
302 pub(super) thumbnail_sws_ctx: Option<*mut SwsContext>,
304 pub(super) thumbnail_cache_key: Option<(u32, u32, u32, u32, AVPixelFormat)>,
306 pub(super) hw_device_ctx: Option<*mut AVBufferRef>,
308 pub(super) active_hw_accel: HardwareAccel,
310 pub(super) frame_pool: Option<Arc<dyn FramePool>>,
312 pub(super) url: Option<String>,
314 pub(super) network_opts: NetworkOptions,
316 pub(super) reconnect_count: u32,
318 pub(super) consecutive_invalid: u32,
321}
322
323impl VideoDecoderInner {
324 #[allow(clippy::too_many_arguments)]
341 pub(crate) fn new(
342 path: &Path,
343 output_format: Option<PixelFormat>,
344 output_scale: Option<OutputScale>,
345 hardware_accel: HardwareAccel,
346 thread_count: usize,
347 frame_rate: Option<u32>,
348 frame_pool: Option<Arc<dyn FramePool>>,
349 network_opts: Option<NetworkOptions>,
350 ) -> Result<(Self, VideoStreamInfo, ContainerInfo), DecodeError> {
351 ff_sys::ensure_initialized();
353
354 let path_str = path.to_str().unwrap_or("");
355 let is_image_sequence = path_str.contains('%');
356 let is_network_url = crate::network::is_url(path_str);
357
358 let url = if is_network_url {
359 Some(path_str.to_owned())
360 } else {
361 None
362 };
363 let stored_network_opts = network_opts.clone().unwrap_or_default();
364
365 if is_network_url {
367 crate::network::check_srt_url(path_str)?;
368 }
369
370 let format_ctx_guard = unsafe {
373 if is_network_url {
374 let network = network_opts.unwrap_or_default();
375 log::info!(
376 "opening network source url={} connect_timeout_ms={} read_timeout_ms={}",
377 crate::network::sanitize_url(path_str),
378 network.connect_timeout.as_millis(),
379 network.read_timeout.as_millis(),
380 );
381 AvFormatContextGuard::new_url(path_str, &network)?
382 } else if is_image_sequence {
383 let fps = frame_rate.unwrap_or(25);
384 AvFormatContextGuard::new_image_sequence(path, fps)?
385 } else {
386 AvFormatContextGuard::new(path)?
387 }
388 };
389 let format_ctx = format_ctx_guard.as_ptr();
390
391 unsafe {
394 ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
395 code: e,
396 message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
397 })?;
398 }
399
400 let is_live = unsafe {
404 let iformat = (*format_ctx).iformat;
405 !iformat.is_null() && ((*iformat).flags & ff_sys::AVFMT_TS_DISCONT) != 0
406 };
407
408 let (stream_index, codec_id) =
411 unsafe { Self::find_video_stream(format_ctx) }.ok_or_else(|| {
412 DecodeError::NoVideoStream {
413 path: path.to_path_buf(),
414 }
415 })?;
416
417 let codec_name = unsafe { Self::extract_codec_name(codec_id) };
420 let codec = unsafe {
421 ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
422 if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_EXR {
425 DecodeError::DecoderUnavailable {
426 codec: "exr".to_string(),
427 hint: "Requires FFmpeg built with EXR support \
428 (--enable-decoder=exr)"
429 .to_string(),
430 }
431 } else {
432 DecodeError::UnsupportedCodec {
433 codec: format!("{codec_name} (codec_id={codec_id:?})"),
434 }
435 }
436 })?
437 };
438
439 let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
442 let codec_ctx = codec_ctx_guard.as_ptr();
443
444 unsafe {
447 let stream = (*format_ctx).streams.add(stream_index as usize);
448 let codecpar = (*(*stream)).codecpar;
449 ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
450 DecodeError::Ffmpeg {
451 code: e,
452 message: format!(
453 "Failed to copy codec parameters: {}",
454 ff_sys::av_error_string(e)
455 ),
456 }
457 })?;
458
459 if thread_count > 0 {
461 (*codec_ctx).thread_count = thread_count as i32;
462 }
463 }
464
465 let (hw_device_ctx, active_hw_accel) =
468 unsafe { Self::init_hardware_accel(codec_ctx, hardware_accel)? };
469
470 unsafe {
473 ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
474 if let Some(hw_ctx) = hw_device_ctx {
479 ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
480 }
481 DecodeError::Ffmpeg {
482 code: e,
483 message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
484 }
485 })?;
486 }
487
488 let stream_info =
491 unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
492
493 let container_info = unsafe { Self::extract_container_info(format_ctx) };
496
497 let packet_guard = unsafe { AvPacketGuard::new()? };
500 let frame_guard = unsafe { AvFrameGuard::new()? };
501
502 Ok((
504 Self {
505 format_ctx: format_ctx_guard.into_raw(),
506 codec_ctx: codec_ctx_guard.into_raw(),
507 stream_index: stream_index as i32,
508 sws_ctx: None,
509 sws_cache_key: None,
510 output_format,
511 output_scale,
512 is_live,
513 eof: false,
514 position: Duration::ZERO,
515 packet: packet_guard.into_raw(),
516 frame: frame_guard.into_raw(),
517 thumbnail_sws_ctx: None,
518 thumbnail_cache_key: None,
519 hw_device_ctx,
520 active_hw_accel,
521 frame_pool,
522 url,
523 network_opts: stored_network_opts,
524 reconnect_count: 0,
525 consecutive_invalid: 0,
526 },
527 stream_info,
528 container_info,
529 ))
530 }
531}
532
533impl Drop for VideoDecoderInner {
534 fn drop(&mut self) {
535 if let Some(sws_ctx) = self.sws_ctx {
537 unsafe {
539 ff_sys::swscale::free_context(sws_ctx);
540 }
541 }
542
543 if let Some(thumbnail_ctx) = self.thumbnail_sws_ctx {
545 unsafe {
547 ff_sys::swscale::free_context(thumbnail_ctx);
548 }
549 }
550
551 if let Some(hw_ctx) = self.hw_device_ctx {
553 unsafe {
555 ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
556 }
557 }
558
559 if !self.frame.is_null() {
561 unsafe {
563 ff_sys::av_frame_free(&mut (self.frame as *mut _));
564 }
565 }
566
567 if !self.packet.is_null() {
568 unsafe {
570 ff_sys::av_packet_free(&mut (self.packet as *mut _));
571 }
572 }
573
574 if !self.codec_ctx.is_null() {
576 unsafe {
578 ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
579 }
580 }
581
582 if !self.format_ctx.is_null() {
584 unsafe {
586 ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
587 }
588 }
589 }
590}
591
592unsafe impl Send for VideoDecoderInner {}
595
596#[cfg(test)]
597mod tests {
598 use ff_format::PixelFormat;
599 use ff_format::codec::VideoCodec;
600 use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
601
602 use crate::HardwareAccel;
603
604 use super::VideoDecoderInner;
605
606 #[test]
611 fn pixel_format_yuv420p() {
612 assert_eq!(
613 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
614 PixelFormat::Yuv420p
615 );
616 }
617
618 #[test]
619 fn pixel_format_yuv422p() {
620 assert_eq!(
621 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P),
622 PixelFormat::Yuv422p
623 );
624 }
625
626 #[test]
627 fn pixel_format_yuv444p() {
628 assert_eq!(
629 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P),
630 PixelFormat::Yuv444p
631 );
632 }
633
634 #[test]
635 fn pixel_format_rgb24() {
636 assert_eq!(
637 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
638 PixelFormat::Rgb24
639 );
640 }
641
642 #[test]
643 fn pixel_format_bgr24() {
644 assert_eq!(
645 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24),
646 PixelFormat::Bgr24
647 );
648 }
649
650 #[test]
651 fn pixel_format_rgba() {
652 assert_eq!(
653 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
654 PixelFormat::Rgba
655 );
656 }
657
658 #[test]
659 fn pixel_format_bgra() {
660 assert_eq!(
661 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA),
662 PixelFormat::Bgra
663 );
664 }
665
666 #[test]
667 fn pixel_format_gray8() {
668 assert_eq!(
669 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
670 PixelFormat::Gray8
671 );
672 }
673
674 #[test]
675 fn pixel_format_nv12() {
676 assert_eq!(
677 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV12),
678 PixelFormat::Nv12
679 );
680 }
681
682 #[test]
683 fn pixel_format_nv21() {
684 assert_eq!(
685 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV21),
686 PixelFormat::Nv21
687 );
688 }
689
690 #[test]
691 fn pixel_format_yuv420p10le_should_return_yuv420p10le() {
692 assert_eq!(
693 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE),
694 PixelFormat::Yuv420p10le
695 );
696 }
697
698 #[test]
699 fn pixel_format_yuv422p10le_should_return_yuv422p10le() {
700 assert_eq!(
701 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P10LE),
702 PixelFormat::Yuv422p10le
703 );
704 }
705
706 #[test]
707 fn pixel_format_yuv444p10le_should_return_yuv444p10le() {
708 assert_eq!(
709 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P10LE),
710 PixelFormat::Yuv444p10le
711 );
712 }
713
714 #[test]
715 fn pixel_format_p010le_should_return_p010le() {
716 assert_eq!(
717 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_P010LE),
718 PixelFormat::P010le
719 );
720 }
721
722 #[test]
723 fn pixel_format_unknown_falls_back_to_yuv420p() {
724 assert_eq!(
725 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NONE),
726 PixelFormat::Yuv420p
727 );
728 }
729
730 #[test]
735 fn color_space_bt709() {
736 assert_eq!(
737 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT709),
738 ColorSpace::Bt709
739 );
740 }
741
742 #[test]
743 fn color_space_bt470bg_yields_bt601() {
744 assert_eq!(
745 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT470BG),
746 ColorSpace::Bt601
747 );
748 }
749
750 #[test]
751 fn color_space_smpte170m_yields_bt601() {
752 assert_eq!(
753 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M),
754 ColorSpace::Bt601
755 );
756 }
757
758 #[test]
759 fn color_space_bt2020_ncl() {
760 assert_eq!(
761 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL),
762 ColorSpace::Bt2020
763 );
764 }
765
766 #[test]
767 fn color_space_unknown_falls_back_to_bt709() {
768 assert_eq!(
769 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_UNSPECIFIED),
770 ColorSpace::Bt709
771 );
772 }
773
774 #[test]
779 fn color_range_jpeg_yields_full() {
780 assert_eq!(
781 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_JPEG),
782 ColorRange::Full
783 );
784 }
785
786 #[test]
787 fn color_range_mpeg_yields_limited() {
788 assert_eq!(
789 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_MPEG),
790 ColorRange::Limited
791 );
792 }
793
794 #[test]
795 fn color_range_unknown_falls_back_to_limited() {
796 assert_eq!(
797 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_UNSPECIFIED),
798 ColorRange::Limited
799 );
800 }
801
802 #[test]
807 fn color_primaries_bt709() {
808 assert_eq!(
809 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT709),
810 ColorPrimaries::Bt709
811 );
812 }
813
814 #[test]
815 fn color_primaries_bt470bg_yields_bt601() {
816 assert_eq!(
817 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG),
818 ColorPrimaries::Bt601
819 );
820 }
821
822 #[test]
823 fn color_primaries_smpte170m_yields_bt601() {
824 assert_eq!(
825 VideoDecoderInner::convert_color_primaries(
826 ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M
827 ),
828 ColorPrimaries::Bt601
829 );
830 }
831
832 #[test]
833 fn color_primaries_bt2020() {
834 assert_eq!(
835 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020),
836 ColorPrimaries::Bt2020
837 );
838 }
839
840 #[test]
841 fn color_primaries_unknown_falls_back_to_bt709() {
842 assert_eq!(
843 VideoDecoderInner::convert_color_primaries(
844 ff_sys::AVColorPrimaries_AVCOL_PRI_UNSPECIFIED
845 ),
846 ColorPrimaries::Bt709
847 );
848 }
849
850 #[test]
855 fn codec_h264() {
856 assert_eq!(
857 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_H264),
858 VideoCodec::H264
859 );
860 }
861
862 #[test]
863 fn codec_hevc_yields_h265() {
864 assert_eq!(
865 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_HEVC),
866 VideoCodec::H265
867 );
868 }
869
870 #[test]
871 fn codec_vp8() {
872 assert_eq!(
873 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP8),
874 VideoCodec::Vp8
875 );
876 }
877
878 #[test]
879 fn codec_vp9() {
880 assert_eq!(
881 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP9),
882 VideoCodec::Vp9
883 );
884 }
885
886 #[test]
887 fn codec_av1() {
888 assert_eq!(
889 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_AV1),
890 VideoCodec::Av1
891 );
892 }
893
894 #[test]
895 fn codec_mpeg4() {
896 assert_eq!(
897 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_MPEG4),
898 VideoCodec::Mpeg4
899 );
900 }
901
902 #[test]
903 fn codec_prores() {
904 assert_eq!(
905 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_PRORES),
906 VideoCodec::ProRes
907 );
908 }
909
910 #[test]
911 fn codec_unknown_falls_back_to_h264() {
912 assert_eq!(
913 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_NONE),
914 VideoCodec::H264
915 );
916 }
917
918 #[test]
923 fn hw_accel_auto_yields_none() {
924 assert_eq!(
925 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Auto),
926 None
927 );
928 }
929
930 #[test]
931 fn hw_accel_none_yields_none() {
932 assert_eq!(
933 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::None),
934 None
935 );
936 }
937
938 #[test]
939 fn hw_accel_nvdec_yields_cuda() {
940 assert_eq!(
941 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Nvdec),
942 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_CUDA)
943 );
944 }
945
946 #[test]
947 fn hw_accel_qsv_yields_qsv() {
948 assert_eq!(
949 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Qsv),
950 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_QSV)
951 );
952 }
953
954 #[test]
955 fn hw_accel_amf_yields_d3d11va() {
956 assert_eq!(
957 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Amf),
958 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_D3D11VA)
959 );
960 }
961
962 #[test]
963 fn hw_accel_videotoolbox() {
964 assert_eq!(
965 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::VideoToolbox),
966 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VIDEOTOOLBOX)
967 );
968 }
969
970 #[test]
971 fn hw_accel_vaapi() {
972 assert_eq!(
973 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Vaapi),
974 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VAAPI)
975 );
976 }
977
978 #[test]
983 fn pixel_format_to_av_round_trip_yuv420p() {
984 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv420p);
985 assert_eq!(
986 VideoDecoderInner::convert_pixel_format(av),
987 PixelFormat::Yuv420p
988 );
989 }
990
991 #[test]
992 fn pixel_format_to_av_round_trip_yuv422p() {
993 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv422p);
994 assert_eq!(
995 VideoDecoderInner::convert_pixel_format(av),
996 PixelFormat::Yuv422p
997 );
998 }
999
1000 #[test]
1001 fn pixel_format_to_av_round_trip_yuv444p() {
1002 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv444p);
1003 assert_eq!(
1004 VideoDecoderInner::convert_pixel_format(av),
1005 PixelFormat::Yuv444p
1006 );
1007 }
1008
1009 #[test]
1010 fn pixel_format_to_av_round_trip_rgb24() {
1011 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgb24);
1012 assert_eq!(
1013 VideoDecoderInner::convert_pixel_format(av),
1014 PixelFormat::Rgb24
1015 );
1016 }
1017
1018 #[test]
1019 fn pixel_format_to_av_round_trip_bgr24() {
1020 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgr24);
1021 assert_eq!(
1022 VideoDecoderInner::convert_pixel_format(av),
1023 PixelFormat::Bgr24
1024 );
1025 }
1026
1027 #[test]
1028 fn pixel_format_to_av_round_trip_rgba() {
1029 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgba);
1030 assert_eq!(
1031 VideoDecoderInner::convert_pixel_format(av),
1032 PixelFormat::Rgba
1033 );
1034 }
1035
1036 #[test]
1037 fn pixel_format_to_av_round_trip_bgra() {
1038 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgra);
1039 assert_eq!(
1040 VideoDecoderInner::convert_pixel_format(av),
1041 PixelFormat::Bgra
1042 );
1043 }
1044
1045 #[test]
1046 fn pixel_format_to_av_round_trip_gray8() {
1047 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Gray8);
1048 assert_eq!(
1049 VideoDecoderInner::convert_pixel_format(av),
1050 PixelFormat::Gray8
1051 );
1052 }
1053
1054 #[test]
1055 fn pixel_format_to_av_round_trip_nv12() {
1056 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv12);
1057 assert_eq!(
1058 VideoDecoderInner::convert_pixel_format(av),
1059 PixelFormat::Nv12
1060 );
1061 }
1062
1063 #[test]
1064 fn pixel_format_to_av_round_trip_nv21() {
1065 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv21);
1066 assert_eq!(
1067 VideoDecoderInner::convert_pixel_format(av),
1068 PixelFormat::Nv21
1069 );
1070 }
1071
1072 #[test]
1073 fn pixel_format_to_av_unknown_falls_back_to_yuv420p_av() {
1074 assert_eq!(
1076 VideoDecoderInner::pixel_format_to_av(PixelFormat::Other(999)),
1077 ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
1078 );
1079 }
1080
1081 #[test]
1086 fn codec_name_should_return_h264_for_h264_codec_id() {
1087 let name =
1088 unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_H264) };
1089 assert_eq!(name, "h264");
1090 }
1091
1092 #[test]
1093 fn codec_name_should_return_none_for_none_codec_id() {
1094 let name =
1095 unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_NONE) };
1096 assert_eq!(name, "none");
1097 }
1098
1099 #[test]
1100 fn convert_pixel_format_should_map_gbrpf32le() {
1101 assert_eq!(
1102 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GBRPF32LE),
1103 PixelFormat::Gbrpf32le
1104 );
1105 }
1106
1107 #[test]
1108 fn unsupported_codec_error_should_include_codec_name() {
1109 let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_H264;
1110 let codec_name = unsafe { VideoDecoderInner::extract_codec_name(codec_id) };
1111 let error = crate::error::DecodeError::UnsupportedCodec {
1112 codec: format!("{codec_name} (codec_id={codec_id:?})"),
1113 };
1114 let msg = error.to_string();
1115 assert!(msg.contains("h264"), "expected codec name in error: {msg}");
1116 assert!(
1117 msg.contains("codec_id="),
1118 "expected codec_id in error: {msg}"
1119 );
1120 }
1121}