fix(ui): redesign overlay as top dock with 3 transparent cards and unified settings

This commit is contained in:
katR 2026-09-05 11:45:37 +05:30
parent 10a2ea5343
commit 98dc90e85c
9 changed files with 896 additions and 1182 deletions

View file

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

View file

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

View file

@ -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() {

View file

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

View file

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

View file

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

View file

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

View file

@ -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() {

File diff suppressed because it is too large Load diff