//! FFmpeg wrapper use anyhow::{Context, Result}; use std::collections::VecDeque; 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, frame_buffer: VecDeque>, frame_times: VecDeque, buffer_loaded: bool, current_read_ms: u64, display_time_ms: u64, reverse_start_ms: u64, reverse_end_ms: u64, in_reverse_mode: bool, } 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, frame_buffer: VecDeque::new(), frame_times: VecDeque::new(), buffer_loaded: false, current_read_ms: 0, display_time_ms: 0, reverse_start_ms: 0, reverse_end_ms: 0, in_reverse_mode: false, }) } 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)); self.current_read_ms = start_ms; Ok(()) } pub fn seek(&mut self, timestamp_ms: u64) -> Result<()> { self.in_reverse_mode = false; self.frame_buffer.clear(); self.current_read_ms = timestamp_ms; self.start_decoding(timestamp_ms)?; let frame_size = (self.info.width * self.info.height * 3) as usize; for _ in 0..5 { let mut dummy = vec![0u8; frame_size]; if let Some(ref mut reader) = self.stdout { if reader.read_exact(&mut dummy).is_err() { break; } } } Ok(()) } pub fn enable_reverse(&mut self, start_ms: u64, end_ms: u64) { self.stop(); self.frame_buffer.clear(); self.frame_times.clear(); self.buffer_loaded = false; self.reverse_start_ms = start_ms; self.reverse_end_ms = end_ms; self.current_read_ms = end_ms; self.display_time_ms = start_ms; self.in_reverse_mode = true; self.start_decoding(end_ms).ok(); } 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; if let Some(frame) = self.frame_buffer.pop_front() { if self.in_reverse_mode && self.frame_buffer.len() < 300 { let _ = self.refill_reverse_buffer(600); } return Ok(Some(frame)); } if self.in_reverse_mode { if self.current_read_ms <= self.reverse_end_ms { return Ok(None); } let _ = self.refill_reverse_buffer(30); if let Some(frame) = self.frame_buffer.pop_front() { return Ok(Some(frame)); } return Ok(None); } let mut buffer = vec![0u8; frame_size]; if let Some(ref mut reader) = self.stdout { match reader.read_exact(&mut buffer) { Ok(_) => { self.current_read_ms += 16; Ok(Some(buffer)) } Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None), Err(e) => Err(e.into()), } } else { Ok(None) } } pub fn fill_reverse_buffer(&mut self, num_frames: usize) -> Result<()> { let frame_size = (self.info.width * self.info.height * 3) as usize; self.frame_buffer.clear(); self.frame_times.clear(); let frame_duration_ms = (1000.0 / self.info.fps) as u64; let total_range = self.reverse_start_ms.saturating_sub(self.reverse_end_ms); let max_frames = (total_range / frame_duration_ms).max(1) as usize; let frames_to_read = max_frames.min(num_frames); let mut temp_frames: Vec> = Vec::new(); let mut temp_times: Vec = Vec::new(); for _ in 0..frames_to_read { let mut buffer = vec![0u8; frame_size]; if let Some(ref mut reader) = self.stdout { match reader.read_exact(&mut buffer) { Ok(_) => { temp_frames.push(buffer); temp_times.push(self.current_read_ms); self.current_read_ms += frame_duration_ms; } Err(_) => break, } } else { break; } } for i in (0..temp_frames.len()).rev() { self.frame_buffer.push_back(temp_frames.remove(i)); self.frame_times.push_back(temp_times.remove(i)); } Ok(()) } pub fn refill_reverse_buffer(&mut self, _num_frames: usize) -> Result<()> { if !self.in_reverse_mode { return Ok(()); } self.frame_buffer.clear(); self.frame_times.clear(); self.current_read_ms = self.reverse_end_ms; self.display_time_ms = self.reverse_start_ms; let frame_duration_ms = (1000.0 / self.info.fps) as u64; let frames_to_read = ((self.reverse_start_ms - self.reverse_end_ms) / frame_duration_ms).max(1) as usize; self.stop(); self.start_decoding(self.reverse_end_ms)?; let frame_size = (self.info.width * self.info.height * 3) as usize; let mut temp_frames: Vec> = Vec::new(); let mut temp_times: Vec = Vec::new(); for _ in 0..frames_to_read { let mut buffer = vec![0u8; frame_size]; if let Some(ref mut reader) = self.stdout { match reader.read_exact(&mut buffer) { Ok(_) => { temp_frames.push(buffer); temp_times.push(self.current_read_ms); self.current_read_ms += frame_duration_ms; } Err(_) => break, } } else { break; } } for i in (0..temp_frames.len()).rev() { self.frame_buffer.push_back(temp_frames.remove(i)); self.frame_times.push_back(temp_times.remove(i)); } Ok(()) } pub fn has_frames_in_buffer(&self) -> bool { !self.frame_buffer.is_empty() } pub fn buffer_size(&self) -> usize { self.frame_buffer.len() } pub fn get_buffer_front(&mut self) -> Option> { self.frame_buffer.pop_front() } pub fn get_frame_time(&mut self) -> Option { if self.in_reverse_mode { let time = self.display_time_ms; self.display_time_ms = self .display_time_ms .saturating_sub((1000.0 / self.info.fps) as u64); Some(time) } else { self.frame_times.pop_front() } } pub fn needs_loading(&self) -> bool { self.in_reverse_mode && self.frame_buffer.len() < 300 } pub fn get_current_time_ms(&self) -> u64 { self.current_read_ms } pub fn set_current_time_ms(&mut self, ms: u64) { self.current_read_ms = ms; } pub fn get_reverse_end_ms(&self) -> u64 { self.reverse_end_ms } pub fn is_at_boundary(&self) -> bool { self.in_reverse_mode && self.current_read_ms <= self.reverse_end_ms } } impl Drop for FFmpegDecoder { fn drop(&mut self) { self.stop(); } }