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::video::builder::OutputScale;
48use ff_common::FramePool;
49
50/// Tolerance in seconds for keyframe/backward seek modes.
51///
52/// When seeking in Keyframe or Backward mode, frames are skipped until we're within
53/// this tolerance of the target position. This balances accuracy with performance for
54/// typical GOP sizes (1-2 seconds).
55const KEYFRAME_SEEK_TOLERANCE_SECS: u64 = 1;
56
57mod context;
58mod decoding;
59mod format_convert;
60mod hardware;
61mod seeking;
62
63/// RAII guard for `AVFormatContext` to ensure proper cleanup.
64struct AvFormatContextGuard(*mut AVFormatContext);
65
66impl AvFormatContextGuard {
67    /// Creates a new guard by opening an input file.
68    ///
69    /// # Safety
70    ///
71    /// Caller must ensure FFmpeg is initialized and path is valid.
72    unsafe fn new(path: &Path) -> Result<Self, DecodeError> {
73        // SAFETY: Caller ensures FFmpeg is initialized and path is valid
74        let format_ctx = unsafe {
75            ff_sys::avformat::open_input(path).map_err(|e| DecodeError::Ffmpeg {
76                code: e,
77                message: format!("Failed to open file: {}", ff_sys::av_error_string(e)),
78            })?
79        };
80        Ok(Self(format_ctx))
81    }
82
83    /// Returns the raw pointer.
84    const fn as_ptr(&self) -> *mut AVFormatContext {
85        self.0
86    }
87
88    /// Consumes the guard and returns the raw pointer without dropping.
89    fn into_raw(self) -> *mut AVFormatContext {
90        let ptr = self.0;
91        std::mem::forget(self);
92        ptr
93    }
94
95    /// Opens an image sequence using the `image2` demuxer.
96    ///
97    /// # Safety
98    ///
99    /// Caller must ensure FFmpeg is initialized and path is valid.
100    unsafe fn new_image_sequence(path: &Path, framerate: u32) -> Result<Self, DecodeError> {
101        // SAFETY: Caller ensures FFmpeg is initialized and path is a valid image-sequence pattern
102        let format_ctx = unsafe {
103            ff_sys::avformat::open_input_image_sequence(path, framerate).map_err(|e| {
104                DecodeError::Ffmpeg {
105                    code: e,
106                    message: format!(
107                        "Failed to open image sequence: {}",
108                        ff_sys::av_error_string(e)
109                    ),
110                }
111            })?
112        };
113        Ok(Self(format_ctx))
114    }
115
116    /// Opens a network URL with connect/read timeouts from `NetworkOptions`.
117    ///
118    /// # Safety
119    ///
120    /// Caller must ensure `FFmpeg` is initialized and `url` is a valid URL string.
121    unsafe fn new_url(url: &str, network: &NetworkOptions) -> Result<Self, DecodeError> {
122        // SAFETY: Caller ensures FFmpeg is initialized; url is a valid network URL.
123        let format_ctx = unsafe {
124            ff_sys::avformat::open_input_url(url, network.connect_timeout, network.read_timeout)
125                .map_err(|e| {
126                    crate::network::map_network_error(e, crate::network::sanitize_url(url))
127                })?
128        };
129        Ok(Self(format_ctx))
130    }
131}
132
133impl Drop for AvFormatContextGuard {
134    fn drop(&mut self) {
135        if !self.0.is_null() {
136            // SAFETY: self.0 is valid and owned by this guard
137            unsafe {
138                ff_sys::avformat::close_input(&mut (self.0 as *mut _));
139            }
140        }
141    }
142}
143
144/// RAII guard for `AVCodecContext` to ensure proper cleanup.
145struct AvCodecContextGuard(*mut AVCodecContext);
146
147impl AvCodecContextGuard {
148    /// Creates a new guard by allocating a codec context.
149    ///
150    /// # Safety
151    ///
152    /// Caller must ensure codec pointer is valid.
153    unsafe fn new(codec: *const ff_sys::AVCodec) -> Result<Self, DecodeError> {
154        // SAFETY: Caller ensures codec pointer is valid
155        let codec_ctx = unsafe {
156            ff_sys::avcodec::alloc_context3(codec).map_err(|e| DecodeError::Ffmpeg {
157                code: e,
158                message: format!("Failed to allocate codec context: {e}"),
159            })?
160        };
161        Ok(Self(codec_ctx))
162    }
163
164    /// Returns the raw pointer.
165    const fn as_ptr(&self) -> *mut AVCodecContext {
166        self.0
167    }
168
169    /// Consumes the guard and returns the raw pointer without dropping.
170    fn into_raw(self) -> *mut AVCodecContext {
171        let ptr = self.0;
172        std::mem::forget(self);
173        ptr
174    }
175}
176
177impl Drop for AvCodecContextGuard {
178    fn drop(&mut self) {
179        if !self.0.is_null() {
180            // SAFETY: self.0 is valid and owned by this guard
181            unsafe {
182                ff_sys::avcodec::free_context(&mut (self.0 as *mut _));
183            }
184        }
185    }
186}
187
188/// RAII guard for `AVPacket` to ensure proper cleanup.
189struct AvPacketGuard(*mut AVPacket);
190
191impl AvPacketGuard {
192    /// Creates a new guard by allocating a packet.
193    ///
194    /// # Safety
195    ///
196    /// Must be called after FFmpeg initialization.
197    unsafe fn new() -> Result<Self, DecodeError> {
198        // SAFETY: Caller ensures FFmpeg is initialized
199        let packet = unsafe { ff_sys::av_packet_alloc() };
200        if packet.is_null() {
201            return Err(DecodeError::Ffmpeg {
202                code: 0,
203                message: "Failed to allocate packet".to_string(),
204            });
205        }
206        Ok(Self(packet))
207    }
208
209    /// Consumes the guard and returns the raw pointer without dropping.
210    fn into_raw(self) -> *mut AVPacket {
211        let ptr = self.0;
212        std::mem::forget(self);
213        ptr
214    }
215}
216
217impl Drop for AvPacketGuard {
218    fn drop(&mut self) {
219        if !self.0.is_null() {
220            // SAFETY: self.0 is valid and owned by this guard
221            unsafe {
222                ff_sys::av_packet_free(&mut (self.0 as *mut _));
223            }
224        }
225    }
226}
227
228/// RAII guard for `AVFrame` to ensure proper cleanup.
229struct AvFrameGuard(*mut AVFrame);
230
231impl AvFrameGuard {
232    /// Creates a new guard by allocating a frame.
233    ///
234    /// # Safety
235    ///
236    /// Must be called after FFmpeg initialization.
237    unsafe fn new() -> Result<Self, DecodeError> {
238        // SAFETY: Caller ensures FFmpeg is initialized
239        let frame = unsafe { ff_sys::av_frame_alloc() };
240        if frame.is_null() {
241            return Err(DecodeError::Ffmpeg {
242                code: 0,
243                message: "Failed to allocate frame".to_string(),
244            });
245        }
246        Ok(Self(frame))
247    }
248
249    /// Returns the raw pointer.
250    const fn as_ptr(&self) -> *mut AVFrame {
251        self.0
252    }
253
254    /// Consumes the guard and returns the raw pointer without dropping.
255    fn into_raw(self) -> *mut AVFrame {
256        let ptr = self.0;
257        std::mem::forget(self);
258        ptr
259    }
260}
261
262impl Drop for AvFrameGuard {
263    fn drop(&mut self) {
264        if !self.0.is_null() {
265            // SAFETY: self.0 is valid and owned by this guard
266            unsafe {
267                ff_sys::av_frame_free(&mut (self.0 as *mut _));
268            }
269        }
270    }
271}
272
273/// Internal decoder state holding FFmpeg contexts.
274///
275/// This structure manages the lifecycle of FFmpeg objects and is responsible
276/// for proper cleanup when dropped.
277pub(crate) struct VideoDecoderInner {
278    /// Format context for reading the media file
279    pub(super) format_ctx: *mut AVFormatContext,
280    /// Codec context for decoding video frames
281    pub(super) codec_ctx: *mut AVCodecContext,
282    /// Video stream index in the format context
283    pub(super) stream_index: i32,
284    /// SwScale context for pixel format conversion and/or scaling (optional)
285    pub(super) sws_ctx: Option<*mut SwsContext>,
286    /// Cache key for the main sws_ctx: (src_w, src_h, src_fmt, dst_w, dst_h, dst_fmt)
287    pub(super) sws_cache_key: Option<(u32, u32, i32, u32, u32, i32)>,
288    /// Target output pixel format (if conversion is needed)
289    pub(super) output_format: Option<PixelFormat>,
290    /// Requested output scale (if resizing is needed)
291    pub(super) output_scale: Option<OutputScale>,
292    /// Whether the source is a live/streaming input (seeking is not supported)
293    pub(super) is_live: bool,
294    /// Whether end of file has been reached
295    pub(super) eof: bool,
296    /// Current playback position
297    pub(super) position: Duration,
298    /// Reusable packet for reading from file
299    pub(super) packet: *mut AVPacket,
300    /// Reusable frame for decoding
301    pub(super) frame: *mut AVFrame,
302    /// Cached SwScale context for thumbnail generation
303    pub(super) thumbnail_sws_ctx: Option<*mut SwsContext>,
304    /// Last thumbnail dimensions (for cache invalidation)
305    pub(super) thumbnail_cache_key: Option<(u32, u32, u32, u32, AVPixelFormat)>,
306    /// Hardware device context (if hardware acceleration is active)
307    pub(super) hw_device_ctx: Option<*mut AVBufferRef>,
308    /// Active hardware acceleration mode
309    pub(super) active_hw_accel: HardwareAccel,
310    /// Optional frame pool for memory reuse
311    pub(super) frame_pool: Option<Arc<dyn FramePool>>,
312    /// URL used to open this source — `None` for file-path and image-sequence sources.
313    pub(super) url: Option<String>,
314    /// Network options used for the initial open (timeouts, reconnect config).
315    pub(super) network_opts: NetworkOptions,
316    /// Number of successful reconnects so far (for logging).
317    pub(super) reconnect_count: u32,
318    /// Number of consecutive `AVERROR_INVALIDDATA` packets skipped without a successful frame.
319    /// Reset to 0 on each successfully decoded frame.
320    pub(super) consecutive_invalid: u32,
321}
322
323impl VideoDecoderInner {
324    /// Opens a media file and initializes the decoder.
325    ///
326    /// # Arguments
327    ///
328    /// * `path` - Path to the media file
329    /// * `output_format` - Optional target pixel format for conversion
330    /// * `hardware_accel` - Hardware acceleration mode
331    /// * `thread_count` - Number of decoding threads (0 = auto)
332    ///
333    /// # Errors
334    ///
335    /// Returns an error if:
336    /// - The file cannot be opened
337    /// - No video stream is found
338    /// - The codec is not supported
339    /// - Decoder initialization fails
340    #[allow(clippy::too_many_arguments)]
341    pub(crate) fn new(
342        path: &Path,
343        output_format: Option<PixelFormat>,
344        output_scale: Option<OutputScale>,
345        hardware_accel: HardwareAccel,
346        thread_count: usize,
347        frame_rate: Option<u32>,
348        frame_pool: Option<Arc<dyn FramePool>>,
349        network_opts: Option<NetworkOptions>,
350    ) -> Result<(Self, VideoStreamInfo, ContainerInfo), DecodeError> {
351        // Ensure FFmpeg is initialized (thread-safe and idempotent)
352        ff_sys::ensure_initialized();
353
354        let path_str = path.to_str().unwrap_or("");
355        let is_image_sequence = path_str.contains('%');
356        let is_network_url = crate::network::is_url(path_str);
357
358        let url = if is_network_url {
359            Some(path_str.to_owned())
360        } else {
361            None
362        };
363        let stored_network_opts = network_opts.clone().unwrap_or_default();
364
365        // Verify SRT availability before attempting to open (feature + runtime check).
366        if is_network_url {
367            crate::network::check_srt_url(path_str)?;
368        }
369
370        // Open the input (with RAII guard for cleanup on error).
371        // SAFETY: Path/URL is valid; AvFormatContextGuard ensures cleanup.
372        let format_ctx_guard = unsafe {
373            if is_network_url {
374                let network = network_opts.unwrap_or_default();
375                log::info!(
376                    "opening network source url={} connect_timeout_ms={} read_timeout_ms={}",
377                    crate::network::sanitize_url(path_str),
378                    network.connect_timeout.as_millis(),
379                    network.read_timeout.as_millis(),
380                );
381                AvFormatContextGuard::new_url(path_str, &network)?
382            } else if is_image_sequence {
383                let fps = frame_rate.unwrap_or(25);
384                AvFormatContextGuard::new_image_sequence(path, fps)?
385            } else {
386                AvFormatContextGuard::new(path)?
387            }
388        };
389        let format_ctx = format_ctx_guard.as_ptr();
390
391        // Read stream information
392        // SAFETY: format_ctx is valid and owned by guard
393        unsafe {
394            ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
395                code: e,
396                message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
397            })?;
398        }
399
400        // Detect live/streaming source via the AVFMT_TS_DISCONT flag on AVInputFormat.
401        // SAFETY: format_ctx is valid and non-null; iformat is set by avformat_open_input
402        //         and is non-null for all successfully opened formats.
403        let is_live = unsafe {
404            let iformat = (*format_ctx).iformat;
405            !iformat.is_null() && ((*iformat).flags & ff_sys::AVFMT_TS_DISCONT) != 0
406        };
407
408        // Find the video stream
409        // SAFETY: format_ctx is valid
410        let (stream_index, codec_id) =
411            unsafe { Self::find_video_stream(format_ctx) }.ok_or_else(|| {
412                DecodeError::NoVideoStream {
413                    path: path.to_path_buf(),
414                }
415            })?;
416
417        // Find the decoder for this codec
418        // SAFETY: codec_id is valid from FFmpeg
419        let codec_name = unsafe { Self::extract_codec_name(codec_id) };
420        let codec = unsafe {
421            ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
422                // Distinguish between a totally unknown codec ID and a known codec
423                // whose decoder was not compiled into this FFmpeg build.
424                if codec_id == ff_sys::AVCodecID_AV_CODEC_ID_EXR {
425                    DecodeError::DecoderUnavailable {
426                        codec: "exr".to_string(),
427                        hint: "Requires FFmpeg built with EXR support \
428                               (--enable-decoder=exr)"
429                            .to_string(),
430                    }
431                } else {
432                    DecodeError::UnsupportedCodec {
433                        codec: format!("{codec_name} (codec_id={codec_id:?})"),
434                    }
435                }
436            })?
437        };
438
439        // Allocate codec context (with RAII guard)
440        // SAFETY: codec pointer is valid, AvCodecContextGuard ensures cleanup
441        let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
442        let codec_ctx = codec_ctx_guard.as_ptr();
443
444        // Copy codec parameters from stream to context
445        // SAFETY: format_ctx and codec_ctx are valid, stream_index is valid
446        unsafe {
447            let stream = (*format_ctx).streams.add(stream_index as usize);
448            let codecpar = (*(*stream)).codecpar;
449            ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
450                DecodeError::Ffmpeg {
451                    code: e,
452                    message: format!(
453                        "Failed to copy codec parameters: {}",
454                        ff_sys::av_error_string(e)
455                    ),
456                }
457            })?;
458
459            // Set thread count
460            if thread_count > 0 {
461                (*codec_ctx).thread_count = thread_count as i32;
462            }
463        }
464
465        // Initialize hardware acceleration if requested
466        // SAFETY: codec_ctx is valid and not yet opened
467        let (hw_device_ctx, active_hw_accel) =
468            unsafe { Self::init_hardware_accel(codec_ctx, hardware_accel)? };
469
470        // Open the codec
471        // SAFETY: codec_ctx and codec are valid, hardware device context is set if requested
472        unsafe {
473            ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
474                // If codec opening failed, we still own our reference to hw_device_ctx
475                // but it will be cleaned up when codec_ctx is freed (which happens
476                // when codec_ctx_guard is dropped)
477                // Our reference in hw_device_ctx will be cleaned up here
478                if let Some(hw_ctx) = hw_device_ctx {
479                    ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
480                }
481                DecodeError::Ffmpeg {
482                    code: e,
483                    message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
484                }
485            })?;
486        }
487
488        // Extract stream information
489        // SAFETY: All pointers are valid
490        let stream_info =
491            unsafe { Self::extract_stream_info(format_ctx, stream_index as i32, codec_ctx)? };
492
493        // Extract container information
494        // SAFETY: format_ctx is valid and avformat_find_stream_info has been called
495        let container_info = unsafe { Self::extract_container_info(format_ctx) };
496
497        // Allocate packet and frame (with RAII guards)
498        // SAFETY: FFmpeg is initialized, guards ensure cleanup
499        let packet_guard = unsafe { AvPacketGuard::new()? };
500        let frame_guard = unsafe { AvFrameGuard::new()? };
501
502        // All initialization successful - transfer ownership to VideoDecoderInner
503        Ok((
504            Self {
505                format_ctx: format_ctx_guard.into_raw(),
506                codec_ctx: codec_ctx_guard.into_raw(),
507                stream_index: stream_index as i32,
508                sws_ctx: None,
509                sws_cache_key: None,
510                output_format,
511                output_scale,
512                is_live,
513                eof: false,
514                position: Duration::ZERO,
515                packet: packet_guard.into_raw(),
516                frame: frame_guard.into_raw(),
517                thumbnail_sws_ctx: None,
518                thumbnail_cache_key: None,
519                hw_device_ctx,
520                active_hw_accel,
521                frame_pool,
522                url,
523                network_opts: stored_network_opts,
524                reconnect_count: 0,
525                consecutive_invalid: 0,
526            },
527            stream_info,
528            container_info,
529        ))
530    }
531}
532
533impl Drop for VideoDecoderInner {
534    fn drop(&mut self) {
535        // Free SwScale context if allocated
536        if let Some(sws_ctx) = self.sws_ctx {
537            // SAFETY: sws_ctx is valid and owned by this instance
538            unsafe {
539                ff_sys::swscale::free_context(sws_ctx);
540            }
541        }
542
543        // Free cached thumbnail SwScale context if allocated
544        if let Some(thumbnail_ctx) = self.thumbnail_sws_ctx {
545            // SAFETY: thumbnail_ctx is valid and owned by this instance
546            unsafe {
547                ff_sys::swscale::free_context(thumbnail_ctx);
548            }
549        }
550
551        // Free hardware device context if allocated
552        if let Some(hw_ctx) = self.hw_device_ctx {
553            // SAFETY: hw_ctx is valid and owned by this instance
554            unsafe {
555                ff_sys::av_buffer_unref(&mut (hw_ctx as *mut _));
556            }
557        }
558
559        // Free frame and packet
560        if !self.frame.is_null() {
561            // SAFETY: self.frame is valid and owned by this instance
562            unsafe {
563                ff_sys::av_frame_free(&mut (self.frame as *mut _));
564            }
565        }
566
567        if !self.packet.is_null() {
568            // SAFETY: self.packet is valid and owned by this instance
569            unsafe {
570                ff_sys::av_packet_free(&mut (self.packet as *mut _));
571            }
572        }
573
574        // Free codec context
575        if !self.codec_ctx.is_null() {
576            // SAFETY: self.codec_ctx is valid and owned by this instance
577            unsafe {
578                ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
579            }
580        }
581
582        // Close format context
583        if !self.format_ctx.is_null() {
584            // SAFETY: self.format_ctx is valid and owned by this instance
585            unsafe {
586                ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
587            }
588        }
589    }
590}
591
592// SAFETY: VideoDecoderInner manages FFmpeg contexts which are thread-safe when not shared.
593// We don't expose mutable access across threads, so Send is safe.
594unsafe impl Send for VideoDecoderInner {}
595
596#[cfg(test)]
597mod tests {
598    use ff_format::PixelFormat;
599    use ff_format::codec::VideoCodec;
600    use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
601
602    use crate::HardwareAccel;
603
604    use super::VideoDecoderInner;
605
606    // -------------------------------------------------------------------------
607    // convert_pixel_format
608    // -------------------------------------------------------------------------
609
610    #[test]
611    fn pixel_format_yuv420p() {
612        assert_eq!(
613            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
614            PixelFormat::Yuv420p
615        );
616    }
617
618    #[test]
619    fn pixel_format_yuv422p() {
620        assert_eq!(
621            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P),
622            PixelFormat::Yuv422p
623        );
624    }
625
626    #[test]
627    fn pixel_format_yuv444p() {
628        assert_eq!(
629            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P),
630            PixelFormat::Yuv444p
631        );
632    }
633
634    #[test]
635    fn pixel_format_rgb24() {
636        assert_eq!(
637            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
638            PixelFormat::Rgb24
639        );
640    }
641
642    #[test]
643    fn pixel_format_bgr24() {
644        assert_eq!(
645            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24),
646            PixelFormat::Bgr24
647        );
648    }
649
650    #[test]
651    fn pixel_format_rgba() {
652        assert_eq!(
653            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
654            PixelFormat::Rgba
655        );
656    }
657
658    #[test]
659    fn pixel_format_bgra() {
660        assert_eq!(
661            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA),
662            PixelFormat::Bgra
663        );
664    }
665
666    #[test]
667    fn pixel_format_gray8() {
668        assert_eq!(
669            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
670            PixelFormat::Gray8
671        );
672    }
673
674    #[test]
675    fn pixel_format_nv12() {
676        assert_eq!(
677            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV12),
678            PixelFormat::Nv12
679        );
680    }
681
682    #[test]
683    fn pixel_format_nv21() {
684        assert_eq!(
685            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV21),
686            PixelFormat::Nv21
687        );
688    }
689
690    #[test]
691    fn pixel_format_yuv420p10le_should_return_yuv420p10le() {
692        assert_eq!(
693            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE),
694            PixelFormat::Yuv420p10le
695        );
696    }
697
698    #[test]
699    fn pixel_format_yuv422p10le_should_return_yuv422p10le() {
700        assert_eq!(
701            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P10LE),
702            PixelFormat::Yuv422p10le
703        );
704    }
705
706    #[test]
707    fn pixel_format_yuv444p10le_should_return_yuv444p10le() {
708        assert_eq!(
709            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P10LE),
710            PixelFormat::Yuv444p10le
711        );
712    }
713
714    #[test]
715    fn pixel_format_p010le_should_return_p010le() {
716        assert_eq!(
717            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_P010LE),
718            PixelFormat::P010le
719        );
720    }
721
722    #[test]
723    fn pixel_format_unknown_falls_back_to_yuv420p() {
724        assert_eq!(
725            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NONE),
726            PixelFormat::Yuv420p
727        );
728    }
729
730    // -------------------------------------------------------------------------
731    // convert_color_space
732    // -------------------------------------------------------------------------
733
734    #[test]
735    fn color_space_bt709() {
736        assert_eq!(
737            VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT709),
738            ColorSpace::Bt709
739        );
740    }
741
742    #[test]
743    fn color_space_bt470bg_yields_bt601() {
744        assert_eq!(
745            VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT470BG),
746            ColorSpace::Bt601
747        );
748    }
749
750    #[test]
751    fn color_space_smpte170m_yields_bt601() {
752        assert_eq!(
753            VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M),
754            ColorSpace::Bt601
755        );
756    }
757
758    #[test]
759    fn color_space_bt2020_ncl() {
760        assert_eq!(
761            VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL),
762            ColorSpace::Bt2020
763        );
764    }
765
766    #[test]
767    fn color_space_unknown_falls_back_to_bt709() {
768        assert_eq!(
769            VideoDecoderInner::convert_color_space(ff_sys::AVColorSpace_AVCOL_SPC_UNSPECIFIED),
770            ColorSpace::Bt709
771        );
772    }
773
774    // -------------------------------------------------------------------------
775    // convert_color_range
776    // -------------------------------------------------------------------------
777
778    #[test]
779    fn color_range_jpeg_yields_full() {
780        assert_eq!(
781            VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_JPEG),
782            ColorRange::Full
783        );
784    }
785
786    #[test]
787    fn color_range_mpeg_yields_limited() {
788        assert_eq!(
789            VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_MPEG),
790            ColorRange::Limited
791        );
792    }
793
794    #[test]
795    fn color_range_unknown_falls_back_to_limited() {
796        assert_eq!(
797            VideoDecoderInner::convert_color_range(ff_sys::AVColorRange_AVCOL_RANGE_UNSPECIFIED),
798            ColorRange::Limited
799        );
800    }
801
802    // -------------------------------------------------------------------------
803    // convert_color_primaries
804    // -------------------------------------------------------------------------
805
806    #[test]
807    fn color_primaries_bt709() {
808        assert_eq!(
809            VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT709),
810            ColorPrimaries::Bt709
811        );
812    }
813
814    #[test]
815    fn color_primaries_bt470bg_yields_bt601() {
816        assert_eq!(
817            VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG),
818            ColorPrimaries::Bt601
819        );
820    }
821
822    #[test]
823    fn color_primaries_smpte170m_yields_bt601() {
824        assert_eq!(
825            VideoDecoderInner::convert_color_primaries(
826                ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M
827            ),
828            ColorPrimaries::Bt601
829        );
830    }
831
832    #[test]
833    fn color_primaries_bt2020() {
834        assert_eq!(
835            VideoDecoderInner::convert_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020),
836            ColorPrimaries::Bt2020
837        );
838    }
839
840    #[test]
841    fn color_primaries_unknown_falls_back_to_bt709() {
842        assert_eq!(
843            VideoDecoderInner::convert_color_primaries(
844                ff_sys::AVColorPrimaries_AVCOL_PRI_UNSPECIFIED
845            ),
846            ColorPrimaries::Bt709
847        );
848    }
849
850    // -------------------------------------------------------------------------
851    // convert_codec
852    // -------------------------------------------------------------------------
853
854    #[test]
855    fn codec_h264() {
856        assert_eq!(
857            VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_H264),
858            VideoCodec::H264
859        );
860    }
861
862    #[test]
863    fn codec_hevc_yields_h265() {
864        assert_eq!(
865            VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_HEVC),
866            VideoCodec::H265
867        );
868    }
869
870    #[test]
871    fn codec_vp8() {
872        assert_eq!(
873            VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP8),
874            VideoCodec::Vp8
875        );
876    }
877
878    #[test]
879    fn codec_vp9() {
880        assert_eq!(
881            VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP9),
882            VideoCodec::Vp9
883        );
884    }
885
886    #[test]
887    fn codec_av1() {
888        assert_eq!(
889            VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_AV1),
890            VideoCodec::Av1
891        );
892    }
893
894    #[test]
895    fn codec_mpeg4() {
896        assert_eq!(
897            VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_MPEG4),
898            VideoCodec::Mpeg4
899        );
900    }
901
902    #[test]
903    fn codec_prores() {
904        assert_eq!(
905            VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_PRORES),
906            VideoCodec::ProRes
907        );
908    }
909
910    #[test]
911    fn codec_unknown_falls_back_to_h264() {
912        assert_eq!(
913            VideoDecoderInner::convert_codec(ff_sys::AVCodecID_AV_CODEC_ID_NONE),
914            VideoCodec::H264
915        );
916    }
917
918    // -------------------------------------------------------------------------
919    // hw_accel_to_device_type
920    // -------------------------------------------------------------------------
921
922    #[test]
923    fn hw_accel_auto_yields_none() {
924        assert_eq!(
925            VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Auto),
926            None
927        );
928    }
929
930    #[test]
931    fn hw_accel_none_yields_none() {
932        assert_eq!(
933            VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::None),
934            None
935        );
936    }
937
938    #[test]
939    fn hw_accel_nvdec_yields_cuda() {
940        assert_eq!(
941            VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Nvdec),
942            Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_CUDA)
943        );
944    }
945
946    #[test]
947    fn hw_accel_qsv_yields_qsv() {
948        assert_eq!(
949            VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Qsv),
950            Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_QSV)
951        );
952    }
953
954    #[test]
955    fn hw_accel_amf_yields_d3d11va() {
956        assert_eq!(
957            VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Amf),
958            Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_D3D11VA)
959        );
960    }
961
962    #[test]
963    fn hw_accel_videotoolbox() {
964        assert_eq!(
965            VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::VideoToolbox),
966            Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VIDEOTOOLBOX)
967        );
968    }
969
970    #[test]
971    fn hw_accel_vaapi() {
972        assert_eq!(
973            VideoDecoderInner::hw_accel_to_device_type(HardwareAccel::Vaapi),
974            Some(ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VAAPI)
975        );
976    }
977
978    // -------------------------------------------------------------------------
979    // pixel_format_to_av — round-trip
980    // -------------------------------------------------------------------------
981
982    #[test]
983    fn pixel_format_to_av_round_trip_yuv420p() {
984        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv420p);
985        assert_eq!(
986            VideoDecoderInner::convert_pixel_format(av),
987            PixelFormat::Yuv420p
988        );
989    }
990
991    #[test]
992    fn pixel_format_to_av_round_trip_yuv422p() {
993        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv422p);
994        assert_eq!(
995            VideoDecoderInner::convert_pixel_format(av),
996            PixelFormat::Yuv422p
997        );
998    }
999
1000    #[test]
1001    fn pixel_format_to_av_round_trip_yuv444p() {
1002        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Yuv444p);
1003        assert_eq!(
1004            VideoDecoderInner::convert_pixel_format(av),
1005            PixelFormat::Yuv444p
1006        );
1007    }
1008
1009    #[test]
1010    fn pixel_format_to_av_round_trip_rgb24() {
1011        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgb24);
1012        assert_eq!(
1013            VideoDecoderInner::convert_pixel_format(av),
1014            PixelFormat::Rgb24
1015        );
1016    }
1017
1018    #[test]
1019    fn pixel_format_to_av_round_trip_bgr24() {
1020        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgr24);
1021        assert_eq!(
1022            VideoDecoderInner::convert_pixel_format(av),
1023            PixelFormat::Bgr24
1024        );
1025    }
1026
1027    #[test]
1028    fn pixel_format_to_av_round_trip_rgba() {
1029        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Rgba);
1030        assert_eq!(
1031            VideoDecoderInner::convert_pixel_format(av),
1032            PixelFormat::Rgba
1033        );
1034    }
1035
1036    #[test]
1037    fn pixel_format_to_av_round_trip_bgra() {
1038        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Bgra);
1039        assert_eq!(
1040            VideoDecoderInner::convert_pixel_format(av),
1041            PixelFormat::Bgra
1042        );
1043    }
1044
1045    #[test]
1046    fn pixel_format_to_av_round_trip_gray8() {
1047        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Gray8);
1048        assert_eq!(
1049            VideoDecoderInner::convert_pixel_format(av),
1050            PixelFormat::Gray8
1051        );
1052    }
1053
1054    #[test]
1055    fn pixel_format_to_av_round_trip_nv12() {
1056        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv12);
1057        assert_eq!(
1058            VideoDecoderInner::convert_pixel_format(av),
1059            PixelFormat::Nv12
1060        );
1061    }
1062
1063    #[test]
1064    fn pixel_format_to_av_round_trip_nv21() {
1065        let av = VideoDecoderInner::pixel_format_to_av(PixelFormat::Nv21);
1066        assert_eq!(
1067            VideoDecoderInner::convert_pixel_format(av),
1068            PixelFormat::Nv21
1069        );
1070    }
1071
1072    #[test]
1073    fn pixel_format_to_av_unknown_falls_back_to_yuv420p_av() {
1074        // Other(999) has no explicit mapping and hits the _ fallback arm.
1075        assert_eq!(
1076            VideoDecoderInner::pixel_format_to_av(PixelFormat::Other(999)),
1077            ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
1078        );
1079    }
1080
1081    // -------------------------------------------------------------------------
1082    // extract_codec_name
1083    // -------------------------------------------------------------------------
1084
1085    #[test]
1086    fn codec_name_should_return_h264_for_h264_codec_id() {
1087        let name =
1088            unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_H264) };
1089        assert_eq!(name, "h264");
1090    }
1091
1092    #[test]
1093    fn codec_name_should_return_none_for_none_codec_id() {
1094        let name =
1095            unsafe { VideoDecoderInner::extract_codec_name(ff_sys::AVCodecID_AV_CODEC_ID_NONE) };
1096        assert_eq!(name, "none");
1097    }
1098
1099    #[test]
1100    fn convert_pixel_format_should_map_gbrpf32le() {
1101        assert_eq!(
1102            VideoDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GBRPF32LE),
1103            PixelFormat::Gbrpf32le
1104        );
1105    }
1106
1107    #[test]
1108    fn unsupported_codec_error_should_include_codec_name() {
1109        let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_H264;
1110        let codec_name = unsafe { VideoDecoderInner::extract_codec_name(codec_id) };
1111        let error = crate::error::DecodeError::UnsupportedCodec {
1112            codec: format!("{codec_name} (codec_id={codec_id:?})"),
1113        };
1114        let msg = error.to_string();
1115        assert!(msg.contains("h264"), "expected codec name in error: {msg}");
1116        assert!(
1117            msg.contains("codec_id="),
1118            "expected codec_id in error: {msg}"
1119        );
1120    }
1121}