virtual mode
This commit is contained in:
@@ -6,11 +6,15 @@ import { P2PClient } from "./webrtc-p2p.js";
|
||||
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-settings.js";
|
||||
|
||||
(async function () {
|
||||
const clientId = new URLSearchParams(location.search).get("clientId");
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const clientId = urlParams.get("clientId");
|
||||
if (!clientId) {
|
||||
alert("Missing clientId");
|
||||
return;
|
||||
}
|
||||
// The launcher uses mode=virtual. Keep mode=hidden as a compatibility alias
|
||||
// for older bookmarked URLs.
|
||||
const virtualMode = ["virtual", "hidden"].includes(urlParams.get("mode"));
|
||||
|
||||
const allowed = await checkFeatureAccess("hvnc", clientId);
|
||||
if (!allowed) return;
|
||||
@@ -18,6 +22,20 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
const clientLabel = document.getElementById("clientLabel");
|
||||
clientLabel.textContent = clientId;
|
||||
|
||||
if (virtualMode) {
|
||||
document.title = "Overlord Virtual";
|
||||
const headerSpan = document.querySelector(".p-3.text-sm.text-slate-400 span");
|
||||
if (headerSpan) {
|
||||
headerSpan.textContent = "Virtual Mode (Virtual Monitor) - Client: ";
|
||||
headerSpan.appendChild(clientLabel);
|
||||
}
|
||||
const ghostIcon = document.querySelector(".p-3.text-sm.text-slate-400 .fa-ghost");
|
||||
if (ghostIcon) {
|
||||
ghostIcon.classList.remove("fa-ghost", "text-violet-400");
|
||||
ghostIcon.classList.add("fa-eye-slash", "text-fuchsia-400");
|
||||
}
|
||||
}
|
||||
|
||||
let ws = null;
|
||||
let reconnectTimer = null;
|
||||
let reconnectDelay = 1000;
|
||||
@@ -78,9 +96,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
const viewerFps = document.getElementById("viewerFps");
|
||||
const statusEl = document.getElementById("streamStatus");
|
||||
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
||||
const dxgiCtrl = document.getElementById("dxgiCtrl");
|
||||
const uiaCtrl = document.getElementById("uiaCtrl");
|
||||
const hvncResolutionSelect = document.getElementById("hvncResolutionSelect");
|
||||
const targetFpsSelect = document.getElementById("targetFpsSelect");
|
||||
let whepClient = null;
|
||||
let p2pClient = null;
|
||||
let webrtcActive = false;
|
||||
@@ -92,7 +110,6 @@ 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 (dxgiCtrl) sendCmd("hvnc_enable_dxgi", { enabled: dxgiCtrl.checked });
|
||||
if (uiaCtrl) sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
|
||||
pushHvncResolution();
|
||||
}
|
||||
@@ -142,12 +159,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (!settings || typeof settings !== "object") return;
|
||||
savedDisplay = Number.isFinite(Number(settings.display)) ? Number(settings.display) : savedDisplay;
|
||||
setSelectValue(hvncResolutionSelect, settings.resolution);
|
||||
setSelectValue(targetFpsSelect, settings.targetFps);
|
||||
setSelectValue(webrtcMode, settings.webrtcMode);
|
||||
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
|
||||
if (mouseCtrl && typeof settings.mouse === "boolean") mouseCtrl.checked = settings.mouse;
|
||||
if (kbdCtrl && typeof settings.keyboard === "boolean") kbdCtrl.checked = settings.keyboard;
|
||||
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
|
||||
if (dxgiCtrl && typeof settings.dxgi === "boolean") dxgiCtrl.checked = settings.dxgi;
|
||||
if (uiaCtrl && typeof settings.uia === "boolean") uiaCtrl.checked = settings.uia;
|
||||
if (typeof settings.preferH264 === "boolean") {
|
||||
prefersH264 = settings.preferH264 && typeof VideoDecoder === "function";
|
||||
@@ -167,13 +184,13 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
return {
|
||||
display: Number(displaySelect?.value || 0),
|
||||
resolution: hvncResolutionSelect?.value || "1080",
|
||||
targetFps: Number(targetFpsSelect?.value || 120),
|
||||
quality: Number(qualitySlider?.value || 90),
|
||||
preferH264: !!prefersH264,
|
||||
webrtcMode: getWebrtcMode(),
|
||||
mouse: !!mouseCtrl?.checked,
|
||||
keyboard: !!kbdCtrl?.checked,
|
||||
clipboardSync: !!clipboardSyncCtrl?.checked,
|
||||
dxgi: !!dxgiCtrl?.checked,
|
||||
uia: !!uiaCtrl?.checked,
|
||||
cloneProfile: document.getElementById("hvncCloneToggle")?.checked !== false,
|
||||
cloneLite: document.getElementById("hvncCloneLiteToggle")?.checked === true,
|
||||
@@ -369,9 +386,11 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
display: parseInt(displaySelect.value, 10) || 0,
|
||||
});
|
||||
}
|
||||
pushTargetFps();
|
||||
sendCmd("hvnc_start", {
|
||||
autoStartExplorer: false,
|
||||
webrtc: mode === "relayed",
|
||||
...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}),
|
||||
});
|
||||
if (mode === "p2p") startP2P();
|
||||
syncInputEnableState();
|
||||
@@ -903,6 +922,22 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
sendCmd("hvnc_set_quality", { quality: q, codec });
|
||||
}
|
||||
|
||||
function selectedTargetFps() {
|
||||
const fps = Number(targetFpsSelect?.value || 120);
|
||||
return Number.isFinite(fps) ? Math.max(1, Math.min(240, Math.floor(fps))) : 120;
|
||||
}
|
||||
|
||||
function pushTargetFps() {
|
||||
sendCmd("hvnc_set_fps", { fps: selectedTargetFps() });
|
||||
}
|
||||
|
||||
if (targetFpsSelect) {
|
||||
targetFpsSelect.addEventListener("change", function () {
|
||||
pushTargetFps();
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
}
|
||||
|
||||
function pushTransportQuality(mode) {
|
||||
if (mode === "relayed" || mode === "p2p") {
|
||||
const q = Number(qualitySlider?.value) || 90;
|
||||
@@ -1046,12 +1081,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
});
|
||||
}
|
||||
pushTransportQuality(mode);
|
||||
pushTargetFps();
|
||||
desiredStreaming = true;
|
||||
lastFrameAt = 0;
|
||||
setStreamState("starting", "Starting stream");
|
||||
sendCmd("hvnc_start", {
|
||||
autoStartExplorer: false,
|
||||
webrtc: mode === "relayed",
|
||||
...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}),
|
||||
});
|
||||
if (mode === "p2p") startP2P();
|
||||
syncInputEnableState();
|
||||
@@ -1084,12 +1121,6 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
sendCmd("hvnc_enable_keyboard", { enabled: kbdCtrl.checked });
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
if (dxgiCtrl) {
|
||||
dxgiCtrl.addEventListener("change", function () {
|
||||
sendCmd("hvnc_enable_dxgi", { enabled: dxgiCtrl.checked });
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
}
|
||||
if (uiaCtrl) {
|
||||
uiaCtrl.addEventListener("change", function () {
|
||||
sendCmd("hvnc_enable_uia", { enabled: uiaCtrl.checked });
|
||||
@@ -1404,7 +1435,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (displaySelect && displaySelect.value !== undefined) {
|
||||
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) || 0 });
|
||||
}
|
||||
sendCmd("hvnc_start", { autoStartExplorer: false, webrtc: mode === "relayed" });
|
||||
pushTargetFps();
|
||||
sendCmd("hvnc_start", { autoStartExplorer: false, webrtc: mode === "relayed", ...(virtualMode ? { virtual_mode: true, hidden_mode: true } : {}) });
|
||||
if (mode === "p2p") startP2P();
|
||||
syncInputEnableState();
|
||||
} else {
|
||||
|
||||
@@ -441,6 +441,9 @@ function applyMenuSupportRules(clientId) {
|
||||
const hvncBtn = menu.querySelector('[data-open="Backstage"]');
|
||||
setAvailability(hvncBtn, 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");
|
||||
|
||||
const webcamBtn = menu.querySelector('[data-open="webcam"]');
|
||||
setAvailability(webcamBtn, isOnline && isWindows, isOnline ? "Webcam viewer is only supported on Windows clients." : "Client is offline");
|
||||
|
||||
@@ -497,6 +500,7 @@ const MENU_OPEN_TO_FEATURE = {
|
||||
console: "console",
|
||||
remotedesktop: "remote_desktop",
|
||||
Backstage: "hvnc",
|
||||
Hidden: "hvnc",
|
||||
webcam: "webcam",
|
||||
files: "file_browser",
|
||||
processes: "processes",
|
||||
@@ -2055,6 +2059,13 @@ menu.addEventListener("click", async (e) => {
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
}
|
||||
if (open === "Virtual") {
|
||||
// mode=hidden is understood by older hvnc.js builds; current builds treat
|
||||
// it as the virtual-monitor mode compatibility alias.
|
||||
window.open(`/hvnc?clientId=${contextCard}&mode=hidden`, "_blank", "noopener");
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
}
|
||||
if (open === "files") {
|
||||
const platform = detectClientPlatform(contextCard);
|
||||
if (platform !== "windows") {
|
||||
|
||||
@@ -9,6 +9,7 @@ const MENU_GROUPS = [
|
||||
{ label: "Console", icon: "fa-solid fa-terminal", icolor: "text-emerald-400", open: "console" },
|
||||
{ label: "Remote Desktop", icon: "fa-solid fa-desktop", icolor: "text-purple-400", open: "remotedesktop" },
|
||||
{ label: "Backstage", icon: "fa-solid fa-ghost", icolor: "text-violet-400", open: "Backstage" },
|
||||
{ label: "Virtual", icon: "fa-solid fa-eye-slash", icolor: "text-fuchsia-400", open: "Virtual", windowsOnly: true },
|
||||
{ label: "Voice", icon: "fa-solid fa-headset", icolor: "text-teal-400", open: "voice" },
|
||||
],
|
||||
},
|
||||
@@ -228,7 +229,7 @@ menuStyle.textContent = `
|
||||
transition: opacity 0.1s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Allow classList.add/remove("hidden") to work on items inside the menu */
|
||||
/* Allow classList.add/remove("Virtual") to work on items inside the menu */
|
||||
#command-menu .hidden { display: none !important; }
|
||||
/* Mobile: stack submenu below the main column, scroll the whole menu */
|
||||
@media (max-width: 600px) {
|
||||
@@ -463,14 +464,14 @@ export function openModal(src) {
|
||||
|
||||
setTimeout(() => {
|
||||
modalImg.src = src;
|
||||
modal.classList.remove("hidden");
|
||||
modal.classList.remove("Virtual");
|
||||
modal.classList.add("flex");
|
||||
}, 10);
|
||||
}
|
||||
|
||||
export function closeModal() {
|
||||
modal.classList.remove("flex");
|
||||
modal.classList.add("hidden");
|
||||
modal.classList.add("Virtual");
|
||||
}
|
||||
|
||||
export function wireModalClose() {
|
||||
|
||||
@@ -33,12 +33,6 @@
|
||||
<button id="commandsBtn" class="button ghost">
|
||||
<i class="fa-solid fa-terminal"></i><span>Commands</span>
|
||||
</button>
|
||||
<button id="killAllBtn" class="button ghost" title="Kill all programs running on the hidden desktop">
|
||||
<i class="fa-solid fa-skull"></i><span>Kill All</span>
|
||||
</button>
|
||||
<button id="windowMapBtn" class="button ghost" title="Show window positions and monitor layout">
|
||||
<i class="fa-solid fa-map"></i><span>Window Map</span>
|
||||
</button>
|
||||
<button id="fullscreenBtn" class="button ghost">
|
||||
<i class="fa-solid fa-expand"></i><span>Fullscreen</span>
|
||||
</button>
|
||||
@@ -156,6 +150,18 @@
|
||||
<option value="relayed">WebRTC Relayed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<label class="text-sm text-slate-300 w-20" for="targetFpsSelect">Max FPS</label>
|
||||
<select id="targetFpsSelect" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-100" title="Target capture and encoder frame rate">
|
||||
<option value="30">30 fps</option>
|
||||
<option value="60">60 fps</option>
|
||||
<option value="90">90 fps</option>
|
||||
<option value="120" selected>120 fps</option>
|
||||
<option value="144">144 fps</option>
|
||||
<option value="165">165 fps</option>
|
||||
<option value="240">240 fps</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Prefer H.264 when available; disable to force JPEG">
|
||||
<input type="checkbox" id="codecH264" checked /> H.264
|
||||
@@ -176,11 +182,6 @@
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Sync clipboard between your browser and the remote machine">
|
||||
<input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Use DXGI Present hook for faster capture (requires injected process). WARNING: If Chrome or HVNC is running slowly, DXGI capture may be the cause — uncheck this and restart Chrome inside HVNC to fix it.">
|
||||
<input type="checkbox" id="dxgiCtrl" />
|
||||
<span>DXGI Capture</span>
|
||||
<i class="fa-solid fa-triangle-exclamation text-amber-400 text-xs" title="If Chrome or HVNC is running slowly, DXGI capture may be the cause — uncheck this and restart Chrome inside HVNC to fix it."></i>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Use UI Automation for WinUI3 app input (modern Windows apps). Disable if input is unreliable in non-WinUI3 apps.">
|
||||
<input type="checkbox" id="uiaCtrl" /> UIA Support
|
||||
</label>
|
||||
@@ -198,7 +199,7 @@
|
||||
>
|
||||
<div class="p-3 text-sm text-slate-400 flex-shrink-0 flex items-center gap-2">
|
||||
<i class="fa-solid fa-ghost text-violet-400"></i>
|
||||
<span>Hidden Virtual Desktop - Client: <span id="clientLabel" class="text-slate-100">-</span></span>
|
||||
<span>Virtual Monitor - Client: <span id="clientLabel" class="text-slate-100">-</span></span>
|
||||
</div>
|
||||
<div
|
||||
class="flex-1 flex items-center justify-center p-4"
|
||||
@@ -429,30 +430,6 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Window Map Modal -->
|
||||
<div id="windowMapModal" class="fixed inset-0 z-[100] items-center justify-center bg-black/60 backdrop-blur-sm" style="display:none;">
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-[90vw] max-w-[900px] max-h-[85vh] flex flex-col">
|
||||
<div class="flex items-center justify-between px-5 py-3 border-b border-slate-800">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-map text-violet-400"></i>
|
||||
<span class="text-base font-semibold text-slate-100">Window Map</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="windowMapRefreshBtn" class="button ghost text-xs" title="Refresh window list">
|
||||
<i class="fa-solid fa-arrows-rotate"></i> Refresh
|
||||
</button>
|
||||
<button id="windowMapCloseBtn" class="button ghost text-xs">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-5 flex flex-col gap-4">
|
||||
<div id="windowMapCanvas" class="relative w-full bg-slate-950 rounded-lg border border-slate-800 overflow-hidden" style="min-height: 200px;"></div>
|
||||
<div id="windowMapList" class="flex flex-col gap-1 text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -774,6 +774,7 @@ function broadcastRemoteDesktopFrame(clientId: string, bytes: Uint8Array, header
|
||||
|
||||
type HVNCStreamingState = {
|
||||
isStreaming: boolean;
|
||||
virtualMode: boolean;
|
||||
display: number;
|
||||
quality: number;
|
||||
codec: string;
|
||||
@@ -782,7 +783,7 @@ type HVNCStreamingState = {
|
||||
};
|
||||
|
||||
function defaultHVNCStreamingState(): HVNCStreamingState {
|
||||
return { isStreaming: false, display: 0, quality: 90, codec: "", maxFps: 120, lastFps: 0 };
|
||||
return { isStreaming: false, virtualMode: false, display: 0, quality: 90, codec: "", maxFps: 120, lastFps: 0 };
|
||||
}
|
||||
|
||||
export const hvncStreamingState = new Map<string, HVNCStreamingState>();
|
||||
@@ -1135,6 +1136,13 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
logger.debug(`[hvnc] inbound viewer msg type=${payload.type} client=${clientId}`);
|
||||
switch (payload.type) {
|
||||
case "hvnc_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", {});
|
||||
state.isStreaming = false;
|
||||
logger.debug(`[hvnc] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
|
||||
}
|
||||
if (!state.isStreaming) {
|
||||
if ((payload as any).webrtc === true) {
|
||||
const streamPath = webrtcStreamPathFor(clientId, "hvnc");
|
||||
@@ -1156,13 +1164,16 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
sendHVNCCommand(target, "hvnc_set_fps", { fps: clampDesktopFps(state.maxFps) });
|
||||
sendHVNCCommand(target, "hvnc_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}`);
|
||||
logger.debug(`[hvnc] started streaming for client ${clientId} (virtual_mode=${virtualMode})`);
|
||||
} else {
|
||||
logger.debug(`[hvnc] ignoring duplicate hvnc_start for client ${clientId}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "hvnc_stop": {
|
||||
const otherHvncViewers = sessionManager.getHvncSessionsForClient(clientId)
|
||||
@@ -1195,8 +1206,8 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
case "hvnc_set_quality": {
|
||||
const newQuality = Number(payload.quality) || 90;
|
||||
const newCodec = String(payload.codec || "").toLowerCase();
|
||||
if (state.quality !== newQuality || state.codec !== newCodec) {
|
||||
sendHVNCCommand(target, "hvnc_set_quality", { quality: newQuality, codec: newCodec });
|
||||
sendHVNCCommand(target, "hvnc_set_quality", { quality: newQuality, codec: newCodec });
|
||||
if (state.quality !== newQuality || state.codec !== newCodec) {
|
||||
state.quality = newQuality;
|
||||
state.codec = newCodec;
|
||||
hvncStreamingState.set(clientId, state);
|
||||
@@ -1213,8 +1224,8 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
break;
|
||||
case "hvnc_set_fps": {
|
||||
const newMaxFps = clampDesktopFps((payload as any).fps);
|
||||
sendHVNCCommand(target, "hvnc_set_fps", { fps: newMaxFps });
|
||||
if (state.maxFps !== newMaxFps) {
|
||||
sendHVNCCommand(target, "hvnc_set_fps", { fps: newMaxFps });
|
||||
state.maxFps = newMaxFps;
|
||||
hvncStreamingState.set(clientId, state);
|
||||
logger.debug(`[hvnc] set target fps=${newMaxFps}`);
|
||||
|
||||
Reference in New Issue
Block a user