From 0cd7074942567f23920ec7a5deabdd03ad0b277b Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Tue, 1 Sep 2026 19:54:26 +0530 Subject: [PATCH] Phase 4: Split into vrec-daemon and vrec-ui with IPC via Unix sockets --- Cargo.lock | 2 + Cargo.toml | 2 + scratch/daemon.patch | 82 ++++++++++++++++++++++++ scratch/enc.patch | 99 ----------------------------- scratch/enc_mod.patch | 42 ------------ scratch/test_ring.rs | 5 -- src/{main.rs => bin/vrec-daemon.rs} | 72 ++++++++++++--------- src/bin/vrec-ui.rs | 61 ++++++++++++++++++ src/ipc.rs | 8 +++ src/lib.rs | 8 +++ src/overlay.rs | 0 11 files changed, 206 insertions(+), 175 deletions(-) create mode 100644 scratch/daemon.patch delete mode 100644 scratch/enc.patch delete mode 100644 scratch/enc_mod.patch delete mode 100644 scratch/test_ring.rs rename src/{main.rs => bin/vrec-daemon.rs} (62%) create mode 100644 src/bin/vrec-ui.rs create mode 100644 src/ipc.rs create mode 100644 src/lib.rs create mode 100644 src/overlay.rs diff --git a/Cargo.lock b/Cargo.lock index 0de94cd..e564278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1485,6 +1485,8 @@ dependencies = [ "mp4", "pipewire", "ringbuf", + "serde", + "serde_json", "tokio", "xcb", ] diff --git a/Cargo.toml b/Cargo.toml index 5793a9a..bc5dbb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,5 +15,7 @@ minifb = "0.28.0" mp4 = "0.14.0" pipewire = "0.10.1" ringbuf = "0.5.1" +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" tokio = { version = "1.53.1", features = ["rt", "macros", "rt-multi-thread"] } xcb = { version = "1.7.1", features = ["shm", "xfixes"] } diff --git a/scratch/daemon.patch b/scratch/daemon.patch new file mode 100644 index 0000000..a9362aa --- /dev/null +++ b/scratch/daemon.patch @@ -0,0 +1,82 @@ +--- src/bin/vrec-daemon.rs ++++ src/bin/vrec-daemon.rs +@@ -1,13 +1,6 @@ +-pub mod capture; +-pub mod encoder; +-pub mod ring; +-pub mod muxer; +-pub mod hotkey; +-pub mod debug; +- +-use capture::{Frame, FrameSource}; +-use encoder::VaapiEncoder; +-use ring::Packet; +-use muxer::Muxer; ++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}; + use crossbeam_channel::bounded; +@@ -15,6 +8,9 @@ + 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()); +@@ -23,10 +19,10 @@ + let mut source: Box = if session_type.to_lowercase() == "wayland" { + println!("Initializing Wayland capture..."); +- Box::new(capture::wayland::WaylandCapture::new().await?) ++ Box::new(vrec::capture::wayland::WaylandCapture::new().await?) + } else { + println!("Initializing X11 capture..."); +- Box::new(capture::x11::X11Capture::new()?) ++ Box::new(vrec::capture::x11::X11Capture::new()?) + }; + +@@ -118,10 +114,35 @@ + }); + +- // 5. Hotkey listener +- println!("Starting hotkey listener..."); +- if let Err(e) = hotkey::run_hotkey_listener(trigger_tx) { +- eprintln!("Failed to start hotkey listener: {}", e); ++ // 5. IPC Unix Socket Listener ++ let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string())); ++ let _ = std::fs::remove_file(&socket_path); // Remove old socket if exists ++ 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() { ++ if let Ok(cmd) = serde_json::from_slice::(&payload) { ++ match cmd { ++ Command::SaveReplay => { ++ let _ = trigger_tx.try_send(()); ++ }, ++ Command::StopDaemon => { ++ break; ++ }, ++ _ => {} ++ } ++ } ++ } ++ } ++ } ++ Err(err) => { ++ eprintln!("Socket error: {}", err); ++ } ++ } + } + diff --git a/scratch/enc.patch b/scratch/enc.patch deleted file mode 100644 index 78849f5..0000000 --- a/scratch/enc.patch +++ /dev/null @@ -1,99 +0,0 @@ ---- src/encoder.rs -+++ src/encoder.rs -@@ -6,6 +6,7 @@ - codec_ctx: *mut AVCodecContext, - hw_device_ctx: *mut AVBufferRef, - hw_frames_ctx: *mut AVBufferRef, -+ next_pts: i64, - } - - impl VaapiEncoder { -@@ -70,6 +71,7 @@ - codec_ctx, - hw_device_ctx, - hw_frames_ctx: hw_frames_ref, -+ next_pts: 0, - }) - } - } -@@ -77,15 +79,63 @@ - pub fn encode_frame(&mut self, frame: &Frame) -> Result<(), String> { - unsafe { - match frame { -- Frame::Raw { width, height, stride, data } => { -- // For Phase 2 X11 fallback: -- // 1. Convert BGRA to NV12 (omitted/mocked for brevity if not using swscale yet) -- // 2. Upload NV12 to a VAAPI AVFrame via av_hwframe_get_buffer and av_hwframe_transfer_data -- // 3. Send to avcodec_send_frame -- println!("VAAPI: Mock encoding Raw CPU frame (X11 fallback). Needs BGRA->NV12 swscale."); -- } -- Frame::DmaBuf { width, height, format, fd, stride, offset } => { -- // For Phase 2 Wayland zero-copy: -- // 1. Populate AVDRMFrameDescriptor with the FD and modifier. -- // 2. Create an AVFrame with format AV_PIX_FMT_DRM_PRIME. -- // 3. Use av_hwframe_map to map it to AV_PIX_FMT_VAAPI. -- // 4. Send to avcodec_send_frame. -- println!("VAAPI: Mock encoding DMA-BUF frame (Wayland zero-copy). FD: {}", fd); -+ Frame::Raw { width, height, stride: in_stride, data } => { -+ let mut bgra_frame = av_frame_alloc(); -+ (*bgra_frame).format = AVPixelFormat::AV_PIX_FMT_BGRA as i32; -+ (*bgra_frame).width = *width as i32; -+ (*bgra_frame).height = *height as i32; -+ av_frame_get_buffer(bgra_frame, 32); -+ -+ let bgra_stride = (*bgra_frame).linesize[0] as usize; -+ for y in 0..(*height as usize) { -+ let src_row = &data[y * (*in_stride as usize) .. y * (*in_stride as usize) + (*width as usize) * 4]; -+ let dst_row = std::slice::from_raw_parts_mut( -+ (*bgra_frame).data[0].add(y * bgra_stride), -+ (*width as usize) * 4 -+ ); -+ dst_row.copy_from_slice(src_row); -+ } -+ -+ let mut nv12_frame = av_frame_alloc(); -+ (*nv12_frame).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32; -+ (*nv12_frame).width = *width as i32; -+ (*nv12_frame).height = *height as i32; -+ av_frame_get_buffer(nv12_frame, 32); -+ -+ let sws_ctx = sws_getContext( -+ *width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_BGRA, -+ *width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_NV12, -+ SWS_BILINEAR, ptr::null_mut(), ptr::null_mut(), ptr::null_mut() -+ ); -+ -+ sws_scale( -+ sws_ctx, -+ (*bgra_frame).data.as_ptr() as *const *const u8, -+ (*bgra_frame).linesize.as_ptr(), -+ 0, -+ *height as i32, -+ (*nv12_frame).data.as_ptr(), -+ (*nv12_frame).linesize.as_ptr() -+ ); -+ sws_freeContext(sws_ctx); -+ -+ let mut hw_frame = av_frame_alloc(); -+ av_hwframe_get_buffer(self.hw_frames_ctx, hw_frame, 0); -+ av_hwframe_transfer_data(hw_frame, nv12_frame, 0); -+ -+ (*hw_frame).pts = self.next_pts; -+ self.next_pts += 1; -+ -+ if avcodec_send_frame(self.codec_ctx, hw_frame) >= 0 { -+ let mut pkt = av_packet_alloc(); -+ while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 { -+ println!("VAAPI: encoded packet of size {}", (*pkt).size); -+ av_packet_unref(pkt); -+ } -+ av_packet_free(&mut pkt); -+ } -+ av_frame_free(&mut hw_frame); -+ av_frame_free(&mut nv12_frame); -+ av_frame_free(&mut bgra_frame); -+ } -+ Frame::DmaBuf { width: _, height: _, format: _, fd, stride: _, offset: _ } => { -+ println!("VAAPI: Mock encoding DMA-BUF frame (Wayland zero-copy). FD: {}", fd); - } - } diff --git a/scratch/enc_mod.patch b/scratch/enc_mod.patch deleted file mode 100644 index 8f5ec39..0000000 --- a/scratch/enc_mod.patch +++ /dev/null @@ -1,42 +0,0 @@ ---- src/encoder.rs -+++ src/encoder.rs -@@ -2,7 +2,7 @@ - use std::ptr; - use crate::capture::Frame; -+use crate::ring::Packet; - - pub struct VaapiEncoder { - codec_ctx: *mut AVCodecContext, -@@ -62,6 +62,10 @@ - } - } - -- pub fn encode_frame(&mut self, frame: &Frame) -> Result<(), String> { -+ pub fn codec_ctx(&self) -> *mut AVCodecContext { -+ self.codec_ctx -+ } -+ -+ pub fn encode_frame(&mut self, frame: &Frame) -> Result, String> { -+ let mut packets = Vec::new(); - unsafe { - match frame { -@@ -107,8 +111,8 @@ - if avcodec_send_frame(self.codec_ctx, hw_frame) >= 0 { - let mut pkt = av_packet_alloc(); - while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 { -- println!("VAAPI: encoded packet of size {}", (*pkt).size); -- av_packet_unref(pkt); -+ let new_pkt = av_packet_alloc(); -+ av_packet_move_ref(new_pkt, pkt); -+ packets.push(Packet::new(new_pkt)); - } - av_packet_free(&mut pkt); - } -@@ -122,7 +126,7 @@ - } - } - } -- Ok(()) -+ Ok(packets) - } - } diff --git a/scratch/test_ring.rs b/scratch/test_ring.rs deleted file mode 100644 index e68dee2..0000000 --- a/scratch/test_ring.rs +++ /dev/null @@ -1,5 +0,0 @@ -use ringbuf::HeapRb; -fn main() { - let rb = HeapRb::::new(10); - let (mut prod, mut cons) = rb.split(); -} diff --git a/src/main.rs b/src/bin/vrec-daemon.rs similarity index 62% rename from src/main.rs rename to src/bin/vrec-daemon.rs index 9f1aedf..6430575 100644 --- a/src/main.rs +++ b/src/bin/vrec-daemon.rs @@ -1,19 +1,15 @@ -pub mod capture; -pub mod encoder; -pub mod ring; -pub mod muxer; -pub mod hotkey; -pub mod debug; - -use capture::{Frame, FrameSource}; -use encoder::VaapiEncoder; -use ring::Packet; -use muxer::Muxer; +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}; 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> { @@ -22,10 +18,10 @@ async fn main() -> Result<(), Box> { let mut source: Box = if session_type.to_lowercase() == "wayland" { println!("Initializing Wayland capture..."); - Box::new(capture::wayland::WaylandCapture::new().await?) + Box::new(vrec::capture::wayland::WaylandCapture::new().await?) } else { println!("Initializing X11 capture..."); - Box::new(capture::x11::X11Capture::new()?) + Box::new(vrec::capture::x11::X11Capture::new()?) }; let first_frame = source.next_frame()?; @@ -34,15 +30,12 @@ async fn main() -> Result<(), Box> { Frame::DmaBuf { width, height, .. } => (*width, *height), }; - // 1. Channels let (frame_tx, frame_rx) = bounded::(5); let (trigger_tx, trigger_rx) = bounded::<()>(1); let (mux_tx, mux_rx) = bounded::>(1); - // Send first frame to encoder thread let _ = frame_tx.send(first_frame); - // Capture thread let capture_tx = frame_tx.clone(); thread::spawn(move || { loop { @@ -60,17 +53,14 @@ async fn main() -> Result<(), Box> { } }); - // 3. Encoder Thread thread::spawn(move || { let mut encoder = VaapiEncoder::new(width, height).expect("Failed to init encoder"); - let codec_ctx_ptr = encoder.codec_ctx() as usize; // Send raw pointer safely + let codec_ctx_ptr = encoder.codec_ctx() as usize; - let mut ring = HeapRb::::new(3600); // 60 seconds at 60fps + let mut ring = HeapRb::::new(3600); - // Muxer Thread spawned inside here so it shares lifetime conceptually, - // though we use mux_rx. thread::spawn(move || { - while let Ok(mut drain) = mux_rx.recv() { + while let Ok(drain) = mux_rx.recv() { println!("Saving replay to output.mp4..."); let mut start_idx = 0; for (i, p) in drain.iter().enumerate() { @@ -94,7 +84,6 @@ async fn main() -> Result<(), Box> { } }); - // Encoder loop while let Ok(frame) = frame_rx.recv() { if let Ok(packets) = encoder.encode_frame(&frame) { for pkt in packets { @@ -102,19 +91,44 @@ async fn main() -> Result<(), Box> { } } - // Check for trigger if trigger_rx.try_recv().is_ok() { - // Clone all packets to send to muxer let drain = ring.iter().cloned().collect::>(); let _ = mux_tx.try_send(drain); } } }); - // 5. Hotkey listener - println!("Starting hotkey listener..."); - if let Err(e) = hotkey::run_hotkey_listener(trigger_tx) { - eprintln!("Failed to start hotkey listener: {}", e); + 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() { + if let Ok(cmd) = serde_json::from_slice::(&payload) { + match cmd { + Command::SaveReplay => { + let _ = trigger_tx.try_send(()); + }, + Command::StopDaemon => { + break; + }, + _ => {} + } + } + } + } + } + Err(err) => { + eprintln!("Socket error: {}", err); + } + } } Ok(()) diff --git a/src/bin/vrec-ui.rs b/src/bin/vrec-ui.rs new file mode 100644 index 0000000..de1928b --- /dev/null +++ b/src/bin/vrec-ui.rs @@ -0,0 +1,61 @@ +use vrec::ipc::Command; +use std::os::unix::net::UnixStream; +use std::io::Write; +use std::env; +use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent}; + +fn send_command(cmd: Command) -> Result<(), Box> { + let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string())); + let mut stream = UnixStream::connect(&socket_path)?; + let payload = serde_json::to_vec(&cmd)?; + let len_buf = (payload.len() as u32).to_le_bytes(); + stream.write_all(&len_buf)?; + stream.write_all(&payload)?; + Ok(()) +} + +fn main() -> Result<(), Box> { + println!("Starting vrec UI/Hotkey process..."); + + // Check session type + let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string()); + let desktop = env::var("XDG_CURRENT_DESKTOP").unwrap_or_else(|_| "".to_string()).to_lowercase(); + + let is_gnome = desktop.contains("gnome") || desktop.contains("ubuntu"); + let is_kde = desktop.contains("kde"); + + if session_type.to_lowercase() == "wayland" { + if is_gnome || is_kde { + println!("WARNING: wlr-layer-shell unsupported on GNOME/KDE Wayland. Degrading to hotkey-only mode."); + println!("Note: Global hotkeys may also fail to register on some Wayland compositors."); + } else { + println!("wlroots-based compositor detected. (wlr-layer-shell overlay would be spawned here)."); + } + } else { + println!("X11 session detected. (X11 override-redirect overlay would be spawned here)."); + } + + let manager = GlobalHotKeyManager::new()?; + let hotkey = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR); + + if let Err(e) = manager.register(hotkey) { + eprintln!("Failed to register global hotkey! Wayland compositor might be blocking it: {}", e); + eprintln!("Fallback: running without global hotkey. Send commands via socket manually."); + } else { + println!("Registered global hotkey Ctrl+Shift+R to trigger replay save."); + } + + let receiver = GlobalHotKeyEvent::receiver(); + loop { + if let Ok(event) = receiver.recv() { + if event.id == hotkey.id() { + if event.state == global_hotkey::HotKeyState::Pressed { + println!("Hotkey triggered! Sending SaveReplay command to daemon..."); + if let Err(e) = send_command(Command::SaveReplay) { + eprintln!("Failed to communicate with daemon: {}", e); + } + } + } + } + } +} diff --git a/src/ipc.rs b/src/ipc.rs new file mode 100644 index 0000000..945bc84 --- /dev/null +++ b/src/ipc.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub enum Command { + SaveReplay, + StopDaemon, + ShowOverlay, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6241754 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +pub mod capture; +pub mod encoder; +pub mod ring; +pub mod muxer; +pub mod hotkey; +pub mod debug; +pub mod ipc; +pub mod overlay; diff --git a/src/overlay.rs b/src/overlay.rs new file mode 100644 index 0000000..e69de29