Phase 4: Split into vrec-daemon and vrec-ui with IPC via Unix sockets

This commit is contained in:
katR 2026-09-01 19:54:26 +05:30
parent 2c1a464c03
commit 0cd7074942
11 changed files with 206 additions and 175 deletions

2
Cargo.lock generated
View file

@ -1485,6 +1485,8 @@ dependencies = [
"mp4",
"pipewire",
"ringbuf",
"serde",
"serde_json",
"tokio",
"xcb",
]

View file

@ -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"] }

82
scratch/daemon.patch Normal file
View file

@ -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<dyn std::error::Error>> {
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
@@ -23,10 +19,10 @@
let mut source: Box<dyn FrameSource> = 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::<Command>(&payload) {
+ match cmd {
+ Command::SaveReplay => {
+ let _ = trigger_tx.try_send(());
+ },
+ Command::StopDaemon => {
+ break;
+ },
+ _ => {}
+ }
+ }
+ }
+ }
+ }
+ Err(err) => {
+ eprintln!("Socket error: {}", err);
+ }
+ }
}

View file

@ -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);
}
}

View file

@ -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<Vec<Packet>, 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)
}
}

View file

@ -1,5 +0,0 @@
use ringbuf::HeapRb;
fn main() {
let rb = HeapRb::<i32>::new(10);
let (mut prod, mut cons) = rb.split();
}

View file

@ -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<dyn std::error::Error + Send + Sync>> {
@ -22,10 +18,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut source: Box<dyn FrameSource> = 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<dyn std::error::Error + Send + Sync>> {
Frame::DmaBuf { width, height, .. } => (*width, *height),
};
// 1. Channels
let (frame_tx, frame_rx) = bounded::<Frame>(5);
let (trigger_tx, trigger_rx) = bounded::<()>(1);
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(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<dyn std::error::Error + Send + Sync>> {
}
});
// 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::<Packet>::new(3600); // 60 seconds at 60fps
let mut ring = HeapRb::<Packet>::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<dyn std::error::Error + Send + Sync>> {
}
});
// 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<dyn std::error::Error + Send + Sync>> {
}
}
// Check for trigger
if trigger_rx.try_recv().is_ok() {
// Clone all packets to send to muxer
let drain = ring.iter().cloned().collect::<Vec<_>>();
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::<Command>(&payload) {
match cmd {
Command::SaveReplay => {
let _ = trigger_tx.try_send(());
},
Command::StopDaemon => {
break;
},
_ => {}
}
}
}
}
}
Err(err) => {
eprintln!("Socket error: {}", err);
}
}
}
Ok(())

61
src/bin/vrec-ui.rs Normal file
View file

@ -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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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);
}
}
}
}
}
}

8
src/ipc.rs Normal file
View file

@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum Command {
SaveReplay,
StopDaemon,
ShowOverlay,
}

8
src/lib.rs Normal file
View file

@ -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;

0
src/overlay.rs Normal file
View file