use std::error::Error;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq)]
pub enum BurstFormat {
Gif,
FrameSequence,
}
#[derive(Debug, Clone)]
pub struct BurstConfig {
pub frame_count: u32,
pub interval_ms: u32,
pub output_format: BurstFormat,
pub output_dir: PathBuf,
pub max_captures: usize,
pub max_bytes: u64,
}
impl Default for BurstConfig {
fn default() -> Self {
Self {
frame_count: 1,
interval_ms: 100,
output_format: BurstFormat::FrameSequence,
output_dir: PathBuf::from("screenshots"),
max_captures: 50,
max_bytes: 100 * 1024 * 1024,
}
}
}
pub struct BurstResult {
pub output_path: PathBuf,
pub frame_paths: Vec<PathBuf>,
pub timestamp: String,
pub frame_count: u32,
pub total_bytes: u64,
}
fn validate_config(config: &BurstConfig) -> Result<(), Box<dyn Error>> {
if config.frame_count < 1 || config.frame_count > 100 {
return Err(format!(
"frame_count must be 1-100, got {}",
config.frame_count
).into());
}
if config.interval_ms < 10 {
return Err(format!(
"interval_ms must be >= 10, got {}",
config.interval_ms
).into());
}
Ok(())
}
pub type CaptureFn = dyn Fn(&Path) -> Result<(), Box<dyn Error>>;
pub fn capture_burst(config: &BurstConfig) -> Result<BurstResult, Box<dyn Error>> {
capture_burst_with_capturer(config, &default_capture_fn)
}
pub fn capture_burst_with_capturer(
config: &BurstConfig,
capture_fn: &CaptureFn,
) -> Result<BurstResult, Box<dyn Error>> {
validate_config(config)?;
std::fs::create_dir_all(&config.output_dir)?;
let now = chrono_timestamp();
let mut frame_paths: Vec<PathBuf> = Vec::new();
let mut total_bytes: u64 = 0;
for i in 0..config.frame_count {
let filename = format!("burst_{}_{:03}.bmp", now, i + 1);
let path = config.output_dir.join(&filename);
match capture_fn(&path) {
Ok(()) => {
if let Ok(meta) = std::fs::metadata(&path) {
total_bytes += meta.len();
}
frame_paths.push(path);
}
Err(_) => {
break;
}
}
if i + 1 < config.frame_count {
std::thread::sleep(std::time::Duration::from_millis(
config.interval_ms as u64,
));
}
}
let frame_count = frame_paths.len() as u32;
let output_path = config.output_dir.clone();
Ok(BurstResult {
output_path,
frame_paths,
timestamp: now,
frame_count,
total_bytes,
})
}
fn chrono_timestamp() -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let days = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let (year, month, day) = days_to_ymd(days);
format!(
"{:04}{:02}{:02}_{:02}{:02}{:02}",
year, month, day, hours, minutes, seconds
)
}
fn days_to_ymd(days_since_epoch: u64) -> (u32, u32, u32) {
let z = days_since_epoch as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as u32, m as u32, d as u32)
}
fn default_capture_fn(path: &Path) -> Result<(), Box<dyn Error>> {
#[cfg(target_os = "windows")]
{
let _ = path;
Err("DXGI capture not yet wired in OSS crate".into())
}
#[cfg(not(target_os = "windows"))]
{
let _ = path;
Err("capture not supported on this platform".into())
}
}