diff --git a/install.sh b/install.sh index b46a21a..6348bed 100755 --- a/install.sh +++ b/install.sh @@ -25,9 +25,22 @@ ln -sf "$BIN_DIR/scythe-daemon" "$BIN_DIR/vrec-daemon" ln -sf "$BIN_DIR/scythe-ui" "$BIN_DIR/vrec-ui" ln -sf "$BIN_DIR/scythe-ui" "$BIN_DIR/vrec" -if pgrep -x scythe-daemon > /dev/null; then +SYSTEMD_USER_DIR="$HOME/.config/systemd/user" +if [ -f "$SYSTEMD_USER_DIR/vrec-daemon.service" ] || systemctl --user is-active --quiet vrec-daemon.service 2>/dev/null; then + echo "Migrating legacy vrec-daemon systemd service to scythe-daemon.service..." + systemctl --user stop vrec-daemon.service 2>/dev/null || true + systemctl --user disable vrec-daemon.service 2>/dev/null || true + rm -f "$SYSTEMD_USER_DIR/vrec-daemon.service" + rm -f "$SYSTEMD_USER_DIR/default.target.wants/vrec-daemon.service" +fi + +if systemctl --user is-active --quiet scythe-daemon.service 2>/dev/null; then + echo "Restarting active scythe-daemon systemd service..." + systemctl --user restart scythe-daemon.service +elif pgrep -x scythe-daemon > /dev/null || pgrep -x vrec-daemon > /dev/null; then echo "Restarting active scythe-daemon engine..." - pkill -x scythe-daemon || true + pkill -x scythe-daemon 2>/dev/null || true + pkill -x vrec-daemon 2>/dev/null || true sleep 0.5 nohup "$BIN_DIR/scythe-daemon" > /dev/null 2>&1 & disown diff --git a/src/bin/scythe-daemon.rs b/src/bin/scythe-daemon.rs index e66c52b..c994d23 100644 --- a/src/bin/scythe-daemon.rs +++ b/src/bin/scythe-daemon.rs @@ -488,6 +488,7 @@ async fn main() -> Result<(), Box> { let mut ticker = crossbeam_channel::tick(std::time::Duration::from_nanos(1_000_000_000 / config.fps.max(1) as u64)); let mut latest_frame: Option = None; let mut has_new_frame = false; + let mut last_encoded_raw_ptr: usize = 0; loop { crossbeam_channel::select! { @@ -671,24 +672,44 @@ async fn main() -> Result<(), Box> { recv(ticker) -> _ => { while ticker.try_recv().is_ok() {} + if !normal_recording && !config.replay_enabled { + has_new_frame = false; + continue; + } + let now = std::time::Instant::now(); let elapsed = now.duration_since(stream_start); let raw_pts = (elapsed.as_secs_f64() * config.fps as f64).round() as i64; let pts = if raw_pts > last_video_pts { raw_pts } else { last_video_pts + 1 }; last_video_pts = pts; - let packets_res = if has_new_frame { + let is_same_raw_frame = match &latest_frame { + Some(Frame::Raw { data, .. }) => { + let ptr = Arc::as_ptr(data) as usize; + ptr != 0 && ptr == last_encoded_raw_ptr + } + _ => false, + }; + + let packets_res = if has_new_frame && !is_same_raw_frame { has_new_frame = false; if let Some(ref f) = latest_frame { + if let Frame::Raw { data, .. } = f { + last_encoded_raw_ptr = Arc::as_ptr(data) as usize; + } encoder.encode_frame(f, pts) } else { Ok(Vec::new()) } } else { + has_new_frame = false; match encoder.encode_cached_frame(pts) { Ok(pkts) => Ok(pkts), Err(_) => { if let Some(ref f) = latest_frame { + if let Frame::Raw { data, .. } = f { + last_encoded_raw_ptr = Arc::as_ptr(data) as usize; + } encoder.encode_frame(f, pts) } else { Ok(Vec::new()) diff --git a/src/capture/mock.rs b/src/capture/mock.rs index 4bb6e30..3f0a28f 100644 --- a/src/capture/mock.rs +++ b/src/capture/mock.rs @@ -1,15 +1,32 @@ use super::{Frame, FrameSource}; use std::time::Duration; +use std::sync::Arc; + pub struct MockCapture { width: u32, height: u32, frame_count: u64, + static_frame: Arc>, } impl MockCapture { pub fn new() -> Self { - Self { width: 1920, height: 1080, frame_count: 0 } + let width = 1920; + let height = 1080; + let mut data = vec![0u8; (width * height * 4) as usize]; + for chunk in data.chunks_exact_mut(4) { + chunk[0] = 64; // B + chunk[1] = 64; // G + chunk[2] = 64; // R + chunk[3] = 255; // A + } + Self { + width, + height, + frame_count: 0, + static_frame: Arc::new(data), + } } } @@ -22,24 +39,13 @@ impl Default for MockCapture { impl FrameSource for MockCapture { fn next_frame(&mut self) -> Result> { std::thread::sleep(Duration::from_millis(16)); // ~60fps - let mut data = vec![0u8; (self.width * self.height * 4) as usize]; - - // Fill with a changing color - let color = (self.frame_count % 255) as u8; - for chunk in data.chunks_exact_mut(4) { - chunk[0] = color; // B - chunk[1] = color; // G - chunk[2] = 255; // R - chunk[3] = 255; // A - } - self.frame_count += 1; Ok(Frame::Raw { width: self.width, height: self.height, stride: self.width * 4, - data: std::sync::Arc::new(data), + data: Arc::clone(&self.static_frame), }) } } diff --git a/src/config.rs b/src/config.rs index 47c6d3f..347f919 100644 --- a/src/config.rs +++ b/src/config.rs @@ -147,6 +147,22 @@ impl ScytheConfig { let mut p = Self::expand_tilde(&self.output_directory); let _ = fs::create_dir_all(&p); p.push(filename); + + if !p.exists() { + return p.to_string_lossy().to_string(); + } + + let stem = p.file_stem().and_then(|s| s.to_str()).unwrap_or("recording"); + let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("mp4"); + let parent = p.parent().unwrap_or_else(|| std::path::Path::new(".")); + + for counter in 1..10000 { + let candidate = parent.join(format!("{}_{}.{}", stem, counter, ext)); + if !candidate.exists() { + return candidate.to_string_lossy().to_string(); + } + } + p.to_string_lossy().to_string() } @@ -361,4 +377,26 @@ mod tests { assert!(part.chars().all(|c| c.is_ascii_digit())); } } + + #[test] + fn test_resolve_save_path_collision() { + let temp_dir = std::env::temp_dir().join(format!("scythe_test_collision_{}", std::process::id())); + let cfg = ScytheConfig { + output_directory: temp_dir.to_string_lossy().to_string(), + ..Default::default() + }; + + let path1 = cfg.resolve_save_path("test_clip.mp4"); + assert!(path1.ends_with("test_clip.mp4")); + fs::write(&path1, b"dummy").unwrap(); + + let path2 = cfg.resolve_save_path("test_clip.mp4"); + assert!(path2.ends_with("test_clip_1.mp4"), "Expected collision to append _1: {}", path2); + fs::write(&path2, b"dummy2").unwrap(); + + let path3 = cfg.resolve_save_path("test_clip.mp4"); + assert!(path3.ends_with("test_clip_2.mp4"), "Expected collision to append _2: {}", path3); + + let _ = fs::remove_dir_all(temp_dir); + } }