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