feat: Implement Normal Recording alongside Instant Replay in backend daemon
This commit is contained in:
parent
5ec04365e1
commit
5fac181cfe
7 changed files with 136 additions and 58 deletions
21
scratch/ui_clean.patch
Normal file
21
scratch/ui_clean.patch
Normal 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>> {
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
src/ipc.rs
13
src/ipc.rs
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue