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/.
This commit is contained in:
glass beadian 2026-10-10 11:56:42 +00:00
commit aa5e262f3a
880 changed files with 111042 additions and 0 deletions

View file

@ -0,0 +1,22 @@
{
"name": "@tactical-media-broadcast-hub/testing",
"private": true,
"version": "0.1.0",
"type": "module",
"bin": {
"live-source-simulator": "src/simulator.ts"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit",
"simulate": "tsx src/simulator.ts"
},
"dependencies": {
"tsx": "^4.17.0"
},
"devDependencies": {
"@types/node": "^22.5.0",
"typescript": "^5.5.4"
}
}

View file

@ -0,0 +1,32 @@
export interface ScenarioStep {
/** Source that changes state in this step. */
source: "A" | "B" | "C";
action: "go-live" | "fail" | "recover" | "go-offline";
/** Seconds to hold after this step before the next one. */
holdSeconds: number;
}
export interface Scenario {
name: string;
description: string;
steps: ScenarioStep[];
}
export const SCENARIOS: Record<string, Scenario> = {
"dod-flap": {
name: "dod-flap",
description: "Deterministic Auto Director exercise: A live → B live → A fails → C live → A recovers",
steps: [
{ source: "A", action: "go-live", holdSeconds: 20 },
{ source: "B", action: "go-live", holdSeconds: 20 },
{ source: "A", action: "fail", holdSeconds: 10 },
{ source: "C", action: "go-live", holdSeconds: 20 },
{ source: "A", action: "recover", holdSeconds: 20 },
],
},
"single-live": {
name: "single-live",
description: "One source goes live and holds — baseline relay check",
steps: [{ source: "A", action: "go-live", holdSeconds: 60 }],
},
};

View file

@ -0,0 +1,69 @@
#!/usr/bin/env node
/**
* Live-source simulator: drives ffmpeg testsrc-based streams through a
* scripted live/offline/fail scenario so the Auto Director can be exercised
* deterministically without real PeerTube instances.
*/
import { spawnSync } from "node:child_process";
import process from "node:process";
import { SCENARIOS, type Scenario } from "./scenarios.js";
function ffmpegAvailable(): boolean {
const probe = spawnSync("ffmpeg", ["-version"], { encoding: "utf8" });
return probe.status === 0;
}
function runStep(step: Scenario["steps"][number], sinkUrl: string): void {
const label = `${step.source}-${step.action}`;
const duration = step.holdSeconds;
const args = [
"-re",
"-f", "lavfi",
"-i", `testsrc2=size=640x360:rate=30:duration=${duration}`,
"-f", "lavfi",
"-i", `sine=frequency=${step.source === "A" ? 440 : step.source === "B" ? 550 : 660}:duration=${duration}`,
"-c:v", "libx264", "-preset", "veryfast", "-tune", "zerolatency",
"-c:a", "aac", "-b:a", "128k",
"-f", "flv",
`${sinkUrl}/${label}`,
];
process.stdout.write(`[${new Date().toISOString()}] ${label}: pushing ${duration}s to ${sinkUrl}\n`);
const result = spawnSync("ffmpeg", args, { stdio: "inherit" });
if (result.status !== 0) {
process.stderr.write(`[${label}] ffmpeg exited ${result.status}\n`);
}
}
function usage(): never {
process.stderr.write(
"usage: live-source-simulator --scenario <name> [--sink rtmp://localhost:1935/mock]\n" +
"scenarios: " + Object.keys(SCENARIOS).join(", ") + "\n",
);
process.exit(2);
}
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
process.stdout.write(
"usage: live-source-simulator --scenario <name> [--sink rtmp://localhost:1935/mock]\n" +
"scenarios: " + Object.keys(SCENARIOS).join(", ") + "\n",
);
process.exit(0);
}
const scenarioIdx = args.indexOf("--scenario");
const sinkIdx = args.indexOf("--sink");
if (scenarioIdx === -1 || !args[scenarioIdx + 1]) usage();
const scenario: Scenario | undefined = SCENARIOS[args[scenarioIdx + 1] ?? ""];
if (!scenario) usage();
const sink = (sinkIdx !== -1 && args[sinkIdx + 1]) || "rtmp://localhost:1935/mock";
if (!ffmpegAvailable()) {
process.stderr.write("ffmpeg not found on PATH — install ffmpeg (the simulator is the deterministic test source)\n");
process.exit(1);
}
process.stdout.write(`scenario ${scenario.name}: ${scenario.description}\n`);
for (const step of scenario.steps) {
runStep(step, sink);
}
process.stdout.write("scenario complete\n");

View file

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["src"]
}