HEVC (and some other small shit)

This commit is contained in:
KingKDot
2026-07-18 01:27:58 -05:00
parent b619658a95
commit ae74e17d13
34 changed files with 1129 additions and 101 deletions
+33
View File
@@ -0,0 +1,33 @@
import { expect, test } from "bun:test";
import { rm } from "node:fs/promises";
import { isAbsolute, resolve } from "node:path";
import { tmpdir } from "node:os";
test("creates isolated test data when the Bun preload was not discovered", async () => {
const childEnv = { ...process.env };
delete childEnv.DATA_DIR;
delete childEnv.OVERLORD_TEST_DATA_DIR;
childEnv.NODE_ENV = "test";
const child = Bun.spawn([
"bun",
"-e",
'const { resolveDataDir } = await import("./src/paths.ts"); console.log(resolveDataDir());',
], {
cwd: resolve(import.meta.dirname, ".."),
env: childEnv,
stdout: "pipe",
stderr: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
expect(exitCode, stderr).toBe(0);
const dataDir = stdout.trim();
expect(isAbsolute(dataDir)).toBe(true);
expect(dataDir.toLowerCase().startsWith(resolve(tmpdir()).toLowerCase())).toBe(true);
await rm(dataDir, { recursive: true, force: true });
});
+14 -4
View File
@@ -1,7 +1,19 @@
import path from "path";
import { existsSync, mkdirSync } from "fs";
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "fs";
import { tmpdir } from "os";
let fallbackTestDataDir = "";
function createFallbackTestDataDir(): string {
if (fallbackTestDataDir) return fallbackTestDataDir;
const dir = mkdtempSync(path.join(tmpdir(), "overlord-bun-test-"));
writeFileSync(path.join(dir, "config.json"), "{}\n", "utf8");
process.env.DATA_DIR = dir;
process.env.OVERLORD_TEST_DATA_DIR = "1";
fallbackTestDataDir = dir;
return dir;
}
function assertSafeTestDataDir(dir: string): void {
if (String(process.env.NODE_ENV || "").toLowerCase() !== "test") return;
@@ -32,9 +44,7 @@ export function resolveDataDir(): string {
}
if (String(process.env.NODE_ENV || "").toLowerCase() === "test") {
throw new Error(
"Refusing to use the default application data directory while NODE_ENV=test. Run tests through `bun test`.",
);
return createFallbackTestDataDir();
}
if (process.platform === "win32" && process.env.APPDATA) {
+12
View File
@@ -61,4 +61,16 @@ describe("protocol encode/decode", () => {
expect(decoded.header.fps).toBe(24);
expect(Array.from(decoded.data)).toEqual(Array.from(frame.data));
});
test("round trips HEVC frame metadata", () => {
const frame: Frame = {
type: "frame",
header: { monitor: 0, fps: 60, format: "hevc" },
data: new Uint8Array([0, 0, 0, 1, 0x40, 0x01]),
};
const decoded = decodeMessage(encodeMessage(frame)) as Frame;
expect(decoded.header.format).toBe("hevc");
expect(Array.from(decoded.data)).toEqual(Array.from(frame.data));
});
});
+25 -1
View File
@@ -158,6 +158,30 @@ export type CommandResult = {
message?: string;
};
export type DesktopCodecCapability = {
codec: "hevc" | "h264" | "jpeg" | "raw" | string;
encoders?: string[];
transports: Array<"websocket" | "webrtc" | string>;
hardware?: boolean;
};
export type DesktopEncoderCapabilities = {
type: "desktop_encoder_capabilities";
commandId?: string;
probed: boolean;
display: number;
profiles: Array<{
maxHeight: number;
width: number;
height: number;
fps: number;
label: string;
providers: string[];
}>;
codecs: DesktopCodecCapability[];
detail?: string;
};
export type ClientLogEntry = {
seq: number;
at: number;
@@ -190,7 +214,7 @@ export type ScreenshotResult = {
export type FrameHeader = {
monitor: number;
fps: number;
format: "jpeg" | "webp" | "raw" | "h264";
format: "jpeg" | "webp" | "raw" | "h264" | "hevc";
width?: number;
height?: number;
hash?: string;
@@ -0,0 +1,68 @@
import { describe, expect, test } from "bun:test";
import { negotiateDesktopCodec } from "./desktop-codec-negotiation";
const encoders = [
{ codec: "hevc", transports: ["websocket"] },
{ codec: "h264", transports: ["websocket", "webrtc"] },
{ codec: "jpeg", transports: ["websocket"] },
];
describe("desktop codec negotiation", () => {
test("selects the first mutually supported preferred codec", () => {
expect(negotiateDesktopCodec({
encoderCodecs: encoders,
decoderCodecs: ["hevc", "h264", "jpeg"],
preferredCodecs: ["hevc", "h264", "jpeg"],
transport: "websocket",
})).toEqual({
selectedCodec: "hevc",
fallbackCodecs: ["hevc", "h264", "jpeg"],
transport: "websocket",
});
});
test("filters codecs that are unavailable on WebRTC", () => {
expect(negotiateDesktopCodec({
encoderCodecs: encoders,
decoderCodecs: ["hevc", "h264", "jpeg"],
preferredCodecs: ["hevc", "h264", "jpeg"],
transport: "p2p",
})).toEqual({
selectedCodec: "h264",
fallbackCodecs: ["h264"],
transport: "webrtc",
});
});
test("falls back to JPEG when H.264 decoding is unavailable", () => {
const result = negotiateDesktopCodec({
encoderCodecs: encoders,
decoderCodecs: ["jpeg"],
preferredCodecs: ["h264", "jpeg"],
transport: "websocket",
});
expect(result.selectedCodec).toBe("jpeg");
expect(result.fallbackCodecs).toEqual(["jpeg"]);
});
test("returns no codec instead of selecting an incompatible transport", () => {
const result = negotiateDesktopCodec({
encoderCodecs: encoders,
decoderCodecs: ["jpeg"],
preferredCodecs: ["jpeg"],
transport: "webrtc",
});
expect(result.selectedCodec).toBe("");
expect(result.fallbackCodecs).toEqual([]);
});
test("does not re-add a codec the viewer explicitly omitted", () => {
const result = negotiateDesktopCodec({
encoderCodecs: encoders,
decoderCodecs: ["h264", "jpeg"],
preferredCodecs: ["jpeg"],
transport: "websocket",
});
expect(result.fallbackCodecs).toEqual(["jpeg"]);
});
});
@@ -0,0 +1,88 @@
export type DesktopCodecTransport = "websocket" | "webrtc";
export type DesktopCodecCapability = {
codec: string;
encoders?: string[];
transports?: string[];
hardware?: boolean;
};
export type DesktopCodecNegotiation = {
selectedCodec: string;
fallbackCodecs: string[];
transport: DesktopCodecTransport;
};
const KNOWN_CODECS = new Set(["hevc", "h264", "jpeg", "raw"]);
const DEFAULT_PREFERENCE = ["h264", "jpeg", "raw"];
function normalizeCodec(value: unknown): string {
const codec = String(value || "").trim().toLowerCase();
if (codec === "h265") return "hevc";
if (codec === "mjpeg" || codec === "jpg") return "jpeg";
return KNOWN_CODECS.has(codec) ? codec : "";
}
function normalizeTransport(value: unknown): DesktopCodecTransport {
const transport = String(value || "").trim().toLowerCase();
return transport === "webrtc" || transport === "relayed" || transport === "p2p"
? "webrtc"
: "websocket";
}
function normalizeDeclaredTransport(value: unknown): DesktopCodecTransport | "" {
const transport = String(value || "").trim().toLowerCase();
if (transport === "websocket") return "websocket";
if (transport === "webrtc" || transport === "relayed" || transport === "p2p") return "webrtc";
return "";
}
function decoderCodecSet(values: unknown): Set<string> {
if (!Array.isArray(values)) return new Set();
return new Set(values.map(normalizeCodec).filter(Boolean));
}
function encoderCodecSet(
values: unknown,
transport: DesktopCodecTransport,
): Set<string> {
if (!Array.isArray(values)) return new Set();
const codecs = new Set<string>();
for (const value of values) {
if (typeof value === "string") {
const codec = normalizeCodec(value);
if (codec) codecs.add(codec);
continue;
}
if (!value || typeof value !== "object") continue;
const capability = value as DesktopCodecCapability;
const codec = normalizeCodec(capability.codec);
if (!codec) continue;
const transports = Array.isArray(capability.transports)
? capability.transports.map(normalizeDeclaredTransport).filter(Boolean)
: ["websocket" as const];
if (transports.includes(transport)) codecs.add(codec);
}
return codecs;
}
export function negotiateDesktopCodec(options: {
encoderCodecs: unknown;
decoderCodecs: unknown;
preferredCodecs?: unknown;
transport?: unknown;
}): DesktopCodecNegotiation {
const transport = normalizeTransport(options.transport);
const encoders = encoderCodecSet(options.encoderCodecs, transport);
const decoders = decoderCodecSet(options.decoderCodecs);
const requested = Array.isArray(options.preferredCodecs)
? options.preferredCodecs.map(normalizeCodec).filter(Boolean)
: [];
const preference = [...new Set(requested.length > 0 ? requested : DEFAULT_PREFERENCE)];
const fallbackCodecs = preference.filter((codec) => encoders.has(codec) && decoders.has(codec));
return {
selectedCodec: fallbackCodecs[0] || "",
fallbackCodecs,
transport,
};
}
@@ -7,7 +7,11 @@ import {
setUserClientAccessScope,
setUserFeaturePermission,
} from "../../users";
import { handleWebrtcRoutes } from "./webrtc-routes";
import {
handleWebrtcRoutes,
revokeWebrtcViewerSessions,
trackWebrtcViewerSession,
} from "./webrtc-routes";
const PASSWORD = "Aa1!WebrtcAuthTestPass_2026";
const createdUserIds: number[] = [];
@@ -57,4 +61,30 @@ describe("WebRTC viewer authorization", () => {
);
expect(response!.status).toBe(403);
});
test("revokes only the matching user's tracked media sessions", async () => {
const deleted: string[] = [];
trackWebrtcViewerSession({
upstreamUrl: "http://mediamtx/session-a",
userId: 1001,
clientId: "client-a",
kind: "desktop",
createdAt: Date.now(),
});
trackWebrtcViewerSession({
upstreamUrl: "http://mediamtx/session-b",
userId: 1002,
clientId: "client-a",
kind: "desktop",
createdAt: Date.now(),
});
const count = await revokeWebrtcViewerSessions(1001, "client-a", async (url) => {
deleted.push(url);
});
expect(count).toBe(1);
expect(deleted).toEqual(["http://mediamtx/session-a"]);
await revokeWebrtcViewerSessions(1002, "client-a", async () => {});
});
});
@@ -12,6 +12,50 @@ const PUBLISH_TOKEN_TTL_MS = 60_000;
type PublishToken = { clientId: string; expiresAt: number };
const publishTokens = new Map<string, PublishToken>();
export type ViewerMediaSession = {
upstreamUrl: string;
userId: number;
clientId: string;
kind: WebrtcKind;
createdAt: number;
};
const viewerMediaSessions = new Map<string, ViewerMediaSession>();
export function trackWebrtcViewerSession(session: ViewerMediaSession): void {
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
for (const [upstreamUrl, existing] of viewerMediaSessions) {
if (existing.createdAt < cutoff) viewerMediaSessions.delete(upstreamUrl);
}
viewerMediaSessions.set(session.upstreamUrl, session);
}
export function forgetWebrtcViewerSession(upstreamUrl: string): void {
viewerMediaSessions.delete(upstreamUrl);
}
export async function revokeWebrtcViewerSessions(
userId: number,
clientId: string,
deleteSession: (upstreamUrl: string) => Promise<unknown> = async (upstreamUrl) => {
await fetch(upstreamUrl, { method: "DELETE" });
},
): Promise<number> {
const revoked: ViewerMediaSession[] = [];
for (const [upstreamUrl, session] of viewerMediaSessions) {
if (session.userId !== userId || session.clientId !== clientId) continue;
viewerMediaSessions.delete(upstreamUrl);
revoked.push(session);
}
await Promise.allSettled(revoked.map(async (session) => {
try {
await deleteSession(session.upstreamUrl);
} catch (error) {
logger.warn(`[webrtc] failed to revoke viewer session client=${clientId}: ${(error as Error).message}`);
}
}));
return revoked.length;
}
function prunePublishTokens(now = Date.now()) {
for (const [t, entry] of publishTokens) {
if (entry.expiresAt < now) publishTokens.delete(t);
@@ -66,6 +110,7 @@ export async function handleWebrtcRoutes(req: Request, url: URL): Promise<Respon
const [, streamPath, kind, sessionSuffix] = match;
const clientId = streamPath.split("/")[1];
let viewerUserId: number | undefined;
if (kind === "whip") {
const authHeader = req.headers.get("authorization") || "";
@@ -87,6 +132,7 @@ export async function handleWebrtcRoutes(req: Request, url: URL): Promise<Respon
requireClientAccess(user, clientId);
const streamKind = streamPath.split("/")[2] as WebrtcKind;
requireFeatureAccess(user, WEBRTC_FEATURES[streamKind]);
viewerUserId = user.userId;
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
@@ -123,9 +169,21 @@ export async function handleWebrtcRoutes(req: Request, url: URL): Promise<Respon
if (loc) {
try {
const abs = new URL(loc, MEDIAMTX_URL);
if (kind === "whep" && req.method === "POST" && upstreamResp.ok && viewerUserId !== undefined) {
trackWebrtcViewerSession({
upstreamUrl: abs.toString(),
userId: viewerUserId,
clientId,
kind: streamPath.split("/")[2] as WebrtcKind,
createdAt: Date.now(),
});
}
respHeaders.set("location", `/api/webrtc${abs.pathname}`);
} catch { }
}
if (kind === "whep" && req.method === "DELETE" && upstreamResp.ok) {
forgetWebrtcViewerSession(upstreamUrl);
}
return new Response(upstreamResp.body, {
status: upstreamResp.status,
@@ -8,6 +8,7 @@ import {
getUserById,
type FeatureName,
} from "../users";
import { revokeWebrtcViewerSessions } from "./routes/webrtc-routes";
const VIEWER_FEATURES: Partial<Record<SocketRole, FeatureName>> = {
console_viewer: "console",
@@ -31,6 +32,9 @@ const activeViewerSockets = new Set<ServerWebSocket<SocketData>>();
function deny(ws: ServerWebSocket<SocketData>, reason: string): false {
activeViewerSockets.delete(ws);
if (ws.data.userId !== undefined && ws.data.clientId) {
void revokeWebrtcViewerSessions(ws.data.userId, ws.data.clientId);
}
try {
ws.close(1008, reason);
} catch {}
@@ -10,6 +10,7 @@ import {
handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen,
handleDesktopStreamStats,
handleDesktopEncoderCapabilities,
backstageStreamingState,
rdStreamingState,
} from "./ws-console-rd-backstage";
@@ -217,6 +218,39 @@ describe("remote desktop viewer control", () => {
expect(message.transport).toBe("webrtc");
});
test("selects one mutually compatible codec across all viewer transports", () => {
const clientId = `rd-codecs-${Date.now().toString(36)}`;
createClient(clientId);
const canvasViewer = createMockWs({ clientId });
const webrtcViewer = createMockWs({ clientId });
handleRemoteDesktopViewerOpen(canvasViewer as any);
handleRemoteDesktopViewerOpen(webrtcViewer as any);
canvasViewer.data.rdDecoderCodecs = ["hevc", "h264", "jpeg"];
canvasViewer.data.rdPreferredCodecs = ["hevc", "h264", "jpeg"];
canvasViewer.data.rdCodecTransport = "websocket";
webrtcViewer.data.rdDecoderCodecs = ["hevc", "h264"];
webrtcViewer.data.rdPreferredCodecs = ["hevc", "h264"];
webrtcViewer.data.rdCodecTransport = "webrtc";
handleDesktopEncoderCapabilities(clientId, {
type: "desktop_encoder_capabilities",
profiles: [],
codecs: [
{ codec: "hevc", transports: ["websocket"] },
{ codec: "h264", transports: ["websocket", "webrtc"] },
{ codec: "jpeg", transports: ["websocket"] },
],
});
const canvasMessage = decodeMessage(canvasViewer.sent.at(-1) as Uint8Array) as any;
const webrtcMessage = decodeMessage(webrtcViewer.sent.at(-1) as Uint8Array) as any;
expect(canvasMessage.selectedCodec).toBe("h264");
expect(canvasMessage.fallbackCodecs).toEqual(["h264"]);
expect(webrtcMessage.selectedCodec).toBe("h264");
expect(webrtcMessage.fallbackCodecs).toEqual(["h264"]);
});
test("holds Canvas acknowledgements only while the browser decoder is under pressure", () => {
const clientId = `rd-flow-${Date.now().toString(36)}`;
const { agentWs } = createClient(clientId);
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from "fs";
import path from "path";
import * as clientManager from "../clientManager";
import { logger } from "../logger";
import { negotiateDesktopCodec } from "./desktop-codec-negotiation";
import { metrics } from "../metrics";
import { encodeMessage } from "../protocol";
import { resolveRuntimeRoot } from "./runtime-paths";
@@ -377,6 +378,15 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
}
switch (payload.type) {
case "desktop_encoder_capabilities":
ws.data.rdDecoderCodecs = Array.isArray((payload as any).decoderCodecs)
? (payload as any).decoderCodecs.map((codec: unknown) => String(codec || "").trim().toLowerCase()).filter(Boolean).slice(0, 8)
: ["h264", "jpeg", "raw"];
ws.data.rdPreferredCodecs = Array.isArray((payload as any).preferredCodecs)
? (payload as any).preferredCodecs.map((codec: unknown) => String(codec || "").trim().toLowerCase()).filter(Boolean).slice(0, 8)
: ["h264", "jpeg", "raw"];
ws.data.rdCodecTransport = ["webrtc", "relayed", "p2p"].includes(String((payload as any).transport || "").toLowerCase())
? "webrtc"
: "websocket";
sendDesktopCommand(target, "desktop_encoder_capabilities", {
display: Number((payload as any).display) || 0,
});
@@ -488,6 +498,14 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
});
break;
}
if (String(state.codec || "").toLowerCase() === "hevc") {
safeSendViewer(ws, {
type: "recording_status",
recording: null,
error: "HEVC stream recording is not supported yet. Switch the stream codec to H.264 or JPEG first.",
});
break;
}
const inputCodec = String(state.codec || "").toLowerCase() === "h264" ? "h264" : "mjpeg";
const compact = !!(payload as any).compact;
const requestedFps = Number((payload as any).fps) || 0;
@@ -1495,8 +1513,43 @@ export function sendbackstageCommand(target: ClientInfo | undefined, commandType
}
export function handleDesktopEncoderCapabilities(clientId: string, payload: any) {
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
safeSendViewer(session.viewer, payload);
const encoderCodecs = Array.isArray(payload?.codecs) && payload.codecs.length > 0
? payload.codecs
: [
{ codec: "h264", transports: ["websocket", "webrtc"] },
{ codec: "jpeg", transports: ["websocket"] },
{ codec: "raw", transports: ["websocket"] },
];
const sessions = sessionManager.getRdSessionsForClient(clientId);
const negotiations = sessions.map((session) => negotiateDesktopCodec({
encoderCodecs,
decoderCodecs: session.viewer.data.rdDecoderCodecs || ["h264", "jpeg", "raw"],
preferredCodecs: session.viewer.data.rdPreferredCodecs,
transport: session.viewer.data.rdCodecTransport,
}));
const sharedFallbackCodecs = negotiations.length > 0
? negotiations[0].fallbackCodecs.filter((codec) =>
negotiations.every((negotiation) => negotiation.fallbackCodecs.includes(codec)),
)
: [];
const selectedCodec = sharedFallbackCodecs[0] || "";
for (let index = 0; index < sessions.length; index++) {
const session = sessions[index];
const negotiation = {
...negotiations[index],
selectedCodec,
fallbackCodecs: sharedFallbackCodecs,
};
session.viewer.data.rdSelectedCodec = selectedCodec;
safeSendViewer(session.viewer, {
...payload,
codecs: encoderCodecs,
negotiation,
selectedCodec,
fallbackCodecs: sharedFallbackCodecs,
transport: negotiation.transport,
});
}
}
@@ -0,0 +1,21 @@
import { describe, expect, test } from "bun:test";
import { buildViewerFrameBuffer } from "./ws-viewer-utils";
describe("viewer frame protocol", () => {
test("encodes HEVC as compact frame format 5", () => {
const payload = new Uint8Array([0, 0, 0, 1, 0x40]);
const frame = buildViewerFrameBuffer(payload, {
format: "hevc",
monitor: 2,
fps: 60,
width: 1920,
height: 1080,
});
expect(frame[3]).toBe(2);
expect(frame[4]).toBe(2);
expect(frame[5]).toBe(60);
expect(frame[6]).toBe(5);
expect(frame.slice(12)).toEqual(payload);
});
});
@@ -49,6 +49,8 @@ export function buildViewerFrameBuffer(bytes: Uint8Array, header?: any): Uint8Ar
? 3
: header?.format === "h264"
? 4
: header?.format === "hevc"
? 5
: 1;
meta[6] = fmt;
meta[7] = 0;
+4
View File
@@ -31,6 +31,10 @@ export type SocketData = {
enrollmentNonce?: string;
disconnectReason?: string;
disconnectDetail?: string;
rdDecoderCodecs?: string[];
rdPreferredCodecs?: string[];
rdCodecTransport?: "websocket" | "webrtc";
rdSelectedCodec?: string;
};
export type ConsoleSession = {