feat(streamer): supervisor wiring all subprocesses
This commit is contained in:
parent
7cdc8f0d44
commit
0ff5ecd9d0
1 changed files with 125 additions and 0 deletions
125
streamer/src/index.ts
Normal file
125
streamer/src/index.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { loadConfig } from "./config.js";
|
||||||
|
import { NowPlayingWatcher } from "./nowplaying.js";
|
||||||
|
import { PageServer } from "./page-server.js";
|
||||||
|
import { PcmTap } from "./pcm-tap.js";
|
||||||
|
import { SpectrumAnalyzer } from "./fft.js";
|
||||||
|
import { ChromeRenderer } from "./chrome.js";
|
||||||
|
import { Ffmpeg } from "./ffmpeg.js";
|
||||||
|
import { IcecastPoller } from "./icecast.js";
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const log = (level: string, msg: string, extra?: unknown) =>
|
||||||
|
console.log(JSON.stringify({ ts: new Date().toISOString(), level, msg, ...(extra ? { extra } : {}) }));
|
||||||
|
|
||||||
|
const PAGE_PORT = 8080;
|
||||||
|
const STATIC_DIR = join(import.meta.dirname ?? __dirname, "views");
|
||||||
|
const BARS = cfg.style === "denpa" ? 48 : 72;
|
||||||
|
|
||||||
|
let lastFrameAt = 0;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const np = new NowPlayingWatcher({
|
||||||
|
station: cfg.station,
|
||||||
|
nowPlayingDir: cfg.nowPlayingDir,
|
||||||
|
libraryDir: cfg.libraryDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = new PageServer({
|
||||||
|
port: PAGE_PORT,
|
||||||
|
staticDir: STATIC_DIR,
|
||||||
|
libraryDir: cfg.libraryDir,
|
||||||
|
style: cfg.style,
|
||||||
|
station: cfg.station,
|
||||||
|
tz: cfg.tz,
|
||||||
|
tuneInUrl: cfg.stationTuneInUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pcm = new PcmTap({ host: cfg.pcmHost, port: cfg.pcmPort });
|
||||||
|
const spec = new SpectrumAnalyzer({ bars: BARS, sampleRate: 48000 });
|
||||||
|
|
||||||
|
const ice = cfg.icecastStatusUrl
|
||||||
|
? new IcecastPoller({ statusUrl: cfg.icecastStatusUrl, mountName: `/${cfg.station}.mp3` })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const ffmpeg = new Ffmpeg({
|
||||||
|
rtmpUrl: cfg.rtmpUrl,
|
||||||
|
width: cfg.resolution.width,
|
||||||
|
height: cfg.resolution.height,
|
||||||
|
framerate: cfg.framerate,
|
||||||
|
videoBitrate: cfg.videoBitrate,
|
||||||
|
audioBitrate: cfg.audioBitrate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const chrome = new ChromeRenderer({
|
||||||
|
pageUrl: `http://127.0.0.1:${PAGE_PORT}/?style=${cfg.style}`,
|
||||||
|
width: cfg.resolution.width,
|
||||||
|
height: cfg.resolution.height,
|
||||||
|
framerate: cfg.framerate,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.start();
|
||||||
|
await np.start();
|
||||||
|
np.on("change", (s) => page.pushNowPlaying(s));
|
||||||
|
if (np.current()) page.pushNowPlaying(np.current());
|
||||||
|
|
||||||
|
const pushTimer: NodeJS.Timeout = setInterval(() => page.pushSpectrum(spec.bars()), Math.round(1000 / 30));
|
||||||
|
|
||||||
|
ice?.on("listeners", (l) => page.pushListeners(l));
|
||||||
|
ice?.start();
|
||||||
|
|
||||||
|
const { videoIn, audioIn } = ffmpeg.start();
|
||||||
|
ffmpeg.on("log", (m: string) => log("info", "ffmpeg", m.trim()));
|
||||||
|
ffmpeg.on("exit", (code: number | null) => {
|
||||||
|
log("error", "ffmpeg exited", { code });
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
pcm.on("data", (chunk: Buffer) => {
|
||||||
|
spec.feed(chunk);
|
||||||
|
audioIn.write(chunk);
|
||||||
|
});
|
||||||
|
pcm.on("connecting", () => log("info", "pcm connecting"));
|
||||||
|
pcm.on("connected", () => log("info", "pcm connected"));
|
||||||
|
pcm.on("disconnected", () => log("warn", "pcm disconnected"));
|
||||||
|
pcm.start();
|
||||||
|
|
||||||
|
await chrome.start();
|
||||||
|
chrome.on("frame", (buf: Buffer) => {
|
||||||
|
lastFrameAt = Date.now();
|
||||||
|
videoIn.write(buf);
|
||||||
|
});
|
||||||
|
chrome.on("disconnected", () => {
|
||||||
|
log("error", "chrome disconnected");
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// health endpoint
|
||||||
|
const health = createServer((req, res) => {
|
||||||
|
if (req.url !== "/health") { res.statusCode = 404; res.end(); return; }
|
||||||
|
const stale = Date.now() - lastFrameAt > 5000;
|
||||||
|
res.statusCode = stale ? 503 : 200;
|
||||||
|
res.setHeader("content-type", "application/json");
|
||||||
|
res.end(JSON.stringify({ ok: !stale, lastFrameAt }));
|
||||||
|
});
|
||||||
|
health.listen(cfg.healthPort, "0.0.0.0", () => log("info", `health :${cfg.healthPort}`));
|
||||||
|
|
||||||
|
const shutdown = async () => {
|
||||||
|
clearInterval(pushTimer);
|
||||||
|
pcm.stop();
|
||||||
|
await chrome.stop();
|
||||||
|
ffmpeg.stop();
|
||||||
|
np.stop();
|
||||||
|
ice?.stop();
|
||||||
|
await page.stop();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
process.on("SIGTERM", shutdown);
|
||||||
|
process.on("SIGINT", shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
log("error", "fatal", { err: err instanceof Error ? err.message : String(err) });
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue