From 0c20fb86633104744dbccf30ad732296694fff1b Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Sun, 8 Feb 2026 12:44:10 +0100 Subject: Initial pipewire --- src/utils/mod.rs | 350 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 src/utils/mod.rs (limited to 'src/utils') diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..b89357e --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,350 @@ +//! Utility functions and types for the geek-szitman-supercamera crate + +use std::time::{Duration, Instant}; +use tracing::info; + +/// Signal handler for graceful shutdown +pub struct SignalHandler { + shutdown_requested: std::sync::Arc, +} + +impl SignalHandler { + /// Create a new signal handler + pub fn new() -> Result> { + let shutdown_requested = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let shutdown_clone = shutdown_requested.clone(); + + ctrlc::set_handler(move || { + info!("Received shutdown signal"); + shutdown_clone.store(true, std::sync::atomic::Ordering::SeqCst); + })?; + + Ok(Self { shutdown_requested }) + } + + /// Check if shutdown was requested + pub fn shutdown_requested(&self) -> bool { + self.shutdown_requested + .load(std::sync::atomic::Ordering::SeqCst) + } + + /// Wait for shutdown signal + pub fn wait_for_shutdown(&self) { + while !self.shutdown_requested() { + std::thread::sleep(Duration::from_millis(100)); + } + } + + /// Request shutdown programmatically + pub fn request_shutdown(&self) { + self.shutdown_requested + .store(true, std::sync::atomic::Ordering::SeqCst); + } + + /// Wait for shutdown signal with timeout. Returns true if shutdown was requested, false if timed out. + pub fn wait_for_shutdown_with_timeout(&self, timeout: Duration) -> bool { + let start = Instant::now(); + while !self.shutdown_requested() { + if start.elapsed() >= timeout { + return false; // timed out + } + std::thread::sleep(Duration::from_millis(100)); + } + true + } +} + +/// Performance metrics tracker +pub struct PerformanceTracker { + start_time: Instant, + frame_count: u64, + total_bytes: u64, + last_fps_update: Instant, + current_fps: f64, +} + +impl PerformanceTracker { + /// Create a new performance tracker + pub fn new() -> Self { + Self { + start_time: Instant::now(), + frame_count: 0, + total_bytes: 0, + last_fps_update: Instant::now(), + current_fps: 0.0, + } + } + + /// Record a frame + pub fn record_frame(&mut self, bytes: usize) { + self.frame_count += 1; + self.total_bytes += bytes as u64; + + // Update FPS every second + let now = Instant::now(); + if now.duration_since(self.last_fps_update) >= Duration::from_secs(1) { + let elapsed = now.duration_since(self.last_fps_update).as_secs_f64(); + self.current_fps = 1.0 / elapsed; + self.last_fps_update = now; + } + } + + /// Get current FPS + pub fn current_fps(&self) -> f64 { + self.current_fps + } + + /// Get total frame count + pub fn total_frames(&self) -> u64 { + self.frame_count + } + + /// Get total bytes processed + pub fn total_bytes(&self) -> u64 { + self.total_bytes + } + + /// Get average frame size + pub fn average_frame_size(&self) -> f64 { + if self.frame_count > 0 { + self.total_bytes as f64 / self.frame_count as f64 + } else { + 0.0 + } + } + + /// Get uptime + pub fn uptime(&self) -> Duration { + self.start_time.elapsed() + } + + /// Get performance summary + pub fn summary(&self) -> PerformanceSummary { + PerformanceSummary { + uptime: self.uptime(), + total_frames: self.total_frames(), + total_bytes: self.total_bytes(), + current_fps: self.current_fps(), + average_frame_size: self.average_frame_size(), + } + } +} + +impl Default for PerformanceTracker { + fn default() -> Self { + Self::new() + } +} + +/// Performance summary +#[derive(Debug, Clone)] +pub struct PerformanceSummary { + pub uptime: Duration, + pub total_frames: u64, + pub total_bytes: u64, + pub current_fps: f64, + pub average_frame_size: f64, +} + +impl PerformanceSummary { + /// Format uptime as human-readable string + pub fn uptime_formatted(&self) -> String { + let secs = self.uptime.as_secs(); + let hours = secs / 3600; + let minutes = (secs % 3600) / 60; + let seconds = secs % 60; + + if hours > 0 { + format!("{hours}h {minutes}m {seconds}s") + } else if minutes > 0 { + format!("{minutes}m {seconds}s") + } else { + format!("{seconds}s") + } + } + + /// Format bytes as human-readable string + pub fn bytes_formatted(&self) -> String { + const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"]; + let mut size = self.total_bytes as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < UNITS.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + format!("{:.1} {}", size, UNITS[unit_index]) + } +} + +/// Configuration file loader +pub struct ConfigLoader; + +impl ConfigLoader { + /// Load configuration from file + pub fn load_from_file(path: &str) -> Result> + where + T: serde::de::DeserializeOwned, + { + let content = std::fs::read_to_string(path)?; + let config: T = serde_json::from_str(&content)?; + Ok(config) + } + + /// Save configuration to file + pub fn save_to_file(path: &str, config: &T) -> Result<(), Box> + where + T: serde::Serialize, + { + let content = serde_json::to_string_pretty(config)?; + std::fs::write(path, content)?; + Ok(()) + } + + /// Load configuration with fallback to defaults + pub fn load_with_defaults(path: &str) -> T + where + T: serde::de::DeserializeOwned + Default, + { + Self::load_from_file(path).unwrap_or_default() + } +} + +/// File utilities +pub struct FileUtils; + +impl FileUtils { + /// Ensure directory exists + pub fn ensure_dir(path: &str) -> Result<(), Box> { + std::fs::create_dir_all(path)?; + Ok(()) + } + + /// Get file size + pub fn get_file_size(path: &str) -> Result> { + let metadata = std::fs::metadata(path)?; + Ok(metadata.len()) + } + + /// Check if file exists + pub fn file_exists(path: &str) -> bool { + std::path::Path::new(path).exists() + } + + /// Get file extension + pub fn get_extension(path: &str) -> Option { + std::path::Path::new(path) + .extension() + .and_then(|ext| ext.to_str()) + .map(|s| s.to_string()) + } +} + +/// Time utilities +pub struct TimeUtils; + +impl TimeUtils { + /// Format duration as human-readable string + pub fn format_duration(duration: Duration) -> String { + let secs = duration.as_secs(); + let millis = duration.subsec_millis(); + + if secs > 0 { + format!("{secs}.{millis:03}s") + } else { + format!("{millis}ms") + } + } + + /// Sleep with progress callback + pub fn sleep_with_progress(duration: Duration, mut progress_callback: F) + where + F: FnMut(f64), + { + let start = Instant::now(); + let total_duration = duration.as_millis() as f64; + + while start.elapsed() < duration { + let elapsed = start.elapsed().as_millis() as f64; + let progress = (elapsed / total_duration).min(1.0); + progress_callback(progress); + + std::thread::sleep(Duration::from_millis(10)); + } + + progress_callback(1.0); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_performance_tracker() { + let mut tracker = PerformanceTracker::new(); + + // Record some frames + tracker.record_frame(1024); + tracker.record_frame(2048); + tracker.record_frame(1536); + + assert_eq!(tracker.total_frames(), 3); + assert_eq!(tracker.total_bytes(), 4608); + assert_eq!(tracker.average_frame_size(), 1536.0); + } + + #[test] + fn test_performance_summary() { + let mut tracker = PerformanceTracker::new(); + tracker.record_frame(1024); + + let summary = tracker.summary(); + assert_eq!(summary.total_frames, 1); + assert_eq!(summary.total_bytes, 1024); + assert_eq!(summary.average_frame_size, 1024.0); + + // Test formatting + assert!(summary.uptime_formatted().contains("s")); + assert_eq!(summary.bytes_formatted(), "1.0 KB"); + } + + #[test] + fn test_file_utils() { + // Test file existence check + assert!(FileUtils::file_exists("src/utils/mod.rs")); + assert!(!FileUtils::file_exists("nonexistent_file.txt")); + + // Test extension extraction + assert_eq!( + FileUtils::get_extension("test.txt"), + Some("txt".to_string()) + ); + assert_eq!(FileUtils::get_extension("no_extension"), None); + } + + #[test] + fn test_time_utils() { + let duration = Duration::from_millis(1500); + let formatted = TimeUtils::format_duration(duration); + assert!(formatted.contains("1.500s")); + + let short_duration = Duration::from_millis(500); + let formatted = TimeUtils::format_duration(short_duration); + assert!(formatted.contains("500ms")); + } + + #[test] + fn test_sleep_with_progress() { + let mut progress_values = Vec::new(); + let duration = Duration::from_millis(100); + + TimeUtils::sleep_with_progress(duration, |progress| { + progress_values.push(progress); + }); + + assert!(!progress_values.is_empty()); + assert!(progress_values.last().unwrap() >= &1.0); + } +} -- cgit v1.2.3