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;
33
34struct AvFormatContextGuard(*mut AVFormatContext);
38
39impl AvFormatContextGuard {
40 unsafe fn new(path: &Path) -> Result<Self, DecodeError> {
41 let format_ctx = unsafe {
43 ff_sys::avformat::open_input(path).map_err(|e| DecodeError::Ffmpeg {
44 code: e,
45 message: format!("Failed to open file: {}", ff_sys::av_error_string(e)),
46 })?
47 };
48 Ok(Self(format_ctx))
49 }
50
51 const fn as_ptr(&self) -> *mut AVFormatContext {
52 self.0
53 }
54
55 fn into_raw(self) -> *mut AVFormatContext {
56 let ptr = self.0;
57 std::mem::forget(self);
58 ptr
59 }
60}
61
62impl Drop for AvFormatContextGuard {
63 fn drop(&mut self) {
64 if !self.0.is_null() {
65 unsafe {
67 ff_sys::avformat::close_input(&mut (self.0 as *mut _));
68 }
69 }
70 }
71}
72
73struct AvCodecContextGuard(*mut AVCodecContext);
75
76impl AvCodecContextGuard {
77 unsafe fn new(codec: *const ff_sys::AVCodec) -> Result<Self, DecodeError> {
78 let codec_ctx = unsafe {
80 ff_sys::avcodec::alloc_context3(codec).map_err(|e| DecodeError::Ffmpeg {
81 code: e,
82 message: format!("Failed to allocate codec context: {e}"),
83 })?
84 };
85 Ok(Self(codec_ctx))
86 }
87
88 const fn as_ptr(&self) -> *mut AVCodecContext {
89 self.0
90 }
91
92 fn into_raw(self) -> *mut AVCodecContext {
93 let ptr = self.0;
94 std::mem::forget(self);
95 ptr
96 }
97}
98
99impl Drop for AvCodecContextGuard {
100 fn drop(&mut self) {
101 if !self.0.is_null() {
102 unsafe {
104 ff_sys::avcodec::free_context(&mut (self.0 as *mut _));
105 }
106 }
107 }
108}
109
110pub(crate) struct ImageDecoderInner {
116 format_ctx: *mut AVFormatContext,
118 codec_ctx: *mut AVCodecContext,
120 stream_index: usize,
122 packet: *mut AVPacket,
124 frame: *mut AVFrame,
126}
127
128unsafe impl Send for ImageDecoderInner {}
132
133impl ImageDecoderInner {
134 pub(crate) fn new(path: &Path) -> Result<Self, DecodeError> {
145 ff_sys::ensure_initialized();
146
147 let format_ctx_guard = unsafe { AvFormatContextGuard::new(path)? };
150 let format_ctx = format_ctx_guard.as_ptr();
151
152 unsafe {
155 ff_sys::avformat::find_stream_info(format_ctx).map_err(|e| DecodeError::Ffmpeg {
156 code: e,
157 message: format!("Failed to find stream info: {}", ff_sys::av_error_string(e)),
158 })?;
159 }
160
161 let (stream_index, codec_id) =
164 unsafe { Self::find_video_stream(format_ctx) }.ok_or_else(|| {
165 DecodeError::NoVideoStream {
166 path: path.to_path_buf(),
167 }
168 })?;
169
170 let codec_name = unsafe {
174 let name_ptr = ff_sys::avcodec_get_name(codec_id);
175 if name_ptr.is_null() {
176 String::from("unknown")
177 } else {
178 CStr::from_ptr(name_ptr).to_string_lossy().into_owned()
179 }
180 };
181 let codec = unsafe {
182 ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
183 DecodeError::UnsupportedCodec {
184 codec: format!("{codec_name} (codec_id={codec_id:?})"),
185 }
186 })?
187 };
188
189 let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
192 let codec_ctx = codec_ctx_guard.as_ptr();
193
194 unsafe {
197 let stream = (*format_ctx).streams.add(stream_index);
198 let codecpar = (*(*stream)).codecpar;
199 ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
200 DecodeError::Ffmpeg {
201 code: e,
202 message: format!(
203 "Failed to copy codec parameters: {}",
204 ff_sys::av_error_string(e)
205 ),
206 }
207 })?;
208 }
209
210 unsafe {
213 ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
214 DecodeError::Ffmpeg {
215 code: e,
216 message: format!("Failed to open codec: {}", ff_sys::av_error_string(e)),
217 }
218 })?;
219 }
220
221 let packet = unsafe { ff_sys::av_packet_alloc() };
224 if packet.is_null() {
225 return Err(DecodeError::Ffmpeg {
226 code: 0,
227 message: "Failed to allocate packet".to_string(),
228 });
229 }
230 let frame = unsafe { ff_sys::av_frame_alloc() };
231 if frame.is_null() {
232 unsafe { ff_sys::av_packet_free(&mut (packet as *mut _)) };
233 return Err(DecodeError::Ffmpeg {
234 code: 0,
235 message: "Failed to allocate frame".to_string(),
236 });
237 }
238
239 Ok(Self {
240 format_ctx: format_ctx_guard.into_raw(),
241 codec_ctx: codec_ctx_guard.into_raw(),
242 stream_index,
243 packet,
244 frame,
245 })
246 }
247
248 pub(crate) fn width(&self) -> u32 {
250 unsafe { (*self.codec_ctx).width as u32 }
252 }
253
254 pub(crate) fn height(&self) -> u32 {
256 unsafe { (*self.codec_ctx).height as u32 }
258 }
259
260 pub(crate) fn decode(self) -> Result<VideoFrame, DecodeError> {
268 let ret = unsafe { ff_sys::av_read_frame(self.format_ctx, self.packet) };
271 if ret < 0 {
272 return Err(DecodeError::Ffmpeg {
273 code: ret,
274 message: format!("Failed to read frame: {}", ff_sys::av_error_string(ret)),
275 });
276 }
277
278 let ret = unsafe { ff_sys::avcodec_send_packet(self.codec_ctx, self.packet) };
281 unsafe { ff_sys::av_packet_unref(self.packet) };
282 if ret < 0 {
283 return Err(DecodeError::Ffmpeg {
284 code: ret,
285 message: format!(
286 "Failed to send packet to decoder: {}",
287 ff_sys::av_error_string(ret)
288 ),
289 });
290 }
291
292 let ret = unsafe { ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame) };
295 if ret < 0 {
296 return Err(DecodeError::Ffmpeg {
297 code: ret,
298 message: format!(
299 "Failed to receive decoded frame: {}",
300 ff_sys::av_error_string(ret)
301 ),
302 });
303 }
304
305 let video_frame = unsafe { self.av_frame_to_video_frame(self.frame)? };
308 Ok(video_frame)
309 }
310
311 unsafe fn find_video_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
317 unsafe {
319 let nb_streams = (*format_ctx).nb_streams as usize;
320 for i in 0..nb_streams {
321 let stream = (*format_ctx).streams.add(i);
322 let codecpar = (*(*stream)).codecpar;
323 if (*codecpar).codec_type == AVMediaType_AVMEDIA_TYPE_VIDEO {
324 return Some((i, (*codecpar).codec_id));
325 }
326 }
327 }
328 None
329 }
330
331 fn convert_pixel_format(fmt: AVPixelFormat) -> PixelFormat {
338 if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
339 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P
340 {
341 PixelFormat::Yuv420p
342 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P
343 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ422P
344 {
345 PixelFormat::Yuv422p
346 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P
347 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ444P
348 {
349 PixelFormat::Yuv444p
350 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24 {
351 PixelFormat::Rgb24
352 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24 {
353 PixelFormat::Bgr24
354 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA {
355 PixelFormat::Rgba
356 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA {
357 PixelFormat::Bgra
358 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8 {
359 PixelFormat::Gray8
360 } else {
361 log::warn!(
362 "pixel_format unsupported, falling back to Rgb24 requested={fmt} fallback=Rgb24"
363 );
364 PixelFormat::Rgb24
365 }
366 }
367
368 unsafe fn av_frame_to_video_frame(
374 &self,
375 frame: *const AVFrame,
376 ) -> Result<VideoFrame, DecodeError> {
377 unsafe {
379 let width = (*frame).width as u32;
380 let height = (*frame).height as u32;
381 let format = Self::convert_pixel_format((*frame).format);
382
383 let pts = (*frame).pts;
385 let timestamp = if pts == ff_sys::AV_NOPTS_VALUE {
386 Timestamp::default()
387 } else {
388 let stream = (*self.format_ctx).streams.add(self.stream_index);
389 let time_base = (*(*stream)).time_base;
390 Timestamp::new(
391 pts as i64,
392 Rational::new(time_base.num as i32, time_base.den as i32),
393 )
394 };
395
396 let (planes, strides) = Self::extract_planes_and_strides(frame, width, height, format)?;
397
398 VideoFrame::new(planes, strides, width, height, format, timestamp, true).map_err(|e| {
400 DecodeError::Ffmpeg {
401 code: 0,
402 message: format!("Failed to create VideoFrame: {e}"),
403 }
404 })
405 }
406 }
407
408 unsafe fn extract_planes_and_strides(
417 frame: *const AVFrame,
418 width: u32,
419 height: u32,
420 format: PixelFormat,
421 ) -> Result<(Vec<PooledBuffer>, Vec<usize>), DecodeError> {
422 unsafe {
424 let w = width as usize;
425 let h = height as usize;
426 let mut planes: Vec<PooledBuffer> = Vec::new();
427 let mut strides: Vec<usize> = Vec::new();
428
429 match format {
430 PixelFormat::Rgba | PixelFormat::Bgra => {
431 let bytes_per_pixel = 4_usize;
432 let stride = (*frame).linesize[0] as usize;
433 let row_w = w * bytes_per_pixel;
434 let mut buf = vec![0u8; row_w * h];
435 let src = (*frame).data[0];
436 if src.is_null() {
437 return Err(DecodeError::Ffmpeg {
438 code: 0,
439 message: "Null plane data for packed format".to_string(),
440 });
441 }
442 for row in 0..h {
443 ptr::copy_nonoverlapping(
444 src.add(row * stride),
445 buf[row * row_w..].as_mut_ptr(),
446 row_w,
447 );
448 }
449 planes.push(PooledBuffer::standalone(buf));
450 strides.push(row_w);
451 }
452 PixelFormat::Rgb24 | PixelFormat::Bgr24 => {
453 let bytes_per_pixel = 3_usize;
454 let stride = (*frame).linesize[0] as usize;
455 let row_w = w * bytes_per_pixel;
456 let mut buf = vec![0u8; row_w * h];
457 let src = (*frame).data[0];
458 if src.is_null() {
459 return Err(DecodeError::Ffmpeg {
460 code: 0,
461 message: "Null plane data for packed format".to_string(),
462 });
463 }
464 for row in 0..h {
465 ptr::copy_nonoverlapping(
466 src.add(row * stride),
467 buf[row * row_w..].as_mut_ptr(),
468 row_w,
469 );
470 }
471 planes.push(PooledBuffer::standalone(buf));
472 strides.push(row_w);
473 }
474 PixelFormat::Gray8 => {
475 let stride = (*frame).linesize[0] as usize;
476 let mut buf = vec![0u8; w * h];
477 let src = (*frame).data[0];
478 if src.is_null() {
479 return Err(DecodeError::Ffmpeg {
480 code: 0,
481 message: "Null plane data for Gray8".to_string(),
482 });
483 }
484 for row in 0..h {
485 ptr::copy_nonoverlapping(
486 src.add(row * stride),
487 buf[row * w..].as_mut_ptr(),
488 w,
489 );
490 }
491 planes.push(PooledBuffer::standalone(buf));
492 strides.push(w);
493 }
494 PixelFormat::Yuv420p | PixelFormat::Nv12 | PixelFormat::Nv21 => {
495 let y_stride = (*frame).linesize[0] as usize;
497 let mut y_buf = vec![0u8; w * h];
498 let y_src = (*frame).data[0];
499 if y_src.is_null() {
500 return Err(DecodeError::Ffmpeg {
501 code: 0,
502 message: "Null Y plane".to_string(),
503 });
504 }
505 for row in 0..h {
506 ptr::copy_nonoverlapping(
507 y_src.add(row * y_stride),
508 y_buf[row * w..].as_mut_ptr(),
509 w,
510 );
511 }
512 planes.push(PooledBuffer::standalone(y_buf));
513 strides.push(w);
514
515 if matches!(format, PixelFormat::Nv12 | PixelFormat::Nv21) {
516 let uv_h = h / 2;
518 let uv_stride = (*frame).linesize[1] as usize;
519 let mut uv_buf = vec![0u8; w * uv_h];
520 let uv_src = (*frame).data[1];
521 if !uv_src.is_null() {
522 for row in 0..uv_h {
523 ptr::copy_nonoverlapping(
524 uv_src.add(row * uv_stride),
525 uv_buf[row * w..].as_mut_ptr(),
526 w,
527 );
528 }
529 }
530 planes.push(PooledBuffer::standalone(uv_buf));
531 strides.push(w);
532 } else {
533 let uv_w = w / 2;
535 let uv_h = h / 2;
536 for plane_idx in 1..=2usize {
537 let uv_stride = (*frame).linesize[plane_idx] as usize;
538 let mut uv_buf = vec![0u8; uv_w * uv_h];
539 let uv_src = (*frame).data[plane_idx];
540 if !uv_src.is_null() {
541 for row in 0..uv_h {
542 ptr::copy_nonoverlapping(
543 uv_src.add(row * uv_stride),
544 uv_buf[row * uv_w..].as_mut_ptr(),
545 uv_w,
546 );
547 }
548 }
549 planes.push(PooledBuffer::standalone(uv_buf));
550 strides.push(uv_w);
551 }
552 }
553 }
554 PixelFormat::Yuv422p => {
555 let uv_w = w / 2;
557 let plane_dims = [(w, h), (uv_w, h), (uv_w, h)];
558 for (plane_idx, (pw, ph)) in plane_dims.iter().enumerate() {
559 let stride = (*frame).linesize[plane_idx] as usize;
560 let mut buf = vec![0u8; pw * ph];
561 let src = (*frame).data[plane_idx];
562 if !src.is_null() {
563 for row in 0..*ph {
564 ptr::copy_nonoverlapping(
565 src.add(row * stride),
566 buf[row * pw..].as_mut_ptr(),
567 *pw,
568 );
569 }
570 }
571 planes.push(PooledBuffer::standalone(buf));
572 strides.push(*pw);
573 }
574 }
575 PixelFormat::Yuv444p => {
576 for plane_idx in 0..3usize {
578 let stride = (*frame).linesize[plane_idx] as usize;
579 let mut buf = vec![0u8; w * h];
580 let src = (*frame).data[plane_idx];
581 if !src.is_null() {
582 for row in 0..h {
583 ptr::copy_nonoverlapping(
584 src.add(row * stride),
585 buf[row * w..].as_mut_ptr(),
586 w,
587 );
588 }
589 }
590 planes.push(PooledBuffer::standalone(buf));
591 strides.push(w);
592 }
593 }
594 _ => {
595 return Err(DecodeError::Ffmpeg {
596 code: 0,
597 message: format!("Unsupported pixel format for image decoding: {format:?}"),
598 });
599 }
600 }
601
602 Ok((planes, strides))
603 }
604 }
605}
606
607impl Drop for ImageDecoderInner {
608 fn drop(&mut self) {
609 unsafe {
612 if !self.frame.is_null() {
613 ff_sys::av_frame_free(&mut (self.frame as *mut _));
614 }
615 if !self.packet.is_null() {
616 ff_sys::av_packet_free(&mut (self.packet as *mut _));
617 }
618 if !self.codec_ctx.is_null() {
619 ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
620 }
621 if !self.format_ctx.is_null() {
622 ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
623 }
624 }
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn convert_pixel_format_yuv420p_should_map_to_yuv420p() {
634 assert_eq!(
635 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
636 PixelFormat::Yuv420p
637 );
638 }
639
640 #[test]
641 fn convert_pixel_format_yuvj420p_should_map_to_yuv420p() {
642 assert_eq!(
643 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P),
644 PixelFormat::Yuv420p
645 );
646 }
647
648 #[test]
649 fn convert_pixel_format_rgb24_should_map_to_rgb24() {
650 assert_eq!(
651 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
652 PixelFormat::Rgb24
653 );
654 }
655
656 #[test]
657 fn convert_pixel_format_rgba_should_map_to_rgba() {
658 assert_eq!(
659 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
660 PixelFormat::Rgba
661 );
662 }
663
664 #[test]
665 fn convert_pixel_format_gray8_should_map_to_gray8() {
666 assert_eq!(
667 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
668 PixelFormat::Gray8
669 );
670 }
671
672 #[test]
673 fn unsupported_codec_error_should_include_codec_name() {
674 let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_PNG;
675 let codec_name = unsafe {
677 let name_ptr = ff_sys::avcodec_get_name(codec_id);
678 if name_ptr.is_null() {
679 String::from("unknown")
680 } else {
681 std::ffi::CStr::from_ptr(name_ptr)
682 .to_string_lossy()
683 .into_owned()
684 }
685 };
686 let error = crate::error::DecodeError::UnsupportedCodec {
687 codec: format!("{codec_name} (codec_id={codec_id:?})"),
688 };
689 let msg = error.to_string();
690 assert!(msg.contains("png"), "expected codec name in error: {msg}");
691 assert!(
692 msg.contains("codec_id="),
693 "expected codec_id in error: {msg}"
694 );
695 }
696}