major remote desktop improvements

This commit is contained in:
KingKDot
2026-07-30 00:22:08 -05:00
parent a68172538d
commit 095a119bf4
35 changed files with 984 additions and 120 deletions
+7 -7
View File
@@ -26,7 +26,7 @@ export async function showCertBannerIfNeeded(anchor) {
} catch { }
}
const os = detectOS();
const os = getCertificateTrustGuide();
const banner = document.createElement("div");
banner.id = "cert-trust-banner";
@@ -45,7 +45,7 @@ export async function showCertBannerIfNeeded(anchor) {
</p>
<div class="flex flex-wrap items-center gap-2">
<a href="/api/cert/download"
download="overlord-ca.crt"
download="overlord-server.crt"
class="inline-flex items-center gap-1.5 rounded bg-amber-600 hover:bg-amber-500
px-3 py-1 text-xs font-medium text-white transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2"
@@ -80,13 +80,13 @@ export async function showCertBannerIfNeeded(anchor) {
});
}
function detectOS() {
export function getCertificateTrustGuide() {
const ua = navigator.userAgent.toLowerCase();
if (ua.includes("win")) {
return {
label: "Windows",
instructions:
`1. Double-click the downloaded <strong>overlord-ca.crt</strong> file.
`1. Double-click the downloaded <strong>overlord-server.crt</strong> file.
2. Click <strong>Install Certificate...</strong>
3. Choose <strong>Current User</strong> or <strong>Local Machine</strong>.
4. Select <strong>"Place all certificates in the following store"</strong>.
@@ -99,7 +99,7 @@ function detectOS() {
return {
label: "macOS",
instructions:
`1. Double-click the downloaded <strong>overlord-ca.crt</strong> file — it opens in Keychain Access.
`1. Double-click the downloaded <strong>overlord-server.crt</strong> file — it opens in Keychain Access.
2. If prompted, add it to the <strong>login</strong> or <strong>System</strong> keychain.
3. Find the certificate in the list, double-click it.
4. Expand <strong>Trust</strong> and set <strong>"When using this certificate"</strong> to <strong>Always Trust</strong>.
@@ -111,11 +111,11 @@ function detectOS() {
label: "Linux",
instructions:
`<strong>Debian / Ubuntu:</strong>
sudo cp overlord-ca.crt /usr/local/share/ca-certificates/overlord-ca.crt
sudo cp overlord-server.crt /usr/local/share/ca-certificates/overlord-server.crt
sudo update-ca-certificates
<strong>Fedora / RHEL:</strong>
sudo cp overlord-ca.crt /etc/pki/ca-trust/source/anchors/overlord-ca.crt
sudo cp overlord-server.crt /etc/pki/ca-trust/source/anchors/overlord-server.crt
sudo update-ca-trust
Then <strong>restart your browser</strong>.
+93 -32
View File
@@ -153,6 +153,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
let pendingFrame = null;
let videoDecoder = null;
let videoDecoderConfigKey = "";
let pendingDecodedVideoFrame = null;
let decodedVideoPresentationFrame = 0;
const disabledDecoderCodecs = new Set();
let h264StreamCodec = "";
let canvasDecodePressure = false;
@@ -167,6 +169,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
let h264KeyframeErrorStreak = 0;
let h264RecoveryAttempts = 0;
let h264LastDecodeWarnAt = 0;
let h264WaitingForKeyframe = true;
const H264_LOW_FPS_THRESHOLD = 6;
const H264_FALLBACK_WARMUP_MS = 10000;
const H264_MIN_FRAMES_BEFORE_FALLBACK = 120;
@@ -175,9 +178,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
const H264_MAX_RECOVERY_ATTEMPTS = 1;
const H264_DECODE_WARN_THROTTLE_MS = 2000;
const inputBackpressureBytes = 256 * 1024;
const VIEWER_FRAME_GAP_KEYFRAME_MS = 500;
const VIEWER_FRAME_GAP_KEYFRAME_THROTTLE_MS = 1000;
let lastViewerFrameGapKeyframeAt = 0;
let lastViewerKeyframeRequestAt = 0;
const diagnostics = {
agent: null,
network: null,
@@ -240,6 +242,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
h264FirstFrameAt = 0;
h264FramesSeen = 0;
h264KeyframeErrorStreak = 0;
h264WaitingForKeyframe = true;
}
/* ── Remote Desktop Audio (system audio from client) ── */
@@ -1139,6 +1142,15 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
} else if ((finite(media.lossPercent) ?? 0) > 3 || (finite(network.rttMs) ?? 0) > 150 || (finite(media.jitterMs) ?? 0) > 35) {
summary = "Network conditions are causing delay";
severity = "bad";
} else if (
mode === "off" &&
fps != null &&
viewerRate != null &&
viewerRate < fps * 0.8 &&
diagnostics.coalescedFrames > 0
) {
summary = `Display is presenting ${Math.round(viewerRate)} of ${Math.round(fps)} decoded FPS`;
severity = "warn";
} else if ((queue ?? 0) > 2 || (renderMs != null && renderMs > frameBudget)) {
summary = "Viewer render queue is backing up";
severity = "warn";
@@ -1969,7 +1981,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
pushBitrate();
desiredStreaming = true;
lastFrameAt = 0;
lastViewerFrameGapKeyframeAt = 0;
lastViewerKeyframeRequestAt = 0;
diagnostics.coalescedFrames = 0;
firstFrameLogged = false;
resetH264SessionState();
setStreamState("starting", "Starting stream");
@@ -2322,17 +2335,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function markFrameReceived() {
const now = performance.now();
const frameGapMs = lastFrameAt ? now - lastFrameAt : 0;
lastFrameAt = now;
clearOfflineTimer();
if (
desiredStreaming &&
frameGapMs >= VIEWER_FRAME_GAP_KEYFRAME_MS &&
(!lastViewerFrameGapKeyframeAt || now - lastViewerFrameGapKeyframeAt >= VIEWER_FRAME_GAP_KEYFRAME_THROTTLE_MS)
) {
lastViewerFrameGapKeyframeAt = now;
sendCmd("desktop_request_keyframe", { reason: "viewer_frame_gap" });
}
if (!firstFrameLogged) {
firstFrameLogged = true;
rdDebug("first frame received", {
@@ -2394,6 +2398,14 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function destroyVideoDecoder() {
updateCanvasDecodePressure(true);
if (decodedVideoPresentationFrame) {
cancelAnimationFrame(decodedVideoPresentationFrame);
decodedVideoPresentationFrame = 0;
}
if (pendingDecodedVideoFrame) {
pendingDecodedVideoFrame.frame.close();
pendingDecodedVideoFrame = null;
}
if (videoDecoder) {
try {
videoDecoder.close();
@@ -2406,6 +2418,47 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
resetH264RuntimeState();
}
function presentLatestDecodedVideoFrame() {
decodedVideoPresentationFrame = 0;
const pending = pendingDecodedVideoFrame;
pendingDecodedVideoFrame = null;
if (!pending) return;
const { frame, timing, width, height } = pending;
if (width > 0 && height > 0 && (canvas.width !== width || canvas.height !== height)) {
canvas.width = width;
canvas.height = height;
frameWidth = width;
frameHeight = height;
}
try {
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
} finally {
frame.close();
}
const renderedAt = performance.now();
if (timing) {
diagnostics.renderMs = smoothed(diagnostics.renderMs, Math.max(0, renderedAt - timing.receivedAt));
}
diagnostics.width = width;
diagnostics.height = height;
updateFpsDisplay();
}
function queueDecodedVideoFrame(frame, timing) {
const width = frame.displayWidth || frame.codedWidth || frameWidth;
const height = frame.displayHeight || frame.codedHeight || frameHeight;
if (pendingDecodedVideoFrame) {
pendingDecodedVideoFrame.frame.close();
diagnostics.coalescedFrames += 1;
}
pendingDecodedVideoFrame = { frame, timing, width, height };
if (!decodedVideoPresentationFrame) {
decodedVideoPresentationFrame = requestAnimationFrame(presentLatestDecodedVideoFrame);
}
}
function normalizeFallbackReason(reason) {
if (!reason) return "unspecified";
if (typeof reason === "string") return reason;
@@ -2458,6 +2511,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
h264RecoveryAttempts += 1;
h264KeyframeErrorStreak = 0;
destroyVideoDecoder();
console.warn("rd: h264 decode stuck waiting for keyframe; auto-restarting stream once", {
reason,
@@ -2578,34 +2632,24 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
output: (frame) => {
const outputStartedAt = performance.now();
const timing = h264PendingTimings.shift();
const width = frame.displayWidth || frame.codedWidth || frameWidth;
const height = frame.displayHeight || frame.codedHeight || frameHeight;
if (width > 0 && height > 0 && (canvas.width !== width || canvas.height !== height)) {
canvas.width = width;
canvas.height = height;
frameWidth = width;
frameHeight = height;
}
try {
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
} finally {
frame.close();
}
const renderedAt = performance.now();
h264KeyframeErrorStreak = 0;
if (timing) {
diagnostics.decodeMs = smoothed(diagnostics.decodeMs, Math.max(0, outputStartedAt - timing.decodeStartedAt));
diagnostics.renderMs = smoothed(diagnostics.renderMs, Math.max(0, renderedAt - timing.receivedAt));
}
diagnostics.decodeQueue = videoDecoder?.decodeQueueSize || h264PendingTimings.length;
diagnostics.width = width;
diagnostics.height = height;
updateFpsDisplay();
queueDecodedVideoFrame(frame, timing);
updateCanvasDecodePressure();
},
error: (err) => {
console.warn(`rd: ${codecName} decoder error`, err);
if (codecName === "h264") h264WaitingForKeyframe = true;
updateCanvasDecodePressure(true);
setTimeout(() => fallbackFromVideoCodec(codecName, err), 0);
setTimeout(() => {
if (codecName === "h264" && tryRecoverH264Stream("h264_async_decoder_error")) {
return;
}
fallbackFromVideoCodec(codecName, err);
}, 0);
},
});
videoDecoder.addEventListener("dequeue", () => {
@@ -2795,6 +2839,23 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
h264FramesSeen += 1;
const isKey = codecName === "hevc" ? isHEVCKeyFrame(videoBytes) : isH264KeyFrame(videoBytes);
if (codecName === "h264" && h264WaitingForKeyframe && !isKey) {
h264KeyframeErrorStreak += 1;
const now = performance.now();
if (!lastViewerKeyframeRequestAt || now - lastViewerKeyframeRequestAt >= VIEWER_FRAME_GAP_KEYFRAME_THROTTLE_MS) {
lastViewerKeyframeRequestAt = now;
sendCmd("desktop_request_keyframe", { reason: "h264_decoder_keyframe_required" });
}
if (h264KeyframeErrorStreak >= H264_KEYFRAME_ERROR_RESTART_THRESHOLD) {
const restarted = tryRecoverH264Stream("h264_keyframe_timeout");
if (!restarted) fallbackToJpegCodec("h264_keyframe_timeout_loop");
}
updateCanvasDecodePressure(true);
return;
}
if (codecName === "h264" && isKey) {
h264WaitingForKeyframe = false;
}
// If software H264 encode on the agent cannot keep up, automatically
// fall back to JPEG blocks for a smoother interactive stream.
@@ -2827,10 +2888,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
videoDecoder.decode(chunk);
diagnostics.decodeQueue = videoDecoder.decodeQueueSize;
updateCanvasDecodePressure();
h264KeyframeErrorStreak = 0;
} catch (err) {
h264PendingTimings.pop();
if (isKeyframeRequiredError(err)) {
if (codecName === "h264") h264WaitingForKeyframe = true;
h264KeyframeErrorStreak += 1;
const now = Date.now();
if (now - h264LastDecodeWarnAt >= H264_DECODE_WARN_THROTTLE_MS) {
+45
View File
@@ -6,6 +6,7 @@ import {
requestDesktopNotificationPermission,
} from "./notify-client.js";
import { escapeHtml, formatBytes as formatSharedBytes, formatDate as formatSharedDate } from "./format.js";
import { getCertificateTrustGuide } from "./cert-banner.js";
const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds";
const NAV_MODE_KEY = "sb_mode";
@@ -81,6 +82,10 @@ const tlsCertbotDomainInput = document.getElementById("tls-certbot-domain");
const tlsCertbotCertFileInput = document.getElementById("tls-certbot-cert-file");
const tlsCertbotKeyFileInput = document.getElementById("tls-certbot-key-file");
const tlsCertbotCaFileInput = document.getElementById("tls-certbot-ca-file");
const tlsActiveCertSource = document.getElementById("tls-active-cert-source");
const tlsDownloadCertBtn = document.getElementById("tls-download-cert-btn");
const tlsTrustHelpBtn = document.getElementById("tls-trust-help-btn");
const tlsTrustInstructions = document.getElementById("tls-trust-instructions");
const oidcForm = document.getElementById("oidc-form");
const oidcPermissionNote = document.getElementById("oidc-permission-note");
@@ -457,6 +462,45 @@ function applyTlsForm() {
tlsCertbotCaFileInput.value = certbot.caFileName || "chain.pem";
}
tlsTrustHelpBtn?.addEventListener("click", () => {
tlsTrustInstructions?.classList.toggle("hidden");
});
async function loadActiveTlsCertificate() {
const guide = getCertificateTrustGuide();
if (tlsTrustHelpBtn) {
tlsTrustHelpBtn.innerHTML = `<i class="fa-solid fa-shield-halved"></i>Trust on ${guide.label}`;
}
if (tlsTrustInstructions) {
tlsTrustInstructions.innerHTML = guide.instructions;
}
try {
const res = await fetch("/api/cert/info", { credentials: "include" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const source = String(data.source || "unknown");
if (tlsActiveCertSource) {
tlsActiveCertSource.textContent = source === "self-signed"
? "Self-signed"
: source === "certbot"
? "Certbot / Let's Encrypt"
: source === "configured"
? "Configured certificate"
: "Unknown source";
tlsActiveCertSource.classList.toggle("border-amber-600", source === "self-signed");
tlsActiveCertSource.classList.toggle("bg-amber-950", source === "self-signed");
tlsActiveCertSource.classList.toggle("text-amber-200", source === "self-signed");
}
} catch {
if (tlsActiveCertSource) tlsActiveCertSource.textContent = "Unavailable";
if (tlsDownloadCertBtn) {
tlsDownloadCertBtn.setAttribute("aria-disabled", "true");
tlsDownloadCertBtn.classList.add("pointer-events-none", "opacity-50");
}
}
}
async function loadTlsSettings() {
if (!currentUser) return;
@@ -489,6 +533,7 @@ async function loadTlsSettings() {
tlsConfig = data.tls || null;
applyTlsForm();
setTlsFormDisabled(false);
await loadActiveTlsCertificate();
}
function listToCsv(value) {
+40
View File
@@ -788,6 +788,46 @@
Your permissions do not allow TLS setting changes.
</div>
<div class="rounded-lg border border-cyan-800/70 bg-cyan-950/20 px-4 py-4 space-y-3">
<div class="flex flex-col sm:flex-row sm:items-center gap-3">
<div class="flex-1">
<div class="flex flex-wrap items-center gap-2">
<p class="text-sm font-medium text-cyan-100">Active server certificate</p>
<span id="tls-active-cert-source" class="rounded-full border border-slate-600 bg-slate-800 px-2 py-0.5 text-xs text-slate-300">
Checking…
</span>
</div>
<p class="mt-1 text-xs text-slate-400">
Download and trust this certificate when a locally generated certificate causes HTTPS or WebSocket trust errors.
</p>
</div>
<div class="flex flex-wrap gap-2">
<a
id="tls-download-cert-btn"
href="/api/cert/download"
download="overlord-server.crt"
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-cyan-700 hover:bg-cyan-600 transition-colors text-white text-sm font-medium"
>
<i class="fa-solid fa-download"></i>Download Certificate
</a>
<button
type="button"
id="tls-trust-help-btn"
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-600 bg-slate-800 hover:bg-slate-700 transition-colors text-slate-100 text-sm font-medium"
>
<i class="fa-solid fa-shield-halved"></i>Trust Instructions
</button>
</div>
</div>
<div
id="tls-trust-instructions"
class="hidden rounded-lg border border-slate-700 bg-slate-950/80 p-3 text-xs text-slate-300 leading-relaxed whitespace-pre-line"
></div>
<p class="text-xs text-amber-300/90">
Browsers cannot install trusted certificates automatically. Your operating system will require confirmation, and the browser must be restarted afterward.
</p>
</div>
<label class="flex items-center gap-2 text-sm">
<input id="tls-certbot-enabled" type="checkbox" class="h-4 w-4" />
<span>Use certbot certificates in production runtime (NODE_ENV=production)</span>