Skip to main content

ff_decode/image/
decoder_inner.rs

1//! Internal image 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::ptr_as_ptr)]
16#![allow(clippy::doc_markdown)]
17#![allow(clippy::unnecessary_cast)]
18#![allow(clippy::cast_precision_loss)]
19#![allow(clippy::cast_lossless)]
20
21use std::ffi::CStr;
22use std::path::Path;
23use std::ptr;
24
25use ff_format::time::{Rational, Timestamp};
26use ff_format::{PixelFormat, PooledBuffer, VideoFrame};
27use ff_sys::{
28    AVCodecContext, AVCodecID, AVFormatContext, AVFrame, AVMediaType_AVMEDIA_TYPE_VIDEO, AVPacket,
29    AVPixelFormat,
30};
31
32use crate::error::DecodeError;
33use crate::shared::guards_inner::{AvCodecContextGuard, AvFormatContextGuard};
34use crate::shared::plane_inner::plane_row_ptr;
35
36// ── ImageDecoderInner ─────────────────────────────────────────────────────────
37
38/// Internal state for the image decoder.
39///
40/// Holds raw FFmpeg pointers and is responsible for proper cleanup in `Drop`.
41pub(crate) struct ImageDecoderInner {
42    /// Format context for reading the image file.
43    format_ctx: *mut AVFormatContext,
44    /// Codec context for decoding the image.
45    codec_ctx: *mut AVCodecContext,
46    /// Video stream index in the format context.
47    stream_index: usize,
48    /// Reusable packet for reading from file.
49    packet: *mut AVPacket,
50    /// Reusable frame for decoding.
51    frame: *mut AVFrame,
52}
53
54// SAFETY: `ImageDecoderInner` owns all FFmpeg contexts exclusively.
55//         FFmpeg contexts are not safe for concurrent access (not Sync),
56//         but ownership transfer between threads is safe.
57unsafe impl Send for ImageDecoderInner {}
58
59impl ImageDecoderInner {
60    /// Opens an image file and prepares the decoder.
61    ///
62    /// Performs the full FFmpeg initialization sequence:
63    /// 1. `avformat_open_input`
64    /// 2. `avformat_find_stream_info`
65    /// 3. `av_find_best_stream(AVMEDIA_TYPE_VIDEO)`
66    /// 4. `avcodec_find_decoder`
67    /// 5. `avcodec_alloc_context3`
68    /// 6. `avcodec_parameters_to_context`
69    /// 7. `avcodec_open2`
70    pub(crate) fn new(path: &Path) -> Result<Self, DecodeError> {
71        ff_sys::ensure_initialized();
72
73        // 1. avformat_open_input
74        // SAFETY: Path is valid; AvFormatContextGuard ensures cleanup on error.
75        let format_ctx_guard = unsafe { AvFormatContextGuard::new(path)? };
76        let format_ctx = format_ctx_guard.as_ptr();
77
78        // 2. avformat_find_stream_info
79        // SAFETY: format_ctx is valid and owned by the guard.
80        unsafe {
81            ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
82                code: e,
83                message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
84            })?;
85        }
86
87        // 3. Find the video stream.
88        // SAFETY: format_ctx is valid.
89        let (stream_index, codec_id) =
90            unsafe { Self::find_video_stream(format_ctx) }.ok_or_else(|| {
91                DecodeError::NoVideoStream {
92                    path: path.to_path_buf(),
93                }
94            })?;
95
96        // 4. avcodec_find_decoder
97        // SAFETY: codec_id comes from FFmpeg.
98        // SAFETY: avcodec_get_name is safe for any codec ID value and returns a static C string.
99        let codec_name = unsafe {
100            let name_ptr = ff_sys::avcodec_get_name(codec_id);
101            if name_ptr.is_null() {
102                String::from("unknown")
103            } else {
104                CStr::from_ptr(name_ptr).to_string_lossy().into_owned()
105            }
106        };
107        let codec = unsafe {
108            ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
109                DecodeError::UnsupportedCodec {
110                    codec: format!("{codec_name} (codec_id={codec_id:?})"),
111                }
112            })?
113        };
114
115        // 5. avcodec_alloc_context3
116        // SAFETY: codec pointer is valid; AvCodecContextGuard ensures cleanup.
117        let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
118        let codec_ctx = codec_ctx_guard.as_ptr();
119
120        // 6. avcodec_parameters_to_context
121        // SAFETY: All pointers are valid; stream_index was validated above.
122        unsafe {
123            let stream = (*format_ctx).streams.add(stream_index);
124            let codecpar = (*(*stream)).codecpar;
125            ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
126                DecodeError::Ffmpeg {
127                    code: e,
128                    message: format!(
129                        "Failed to copy codec parameters: {}",
130                        ff_sys::av_error_string(e)
131                    ),
132                }
133            })?;
134        }
135
136        // 7. avcodec_open2
137        // SAFETY: codec_ctx and codec are valid; no hardware acceleration for images.
138        unsafe {
139            ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
140                DecodeError::Ffmpeg {
141                    code: e,
142                    message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
143                }
144            })?;
145        }
146
147        // Allocate packet and frame.
148        // SAFETY: FFmpeg is initialized.
149        let packet = unsafe { ff_sys::av_packet_alloc() };
150        if packet.is_null() {
151            return Err(DecodeError::Ffmpeg {
152                code: 0,
153                message: "Failed to allocate packet".to_string(),
154            });
155        }
156        let frame = unsafe { ff_sys::av_frame_alloc() };
157        if frame.is_null() {
158            unsafe { ff_sys::av_packet_free(&mut (packet as *mut _)) };
159            return Err(DecodeError::Ffmpeg {
160                code: 0,
161                message: "Failed to allocate frame".to_string(),
162            });
163        }
164
165        Ok(Self {
166            format_ctx: format_ctx_guard.into_raw(),
167            codec_ctx: codec_ctx_guard.into_raw(),
168            stream_index,
169            packet,
170            frame,
171        })
172    }
173
174    /// Returns the image width in pixels.
175    pub(crate) fn width(&self) -> u32 {
176        // SAFETY: codec_ctx is valid for the lifetime of `self`.
177        unsafe { (*self.codec_ctx).width as u32 }
178    }
179
180    /// Returns the image height in pixels.
181    pub(crate) fn height(&self) -> u32 {
182        // SAFETY: codec_ctx is valid for the lifetime of `self`.
183        unsafe { (*self.codec_ctx).height as u32 }
184    }
185
186    /// Decodes the image, consuming `self` and returning a [`VideoFrame`].
187    ///
188    /// Follows the sequence:
189    /// 1. `av_read_frame`
190    /// 2. `avcodec_send_packet`
191    /// 3. `avcodec_receive_frame`
192    /// 4. Convert to [`VideoFrame`]
193    pub(crate) fn decode(self) -> Result<VideoFrame, DecodeError> {
194        // 1. av_read_frame
195        // SAFETY: format_ctx and packet are valid.
196        let ret = unsafe { ff_sys::av_read_frame(self.format_ctx, self.packet) };
197        if ret < 0 {
198            return Err(DecodeError::Ffmpeg {
199                code: ret,
200                message: format!("Failed to read frame: {}", ff_sys::av_error_string(ret)),
201            });
202        }
203
204        // 2. avcodec_send_packet
205        // SAFETY: codec_ctx and packet are valid; packet contains image data.
206        let ret = unsafe { ff_sys::avcodec_send_packet(self.codec_ctx, self.packet) };
207        unsafe { ff_sys::av_packet_unref(self.packet) };
208        if ret < 0 {
209            return Err(DecodeError::Ffmpeg {
210                code: ret,
211                message: format!(
212                    "Failed to send packet to decoder: {}",
213                    ff_sys::av_error_string(ret)
214                ),
215            });
216        }
217
218        // 3. avcodec_receive_frame
219        // SAFETY: codec_ctx and frame are valid.
220        let ret = unsafe { ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame) };
221        if ret < 0 {
222            return Err(DecodeError::Ffmpeg {
223                code: ret,
224                message: format!(
225                    "Failed to receive decoded frame: {}",
226                    ff_sys::av_error_string(ret)
227                ),
228            });
229        }
230
231        // 4. Convert to VideoFrame.
232        // SAFETY: frame is valid and contains decoded image data.
233        let video_frame = unsafe { self.av_frame_to_video_frame(self.frame)? };
234        Ok(video_frame)
235    }
236
237    /// Finds the first video stream in the format context.
238    ///
239    /// # Safety
240    ///
241    /// `format_ctx` must be a valid, fully initialized `AVFormatContext`.
242    unsafe fn find_video_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
243        // SAFETY: Caller ensures format_ctx is valid.
244        unsafe {
245            let nb_streams = (*format_ctx).nb_streams as usize;
246            for i in 0..nb_streams {
247                let stream = (*format_ctx).streams.add(i);
248                let codecpar = (*(*stream)).codecpar;
249                if (*codecpar).codec_type == AVMediaType_AVMEDIA_TYPE_VIDEO {
250                    return Some((i, (*codecpar).codec_id));
251                }
252            }
253        }
254        None
255    }
256
257    /// Maps an `AVPixelFormat` value to our [`PixelFormat`] enum.
258    ///
259    /// Image decoders commonly produce YUVJ formats (full-range YUV), which
260    /// have the same plane layout as the corresponding YUV formats but with a
261    /// different color range flag.  We map them to their YUV equivalents here
262    /// and rely on the colour-range metadata to distinguish them if needed.
263    fn convert_pixel_format(fmt: AVPixelFormat) -> PixelFormat {
264        if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
265            || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P
266        {
267            PixelFormat::Yuv420p
268        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P
269            || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ422P
270        {
271            PixelFormat::Yuv422p
272        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P
273            || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ444P
274        {
275            PixelFormat::Yuv444p
276        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24 {
277            PixelFormat::Rgb24
278        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24 {
279            PixelFormat::Bgr24
280        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA {
281            PixelFormat::Rgba
282        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA {
283            PixelFormat::Bgra
284        } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8 {
285            PixelFormat::Gray8
286        } else {
287            log::warn!(
288                "pixel_format unsupported, falling back to Rgb24 requested={fmt} fallback=Rgb24"
289            );
290            PixelFormat::Rgb24
291        }
292    }
293
294    /// Converts a decoded `AVFrame` to a [`VideoFrame`].
295    ///
296    /// # Safety
297    ///
298    /// `frame` must be a valid, fully decoded `AVFrame` owned by `self`.
299    unsafe fn av_frame_to_video_frame(
300        &self,
301        frame: *const AVFrame,
302    ) -> Result<VideoFrame, DecodeError> {
303        // SAFETY: Caller ensures frame is valid.
304        unsafe {
305            let width = (*frame).width as u32;
306            let height = (*frame).height as u32;
307            let format = Self::convert_pixel_format((*frame).format);
308
309            // Extract timestamp (images often have no meaningful PTS).
310            let pts = (*frame).pts;
311            let timestamp = if pts == ff_sys::AV_NOPTS_VALUE {
312                Timestamp::default()
313            } else {
314                let stream = (*self.format_ctx).streams.add(self.stream_index);
315                let time_base = (*(*stream)).time_base;
316                Timestamp::new(
317                    pts as i64,
318                    Rational::new(time_base.num as i32, time_base.den as i32),
319                )
320            };
321
322            let (planes, strides) = Self::extract_planes_and_strides(frame, width, height, format)?;
323
324            // Images are always key frames.
325            VideoFrame::new(planes, strides, width, height, format, timestamp, true).map_err(|e| {
326                DecodeError::Ffmpeg {
327                    code: 0,
328                    message: format!("Failed to create VideoFrame: {e}"),
329                }
330            })
331        }
332    }
333
334    /// Extracts pixel data from an `AVFrame` into [`PooledBuffer`] planes.
335    ///
336    /// Copies data row-by-row to strip any FFmpeg padding from line strides.
337    ///
338    /// # Safety
339    ///
340    /// `frame` must be a valid, fully decoded `AVFrame` with `format` matching
341    /// the actual pixel format of the frame.
342    unsafe fn extract_planes_and_strides(
343        frame: *const AVFrame,
344        width: u32,
345        height: u32,
346        format: PixelFormat,
347    ) -> Result<(Vec<PooledBuffer>, Vec<usize>), DecodeError> {
348        // SAFETY: Caller ensures frame is valid and format matches.
349        unsafe {
350            let w = width as usize;
351            let h = height as usize;
352            let mut planes: Vec<PooledBuffer> = Vec::new();
353            let mut strides: Vec<usize> = Vec::new();
354
355            match format {
356                PixelFormat::Rgba | PixelFormat::Bgra => {
357                    let bytes_per_pixel = 4_usize;
358                    let row_w = w * bytes_per_pixel;
359                    let mut buf = vec![0u8; row_w * h];
360                    let src = (*frame).data[0];
361                    if src.is_null() {
362                        return Err(DecodeError::Ffmpeg {
363                            code: 0,
364                            message: "Null plane data for packed format".to_string(),
365                        });
366                    }
367                    let src_linesize = (*frame).linesize[0];
368                    for row in 0..h {
369                        ptr::copy_nonoverlapping(
370                            plane_row_ptr(src, src_linesize, row),
371                            buf[row * row_w..].as_mut_ptr(),
372                            row_w,
373                        );
374                    }
375                    planes.push(PooledBuffer::standalone(buf));
376                    strides.push(row_w);
377                }
378                PixelFormat::Rgb24 | PixelFormat::Bgr24 => {
379                    let bytes_per_pixel = 3_usize;
380                    let row_w = w * bytes_per_pixel;
381                    let mut buf = vec![0u8; row_w * h];
382                    let src = (*frame).data[0];
383                    if src.is_null() {
384                        return Err(DecodeError::Ffmpeg {
385                            code: 0,
386                            message: "Null plane data for packed format".to_string(),
387                        });
388                    }
389                    let src_linesize = (*frame).linesize[0];
390                    for row in 0..h {
391                        ptr::copy_nonoverlapping(
392                            plane_row_ptr(src, src_linesize, row),
393                            buf[row * row_w..].as_mut_ptr(),
394                            row_w,
395                        );
396                    }
397                    planes.push(PooledBuffer::standalone(buf));
398                    strides.push(row_w);
399                }
400                PixelFormat::Gray8 => {
401                    let mut buf = vec![0u8; w * h];
402                    let src = (*frame).data[0];
403                    if src.is_null() {
404                        return Err(DecodeError::Ffmpeg {
405                            code: 0,
406                            message: "Null plane data for Gray8".to_string(),
407                        });
408                    }
409                    let src_linesize = (*frame).linesize[0];
410                    for row in 0..h {
411                        ptr::copy_nonoverlapping(
412                            plane_row_ptr(src, src_linesize, row),
413                            buf[row * w..].as_mut_ptr(),
414                            w,
415                        );
416                    }
417                    planes.push(PooledBuffer::standalone(buf));
418                    strides.push(w);
419                }
420                PixelFormat::Yuv420p | PixelFormat::Nv12 | PixelFormat::Nv21 => {
421                    // Y plane (full size).
422                    let mut y_buf = vec![0u8; w * h];
423                    let y_src = (*frame).data[0];
424                    if y_src.is_null() {
425                        return Err(DecodeError::Ffmpeg {
426                            code: 0,
427                            message: "Null Y plane".to_string(),
428                        });
429                    }
430                    let y_src_linesize = (*frame).linesize[0];
431                    for row in 0..h {
432                        ptr::copy_nonoverlapping(
433                            plane_row_ptr(y_src, y_src_linesize, row),
434                            y_buf[row * w..].as_mut_ptr(),
435                            w,
436                        );
437                    }
438                    planes.push(PooledBuffer::standalone(y_buf));
439                    strides.push(w);
440
441                    if matches!(format, PixelFormat::Nv12 | PixelFormat::Nv21) {
442                        // Interleaved UV plane (half height).
443                        let uv_h = h / 2;
444                        let mut uv_buf = vec![0u8; w * uv_h];
445                        let uv_src = (*frame).data[1];
446                        if !uv_src.is_null() {
447                            let uv_src_linesize = (*frame).linesize[1];
448                            for row in 0..uv_h {
449                                ptr::copy_nonoverlapping(
450                                    plane_row_ptr(uv_src, uv_src_linesize, row),
451                                    uv_buf[row * w..].as_mut_ptr(),
452                                    w,
453                                );
454                            }
455                        }
456                        planes.push(PooledBuffer::standalone(uv_buf));
457                        strides.push(w);
458                    } else {
459                        // YUV 4:2:0 — separate U and V planes (half width, half height).
460                        let uv_w = w / 2;
461                        let uv_h = h / 2;
462                        for plane_idx in 1..=2usize {
463                            let mut uv_buf = vec![0u8; uv_w * uv_h];
464                            let uv_src = (*frame).data[plane_idx];
465                            if !uv_src.is_null() {
466                                let uv_src_linesize = (*frame).linesize[plane_idx];
467                                for row in 0..uv_h {
468                                    ptr::copy_nonoverlapping(
469                                        plane_row_ptr(uv_src, uv_src_linesize, row),
470                                        uv_buf[row * uv_w..].as_mut_ptr(),
471                                        uv_w,
472                                    );
473                                }
474                            }
475                            planes.push(PooledBuffer::standalone(uv_buf));
476                            strides.push(uv_w);
477                        }
478                    }
479                }
480                PixelFormat::Yuv422p => {
481                    // Y plane (full size), U and V planes (half width, full height).
482                    let uv_w = w / 2;
483                    let plane_dims = [(w, h), (uv_w, h), (uv_w, h)];
484                    for (plane_idx, (pw, ph)) in plane_dims.iter().enumerate() {
485                        let mut buf = vec![0u8; pw * ph];
486                        let src = (*frame).data[plane_idx];
487                        if !src.is_null() {
488                            let src_linesize = (*frame).linesize[plane_idx];
489                            for row in 0..*ph {
490                                ptr::copy_nonoverlapping(
491                                    plane_row_ptr(src, src_linesize, row),
492                                    buf[row * pw..].as_mut_ptr(),
493                                    *pw,
494                                );
495                            }
496                        }
497                        planes.push(PooledBuffer::standalone(buf));
498                        strides.push(*pw);
499                    }
500                }
501                PixelFormat::Yuv444p => {
502                    // All three planes are full size.
503                    for plane_idx in 0..3usize {
504                        let mut buf = vec![0u8; w * h];
505                        let src = (*frame).data[plane_idx];
506                        if !src.is_null() {
507                            let src_linesize = (*frame).linesize[plane_idx];
508                            for row in 0..h {
509                                ptr::copy_nonoverlapping(
510                                    plane_row_ptr(src, src_linesize, row),
511                                    buf[row * w..].as_mut_ptr(),
512                                    w,
513                                );
514                            }
515                        }
516                        planes.push(PooledBuffer::standalone(buf));
517                        strides.push(w);
518                    }
519                }
520                _ => {
521                    return Err(DecodeError::Ffmpeg {
522                        code: 0,
523                        message: format!("Unsupported pixel format for image decoding: {format:?}"),
524                    });
525                }
526            }
527
528            Ok((planes, strides))
529        }
530    }
531}
532
533impl Drop for ImageDecoderInner {
534    fn drop(&mut self) {
535        // SAFETY: All pointers are exclusively owned by this struct and were
536        // allocated by the corresponding FFmpeg alloc functions.
537        unsafe {
538            if !self.frame.is_null() {
539                ff_sys::av_frame_free(&mut (self.frame as *mut _));
540            }
541            if !self.packet.is_null() {
542                ff_sys::av_packet_free(&mut (self.packet as *mut _));
543            }
544            if !self.codec_ctx.is_null() {
545                ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
546            }
547            if !self.format_ctx.is_null() {
548                ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
549            }
550        }
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn convert_pixel_format_yuv420p_should_map_to_yuv420p() {
560        assert_eq!(
561            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
562            PixelFormat::Yuv420p
563        );
564    }
565
566    #[test]
567    fn convert_pixel_format_yuvj420p_should_map_to_yuv420p() {
568        assert_eq!(
569            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P),
570            PixelFormat::Yuv420p
571        );
572    }
573
574    #[test]
575    fn convert_pixel_format_rgb24_should_map_to_rgb24() {
576        assert_eq!(
577            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
578            PixelFormat::Rgb24
579        );
580    }
581
582    #[test]
583    fn convert_pixel_format_rgba_should_map_to_rgba() {
584        assert_eq!(
585            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
586            PixelFormat::Rgba
587        );
588    }
589
590    #[test]
591    fn convert_pixel_format_gray8_should_map_to_gray8() {
592        assert_eq!(
593            ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
594            PixelFormat::Gray8
595        );
596    }
597
598    #[test]
599    fn unsupported_codec_error_should_include_codec_name() {
600        let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_PNG;
601        // SAFETY: avcodec_get_name is safe for any codec ID value and returns a static C string.
602        let codec_name = unsafe {
603            let name_ptr = ff_sys::avcodec_get_name(codec_id);
604            if name_ptr.is_null() {
605                String::from("unknown")
606            } else {
607                std::ffi::CStr::from_ptr(name_ptr)
608                    .to_string_lossy()
609                    .into_owned()
610            }
611        };
612        let error = crate::error::DecodeError::UnsupportedCodec {
613            codec: format!("{codec_name} (codec_id={codec_id:?})"),
614        };
615        let msg = error.to_string();
616        assert!(msg.contains("png"), "expected codec name in error: {msg}");
617        assert!(
618            msg.contains("codec_id="),
619            "expected codec_id in error: {msg}"
620        );
621    }
622}