use anyhow::{anyhow, Context, Result};
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, error};
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command as ShellCommand;
use std::process::Stdio;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
use std::time::Duration;
pub fn extract_single_frame<P: AsRef<Path>>(
video: P,
duration_ms: u64,
output_path: PathBuf,
running: Arc<AtomicBool>,
) -> Result<()> {
let pb = ProgressBar::new(1);
let style = ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}",
)
.context("Failed to set progress bar template")?;
pb.set_style(style);
debug!("Starting to extract a single frame from the middle of the video...");
if !running.load(Ordering::SeqCst) {
pb.finish_and_clear();
return Err(anyhow!("Extraction interrupted before starting."));
}
if duration_ms == 0 {
pb.finish_and_clear();
return Err(anyhow!("Failed to determine video length."));
}
let middle_timestamp_ms = duration_ms / 2;
debug!("Calculated middle timestamp (ms): {}", middle_timestamp_ms);
if !running.load(Ordering::SeqCst) {
pb.finish_and_clear();
return Err(anyhow!(
"Extraction interrupted before extracting the frame."
));
}
let middle_timestamp_seconds = middle_timestamp_ms as f64 / 1000.0;
let output_is_file = output_path.is_file();
let temp_output_path = if output_is_file {
let parent = output_path.parent().unwrap_or_else(|| Path::new("."));
let filename = output_path
.file_name()
.ok_or_else(|| anyhow!("Invalid file name"))?;
let filename_str = filename.to_string_lossy();
let formatted_filename = if filename_str.to_lowercase().ends_with(".png") {
let base = &filename_str[..filename_str.len() - 4];
format!("{}%04d.png", base)
} else {
format!("{}%04d.png", filename_str)
};
parent.join(formatted_filename)
} else {
output_path.join("sample_frame%04d.png")
};
let video_str = video
.as_ref()
.to_str()
.ok_or_else(|| anyhow!("Invalid video file path"))?;
let temp_output_str = temp_output_path
.to_str()
.ok_or_else(|| anyhow!("Invalid output file path"))?;
extract_frame(
video_str,
middle_timestamp_seconds,
temp_output_str,
running.clone(),
)
.with_context(|| {
format!(
"Failed to extract frame at {:.3} seconds from the video.",
middle_timestamp_seconds
)
})?;
pb.inc(1);
if output_is_file {
let extracted_file = temp_output_path.with_file_name(format!(
"{}0001.png",
output_path.file_stem().unwrap().to_string_lossy()
));
std::fs::rename(&extracted_file, &output_path).with_context(|| {
format!(
"Failed to rename {} to {}",
extracted_file.display(),
output_path.display()
)
})?;
}
if running.load(Ordering::SeqCst) {
debug!(
"Successfully extracted frame at {:.3} seconds as {:?}",
middle_timestamp_seconds, output_path
);
} else {
pb.finish_and_clear();
return Err(anyhow!("Extraction was interrupted midway."));
}
pb.finish();
Ok(())
}
pub fn extract_multiple_frames(
video: &Path,
duration_ms: u64,
num_frames: usize,
output_dir: &Path,
running: Arc<AtomicBool>,
) -> Result<()> {
log::debug!("Starting to extract multiple frames from the video...");
if !output_dir.exists() {
fs::create_dir_all(output_dir)
.with_context(|| format!("Failed to create output directory: {:?}", output_dir))?;
}
if !running.load(Ordering::SeqCst) {
return Err(anyhow!("Extraction interrupted before starting."));
}
if duration_ms == 0 {
return Err(anyhow!("Failed to determine video length."));
}
let frame_interval_ms = duration_ms / (num_frames as u64 + 1);
let video_str = video
.to_str()
.ok_or_else(|| anyhow!("Invalid video path"))?;
let pb = ProgressBar::new(num_frames as u64);
let style = ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}",
)
.context("Failed to set progress bar template")?;
pb.set_style(style);
for i in 0..num_frames {
if !running.load(Ordering::SeqCst) {
pb.finish_and_clear();
return Err(anyhow!("Extraction interrupted during frame extraction."));
}
let timestamp_ms = frame_interval_ms * (i as u64 + 1);
debug!("Extracting frame {} at {} ms", i + 1, timestamp_ms);
let output_file_path = output_dir.join(format!("sample_frame_{}.png", i + 1));
debug!("Output file set to: {:?}", output_file_path);
let timestamp_seconds = timestamp_ms as f64 / 1000.0;
extract_frame(
video_str,
timestamp_seconds,
output_file_path
.to_str()
.ok_or_else(|| anyhow!("Invalid output file path"))?,
running.clone(),
)
.with_context(|| {
format!(
"Failed to extract frame at {:.3} seconds from the video.",
timestamp_seconds
)
})?;
pb.inc(1);
}
pb.finish();
if running.load(Ordering::SeqCst) {
debug!("Successfully extracted {} frames.", num_frames);
} else {
return Err(anyhow!("Extraction was interrupted midway."));
}
Ok(())
}
fn extract_frame(
video: &str,
timestamp_seconds: f64,
output: &str,
running: Arc<AtomicBool>,
) -> Result<()> {
debug!(
"Attempting to extract frame at {:.3} seconds from video '{}' to '{}'",
timestamp_seconds, video, output
);
let ffmpeg_command = format!(
"ffmpeg -i {} -ss {:.3} -frames:v 1 {} -y",
video, timestamp_seconds, output
);
debug!("Final ffmpeg command: {}", ffmpeg_command);
let mut child = ShellCommand::new("ffmpeg")
.arg("-i")
.arg(video)
.arg("-ss")
.arg(format!("{:.3}", timestamp_seconds)) .arg("-frames:v")
.arg("1") .arg(output) .arg("-y") .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn()
.with_context(|| {
format!(
"Failed to start ffmpeg process for frame extraction at {:.3} seconds",
timestamp_seconds
)
})?;
debug!("FFmpeg process spawned with PID: {:?}", child.id());
while running.load(Ordering::SeqCst) {
if let Ok(Some(status)) = child.try_wait() {
if status.success() {
debug!("Frame extracted successfully to {}", output);
return Ok(());
} else {
return Err(anyhow!("FFmpeg command failed with status: {}", status));
}
}
thread::sleep(Duration::from_millis(100));
}
error!("Interrupt signal received, terminating FFmpeg process...");
if let Err(e) = child.kill() {
return Err(anyhow!("Failed to kill FFmpeg process: {}", e));
}
if let Err(e) = child.wait() {
return Err(anyhow!(
"Failed to wait for FFmpeg process to terminate: {}",
e
));
}
Err(anyhow!("Extraction interrupted before completion"))
}