Skip to main content

ff_decode/video/decoder_inner/
mod.rs

1//! Internal video decoder implementation using FFmpeg.
2//!
3//! This module contains the low-level decoder logic that directly interacts
4//! with FFmpeg's C API through the ff-sys crate. It is not exposed publicly.
5
6// Allow unsafe code in this module as it's necessary for FFmpeg FFI
7#![allow(unsafe_code)]
8// Allow specific clippy lints for FFmpeg FFI code
9#![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
52/// Tolerance in seconds for keyframe/backward seek modes.
53///
54/// When seeking in Keyframe or Backward mode, frames are skipped until we're within
55/// this tolerance of the target position. This balances accuracy with performance for
56/// typical GOP sizes (1-2 seconds).
57const KEYFRAME_SEEK_TOLERANCE_SECS: u64 = 1;
58
59mod context;
60mod decoding;
61mod format_convert;
62mod hardware;
63mod seeking;
64
65/// Internal decoder state holding FFmpeg contexts.
66///
67/// This structure manages the lifecycle of FFmpeg objects and is responsible
68/// for proper cleanup when dropped.
69pub(crate) struct VideoDecoderInner {
70    /// Format context for reading the media file
71    pub(super) format_ctx: InputFormatContext,
72    /// Codec context for decoding video frames
73    pub(super) codec_ctx: ff_sys::CodecContext,
74    /// Video stream index in the format context
75    pub(super) stream_index: i32,
76    /// SwScale context for pixel format conversion and/or scaling (optional)
77    pub(super) sws_ctx: Option<ff_sys::ScaleContext>,
78    /// Cache key for the main sws_ctx: (src_w, src_h, src_fmt, dst_w, dst_h, dst_fmt)
79    pub(super) sws_cache_key: Option<(u32, u32, i32, u32, u32, i32)>,
80    /// Target output pixel format (if conversion is needed)
81    pub(super) output_format: Option<PixelFormat>,
82    /// Requested output scale (if resizing is needed)
83    pub(super) output_scale: Option<OutputScale>,
84    /// Whether the source is a live/streaming input (seeking is not supported)
85    pub(super) is_live: bool,
86    /// Whether end of file has been reached
87    pub(super) eof: bool,
88    /// Current playback position
89    pub(super) position: Duration,
90    /// Reusable packet for reading from file
91    pub(super) packet: Packet,
92    /// Reusable frame for decoding
93    pub(super) frame: Frame,
94    /// Cached SwScale context for thumbnail generation
95    pub(super) thumbnail_sws_ctx: Option<ff_sys::ScaleContext>,
96    /// Last thumbnail dimensions (for cache invalidation)
97    pub(super) thumbnail_cache_key: Option<(u32, u32, u32, u32, AVPixelFormat)>,
98    /// Owned hardware device reference kept alive for as long as the codec
99    /// context uses it. Held only for its `Drop` (the codec keeps its own
100    /// reference), so it is never read after construction.
101    #[expect(dead_code, reason = "RAII drop-guard: released when the decoder drops")]
102    pub(super) hw_device_ctx: Option<HwDeviceContext>,
103    /// Active hardware acceleration mode
104    pub(super) active_hw_accel: HardwareAccel,
105    /// Optional frame pool for memory reuse
106    pub(super) frame_pool: Option<Arc<dyn FramePool>>,
107    /// URL used to open this source — `None` for file-path and image-sequence sources.
108    pub(super) url: Option<String>,
109    /// Network options used for the initial open (timeouts, reconnect config).
110    pub(super) network_opts: NetworkOptions,
111    /// Number of successful reconnects so far (for logging).
112    pub(super) reconnect_count: u32,
113    /// Number of consecutive `AVERROR_INVALIDDATA` packets skipped without a successful frame.
114    /// Reset to 0 on each successfully decoded frame.
115    pub(super) consecutive_invalid: u32,
116}
117
118impl VideoDecoderInner {
119    /// Opens a media file and initializes the decoder.
120    ///
121    /// # Arguments
122    ///
123    /// * `path` - Path to the media file
124    /// * `output_format` - Optional target pixel format for conversion
125    /// * `hardware_accel` - Hardware acceleration mode
126    /// * `thread_count` - Number of decoding threads (0 = auto)
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if:
131    /// - The file cannot be opened
132    /// - No video stream is found
133    /// - The codec is not supported
134    /// - Decoder initialization fails
135    #[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        // Ensure FFmpeg is initialized (thread-safe and idempotent)
148        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        // Verify SRT availability before attempting to open (feature + runtime check).
162        if is_network_url {
163            crate::network::check_srt_url(path_str)?;
164        }
165
166        // Open the input (owned demux context). A caller-supplied source wins over
167        // the path, which is then only a label for diagnostics.
168        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        // Read stream information
187        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        // Detect live/streaming source via the AVFMT_TS_DISCONT flag on AVInputFormat.
196        let is_live = (ctx.iformat_flags() & ff_sys::AVFMT_TS_DISCONT) != 0;
197
198        // Find the video stream
199        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        // Find the decoder for this codec
205        // SAFETY: codec_id is valid from FFmpeg
206        let codec_name = unsafe { Self::extract_codec_name(codec_id) };
207        let codec = ff_sys::Codec::find_decoder(codec_id).ok_or_else(|| {
208            // Distinguish between a totally unknown codec ID and a known codec
209            // whose decoder was not compiled into this FFmpeg build.
210            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        // Allocate codec context (freed on drop by CodecContext).
225        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        // Copy codec parameters from stream to context
235        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        // Set thread count
252        if thread_count > 0 {
253            codec_ctx.set_thread_count(thread_count as i32);
254        }
255
256        // Initialize hardware acceleration if requested. The owned
257        // `HwDeviceContext` (when Some) holds our reference to the hw device; the
258        // codec context takes its own via `set_hw_device_ctx`, so both are freed
259        // independently on drop.
260        let (hw_device_ctx, active_hw_accel) =
261            Self::init_hardware_accel(&mut codec_ctx, hardware_accel)?;
262
263        // Open the codec. On failure, `hw_device_ctx` (owned) drops with the rest
264        // of this constructor's locals, releasing our reference; `codec_ctx` drops
265        // too, releasing its own — no manual cleanup needed.
266        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        // Extract stream and container information through the borrowed
277        // stream / codec-context accessors.
278        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        // Extract container information
287        let container_info = Self::extract_container_info(&ctx);
288
289        // Allocate packet and frame (owned; free on drop, including on an early
290        // return from a later `?` in this constructor).
291        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        // All initialization successful - transfer ownership to VideoDecoderInner
307        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
337// No manual `Drop` is needed: every field owns its FFmpeg resource and frees
338// itself when the struct drops (fields drop in declaration order). The hw device
339// reference (`HwDeviceContext`) is reference-counted and the `CodecContext` holds
340// its own reference, so the order in which the two release relative to each other
341// does not matter.
342
343// SAFETY: VideoDecoderInner manages FFmpeg contexts which are thread-safe when not shared.
344// We don't expose mutable access across threads, so Send is safe.
345unsafe 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    // -------------------------------------------------------------------------
358    // convert_pixel_format
359    // -------------------------------------------------------------------------
360
361    #[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    // -------------------------------------------------------------------------
482    // convert_color_space
483    // -------------------------------------------------------------------------
484
485    #[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    // -------------------------------------------------------------------------
526    // convert_color_range
527    // -------------------------------------------------------------------------
528
529    #[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    // -------------------------------------------------------------------------
554    // convert_color_primaries
555    // -------------------------------------------------------------------------
556
557    #[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    // -------------------------------------------------------------------------
602    // convert_codec
603    // -------------------------------------------------------------------------
604
605    #[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    // -------------------------------------------------------------------------
670    // hw_accel_to_device_type
671    // -------------------------------------------------------------------------
672
673    #[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    // -------------------------------------------------------------------------
730    // pixel_format_to_av — round-trip
731    // -------------------------------------------------------------------------
732
733    #[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        // Other(999) has no explicit mapping and hits the _ fallback arm.
826        assert_eq!(
827            VideoDecoderInner::pixel_format_to_av(PixelFormat::Other(999)),
828            ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
829        );
830    }
831
832    // -------------------------------------------------------------------------
833    // extract_codec_name
834    // -------------------------------------------------------------------------
835
836    #[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}