diff --git a/record_1788315688.mp4 b/record_1788315688.mp4 new file mode 100644 index 0000000..53f4651 Binary files /dev/null and b/record_1788315688.mp4 differ diff --git a/record_1788315823.mp4 b/record_1788315823.mp4 new file mode 100644 index 0000000..17e5e5f Binary files /dev/null and b/record_1788315823.mp4 differ diff --git a/record_1788315881.mp4 b/record_1788315881.mp4 new file mode 100644 index 0000000..32fa41a Binary files /dev/null and b/record_1788315881.mp4 differ diff --git a/replay_1788315659.mp4 b/replay_1788315659.mp4 new file mode 100644 index 0000000..1f241f3 Binary files /dev/null and b/replay_1788315659.mp4 differ diff --git a/scratch/muxer_fix.patch b/scratch/muxer_fix.patch new file mode 100644 index 0000000..83d06bc --- /dev/null +++ b/scratch/muxer_fix.patch @@ -0,0 +1,60 @@ +--- src/bin/vrec-daemon.rs ++++ src/bin/vrec-daemon.rs +@@ -62,6 +62,8 @@ + let mut config = vrec::config::VrecConfig::load(); + let mut ring = HeapRb::::new((config.replay_duration_sec * 60).max(60) as usize); + let mut normal_muxer: Option = None; ++ let mut normal_recording = false; ++ let mut normal_waiting_keyframe = false; + + thread::spawn(move || { + while let Ok(drain) = mux_rx.recv() { +@@ -108,16 +110,14 @@ + } + }, + Command::StartRecording => { +- if config.record_enabled && normal_muxer.is_none() { +- let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; +- let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); +- let name = format!("record_{}.mp4", ts); +- normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() }; +- println!("Started normal recording to {}", name); ++ if config.record_enabled && !normal_recording { ++ normal_recording = true; ++ normal_waiting_keyframe = true; ++ println!("StartRecording requested, waiting for keyframe..."); + } + }, + Command::StopRecording => { +- if let Some(mut m) = normal_muxer.take() { ++ if let Some(mut m) = normal_muxer.take() { + let _ = m.finalize(); + println!("Stopped normal recording."); + } ++ normal_recording = false; + }, + _ => {} + } +@@ -129,7 +129,19 @@ + ring.push_overwrite(pkt.clone()); + } +- if let Some(muxer) = normal_muxer.as_mut() { +- let _ = muxer.write_packet(&pkt); ++ if normal_recording { ++ if normal_waiting_keyframe { ++ if pkt.is_keyframe() { ++ normal_waiting_keyframe = false; ++ let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; ++ let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); ++ let name = format!("record_{}.mp4", ts); ++ normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() }; ++ println!("Started normal recording to {}", name); ++ } ++ } ++ if !normal_waiting_keyframe { ++ if let Some(muxer) = normal_muxer.as_mut() { ++ let _ = muxer.write_packet(&pkt); ++ } ++ } + } + } diff --git a/send_ipc.py b/send_ipc.py new file mode 100644 index 0000000..c0b6f4c --- /dev/null +++ b/send_ipc.py @@ -0,0 +1,14 @@ +import socket +import json +import struct +import os + +def send(cmd): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/vrec.sock") + payload = json.dumps(cmd).encode('utf-8') + sock.sendall(struct.pack(' Result<(), Box> { let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string()); println!("Detected session type: {}", session_type); - let mut source: Box = if session_type.to_lowercase() == "wayland" { + let mut source: Box = if std::env::args().any(|a| a == "--mock") { + println!("Initializing MOCK capture..."); + Box::new(vrec::capture::mock::MockCapture::new()) + } else if session_type.to_lowercase() == "wayland" { println!("Initializing Wayland capture..."); Box::new(vrec::capture::wayland::WaylandCapture::new().await?) } else { @@ -60,6 +63,8 @@ async fn main() -> Result<(), Box> { let mut config = vrec::config::VrecConfig::load(); let mut ring = HeapRb::::new((config.replay_duration_sec * 60).max(60) as usize); let mut normal_muxer: Option = None; + let mut normal_recording = false; + let mut normal_waiting_keyframe = false; thread::spawn(move || { while let Ok(drain) = mux_rx.recv() { @@ -107,12 +112,10 @@ async fn main() -> Result<(), Box> { } }, Command::StartRecording => { - if config.record_enabled && normal_muxer.is_none() { - let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; - let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); - let name = format!("record_{}.mp4", ts); - normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() }; - println!("Started normal recording to {}", name); + if config.record_enabled && !normal_recording { + normal_recording = true; + normal_waiting_keyframe = true; + println!("StartRecording requested, waiting for keyframe..."); } }, Command::StopRecording => { @@ -120,6 +123,7 @@ async fn main() -> Result<(), Box> { let _ = m.finalize(); println!("Stopped normal recording."); } + normal_recording = false; }, _ => {} } @@ -130,8 +134,23 @@ async fn main() -> Result<(), Box> { if config.replay_enabled { ring.push_overwrite(pkt.clone()); } - if let Some(muxer) = normal_muxer.as_mut() { - let _ = muxer.write_packet(&pkt); + if normal_recording { + if normal_waiting_keyframe { + if pkt.is_keyframe() { + normal_waiting_keyframe = false; + let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; + let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + let name = format!("record_{}.mp4", ts); + normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() }; + println!("Started normal recording to {}", name); + } + } + + if !normal_waiting_keyframe { + if let Some(muxer) = normal_muxer.as_mut() { + let _ = muxer.write_packet(&pkt); + } + } } } } diff --git a/src/bin/vrec-daemon.rs.orig b/src/bin/vrec-daemon.rs.orig new file mode 100644 index 0000000..2ad3359 --- /dev/null +++ b/src/bin/vrec-daemon.rs.orig @@ -0,0 +1,176 @@ +use vrec::capture::{Frame, FrameSource}; +use vrec::encoder::VaapiEncoder; +use vrec::ring::Packet; +use vrec::muxer::Muxer; +use vrec::ipc::Command; +use ringbuf::HeapRb; +use ringbuf::traits::{RingBuffer, Consumer, Observer}; +use crossbeam_channel::bounded; +use std::thread; +use std::env; +use std::os::unix::net::UnixListener; +use std::io::Read; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string()); + println!("Detected session type: {}", session_type); + + let mut source: Box = if std::env::args().any(|a| a == "--mock") { + println!("Initializing MOCK capture..."); + Box::new(vrec::capture::mock::MockCapture::new()) + } else if session_type.to_lowercase() == "wayland" { + println!("Initializing Wayland capture..."); + Box::new(vrec::capture::wayland::WaylandCapture::new().await?) + } else { + println!("Initializing X11 capture..."); + Box::new(vrec::capture::x11::X11Capture::new()?) + }; + + let first_frame = source.next_frame()?; + let (width, height) = match &first_frame { + Frame::Raw { width, height, .. } => (*width, *height), + Frame::DmaBuf { width, height, .. } => (*width, *height), + }; + + let (frame_tx, frame_rx) = bounded::(5); + let (cmd_tx, cmd_rx) = bounded::(10); + let (mux_tx, mux_rx) = bounded::>(1); + + let _ = frame_tx.send(first_frame); + + let capture_tx = frame_tx.clone(); + thread::spawn(move || { + loop { + match source.next_frame() { + Ok(frame) => { + if capture_tx.send(frame).is_err() { + break; + } + } + Err(e) => { + eprintln!("Capture error: {}", e); + break; + } + } + } + }); + + thread::spawn(move || { + let mut encoder = VaapiEncoder::new(width, height).expect("Failed to init encoder"); + let codec_ctx_ptr = encoder.codec_ctx() as usize; + + let mut config = vrec::config::VrecConfig::load(); + let mut ring = HeapRb::::new((config.replay_duration_sec * 60).max(60) as usize); + let mut normal_muxer: Option = None; + + thread::spawn(move || { + while let Ok(drain) = mux_rx.recv() { + let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + let name = format!("replay_{}.mp4", ts); + println!("Saving replay to {}...", name); + let mut start_idx = 0; + for (i, p) in drain.iter().enumerate() { + if p.is_keyframe() { + start_idx = i; + break; + } + } + + if start_idx < drain.len() { + let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; + let mut muxer = unsafe { Muxer::new(&name, codec_ctx).unwrap() }; + for p in drain.into_iter().skip(start_idx) { + let _ = muxer.write_packet(&p); + } + let _ = muxer.finalize(); + println!("Replay saved!"); + } else { + println!("No keyframe found in buffer, skipping save."); + } + } + }); + + while let Ok(frame) = frame_rx.recv() { + while let Ok(cmd) = cmd_rx.try_recv() { + match cmd { + Command::ReloadConfig => { + config = vrec::config::VrecConfig::load(); + println!("Daemon config reloaded!"); + let new_capacity = (config.replay_duration_sec * 60).max(60) as usize; + if ring.capacity().get() != new_capacity { + ring = HeapRb::::new(new_capacity); + println!("Replay buffer resized to {} frames.", new_capacity); + } + }, + Command::SaveReplay => { + if config.replay_enabled { + let drain = ring.iter().cloned().collect::>(); + let _ = mux_tx.try_send(drain); + } + }, + Command::StartRecording => { + if config.record_enabled && normal_muxer.is_none() { + let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext; + let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + let name = format!("record_{}.mp4", ts); + normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() }; + println!("Started normal recording to {}", name); + } + }, + Command::StopRecording => { + if let Some(mut m) = normal_muxer.take() { + let _ = m.finalize(); + println!("Stopped normal recording."); + } + }, + _ => {} + } + } + + if let Ok(packets) = encoder.encode_frame(&frame) { + for pkt in packets { + if config.replay_enabled { + ring.push_overwrite(pkt.clone()); + } + if let Some(muxer) = normal_muxer.as_mut() { + let _ = muxer.write_packet(&pkt); + } + } + } + } + }); + + let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string())); + let _ = std::fs::remove_file(&socket_path); + let listener = UnixListener::bind(&socket_path)?; + println!("Daemon listening on IPC socket: {}", socket_path); + + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + let mut len_buf = [0u8; 4]; + if stream.read_exact(&mut len_buf).is_ok() { + let len = u32::from_le_bytes(len_buf) as usize; + let mut payload = vec![0u8; len]; + if stream.read_exact(&mut payload).is_ok() + && let Ok(cmd) = serde_json::from_slice::(&payload) { + match cmd { + Command::StopDaemon => { + break; + }, + other => { + let _ = cmd_tx.try_send(other); + } + } + } + } + } + Err(err) => { + eprintln!("Socket error: {}", err); + } + } + } + + Ok(()) +} diff --git a/src/capture/mock.rs b/src/capture/mock.rs new file mode 100644 index 0000000..f7a0cdb --- /dev/null +++ b/src/capture/mock.rs @@ -0,0 +1,39 @@ +use super::{Frame, FrameSource}; +use std::time::Duration; + +pub struct MockCapture { + width: u32, + height: u32, + frame_count: u64, +} + +impl MockCapture { + pub fn new() -> Self { + Self { width: 1920, height: 1080, frame_count: 0 } + } +} + +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, + }) + } +} diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 0edaec7..f4bb4ee 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -34,3 +34,4 @@ pub enum Frame { pub trait FrameSource: Send { fn next_frame(&mut self) -> Result>; } +pub mod mock; diff --git a/test_ipc.py b/test_ipc.py new file mode 100644 index 0000000..2af96a1 --- /dev/null +++ b/test_ipc.py @@ -0,0 +1,11 @@ +import socket +import json +import struct +import os + +sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +sock.connect(os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/vrec.sock") +payload = b'"SaveReplay"' +sock.sendall(struct.pack('