# 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) | `