mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(desktop): harden capture and release packaging (#2049)
This commit is contained in:
@@ -47,6 +47,8 @@ if (targetNativeArch === 'universal' && targetPlatform !== 'darwin') {
|
||||
|
||||
const targetArchs = electronArch && electronArch !== 'universal' ? [electronArch] : supportedTargetArchs;
|
||||
const macTargetArchs = targetNativeArch ? [targetNativeArch] : supportedTargetArchs;
|
||||
const winGameCaptureTargetArchs =
|
||||
targetPlatform === 'win32' && targetNativeArch ? [targetNativeArch] : supportedTargetArchs;
|
||||
const winTargets = [
|
||||
{
|
||||
target: 'dir',
|
||||
@@ -150,11 +152,9 @@ const nativeRuntimeFilePatterns = [
|
||||
'node_modules/@fluxer/win-game-capture/package.json',
|
||||
'node_modules/@fluxer/win-game-capture/index.js',
|
||||
'node_modules/@fluxer/win-game-capture/loader-diagnostics.cjs',
|
||||
'node_modules/@fluxer/win-game-capture/*.node',
|
||||
'node_modules/@fluxer/win-game-capture/*.dll',
|
||||
'node_modules/@fluxer/win-game-capture/*.exe',
|
||||
'node_modules/@fluxer/win-game-capture/compatibility.json',
|
||||
'node_modules/@fluxer/win-game-capture/fluxer-vulkan-layer.*.json',
|
||||
...winGameCaptureTargetArchs.map(
|
||||
(arch) => `node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
|
||||
),
|
||||
'node_modules/@fluxer/win-clipboard/package.json',
|
||||
'node_modules/@fluxer/win-clipboard/index.js',
|
||||
'node_modules/@fluxer/win-clipboard/loader-diagnostics.cjs',
|
||||
@@ -224,11 +224,10 @@ const nativeRuntimeFilePatterns = [
|
||||
'node_modules/.pnpm/@fluxer+*/node_modules/@fluxer/*/loader-diagnostics.cjs',
|
||||
'node_modules/.pnpm/@fluxer+*/node_modules/@fluxer/*/pure.cjs',
|
||||
'node_modules/.pnpm/@fluxer+win-process-loopback@*/node_modules/@fluxer/win-process-loopback/*.node',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.node',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.dll',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.exe',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/compatibility.json',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/fluxer-vulkan-layer.*.json',
|
||||
...winGameCaptureTargetArchs.map(
|
||||
(arch) =>
|
||||
`node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
|
||||
),
|
||||
'node_modules/.pnpm/@fluxer+win-clipboard@*/node_modules/@fluxer/win-clipboard/*.node',
|
||||
'node_modules/.pnpm/@fluxer+win-shell@*/node_modules/@fluxer/win-shell/*.node',
|
||||
'node_modules/.pnpm/@fluxer+win-toast@*/node_modules/@fluxer/win-toast/*.node',
|
||||
@@ -352,6 +351,21 @@ function pnpmStoreDirName(packageName) {
|
||||
return packageName.replace('/', '+');
|
||||
}
|
||||
|
||||
function windowsGameCaptureArtifactExcludes(arch) {
|
||||
const packageRoots = [
|
||||
'node_modules/@fluxer/win-game-capture',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture',
|
||||
];
|
||||
const excludedNodeArchs = [...supportedTargetArchs, 'ia32'].filter((candidate) => candidate !== arch);
|
||||
return packageRoots.flatMap((packageRoot) => [
|
||||
`!${packageRoot}/compatibility.json`,
|
||||
`!${packageRoot}/fluxer-game-hook.*`,
|
||||
`!${packageRoot}/fluxer-inject-helper.*`,
|
||||
`!${packageRoot}/fluxer-vulkan-layer.*`,
|
||||
...excludedNodeArchs.map((excludedArch) => `!${packageRoot}/win-game-capture.win32-${excludedArch}-msvc.node`),
|
||||
]);
|
||||
}
|
||||
|
||||
function platformNativeExcludes(platform, arch) {
|
||||
const keepFluxerPackages = new Set(fluxerNativePackagesByPlatform[platform] ?? []);
|
||||
const fluxerPackageExcludes = fluxerNativePackages
|
||||
@@ -371,6 +385,7 @@ function platformNativeExcludes(platform, arch) {
|
||||
}
|
||||
return [
|
||||
...fluxerPackageExcludes,
|
||||
...windowsGameCaptureArtifactExcludes(arch),
|
||||
...velopackNativeFiles
|
||||
.filter((fileName) => fileName !== keepVelopackNativeFile)
|
||||
.map((fileName) => `!node_modules/velopack/lib/native/${fileName}`),
|
||||
@@ -403,22 +418,11 @@ function platformTag(platform, arch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function addWindowsGameCaptureArtifacts(artifacts, tag, arch) {
|
||||
const add = (relativePath) => {
|
||||
artifacts.push({
|
||||
packageName: '@fluxer/win-game-capture',
|
||||
relativePath,
|
||||
});
|
||||
};
|
||||
add(`win-game-capture.${tag}.node`);
|
||||
add(`fluxer-game-hook.${tag}.dll`);
|
||||
add(`fluxer-inject-helper.${tag}.exe`);
|
||||
add(`fluxer-vulkan-layer.${tag}.dll`);
|
||||
add(`fluxer-vulkan-layer.${tag}.json`);
|
||||
if (arch === 'x64') {
|
||||
add('fluxer-game-hook.win32-ia32-msvc.dll');
|
||||
add('fluxer-inject-helper.win32-ia32-msvc.exe');
|
||||
}
|
||||
function addWindowsGameCaptureArtifacts(artifacts, tag) {
|
||||
artifacts.push({
|
||||
packageName: '@fluxer/win-game-capture',
|
||||
relativePath: `win-game-capture.${tag}.node`,
|
||||
});
|
||||
}
|
||||
|
||||
function expectedNativeRuntimeArtifacts(platform, arch) {
|
||||
@@ -477,7 +481,7 @@ function expectedNativeRuntimeArtifactsForArch(platform, arch) {
|
||||
packageName: '@fluxer/win-process-loopback',
|
||||
relativePath: `win-process-loopback.${tag}.node`,
|
||||
});
|
||||
addWindowsGameCaptureArtifacts(artifacts, tag, arch);
|
||||
addWindowsGameCaptureArtifacts(artifacts, tag);
|
||||
artifacts.push({
|
||||
packageName: '@fluxer/win-clipboard',
|
||||
relativePath: `win-clipboard.${tag}.node`,
|
||||
@@ -1305,10 +1309,9 @@ module.exports = {
|
||||
asarUnpack: [
|
||||
'**/*.node',
|
||||
'node_modules/@fluxer/win-process-loopback/*.node',
|
||||
'node_modules/@fluxer/win-game-capture/*.node',
|
||||
'node_modules/@fluxer/win-game-capture/*.dll',
|
||||
'node_modules/@fluxer/win-game-capture/*.exe',
|
||||
'node_modules/@fluxer/win-game-capture/*.json',
|
||||
...winGameCaptureTargetArchs.map(
|
||||
(arch) => `node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
|
||||
),
|
||||
'node_modules/@fluxer/win-clipboard/*.node',
|
||||
'node_modules/@fluxer/win-shell/*.node',
|
||||
'node_modules/@fluxer/win-toast/*.node',
|
||||
@@ -1331,10 +1334,10 @@ module.exports = {
|
||||
'node_modules/@fluxer/webauthn/*.node',
|
||||
'node_modules/@fluxer/webauthn/*.so*',
|
||||
'node_modules/.pnpm/@fluxer+win-process-loopback@*/node_modules/@fluxer/win-process-loopback/*.node',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.node',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.dll',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.exe',
|
||||
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.json',
|
||||
...winGameCaptureTargetArchs.map(
|
||||
(arch) =>
|
||||
`node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
|
||||
),
|
||||
'node_modules/.pnpm/@fluxer+win-clipboard@*/node_modules/@fluxer/win-clipboard/*.node',
|
||||
'node_modules/.pnpm/@fluxer+win-shell@*/node_modules/@fluxer/win-shell/*.node',
|
||||
'node_modules/.pnpm/@fluxer+win-toast@*/node_modules/@fluxer/win-toast/*.node',
|
||||
|
||||
@@ -8,6 +8,10 @@ publish = false
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
game-capture-hook = []
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
|
||||
@@ -18,19 +18,7 @@
|
||||
"index.d.ts",
|
||||
"loader-diagnostics.cjs",
|
||||
"win-game-capture.win32-x64-msvc.node",
|
||||
"win-game-capture.win32-arm64-msvc.node",
|
||||
"fluxer-game-hook.win32-x64-msvc.dll",
|
||||
"fluxer-game-hook.win32-ia32-msvc.dll",
|
||||
"fluxer-game-hook.win32-arm64-msvc.dll",
|
||||
"fluxer-inject-helper.win32-x64-msvc.exe",
|
||||
"fluxer-inject-helper.win32-ia32-msvc.exe",
|
||||
"fluxer-inject-helper.win32-arm64-msvc.exe",
|
||||
"fluxer-vulkan-layer.win32-x64-msvc.dll",
|
||||
"fluxer-vulkan-layer.win32-x64-msvc.json",
|
||||
"fluxer-vulkan-layer.win32-ia32-msvc.dll",
|
||||
"fluxer-vulkan-layer.win32-ia32-msvc.json",
|
||||
"fluxer-vulkan-layer.win32-arm64-msvc.dll",
|
||||
"fluxer-vulkan-layer.win32-arm64-msvc.json"
|
||||
"win-game-capture.win32-arm64-msvc.node"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon",
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::ptr::{null, null_mut};
|
||||
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{HWND, LPARAM, RECT},
|
||||
Graphics::Gdi::{
|
||||
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITORINFO,
|
||||
MonitorFromRect,
|
||||
},
|
||||
System::Threading::GetCurrentProcessId,
|
||||
UI::WindowsAndMessaging::{
|
||||
EnumWindows, GW_OWNER, GWL_STYLE, GetForegroundWindow, GetWindow, GetWindowLongPtrW,
|
||||
GetWindowRect, GetWindowThreadProcessId, IsWindow, IsWindowVisible, WS_BORDER, WS_MAXIMIZE,
|
||||
},
|
||||
};
|
||||
|
||||
const MONITOR_LIMIT: usize = 16;
|
||||
|
||||
fn parse_hwnd_source_id(source_id: &str) -> Option<HWND> {
|
||||
let token = source_id.strip_prefix("window:")?.split(':').next()?;
|
||||
let value = if let Some(hex) = token
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| token.strip_prefix("0X"))
|
||||
{
|
||||
isize::from_str_radix(hex, 16).ok()?
|
||||
} else {
|
||||
token.parse::<isize>().ok()?
|
||||
};
|
||||
let hwnd = value as HWND;
|
||||
(unsafe { IsWindow(hwnd) } != 0).then_some(hwnd)
|
||||
}
|
||||
|
||||
fn parse_screen_ordinal(source_id: &str) -> Option<usize> {
|
||||
let token = source_id.strip_prefix("screen:")?.split(':').next()?;
|
||||
token.parse::<usize>().ok()
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_game_capture_target(
|
||||
source_id: &str,
|
||||
source_kind: &str,
|
||||
) -> Result<HWND, String> {
|
||||
if let Some(hwnd) = parse_hwnd_source_id(source_id) {
|
||||
return Ok(hwnd);
|
||||
}
|
||||
if source_kind != "game" && source_kind != "screen" {
|
||||
return Err(format!(
|
||||
"invalid game capture source: {source_kind}:{source_id}"
|
||||
));
|
||||
}
|
||||
let monitor = monitor_for_screen_source(source_id)?;
|
||||
find_fullscreen_window_on_monitor(monitor)
|
||||
.or_else(find_foreground_fullscreen_window)
|
||||
.or_else(find_fullscreen_window_on_any_monitor)
|
||||
.ok_or_else(|| "no fullscreen game window found on selected display".to_string())
|
||||
}
|
||||
|
||||
fn monitor_for_screen_source(source_id: &str) -> Result<HMONITOR, String> {
|
||||
let ordinal = parse_screen_ordinal(source_id).unwrap_or(0);
|
||||
let monitors = enumerate_monitors();
|
||||
monitors
|
||||
.get(ordinal)
|
||||
.copied()
|
||||
.or_else(|| monitors.first().copied())
|
||||
.ok_or_else(|| "no monitors available for game capture".to_string())
|
||||
}
|
||||
|
||||
fn enumerate_monitors() -> Vec<HMONITOR> {
|
||||
unsafe extern "system" fn enum_monitor(
|
||||
monitor: HMONITOR,
|
||||
_hdc: windows_sys::Win32::Graphics::Gdi::HDC,
|
||||
_rect: *mut RECT,
|
||||
param: LPARAM,
|
||||
) -> i32 {
|
||||
let monitors = &mut *(param as *mut Vec<HMONITOR>);
|
||||
if monitors.len() >= MONITOR_LIMIT {
|
||||
return 0;
|
||||
}
|
||||
monitors.push(monitor);
|
||||
1
|
||||
}
|
||||
let mut monitors = Vec::with_capacity(MONITOR_LIMIT);
|
||||
unsafe {
|
||||
EnumDisplayMonitors(
|
||||
null_mut(),
|
||||
null(),
|
||||
Some(enum_monitor),
|
||||
&mut monitors as *mut _ as LPARAM,
|
||||
);
|
||||
}
|
||||
assert!(monitors.len() <= MONITOR_LIMIT, "monitor targets bounded");
|
||||
monitors
|
||||
}
|
||||
|
||||
pub(crate) fn monitor_rect(monitor: HMONITOR) -> Option<RECT> {
|
||||
let mut info = MONITORINFO {
|
||||
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
|
||||
rcMonitor: RECT::default(),
|
||||
rcWork: RECT::default(),
|
||||
dwFlags: 0,
|
||||
};
|
||||
if unsafe { GetMonitorInfoW(monitor, &mut info) } == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(info.rcMonitor)
|
||||
}
|
||||
|
||||
fn rect_matches_monitor(window_rect: &RECT, monitor_rect: &RECT) -> bool {
|
||||
const TOLERANCE: i32 = 2;
|
||||
(window_rect.left - monitor_rect.left).abs() <= TOLERANCE
|
||||
&& (window_rect.top - monitor_rect.top).abs() <= TOLERANCE
|
||||
&& (window_rect.right - monitor_rect.right).abs() <= TOLERANCE
|
||||
&& (window_rect.bottom - monitor_rect.bottom).abs() <= TOLERANCE
|
||||
}
|
||||
|
||||
fn is_regular_maximized_window(hwnd: HWND) -> bool {
|
||||
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) } as u32;
|
||||
(style & WS_MAXIMIZE) != 0 && (style & WS_BORDER) != 0
|
||||
}
|
||||
|
||||
fn is_fullscreen_window_on_monitor(hwnd: HWND, monitor: HMONITOR, monitor_rect: &RECT) -> bool {
|
||||
if unsafe { IsWindowVisible(hwnd) } == 0 {
|
||||
return false;
|
||||
}
|
||||
if !unsafe { GetWindow(hwnd, GW_OWNER) }.is_null() {
|
||||
return false;
|
||||
}
|
||||
let mut pid = 0u32;
|
||||
unsafe {
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
}
|
||||
if pid == 0 || pid == unsafe { GetCurrentProcessId() } {
|
||||
return false;
|
||||
}
|
||||
if is_regular_maximized_window(hwnd) {
|
||||
return false;
|
||||
}
|
||||
let mut rect = RECT::default();
|
||||
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
|
||||
return false;
|
||||
}
|
||||
let window_monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
|
||||
window_monitor == monitor && rect_matches_monitor(&rect, monitor_rect)
|
||||
}
|
||||
|
||||
fn find_foreground_fullscreen_window() -> Option<HWND> {
|
||||
let hwnd = unsafe { GetForegroundWindow() };
|
||||
if hwnd.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut rect = RECT::default();
|
||||
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
|
||||
return None;
|
||||
}
|
||||
let monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
|
||||
let monitor_rect = monitor_rect(monitor)?;
|
||||
is_fullscreen_window_on_monitor(hwnd, monitor, &monitor_rect).then_some(hwnd)
|
||||
}
|
||||
|
||||
fn find_fullscreen_window_on_any_monitor() -> Option<HWND> {
|
||||
for monitor in enumerate_monitors() {
|
||||
if let Some(hwnd) = find_fullscreen_window_on_monitor(monitor) {
|
||||
return Some(hwnd);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_fullscreen_window_on_monitor(monitor: HMONITOR) -> Option<HWND> {
|
||||
struct Search {
|
||||
monitor: HMONITOR,
|
||||
monitor_rect: RECT,
|
||||
result: HWND,
|
||||
own_pid: u32,
|
||||
}
|
||||
unsafe extern "system" fn enum_window(hwnd: HWND, param: LPARAM) -> i32 {
|
||||
let search = &mut *(param as *mut Search);
|
||||
let mut pid = 0u32;
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
if pid == 0 || pid == search.own_pid {
|
||||
return 1;
|
||||
}
|
||||
if !is_fullscreen_window_on_monitor(hwnd, search.monitor, &search.monitor_rect) {
|
||||
return 1;
|
||||
}
|
||||
search.result = hwnd;
|
||||
0
|
||||
}
|
||||
|
||||
let monitor_rect = monitor_rect(monitor)?;
|
||||
let mut search = Search {
|
||||
monitor,
|
||||
monitor_rect,
|
||||
result: null_mut(),
|
||||
own_pid: unsafe { GetCurrentProcessId() },
|
||||
};
|
||||
unsafe {
|
||||
let _ = EnumWindows(Some(enum_window), &mut search as *mut _ as LPARAM);
|
||||
}
|
||||
(!search.result.is_null()).then_some(search.result)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use windows::Win32::{
|
||||
Foundation::HMODULE,
|
||||
Graphics::{
|
||||
Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN},
|
||||
Direct3D11::{
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device,
|
||||
ID3D11DeviceContext,
|
||||
},
|
||||
Dxgi::{IDXGIAdapter, IDXGIDevice},
|
||||
},
|
||||
};
|
||||
use windows::core::Interface;
|
||||
|
||||
pub(crate) fn create_shared_texture_device(
|
||||
adapter: Option<&IDXGIAdapter>,
|
||||
) -> Result<(ID3D11Device, ID3D11DeviceContext), String> {
|
||||
let mut device = None;
|
||||
let mut context = None;
|
||||
let driver_type = if adapter.is_some() {
|
||||
D3D_DRIVER_TYPE_UNKNOWN
|
||||
} else {
|
||||
D3D_DRIVER_TYPE_HARDWARE
|
||||
};
|
||||
unsafe {
|
||||
D3D11CreateDevice(
|
||||
adapter,
|
||||
driver_type,
|
||||
HMODULE::default(),
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
||||
None,
|
||||
D3D11_SDK_VERSION,
|
||||
Some(&mut device),
|
||||
None,
|
||||
Some(&mut context),
|
||||
)
|
||||
.map_err(|error| format!("D3D11CreateDevice for shared capture texture: {error}"))?;
|
||||
}
|
||||
let device = device.ok_or("D3D11 shared texture device was None")?;
|
||||
let context = context.ok_or("D3D11 shared texture context was None")?;
|
||||
if let Ok(dxgi_device) = device.cast::<IDXGIDevice>() {
|
||||
let _ = unsafe { dxgi_device.SetGPUThreadPriority(7) };
|
||||
}
|
||||
Ok((device, context))
|
||||
}
|
||||
@@ -394,6 +394,7 @@ pub(crate) fn resolve_output_size(
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(feature = "game-capture-hook")]
|
||||
pub(crate) fn wall_clock_us() -> i64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now()
|
||||
|
||||
@@ -5,7 +5,9 @@ use crate::game_capture_abi::{ENV_FORCE_CPU, ENV_VERBOSE, env_flag_enabled};
|
||||
use crate::nv12_gpu::Nv12GpuConverter;
|
||||
use crate::{
|
||||
CaptureInner,
|
||||
capture_target::{monitor_rect, resolve_game_capture_target},
|
||||
compatibility::{InjectionPolicy, injection_policy_for_window},
|
||||
d3d11_device::create_shared_texture_device,
|
||||
dxgi_capture::{resolve_output_size, wall_clock_us},
|
||||
emit_lifecycle, emit_shared_texture_frame,
|
||||
game_capture_abi::{
|
||||
@@ -26,26 +28,18 @@ use std::{
|
||||
sync::{Arc, Mutex, atomic::Ordering},
|
||||
};
|
||||
use windows::Win32::{
|
||||
Foundation::{HANDLE as WinHandle, HMODULE},
|
||||
Foundation::HANDLE as WinHandle,
|
||||
Graphics::{
|
||||
Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN},
|
||||
Direct3D11::{
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
|
||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
|
||||
},
|
||||
Dxgi::{CreateDXGIFactory1, IDXGIAdapter, IDXGIDevice, IDXGIFactory1},
|
||||
Direct3D11::{D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D},
|
||||
Dxgi::{CreateDXGIFactory1, IDXGIAdapter, IDXGIFactory1},
|
||||
},
|
||||
};
|
||||
use windows::core::Interface;
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{
|
||||
CloseHandle, FreeLibrary, HANDLE, HINSTANCE, HMODULE as WinSysHmodule, HWND,
|
||||
INVALID_HANDLE_VALUE, LPARAM, RECT, WAIT_ABANDONED, WAIT_OBJECT_0, WAIT_TIMEOUT,
|
||||
},
|
||||
Graphics::Gdi::{
|
||||
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITORINFO,
|
||||
MonitorFromRect,
|
||||
},
|
||||
Graphics::Gdi::{MONITOR_DEFAULTTONEAREST, MonitorFromRect},
|
||||
System::{
|
||||
Diagnostics::Debug::WriteProcessMemory,
|
||||
LibraryLoader::{GetModuleHandleW, GetProcAddress, LoadLibraryW},
|
||||
@@ -67,10 +61,8 @@ use windows_sys::Win32::{
|
||||
},
|
||||
},
|
||||
UI::WindowsAndMessaging::{
|
||||
EnumWindows, GW_OWNER, GWL_STYLE, GetClientRect, GetForegroundWindow, GetWindow,
|
||||
GetWindowLongPtrW, GetWindowRect, GetWindowThreadProcessId, HHOOK, IsWindow,
|
||||
IsWindowVisible, PostThreadMessageW, SetWindowsHookExW, UnhookWindowsHookEx, WH_GETMESSAGE,
|
||||
WM_NULL, WS_BORDER, WS_MAXIMIZE,
|
||||
GetClientRect, GetWindowRect, GetWindowThreadProcessId, HHOOK, IsWindow, IsWindowVisible,
|
||||
PostThreadMessageW, SetWindowsHookExW, UnhookWindowsHookEx, WH_GETMESSAGE, WM_NULL,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -812,87 +804,6 @@ fn inject_via_set_windows_hook(
|
||||
Ok(InstalledWindowsHook { hook, module })
|
||||
}
|
||||
|
||||
fn parse_hwnd_source_id(source_id: &str) -> Option<HWND> {
|
||||
let token = source_id.strip_prefix("window:")?.split(':').next()?;
|
||||
let value = if let Some(hex) = token
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| token.strip_prefix("0X"))
|
||||
{
|
||||
isize::from_str_radix(hex, 16).ok()?
|
||||
} else {
|
||||
token.parse::<isize>().ok()?
|
||||
};
|
||||
let hwnd = value as HWND;
|
||||
if unsafe { IsWindow(hwnd) } != 0 {
|
||||
Some(hwnd)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_screen_ordinal(source_id: &str) -> Option<usize> {
|
||||
let token = source_id.strip_prefix("screen:")?.split(':').next()?;
|
||||
token.parse::<usize>().ok()
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_game_capture_target(
|
||||
source_id: &str,
|
||||
source_kind: &str,
|
||||
) -> Result<HWND, String> {
|
||||
if let Some(hwnd) = parse_hwnd_source_id(source_id) {
|
||||
return Ok(hwnd);
|
||||
}
|
||||
if source_kind == "game" || source_kind == "screen" {
|
||||
let monitor = monitor_for_screen_source(source_id)?;
|
||||
if let Some(hwnd) = find_fullscreen_window_on_monitor(monitor) {
|
||||
return Ok(hwnd);
|
||||
}
|
||||
if let Some(hwnd) = find_foreground_fullscreen_window() {
|
||||
return Ok(hwnd);
|
||||
}
|
||||
if let Some(hwnd) = find_fullscreen_window_on_any_monitor() {
|
||||
return Ok(hwnd);
|
||||
}
|
||||
return Err("no fullscreen game window found on selected display".into());
|
||||
}
|
||||
Err(format!(
|
||||
"invalid game capture source: {source_kind}:{source_id}"
|
||||
))
|
||||
}
|
||||
|
||||
fn monitor_for_screen_source(source_id: &str) -> Result<HMONITOR, String> {
|
||||
let ordinal = parse_screen_ordinal(source_id).unwrap_or(0);
|
||||
let monitors = enumerate_monitors();
|
||||
monitors
|
||||
.get(ordinal)
|
||||
.copied()
|
||||
.or_else(|| monitors.first().copied())
|
||||
.ok_or_else(|| "no monitors available for game capture".to_string())
|
||||
}
|
||||
|
||||
fn enumerate_monitors() -> Vec<HMONITOR> {
|
||||
unsafe extern "system" fn enum_monitor(
|
||||
monitor: HMONITOR,
|
||||
_hdc: windows_sys::Win32::Graphics::Gdi::HDC,
|
||||
_rect: *mut RECT,
|
||||
param: LPARAM,
|
||||
) -> i32 {
|
||||
let monitors = &mut *(param as *mut Vec<HMONITOR>);
|
||||
monitors.push(monitor);
|
||||
1
|
||||
}
|
||||
let mut monitors = Vec::new();
|
||||
unsafe {
|
||||
EnumDisplayMonitors(
|
||||
null_mut(),
|
||||
null(),
|
||||
Some(enum_monitor),
|
||||
&mut monitors as *mut _ as LPARAM,
|
||||
);
|
||||
}
|
||||
monitors
|
||||
}
|
||||
|
||||
fn target_process_id(hwnd: HWND) -> Result<u32, String> {
|
||||
let mut pid = 0u32;
|
||||
unsafe {
|
||||
@@ -1039,155 +950,6 @@ fn choose_shared_buffer_dimensions(
|
||||
Some((buffer_width, buffer_height))
|
||||
}
|
||||
|
||||
fn monitor_rect(monitor: HMONITOR) -> Option<RECT> {
|
||||
let mut info = MONITORINFO {
|
||||
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
|
||||
rcMonitor: RECT::default(),
|
||||
rcWork: RECT::default(),
|
||||
dwFlags: 0,
|
||||
};
|
||||
if unsafe { GetMonitorInfoW(monitor, &mut info) } == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(info.rcMonitor)
|
||||
}
|
||||
|
||||
fn rect_matches_monitor(window_rect: &RECT, monitor_rect: &RECT) -> bool {
|
||||
let tolerance = 2;
|
||||
(window_rect.left - monitor_rect.left).abs() <= tolerance
|
||||
&& (window_rect.top - monitor_rect.top).abs() <= tolerance
|
||||
&& (window_rect.right - monitor_rect.right).abs() <= tolerance
|
||||
&& (window_rect.bottom - monitor_rect.bottom).abs() <= tolerance
|
||||
}
|
||||
|
||||
fn is_regular_maximized_window(hwnd: HWND) -> bool {
|
||||
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) } as u32;
|
||||
(style & WS_MAXIMIZE) != 0 && (style & WS_BORDER) != 0
|
||||
}
|
||||
|
||||
fn is_fullscreen_window_on_monitor(hwnd: HWND, monitor: HMONITOR, monitor_rect: &RECT) -> bool {
|
||||
if unsafe { IsWindowVisible(hwnd) } == 0 || !unsafe { GetWindow(hwnd, GW_OWNER) }.is_null() {
|
||||
return false;
|
||||
}
|
||||
let mut pid = 0u32;
|
||||
unsafe {
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
}
|
||||
if pid == 0 || pid == unsafe { GetCurrentProcessId() } {
|
||||
return false;
|
||||
}
|
||||
if is_regular_maximized_window(hwnd) {
|
||||
return false;
|
||||
}
|
||||
let mut rect = RECT::default();
|
||||
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
|
||||
return false;
|
||||
}
|
||||
let window_monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
|
||||
window_monitor == monitor && rect_matches_monitor(&rect, monitor_rect)
|
||||
}
|
||||
|
||||
fn find_foreground_fullscreen_window() -> Option<HWND> {
|
||||
let hwnd = unsafe { GetForegroundWindow() };
|
||||
if hwnd.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut rect = RECT::default();
|
||||
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
|
||||
return None;
|
||||
}
|
||||
let monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
|
||||
let monitor_rect = monitor_rect(monitor)?;
|
||||
if is_fullscreen_window_on_monitor(hwnd, monitor, &monitor_rect) {
|
||||
Some(hwnd)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn find_fullscreen_window_on_any_monitor() -> Option<HWND> {
|
||||
for monitor in enumerate_monitors() {
|
||||
if let Some(hwnd) = find_fullscreen_window_on_monitor(monitor) {
|
||||
return Some(hwnd);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_fullscreen_window_on_monitor(monitor: HMONITOR) -> Option<HWND> {
|
||||
struct Search {
|
||||
monitor: HMONITOR,
|
||||
monitor_rect: RECT,
|
||||
result: HWND,
|
||||
own_pid: u32,
|
||||
}
|
||||
unsafe extern "system" fn enum_window(hwnd: HWND, param: LPARAM) -> i32 {
|
||||
let search = &mut *(param as *mut Search);
|
||||
let mut pid = 0u32;
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
if pid != 0
|
||||
&& pid != search.own_pid
|
||||
&& is_fullscreen_window_on_monitor(hwnd, search.monitor, &search.monitor_rect)
|
||||
{
|
||||
search.result = hwnd;
|
||||
return 0;
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
let monitor_rect = monitor_rect(monitor)?;
|
||||
let mut search = Search {
|
||||
monitor,
|
||||
monitor_rect,
|
||||
result: null_mut(),
|
||||
own_pid: unsafe { GetCurrentProcessId() },
|
||||
};
|
||||
unsafe {
|
||||
let _ = EnumWindows(Some(enum_window), &mut search as *mut _ as LPARAM);
|
||||
}
|
||||
if search.result.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(search.result)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_shared_texture_device(
|
||||
adapter: Option<&IDXGIAdapter>,
|
||||
) -> Result<(ID3D11Device, ID3D11DeviceContext), String> {
|
||||
let mut device = None;
|
||||
let mut context = None;
|
||||
let driver_type = if adapter.is_some() {
|
||||
D3D_DRIVER_TYPE_UNKNOWN
|
||||
} else {
|
||||
D3D_DRIVER_TYPE_HARDWARE
|
||||
};
|
||||
unsafe {
|
||||
D3D11CreateDevice(
|
||||
adapter,
|
||||
driver_type,
|
||||
HMODULE::default(),
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
||||
None,
|
||||
D3D11_SDK_VERSION,
|
||||
Some(&mut device),
|
||||
None,
|
||||
Some(&mut context),
|
||||
)
|
||||
.map_err(|e| format!("D3D11CreateDevice for shared game texture: {e}"))?;
|
||||
}
|
||||
let device = device.ok_or("D3D11 shared texture device was None")?;
|
||||
let context = context.ok_or("D3D11 shared texture context was None")?;
|
||||
set_gpu_thread_priority(&device);
|
||||
Ok((device, context))
|
||||
}
|
||||
|
||||
fn set_gpu_thread_priority(device: &ID3D11Device) {
|
||||
if let Ok(dxgi_device) = device.cast::<IDXGIDevice>() {
|
||||
let _ = unsafe { dxgi_device.SetGPUThreadPriority(7) };
|
||||
}
|
||||
}
|
||||
|
||||
const SHARED_TEXTURE_ADAPTER_LIMIT: u32 = 16;
|
||||
|
||||
fn shared_texture_adapter_candidates() -> Vec<Option<IDXGIAdapter>> {
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
#![deny(clippy::all)]
|
||||
#![allow(unsafe_op_in_unsafe_fn)]
|
||||
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
#[cfg(target_os = "windows")]
|
||||
mod capture_target;
|
||||
#[cfg(any(all(target_os = "windows", feature = "game-capture-hook"), test))]
|
||||
mod compatibility;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod d3d11_device;
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
mod dxgi_capture;
|
||||
pub mod encoder_attach;
|
||||
mod fallback;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
mod game_capture;
|
||||
mod game_capture_abi;
|
||||
mod gpu_priority;
|
||||
@@ -19,7 +23,7 @@ mod nv12_gpu;
|
||||
mod sources;
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
mod stall;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
mod vulkan_layer_registry;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod wgc_capture;
|
||||
@@ -39,7 +43,7 @@ use dxgi_capture::DxgiCaptureSession;
|
||||
use fluxer_encoder_ring::EncoderFrameRate;
|
||||
#[cfg(target_os = "windows")]
|
||||
use fluxer_screen_frame_bus::EnqueueOutcome;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
use game_capture::GameCaptureSession;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
@@ -48,7 +52,6 @@ use wgc_capture::WgcCaptureSession;
|
||||
|
||||
const LIFECYCLE_QUEUE_LIMIT: usize = 8;
|
||||
const START_OPTION_UNSUPPORTED_LIMIT: usize = 4;
|
||||
const GAME_CAPTURE_HOOK_FORCE_DISABLED: bool = true;
|
||||
|
||||
type LifecycleTsfn = Arc<
|
||||
ThreadsafeFunction<
|
||||
@@ -226,7 +229,7 @@ pub struct CaptureInner {
|
||||
pub session: Mutex<Option<DxgiCaptureSession>>,
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) wgc_session: Mutex<Option<WgcCaptureSession>>,
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
pub game_session: Mutex<Option<Arc<GameCaptureSession>>>,
|
||||
pub running: std::sync::atomic::AtomicBool,
|
||||
pub fallback: Mutex<Option<fallback::FallbackTracker>>,
|
||||
@@ -421,6 +424,7 @@ pub(crate) fn note_media_frame_without_sink(inner: &CaptureInner, message: &'sta
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
pub(crate) fn note_cpu_fallback_frame_dropped(inner: &CaptureInner, message: &'static str) {
|
||||
inner
|
||||
.cpu_fallback_frames_dropped
|
||||
@@ -470,7 +474,7 @@ impl ScreenCapture {
|
||||
session: Mutex::new(None),
|
||||
#[cfg(target_os = "windows")]
|
||||
wgc_session: Mutex::new(None),
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
game_session: Mutex::new(None),
|
||||
running: std::sync::atomic::AtomicBool::new(false),
|
||||
fallback: Mutex::new(None),
|
||||
@@ -586,43 +590,48 @@ impl ScreenCapture {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let frame_sink = frame_sink_counter_snapshot(&self.inner);
|
||||
let guard = self.inner.game_session.lock();
|
||||
if let Some(session) = guard.as_ref() {
|
||||
let requested_injection_method = session.requested_injection_method().to_string();
|
||||
let injection_method = session.used_injection_method().to_string();
|
||||
if let Some(info) = session.read_shared_info() {
|
||||
return Some(CaptureDiagnostics {
|
||||
state: info.state,
|
||||
api_type: info.api_type,
|
||||
transport: info.transport,
|
||||
fallback_reason: info.fallback_reason,
|
||||
capture_flags: info.capture_flags,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
dxgi_format: info.dxgi_format,
|
||||
frame_counter: info.frame_counter as f64,
|
||||
dropped_frame_counter: info.dropped_frame_counter as f64,
|
||||
last_present_timestamp_us: info.last_present_timestamp_us as f64,
|
||||
last_error: info.last_error,
|
||||
#[cfg(feature = "game-capture-hook")]
|
||||
{
|
||||
let guard = self.inner.game_session.lock();
|
||||
if let Some(session) = guard.as_ref() {
|
||||
let requested_injection_method =
|
||||
session.requested_injection_method().to_string();
|
||||
let injection_method = session.used_injection_method().to_string();
|
||||
if let Some(info) = session.read_shared_info() {
|
||||
return Some(CaptureDiagnostics {
|
||||
state: info.state,
|
||||
api_type: info.api_type,
|
||||
transport: info.transport,
|
||||
fallback_reason: info.fallback_reason,
|
||||
capture_flags: info.capture_flags,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
dxgi_format: info.dxgi_format,
|
||||
frame_counter: info.frame_counter as f64,
|
||||
dropped_frame_counter: info.dropped_frame_counter as f64,
|
||||
last_present_timestamp_us: info.last_present_timestamp_us as f64,
|
||||
last_error: info.last_error,
|
||||
requested_injection_method,
|
||||
injection_method,
|
||||
active_strategy: snapshot.active_strategy,
|
||||
last_fallback_reason: snapshot.last_fallback_reason,
|
||||
start_options: current_start_options(&self.inner),
|
||||
frame_sink_accepted: frame_sink.accepted as f64,
|
||||
frame_sink_coalesced: frame_sink.coalesced as f64,
|
||||
frame_sink_rejected: frame_sink.rejected as f64,
|
||||
media_frames_dropped_without_sink: frame_sink.dropped_without_sink
|
||||
as f64,
|
||||
cpu_fallback_frames_dropped: frame_sink.cpu_fallback_dropped as f64,
|
||||
});
|
||||
}
|
||||
return Some(strategy_only_diagnostics(
|
||||
&snapshot,
|
||||
requested_injection_method,
|
||||
injection_method,
|
||||
active_strategy: snapshot.active_strategy,
|
||||
last_fallback_reason: snapshot.last_fallback_reason,
|
||||
start_options: current_start_options(&self.inner),
|
||||
frame_sink_accepted: frame_sink.accepted as f64,
|
||||
frame_sink_coalesced: frame_sink.coalesced as f64,
|
||||
frame_sink_rejected: frame_sink.rejected as f64,
|
||||
media_frames_dropped_without_sink: frame_sink.dropped_without_sink as f64,
|
||||
cpu_fallback_frames_dropped: frame_sink.cpu_fallback_dropped as f64,
|
||||
});
|
||||
current_start_options(&self.inner),
|
||||
frame_sink,
|
||||
));
|
||||
}
|
||||
return Some(strategy_only_diagnostics(
|
||||
&snapshot,
|
||||
requested_injection_method,
|
||||
injection_method,
|
||||
current_start_options(&self.inner),
|
||||
frame_sink,
|
||||
));
|
||||
}
|
||||
Some(strategy_only_diagnostics(
|
||||
&snapshot,
|
||||
@@ -669,7 +678,7 @@ impl ScreenCapture {
|
||||
|
||||
#[napi(js_name = "getSharedTextureHandle")]
|
||||
pub fn get_shared_texture_handle(&self) -> Option<SharedTextureHandleInfo> {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
{
|
||||
let guard = self.inner.game_session.lock();
|
||||
let session = guard.as_ref()?;
|
||||
@@ -684,7 +693,7 @@ impl ScreenCapture {
|
||||
}
|
||||
None
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
|
||||
{
|
||||
None
|
||||
}
|
||||
@@ -706,6 +715,9 @@ impl ScreenCapture {
|
||||
*guard = None;
|
||||
let mut wgc_guard = self.inner.wgc_session.lock();
|
||||
*wgc_guard = None;
|
||||
}
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
{
|
||||
let mut game_guard = self.inner.game_session.lock();
|
||||
*game_guard = None;
|
||||
}
|
||||
@@ -955,7 +967,8 @@ impl ScreenCapture {
|
||||
return Err(napi::Error::from_reason("Capture already running"));
|
||||
}
|
||||
|
||||
if source_kind == "game" && !GAME_CAPTURE_HOOK_FORCE_DISABLED {
|
||||
#[cfg(feature = "game-capture-hook")]
|
||||
if source_kind == "game" {
|
||||
return self.start_windows_game(
|
||||
source_id,
|
||||
source_kind,
|
||||
@@ -991,7 +1004,7 @@ impl ScreenCapture {
|
||||
}
|
||||
|
||||
let hwnd = if source_kind == "game" {
|
||||
let target = game_capture::resolve_game_capture_target(&source_id, &source_kind)
|
||||
let target = capture_target::resolve_game_capture_target(&source_id, &source_kind)
|
||||
.map_err(|e| {
|
||||
napi::Error::from_reason(format!("Failed to resolve game capture target: {e}"))
|
||||
})?;
|
||||
@@ -1115,6 +1128,7 @@ impl ScreenCapture {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "game-capture-hook")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn start_windows_game(
|
||||
&self,
|
||||
@@ -1278,7 +1292,7 @@ pub fn is_supported() -> bool {
|
||||
|
||||
#[napi(js_name = "isGameCaptureHookAvailable")]
|
||||
pub fn is_game_capture_hook_available() -> bool {
|
||||
cfg!(target_os = "windows") && !GAME_CAPTURE_HOOK_FORCE_DISABLED
|
||||
cfg!(all(target_os = "windows", feature = "game-capture-hook"))
|
||||
}
|
||||
|
||||
#[napi(js_name = "getAvailability")]
|
||||
@@ -1314,37 +1328,37 @@ pub fn restore_gpu_scheduling_priority(process_id: Option<u32>) -> Result<()> {
|
||||
|
||||
#[napi(js_name = "registerVulkanLayerManifest")]
|
||||
pub fn register_vulkan_layer_manifest(manifest_path: String) -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
{
|
||||
vulkan_layer_registry::register_manifest(&manifest_path).map_err(napi::Error::from_reason)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
|
||||
{
|
||||
let _ = manifest_path;
|
||||
Err(napi::Error::from_reason(
|
||||
"Vulkan game capture layer only supported on Windows",
|
||||
"Vulkan game capture layer is not included in this build",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "unregisterVulkanLayerManifest")]
|
||||
pub fn unregister_vulkan_layer_manifest(manifest_path: String) -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
{
|
||||
vulkan_layer_registry::unregister_manifest(&manifest_path).map_err(napi::Error::from_reason)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
|
||||
{
|
||||
let _ = manifest_path;
|
||||
Err(napi::Error::from_reason(
|
||||
"Vulkan game capture layer only supported on Windows",
|
||||
"Vulkan game capture layer is not included in this build",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "getVulkanLayerRegistrationState")]
|
||||
pub fn get_vulkan_layer_registration_state(manifest_path: String) -> VulkanLayerRegistrationState {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
|
||||
{
|
||||
let state = vulkan_layer_registry::registration_state(&manifest_path);
|
||||
VulkanLayerRegistrationState {
|
||||
@@ -1354,7 +1368,7 @@ pub fn get_vulkan_layer_registration_state(manifest_path: String) -> VulkanLayer
|
||||
manifest_path: state.manifest_path,
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
|
||||
{
|
||||
VulkanLayerRegistrationState {
|
||||
registered: false,
|
||||
|
||||
@@ -208,7 +208,7 @@ impl WgcCaptureSession {
|
||||
requested_height: Option<u32>,
|
||||
) -> Result<Self, String> {
|
||||
ensure_winrt_initialized();
|
||||
let (device, context) = crate::game_capture::create_shared_texture_device(None)?;
|
||||
let (device, context) = crate::d3d11_device::create_shared_texture_device(None)?;
|
||||
let dxgi_device: IDXGIDevice = device
|
||||
.cast()
|
||||
.map_err(|e| format!("IDXGIDevice cast: {e}"))?;
|
||||
|
||||
@@ -157,43 +157,58 @@ function collectRuntimeArtifactPaths(packageDir) {
|
||||
}
|
||||
|
||||
function isNativeRuntimeSidecar(fileName) {
|
||||
return (
|
||||
fileName.endsWith('.node') ||
|
||||
/\.so(?:\.|$)/.test(fileName) ||
|
||||
/\.(?:dll|exe)$/i.test(fileName) ||
|
||||
isWindowsNativeRuntimeManifest(fileName)
|
||||
);
|
||||
return fileName.endsWith('.node') || /\.so(?:\.|$)/.test(fileName) || /\.(?:dll|exe)$/i.test(fileName);
|
||||
}
|
||||
|
||||
function isWindowsNativeRuntimeManifest(fileName) {
|
||||
return (
|
||||
fileName === 'compatibility.json' || /^fluxer-vulkan-layer\.win32-(?:x64|ia32|arm64)-msvc\.json$/i.test(fileName)
|
||||
);
|
||||
function electronArch() {
|
||||
return process.env.ELECTRON_ARCH || process.env.npm_config_arch || process.arch;
|
||||
}
|
||||
|
||||
function addWinGameCaptureRuntimeArtifacts(artifacts, tag, arch) {
|
||||
const add = (relativePath) => {
|
||||
artifacts.push({
|
||||
label: '@fluxer/win-game-capture',
|
||||
relativePath,
|
||||
runtimeFiles: [],
|
||||
});
|
||||
};
|
||||
function primaryWinGameCaptureNodeFileName() {
|
||||
const arch = electronArch();
|
||||
const tag = platformTag(process.platform, arch);
|
||||
if (!tag || process.platform !== 'win32') {
|
||||
throw new Error(`Cannot resolve the primary win-game-capture node for ${process.platform}/${arch}`);
|
||||
}
|
||||
return `win-game-capture.${tag}.node`;
|
||||
}
|
||||
|
||||
function removeStaleWinGameCaptureArtifacts(packageDir, primaryNodeFileName) {
|
||||
if (!fs.existsSync(packageDir)) return;
|
||||
const stale = fs
|
||||
.readdirSync(packageDir, {withFileTypes: true})
|
||||
.filter((entry) => {
|
||||
if (!entry.isFile()) return false;
|
||||
return (
|
||||
entry.name.startsWith('fluxer-game-hook.') ||
|
||||
entry.name.startsWith('fluxer-inject-helper.') ||
|
||||
entry.name.startsWith('fluxer-vulkan-layer.') ||
|
||||
(entry.name.startsWith('win-game-capture.') &&
|
||||
entry.name.endsWith('.node') &&
|
||||
entry.name !== primaryNodeFileName)
|
||||
);
|
||||
})
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
for (const fileName of stale) {
|
||||
const artifactPath = path.join(packageDir, fileName);
|
||||
fs.rmSync(artifactPath);
|
||||
console.log(` Removed stale @fluxer/win-game-capture artifact ${path.relative(ROOT_DIR, artifactPath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function addWinGameCaptureRuntimeArtifacts(artifacts, tag) {
|
||||
artifacts.push({
|
||||
label: '@fluxer/win-game-capture',
|
||||
relativePath: `win-game-capture.${tag}.node`,
|
||||
});
|
||||
add(`fluxer-game-hook.${tag}.dll`);
|
||||
add(`fluxer-inject-helper.${tag}.exe`);
|
||||
add(`fluxer-vulkan-layer.${tag}.dll`);
|
||||
add(`fluxer-vulkan-layer.${tag}.json`);
|
||||
if (arch === 'x64') {
|
||||
add('fluxer-game-hook.win32-ia32-msvc.dll');
|
||||
add('fluxer-inject-helper.win32-ia32-msvc.exe');
|
||||
}
|
||||
}
|
||||
|
||||
function copyRuntimeArtifactsToInstalledPackages({label, packageDir}) {
|
||||
const primaryNodeFileName = label === '@fluxer/win-game-capture' ? primaryWinGameCaptureNodeFileName() : null;
|
||||
if (primaryNodeFileName) {
|
||||
removeStaleWinGameCaptureArtifacts(packageDir, primaryNodeFileName);
|
||||
}
|
||||
const artifacts = collectRuntimeArtifactPaths(packageDir);
|
||||
if (artifacts.length === 0) {
|
||||
return;
|
||||
@@ -206,6 +221,9 @@ function copyRuntimeArtifactsToInstalledPackages({label, packageDir}) {
|
||||
return;
|
||||
}
|
||||
for (const installedPackageDir of installedPackageDirs) {
|
||||
if (primaryNodeFileName) {
|
||||
removeStaleWinGameCaptureArtifacts(installedPackageDir, primaryNodeFileName);
|
||||
}
|
||||
for (const artifact of artifacts) {
|
||||
const sourcePath = path.join(packageDir, artifact);
|
||||
const targetPath = path.join(installedPackageDir, artifact);
|
||||
@@ -225,7 +243,7 @@ function platformTag(platform, arch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function expectedNativeRuntimeArtifacts(platform = process.platform, arch = process.env.ELECTRON_ARCH || process.arch) {
|
||||
function expectedNativeRuntimeArtifacts(platform = process.platform, arch = electronArch()) {
|
||||
if (platform === 'darwin' && arch === 'universal') {
|
||||
return [
|
||||
...expectedNativeRuntimeArtifactsForArch(platform, 'arm64'),
|
||||
@@ -284,7 +302,7 @@ function expectedNativeRuntimeArtifactsForArch(platform, arch) {
|
||||
label: '@fluxer/win-process-loopback',
|
||||
relativePath: `win-process-loopback.${tag}.node`,
|
||||
});
|
||||
addWinGameCaptureRuntimeArtifacts(artifacts, tag, arch);
|
||||
addWinGameCaptureRuntimeArtifacts(artifacts, tag);
|
||||
artifacts.push({
|
||||
label: '@fluxer/win-clipboard',
|
||||
relativePath: `win-clipboard.${tag}.node`,
|
||||
|
||||
@@ -688,7 +688,6 @@ export interface ElectronAPI {
|
||||
pasteFromClipboard: () => Promise<void>;
|
||||
onDeepLink: (callback: (url: string) => void) => () => void;
|
||||
getInitialDeepLink: () => Promise<string | null>;
|
||||
onRpcNavigate: (callback: (path: string) => void) => () => void;
|
||||
autostartEnable: () => Promise<void>;
|
||||
autostartDisable: () => Promise<void>;
|
||||
autostartIsEnabled: () => Promise<boolean>;
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import http from 'node:http';
|
||||
import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
|
||||
import {CANARY_APP_URL, STABLE_APP_URL} from '@electron/common/Constants';
|
||||
import {getCustomAppUrl} from '@electron/common/DesktopConfig';
|
||||
import {getMainWindow, showWindow} from '@electron/main/Window';
|
||||
import {app} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
const RPC_PORT = BUILD_CHANNEL === 'canary' ? 21864 : 21863;
|
||||
const ALLOWED_ORIGINS = [STABLE_APP_URL, CANARY_APP_URL];
|
||||
const isAllowedOrigin = (origin?: string): boolean => {
|
||||
if (!origin) return false;
|
||||
if (ALLOWED_ORIGINS.includes(origin)) return true;
|
||||
const customUrl = getCustomAppUrl();
|
||||
return customUrl != null && origin === customUrl;
|
||||
};
|
||||
const refererMatchesAllowedOrigin = (referer?: string): boolean => {
|
||||
if (!referer) return false;
|
||||
if (ALLOWED_ORIGINS.some((allowed) => referer.startsWith(allowed))) return true;
|
||||
const customUrl = getCustomAppUrl();
|
||||
return customUrl != null && referer.startsWith(customUrl);
|
||||
};
|
||||
|
||||
let server: http.Server | null = null;
|
||||
|
||||
interface RpcRequest {
|
||||
method: string;
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface RpcResponse {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const sendJson = (res: http.ServerResponse, status: number, data: RpcResponse) => {
|
||||
res.writeHead(status, {'Content-Type': 'application/json'});
|
||||
res.end(JSON.stringify(data));
|
||||
};
|
||||
const rejectIfDisallowedPage = (req: http.IncomingMessage, res: http.ServerResponse): boolean => {
|
||||
const origin = req.headers.origin;
|
||||
const referer = req.headers.referer;
|
||||
if (!origin && !referer) {
|
||||
res.writeHead(403);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
if (origin && !isAllowedOrigin(origin)) {
|
||||
res.writeHead(403);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
if (referer && !refererMatchesAllowedOrigin(referer)) {
|
||||
res.writeHead(403);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
if (origin && referer && !referer.startsWith(origin)) {
|
||||
res.writeHead(403);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const handleCors = (req: http.IncomingMessage, res: http.ServerResponse): boolean => {
|
||||
const origin = req.headers.origin;
|
||||
if (origin && !isAllowedOrigin(origin)) {
|
||||
res.writeHead(403);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
if (origin && isAllowedOrigin(origin)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.setHeader('Access-Control-Max-Age', '86400');
|
||||
}
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const parseBody = (req: http.IncomingMessage): Promise<RpcRequest | null> => {
|
||||
return new Promise((resolve) => {
|
||||
if (req.method !== 'POST') {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
let body = '';
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
if (body.length > 1024 * 1024) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(body) as RpcRequest);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
req.on('error', () => resolve(null));
|
||||
});
|
||||
};
|
||||
const handleHealth = (_req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
sendJson(res, 200, {
|
||||
success: true,
|
||||
data: {
|
||||
status: 'ok',
|
||||
channel: BUILD_CHANNEL,
|
||||
version: app.getVersion(),
|
||||
platform: process.platform,
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleNavigate = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
const body = await parseBody(req);
|
||||
if (!body?.params?.path || typeof body.params['path'] !== 'string') {
|
||||
sendJson(res, 400, {success: false, error: 'Missing or invalid path parameter'});
|
||||
return;
|
||||
}
|
||||
const path = body.params['path'];
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
sendJson(res, 503, {success: false, error: 'Main window not available'});
|
||||
return;
|
||||
}
|
||||
mainWindow.webContents.send('rpc-navigate', path);
|
||||
showWindow();
|
||||
sendJson(res, 200, {success: true, data: {navigated: true, path}});
|
||||
};
|
||||
const handleFocus = (_req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
sendJson(res, 503, {success: false, error: 'Main window not available'});
|
||||
return;
|
||||
}
|
||||
showWindow();
|
||||
sendJson(res, 200, {success: true, data: {focused: true}});
|
||||
};
|
||||
const requestHandler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
const remoteAddress = req.socket.remoteAddress;
|
||||
if (remoteAddress !== '127.0.0.1' && remoteAddress !== '::1' && remoteAddress !== '::ffff:127.0.0.1') {
|
||||
res.writeHead(403);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (rejectIfDisallowedPage(req, res)) {
|
||||
return;
|
||||
}
|
||||
if (handleCors(req, res)) {
|
||||
return;
|
||||
}
|
||||
const url = req.url ?? '/';
|
||||
try {
|
||||
switch (url) {
|
||||
case '/health':
|
||||
handleHealth(req, res);
|
||||
break;
|
||||
case '/navigate':
|
||||
await handleNavigate(req, res);
|
||||
break;
|
||||
case '/focus':
|
||||
handleFocus(req, res);
|
||||
break;
|
||||
default:
|
||||
sendJson(res, 404, {success: false, error: 'Not found'});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('[RPC] Request handler error:', error);
|
||||
sendJson(res, 500, {success: false, error: 'Internal server error'});
|
||||
}
|
||||
};
|
||||
export const startRpcServer = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (server) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
server = http.createServer(requestHandler);
|
||||
server.on('error', (error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
log.warn(`[RPC] Port ${RPC_PORT} already in use, RPC server disabled`);
|
||||
server = null;
|
||||
resolve();
|
||||
} else {
|
||||
log.error('[RPC] Server error:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
server.listen(RPC_PORT, '127.0.0.1', () => {
|
||||
log.info(`[RPC] Server listening on http://127.0.0.1:${RPC_PORT}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
export const stopRpcServer = (): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (!server) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
log.error('[RPC] Error closing server:', err);
|
||||
}
|
||||
server = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -68,7 +68,6 @@ import {
|
||||
import {runNativeModulePreflight} from '@electron/main/NativeModulePreflight';
|
||||
import {cleanupNativeScreenCapture, registerNativeScreenCaptureHandlers} from '@electron/main/NativeScreenCapture';
|
||||
import {appendOpenH264Switches} from '@electron/main/OpenH264Manager';
|
||||
import {startRpcServer, stopRpcServer} from '@electron/main/RpcServer';
|
||||
import {cleanupLinuxChromiumSpellcheckDictionaries} from '@electron/main/Spellcheck';
|
||||
import {registerUpdater} from '@electron/main/Updater';
|
||||
import {
|
||||
@@ -430,9 +429,6 @@ if (launchConfigurationError) {
|
||||
showWindow();
|
||||
}
|
||||
});
|
||||
void startRpcServer().catch((error: unknown) => {
|
||||
log.error('[RPC] Failed to start RPC server:', error);
|
||||
});
|
||||
log.info('App initialized successfully');
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
@@ -478,7 +474,7 @@ if (launchConfigurationError) {
|
||||
cleanupNativeHardwareEncoderHandlers();
|
||||
cleanupVirtmic();
|
||||
destroyDesktopTray();
|
||||
const asyncCleanups: Array<Promise<unknown>> = [stopRpcServer()];
|
||||
const asyncCleanups: Array<Promise<unknown>> = [];
|
||||
if (netLog.currentlyLogging) {
|
||||
asyncCleanups.push(
|
||||
netLog.stopLogging().catch((error) => {
|
||||
|
||||
@@ -425,15 +425,6 @@ const api: ElectronAPI = {
|
||||
};
|
||||
},
|
||||
getInitialDeepLink: (): Promise<string | null> => ipcRenderer.invoke('get-initial-deep-link'),
|
||||
onRpcNavigate: (callback: (path: string) => void): (() => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, path: string): void => {
|
||||
callback(path);
|
||||
};
|
||||
ipcRenderer.on('rpc-navigate', handler);
|
||||
return () => {
|
||||
ipcRenderer.removeListener('rpc-navigate', handler);
|
||||
};
|
||||
},
|
||||
autostartEnable: (): Promise<void> => ipcRenderer.invoke('autostart-enable'),
|
||||
autostartDisable: (): Promise<void> => ipcRenderer.invoke('autostart-disable'),
|
||||
autostartIsEnabled: (): Promise<boolean> => ipcRenderer.invoke('autostart-is-enabled'),
|
||||
|
||||
Reference in New Issue
Block a user