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); + } + } +}