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;
23
24use ff_format::time::{Rational, Timestamp};
25use ff_format::{PixelFormat, PooledBuffer, VideoFrame};
26use ff_sys::{
27 AVCodecID, AVMediaType_AVMEDIA_TYPE_VIDEO, AVPixelFormat, Frame, InputFormatContext, Packet,
28};
29
30use crate::error::DecodeError;
31use crate::shared::guards_inner::open_input_ctx;
32
33pub(crate) struct ImageDecoderInner {
39 format_ctx: InputFormatContext,
41 codec_ctx: ff_sys::CodecContext,
43 stream_index: usize,
45 packet: Packet,
47 frame: Frame,
49}
50
51unsafe impl Send for ImageDecoderInner {}
55
56impl ImageDecoderInner {
57 pub(crate) fn new(path: &Path) -> Result<Self, DecodeError> {
68 ff_sys::ensure_initialized();
69
70 let mut ctx = open_input_ctx(path)?;
72
73 ctx.find_stream_info().map_err(|e| DecodeError::Ffmpeg {
75 code: e.code(),
76 message: format!(
77 "Failed to find stream info: {}",
78 ff_sys::av_error_string(e.code())
79 ),
80 })?;
81
82 let (stream_index, codec_id) =
84 Self::find_video_stream(&ctx).ok_or_else(|| DecodeError::NoVideoStream {
85 path: path.to_path_buf(),
86 })?;
87
88 let codec_name = unsafe {
92 let name_ptr = ff_sys::avcodec_get_name(codec_id);
93 if name_ptr.is_null() {
94 String::from("unknown")
95 } else {
96 CStr::from_ptr(name_ptr).to_string_lossy().into_owned()
97 }
98 };
99 let codec =
100 ff_sys::Codec::find_decoder(codec_id).ok_or_else(|| DecodeError::UnsupportedCodec {
101 codec: format!("{codec_name} (codec_id={codec_id:?})"),
102 })?;
103
104 let mut codec_ctx =
106 ff_sys::CodecContext::new(Some(codec)).map_err(|e| DecodeError::Ffmpeg {
107 code: e.code(),
108 message: format!(
109 "Failed to allocate codec context: {}",
110 ff_sys::av_error_string(e.code())
111 ),
112 })?;
113
114 let codecpar = ctx
116 .stream(stream_index)
117 .ok_or_else(|| DecodeError::NoVideoStream {
118 path: path.to_path_buf(),
119 })?
120 .codecpar();
121 codec_ctx
122 .apply_parameters(&codecpar)
123 .map_err(|e| DecodeError::Ffmpeg {
124 code: e.code(),
125 message: format!(
126 "Failed to copy codec parameters: {}",
127 ff_sys::av_error_string(e.code())
128 ),
129 })?;
130
131 codec_ctx
133 .open_codec(codec)
134 .map_err(|e| DecodeError::Ffmpeg {
135 code: e.code(),
136 message: format!(
137 "Failed to open codec: {}",
138 ff_sys::av_error_string(e.code())
139 ),
140 })?;
141
142 let packet = Packet::new().map_err(|e| DecodeError::Ffmpeg {
145 code: e.code(),
146 message: format!(
147 "Failed to allocate packet: {}",
148 ff_sys::av_error_string(e.code())
149 ),
150 })?;
151 let frame = Frame::new().map_err(|e| DecodeError::Ffmpeg {
152 code: e.code(),
153 message: format!(
154 "Failed to allocate frame: {}",
155 ff_sys::av_error_string(e.code())
156 ),
157 })?;
158
159 Ok(Self {
160 format_ctx: ctx,
161 codec_ctx,
162 stream_index,
163 packet,
164 frame,
165 })
166 }
167
168 pub(crate) fn width(&self) -> u32 {
170 self.codec_ctx.width() as u32
171 }
172
173 pub(crate) fn height(&self) -> u32 {
175 self.codec_ctx.height() as u32
176 }
177
178 pub(crate) fn decode(mut self) -> Result<VideoFrame, DecodeError> {
186 if let Err(e) = self.format_ctx.read_frame(&mut self.packet) {
188 let ret = e.code();
189 return Err(DecodeError::Ffmpeg {
190 code: ret,
191 message: format!("Failed to read frame: {}", ff_sys::av_error_string(ret)),
192 });
193 }
194
195 let send_result = self.codec_ctx.send_packet(&self.packet);
197 self.packet.unref();
198 if let Err(e) = send_result {
199 return Err(DecodeError::Ffmpeg {
200 code: e.code(),
201 message: format!(
202 "Failed to send packet to decoder: {}",
203 ff_sys::av_error_string(e.code())
204 ),
205 });
206 }
207
208 match self
210 .codec_ctx
211 .receive_frame(&mut self.frame)
212 .map_err(|e| DecodeError::Ffmpeg {
213 code: e.code(),
214 message: format!(
215 "Failed to receive decoded frame: {}",
216 ff_sys::av_error_string(e.code())
217 ),
218 })? {
219 ff_sys::ReceiveOutcome::Frame => {}
220 ff_sys::ReceiveOutcome::NeedInput => {
223 return Err(DecodeError::Ffmpeg {
224 code: ff_sys::error_codes::EAGAIN,
225 message: format!(
226 "Failed to receive decoded frame: {}",
227 ff_sys::av_error_string(ff_sys::error_codes::EAGAIN)
228 ),
229 });
230 }
231 ff_sys::ReceiveOutcome::Drained => {
232 return Err(DecodeError::Ffmpeg {
233 code: ff_sys::error_codes::EOF,
234 message: format!(
235 "Failed to receive decoded frame: {}",
236 ff_sys::av_error_string(ff_sys::error_codes::EOF)
237 ),
238 });
239 }
240 }
241
242 let video_frame = unsafe { self.av_frame_to_video_frame(&self.frame)? };
245 Ok(video_frame)
246 }
247
248 fn find_video_stream(format_ctx: &InputFormatContext) -> Option<(usize, AVCodecID)> {
250 for stream in format_ctx.streams() {
251 let codecpar = stream.codecpar();
252 if codecpar.codec_type() == AVMediaType_AVMEDIA_TYPE_VIDEO {
253 return Some((stream.index() as usize, codecpar.codec_id()));
254 }
255 }
256 None
257 }
258
259 fn convert_pixel_format(fmt: AVPixelFormat) -> PixelFormat {
266 if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P
267 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P
268 {
269 PixelFormat::Yuv420p
270 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P
271 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ422P
272 {
273 PixelFormat::Yuv422p
274 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P
275 || fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ444P
276 {
277 PixelFormat::Yuv444p
278 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24 {
279 PixelFormat::Rgb24
280 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24 {
281 PixelFormat::Bgr24
282 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA {
283 PixelFormat::Rgba
284 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA {
285 PixelFormat::Bgra
286 } else if fmt == ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8 {
287 PixelFormat::Gray8
288 } else {
289 log::warn!(
290 "pixel_format unsupported, falling back to Rgb24 requested={fmt} fallback=Rgb24"
291 );
292 PixelFormat::Rgb24
293 }
294 }
295
296 unsafe fn av_frame_to_video_frame(&self, frame: &Frame) -> Result<VideoFrame, DecodeError> {
306 let width = frame.width() as u32;
307 let height = frame.height() as u32;
308 let format = Self::convert_pixel_format(frame.format());
309
310 let pts = frame.pts();
312 let timestamp = if pts == ff_sys::AV_NOPTS_VALUE {
313 Timestamp::default()
314 } else {
315 match self.format_ctx.stream(self.stream_index) {
316 Some(stream) => {
317 let time_base = stream.time_base();
318 Timestamp::new(
319 pts,
320 Rational::new(time_base.num as i32, time_base.den as i32),
321 )
322 }
323 None => Timestamp::default(),
324 }
325 };
326
327 let (planes, strides) =
329 unsafe { Self::extract_planes_and_strides(frame, width, height, format)? };
330
331 VideoFrame::new(planes, strides, width, height, format, timestamp, true).map_err(|e| {
333 DecodeError::Ffmpeg {
334 code: 0,
335 message: format!("Failed to create VideoFrame: {e}"),
336 }
337 })
338 }
339
340 unsafe fn extract_planes_and_strides(
349 frame: &Frame,
350 width: u32,
351 height: u32,
352 format: PixelFormat,
353 ) -> Result<(Vec<PooledBuffer>, Vec<usize>), DecodeError> {
354 let w = width as usize;
355 let h = height as usize;
356 let mut planes: Vec<PooledBuffer> = Vec::new();
357 let mut strides: Vec<usize> = Vec::new();
358
359 let copy_plane = |i: usize, buf: &mut [u8], rows: usize, row_bytes: usize| unsafe {
368 frame
369 .copy_plane_rows(i, buf, row_bytes, rows, row_bytes)
370 .is_some()
371 };
372
373 match format {
374 PixelFormat::Rgba | PixelFormat::Bgra => {
375 let row_w = w * 4;
376 let mut buf = vec![0u8; row_w * h];
377 if !copy_plane(0, &mut buf, h, row_w) {
378 return Err(DecodeError::Ffmpeg {
379 code: 0,
380 message: "Null plane data for packed format".to_string(),
381 });
382 }
383 planes.push(PooledBuffer::standalone(buf));
384 strides.push(row_w);
385 }
386 PixelFormat::Rgb24 | PixelFormat::Bgr24 => {
387 let row_w = w * 3;
388 let mut buf = vec![0u8; row_w * h];
389 if !copy_plane(0, &mut buf, h, row_w) {
390 return Err(DecodeError::Ffmpeg {
391 code: 0,
392 message: "Null plane data for packed format".to_string(),
393 });
394 }
395 planes.push(PooledBuffer::standalone(buf));
396 strides.push(row_w);
397 }
398 PixelFormat::Gray8 => {
399 let mut buf = vec![0u8; w * h];
400 if !copy_plane(0, &mut buf, h, w) {
401 return Err(DecodeError::Ffmpeg {
402 code: 0,
403 message: "Null plane data for Gray8".to_string(),
404 });
405 }
406 planes.push(PooledBuffer::standalone(buf));
407 strides.push(w);
408 }
409 PixelFormat::Yuv420p | PixelFormat::Nv12 | PixelFormat::Nv21 => {
410 let mut y_buf = vec![0u8; w * h];
412 if !copy_plane(0, &mut y_buf, h, w) {
413 return Err(DecodeError::Ffmpeg {
414 code: 0,
415 message: "Null Y plane".to_string(),
416 });
417 }
418 planes.push(PooledBuffer::standalone(y_buf));
419 strides.push(w);
420
421 if matches!(format, PixelFormat::Nv12 | PixelFormat::Nv21) {
422 let uv_h = h / 2;
424 let mut uv_buf = vec![0u8; w * uv_h];
425 copy_plane(1, &mut uv_buf, uv_h, w);
426 planes.push(PooledBuffer::standalone(uv_buf));
427 strides.push(w);
428 } else {
429 let uv_w = w / 2;
431 let uv_h = h / 2;
432 for plane_idx in 1..=2usize {
433 let mut uv_buf = vec![0u8; uv_w * uv_h];
434 copy_plane(plane_idx, &mut uv_buf, uv_h, uv_w);
435 planes.push(PooledBuffer::standalone(uv_buf));
436 strides.push(uv_w);
437 }
438 }
439 }
440 PixelFormat::Yuv422p => {
441 let uv_w = w / 2;
443 let plane_dims = [(w, h), (uv_w, h), (uv_w, h)];
444 for (plane_idx, (pw, ph)) in plane_dims.iter().enumerate() {
445 let mut buf = vec![0u8; pw * ph];
446 copy_plane(plane_idx, &mut buf, *ph, *pw);
447 planes.push(PooledBuffer::standalone(buf));
448 strides.push(*pw);
449 }
450 }
451 PixelFormat::Yuv444p => {
452 for plane_idx in 0..3usize {
454 let mut buf = vec![0u8; w * h];
455 copy_plane(plane_idx, &mut buf, h, w);
456 planes.push(PooledBuffer::standalone(buf));
457 strides.push(w);
458 }
459 }
460 _ => {
461 return Err(DecodeError::Ffmpeg {
462 code: 0,
463 message: format!("Unsupported pixel format for image decoding: {format:?}"),
464 });
465 }
466 }
467
468 Ok((planes, strides))
469 }
470}
471
472#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn convert_pixel_format_yuv420p_should_map_to_yuv420p() {
482 assert_eq!(
483 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P),
484 PixelFormat::Yuv420p
485 );
486 }
487
488 #[test]
489 fn convert_pixel_format_yuvj420p_should_map_to_yuv420p() {
490 assert_eq!(
491 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUVJ420P),
492 PixelFormat::Yuv420p
493 );
494 }
495
496 #[test]
497 fn convert_pixel_format_rgb24_should_map_to_rgb24() {
498 assert_eq!(
499 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24),
500 PixelFormat::Rgb24
501 );
502 }
503
504 #[test]
505 fn convert_pixel_format_rgba_should_map_to_rgba() {
506 assert_eq!(
507 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA),
508 PixelFormat::Rgba
509 );
510 }
511
512 #[test]
513 fn convert_pixel_format_gray8_should_map_to_gray8() {
514 assert_eq!(
515 ImageDecoderInner::convert_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8),
516 PixelFormat::Gray8
517 );
518 }
519
520 #[test]
521 fn unsupported_codec_error_should_include_codec_name() {
522 let codec_id = ff_sys::AVCodecID_AV_CODEC_ID_PNG;
523 let codec_name = unsafe {
525 let name_ptr = ff_sys::avcodec_get_name(codec_id);
526 if name_ptr.is_null() {
527 String::from("unknown")
528 } else {
529 std::ffi::CStr::from_ptr(name_ptr)
530 .to_string_lossy()
531 .into_owned()
532 }
533 };
534 let error = crate::error::DecodeError::UnsupportedCodec {
535 codec: format!("{codec_name} (codec_id={codec_id:?})"),
536 };
537 let msg = error.to_string();
538 assert!(msg.contains("png"), "expected codec name in error: {msg}");
539 assert!(
540 msg.contains("codec_id="),
541 "expected codec_id in error: {msg}"
542 );
543 }
544}