Phase 4: Split into vrec-daemon and vrec-ui with IPC via Unix sockets
This commit is contained in:
parent
2c1a464c03
commit
0cd7074942
11 changed files with 206 additions and 175 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1485,6 +1485,8 @@ dependencies = [
|
|||
"mp4",
|
||||
"pipewire",
|
||||
"ringbuf",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"xcb",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
82
scratch/daemon.patch
Normal 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);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
use ringbuf::HeapRb;
|
||||
fn main() {
|
||||
let rb = HeapRb::<i32>::new(10);
|
||||
let (mut prod, mut cons) = rb.split();
|
||||
}
|
||||
|
|
@ -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
61
src/bin/vrec-ui.rs
Normal 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
8
src/ipc.rs
Normal 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
8
src/lib.rs
Normal 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
0
src/overlay.rs
Normal file
Loading…
Add table
Reference in a new issue