import * as http from "node:http"; import { readFileSync } from "node:fs"; import { createHash, createPublicKey } from "node:crypto"; import { resolve } from "node:path"; import pino from "pino"; import { DurableFence } from "./bus/fence"; import { RedisBus } from "./bus/redis"; import { FfmpegRelayJob, type EgressRelayFactory, type VaultHandleBroker } from "./relay/ffmpeg"; import type { EndpointValidator } from "./relay/endpoints"; import { MediaSupervisor, type RelayJob } from "./supervisor"; import { ProductionJob, type AssetStore, type ProductionEgress } from "./compositor/production-job"; import { resolveHelperBinary } from "./gate/binary"; import { isDesiredChannel, type DesiredChannel } from "./types"; const logger = pino({ level: process.env.LOG_LEVEL ?? "info" }); const port = Number(process.env.PORT ?? 3100); const workerId = process.env.MEDIA_WORKER_ID ?? "media-worker-dev"; let supervisor: MediaSupervisor | null = null; let bus: RedisBus | null = null; let ready = false; const server = http.createServer((req, res) => { if (req.url === "/healthz") { res.writeHead(ready ? 200 : 503, { "content-type": "application/json" }); res.end(JSON.stringify({ status: ready ? "alive" : "starting", worker: workerId, channels: supervisor?.status() ?? [] })); return; } res.writeHead(404).end(); }); function readDesired(): DesiredChannel[] { const path = process.env.MEDIA_DESIRED_CONFIG; if (!path) return []; const value: unknown = JSON.parse(readFileSync(path, "utf8")); if (!Array.isArray(value) || !value.every(isDesiredChannel)) throw new Error("invalid desired channel config"); return value; } /** Deliberately fails closed until WP7 injects an egress relay and vault broker. */ const unavailableEgress: EgressRelayFactory = { async prepare() { throw new Error("egress relay is not configured"); } }; const unavailableValidator: EndpointValidator = { async validate() { throw new Error("endpoint validator is not configured"); } }; const unavailableVault: VaultHandleBroker = { async deliverPushTarget() { throw new Error("vault broker is not configured"); } }; const unavailableProductionEgress: ProductionEgress = { async prepare() { throw new Error("production egress is not configured"); } }; const unavailableAssets: AssetStore = { async localPath() { throw new Error("overlay asset store is not configured"); } }; const redactor = { redact: (_chunk: string) => "[media stderr redacted until a job secret-aware redactor is injected]" }; /** Per-channel scratch for gate FIFOs; the name is a hash so channel ids never reach a path. */ function scratchFor(channel: DesiredChannel): string { const id = createHash("sha256").update(`${channel.workspaceId}:${channel.channelId}`).digest("hex").slice(0, 32); return resolve(process.env.MEDIA_SCRATCH_DIR ?? ".media-worker-state/scratch", id); } /** v2 (production) snapshots run the compositor chain; v1 snapshots keep the single relay. */ function createRelay(channel: DesiredChannel, onExit: (reason: string) => void): RelayJob { const onStderr = (message: string): void => logger.warn({ message }, "ffmpeg stderr"); if (channel.production) { return new ProductionJob({ validator: unavailableValidator, egress: unavailableProductionEgress, vault: unavailableVault, assets: unavailableAssets, redactor, onStderr, onExit, scratchDir: scratchFor(channel), }); } return new FfmpegRelayJob(unavailableValidator, unavailableEgress, unavailableVault, redactor, onStderr, onExit); } /** Production channels need the packaged native helpers; say at boot which ones resolve (src/gate/binary.ts). */ function checkHelpers(): void { for (const name of ["frame-gate", "source-bus"] as const) { try { logger.info({ helper: name, path: resolveHelperBinary(name) }, "native helper"); } catch (error) { logger.warn({ helper: name, err: String(error) }, "native helper missing: production channels will fail to start"); } } } async function start(): Promise { checkHelpers(); const desired = readDesired(); const keyPath = process.env.MEDIA_CONTROL_PUBLIC_KEY; const keys = new Map>(); if (keyPath) keys.set(process.env.MEDIA_CONTROL_KID ?? "control-v1", createPublicKey(readFileSync(keyPath))); const fence = new DurableFence(process.env.MEDIA_FENCE_PATH ?? ".media-worker-state/fence.json"); bus = await RedisBus.connect(process.env.REDIS_URL ?? `redis://${process.env.REDIS_HOST ?? "localhost"}:6379`); supervisor = new MediaSupervisor(workerId, desired, bus, fence, keys, createRelay, { transition: (event) => logger.info(event, "media state transition") }); await supervisor.start(); const workspaces = [...new Set(desired.map((item) => item.workspaceId))]; for (const workspaceId of workspaces) { // ACLs should grant only this worker's assigned command channels. await bus.subscribe(workerId, [workspaceId], (message) => { let command: unknown; try { command = JSON.parse(message); } catch { logger.warn("malformed command rejected"); return; } void supervisor?.handle(command).catch(() => logger.warn("command rejected")); }); } ready = true; logger.info({ port, workerId, channels: desired.length }, "media worker listening"); } server.listen(port, () => { void start().catch((error: unknown) => { logger.error({ err: String(error) }, "media worker startup failed"); process.exitCode = 1; server.close(); }); }); for (const signal of ["SIGINT", "SIGTERM"] as const) process.on(signal, () => { ready = false; void supervisor?.stop().finally(() => { bus?.close(); server.close(); }); });