T-HUB/packages/media-sandbox/src/probe.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

58 lines
3.9 KiB
TypeScript

import { MediaCommandBuilder, type Demuxer } from "./command";
import { spawnSandbox, type SandboxChild, type SandboxOptions } from "./runtime";
export interface ProbeResult {
format: { format_name?: string; duration?: string; size?: string };
streams: Array<{ index: number; codec_type: "video" | "audio" | "subtitle" | "data" | "attachment"; codec_name?: string; width?: number; height?: number; sample_rate?: string; channels?: number }>;
}
const fail = (): never => { throw new Error("invalid probe output"); };
function record(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) fail();
return value as Record<string, unknown>;
}
function keys(value: Record<string, unknown>, allowed: string[]): void {
if (Object.keys(value).some(key => !allowed.includes(key))) fail();
}
export function validateProbe(value: unknown): ProbeResult {
const root = record(value); keys(root, ["format", "streams"]);
const format = record(root.format); keys(format, ["format_name", "duration", "size"]);
for (const [k, v] of Object.entries(format)) {
if (typeof v !== "string" || v.length > 128 || !(k === "format_name" ? /^[a-z0-9_,]+$/ : /^\d+(?:\.\d+)?$/).test(v)) fail();
if (k !== "format_name" && !Number.isFinite(Number(v))) fail();
}
if (!Array.isArray(root.streams) || root.streams.length > 64) fail();
const indices = new Set<number>();
for (const raw of root.streams as unknown[]) {
const stream = record(raw); keys(stream, ["index", "codec_type", "codec_name", "width", "height", "sample_rate", "channels"]);
if (!Number.isSafeInteger(stream.index) || Number(stream.index) < 0 || indices.has(Number(stream.index))) fail();
indices.add(Number(stream.index));
if (!["video", "audio", "subtitle", "data", "attachment"].includes(String(stream.codec_type))) fail();
for (const k of ["width", "height", "channels"]) {
if (stream[k] !== undefined && (!Number.isSafeInteger(stream[k]) || Number(stream[k]) <= 0 || Number(stream[k]) > (k === "channels" ? 64 : 16384))) fail();
}
if (stream.codec_name !== undefined && (typeof stream.codec_name !== "string" || !/^[a-z0-9_]{1,128}$/.test(stream.codec_name))) fail();
if (stream.sample_rate !== undefined && (typeof stream.sample_rate !== "string" || !/^\d{1,7}$/.test(stream.sample_rate) || Number(stream.sample_rate) > 768000)) fail();
}
return value as ProbeResult;
}
/** Exported collector allows testing hostile/oversized output using a real child. */
export async function collectProbe(job: SandboxChild, maxBytes = 256 * 1024, onLog: (line: string) => void = () => undefined): Promise<ProbeResult> {
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 1024 * 1024) { job.kill(); throw new Error("invalid probe output cap"); }
let size = 0; let oversized = false; const chunks: Buffer[] = [];
job.child.stdout?.on("data", (chunk: Buffer) => {
size += chunk.length;
if (size > maxBytes) { oversized = true; job.kill(); return; }
chunks.push(chunk);
});
job.child.stderr?.on("data", () => onLog("media probe stderr suppressed"));
let spawnFailed = false; job.child.once("error", () => { spawnFailed = true; });
const result = await job.exited;
if (oversized) throw new Error("probe output limit exceeded");
if (spawnFailed || result.code !== 0) throw new Error("media probe failed");
let parsed: unknown;
try { parsed = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { return fail(); }
return validateProbe(parsed);
}
export function probe(assetId: string, format: Demuxer, options: SandboxOptions, onLog?: (line: string) => void): Promise<ProbeResult> {
if (!options.asset || options.asset.id !== assetId) throw new Error("probe asset mapping required");
return collectProbe(spawnSandbox(new MediaCommandBuilder().probe(assetId, format), options, { stdio: ["ignore", "pipe", "pipe"] }), undefined, onLog);
}