//! FFmpeg wrapper use anyhow::{Context, Result}; use std::io::{BufReader, Read}; use std::path::Path; use std::process::{Child, ChildStdout, Command, Stdio}; #[derive(Debug, Clone)] pub struct VideoInfo { pub width: u32, pub height: u32, pub fps: f64, pub duration_ms: u64, pub frame_count: u64, pub codec: String, } pub struct FFmpegDecoder { path: String, process: Option, stdout: Option>, info: VideoInfo, } impl FFmpegDecoder { pub fn new(path: &Path) -> Result { let path_str = path.to_string_lossy().to_string(); let info = Self::probe(path)?; Ok(Self { path: path_str, process: None, stdout: None, info, }) } fn probe(path: &Path) -> Result { let output = Command::new("ffprobe") .args([ "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", path.to_str().unwrap_or(""), ]) .output() .context("Failed to run ffprobe")?; let json: serde_json::Value = serde_json::from_slice(&output.stdout).context("Failed to parse ffprobe output")?; let video_stream = json["streams"] .as_array() .and_then(|streams| streams.iter().find(|s| s["codec_type"] == "video")) .context("No video stream found")?; let width = video_stream["width"].as_u64().unwrap_or(0) as u32; let height = video_stream["height"].as_u64().unwrap_or(0) as u32; let fps_str = video_stream["r_frame_rate"].as_str().unwrap_or("30/1"); let (num, den) = { let parts: Vec<&str> = fps_str.split('/').collect(); if parts.len() == 2 { ( parts[0].parse::().unwrap_or(30.0), parts[1].parse::().unwrap_or(1.0), ) } else { (fps_str.parse::().unwrap_or(30.0), 1.0) } }; let fps = num / den; let duration_str = json["format"]["duration"].as_str().unwrap_or("0"); let duration_sec: f64 = duration_str.parse().unwrap_or(0.0); let duration_ms = (duration_sec * 1000.0) as u64; let frame_count = (duration_sec * fps) as u64; let codec = video_stream["codec_name"] .as_str() .unwrap_or("unknown") .to_string(); Ok(VideoInfo { width, height, fps, duration_ms, frame_count, codec, }) } pub fn get_info(&self) -> VideoInfo { self.info.clone() } pub fn start_decoding(&mut self, start_ms: u64) -> Result<()> { self.stop(); let start_sec = start_ms as f64 / 1000.0; let mut child = Command::new("ffmpeg") .args([ "-ss", &format!("{}", start_sec), "-i", &self.path, "-f", "rawvideo", "-pix_fmt", "rgb24", "-", ]) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .context("Failed to start ffmpeg")?; let stdout = child.stdout.take().context("Failed to capture stdout")?; self.process = Some(child); self.stdout = Some(BufReader::new(stdout)); Ok(()) } pub fn seek(&mut self, timestamp_ms: u64) -> Result<()> { self.start_decoding(timestamp_ms) } pub fn stop(&mut self) { if let Some(mut child) = self.process.take() { let _ = child.kill(); let _ = child.wait(); } self.stdout = None; } pub fn read_frame(&mut self) -> Result>> { let frame_size = (self.info.width * self.info.height * 3) as usize; let mut buffer = vec![0u8; frame_size]; if let Some(ref mut reader) = self.stdout { match reader.read_exact(&mut buffer) { Ok(_) => Ok(Some(buffer)), Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None), Err(e) => Err(e.into()), } } else { Ok(None) } } } impl Drop for FFmpegDecoder { fn drop(&mut self) { self.stop(); } }