# AppRobotWebcam Webcam-Service für den AppRobot. Liefert Live-MJPEG-Streams und HD-Standbilder über einen einzelnen HTTP-Port — als Docker-Container, ohne externe Streaming-Server. ## Was es tut | | | |---|---| | **Live-Stream** | MJPEG multipart im Browser ``, ~139 ms Latenz | | **HD-Snapshot** | Ein JPEG pro Kamera auf Knopfdruck oder per HTTP GET | | **Snapshot alle** | Alle Kameras parallel in einem Schritt | | **REST-API** | Kameraliste, Snapshots, Streams — für andere Container nutzbar | ## Kameras (aktuell) | ID | Modell | Live | HD-Grab | |---|---|---|---| | cam0 | Logitech C270 | 640×480 | 1280×960 | | cam1 | Logitech C270 | 640×480 | 1280×960 | | cam2 | Logitech C920 | 640×480 | 1920×1080 | Konfiguration ausschliesslich über `cameras.json` — kein Redeploy bei Kamera-Änderungen. ## Zugriff ``` http://:8444/ Viewer http://:8444/api/stream/cam0 Live-MJPEG http://:8444/api/snapshot/cam0 640er JPEG http://:8444/api/snapshot/cam0/hires HD-JPEG http://:8444/api/cameras Kamera-Metadaten (JSON) http://:8444/health Status ``` ## API-Referenz Basis-URL: `http://:8444` --- ### `GET /api/cameras` Vollständige Kamera-Metadaten. Primärer Einstiegspunkt für andere Container. ```json { "cameras": [ { "id": "cam0", "name": "Kamera 0", "position": "front", "stream": true, "hires": true, "encode": "copybsf", "mseCodec": null, "note": "usb-046d_0825_3BB3FE20-video-index0", "calibrationUrl": "/api/cameras/cam0/calibration" } ] } ``` `calibrationUrl` fehlt, solange noch keine `.npz` für diese Kamera vorhanden ist. `encode`: `"copybsf"` (MJPEG-Copy, Default) | `"mjpeg"` (Re-Encode) | `"h264"` (GPU, MSE). `mseCodec`: nur bei `encode="h264"` gesetzt, z.B. `"avc1.4D001F"`. --- ### `GET /api/cameras/{id}/calibration` Liefert die `.npz`-Kalibrierungsdatei (Kameramatrix + Verzerrungskoeffizienten) als Binary. ``` Content-Type: application/octet-stream Content-Disposition: attachment; filename="cam0_calibration.npz" Cache-Control: public, max-age=86400 ``` 404 wenn keine Kalibrierung vorhanden. Einlesen in Python: ```python import numpy as np, requests d = np.load(requests.get(".../api/cameras/cam0/calibration", stream=True).raw) K, D = d["camera_matrix"], d["dist_coeffs"] ``` --- ### `PUT /api/cameras/{id}/calibration` Nimmt eine neue `.npz`-Datei entgegen (Homing-Prozess → Webcam-Service). Schreibt zwei Dateien nach `data/calibration/{id}/`: | Datei | Zweck | |---|---| | `calibration_YYYYMMDD_HHMMSS.npz` | Archiv (bleibt erhalten) | | `calibration.npz` | Aktuell — wird bei jedem PUT überschrieben | Existiert beim ersten Aufruf nur `calibration.npz` (noch kein Archiv), wird sie anhand ihres `mtime` automatisch in `calibration_.npz` umbenannt, bevor die neue Datei abgelegt wird. ``` Content-Type: application/octet-stream Body: rohe .npz-Bytes ``` ```json // Response 200 { "id": "cam0", "saved": "calibration_20260610_143022.npz", "size": 1284, "calibrationUrl": "/api/cameras/cam0/calibration" } ``` Der In-Memory-Buffer wird sofort aktualisiert — `GET /api/cameras/cam0/calibration` liefert ab diesem Moment die neue Datei, ohne Server-Neustart. 400 bei leerem Body, 404 bei unbekannter Kamera-ID. Aufruf aus Python (Homing): ```python import requests with open("cam0_calibration.npz", "rb") as f: r = requests.put( "http://thinkcentre.local:8444/api/cameras/cam0/calibration", data=f, headers={"Content-Type": "application/octet-stream"}, ) r.raise_for_status() print(r.json()) # {'id': 'cam0', 'saved': 'calibration_20260610_143022.npz', ...} ``` --- ### `GET /api/snapshot/{id}` Letztes Live-JPEG (Live-Auflösung, z.B. 640×480) aus dem RAM-Puffer. Bei `stream: false`: one-shot — öffnet Gerät kurz, schließt es wieder. ``` Content-Type: image/jpeg X-Camera-Id: cam0 X-Frame-Width: 640 X-Timestamp: 2026-06-10T07:30:00.000Z Cache-Control: no-store ``` 503 wenn kein Frame verfügbar (Kamera nicht erreichbar). --- ### `GET /api/snapshot/{id}/hires` HD-JPEG (volle Auflösung, z.B. 1280×960 oder 1920×1080). Pausiert den Live-Stream kurz, nimmt ein Einzelbild auf, startet Live neu. Gleiche Response-Headers wie `/api/snapshot/{id}`. --- ### `GET /api/stream/{id}` Live-Stream. Format hängt vom `encode`-Modus ab: | encode | Content-Type | Player | |---|---|---| | `copybsf` / `mjpeg` | `multipart/x-mixed-replace; boundary=frame` | `` | | `h264` | `video/mp4` (fragmentiertes MP4) | `