- Read frames from reverse_end_ms instead of reverse_start_ms - Reverse buffer now displays correct video content (130s instead of 139s) - Maintain forward playback compatibility
357 lines
10 KiB
Rust
357 lines
10 KiB
Rust
//! 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<Child>,
|
|
stdout: Option<BufReader<ChildStdout>>,
|
|
info: VideoInfo,
|
|
frame_buffer: VecDeque<Vec<u8>>,
|
|
frame_times: VecDeque<u64>,
|
|
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<Self> {
|
|
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<VideoInfo> {
|
|
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::<f64>().unwrap_or(30.0),
|
|
parts[1].parse::<f64>().unwrap_or(1.0),
|
|
)
|
|
} else {
|
|
(fps_str.parse::<f64>().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<Option<Vec<u8>>> {
|
|
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<u8>> = Vec::new();
|
|
let mut temp_times: Vec<u64> = 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<u8>> = Vec::new();
|
|
let mut temp_times: Vec<u64> = 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<Vec<u8>> {
|
|
self.frame_buffer.pop_front()
|
|
}
|
|
|
|
pub fn get_frame_time(&mut self) -> Option<u64> {
|
|
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();
|
|
}
|
|
}
|