Files
appRobotHoming/server/webcamClient.js
2026-06-10 09:39:32 +02:00

101 lines
3.4 KiB
JavaScript

/**
* 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<Response>}
*/
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<ArrayBuffer>}
*/
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
*/