1#![allow(unsafe_code)]
8#![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
35pub(crate) struct ImageDecoderInner {
41 format_ctx: *mut AVFormatContext,
43 codec_ctx: *mut AVCodecContext,
45 stream_index: usize,
47 packet: *mut AVPacket,
49 frame: *mut AVFrame,
51}
52
53unsafe impl Send for ImageDecoderInner {}
57
58impl ImageDecoderInner {
59 pub(crate) fn new(path: &Path) -> Result<Self, DecodeError> {
70 ff_sys::ensure_initialized();
71
72 let format_ctx_guard = unsafe { AvFormatContextGuard::new(path)? };
75 let format_ctx = format_ctx_guard.as_ptr();
76
77 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 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 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 let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
117 let codec_ctx = codec_ctx_guard.as_ptr();
118
119 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 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 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 pub(crate) fn width(&self) -> u32 {
175 unsafe { (*self.codec_ctx).width as u32 }
177 }
178
179 pub(crate) fn height(&self) -> u32 {
181 unsafe { (*self.codec_ctx).height as u32 }
183 }
184
185 pub(crate) fn decode(self) -> Result<VideoFrame, DecodeError> {
193 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 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 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 let video_frame = unsafe { self.av_frame_to_video_frame(self.frame)? };
233 Ok(video_frame)
234 }
235
236 unsafe fn find_video_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
242 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 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 unsafe fn av_frame_to_video_frame(
299 &self,
300 frame: *const AVFrame,
301 ) -> Result<VideoFrame, DecodeError> {
302 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 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 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 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 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 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 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 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 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 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 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 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}