/** @typedef {{ index: number, width: number, height: number, position: string, label?: string }} PanelConfig */ /** @typedef {{ frame: number, animation: string, panels: Array<{ index: number, width: number, height: number, rgb: number[] }> }} FramePayload */ /** @typedef {{ panels: PanelConfig[], animations: string[], defaultFps: Record, defaultBrightness: number }} PortalConfig */ /** * @param {string} path * @returns {Promise} */ export async function fetchConfig(path = "/api/config") { const res = await fetch(path); if (!res.ok) throw new Error(`config ${res.status}`); return res.json(); } export class FrameStream { /** @param {() => void} onFrame */ constructor(onFrame) { this._onFrame = onFrame; this._source = null; this._retry = null; /** @type {(() => void) | null} */ this.onDisconnect = null; } /** @param {{ animation: string, brightness: number, fps: number }} opts */ start(opts) { this.stop(); const params = new URLSearchParams({ animation: opts.animation, brightness: String(opts.brightness), fps: String(opts.fps), }); this._source = new EventSource(`/api/stream?${params}`); this._source.onmessage = (event) => { this._onFrame(JSON.parse(event.data)); }; this._source.onerror = () => { this.onDisconnect?.(); this.stop(); this._retry = setTimeout(() => this.start(opts), 1500); }; } stop() { if (this._retry) { clearTimeout(this._retry); this._retry = null; } if (this._source) { this._source.close(); this._source = null; } } }