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::path::Path;
22use std::ptr;
23
24use ff_format::time::{Rational, Timestamp};
25use ff_format::{PixelFormat, PooledBuffer, VideoFrame};
26use ff_sys::{
27 AVCodecContext, AVCodecID, AVFormatContext, AVFrame, AVMediaType_AVMEDIA_TYPE_VIDEO, AVPacket,
28 AVPixelFormat,
29};
30
31use crate::error::DecodeError;
32
33struct AvFormatContextGuard(*mut AVFormatContext);
37
38impl AvFormatContextGuard {
39 unsafe fn new(path: &Path) -> Result<Self, DecodeError> {
40 let format_ctx = unsafe {
42 ff_sys::avformat::open_input(path).map_err(|e| {
43 DecodeError::Ffmpeg(format!(
44 "Failed to open file: {}",
45 ff_sys::av_error_string(e)
46 ))
47 })?
48 };
49 Ok(Self(format_ctx))
50 }
51
52 const fn as_ptr(&self) -> *mut AVFormatContext {
53 self.0
54 }
55
56 fn into_raw(self) -> *mut AVFormatContext {
57 let ptr = self.0;
58 std::mem::forget(self);
59 ptr
60 }
61}
62
63impl Drop for AvFormatContextGuard {
64 fn drop(&mut self) {
65 if !self.0.is_null() {
66 unsafe {
68 ff_sys::avformat::close_input(&mut (self.0 as *mut _));
69 }
70 }
71 }
72}
73
74struct AvCodecContextGuard(*mut AVCodecContext);
76
77impl AvCodecContextGuard {
78 unsafe fn new(codec: *const ff_sys::AVCodec) -> Result<Self, DecodeError> {
79 let codec_ctx = unsafe {
81 ff_sys::avcodec::alloc_context3(codec).map_err(|e| {
82 DecodeError::Ffmpeg(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| {
156 DecodeError::Ffmpeg(format!(
157 "Failed to find stream info: {}",
158 ff_sys::av_error_string(e)
159 ))
160 })?;
161 }
162
163 let (stream_index, codec_id) =
166 unsafe { Self::find_video_stream(format_ctx) }.ok_or_else(|| {
167 DecodeError::NoVideoStream {
168 path: path.to_path_buf(),
169 }
170 })?;
171
172 let codec = unsafe {
175 ff_sys::avcodec::find_decoder(codec_id).ok_or_else(|| {
176 DecodeError::UnsupportedCodec {
177 codec: format!("codec_id={codec_id:?}"),
178 }
179 })?
180 };
181
182 let codec_ctx_guard = unsafe { AvCodecContextGuard::new(codec)? };
185 let codec_ctx = codec_ctx_guard.as_ptr();
186
187 unsafe {
190 let stream = (*format_ctx).streams.add(stream_index);
191 let codecpar = (*(*stream)).codecpar;
192 ff_sys::avcodec::parameters_to_context(codec_ctx, codecpar).map_err(|e| {
193 DecodeError::Ffmpeg(format!(
194 "Failed to copy codec parameters: {}",
195 ff_sys::av_error_string(e)
196 ))
197 })?;
198 }
199
200 unsafe {
203 ff_sys::avcodec::open2(codec_ctx, codec, ptr::null_mut()).map_err(|e| {
204 DecodeError::Ffmpeg(format!(
205 "Failed to open codec: {}",
206 ff_sys::av_error_string(e)
207 ))
208 })?;
209 }
210
211 let packet = unsafe { ff_sys::av_packet_alloc() };
214 if packet.is_null() {
215 return Err(DecodeError::Ffmpeg("Failed to allocate packet".to_string()));
216 }
217 let frame = unsafe { ff_sys::av_frame_alloc() };
218 if frame.is_null() {
219 unsafe { ff_sys::av_packet_free(&mut (packet as *mut _)) };
220 return Err(DecodeError::Ffmpeg("Failed to allocate frame".to_string()));
221 }
222
223 Ok(Self {
224 format_ctx: format_ctx_guard.into_raw(),
225 codec_ctx: codec_ctx_guard.into_raw(),
226 stream_index,
227 packet,
228 frame,
229 })
230 }
231
232 pub(crate) fn width(&self) -> u32 {
234 unsafe { (*self.codec_ctx).width as u32 }
236 }
237
238 pub(crate) fn height(&self) -> u32 {
240 unsafe { (*self.codec_ctx).height as u32 }
242 }
243
244 pub(crate) fn decode(self) -> Result<VideoFrame, DecodeError> {
252 let ret = unsafe { ff_sys::av_read_frame(self.format_ctx, self.packet) };
255 if ret < 0 {
256 return Err(DecodeError::Ffmpeg(format!(
257 "Failed to read frame: {}",
258 ff_sys::av_error_string(ret)
259 )));
260 }
261
262 let ret = unsafe { ff_sys::avcodec_send_packet(self.codec_ctx, self.packet) };
265 unsafe { ff_sys::av_packet_unref(self.packet) };
266 if ret < 0 {
267 return Err(DecodeError::Ffmpeg(format!(
268 "Failed to send packet to decoder: {}",
269 ff_sys::av_error_string(ret)
270 )));
271 }
272
273 let ret = unsafe { ff_sys::avcodec_receive_frame(self.codec_ctx, self.frame) };
276 if ret < 0 {
277 return Err(DecodeError::Ffmpeg(format!(
278 "Failed to receive decoded frame: {}",
279 ff_sys::av_error_string(ret)
280 )));
281 }
282
283 let video_frame = unsafe { self.av_frame_to_video_frame(self.frame)? };
286 Ok(video_frame)
287 }
288
289 unsafe fn find_video_stream(format_ctx: *mut AVFormatContext) -> Option<(usize, AVCodecID)> {
295 unsafe {
297 let nb_streams = (*format_ctx).nb_streams as usize;
298 for i in 0..nb_streams {
299 let stream = (*format_ctx).streams.add(i);
300 let codecpar = (*(*stream)).codecpar;
301 if (*codecpar).codec_type == AVMediaType_AVMEDIA_TYPE_VIDEO {
302 return Some((i, (*codecpar).codec_id));
303 }
304 }
305 }
306 None
307 }
308
309 fn convert_pixel_format(fmt: AVPixelFormat) -> PixelFormat {
316 if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
317 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P
318 {
319 PixelFormat::Yuv420p
320 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P
321 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ422P
322 {
323 PixelFormat::Yuv422p
324 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P
325 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ444P
326 {
327 PixelFormat::Yuv444p
328 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24 {
329 PixelFormat::Rgb24
330 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24 {
331 PixelFormat::Bgr24
332 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA {
333 PixelFormat::Rgba
334 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA {
335 PixelFormat::Bgra
336 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8 {
337 PixelFormat::Gray8
338 } else {
339 log::warn!(
340 "pixel_format unsupported, falling back to Rgb24 requested={fmt} fallback=Rgb24"
341 );
342 PixelFormat::Rgb24
343 }
344 }
345
346 unsafe fn av_frame_to_video_frame(
352 &self,
353 frame: *const AVFrame,
354 ) -> Result<VideoFrame, DecodeError> {
355 unsafe {
357 let width = (*frame).width as u32;
358 let height = (*frame).height as u32;
359 let format = Self::convert_pixel_format((*frame).format);
360
361 let pts = (*frame).pts;
363 let timestamp = if pts == ff_sys::AV_NOPTS_VALUE {
364 Timestamp::default()
365 } else {
366 let stream = (*self.format_ctx).streams.add(self.stream_index);
367 let time_base = (*(*stream)).time_base;
368 Timestamp::new(
369 pts as i64,
370 Rational::new(time_base.num as i32, time_base.den as i32),
371 )
372 };
373
374 let (planes, strides) = Self::extract_planes_and_strides(frame, width, height, format)?;
375
376 VideoFrame::new(planes, strides, width, height, format, timestamp, true)
378 .map_err(|e| DecodeError::Ffmpeg(format!("Failed to create VideoFrame: {e}")))
379 }
380 }
381
382 unsafe fn extract_planes_and_strides(
391 frame: *const AVFrame,
392 width: u32,
393 height: u32,
394 format: PixelFormat,
395 ) -> Result<(Vec<PooledBuffer>, Vec<usize>), DecodeError> {
396 unsafe {
398 let w = width as usize;
399 let h = height as usize;
400 let mut planes: Vec<PooledBuffer> = Vec::new();
401 let mut strides: Vec<usize> = Vec::new();
402
403 match format {
404 PixelFormat::Rgba | PixelFormat::Bgra => {
405 let bytes_per_pixel = 4_usize;
406 let stride = (*frame).linesize[0] as usize;
407 let row_w = w * bytes_per_pixel;
408 let mut buf = vec![0u8; row_w * h];
409 let src = (*frame).data[0];
410 if src.is_null() {
411 return Err(DecodeError::Ffmpeg(
412 "Null plane data for packed format".to_string(),
413 ));
414 }
415 for row in 0..h {
416 ptr::copy_nonoverlapping(
417 src.add(row * stride),
418 buf[row * row_w..].as_mut_ptr(),
419 row_w,
420 );
421 }
422 planes.push(PooledBuffer::standalone(buf));
423 strides.push(row_w);
424 }
425 PixelFormat::Rgb24 | PixelFormat::Bgr24 => {
426 let bytes_per_pixel = 3_usize;
427 let stride = (*frame).linesize[0] as usize;
428 let row_w = w * bytes_per_pixel;
429 let mut buf = vec![0u8; row_w * h];
430 let src = (*frame).data[0];
431 if src.is_null() {
432 return Err(DecodeError::Ffmpeg(
433 "Null plane data for packed format".to_string(),
434 ));
435 }
436 for row in 0..h {
437 ptr::copy_nonoverlapping(
438 src.add(row * stride),
439 buf[row * row_w..].as_mut_ptr(),
440 row_w,
441 );
442 }
443 planes.push(PooledBuffer::standalone(buf));
444 strides.push(row_w);
445 }
446 PixelFormat::Gray8 => {
447 let stride = (*frame).linesize[0] as usize;
448 let mut buf = vec![0u8; w * h];
449 let src = (*frame).data[0];
450 if src.is_null() {
451 return Err(DecodeError::Ffmpeg("Null plane data for Gray8".to_string()));
452 }
453 for row in 0..h {
454 ptr::copy_nonoverlapping(
455 src.add(row * stride),
456 buf[row * w..].as_mut_ptr(),
457 w,
458 );
459 }
460 planes.push(PooledBuffer::standalone(buf));
461 strides.push(w);
462 }
463 PixelFormat::Yuv420p | PixelFormat::Nv12 | PixelFormat::Nv21 => {
464 let y_stride = (*frame).linesize[0] as usize;
466 let mut y_buf = vec![0u8; w * h];
467 let y_src = (*frame).data[0];
468 if y_src.is_null() {
469 return Err(DecodeError::Ffmpeg("Null Y plane".to_string()));
470 }
471 for row in 0..h {
472 ptr::copy_nonoverlapping(
473 y_src.add(row * y_stride),
474 y_buf[row * w..].as_mut_ptr(),
475 w,
476 );
477 }
478 planes.push(PooledBuffer::standalone(y_buf));
479 strides.push(w);
480
481 if matches!(format, PixelFormat::Nv12 | PixelFormat::Nv21) {
482 let uv_h = h / 2;
484 let uv_stride = (*frame).linesize[1] as usize;
485 let mut uv_buf = vec![0u8; w * uv_h];
486 let uv_src = (*frame).data[1];
487 if !uv_src.is_null() {
488 for row in 0..uv_h {
489 ptr::copy_nonoverlapping(
490 uv_src.add(row * uv_stride),
491 uv_buf[row * w..].as_mut_ptr(),
492 w,
493 );
494 }
495 }
496 planes.push(PooledBuffer::standalone(uv_buf));
497 strides.push(w);
498 } else {
499 let uv_w = w / 2;
501 let uv_h = h / 2;
502 for plane_idx in 1..=2usize {
503 let uv_stride = (*frame).linesize[plane_idx] as usize;
504 let mut uv_buf = vec![0u8; uv_w * uv_h];
505 let uv_src = (*frame).data[plane_idx];
506 if !uv_src.is_null() {
507 for row in 0..uv_h {
508 ptr::copy_nonoverlapping(
509 uv_src.add(row * uv_stride),
510 uv_buf[row * uv_w..].as_mut_ptr(),
511 uv_w,
512 );
513 }
514 }
515 planes.push(PooledBuffer::standalone(uv_buf));
516 strides.push(uv_w);
517 }
518 }
519 }
520 PixelFormat::Yuv422p => {
521 let uv_w = w / 2;
523 let plane_dims = [(w, h), (uv_w, h), (uv_w, h)];
524 for (plane_idx, (pw, ph)) in plane_dims.iter().enumerate() {
525 let stride = (*frame).linesize[plane_idx] as usize;
526 let mut buf = vec![0u8; pw * ph];
527 let src = (*frame).data[plane_idx];
528 if !src.is_null() {
529 for row in 0..*ph {
530 ptr::copy_nonoverlapping(
531 src.add(row * stride),
532 buf[row * pw..].as_mut_ptr(),
533 *pw,
534 );
535 }
536 }
537 planes.push(PooledBuffer::standalone(buf));
538 strides.push(*pw);
539 }
540 }
541 PixelFormat::Yuv444p => {
542 for plane_idx in 0..3usize {
544 let stride = (*frame).linesize[plane_idx] as usize;
545 let mut buf = vec![0u8; w * h];
546 let src = (*frame).data[plane_idx];
547 if !src.is_null() {
548 for row in 0..h {
549 ptr::copy_nonoverlapping(
550 src.add(row * stride),
551 buf[row * w..].as_mut_ptr(),
552 w,
553 );
554 }
555 }
556 planes.push(PooledBuffer::standalone(buf));
557 strides.push(w);
558 }
559 }
560 _ => {
561 return Err(DecodeError::Ffmpeg(format!(
562 "Unsupported pixel format for image decoding: {format:?}"
563 )));
564 }
565 }
566
567 Ok((planes, strides))
568 }
569 }
570}
571
572impl Drop for ImageDecoderInner {
573 fn drop(&mut self) {
574 unsafe {
577 if !self.frame.is_null() {
578 ff_sys::av_frame_free(&mut (self.frame as *mut _));
579 }
580 if !self.packet.is_null() {
581 ff_sys::av_packet_free(&mut (self.packet as *mut _));
582 }
583 if !self.codec_ctx.is_null() {
584 ff_sys::avcodec::free_context(&mut (self.codec_ctx as *mut _));
585 }
586 if !self.format_ctx.is_null() {
587 ff_sys::avformat::close_input(&mut (self.format_ctx as *mut _));
588 }
589 }
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn convert_pixel_format_yuv420p_should_map_to_yuv420p() {
599 assert_eq!(
600 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
601 PixelFormat::Yuv420p
602 );
603 }
604
605 #[test]
606 fn convert_pixel_format_yuvj420p_should_map_to_yuv420p() {
607 assert_eq!(
608 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P),
609 PixelFormat::Yuv420p
610 );
611 }
612
613 #[test]
614 fn convert_pixel_format_rgb24_should_map_to_rgb24() {
615 assert_eq!(
616 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
617 PixelFormat::Rgb24
618 );
619 }
620
621 #[test]
622 fn convert_pixel_format_rgba_should_map_to_rgba() {
623 assert_eq!(
624 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
625 PixelFormat::Rgba
626 );
627 }
628
629 #[test]
630 fn convert_pixel_format_gray8_should_map_to_gray8() {
631 assert_eq!(
632 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
633 PixelFormat::Gray8
634 );
635 }
636}