Files
momentry_core/src/core/frame_cache.rs

139 lines
3.9 KiB
Rust

use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use tracing::info;
/// A single extracted frame with metadata
#[derive(Debug, Clone)]
pub struct CachedFrame {
pub path: PathBuf,
pub frame_number: u64,
pub timestamp_secs: f64,
}
/// Manages shared frame extraction for concurrent processors
pub struct FrameManager {
pub dir: PathBuf,
pub frames: Vec<CachedFrame>,
pub fps: f64,
pub total_frames: u64,
pub duration_secs: f64,
}
impl FrameManager {
/// Extract frames from video at `sample_interval` into a temp directory.
pub async fn extract(
video_path: &str,
sample_interval: u32,
fps: f64,
total_frames: u64,
) -> Result<Self> {
let dir = std::env::temp_dir().join(format!("frames_{}", uuid_from_path(video_path)));
let _ = std::fs::create_dir_all(&dir);
let pattern = dir.join("frame_%05d.jpg").to_string_lossy().to_string();
let video_path = video_path.to_owned();
info!(
"[FrameCache] Extracting frames (interval={}) to {:?}",
sample_interval, dir
);
let output = tokio::process::Command::new("ffmpeg")
.args([
"-y",
"-v",
"quiet",
"-i",
&video_path,
"-vf",
&format!("select=not(mod(n\\,{})),scale=320:-2", sample_interval),
"-vsync",
"vfr",
"-q:v",
"15",
&pattern,
])
.output()
.await
.context("Frame extraction via ffmpeg failed")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("ffmpeg frame extraction failed: {}", stderr);
}
// Read extracted frames
let mut frames: Vec<CachedFrame> = Vec::new();
let mut entries: Vec<_> = std::fs::read_dir(&dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "jpg"))
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in &entries {
let fname = entry.file_name();
let fname_str = fname.to_string_lossy();
if let Some(num_str) = fname_str
.strip_prefix("frame_")
.and_then(|s| s.strip_suffix(".jpg"))
{
if let Ok(frame_num) = num_str.parse::<u64>() {
let timestamp = frame_num as f64 / fps;
frames.push(CachedFrame {
path: entry.path(),
frame_number: frame_num,
timestamp_secs: timestamp,
});
}
}
}
let duration_secs = if fps > 0.0 {
total_frames as f64 / fps
} else {
0.0
};
info!(
"[FrameCache] Extracted {} frames to {:?}",
frames.len(),
dir
);
Ok(FrameManager {
dir,
frames,
fps,
total_frames,
duration_secs,
})
}
/// Clean up the extracted frame files
pub fn cleanup(&self) {
let _ = std::fs::remove_dir_all(&self.dir);
info!("[FrameCache] Cleaned up {:?}", self.dir);
}
/// Get a frame by index
pub fn get_frame(&self, index: usize) -> Option<&CachedFrame> {
self.frames.get(index)
}
/// Number of extracted frames
pub fn len(&self) -> usize {
self.frames.len()
}
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
}
fn uuid_from_path(path: &str) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
path.hash(&mut hasher);
format!("{:x}", hasher.finish())
}