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::sync::Arc;
28use std::time::Duration;
29
30use ff_format::NetworkOptions;
31
32use ff_format::PooledBuffer;
33use ff_format::codec::VideoCodec;
34use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
35use ff_format::container::ContainerInfo;
36use ff_format::time::{Rational, Timestamp};
37use ff_format::{PixelFormat, VideoFrame, VideoStreamInfo};
38use ff_sys::{
39 AVCodecID, AVColorPrimaries, AVColorRange, AVColorSpace, AVHWDeviceType,
40 AVMediaType_AVMEDIA_TYPE_VIDEO, AVPixelFormat, Frame, HwDeviceContext, InputFormatContext,
41 Packet,
42};
43
44use crate::HardwareAccel;
45use crate::error::DecodeError;
46use crate::shared::guards_inner::{
47 open_custom_ctx, open_image_sequence_ctx, open_input_ctx, open_url_ctx,
48};
49use crate::video::builder::OutputScale;
50use ff_common::FramePool;
51
52const KEYFRAME_SEEK_TOLERANCE_SECS: u64 = 1;
58
59mod context;
60mod decoding;
61mod format_convert;
62mod hardware;
63mod seeking;
64
65pub(crate) struct VideoDecoderInner {
70 pub(super) format_ctx: InputFormatContext,
72 pub(super) codec_ctx: ff_sys::CodecContext,
74 pub(super) stream_index: i32,
76 pub(super) sws_ctx: Option<ff_sys::ScaleContext>,
78 pub(super) sws_cache_key: Option<(u32, u32, i32, u32, u32, i32)>,
80 pub(super) output_format: Option<PixelFormat>,
82 pub(super) output_scale: Option<OutputScale>,
84 pub(super) is_live: bool,
86 pub(super) eof: bool,
88 pub(super) position: Duration,
90 pub(super) packet: Packet,
92 pub(super) frame: Frame,
94 pub(super) thumbnail_sws_ctx: Option<ff_sys::ScaleContext>,
96 pub(super) thumbnail_cache_key: Option<(u32, u32, u32, u32, AVPixelFormat)>,
98 #[expect(dead_code, reason = "RAII drop-guard: released when the decoder drops")]
102 pub(super) hw_device_ctx: Option<HwDeviceContext>,
103 pub(super) active_hw_accel: HardwareAccel,
105 pub(super) frame_pool: Option<Arc<dyn FramePool>>,
107 pub(super) url: Option<String>,
109 pub(super) network_opts: NetworkOptions,
111 pub(super) reconnect_count: u32,
113 pub(super) consecutive_invalid: u32,
116}
117
118impl VideoDecoderInner {
119 #[allow(clippy::too_many_arguments)]
136 pub(crate) fn new(
137 path: &Path,
138 output_format: Option<PixelFormat>,
139 output_scale: Option<OutputScale>,
140 hardware_accel: HardwareAccel,
141 thread_count: usize,
142 frame_rate: Option<u32>,
143 frame_pool: Option<Arc<dyn FramePool>>,
144 network_opts: Option<NetworkOptions>,
145 source: Option<Box<dyn ff_sys::IoSource>>,
146 ) -> Result<(Self, VideoStreamInfo, ContainerInfo), DecodeError> {
147 ff_sys::ensure_initialized();
149
150 let path_str = path.to_str().unwrap_or("");
151 let is_image_sequence = path_str.contains('%');
152 let is_network_url = crate::network::is_url(path_str);
153
154 let url = if is_network_url {
155 Some(path_str.to_owned())
156 } else {
157 None
158 };
159 let stored_network_opts = network_opts.clone().unwrap_or_default();
160
161 if is_network_url {
163 crate::network::check_srt_url(path_str)?;
164 }
165
166 let mut ctx = if let Some(source) = source {
169 open_custom_ctx(source)?
170 } else if is_network_url {
171 let network = network_opts.unwrap_or_default();
172 log::info!(
173 "opening network source url={} connect_timeout_ms={} read_timeout_ms={}",
174 crate::network::sanitize_url(path_str),
175 network.connect_timeout.as_millis(),
176 network.read_timeout.as_millis(),
177 );
178 open_url_ctx(path_str, &network)?
179 } else if is_image_sequence {
180 let fps = frame_rate.unwrap_or(25);
181 open_image_sequence_ctx(path, fps)?
182 } else {
183 open_input_ctx(path)?
184 };
185
186 ctx.find_stream_info().map_err(|e| DecodeError::Ffmpeg {
188 code: e.code(),
189 message: format!(
190 "Failed to find stream info: {}",
191 ff_sys::av_error_string(e.code())
192 ),
193 })?;
194
195 let is_live = (ctx.iformat_flags() & ff_sys::AVFMT_TS_DISCONT) != 0;
197
198 let (stream_index, codec_id) =
200 Self::find_video_stream(&ctx).ok_or_else(|| DecodeError::NoVideoStream {
201 path: path.to_path_buf(),
202 })?;
203
204 let codec_name = unsafe { Self::extract_codec_name(codec_id) };
207 let codec = ff_sys::Codec::find_decoder(codec_id).ok_or_else(|| {
208 if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_EXR {
211 DecodeError::DecoderUnavailable {
212 codec: "exr".to_string(),
213 hint: "Requires FFmpeg built with EXR support \
214 (--enable-decoder=exr)"
215 .to_string(),
216 }
217 } else {
218 DecodeError::UnsupportedCodec {
219 codec: format!("{codec_name} (codec_id={codec_id:?})"),
220 }
221 }
222 })?;
223
224 let mut codec_ctx =
226 ff_sys::CodecContext::new(Some(codec)).map_err(|e| DecodeError::Ffmpeg {
227 code: e.code(),
228 message: format!(
229 "Failed to allocate codec context: {}",
230 ff_sys::av_error_string(e.code())
231 ),
232 })?;
233
234 let codecpar = ctx
236 .stream(stream_index)
237 .ok_or_else(|| DecodeError::NoVideoStream {
238 path: path.to_path_buf(),
239 })?
240 .codecpar();
241 codec_ctx
242 .apply_parameters(&codecpar)
243 .map_err(|e| DecodeError::Ffmpeg {
244 code: e.code(),
245 message: format!(
246 "Failed to copy codec parameters: {}",
247 ff_sys::av_error_string(e.code())
248 ),
249 })?;
250
251 if thread_count > 0 {
253 codec_ctx.set_thread_count(thread_count as i32);
254 }
255
256 let (hw_device_ctx, active_hw_accel) =
261 Self::init_hardware_accel(&mut codec_ctx, hardware_accel)?;
262
263 codec_ctx
267 .open_codec(codec)
268 .map_err(|e| DecodeError::Ffmpeg {
269 code: e.code(),
270 message: format!(
271 "Failed to open codec: {}",
272 ff_sys::av_error_string(e.code())
273 ),
274 })?;
275
276 let duration_val = ctx.duration();
279 let stream = ctx
280 .stream(stream_index)
281 .ok_or_else(|| DecodeError::NoVideoStream {
282 path: path.to_path_buf(),
283 })?;
284 let stream_info = Self::extract_stream_info(stream, &codec_ctx, duration_val)?;
285
286 let container_info = Self::extract_container_info(&ctx);
288
289 let packet = Packet::new().map_err(|e| DecodeError::Ffmpeg {
292 code: e.code(),
293 message: format!(
294 "Failed to allocate packet: {}",
295 ff_sys::av_error_string(e.code())
296 ),
297 })?;
298 let frame = Frame::new().map_err(|e| DecodeError::Ffmpeg {
299 code: e.code(),
300 message: format!(
301 "Failed to allocate frame: {}",
302 ff_sys::av_error_string(e.code())
303 ),
304 })?;
305
306 Ok((
308 Self {
309 format_ctx: ctx,
310 codec_ctx,
311 stream_index: stream_index as i32,
312 sws_ctx: None,
313 sws_cache_key: None,
314 output_format,
315 output_scale,
316 is_live,
317 eof: false,
318 position: Duration::ZERO,
319 packet,
320 frame,
321 thumbnail_sws_ctx: None,
322 thumbnail_cache_key: None,
323 hw_device_ctx,
324 active_hw_accel,
325 frame_pool,
326 url,
327 network_opts: stored_network_opts,
328 reconnect_count: 0,
329 consecutive_invalid: 0,
330 },
331 stream_info,
332 container_info,
333 ))
334 }
335}
336
337unsafe impl Send for VideoDecoderInner {}
346
347#[cfg(test)]
348mod tests {
349 use ff_format::PixelFormat;
350 use ff_format::codec::VideoCodec;
351 use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
352
353 use crate::HardwareAccel;
354
355 use super::VideoDecoderInner;
356
357 #[test]
362 fn pixel_format_yuv420p() {
363 assert_eq!(
364 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
365 PixelFormat::Yuv420p
366 );
367 }
368
369 #[test]
370 fn pixel_format_yuv422p() {
371 assert_eq!(
372 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P),
373 PixelFormat::Yuv422p
374 );
375 }
376
377 #[test]
378 fn pixel_format_yuv444p() {
379 assert_eq!(
380 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P),
381 PixelFormat::Yuv444p
382 );
383 }
384
385 #[test]
386 fn pixel_format_rgb24() {
387 assert_eq!(
388 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
389 PixelFormat::Rgb24
390 );
391 }
392
393 #[test]
394 fn pixel_format_bgr24() {
395 assert_eq!(
396 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24),
397 PixelFormat::Bgr24
398 );
399 }
400
401 #[test]
402 fn pixel_format_rgba() {
403 assert_eq!(
404 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
405 PixelFormat::Rgba
406 );
407 }
408
409 #[test]
410 fn pixel_format_bgra() {
411 assert_eq!(
412 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA),
413 PixelFormat::Bgra
414 );
415 }
416
417 #[test]
418 fn pixel_format_gray8() {
419 assert_eq!(
420 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
421 PixelFormat::Gray8
422 );
423 }
424
425 #[test]
426 fn pixel_format_nv12() {
427 assert_eq!(
428 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV12),
429 PixelFormat::Nv12
430 );
431 }
432
433 #[test]
434 fn pixel_format_nv21() {
435 assert_eq!(
436 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV21),
437 PixelFormat::Nv21
438 );
439 }
440
441 #[test]
442 fn pixel_format_yuv420p10le_should_return_yuv420p10le() {
443 assert_eq!(
444 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE),
445 PixelFormat::Yuv420p10le
446 );
447 }
448
449 #[test]
450 fn pixel_format_yuv422p10le_should_return_yuv422p10le() {
451 assert_eq!(
452 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P10LE),
453 PixelFormat::Yuv422p10le
454 );
455 }
456
457 #[test]
458 fn pixel_format_yuv444p10le_should_return_yuv444p10le() {
459 assert_eq!(
460 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P10LE),
461 PixelFormat::Yuv444p10le
462 );
463 }
464
465 #[test]
466 fn pixel_format_p010le_should_return_p010le() {
467 assert_eq!(
468 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_P010LE),
469 PixelFormat::P010le
470 );
471 }
472
473 #[test]
474 fn pixel_format_unknown_falls_back_to_yuv420p() {
475 assert_eq!(
476 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NONE),
477 PixelFormat::Yuv420p
478 );
479 }
480
481 #[test]
486 fn color_space_bt709() {
487 assert_eq!(
488 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT709),
489 ColorSpace::Bt709
490 );
491 }
492
493 #[test]
494 fn color_space_bt470bg_yields_bt470bg() {
495 assert_eq!(
496 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT470BG),
497 ColorSpace::Bt470bg
498 );
499 }
500
501 #[test]
502 fn color_space_smpte170m_yields_smpte170m() {
503 assert_eq!(
504 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M),
505 ColorSpace::Smpte170m
506 );
507 }
508
509 #[test]
510 fn color_space_bt2020_ncl() {
511 assert_eq!(
512 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL),
513 ColorSpace::Bt2020Ncl
514 );
515 }
516
517 #[test]
518 fn color_space_unknown_falls_back_to_bt709() {
519 assert_eq!(
520 VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_UNSPECIFIED),
521 ColorSpace::Bt709
522 );
523 }
524
525 #[test]
530 fn color_range_jpeg_yields_full() {
531 assert_eq!(
532 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_JPEG),
533 ColorRange::Full
534 );
535 }
536
537 #[test]
538 fn color_range_mpeg_yields_limited() {
539 assert_eq!(
540 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_MPEG),
541 ColorRange::Limited
542 );
543 }
544
545 #[test]
546 fn color_range_unknown_falls_back_to_limited() {
547 assert_eq!(
548 VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_UNSPECIFIED),
549 ColorRange::Limited
550 );
551 }
552
553 #[test]
558 fn color_primaries_bt709() {
559 assert_eq!(
560 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT709),
561 ColorPrimaries::Bt709
562 );
563 }
564
565 #[test]
566 fn color_primaries_bt470bg_yields_bt470bg() {
567 assert_eq!(
568 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG),
569 ColorPrimaries::Bt470bg
570 );
571 }
572
573 #[test]
574 fn color_primaries_smpte170m_yields_smpte170m() {
575 assert_eq!(
576 VideoDecoderInner::convert_color_primaries(
577 ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M
578 ),
579 ColorPrimaries::Smpte170m
580 );
581 }
582
583 #[test]
584 fn color_primaries_bt2020() {
585 assert_eq!(
586 VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020),
587 ColorPrimaries::Bt2020
588 );
589 }
590
591 #[test]
592 fn color_primaries_unknown_falls_back_to_bt709() {
593 assert_eq!(
594 VideoDecoderInner::convert_color_primaries(
595 ff_sys::AVColorPrimaries_AVCOL_PRI_UNSPECIFIED
596 ),
597 ColorPrimaries::Bt709
598 );
599 }
600
601 #[test]
606 fn codec_h264() {
607 assert_eq!(
608 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_H264),
609 VideoCodec::H264
610 );
611 }
612
613 #[test]
614 fn codec_hevc_yields_h265() {
615 assert_eq!(
616 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_HEVC),
617 VideoCodec::H265
618 );
619 }
620
621 #[test]
622 fn codec_vp8() {
623 assert_eq!(
624 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP8),
625 VideoCodec::Vp8
626 );
627 }
628
629 #[test]
630 fn codec_vp9() {
631 assert_eq!(
632 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP9),
633 VideoCodec::Vp9
634 );
635 }
636
637 #[test]
638 fn codec_av1() {
639 assert_eq!(
640 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_AV1),
641 VideoCodec::Av1
642 );
643 }
644
645 #[test]
646 fn codec_mpeg4() {
647 assert_eq!(
648 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_MPEG4),
649 VideoCodec::Mpeg4
650 );
651 }
652
653 #[test]
654 fn codec_prores() {
655 assert_eq!(
656 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_PRORES),
657 VideoCodec::ProRes
658 );
659 }
660
661 #[test]
662 fn codec_unknown_falls_back_to_h264() {
663 assert_eq!(
664 VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_NONE),
665 VideoCodec::H264
666 );
667 }
668
669 #[test]
674 fn hw_accel_auto_yields_none() {
675 assert_eq!(
676 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Auto),
677 None
678 );
679 }
680
681 #[test]
682 fn hw_accel_none_yields_none() {
683 assert_eq!(
684 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::None),
685 None
686 );
687 }
688
689 #[test]
690 fn hw_accel_nvdec_yields_cuda() {
691 assert_eq!(
692 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Nvdec),
693 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_CUDA)
694 );
695 }
696
697 #[test]
698 fn hw_accel_qsv_yields_qsv() {
699 assert_eq!(
700 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Qsv),
701 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_QSV)
702 );
703 }
704
705 #[test]
706 fn hw_accel_amf_yields_d3d11va() {
707 assert_eq!(
708 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Amf),
709 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_D3D11VA)
710 );
711 }
712
713 #[test]
714 fn hw_accel_videotoolbox() {
715 assert_eq!(
716 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::VideoToolbox),
717 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VIDEOTOOLBOX)
718 );
719 }
720
721 #[test]
722 fn hw_accel_vaapi() {
723 assert_eq!(
724 VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Vaapi),
725 Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VAAPI)
726 );
727 }
728
729 #[test]
734 fn pixel_format_to_av_round_trip_yuv420p() {
735 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv420p);
736 assert_eq!(
737 VideoDecoderInner::convert_pixel_format(av),
738 PixelFormat::Yuv420p
739 );
740 }
741
742 #[test]
743 fn pixel_format_to_av_round_trip_yuv422p() {
744 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv422p);
745 assert_eq!(
746 VideoDecoderInner::convert_pixel_format(av),
747 PixelFormat::Yuv422p
748 );
749 }
750
751 #[test]
752 fn pixel_format_to_av_round_trip_yuv444p() {
753 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv444p);
754 assert_eq!(
755 VideoDecoderInner::convert_pixel_format(av),
756 PixelFormat::Yuv444p
757 );
758 }
759
760 #[test]
761 fn pixel_format_to_av_round_trip_rgb24() {
762 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgb24);
763 assert_eq!(
764 VideoDecoderInner::convert_pixel_format(av),
765 PixelFormat::Rgb24
766 );
767 }
768
769 #[test]
770 fn pixel_format_to_av_round_trip_bgr24() {
771 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgr24);
772 assert_eq!(
773 VideoDecoderInner::convert_pixel_format(av),
774 PixelFormat::Bgr24
775 );
776 }
777
778 #[test]
779 fn pixel_format_to_av_round_trip_rgba() {
780 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgba);
781 assert_eq!(
782 VideoDecoderInner::convert_pixel_format(av),
783 PixelFormat::Rgba
784 );
785 }
786
787 #[test]
788 fn pixel_format_to_av_round_trip_bgra() {
789 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgra);
790 assert_eq!(
791 VideoDecoderInner::convert_pixel_format(av),
792 PixelFormat::Bgra
793 );
794 }
795
796 #[test]
797 fn pixel_format_to_av_round_trip_gray8() {
798 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Gray8);
799 assert_eq!(
800 VideoDecoderInner::convert_pixel_format(av),
801 PixelFormat::Gray8
802 );
803 }
804
805 #[test]
806 fn pixel_format_to_av_round_trip_nv12() {
807 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv12);
808 assert_eq!(
809 VideoDecoderInner::convert_pixel_format(av),
810 PixelFormat::Nv12
811 );
812 }
813
814 #[test]
815 fn pixel_format_to_av_round_trip_nv21() {
816 let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv21);
817 assert_eq!(
818 VideoDecoderInner::convert_pixel_format(av),
819 PixelFormat::Nv21
820 );
821 }
822
823 #[test]
824 fn pixel_format_to_av_unknown_falls_back_to_yuv420p_av() {
825 assert_eq!(
827 VideoDecoderInner::pixel_format_to_av(PixelFormat::Other(999)),
828 ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
829 );
830 }
831
832 #[test]
837 fn codec_name_should_return_h264_for_h264_codec_id() {
838 let name =
839 unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_H264) };
840 assert_eq!(name, "h264");
841 }
842
843 #[test]
844 fn codec_name_should_return_none_for_none_codec_id() {
845 let name =
846 unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_NONE) };
847 assert_eq!(name, "none");
848 }
849
850 #[test]
851 fn convert_pixel_format_should_map_gbrpf32le() {
852 assert_eq!(
853 VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GBRPF32LE),
854 PixelFormat::Gbrpf32le
855 );
856 }
857
858 #[test]
859 fn unsupported_codec_error_should_include_codec_name() {
860 let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_H264;
861 let codec_name = unsafe { VideoDecoderInner::extract_codec_name(codec_id) };
862 let error = crate::error::DecodeError::UnsupportedCodec {
863 codec: format!("{codec_name} (codec_id={codec_id:?})"),
864 };
865 let msg = error.to_string();
866 assert!(msg.contains("h264"), "expected codec name in error: {msg}");
867 assert!(
868 msg.contains("codec_id="),
869 "expected codec_id in error: {msg}"
870 );
871 }
872}