use std::path::Path;
use std::process::Command;
use crate::error::{Error, Result};
pub const MAX_FRAMES: usize = 8;
pub fn extract_video_frames(data: &[u8]) -> Result<Vec<Vec<u8>>> {
let dir = std::env::temp_dir().join(format!(
"mlex-video-{}-{:x}",
std::process::id(),
data.as_ptr() as usize ^ data.len()
));
std::fs::create_dir_all(&dir)?;
let result = extract_in_dir(data, &dir);
let _ = std::fs::remove_dir_all(&dir);
result
}
fn extract_in_dir(data: &[u8], dir: &Path) -> Result<Vec<Vec<u8>>> {
let input = dir.join("input.bin");
std::fs::write(&input, data)?;
let duration = probe_duration(&input)?;
let fps = if duration > MAX_FRAMES as f64 {
MAX_FRAMES as f64 / duration
} else {
1.0
};
let pattern = dir.join("frame_%04d.png");
let output = Command::new("ffmpeg")
.args(["-nostdin", "-v", "error", "-i"])
.arg(&input)
.args(["-vf", &format!("fps={fps:.6}")])
.args(["-frames:v", &MAX_FRAMES.to_string()])
.arg(&pattern)
.output()
.map_err(|e| {
Error::Model(format!(
"failed to run ffmpeg for video decoding (is ffmpeg on PATH?): {e}"
))
})?;
if !output.status.success() {
return Err(Error::Model(format!(
"ffmpeg failed to extract video frames: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let mut frames = Vec::new();
for i in 1..=MAX_FRAMES {
let path = dir.join(format!("frame_{i:04}.png"));
if !path.exists() {
break;
}
frames.push(std::fs::read(&path)?);
}
if frames.is_empty() {
return Err(Error::Model(
"video produced no frames (empty or undecodable clip)".into(),
));
}
Ok(frames)
}
fn probe_duration(input: &Path) -> Result<f64> {
let output = Command::new("ffprobe")
.args([
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"csv=p=0",
])
.arg(input)
.output()
.map_err(|e| {
Error::Model(format!(
"failed to run ffprobe for video probing (is ffprobe on PATH?): {e}"
))
})?;
if !output.status.success() {
return Err(Error::Model(format!(
"ffprobe failed on video input: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(String::from_utf8_lossy(&output.stdout)
.trim()
.parse::<f64>()
.unwrap_or(0.0))
}