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};
34use crate::shared::plane_inner::plane_row_ptr;
35
36pub(crate) struct ImageDecoderInner {
42 format_ctx: *mut AVFormatContext,
44 codec_ctx: *mut AVCodecContext,
46 stream_index: usize,
48 packet: *mut AVPacket,
50 frame: *mut AVFrame,
52}
53
54unsafe impl Send for ImageDecoderInner {}
58
59impl ImageDecoderInner {
60 pub(crate) fn new(path: &Path) -> Result<Self, DecodeError> {
71 ff_sys::ensure_initialized();
72
73 let format_ctx_guard = unsafe { AvFormatContextGuard::new(path)? };
76 let format_ctx = format_ctx_guard.as_ptr();
77
78 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 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 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 let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
118 let codec_ctx = codec_ctx_guard.as_ptr();
119
120 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 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 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 pub(crate) fn width(&self) -> u32 {
176 unsafe { (*self.codec_ctx).width as u32 }
178 }
179
180 pub(crate) fn height(&self) -> u32 {
182 unsafe { (*self.codec_ctx).height as u32 }
184 }
185
186 pub(crate) fn decode(self) -> Result<VideoFrame, DecodeError> {
194 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 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 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 let video_frame = unsafe { self.av_frame_to_video_frame(self.frame)? };
234 Ok(video_frame)
235 }
236
237 unsafe fn find_video_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
243 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 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 unsafe fn av_frame_to_video_frame(
300 &self,
301 frame: *const AVFrame,
302 ) -> Result<VideoFrame, DecodeError> {
303 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 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 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 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 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 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 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 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 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 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 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 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}