NVENC support
This commit is contained in:
@@ -38,6 +38,7 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
const cursorCtrl = document.getElementById("cursorCtrl");
|
||||
const duplicationCtrl = document.getElementById("duplicationCtrl");
|
||||
const resolutionSelect = document.getElementById("resolutionSelect");
|
||||
const targetFpsSelect = document.getElementById("targetFpsSelect");
|
||||
const smoothingSlider = document.getElementById("smoothingSlider");
|
||||
const smoothingValue = document.getElementById("smoothingValue");
|
||||
const qualitySlider = document.getElementById("qualitySlider");
|
||||
@@ -1101,14 +1102,28 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
const codec = q >= 100 ? "raw" : (prefersH264 ? "h264" : "jpeg");
|
||||
console.debug("rd: pushQuality val=", val, "q=", q, "codec=", codec);
|
||||
setCodecModeLabel(codec, "requested");
|
||||
if (codec === "h264") {
|
||||
ensureDuplicationForH264();
|
||||
}
|
||||
sendCmd("desktop_set_quality", { quality: q, codec });
|
||||
}
|
||||
|
||||
function ensureDuplicationForH264() {
|
||||
if (!duplicationCtrl || duplicationCtrl.disabled) return;
|
||||
const isWindows = clientOs.includes("windows") || clientOs.includes("win");
|
||||
if (!isWindows || duplicationCtrl.checked) return;
|
||||
duplicationCtrl.checked = true;
|
||||
sendCmd("desktop_set_duplication", { enabled: true });
|
||||
}
|
||||
|
||||
if (codecH264) {
|
||||
codecH264.addEventListener("change", function () {
|
||||
resetH264SessionState();
|
||||
prefersH264 = !!codecH264.checked && typeof VideoDecoder === "function";
|
||||
localStorage.setItem(codecPrefKey, prefersH264 ? "1" : "0");
|
||||
if (prefersH264) {
|
||||
ensureDuplicationForH264();
|
||||
}
|
||||
if (!prefersH264) {
|
||||
destroyVideoDecoder();
|
||||
h264LowFpsStreak = 0;
|
||||
@@ -1127,12 +1142,29 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
}
|
||||
}
|
||||
|
||||
function selectedTargetFps() {
|
||||
const fps = Number(targetFpsSelect?.value || 120);
|
||||
return Number.isFinite(fps) ? Math.max(1, Math.min(240, Math.floor(fps))) : 120;
|
||||
}
|
||||
|
||||
function pushTargetFps() {
|
||||
const fps = selectedTargetFps();
|
||||
console.debug("rd: pushTargetFps fps=", fps);
|
||||
sendCmd("desktop_set_fps", { fps });
|
||||
}
|
||||
|
||||
if (resolutionSelect) {
|
||||
resolutionSelect.addEventListener("change", function () {
|
||||
pushResolution();
|
||||
});
|
||||
}
|
||||
|
||||
if (targetFpsSelect) {
|
||||
targetFpsSelect.addEventListener("change", function () {
|
||||
pushTargetFps();
|
||||
});
|
||||
}
|
||||
|
||||
displaySelect.addEventListener("change", function () {
|
||||
console.debug("rd: select display", displaySelect.value);
|
||||
sendCmd("desktop_select_display", {
|
||||
@@ -1163,6 +1195,7 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
display: displaySelect?.value ?? "",
|
||||
quality: qualitySlider?.value ?? "",
|
||||
resolution: resolutionSelect?.value ?? "",
|
||||
targetFps: targetFpsSelect?.value ?? "",
|
||||
prefersH264,
|
||||
duplication: !!duplicationCtrl?.checked,
|
||||
clientOs,
|
||||
@@ -1182,6 +1215,10 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
if (qualitySlider) {
|
||||
pushQuality(qualitySlider.value);
|
||||
}
|
||||
if (prefersH264) {
|
||||
ensureDuplicationForH264();
|
||||
}
|
||||
pushTargetFps();
|
||||
pushResolution();
|
||||
desiredStreaming = true;
|
||||
lastFrameAt = 0;
|
||||
|
||||
@@ -176,6 +176,22 @@
|
||||
<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
|
||||
|
||||
@@ -119,6 +119,7 @@ export type CommandType =
|
||||
| "desktop_select_display"
|
||||
| "desktop_enable_mouse"
|
||||
| "desktop_enable_keyboard"
|
||||
| "desktop_set_fps"
|
||||
| "darwin_request_permissions"
|
||||
| "webrtc_publish"
|
||||
| "webrtc_stop"
|
||||
|
||||
@@ -168,6 +168,7 @@ export const rdStreamingState = new Map<string, {
|
||||
codec: string;
|
||||
duplication: boolean;
|
||||
maxHeight: number;
|
||||
maxFps: number;
|
||||
lastFps: number;
|
||||
}>();
|
||||
const rdInputPending = new Map<string, { clientId: string; sentAt: number; kind: string }>();
|
||||
@@ -181,6 +182,15 @@ function pruneRdInputPending(now = Date.now()) {
|
||||
}
|
||||
}
|
||||
|
||||
function defaultRdStreamingState() {
|
||||
return { isStreaming: false, display: 0, quality: 90, codec: "", duplication: false, maxHeight: 0, maxFps: 120, lastFps: 0 };
|
||||
}
|
||||
|
||||
function clampDesktopFps(value: unknown): number {
|
||||
const fps = Math.floor(Number(value) || 120);
|
||||
return Math.max(1, Math.min(240, fps));
|
||||
}
|
||||
|
||||
function recordRdInput(commandId: string, clientId: string, kind: string) {
|
||||
pruneRdInputPending();
|
||||
rdInputPending.set(commandId, { clientId, sentAt: Date.now(), kind });
|
||||
@@ -325,7 +335,7 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
||||
return;
|
||||
}
|
||||
|
||||
const state = rdStreamingState.get(clientId) || { isStreaming: false, display: 0, quality: 90, codec: "", duplication: false, maxHeight: 0, lastFps: 0 };
|
||||
const state = rdStreamingState.get(clientId) || defaultRdStreamingState();
|
||||
|
||||
logger.debug(`[rd] inbound viewer msg type=${payload.type} client=${clientId}`);
|
||||
switch (payload.type) {
|
||||
@@ -366,6 +376,7 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
||||
whepPath: `/api/webrtc/${streamPath}/whep`,
|
||||
});
|
||||
}
|
||||
sendDesktopCommand(target, "desktop_set_fps", { fps: clampDesktopFps(state.maxFps) });
|
||||
sendDesktopCommand(target, "desktop_start", {});
|
||||
state.isStreaming = true;
|
||||
rdStreamingState.set(clientId, state);
|
||||
@@ -489,6 +500,16 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "desktop_set_fps": {
|
||||
const newMaxFps = clampDesktopFps((payload as any).fps);
|
||||
if (state.maxFps !== newMaxFps) {
|
||||
sendDesktopCommand(target, "desktop_set_fps", { fps: newMaxFps });
|
||||
state.maxFps = newMaxFps;
|
||||
rdStreamingState.set(clientId, state);
|
||||
logger.debug(`[rd] set target fps=${newMaxFps}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "desktop_set_duplication": {
|
||||
const enabled = !!payload.enabled;
|
||||
if (state.duplication !== enabled) {
|
||||
@@ -648,7 +669,7 @@ function handleRemoteDesktopFrame(payload: any) {
|
||||
const clientId = payload.clientId as string;
|
||||
const header = payload.header;
|
||||
const bytes = payload.data as Uint8Array;
|
||||
const state = rdStreamingState.get(clientId) || { isStreaming: false, display: 0, quality: 90, codec: "", duplication: false, maxHeight: 0, lastFps: 0 };
|
||||
const state = rdStreamingState.get(clientId) || defaultRdStreamingState();
|
||||
if (!state.isStreaming) {
|
||||
state.isStreaming = true;
|
||||
}
|
||||
@@ -667,7 +688,7 @@ function handleRemoteDesktopFrame(payload: any) {
|
||||
function broadcastRemoteDesktopFrame(clientId: string, bytes: Uint8Array, header?: any): boolean {
|
||||
const frameFps = Number(header?.fps) || 0;
|
||||
if (frameFps > 0) {
|
||||
const state = rdStreamingState.get(clientId) || { isStreaming: true, display: 0, quality: 90, codec: "", duplication: false, maxHeight: 0, lastFps: 0 };
|
||||
const state = rdStreamingState.get(clientId) || { ...defaultRdStreamingState(), isStreaming: true };
|
||||
state.lastFps = frameFps;
|
||||
rdStreamingState.set(clientId, state);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user