feat: Add faststart web MP4 streaming, graceful SIGINT/SIGTERM shutdown protection, dynamic in-game HUD notifications, dedicated record hotkey, and desktop game audio priority

This commit is contained in:
katR 2026-09-03 16:51:04 +05:30
parent 60ca91c067
commit e4c324a50d
8 changed files with 130 additions and 38 deletions

View file

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

View file

@ -277,6 +277,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
}
},
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 => {

View file

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

View file

@ -16,7 +16,10 @@ pub fn list_input_devices() -> Vec<String> {
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);

View file

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

View file

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

View file

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

View file

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