From 870a41a2f740759bcb1d892fe1b40bdb1266adb3 Mon Sep 17 00:00:00 2001 From: devilreef Date: Thu, 30 Apr 2026 15:21:49 +0600 Subject: [PATCH] feat(streamer): icecast listener-count poller --- streamer/src/icecast.ts | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 streamer/src/icecast.ts diff --git a/streamer/src/icecast.ts b/streamer/src/icecast.ts new file mode 100644 index 0000000..cd1c512 --- /dev/null +++ b/streamer/src/icecast.ts @@ -0,0 +1,43 @@ +import { EventEmitter } from "node:events"; + +export interface IcecastOpts { + statusUrl: string; + mountName: string; + intervalMs?: number; +} + +export interface IcecastListeners { current: number; peak: number; } + +export class IcecastPoller extends EventEmitter { + private timer: NodeJS.Timeout | null = null; + private peak = 0; + + constructor(private opts: IcecastOpts) { super(); } + + start(): void { + this.poll(); + this.timer = setInterval(() => this.poll(), this.opts.intervalMs ?? 30000); + } + + stop(): void { + if (this.timer) clearInterval(this.timer); + } + + private async poll(): Promise { + try { + const r = await fetch(this.opts.statusUrl, { signal: AbortSignal.timeout(5000) }); + if (!r.ok) throw new Error(`icecast status ${r.status}`); + const j = await r.json() as { + icestats: { source?: { listenurl: string; listeners: number }[] | { listenurl: string; listeners: number } }; + }; + const sources = j.icestats.source; + const list = Array.isArray(sources) ? sources : sources ? [sources] : []; + const found = list.find((s) => s.listenurl.endsWith(this.opts.mountName)); + const current = found?.listeners ?? 0; + this.peak = Math.max(this.peak, current); + this.emit("listeners", { current, peak: this.peak }); + } catch (err) { + this.emit("warn", err); + } + } +}