T-HUB/apps/media-worker/src/main.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

101 lines
5.5 KiB
TypeScript

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<void> {
checkHelpers();
const desired = readDesired();
const keyPath = process.env.MEDIA_CONTROL_PUBLIC_KEY;
const keys = new Map<string, ReturnType<typeof createPublicKey>>();
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(); });
});