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::shared::guards_inner::{
48 AvCodecContextGuard, AvFormatContextGuard, AvFrameGuard, AvPacketGuard,
49};
50use crate::video::builder::OutputScale;
51use ff_common::FramePool;
52
53const KEYFRAME_SEEK_TOLERANCE_SECS: u64 = 1;
59
60mod context;
61mod decoding;
62mod format_convert;
63mod hardware;
64mod seeking;
65
66pub(crate) struct VideoDecoderInner {
71 pub(super) format_ctx: *mut AVFormatContext,
73 pub(super) codec_ctx: *mut AVCodecContext,
75 pub(super) stream_index: i32,
77 pub(super) sws_ctx: Option<*mut SwsContext>,
79 pub(super) sws_cache_key: Option<(u32, u32, i32, u32, u32, i32)>,
81 pub(super) output_format: Option<PixelFormat>,
83 pub(super) output_scale: Option<OutputScale>,
85 pub(super) is_live: bool,
87 pub(super) eof: bool,
89 pub(super) position: Duration,
91 pub(super) packet: *mut AVPacket,
93 pub(super) frame: *mut AVFrame,
95 pub(super) thumbnail_sws_ctx: Option<*mut SwsContext>,
97 pub(super) thumbnail_cache_key: Option<(u32, u32, u32, u32, AVPixelFormat)>,
99 pub(super) hw_device_ctx: Option<*mut AVBufferRef>,
101 pub(super) active_hw_accel: HardwareAccel,
103 pub(super) frame_pool: Option<Arc<dyn FramePool>>,
105 pub(super) url: Option<String>,
107 pub(super) network_opts: NetworkOptions,
109 pub(super) reconnect_count: u32,
111 pub(super) consecutive_invalid: u32,
114}
115
116impl VideoDecoderInner {
117 #[allow(clippy::too_many_arguments)]
134 pub(crate) fn new(
135 path: &Path,
136 output_format: Option<PixelFormat>,
137 output_scale: Option<OutputScale>,
138 hardware_accel: HardwareAccel,
139 thread_count: usize,
140 frame_rate: Option<u32>,
141 frame_pool: Option<Arc<dyn FramePool>>,
142 network_opts: Option<NetworkOptions>,
143 ) -> Result<(Self, VideoStreamInfo, ContainerInfo), DecodeError> {
144 ff_sys::ensure_initialized();
146
147 let path_str = path.to_str().unwrap_or("");
148 let is_image_sequence = path_str.contains('%');
149 let is_network_url = crate::network::is_url(path_str);
150
151 let url = if is_network_url {
152 Some(path_str.to_owned())
153 } else {
154 None
155 };
156 let stored_network_opts = network_opts.clone().unwrap_or_default();
157
158 if is_network_url {
160 crate::network::check_srt_url(path_str)?;
161 }
162
163 let format_ctx_guard = unsafe {
166 if is_network_url {
167 let network = network_opts.unwrap_or_default();
168 log::info!(
169 "opening network source url={} connect_timeout_ms={} read_timeout_ms={}",
170 crate::network::sanitize_url(path_str),
171 network.connect_timeout.as_millis(),
172 network.read_timeout.as_millis(),
173 );
174 AvFormatContextGuard::new_url(path_str, &network)?
175 } else if is_image_sequence {
176 let fps = frame_rate.unwrap_or(25);
177 AvFormatContextGuard::new_image_sequence(path, fps)?
178 } else {
179 AvFormatContextGuard::new(path)?
180 }
181 };
182 let format_ctx = format_ctx_guard.as_ptr();
183
184 unsafe {
187 ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
188 code: e,
189 message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
190 })?;
191 }
192
193 let is_live = unsafe {
197 let iformat = (*format_ctx).iformat;
198 !iformat.is_null() && ((*iformat).flags & ff_sys::AVFMT_TS_DISCONT) != 0
199 };
200
201 let (stream_index, codec_id) =
204 unsafe { Self::find_video_stream(format_ctx) }.ok_or_else(|| {
205 DecodeError::NoVideoStream {
206 path: path.to_path_buf(),
207 }
208 })?;
209
210 let codec_name = unsafe { Self::extract_codec_name(codec_id) };
213 let codec = unsafe {
214 ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
215 if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_EXR {
218 DecodeError::DecoderUnavailable {
219 codec: "exr".to_string(),
220 hint: "Requires FFmpeg built with EXR support \
221 (--enable-decoder=exr)"
222 .to_string(),
223 }
224 } else {
225 DecodeError::UnsupportedCodec {
226 codec: format!("{codec_name} (codec_id={codec_id:?})"),
227 }
228 }
229 })?
230 };
231
232 let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
235 let codec_ctx = codec_ctx_guard.as_ptr();
236
237 unsafe {
240 let stream = (*format_ctx).streams.add(stream_index as usize);
241 let codecpar = (*(*stream)).codecpar;
242 ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
243 DecodeError::Ffmpeg {
244 code: e,
245 message: format!(
246 "Failed to copy codec parameters: {}",
247 ff_sys::av_error_string(e)
248 ),
249 }
250 })?;
251
252 if thread_count > 0 {
254 (*codec_ctx).thread_count = thread_count as i32;
255 }
256 }
257
258 let (hw_device_ctx, active_hw_accel) =
261 unsafe { Self::init_hardware_accel(codec_ctx, hardware_accel)? };
262
263 unsafe {
266 ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
267 if let Some(hw_ctx) = hw_device_ctx {
272 ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
273 }
274 DecodeError::Ffmpeg {
275 code: e,
276 message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
277 }
278 })?;
279 }
280
281 let stream_info =
284 unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
285
286 let container_info = unsafe { Self::extract_container_info(format_ctx) };
289
290 let packet_guard = unsafe { AvPacketGuard::new()? };
293 let frame_guard = unsafe { AvFrameGuard::new()? };
294
295 Ok((
297 Self {
298 format_ctx: format_ctx_guard.into_raw(),
299 codec_ctx: codec_ctx_guard.into_raw(),
300 stream_index: stream_index as i32,
301 sws_ctx: None,
302 sws_cache_key: None,
303 output_format,
304 output_scale,
305 is_live,
306 eof: false,
307 position: Duration::ZERO,
308 packet: packet_guard.into_raw(),
309 frame: frame_guard.into_raw(),
310 thumbnail_sws_ctx: None,
311 thumbnail_cache_key: None,
312 hw_device_ctx,
313 active_hw_accel,
314 frame_pool,
315 url,
316 network_opts: stored_network_opts,
317 reconnect_count: 0,
318 consecutive_invalid: 0,
319 },
320 stream_info,
321 container_info,
322 ))
323 }
324}
325
326impl Drop for VideoDecoderInner {
327 fn drop(&mut self) {
328 if let Some(sws_ctx) = self.sws_ctx {
330 unsafe {
332 ff_sys::swscale::free_context(sws_ctx);
333 }
334 }
335
336 if let Some(thumbnail_ctx) = self.thumbnail_sws_ctx {
338 unsafe {
340 ff_sys::swscale::free_context(thumbnail_ctx);
341 }
342 }
343
344 if let Some(hw_ctx) = self.hw_device_ctx {
346 unsafe {
348 ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
349 }
350 }
351
352 if !self.frame.is_null() {
354 unsafe {
356 ff_sys::av_frame_free(&mut (self.frame as *mut _));
357 }
358 }
359
360 if !self.packet.is_null() {
361 unsafe {
363 ff_sys::av_packet_free(&mut (self.packet as *mut _));
364 }
365 }
366
367 if !self.codec_ctx.is_null() {
369 unsafe {
371 ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
372 }
373 }
374
375 if !self.format_ctx.is_null() {
377 unsafe {
379 ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
380 }
381 }
382 }
383}
384
385unsafe impl Send for VideoDecoderInner {}
388
389#[cfg(test)]
390mod tests {
391 use ff_format::PixelFormat;
392 use ff_format::codec::VideoCodec;
393 use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
394
395 use crate::HardwareAccel;
396
397 use super::VideoDecoderInner;
398
399 #[test]
404 fn pixel_format_yuv420p() {
405 assert_eq!(
406 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
407 PixelFormat::Yuv420p
408 );
409 }
410
411 #[test]
412 fn pixel_format_yuv422p() {
413 assert_eq!(
414 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P),
415 PixelFormat::Yuv422p
416 );
417 }
418
419 #[test]
420 fn pixel_format_yuv444p() {
421 assert_eq!(
422 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P),
423 PixelFormat::Yuv444p
424 );
425 }
426
427 #[test]
428 fn pixel_format_rgb24() {
429 assert_eq!(
430 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
431 PixelFormat::Rgb24
432 );
433 }
434
435 #[test]
436 fn pixel_format_bgr24() {
437 assert_eq!(
438 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24),
439 PixelFormat::Bgr24
440 );
441 }
442
443 #[test]
444 fn pixel_format_rgba() {
445 assert_eq!(
446 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
447 PixelFormat::Rgba
448 );
449 }
450
451 #[test]
452 fn pixel_format_bgra() {
453 assert_eq!(
454 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA),
455 PixelFormat::Bgra
456 );
457 }
458
459 #[test]
460 fn pixel_format_gray8() {
461 assert_eq!(
462 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
463 PixelFormat::Gray8
464 );
465 }
466
467 #[test]
468 fn pixel_format_nv12() {
469 assert_eq!(
470 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV12),
471 PixelFormat::Nv12
472 );
473 }
474
475 #[test]
476 fn pixel_format_nv21() {
477 assert_eq!(
478 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV21),
479 PixelFormat::Nv21
480 );
481 }
482
483 #[test]
484 fn pixel_format_yuv420p10le_should_return_yuv420p10le() {
485 assert_eq!(
486 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE),
487 PixelFormat::Yuv420p10le
488 );
489 }
490
491 #[test]
492 fn pixel_format_yuv422p10le_should_return_yuv422p10le() {
493 assert_eq!(
494 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P10LE),
495 PixelFormat::Yuv422p10le
496 );
497 }
498
499 #[test]
500 fn pixel_format_yuv444p10le_should_return_yuv444p10le() {
501 assert_eq!(
502 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P10LE),
503 PixelFormat::Yuv444p10le
504 );
505 }
506
507 #[test]
508 fn pixel_format_p010le_should_return_p010le() {
509 assert_eq!(
510 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_P010LE),
511 PixelFormat::P010le
512 );
513 }
514
515 #[test]
516 fn pixel_format_unknown_falls_back_to_yuv420p() {
517 assert_eq!(
518 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NONE),
519 PixelFormat::Yuv420p
520 );
521 }
522
523 #[test]
528 fn color_space_bt709() {
529 assert_eq!(
530 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT709),
531 ColorSpace::Bt709
532 );
533 }
534
535 #[test]
536 fn color_space_bt470bg_yields_bt470bg() {
537 assert_eq!(
538 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT470BG),
539 ColorSpace::Bt470bg
540 );
541 }
542
543 #[test]
544 fn color_space_smpte170m_yields_smpte170m() {
545 assert_eq!(
546 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M),
547 ColorSpace::Smpte170m
548 );
549 }
550
551 #[test]
552 fn color_space_bt2020_ncl() {
553 assert_eq!(
554 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL),
555 ColorSpace::Bt2020Ncl
556 );
557 }
558
559 #[test]
560 fn color_space_unknown_falls_back_to_bt709() {
561 assert_eq!(
562 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_UNSPECIFIED),
563 ColorSpace::Bt709
564 );
565 }
566
567 #[test]
572 fn color_range_jpeg_yields_full() {
573 assert_eq!(
574 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_JPEG),
575 ColorRange::Full
576 );
577 }
578
579 #[test]
580 fn color_range_mpeg_yields_limited() {
581 assert_eq!(
582 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_MPEG),
583 ColorRange::Limited
584 );
585 }
586
587 #[test]
588 fn color_range_unknown_falls_back_to_limited() {
589 assert_eq!(
590 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_UNSPECIFIED),
591 ColorRange::Limited
592 );
593 }
594
595 #[test]
600 fn color_primaries_bt709() {
601 assert_eq!(
602 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT709),
603 ColorPrimaries::Bt709
604 );
605 }
606
607 #[test]
608 fn color_primaries_bt470bg_yields_bt470bg() {
609 assert_eq!(
610 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG),
611 ColorPrimaries::Bt470bg
612 );
613 }
614
615 #[test]
616 fn color_primaries_smpte170m_yields_smpte170m() {
617 assert_eq!(
618 VideoDecoderInner::convert_color_primaries(
619 ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M
620 ),
621 ColorPrimaries::Smpte170m
622 );
623 }
624
625 #[test]
626 fn color_primaries_bt2020() {
627 assert_eq!(
628 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020),
629 ColorPrimaries::Bt2020
630 );
631 }
632
633 #[test]
634 fn color_primaries_unknown_falls_back_to_bt709() {
635 assert_eq!(
636 VideoDecoderInner::convert_color_primaries(
637 ff_sys::AVColorPrimaries_AVCOL_PRI_UNSPECIFIED
638 ),
639 ColorPrimaries::Bt709
640 );
641 }
642
643 #[test]
648 fn codec_h264() {
649 assert_eq!(
650 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_H264),
651 VideoCodec::H264
652 );
653 }
654
655 #[test]
656 fn codec_hevc_yields_h265() {
657 assert_eq!(
658 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_HEVC),
659 VideoCodec::H265
660 );
661 }
662
663 #[test]
664 fn codec_vp8() {
665 assert_eq!(
666 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP8),
667 VideoCodec::Vp8
668 );
669 }
670
671 #[test]
672 fn codec_vp9() {
673 assert_eq!(
674 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP9),
675 VideoCodec::Vp9
676 );
677 }
678
679 #[test]
680 fn codec_av1() {
681 assert_eq!(
682 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_AV1),
683 VideoCodec::Av1
684 );
685 }
686
687 #[test]
688 fn codec_mpeg4() {
689 assert_eq!(
690 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_MPEG4),
691 VideoCodec::Mpeg4
692 );
693 }
694
695 #[test]
696 fn codec_prores() {
697 assert_eq!(
698 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_PRORES),
699 VideoCodec::ProRes
700 );
701 }
702
703 #[test]
704 fn codec_unknown_falls_back_to_h264() {
705 assert_eq!(
706 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_NONE),
707 VideoCodec::H264
708 );
709 }
710
711 #[test]
716 fn hw_accel_auto_yields_none() {
717 assert_eq!(
718 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Auto),
719 None
720 );
721 }
722
723 #[test]
724 fn hw_accel_none_yields_none() {
725 assert_eq!(
726 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::None),
727 None
728 );
729 }
730
731 #[test]
732 fn hw_accel_nvdec_yields_cuda() {
733 assert_eq!(
734 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Nvdec),
735 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_CUDA)
736 );
737 }
738
739 #[test]
740 fn hw_accel_qsv_yields_qsv() {
741 assert_eq!(
742 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Qsv),
743 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_QSV)
744 );
745 }
746
747 #[test]
748 fn hw_accel_amf_yields_d3d11va() {
749 assert_eq!(
750 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Amf),
751 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_D3D11VA)
752 );
753 }
754
755 #[test]
756 fn hw_accel_videotoolbox() {
757 assert_eq!(
758 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::VideoToolbox),
759 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VIDEOTOOLBOX)
760 );
761 }
762
763 #[test]
764 fn hw_accel_vaapi() {
765 assert_eq!(
766 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Vaapi),
767 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VAAPI)
768 );
769 }
770
771 #[test]
776 fn pixel_format_to_av_round_trip_yuv420p() {
777 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv420p);
778 assert_eq!(
779 VideoDecoderInner::convert_pixel_format(av),
780 PixelFormat::Yuv420p
781 );
782 }
783
784 #[test]
785 fn pixel_format_to_av_round_trip_yuv422p() {
786 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv422p);
787 assert_eq!(
788 VideoDecoderInner::convert_pixel_format(av),
789 PixelFormat::Yuv422p
790 );
791 }
792
793 #[test]
794 fn pixel_format_to_av_round_trip_yuv444p() {
795 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv444p);
796 assert_eq!(
797 VideoDecoderInner::convert_pixel_format(av),
798 PixelFormat::Yuv444p
799 );
800 }
801
802 #[test]
803 fn pixel_format_to_av_round_trip_rgb24() {
804 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgb24);
805 assert_eq!(
806 VideoDecoderInner::convert_pixel_format(av),
807 PixelFormat::Rgb24
808 );
809 }
810
811 #[test]
812 fn pixel_format_to_av_round_trip_bgr24() {
813 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgr24);
814 assert_eq!(
815 VideoDecoderInner::convert_pixel_format(av),
816 PixelFormat::Bgr24
817 );
818 }
819
820 #[test]
821 fn pixel_format_to_av_round_trip_rgba() {
822 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgba);
823 assert_eq!(
824 VideoDecoderInner::convert_pixel_format(av),
825 PixelFormat::Rgba
826 );
827 }
828
829 #[test]
830 fn pixel_format_to_av_round_trip_bgra() {
831 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgra);
832 assert_eq!(
833 VideoDecoderInner::convert_pixel_format(av),
834 PixelFormat::Bgra
835 );
836 }
837
838 #[test]
839 fn pixel_format_to_av_round_trip_gray8() {
840 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Gray8);
841 assert_eq!(
842 VideoDecoderInner::convert_pixel_format(av),
843 PixelFormat::Gray8
844 );
845 }
846
847 #[test]
848 fn pixel_format_to_av_round_trip_nv12() {
849 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv12);
850 assert_eq!(
851 VideoDecoderInner::convert_pixel_format(av),
852 PixelFormat::Nv12
853 );
854 }
855
856 #[test]
857 fn pixel_format_to_av_round_trip_nv21() {
858 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv21);
859 assert_eq!(
860 VideoDecoderInner::convert_pixel_format(av),
861 PixelFormat::Nv21
862 );
863 }
864
865 #[test]
866 fn pixel_format_to_av_unknown_falls_back_to_yuv420p_av() {
867 assert_eq!(
869 VideoDecoderInner::pixel_format_to_av(PixelFormat::Other(999)),
870 ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
871 );
872 }
873
874 #[test]
879 fn codec_name_should_return_h264_for_h264_codec_id() {
880 let name =
881 unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_H264) };
882 assert_eq!(name, "h264");
883 }
884
885 #[test]
886 fn codec_name_should_return_none_for_none_codec_id() {
887 let name =
888 unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_NONE) };
889 assert_eq!(name, "none");
890 }
891
892 #[test]
893 fn convert_pixel_format_should_map_gbrpf32le() {
894 assert_eq!(
895 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GBRPF32LE),
896 PixelFormat::Gbrpf32le
897 );
898 }
899
900 #[test]
901 fn unsupported_codec_error_should_include_codec_name() {
902 let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_H264;
903 let codec_name = unsafe { VideoDecoderInner::extract_codec_name(codec_id) };
904 let error = crate::error::DecodeError::UnsupportedCodec {
905 codec: format!("{codec_name} (codec_id={codec_id:?})"),
906 };
907 let msg = error.to_string();
908 assert!(msg.contains("h264"), "expected codec name in error: {msg}");
909 assert!(
910 msg.contains("codec_id="),
911 "expected codec_id in error: {msg}"
912 );
913 }
914}