fix(ui): redesign overlay as top dock with 3 transparent cards and unified settings
This commit is contained in:
parent
10a2ea5343
commit
98dc90e85c
9 changed files with 896 additions and 1182 deletions
|
|
@ -14,7 +14,7 @@ RequestExecutionLevel user
|
||||||
|
|
||||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\vrec-ui.exe"
|
!define MUI_FINISHPAGE_RUN "$INSTDIR\vrec-ui.exe"
|
||||||
!define MUI_FINISHPAGE_RUN_PARAMETERS "--menu"
|
!define MUI_FINISHPAGE_RUN_PARAMETERS "--menu"
|
||||||
!define MUI_FINISHPAGE_RUN_TEXT "Launch vrec Studio Overlay"
|
!define MUI_FINISHPAGE_RUN_TEXT "Launch vrec"
|
||||||
!insertmacro MUI_PAGE_FINISH
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
!insertmacro MUI_UNPAGE_CONFIRM
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
|
@ -28,8 +28,7 @@ Section "vrec" SecMain
|
||||||
|
|
||||||
; Create Start Menu and Desktop shortcuts
|
; Create Start Menu and Desktop shortcuts
|
||||||
CreateDirectory "$SMPROGRAMS\vrec"
|
CreateDirectory "$SMPROGRAMS\vrec"
|
||||||
CreateShortcut "$SMPROGRAMS\vrec\vrec Studio.lnk" "$INSTDIR\vrec-ui.exe" "--menu"
|
CreateShortcut "$SMPROGRAMS\vrec\vrec.lnk" "$INSTDIR\vrec-ui.exe" "--menu"
|
||||||
CreateShortcut "$SMPROGRAMS\vrec\vrec Background Engine.lnk" "$INSTDIR\vrec-daemon.exe" ""
|
|
||||||
CreateShortcut "$DESKTOP\vrec.lnk" "$INSTDIR\vrec-ui.exe" "--menu"
|
CreateShortcut "$DESKTOP\vrec.lnk" "$INSTDIR\vrec-ui.exe" "--menu"
|
||||||
|
|
||||||
; Register uninstaller with Windows Control Panel / Settings
|
; Register uninstaller with Windows Control Panel / Settings
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
|
||||||
|
|
||||||
use vrec::capture::{Frame, FrameSource};
|
use vrec::capture::{Frame, FrameSource};
|
||||||
use vrec::encoder::VideoEncoder;
|
use vrec::encoder::VideoEncoder;
|
||||||
use vrec::ring::Packet;
|
use vrec::ring::Packet;
|
||||||
|
|
@ -99,9 +101,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let listener = {
|
let listener = {
|
||||||
let l = std::net::TcpListener::bind("127.0.0.1:42069")?;
|
match std::net::TcpListener::bind("127.0.0.1:42069") {
|
||||||
println!("Daemon listening on TCP IPC: 127.0.0.1:42069");
|
Ok(l) => {
|
||||||
l
|
println!("Daemon listening on TCP IPC: 127.0.0.1:42069");
|
||||||
|
l
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if vrec::ipc::query_status().is_ok() {
|
||||||
|
println!("Another instance of vrec-daemon is already running. Exiting cleanly.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
|
std::net::TcpListener::bind("127.0.0.1:42069")?
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let cmd_tx_ipc = cmd_tx.clone();
|
let cmd_tx_ipc = cmd_tx.clone();
|
||||||
|
|
@ -114,75 +127,90 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
for stream in listener.incoming() {
|
for stream in listener.incoming() {
|
||||||
match stream {
|
match stream {
|
||||||
Ok(mut stream) => {
|
Ok(mut stream) => {
|
||||||
let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(1000)));
|
let cmd_tx_ipc = cmd_tx_ipc.clone();
|
||||||
let _ = stream.set_write_timeout(Some(std::time::Duration::from_millis(1000)));
|
let is_recording_state_ipc = Arc::clone(&is_recording_state_ipc);
|
||||||
let mut len_buf = [0u8; 4];
|
let record_start_state_ipc = Arc::clone(&record_start_state_ipc);
|
||||||
if stream.read_exact(&mut len_buf).is_ok() {
|
let replay_enabled_state_ipc = Arc::clone(&replay_enabled_state_ipc);
|
||||||
let len = u32::from_le_bytes(len_buf) as usize;
|
let audio_muted_state_ipc = Arc::clone(&audio_muted_state_ipc);
|
||||||
let mut payload = vec![0u8; len];
|
|
||||||
if stream.read_exact(&mut payload).is_ok()
|
std::thread::spawn(move || {
|
||||||
&& let Ok(cmd) = serde_json::from_slice::<Command>(&payload) {
|
let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(1500)));
|
||||||
match cmd {
|
let _ = stream.set_write_timeout(Some(std::time::Duration::from_millis(1500)));
|
||||||
Command::GetStatus => {
|
let mut len_buf = [0u8; 4];
|
||||||
let rec = is_recording_state_ipc.load(Ordering::SeqCst);
|
if stream.read_exact(&mut len_buf).is_ok() {
|
||||||
let start_ts = record_start_state_ipc.load(Ordering::SeqCst);
|
let len = u32::from_le_bytes(len_buf) as usize;
|
||||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
let mut payload = vec![0u8; len];
|
||||||
let duration = if rec && start_ts > 0 {
|
if stream.read_exact(&mut payload).is_ok()
|
||||||
now.saturating_sub(start_ts)
|
&& let Ok(cmd) = serde_json::from_slice::<Command>(&payload) {
|
||||||
} else {
|
match cmd {
|
||||||
0
|
Command::GetStatus => {
|
||||||
};
|
let rec = is_recording_state_ipc.load(Ordering::SeqCst);
|
||||||
let cfg = vrec::config::VrecConfig::load();
|
let start_ts = record_start_state_ipc.load(Ordering::SeqCst);
|
||||||
let status = DaemonStatus {
|
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||||
is_recording: rec,
|
let duration = if rec && start_ts > 0 {
|
||||||
recording_duration_sec: duration,
|
now.saturating_sub(start_ts)
|
||||||
is_replay_active: replay_enabled_state_ipc.load(Ordering::SeqCst),
|
} else {
|
||||||
audio_muted: audio_muted_state_ipc.load(Ordering::SeqCst),
|
0
|
||||||
audio_mode: cfg.audio_mode,
|
};
|
||||||
show_cursor: cfg.show_cursor,
|
let cfg = vrec::config::VrecConfig::load();
|
||||||
mic_volume: cfg.mic_volume,
|
let status = DaemonStatus {
|
||||||
system_volume: cfg.system_volume,
|
is_recording: rec,
|
||||||
};
|
recording_duration_sec: duration,
|
||||||
if let Ok(resp) = serde_json::to_vec(&status) {
|
is_replay_active: replay_enabled_state_ipc.load(Ordering::SeqCst),
|
||||||
let len_resp = (resp.len() as u32).to_le_bytes();
|
audio_muted: audio_muted_state_ipc.load(Ordering::SeqCst),
|
||||||
let _ = stream.write_all(&len_resp);
|
audio_mode: cfg.audio_mode,
|
||||||
let _ = stream.write_all(&resp);
|
show_cursor: cfg.show_cursor,
|
||||||
|
mic_volume: cfg.mic_volume,
|
||||||
|
system_volume: cfg.system_volume,
|
||||||
|
};
|
||||||
|
if let Ok(resp) = serde_json::to_vec(&status) {
|
||||||
|
let len_resp = (resp.len() as u32).to_le_bytes();
|
||||||
|
let _ = stream.write_all(&len_resp);
|
||||||
|
let _ = stream.write_all(&resp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::ToggleCursor => {
|
||||||
|
let mut cfg = vrec::config::VrecConfig::load();
|
||||||
|
cfg.show_cursor = !cfg.show_cursor;
|
||||||
|
let _ = cfg.save();
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_default();
|
||||||
|
if session_type.to_lowercase() == "wayland" {
|
||||||
|
println!("Cursor display toggled to: {}. Restarting Wayland session...", cfg.show_cursor);
|
||||||
|
let _ = cmd_tx_ipc.send(Command::StopRecording);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Cursor display toggled to: {}", cfg.show_cursor);
|
||||||
|
},
|
||||||
|
Command::CycleAudioMode => {
|
||||||
|
let mut cfg = vrec::config::VrecConfig::load();
|
||||||
|
cfg.audio_mode = match cfg.audio_mode.as_str() {
|
||||||
|
"system" => "mic",
|
||||||
|
"mic" => "both",
|
||||||
|
"both" => "muted",
|
||||||
|
_ => "system",
|
||||||
|
}.to_string();
|
||||||
|
let _ = cfg.save();
|
||||||
|
println!("Audio mode cycled to: {}", cfg.audio_mode);
|
||||||
|
let _ = cmd_tx_ipc.try_send(Command::ReloadConfig);
|
||||||
|
},
|
||||||
|
Command::StopDaemon => {
|
||||||
|
println!("StopDaemon requested: Finalizing active recordings...");
|
||||||
|
vrec::hyprland_binds::unregister_hyprland_binds(&vrec::config::VrecConfig::load());
|
||||||
|
let _ = cmd_tx_ipc.send(Command::StopRecording);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(350));
|
||||||
|
std::process::exit(0);
|
||||||
|
},
|
||||||
|
other => {
|
||||||
|
let _ = cmd_tx_ipc.try_send(other);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Command::ToggleCursor => {
|
|
||||||
let mut cfg = vrec::config::VrecConfig::load();
|
|
||||||
cfg.show_cursor = !cfg.show_cursor;
|
|
||||||
let _ = cfg.save();
|
|
||||||
println!("Cursor display toggled to: {}. Restarting daemon for new capture session...", cfg.show_cursor);
|
|
||||||
let _ = cmd_tx_ipc.send(Command::StopRecording);
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
|
||||||
std::process::exit(0);
|
|
||||||
},
|
|
||||||
Command::CycleAudioMode => {
|
|
||||||
let mut cfg = vrec::config::VrecConfig::load();
|
|
||||||
cfg.audio_mode = match cfg.audio_mode.as_str() {
|
|
||||||
"system" => "mic",
|
|
||||||
"mic" => "both",
|
|
||||||
"both" => "muted",
|
|
||||||
_ => "system",
|
|
||||||
}.to_string();
|
|
||||||
let _ = cfg.save();
|
|
||||||
println!("Audio mode cycled to: {}", cfg.audio_mode);
|
|
||||||
let _ = cmd_tx_ipc.try_send(Command::ReloadConfig);
|
|
||||||
},
|
|
||||||
Command::StopDaemon => {
|
|
||||||
println!("StopDaemon requested: Finalizing active recordings...");
|
|
||||||
vrec::hyprland_binds::unregister_hyprland_binds(&vrec::config::VrecConfig::load());
|
|
||||||
let _ = cmd_tx_ipc.send(Command::StopRecording);
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(350));
|
|
||||||
std::process::exit(0);
|
|
||||||
},
|
|
||||||
other => {
|
|
||||||
let _ = cmd_tx_ipc.try_send(other);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Socket error: {}", err);
|
eprintln!("Socket error: {}", err);
|
||||||
|
|
@ -253,7 +281,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
Frame::Raw { width, height, .. } => (*width, *height),
|
Frame::Raw { width, height, .. } => (*width, *height),
|
||||||
Frame::DmaBuf { width, height, .. } => (*width, *height),
|
Frame::DmaBuf { width, height, .. } => (*width, *height),
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
Frame::D3D11Texture { .. } => (1920, 1080),
|
Frame::D3D11Texture { width, height, .. } => (*width, *height),
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = frame_tx.send(first_frame);
|
let _ = frame_tx.send(first_frame);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
|
||||||
|
|
||||||
use vrec::ipc::{Command, send_command, query_status};
|
use vrec::ipc::{Command, send_command, query_status};
|
||||||
use vrec::overlay::show_notification;
|
use vrec::overlay::show_notification;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
@ -35,7 +37,8 @@ fn ensure_wayland_env() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_daemon_cmd() -> std::process::Command {
|
fn get_daemon_cmd() -> std::process::Command {
|
||||||
if let Ok(mut path) = std::env::current_exe() {
|
#[allow(unused_mut)]
|
||||||
|
let mut cmd = if let Ok(mut path) = std::env::current_exe() {
|
||||||
path.pop();
|
path.pop();
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let candidate = path.join("vrec-daemon.exe");
|
let candidate = path.join("vrec-daemon.exe");
|
||||||
|
|
@ -43,33 +46,102 @@ fn get_daemon_cmd() -> std::process::Command {
|
||||||
let candidate = path.join("vrec-daemon");
|
let candidate = path.join("vrec-daemon");
|
||||||
|
|
||||||
if candidate.exists() {
|
if candidate.exists() {
|
||||||
return std::process::Command::new(candidate);
|
std::process::Command::new(candidate)
|
||||||
|
} else {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
std::process::Command::new("vrec-daemon.exe")
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
std::process::Command::new("vrec-daemon")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
std::process::Command::new("vrec-daemon.exe")
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
std::process::Command::new("vrec-daemon")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
return std::process::Command::new("vrec-daemon.exe");
|
{
|
||||||
#[cfg(not(target_os = "windows"))]
|
use std::os::windows::process::CommandExt;
|
||||||
return std::process::Command::new("vrec-daemon");
|
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ui_cmd() -> std::process::Command {
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut cmd = if let Ok(path) = std::env::current_exe() {
|
||||||
|
std::process::Command::new(path)
|
||||||
|
} else {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
std::process::Command::new("vrec-ui.exe")
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
std::process::Command::new("vrec-ui")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_daemon_running() {
|
fn ensure_daemon_running() {
|
||||||
if query_status().is_err() {
|
if query_status().is_err() {
|
||||||
println!("vrec-daemon not running. Auto-launching vrec-daemon...");
|
|
||||||
let _ = get_daemon_cmd()
|
let _ = get_daemon_cmd()
|
||||||
.stdin(std::process::Stdio::null())
|
.stdin(std::process::Stdio::null())
|
||||||
.stdout(std::process::Stdio::null())
|
.stdout(std::process::Stdio::null())
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(std::process::Stdio::null())
|
||||||
.spawn();
|
.spawn();
|
||||||
for _ in 0..20 {
|
for _ in 0..15 {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
if query_status().is_ok() {
|
if query_status().is_ok() {
|
||||||
println!("vrec-daemon successfully connected.");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_hotkeys_running() {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
use windows::Win32::Foundation::{CloseHandle, GetLastError, WIN32_ERROR};
|
||||||
|
use windows::Win32::System::Threading::CreateMutexW;
|
||||||
|
|
||||||
|
if let Ok(handle) = CreateMutexW(None, false, windows::core::w!("Global\\vrec_hotkeys_single_instance")) {
|
||||||
|
if GetLastError() == WIN32_ERROR(183) { // ERROR_ALREADY_EXISTS
|
||||||
|
let _ = CloseHandle(handle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = CloseHandle(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = get_ui_cmd()
|
||||||
|
.arg("--hotkeys")
|
||||||
|
.stdin(std::process::Stdio::null())
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn send_with_notification(cmd: Command, success_msg: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
fn send_with_notification(cmd: Command, success_msg: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
ensure_daemon_running();
|
ensure_daemon_running();
|
||||||
match send_command(cmd) {
|
match send_command(cmd) {
|
||||||
|
|
@ -135,6 +207,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
}
|
}
|
||||||
"--menu" => {
|
"--menu" => {
|
||||||
ensure_daemon_running();
|
ensure_daemon_running();
|
||||||
|
ensure_hotkeys_running();
|
||||||
vrec::overlay_egui::run_egui_overlay();
|
vrec::overlay_egui::run_egui_overlay();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -197,10 +270,23 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ensure_daemon_running();
|
ensure_daemon_running();
|
||||||
|
ensure_hotkeys_running();
|
||||||
vrec::overlay_egui::run_egui_overlay();
|
vrec::overlay_egui::run_egui_overlay();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let _mutex = unsafe {
|
||||||
|
use windows::Win32::Foundation::{GetLastError, WIN32_ERROR};
|
||||||
|
use windows::Win32::System::Threading::CreateMutexW;
|
||||||
|
|
||||||
|
let handle = CreateMutexW(None, true, windows::core::w!("Global\\vrec_hotkeys_single_instance"));
|
||||||
|
if GetLastError() == WIN32_ERROR(183) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
handle.ok()
|
||||||
|
};
|
||||||
|
|
||||||
println!("Starting vrec UI/Hotkey process...");
|
println!("Starting vrec UI/Hotkey process...");
|
||||||
|
|
||||||
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
|
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
|
||||||
|
|
@ -223,7 +309,13 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
println!("X11 session detected.");
|
println!("X11 session detected.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let manager = GlobalHotKeyManager::new()?;
|
let manager = match GlobalHotKeyManager::new() {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create global hotkey manager: {}", e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
let hotkey_menu = HotKey::new(Some(Modifiers::ALT), Code::KeyZ);
|
let hotkey_menu = HotKey::new(Some(Modifiers::ALT), Code::KeyZ);
|
||||||
let hotkey_save = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR);
|
let hotkey_save = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR);
|
||||||
let hotkey_record = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9);
|
let hotkey_record = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9);
|
||||||
|
|
@ -241,7 +333,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
if let Ok(event) = receiver.try_recv() {
|
if let Ok(event) = receiver.try_recv() {
|
||||||
if event.id == hotkey_menu.id() {
|
if event.id == hotkey_menu.id() {
|
||||||
ensure_daemon_running();
|
ensure_daemon_running();
|
||||||
vrec::overlay_egui::run_egui_overlay();
|
let _ = get_ui_cmd().arg("--menu").spawn();
|
||||||
} else if event.id == hotkey_save.id() {
|
} else if event.id == hotkey_save.id() {
|
||||||
let _ = send_with_notification(Command::SaveReplay, "Replay saved");
|
let _ = send_with_notification(Command::SaveReplay, "Replay saved");
|
||||||
} else if event.id == hotkey_record.id() {
|
} else if event.id == hotkey_record.id() {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ pub enum Frame {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
D3D11Texture {
|
D3D11Texture {
|
||||||
handle: usize,
|
handle: usize,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ pub struct WindowsCapture {
|
||||||
context: ID3D11DeviceContext,
|
context: ID3D11DeviceContext,
|
||||||
duplication: IDXGIOutputDuplication,
|
duplication: IDXGIOutputDuplication,
|
||||||
gpu_texture: ID3D11Texture2D,
|
gpu_texture: ID3D11Texture2D,
|
||||||
width: u32,
|
pub width: u32,
|
||||||
height: u32,
|
pub height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
@ -30,14 +30,40 @@ impl WindowsCapture {
|
||||||
// Initialize COM library on capture thread
|
// Initialize COM library on capture thread
|
||||||
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
|
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
|
||||||
|
|
||||||
// Create D3D11 hardware device
|
// Create DXGI Factory to find the display adapter with active outputs
|
||||||
|
let factory: IDXGIFactory1 = CreateDXGIFactory1()?;
|
||||||
|
let mut chosen_adapter: Option<IDXGIAdapter1> = None;
|
||||||
|
let mut chosen_output: Option<IDXGIOutput1> = None;
|
||||||
|
|
||||||
|
let mut a_idx = 0;
|
||||||
|
while let Ok(adapter) = factory.EnumAdapters1(a_idx) {
|
||||||
|
let mut o_idx = 0;
|
||||||
|
while let Ok(output) = adapter.EnumOutputs(o_idx) {
|
||||||
|
if let Ok(output1) = output.cast::<IDXGIOutput1>() {
|
||||||
|
chosen_adapter = Some(adapter);
|
||||||
|
chosen_output = Some(output1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
o_idx += 1;
|
||||||
|
}
|
||||||
|
if chosen_adapter.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
a_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (adapter, output1) = match (chosen_adapter, chosen_output) {
|
||||||
|
(Some(a), Some(o)) => (a, o),
|
||||||
|
_ => return Err("No active display output found for Windows desktop capture".into()),
|
||||||
|
};
|
||||||
|
|
||||||
let mut device: Option<ID3D11Device> = None;
|
let mut device: Option<ID3D11Device> = None;
|
||||||
let mut context: Option<ID3D11DeviceContext> = None;
|
let mut context: Option<ID3D11DeviceContext> = None;
|
||||||
let mut feature_level = D3D_FEATURE_LEVEL_11_0;
|
let mut feature_level = D3D_FEATURE_LEVEL_11_0;
|
||||||
|
|
||||||
D3D11CreateDevice(
|
D3D11CreateDevice(
|
||||||
None,
|
Some(&adapter),
|
||||||
D3D_DRIVER_TYPE_HARDWARE,
|
D3D_DRIVER_TYPE_UNKNOWN,
|
||||||
HMODULE(std::ptr::null_mut()),
|
HMODULE(std::ptr::null_mut()),
|
||||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
||||||
Some(&[D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0]),
|
Some(&[D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0]),
|
||||||
|
|
@ -50,21 +76,14 @@ impl WindowsCapture {
|
||||||
let device = device.ok_or("Failed to create D3D11 device")?;
|
let device = device.ok_or("Failed to create D3D11 device")?;
|
||||||
let context = context.ok_or("Failed to create D3D11 context")?;
|
let context = context.ok_or("Failed to create D3D11 context")?;
|
||||||
|
|
||||||
// Retrieve DXGI Output for the primary display
|
let desc = output1.GetDesc()?;
|
||||||
let dxgi_device: IDXGIDevice = device.cast()?;
|
let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs();
|
||||||
let adapter = dxgi_device.GetAdapter()?;
|
let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
|
||||||
let output = adapter.EnumOutputs(0)?;
|
|
||||||
let output1: IDXGIOutput1 = output.cast()?;
|
|
||||||
|
|
||||||
let desc = output.GetDesc()?;
|
|
||||||
let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left) as u32;
|
|
||||||
let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top) as u32;
|
|
||||||
|
|
||||||
// Initialize Desktop Duplication
|
// Initialize Desktop Duplication
|
||||||
let duplication = output1.DuplicateOutput(&device)?;
|
let duplication = output1.DuplicateOutput(&device)?;
|
||||||
|
|
||||||
// Allocate a dedicated VRAM-resident GPU texture for 100% zero-copy capture
|
// Allocate a dedicated VRAM-resident GPU texture for 100% zero-copy capture
|
||||||
// (Frame stays on GPU die, zero PCIe bus readback, 0% CPU consumption)
|
|
||||||
let gpu_desc = D3D11_TEXTURE2D_DESC {
|
let gpu_desc = D3D11_TEXTURE2D_DESC {
|
||||||
Width: width,
|
Width: width,
|
||||||
Height: height,
|
Height: height,
|
||||||
|
|
@ -99,10 +118,17 @@ impl WindowsCapture {
|
||||||
unsafe {
|
unsafe {
|
||||||
let dxgi_device: IDXGIDevice = self.device.cast()?;
|
let dxgi_device: IDXGIDevice = self.device.cast()?;
|
||||||
let adapter = dxgi_device.GetAdapter()?;
|
let adapter = dxgi_device.GetAdapter()?;
|
||||||
let output = adapter.EnumOutputs(0)?;
|
let mut o_idx = 0;
|
||||||
let output1: IDXGIOutput1 = output.cast()?;
|
while let Ok(output) = adapter.EnumOutputs(o_idx) {
|
||||||
self.duplication = output1.DuplicateOutput(&self.device)?;
|
if let Ok(output1) = output.cast::<IDXGIOutput1>() {
|
||||||
Ok(())
|
if let Ok(dup) = output1.DuplicateOutput(&self.device) {
|
||||||
|
self.duplication = dup;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o_idx += 1;
|
||||||
|
}
|
||||||
|
Err("Failed to reinitialize desktop duplication".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -120,24 +146,26 @@ impl FrameSource for WindowsCapture {
|
||||||
if let Some(resource) = desktop_resource {
|
if let Some(resource) = desktop_resource {
|
||||||
let texture: ID3D11Texture2D = resource.cast()?;
|
let texture: ID3D11Texture2D = resource.cast()?;
|
||||||
|
|
||||||
// Copy directly on the GPU from the desktop buffer to our persistent VRAM texture
|
// Copy directly on the GPU from desktop buffer to persistent VRAM texture
|
||||||
self.context.CopyResource(&self.gpu_texture, &texture);
|
self.context.CopyResource(&self.gpu_texture, &texture);
|
||||||
|
|
||||||
// Immediately release the desktop frame back to the DWM compositor
|
// Immediately release the desktop frame back to the DWM compositor
|
||||||
let _ = self.duplication.ReleaseFrame();
|
let _ = self.duplication.ReleaseFrame();
|
||||||
|
|
||||||
// Pass the VRAM texture handle directly to the hardware encoder (Zero CPU copy!)
|
|
||||||
return Ok(Frame::D3D11Texture {
|
return Ok(Frame::D3D11Texture {
|
||||||
handle: self.gpu_texture.as_raw() as usize,
|
handle: self.gpu_texture.as_raw() as usize,
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let _ = self.duplication.ReleaseFrame();
|
let _ = self.duplication.ReleaseFrame();
|
||||||
}
|
}
|
||||||
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
||||||
// Display is idle (no new pixels), yield current texture without spinning CPU
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(8));
|
std::thread::sleep(std::time::Duration::from_millis(8));
|
||||||
return Ok(Frame::D3D11Texture {
|
return Ok(Frame::D3D11Texture {
|
||||||
handle: self.gpu_texture.as_raw() as usize,
|
handle: self.gpu_texture.as_raw() as usize,
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
|
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
|
||||||
|
|
@ -154,6 +182,8 @@ impl FrameSource for WindowsCapture {
|
||||||
|
|
||||||
Ok(Frame::D3D11Texture {
|
Ok(Frame::D3D11Texture {
|
||||||
handle: self.gpu_texture.as_raw() as usize,
|
handle: self.gpu_texture.as_raw() as usize,
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@ impl VaapiEncoder {
|
||||||
av_frame_free(&mut drm_frame);
|
av_frame_free(&mut drm_frame);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
Frame::D3D11Texture { handle } => {
|
Frame::D3D11Texture { handle, .. } => {
|
||||||
println!("D3D11: Mock encoding D3D11 texture (Windows zero-copy). Handle: {}", handle);
|
println!("D3D11: Mock encoding D3D11 texture (Windows zero-copy). Handle: {}", handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
src/ipc.rs
18
src/ipc.rs
|
|
@ -87,11 +87,12 @@ pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
use std::net::TcpStream;
|
use std::net::{SocketAddr, TcpStream};
|
||||||
match TcpStream::connect("127.0.0.1:42069") {
|
let addr: SocketAddr = "127.0.0.1:42069".parse().unwrap();
|
||||||
|
match TcpStream::connect_timeout(&addr, Duration::from_millis(300)) {
|
||||||
Ok(mut stream) => {
|
Ok(mut stream) => {
|
||||||
let _ = stream.set_write_timeout(Some(Duration::from_millis(1000)));
|
let _ = stream.set_write_timeout(Some(Duration::from_millis(600)));
|
||||||
let _ = stream.set_read_timeout(Some(Duration::from_millis(1000)));
|
let _ = stream.set_read_timeout(Some(Duration::from_millis(600)));
|
||||||
if stream.write_all(&len_buf).is_ok() && stream.write_all(&payload).is_ok() {
|
if stream.write_all(&len_buf).is_ok() && stream.write_all(&payload).is_ok() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -128,14 +129,15 @@ pub fn query_status() -> Result<DaemonStatus, Box<dyn std::error::Error + Send +
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let stream_res = {
|
let stream_res = {
|
||||||
use std::net::TcpStream;
|
use std::net::{SocketAddr, TcpStream};
|
||||||
TcpStream::connect("127.0.0.1:42069")
|
let addr: SocketAddr = "127.0.0.1:42069".parse().unwrap();
|
||||||
|
TcpStream::connect_timeout(&addr, Duration::from_millis(300))
|
||||||
};
|
};
|
||||||
|
|
||||||
match stream_res {
|
match stream_res {
|
||||||
Ok(mut stream) => {
|
Ok(mut stream) => {
|
||||||
let _ = stream.set_write_timeout(Some(Duration::from_millis(1000)));
|
let _ = stream.set_write_timeout(Some(Duration::from_millis(600)));
|
||||||
let _ = stream.set_read_timeout(Some(Duration::from_millis(1000)));
|
let _ = stream.set_read_timeout(Some(Duration::from_millis(600)));
|
||||||
|
|
||||||
if stream.write_all(&len_buf).is_err() || stream.write_all(&payload).is_err() {
|
if stream.write_all(&len_buf).is_err() || stream.write_all(&payload).is_err() {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
137
src/overlay.rs
137
src/overlay.rs
|
|
@ -16,79 +16,96 @@ pub fn show_notification_overlay() {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_notification(message: &str) {
|
pub fn show_notification(message: &str) {
|
||||||
#[cfg(unix)]
|
#[cfg(target_os = "windows")]
|
||||||
if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() {
|
{
|
||||||
return;
|
let msg = message.to_string();
|
||||||
}
|
std::thread::spawn(move || {
|
||||||
if gtk::init().is_err() {
|
use std::os::windows::process::CommandExt;
|
||||||
|
let script = format!(
|
||||||
|
"[reflection.assembly]::loadwithpartialname('System.Windows.Forms') | Out-Null; \
|
||||||
|
$notify = new-object system.windows.forms.notifyicon; \
|
||||||
|
$notify.icon = [System.Drawing.SystemIcons]::Information; \
|
||||||
|
$notify.visible = $true; \
|
||||||
|
$notify.showballoontip(2000, 'vrec', '{}', [system.windows.forms.tooltipicon]::Info); \
|
||||||
|
Start-Sleep -Seconds 2; \
|
||||||
|
$notify.dispose()",
|
||||||
|
msg.replace('\'', "''")
|
||||||
|
);
|
||||||
|
let _ = std::process::Command::new("powershell")
|
||||||
|
.args(["-NoProfile", "-WindowStyle", "Hidden", "-Command", &script])
|
||||||
|
.creation_flags(0x08000000)
|
||||||
|
.output();
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let app = Application::builder()
|
#[cfg(not(target_os = "windows"))]
|
||||||
.application_id("com.vrec.notification")
|
{
|
||||||
.build();
|
if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if gtk::init().is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let msg_text = message.to_string();
|
let app = Application::builder()
|
||||||
app.connect_activate(move |app| {
|
.application_id("com.vrec.notification")
|
||||||
let window = ApplicationWindow::builder()
|
|
||||||
.application(app)
|
|
||||||
.default_width(320)
|
|
||||||
.default_height(60)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
let msg_text = message.to_string();
|
||||||
{
|
app.connect_activate(move |app| {
|
||||||
window.init_layer_shell();
|
let window = ApplicationWindow::builder()
|
||||||
window.set_layer(Layer::Overlay);
|
.application(app)
|
||||||
window.set_namespace("vrec-notification");
|
.default_width(320)
|
||||||
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30);
|
.default_height(60)
|
||||||
window.set_anchor(gtk_layer_shell::Edge::Top, true);
|
.build();
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
window.set_decorated(false);
|
window.init_layer_shell();
|
||||||
window.set_keep_above(true);
|
window.set_layer(Layer::Overlay);
|
||||||
window.set_skip_taskbar_hint(true);
|
window.set_namespace("vrec-notification");
|
||||||
window.set_position(gtk::WindowPosition::Center);
|
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30);
|
||||||
}
|
window.set_anchor(gtk_layer_shell::Edge::Top, true);
|
||||||
|
|
||||||
let css_provider = CssProvider::new();
|
|
||||||
let css = r#"
|
|
||||||
window {
|
|
||||||
background-color: #12141a;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8);
|
|
||||||
}
|
}
|
||||||
label {
|
|
||||||
color: #ffffff;
|
let css_provider = CssProvider::new();
|
||||||
font-weight: 700;
|
let css = r#"
|
||||||
font-size: 14px;
|
window {
|
||||||
padding: 10px 24px;
|
background-color: #12141a;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let _ = css_provider.load_from_data(css.as_bytes());
|
||||||
|
if let Some(screen) = gdk::Screen::default() {
|
||||||
|
StyleContext::add_provider_for_screen(
|
||||||
|
&screen,
|
||||||
|
&css_provider,
|
||||||
|
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
"#;
|
|
||||||
let _ = css_provider.load_from_data(css.as_bytes());
|
|
||||||
if let Some(screen) = gdk::Screen::default() {
|
|
||||||
StyleContext::add_provider_for_screen(
|
|
||||||
&screen,
|
|
||||||
&css_provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = Label::new(Some(&msg_text));
|
let label = Label::new(Some(&msg_text));
|
||||||
window.add(&label);
|
window.add(&label);
|
||||||
window.show_all();
|
window.show_all();
|
||||||
|
|
||||||
let window_clone = window.clone();
|
let window_clone = window.clone();
|
||||||
gtk::glib::timeout_add_local(Duration::from_millis(2200), move || {
|
gtk::glib::timeout_add_local(Duration::from_millis(2200), move || {
|
||||||
window_clone.close();
|
window_clone.close();
|
||||||
gtk::glib::ControlFlow::Break
|
gtk::glib::ControlFlow::Break
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
app.run_with_args(&[] as &[&str]);
|
app.run_with_args(&[] as &[&str]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_menu_overlay() {
|
pub fn show_menu_overlay() {
|
||||||
|
|
|
||||||
1556
src/overlay_egui.rs
1556
src/overlay_egui.rs
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue