feat: Implement Normal Recording alongside Instant Replay in backend daemon

This commit is contained in:
katR 2026-09-02 07:27:46 +05:30
parent 5ec04365e1
commit 5fac181cfe
7 changed files with 136 additions and 58 deletions

21
scratch/ui_clean.patch Normal file
View file

@ -0,0 +1,21 @@
--- src/bin/vrec-ui.rs
+++ src/bin/vrec-ui.rs
@@ -1,18 +1,8 @@
-use vrec::ipc::Command;
+use vrec::ipc::{Command, send_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>> {

View file

@ -4,7 +4,7 @@ use vrec::ring::Packet;
use vrec::muxer::Muxer;
use vrec::ipc::Command;
use ringbuf::HeapRb;
use ringbuf::traits::{RingBuffer, Consumer};
use ringbuf::traits::{RingBuffer, Consumer, Observer};
use crossbeam_channel::bounded;
use std::thread;
use std::env;
@ -31,7 +31,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
};
let (frame_tx, frame_rx) = bounded::<Frame>(5);
let (trigger_tx, trigger_rx) = bounded::<()>(1);
let (cmd_tx, cmd_rx) = bounded::<Command>(10);
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
let _ = frame_tx.send(first_frame);
@ -57,11 +57,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut encoder = VaapiEncoder::new(width, height).expect("Failed to init encoder");
let codec_ctx_ptr = encoder.codec_ctx() as usize;
let mut ring = HeapRb::<Packet>::new(3600);
let mut config = vrec::config::VrecConfig::load();
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec * 60).max(60) as usize);
let mut normal_muxer: Option<Muxer> = None;
thread::spawn(move || {
while let Ok(drain) = mux_rx.recv() {
println!("Saving replay to output.mp4...");
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() {
@ -72,7 +76,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if start_idx < drain.len() {
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
let mut muxer = unsafe { Muxer::new("output.mp4", codec_ctx).unwrap() };
let mut muxer = unsafe { Muxer::new(&name, codec_ctx).unwrap() };
for p in drain.into_iter().skip(start_idx) {
let _ = muxer.write_packet(&p);
}
@ -85,15 +89,51 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
});
while let Ok(frame) = frame_rx.recv() {
if let Ok(packets) = encoder.encode_frame(&frame) {
for pkt in packets {
ring.push_overwrite(pkt);
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::<Packet>::new(new_capacity);
println!("Replay buffer resized to {} frames.", new_capacity);
}
},
Command::SaveReplay => {
if config.replay_enabled {
let drain = ring.iter().cloned().collect::<Vec<_>>();
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 trigger_rx.try_recv().is_ok() {
let drain = ring.iter().cloned().collect::<Vec<_>>();
let _ = mux_tx.try_send(drain);
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);
}
}
}
}
});
@ -113,13 +153,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if stream.read_exact(&mut payload).is_ok()
&& let Ok(cmd) = serde_json::from_slice::<Command>(&payload) {
match cmd {
Command::SaveReplay => {
let _ = trigger_tx.try_send(());
},
Command::StopDaemon => {
break;
},
_ => {}
other => {
let _ = cmd_tx.try_send(other);
}
}
}
}

View file

@ -1,26 +1,14 @@
use vrec::ipc::Command;
use std::os::unix::net::UnixStream;
use std::io::Write;
use vrec::ipc::{Command, send_command};
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>> {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
match args[1].as_str() {
"--save" => {
vrec::overlay::show_notification_overlay();
return send_command(Command::SaveReplay).map_err(|e| e.into());
return send_command(Command::SaveReplay).map_err(|e| e);
}
"--menu" => {
vrec::overlay::show_menu_overlay();
@ -63,8 +51,8 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let receiver = GlobalHotKeyEvent::receiver();
loop {
if let Ok(event) = receiver.recv() {
if event.state == global_hotkey::HotKeyState::Pressed {
if let Ok(event) = receiver.recv()
&& event.state == global_hotkey::HotKeyState::Pressed {
if event.id == save_hotkey.id() {
println!("Hotkey triggered! Sending SaveReplay command to daemon...");
vrec::overlay::show_notification_overlay();
@ -74,6 +62,5 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
vrec::overlay::show_menu_overlay();
}
}
}
}
}

View file

@ -15,9 +15,23 @@ fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send + S
}
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
match args[1].as_str() {
"--save" => {
vrec::overlay::show_notification_overlay();
return send_command(Command::SaveReplay).map_err(|e| e.into());
}
"--menu" => {
vrec::overlay::show_menu_overlay();
return Ok(());
}
_ => {}
}
}
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();
@ -26,34 +40,38 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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.");
println!("WARNING: Wayland native layer shell used. Ensure your compositor allows it.");
println!("Note: Global hotkeys may fail to register on Wayland compositors.");
} else {
println!("wlroots-based compositor detected. (wlr-layer-shell overlay would be spawned here).");
println!("wlroots-based compositor detected.");
}
} else {
println!("X11 session detected. (X11 override-redirect overlay would be spawned here).");
println!("X11 session detected.");
}
let manager = GlobalHotKeyManager::new()?;
let hotkey = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR);
let manager = GlobalHotKeyManager::new().unwrap();
let save_hotkey = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR);
let menu_hotkey = HotKey::new(Some(Modifiers::ALT), Code::KeyZ);
if let Err(e) = manager.register(hotkey) {
if let Err(e) = manager.register(save_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.");
manager.register(menu_hotkey).unwrap();
println!("Registered global hotkeys:\n - Ctrl+Shift+R (Save Replay)\n - Alt+Z (Open GUI Menu)");
}
let receiver = GlobalHotKeyEvent::receiver();
loop {
if let Ok(event) = receiver.recv() {
if event.id == hotkey.id() {
if event.state == global_hotkey::HotKeyState::Pressed {
if event.state == global_hotkey::HotKeyState::Pressed {
if event.id == save_hotkey.id() {
println!("Hotkey triggered! Sending SaveReplay command to daemon...");
if let Err(e) = send_command(Command::SaveReplay) {
eprintln!("Failed to communicate with daemon: {}", e);
}
vrec::overlay::show_notification_overlay();
let _ = send_command(Command::SaveReplay);
} else if event.id == menu_hotkey.id() {
println!("Alt+Z triggered! Opening GUI Menu Overlay...");
vrec::overlay::show_menu_overlay();
}
}
}

View file

@ -47,13 +47,11 @@ impl VrecConfig {
pub fn load() -> Self {
let path = Self::config_path();
if path.exists() {
if let Ok(data) = fs::read_to_string(path) {
if let Ok(config) = serde_json::from_str(&data) {
if path.exists()
&& let Ok(data) = fs::read_to_string(path)
&& let Ok(config) = serde_json::from_str(&data) {
return config;
}
}
}
Self::default()
}
@ -72,12 +70,11 @@ impl VrecConfig {
use std::os::unix::net::UnixStream;
use std::io::Write;
let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()));
if let Ok(mut stream) = UnixStream::connect(&socket_path) {
if let Ok(payload) = serde_json::to_vec(&crate::ipc::Command::ReloadConfig) {
if let Ok(mut stream) = UnixStream::connect(&socket_path)
&& let Ok(payload) = serde_json::to_vec(&crate::ipc::Command::ReloadConfig) {
let len_buf = (payload.len() as u32).to_le_bytes();
let _ = stream.write_all(&len_buf);
let _ = stream.write_all(&payload);
}
}
}
}

View file

@ -9,3 +9,16 @@ pub enum Command {
StopDaemon,
ShowOverlay,
}
pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::env;
use std::os::unix::net::UnixStream;
use std::io::Write;
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(())
}

View file

@ -142,7 +142,7 @@ pub fn show_menu_overlay() {
});
// Toggle buttons logic (just UI for now, backend IPC will be needed to actually toggle daemon state)
let btn_cfg_1 = config.clone();
let _btn_cfg_1 = config.clone();
let replay_btn_clone = replay_btn.clone();
replay_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
@ -155,16 +155,19 @@ pub fn show_menu_overlay() {
}
});
let btn_cfg_2 = config.clone();
let _btn_cfg_2 = config.clone();
let record_btn_clone = record_btn.clone();
record_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.record_enabled = !cfg.record_enabled;
cfg.save().unwrap(); VrecConfig::notify_daemon_reload();
cfg.save().unwrap();
VrecConfig::notify_daemon_reload();
if cfg.record_enabled {
record_btn_clone.set_label("Recording\n🔴 ON");
let _ = crate::ipc::send_command(crate::ipc::Command::StartRecording);
} else {
record_btn_clone.set_label("Recording\n⚪ OFF");
let _ = crate::ipc::send_command(crate::ipc::Command::StopRecording);
}
});