From e4c324a50dc39a390705f86a8afa5f5a29574a7c Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Thu, 3 Sep 2026 16:51:04 +0530 Subject: [PATCH] feat: Add faststart web MP4 streaming, graceful SIGINT/SIGTERM shutdown protection, dynamic in-game HUD notifications, dedicated record hotkey, and desktop game audio priority --- Cargo.toml | 2 +- src/bin/vrec-daemon.rs | 13 +++++++++ src/bin/vrec-ui.rs | 52 ++++++++++++++++++++++++++++------- src/capture/audio.rs | 17 ++++++++++-- src/capture/wayland_stream.rs | 32 ++++++++++++++------- src/config.rs | 2 ++ src/muxer.rs | 5 +++- src/overlay.rs | 45 ++++++++++++++++++++---------- 8 files changed, 130 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92d0fb3..f85a4a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ mp4 = "0.14.0" 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"] } +tokio = { version = "1.53.1", features = ["rt", "macros", "rt-multi-thread", "signal"] } dirs = "6.0.0" cpal = "0.18.2" rubato = "5.0.0" diff --git a/src/bin/vrec-daemon.rs b/src/bin/vrec-daemon.rs index fbc2e65..b70ec10 100644 --- a/src/bin/vrec-daemon.rs +++ b/src/bin/vrec-daemon.rs @@ -277,6 +277,16 @@ async fn main() -> Result<(), Box> { l }; + let cmd_tx_sig = cmd_tx.clone(); + tokio::spawn(async move { + if tokio::signal::ctrl_c().await.is_ok() { + println!("\nReceived shutdown signal. Finalizing recordings..."); + let _ = cmd_tx_sig.send(Command::StopRecording); + tokio::time::sleep(std::time::Duration::from_millis(350)).await; + let _ = vrec::ipc::send_command(Command::StopDaemon); + } + }); + for stream in listener.incoming() { match stream { Ok(mut stream) => { @@ -309,6 +319,9 @@ async fn main() -> Result<(), Box> { } }, Command::StopDaemon => { + println!("StopDaemon requested: Finalizing active recordings..."); + let _ = cmd_tx.send(Command::StopRecording); + std::thread::sleep(std::time::Duration::from_millis(350)); break; }, other => { diff --git a/src/bin/vrec-ui.rs b/src/bin/vrec-ui.rs index 572b6ba..066a17b 100644 --- a/src/bin/vrec-ui.rs +++ b/src/bin/vrec-ui.rs @@ -1,4 +1,5 @@ -use vrec::ipc::{Command, send_command}; +use vrec::ipc::{Command, send_command, query_status}; +use vrec::overlay::{show_menu_overlay, show_notification}; use std::env; use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent}; @@ -8,21 +9,33 @@ fn main() -> Result<(), Box> { match args[1].as_str() { "--save" => { let res = send_command(Command::SaveReplay); - vrec::overlay::show_notification_overlay(); + show_notification("💾 Replay Saved!"); return res; } "--menu" => { - vrec::overlay::show_menu_overlay(); + show_menu_overlay(); return Ok(()); } "--record" | "--toggle" => { - return send_command(Command::ToggleRecording); + let status = query_status().ok(); + let is_rec = status.as_ref().map(|s| s.is_recording).unwrap_or(false); + let res = send_command(Command::ToggleRecording); + if is_rec { + show_notification("âšī¸ Recording Saved!"); + } else { + show_notification("🔴 Recording Started"); + } + return res; } "--start" => { - return send_command(Command::StartRecording); + let res = send_command(Command::StartRecording); + show_notification("🔴 Recording Started"); + return res; } "--stop" => { - return send_command(Command::StopRecording); + let res = send_command(Command::StopRecording); + show_notification("âšī¸ Recording Saved!"); + return res; } "--reload" => { return send_command(Command::ReloadConfig); @@ -36,7 +49,7 @@ fn main() -> Result<(), Box> { println!(" vrec-ui Run global hotkey manager in background"); println!(" vrec-ui --menu Open GPU Screen Recorder style menu overlay"); println!(" vrec-ui --save Save instant replay and show notification"); - println!(" vrec-ui --record Toggle normal recording on/off"); + println!(" vrec-ui --record Toggle normal recording on/off (with notification)"); println!(" vrec-ui --start Start normal recording"); println!(" vrec-ui --stop Stop normal recording"); println!(" vrec-ui --reload Reload daemon configuration"); @@ -73,6 +86,8 @@ fn main() -> Result<(), Box> { .unwrap_or_else(|| HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR)); let menu_hotkey = vrec::hotkey::parse_hotkey(&config.menu_hotkey) .unwrap_or_else(|| HotKey::new(Some(Modifiers::ALT), Code::KeyZ)); + let record_hotkey = vrec::hotkey::parse_hotkey(&config.record_hotkey) + .unwrap_or_else(|| HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9)); let mut registered_count = 0; if let Err(e) = manager.register(save_hotkey) { @@ -89,8 +104,15 @@ fn main() -> Result<(), Box> { registered_count += 1; } + if let Err(e) = manager.register(record_hotkey) { + eprintln!("Warning: Failed to register record hotkey ({}): {}", config.record_hotkey, e); + } else { + println!("Registered toggle recording hotkey: {}", config.record_hotkey); + registered_count += 1; + } + if registered_count == 0 { - eprintln!("Note: Running without global hotkeys. You can bind `vrec-ui --menu` and `vrec-ui --save` to compositor shortcuts."); + eprintln!("Note: Running without global hotkeys. You can bind `vrec-ui --menu`, `vrec-ui --record`, and `vrec-ui --save` to compositor shortcuts."); } let receiver = GlobalHotKeyEvent::receiver(); @@ -99,11 +121,21 @@ fn main() -> Result<(), Box> { && event.state == global_hotkey::HotKeyState::Pressed { if event.id == save_hotkey.id() { println!("Save replay hotkey pressed. Triggering SaveReplay..."); - vrec::overlay::show_notification_overlay(); let _ = send_command(Command::SaveReplay); + show_notification("💾 Replay Saved!"); } else if event.id == menu_hotkey.id() { println!("Menu hotkey pressed. Opening Overlay Menu..."); - vrec::overlay::show_menu_overlay(); + show_menu_overlay(); + } else if event.id == record_hotkey.id() { + println!("Record hotkey pressed. Toggling Recording..."); + let status = query_status().ok(); + let is_rec = status.as_ref().map(|s| s.is_recording).unwrap_or(false); + let _ = send_command(Command::ToggleRecording); + if is_rec { + show_notification("âšī¸ Recording Saved!"); + } else { + show_notification("🔴 Recording Started"); + } } } } diff --git a/src/capture/audio.rs b/src/capture/audio.rs index 1ff935d..2e09c42 100644 --- a/src/capture/audio.rs +++ b/src/capture/audio.rs @@ -16,7 +16,10 @@ pub fn list_input_devices() -> Vec { let mut names = Vec::new(); if let Ok(devices) = host.input_devices() { for dev in devices { - names.push(get_device_name(&dev)); + let name = get_device_name(&dev); + if !names.contains(&name) { + names.push(name); + } } } names @@ -43,7 +46,17 @@ impl AudioCapture { host.default_input_device().ok_or("No audio input device available")? } } else { - host.default_input_device().ok_or("No audio input device available")? + // When default is selected, prefer desktop audio monitor if available so gameplay sound is recorded + if let Ok(mut devs) = host.input_devices() { + devs.find(|d| { + let n = get_device_name(d).to_lowercase(); + n.contains("monitor") + }) + .or_else(|| host.default_input_device()) + .ok_or("No audio input device available")? + } else { + host.default_input_device().ok_or("No audio input device available")? + } }; let dev_name = get_device_name(&device); diff --git a/src/capture/wayland_stream.rs b/src/capture/wayland_stream.rs index a2448d4..1121c1f 100644 --- a/src/capture/wayland_stream.rs +++ b/src/capture/wayland_stream.rs @@ -73,16 +73,28 @@ impl PipeWireStream { if datas.is_empty() { return; } let data = &mut datas[0]; - // To properly handle DMA-BUFs, we would read the spa_data chunk and extract the fd - // For this MVP skeleton, we will push a mock DmaBuf to satisfy the pipeline - let frame = Frame::DmaBuf { - width: user_data.format.size().width, - height: user_data.format.size().height, - format: 0x34325241, // DRM_FORMAT_ARGB8888 - modifier: user_data.format.modifier(), - fd: data.fd(), - stride: data.chunk().stride() as u32, - offset: data.chunk().offset(), + let fd = data.fd(); + let stride = data.chunk().stride() as u32; + let offset = data.chunk().offset(); + let frame = if fd > 0 { + Frame::DmaBuf { + width: user_data.format.size().width, + height: user_data.format.size().height, + format: 0x34325241, // DRM_FORMAT_ARGB8888 + modifier: user_data.format.modifier(), + fd, + stride, + offset, + } + } else if let Some(slice) = data.data() { + Frame::Raw { + width: user_data.format.size().width, + height: user_data.format.size().height, + stride, + data: slice.to_vec(), + } + } else { + return; }; let _ = user_data.tx.try_send(frame); } diff --git a/src/config.rs b/src/config.rs index 425c813..d65809e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,6 +20,7 @@ pub struct VrecConfig { pub ui_color_theme: String, pub save_hotkey: String, pub menu_hotkey: String, + pub record_hotkey: String, } impl Default for VrecConfig { @@ -41,6 +42,7 @@ impl Default for VrecConfig { ui_color_theme: "dark".to_string(), save_hotkey: "Ctrl+Shift+R".to_string(), menu_hotkey: "Alt+Z".to_string(), + record_hotkey: "Ctrl+Shift+F9".to_string(), } } } diff --git a/src/muxer.rs b/src/muxer.rs index 91a665e..fa3ca38 100644 --- a/src/muxer.rs +++ b/src/muxer.rs @@ -68,7 +68,10 @@ impl Muxer { } } - let ret = avformat_write_header(fmt_ctx, std::ptr::null_mut()); + let mut opts: *mut AVDictionary = std::ptr::null_mut(); + av_dict_set(&mut opts, c"movflags".as_ptr(), c"+faststart".as_ptr(), 0); + let ret = avformat_write_header(fmt_ctx, &mut opts); + av_dict_free(&mut opts); if ret < 0 { if ((*(*fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 { avio_closep(&mut (*fmt_ctx).pb); diff --git a/src/overlay.rs b/src/overlay.rs index cefae46..3505611 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -12,6 +12,10 @@ use crate::config::VrecConfig; use crate::ipc::{self, Command}; pub fn show_notification_overlay() { + show_notification("💾 Replay Saved!"); +} + +pub fn show_notification(message: &str) { #[cfg(unix)] if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() { return; @@ -24,10 +28,11 @@ pub fn show_notification_overlay() { .application_id("com.vrec.notification") .build(); - app.connect_activate(|app| { + let msg_text = message.to_string(); + app.connect_activate(move |app| { let window = ApplicationWindow::builder() .application(app) - .default_width(320) + .default_width(340) .default_height(70) .build(); @@ -36,7 +41,7 @@ pub fn show_notification_overlay() { window.init_layer_shell(); window.set_layer(Layer::Overlay); window.set_namespace("vrec-notification"); - window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30); + window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 35); window.set_anchor(gtk_layer_shell::Edge::Top, true); } @@ -51,16 +56,16 @@ pub fn show_notification_overlay() { let css_provider = CssProvider::new(); let css = r#" window { - background-color: rgba(18, 18, 22, 0.94); + background-color: rgba(18, 20, 26, 0.95); border-radius: 16px; - border: 1px solid rgba(255, 255, 255, 0.15); - box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.6); + border: 1px solid rgba(255, 255, 255, 0.18); + box-shadow: 0px 8px 30px rgba(0, 0, 0, 0.7); } label { color: #ffffff; - font-weight: bold; - font-size: 17px; - padding: 12px 24px; + font-weight: 800; + font-size: 16px; + padding: 12px 28px; } "#; let _ = css_provider.load_from_data(css.as_bytes()); @@ -72,12 +77,12 @@ pub fn show_notification_overlay() { ); } - let label = Label::new(Some("✅ Replay Saved!")); + let label = Label::new(Some(&msg_text)); window.add(&label); window.show_all(); let window_clone = window.clone(); - gtk::glib::timeout_add_local(Duration::from_secs(2), move || { + gtk::glib::timeout_add_local(Duration::from_millis(2200), move || { window_clone.close(); gtk::glib::ControlFlow::Break }); @@ -107,7 +112,7 @@ pub fn show_menu_overlay() { let window = ApplicationWindow::builder() .application(app) .default_width(860) - .default_height(430) + .default_height(470) .build(); #[cfg(target_os = "linux")] @@ -314,6 +319,15 @@ pub fn show_menu_overlay() { dir_lbl.set_halign(gtk::Align::Start); let dir_entry = Entry::new(); dir_entry.set_text(&config.output_directory); + let menu_hk_lbl = Label::new(Some("Menu Overlay Hotkey:")); + menu_hk_lbl.set_halign(gtk::Align::Start); + let menu_hk_entry = Entry::new(); + menu_hk_entry.set_text(&config.menu_hotkey); + + let record_hk_lbl = Label::new(Some("Toggle Record Hotkey:")); + record_hk_lbl.set_halign(gtk::Align::Start); + let record_hk_entry = Entry::new(); + record_hk_entry.set_text(&config.record_hotkey); settings_grid.attach(&dur_lbl, 0, 0, 1, 1); settings_grid.attach(&dur_combo, 1, 0, 1, 1); @@ -331,8 +345,10 @@ pub fn show_menu_overlay() { settings_grid.attach(&auto_switch, 1, 6, 1, 1); settings_grid.attach(&save_hk_lbl, 0, 7, 1, 1); settings_grid.attach(&save_hk_entry, 1, 7, 1, 1); - settings_grid.attach(&menu_hk_lbl, 0, 8, 1, 1); - settings_grid.attach(&menu_hk_entry, 1, 8, 1, 1); + settings_grid.attach(&record_hk_lbl, 0, 8, 1, 1); + settings_grid.attach(&record_hk_entry, 1, 8, 1, 1); + settings_grid.attach(&menu_hk_lbl, 0, 9, 1, 1); + settings_grid.attach(&menu_hk_entry, 1, 9, 1, 1); let save_settings_btn = Button::with_label("💾 Apply & Save Settings"); save_settings_btn.style_context().add_class("apply-btn"); @@ -508,6 +524,7 @@ pub fn show_menu_overlay() { cfg.autostart = auto_switch.is_active(); cfg.save_hotkey = save_hk_entry.text().to_string(); cfg.menu_hotkey = menu_hk_entry.text().to_string(); + cfg.record_hotkey = record_hk_entry.text().to_string(); let _ = cfg.save(); VrecConfig::notify_daemon_reload(); stack_after_save.set_visible_child_name("hud");