use super::frame::{FrameError, PixelBuffer, PixelFormat};
use log::warn;
use playa_ffmpeg as ffmpeg;
use std::path::Path;
use std::sync::Once;
static FFMPEG_LOG_INIT: Once = Once::new();
fn init_ffmpeg_logging() {
FFMPEG_LOG_INIT.call_once(|| {
unsafe {
ffmpeg::ffi::av_log_set_level(ffmpeg::ffi::AV_LOG_QUIET);
}
});
}
pub struct VideoMetadata {
pub frame_count: usize,
pub width: u32,
pub height: u32,
pub fps: f64,
}
impl VideoMetadata {
pub fn from_file(path: &Path) -> Result<Self, FrameError> {
init_ffmpeg_logging();
let ictx = ffmpeg::format::input(path)
.map_err(|e| FrameError::LoadError(format!("Failed to open video: {}", e)))?;
let stream = ictx
.streams()
.best(ffmpeg::media::Type::Video)
.ok_or_else(|| FrameError::LoadError("No video stream found".to_string()))?;
let duration = stream.duration();
let fps_rational = stream.avg_frame_rate();
let time_base = stream.time_base();
let duration_secs =
duration as f64 * time_base.numerator() as f64 / time_base.denominator() as f64;
let fps = fps_rational.numerator() as f64 / fps_rational.denominator() as f64;
let frame_count = (duration_secs * fps) as usize;
let codec_params = stream.parameters();
let decoder_ctx =
ffmpeg::codec::context::Context::from_parameters(codec_params).map_err(|e| {
FrameError::LoadError(format!("Failed to create decoder context: {}", e))
})?;
let decoder = decoder_ctx
.decoder()
.video()
.map_err(|e| FrameError::LoadError(format!("Failed to create video decoder: {}", e)))?;
Ok(VideoMetadata {
frame_count,
width: decoder.width(),
height: decoder.height(),
fps,
})
}
}
pub fn get_video_dimensions(path: &Path) -> Result<(usize, usize), FrameError> {
let metadata = VideoMetadata::from_file(path)?;
Ok((metadata.width as usize, metadata.height as usize))
}
pub fn decode_frame(
path: &Path,
frame_num: usize,
) -> Result<(PixelBuffer, PixelFormat, usize, usize), FrameError> {
init_ffmpeg_logging();
let mut ictx = ffmpeg::format::input(path)
.map_err(|e| FrameError::LoadError(format!("Failed to open video: {}", e)))?;
let stream = ictx
.streams()
.best(ffmpeg::media::Type::Video)
.ok_or_else(|| FrameError::LoadError("No video stream found".to_string()))?;
let stream_idx = stream.index();
let codec_params = stream.parameters();
let mut decoder_ctx = ffmpeg::codec::context::Context::from_parameters(codec_params)
.map_err(|e| FrameError::LoadError(format!("Failed to create decoder context: {}", e)))?;
unsafe {
(*decoder_ctx.as_mut_ptr()).thread_type = ffmpeg::ffi::FF_THREAD_FRAME;
(*decoder_ctx.as_mut_ptr()).thread_count = 0; }
let mut decoder = decoder_ctx
.decoder()
.video()
.map_err(|e| FrameError::LoadError(format!("Failed to create video decoder: {}", e)))?;
let width = decoder.width();
let height = decoder.height();
let mut scaler = ffmpeg::software::scaling::Context::get(
decoder.format(),
width,
height,
ffmpeg::format::Pixel::RGBA,
width,
height,
ffmpeg::software::scaling::Flags::BILINEAR,
)
.map_err(|e| FrameError::LoadError(format!("Failed to create scaler: {}", e)))?;
let fps = stream.avg_frame_rate();
let fps_num = fps.numerator();
let fps_den = fps.denominator();
let time_base = stream.time_base();
let target_ts = if fps_num > 0 && fps_den > 0 {
let frame_tb = ffmpeg::ffi::AVRational {
num: fps_den as i32,
den: fps_num as i32,
};
let stream_tb = ffmpeg::ffi::AVRational {
num: time_base.numerator() as i32,
den: time_base.denominator() as i32,
};
Some(unsafe { ffmpeg::ffi::av_rescale_q(frame_num as i64, frame_tb, stream_tb) })
} else {
None
};
if let Some(target_ts) = target_ts {
let seek_ret = unsafe {
ffmpeg::ffi::av_seek_frame(
ictx.as_mut_ptr(),
stream_idx as i32,
target_ts,
ffmpeg::ffi::AVSEEK_FLAG_BACKWARD,
)
};
if seek_ret < 0 {
warn!("Video seek failed (ret={}), falling back to decode-from-start", seek_ret);
}
}
let mut current_frame = 0;
for (stream, packet) in ictx.packets() {
if stream.index() == stream_idx {
decoder
.send_packet(&packet)
.map_err(|e| FrameError::LoadError(format!("Failed to send packet: {}", e)))?;
let mut decoded = ffmpeg::util::frame::video::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
let reached_target = if let Some(target_ts) = target_ts {
decoded
.pts()
.map(|pts| pts >= target_ts)
.unwrap_or(current_frame >= frame_num)
} else {
current_frame >= frame_num
};
if reached_target {
let mut rgba_frame = ffmpeg::util::frame::video::Video::empty();
scaler.run(&decoded, &mut rgba_frame).map_err(|e| {
FrameError::LoadError(format!("Failed to scale frame: {}", e))
})?;
let rgba_data = rgba_frame.data(0);
let stride = rgba_frame.stride(0) as usize;
let row_bytes = (width * 4) as usize;
let mut output = vec![0u8; row_bytes * height as usize];
for y in 0..height as usize {
let src = y * stride;
let dst = y * row_bytes;
output[dst..dst + row_bytes]
.copy_from_slice(&rgba_data[src..src + row_bytes]);
}
return Ok((
PixelBuffer::U8(output),
PixelFormat::Rgba8,
width as usize,
height as usize,
));
}
current_frame += 1;
}
}
}
Err(FrameError::LoadError(format!(
"Frame {} not found in video",
frame_num
)))
}