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::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
53/// Tolerance in seconds for keyframe/backward seek modes.
54///
55/// When seeking in Keyframe or Backward mode, frames are skipped until we're within
56/// this tolerance of the target position. This balances accuracy with performance for
57/// typical GOP sizes (1-2 seconds).
58const KEYFRAME_SEEK_TOLERANCE_SECS: u64 = 1;
59
60mod context;
61mod decoding;
62mod format_convert;
63mod hardware;
64mod seeking;
65
66/// Internal decoder state holding FFmpeg contexts.
67///
68/// This structure manages the lifecycle of FFmpeg objects and is responsible
69/// for proper cleanup when dropped.
70pub(crate) struct VideoDecoderInner {
71    /// Format context for reading the media file
72    pub(super) format_ctx: *mut AVFormatContext,
73    /// Codec context for decoding video frames
74    pub(super) codec_ctx: *mut AVCodecContext,
75    /// Video stream index in the format context
76    pub(super) stream_index: i32,
77    /// SwScale context for pixel format conversion and/or scaling (optional)
78    pub(super) sws_ctx: Option<*mut SwsContext>,
79    /// Cache key for the main sws_ctx: (src_w, src_h, src_fmt, dst_w, dst_h, dst_fmt)
80    pub(super) sws_cache_key: Option<(u32, u32, i32, u32, u32, i32)>,
81    /// Target output pixel format (if conversion is needed)
82    pub(super) output_format: Option<PixelFormat>,
83    /// Requested output scale (if resizing is needed)
84    pub(super) output_scale: Option<OutputScale>,
85    /// Whether the source is a live/streaming input (seeking is not supported)
86    pub(super) is_live: bool,
87    /// Whether end of file has been reached
88    pub(super) eof: bool,
89    /// Current playback position
90    pub(super) position: Duration,
91    /// Reusable packet for reading from file
92    pub(super) packet: *mut AVPacket,
93    /// Reusable frame for decoding
94    pub(super) frame: *mut AVFrame,
95    /// Cached SwScale context for thumbnail generation
96    pub(super) thumbnail_sws_ctx: Option<*mut SwsContext>,
97    /// Last thumbnail dimensions (for cache invalidation)
98    pub(super) thumbnail_cache_key: Option<(u32, u32, u32, u32, AVPixelFormat)>,
99    /// Hardware device context (if hardware acceleration is active)
100    pub(super) hw_device_ctx: Option<*mut AVBufferRef>,
101    /// Active hardware acceleration mode
102    pub(super) active_hw_accel: HardwareAccel,
103    /// Optional frame pool for memory reuse
104    pub(super) frame_pool: Option<Arc<dyn FramePool>>,
105    /// URL used to open this source — `None` for file-path and image-sequence sources.
106    pub(super) url: Option<String>,
107    /// Network options used for the initial open (timeouts, reconnect config).
108    pub(super) network_opts: NetworkOptions,
109    /// Number of successful reconnects so far (for logging).
110    pub(super) reconnect_count: u32,
111    /// Number of consecutive `AVERROR_INVALIDDATA` packets skipped without a successful frame.
112    /// Reset to 0 on each successfully decoded frame.
113    pub(super) consecutive_invalid: u32,
114}
115
116impl VideoDecoderInner {
117    /// Opens a media file and initializes the decoder.
118    ///
119    /// # Arguments
120    ///
121    /// * `path` - Path to the media file
122    /// * `output_format` - Optional target pixel format for conversion
123    /// * `hardware_accel` - Hardware acceleration mode
124    /// * `thread_count` - Number of decoding threads (0 = auto)
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if:
129    /// - The file cannot be opened
130    /// - No video stream is found
131    /// - The codec is not supported
132    /// - Decoder initialization fails
133    #[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        // Ensure FFmpeg is initialized (thread-safe and idempotent)
145        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        // Verify SRT availability before attempting to open (feature + runtime check).
159        if is_network_url {
160            crate::network::check_srt_url(path_str)?;
161        }
162
163        // Open the input (with RAII guard for cleanup on error).
164        // SAFETY: Path/URL is valid; AvFormatContextGuard ensures cleanup.
165        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        // Read stream information
185        // SAFETY: format_ctx is valid and owned by guard
186        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        // Detect live/streaming source via the AVFMT_TS_DISCONT flag on AVInputFormat.
194        // SAFETY: format_ctx is valid and non-null; iformat is set by avformat_open_input
195        //         and is non-null for all successfully opened formats.
196        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        // Find the video stream
202        // SAFETY: format_ctx is valid
203        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        // Find the decoder for this codec
211        // SAFETY: codec_id is valid from FFmpeg
212        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                // Distinguish between a totally unknown codec ID and a known codec
216                // whose decoder was not compiled into this FFmpeg build.
217                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        // Allocate codec context (with RAII guard)
233        // SAFETY: codec pointer is valid, AvCodecContextGuard ensures cleanup
234        let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
235        let codec_ctx = codec_ctx_guard.as_ptr();
236
237        // Copy codec parameters from stream to context
238        // SAFETY: format_ctx and codec_ctx are valid, stream_index is valid
239        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            // Set thread count
253            if thread_count > 0 {
254                (*codec_ctx).thread_count = thread_count as i32;
255            }
256        }
257
258        // Initialize hardware acceleration if requested
259        // SAFETY: codec_ctx is valid and not yet opened
260        let (hw_device_ctx, active_hw_accel) =
261            unsafe { Self::init_hardware_accel(codec_ctx, hardware_accel)? };
262
263        // Open the codec
264        // SAFETY: codec_ctx and codec are valid, hardware device context is set if requested
265        unsafe {
266            ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
267                // If codec opening failed, we still own our reference to hw_device_ctx
268                // but it will be cleaned up when codec_ctx is freed (which happens
269                // when codec_ctx_guard is dropped)
270                // Our reference in hw_device_ctx will be cleaned up here
271                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        // Extract stream information
282        // SAFETY: All pointers are valid
283        let stream_info =
284            unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
285
286        // Extract container information
287        // SAFETY: format_ctx is valid and avformat_find_stream_info has been called
288        let container_info = unsafe { Self::extract_container_info(format_ctx) };
289
290        // Allocate packet and frame (with RAII guards)
291        // SAFETY: FFmpeg is initialized, guards ensure cleanup
292        let packet_guard = unsafe { AvPacketGuard::new()? };
293        let frame_guard = unsafe { AvFrameGuard::new()? };
294
295        // All initialization successful - transfer ownership to VideoDecoderInner
296        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        // Free SwScale context if allocated
329        if let Some(sws_ctx) = self.sws_ctx {
330            // SAFETY: sws_ctx is valid and owned by this instance
331            unsafe {
332                ff_sys::swscale::free_context(sws_ctx);
333            }
334        }
335
336        // Free cached thumbnail SwScale context if allocated
337        if let Some(thumbnail_ctx) = self.thumbnail_sws_ctx {
338            // SAFETY: thumbnail_ctx is valid and owned by this instance
339            unsafe {
340                ff_sys::swscale::free_context(thumbnail_ctx);
341            }
342        }
343
344        // Free hardware device context if allocated
345        if let Some(hw_ctx) = self.hw_device_ctx {
346            // SAFETY: hw_ctx is valid and owned by this instance
347            unsafe {
348                ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
349            }
350        }
351
352        // Free frame and packet
353        if !self.frame.is_null() {
354            // SAFETY: self.frame is valid and owned by this instance
355            unsafe {
356                ff_sys::av_frame_free(&mut (self.frame as *mut _));
357            }
358        }
359
360        if !self.packet.is_null() {
361            // SAFETY: self.packet is valid and owned by this instance
362            unsafe {
363                ff_sys::av_packet_free(&mut (self.packet as *mut _));
364            }
365        }
366
367        // Free codec context
368        if !self.codec_ctx.is_null() {
369            // SAFETY: self.codec_ctx is valid and owned by this instance
370            unsafe {
371                ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
372            }
373        }
374
375        // Close format context
376        if !self.format_ctx.is_null() {
377            // SAFETY: self.format_ctx is valid and owned by this instance
378            unsafe {
379                ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
380            }
381        }
382    }
383}
384
385// SAFETY: VideoDecoderInner manages FFmpeg contexts which are thread-safe when not shared.
386// We don't expose mutable access across threads, so Send is safe.
387unsafe 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    // -------------------------------------------------------------------------
400    // convert_pixel_format
401    // -------------------------------------------------------------------------
402
403    #[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    // -------------------------------------------------------------------------
524    // convert_color_space
525    // -------------------------------------------------------------------------
526
527    #[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    // -------------------------------------------------------------------------
568    // convert_color_range
569    // -------------------------------------------------------------------------
570
571    #[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    // -------------------------------------------------------------------------
596    // convert_color_primaries
597    // -------------------------------------------------------------------------
598
599    #[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    // -------------------------------------------------------------------------
644    // convert_codec
645    // -------------------------------------------------------------------------
646
647    #[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    // -------------------------------------------------------------------------
712    // hw_accel_to_device_type
713    // -------------------------------------------------------------------------
714
715    #[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    // -------------------------------------------------------------------------
772    // pixel_format_to_av — round-trip
773    // -------------------------------------------------------------------------
774
775    #[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        // Other(999) has no explicit mapping and hits the _ fallback arm.
868        assert_eq!(
869            VideoDecoderInner::pixel_format_to_av(PixelFormat::Other(999)),
870            ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
871        );
872    }
873
874    // -------------------------------------------------------------------------
875    // extract_codec_name
876    // -------------------------------------------------------------------------
877
878    #[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}