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:
parent
60ca91c067
commit
e4c324a50d
8 changed files with 130 additions and 38 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue