/** * webcamClient.js * Kapselt alle Zugriffe auf den WebCam-Service. * Basis-URL kommt von aussen (WEBCAM_URL) — kein Hartkodieren hier. * * Verwendung: * import { WebcamClient } from './webcamClient.js'; * const wc = new WebcamClient(process.env.WEBCAM_URL); * const cameras = await wc.getCameras(); * const response = await wc.getSnapshot('cam0'); // Response-Objekt, JPEG-Body */ const TIMEOUT_MS = 15_000; export class WebcamClient { /** @param {string} baseUrl z.B. "http://appRobotWebcam:8444" */ constructor(baseUrl) { if (!baseUrl) throw new Error('WebcamClient: baseUrl ist erforderlich'); this.baseUrl = baseUrl.replace(/\/$/, ''); } /** * Liste aller Kameras mit Metadaten. * `calibrationUrl` ist enthalten, wenn eine .npz unter data/calibration/{id}/ liegt. * @returns {Promise<{cameras: CameraMeta[]}>} */ async getCameras() { const res = await this.#get('/api/cameras'); if (!res.ok) throw new Error(`getCameras: HTTP ${res.status}`); return res.json(); } /** * HD-JPEG einer Kamera als fetch-Response. * Caller kann res.body direkt pipen (kein Buffering). * @param {string} id Kamera-ID, z.B. "cam0" * @param {boolean} hires true = /hires (Default), false = Live-Auflösung * @returns {Promise} */ async getSnapshot(id, hires = true) { const path = hires ? `/api/snapshot/${id}/hires` : `/api/snapshot/${id}`; const res = await this.#get(path); if (!res.ok) throw new Error(`getSnapshot(${id}): HTTP ${res.status}`); return res; } /** * Kalibrierungsdatei (.npz) als ArrayBuffer. * Kann direkt an den BodyTracker weitergereicht werden. * Wirft bei 404 (noch keine Kalibrierung vorhanden). * @param {string} id Kamera-ID * @returns {Promise} */ async getCalibration(id) { const res = await this.#get(`/api/cameras/${id}/calibration`); if (res.status === 404) throw new Error(`Keine Kalibrierung für Kamera "${id}"`); if (!res.ok) throw new Error(`getCalibration(${id}): HTTP ${res.status}`); return res.arrayBuffer(); } /** * Gesundheitsstatus des WebCam-Service inkl. Kamera-Zustände. * @returns {Promise<{status: string, cameras: CameraState[]}>} */ async health() { const res = await this.#get('/health'); if (!res.ok) throw new Error(`health: HTTP ${res.status}`); return res.json(); } // ── intern ────────────────────────────────────────────────────────────────── #get(path, options = {}) { const url = `${this.baseUrl}${path}`; return fetch(url, { ...options, signal: AbortSignal.timeout(TIMEOUT_MS), }); } } /** * @typedef {Object} CameraMeta * @property {string} id * @property {string} name * @property {string} position "front" | "left" | "right" * @property {boolean} stream * @property {boolean} hires * @property {string} encode * @property {string|null} mseCodec * @property {string} note Hardware-Serial (stable key) * @property {string|null} [calibrationUrl] vorhanden wenn .npz existiert * * @typedef {Object} CameraState * @property {string} id * @property {string} name * @property {string} device * @property {string} state "running" | "idle" | "stopping" * @property {boolean} hasFrame */