Files
appRobotWebcam/README.md
2026-06-10 09:36:43 +02:00

256 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<img>`, ~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://<host>:8444/ Viewer
http://<host>:8444/api/stream/cam0 Live-MJPEG
http://<host>:8444/api/snapshot/cam0 640er JPEG
http://<host>:8444/api/snapshot/cam0/hires HD-JPEG
http://<host>:8444/api/cameras Kamera-Metadaten (JSON)
http://<host>:8444/health Status
```
## API-Referenz
Basis-URL: `http://<host>: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_<ts>.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` | `<img src="...">` |
| `h264` | `video/mp4` (fragmentiertes MP4) | `<video>` via MSE |
Verbindung läuft bis der Client trennt. Langsame Clients droppen Frames (MJPEG)
bzw. werden getrennt (H.264, um Decode-Lücken zu vermeiden).
---
### `GET /api/config`
Laufzeit-Konfiguration aller Kameras.
```json
{
"liveSizes": ["320x240", "640x480", "1280x960"],
"encodes": ["copybsf", "mjpeg", "h264"],
"cameras": [
{
"id": "cam0",
"name": "Kamera 0",
"liveSize": "640x480",
"stream": true,
"encode": "copybsf"
}
]
}
```
---
### `POST /api/config`
Ändert Auflösung, Stream-Zustand oder Encoder einer oder mehrerer Kameras.
Änderungen werden in `cameras.json` gespeichert und sofort aktiv (Hot-Reload).
```json
// Request Body
{ "cameras": [{ "id": "cam0", "liveSize": "320x240", "stream": true, "encode": "copybsf" }] }
```
Alle Felder ausser `id` sind optional. Antwort: wie `GET /api/config`.
400 bei ungültiger Auflösung, unbekannter Kamera-ID oder falschem Typ.
---
### `GET /health`
```json
{
"status": "ok",
"cameras": [
{ "id": "cam0", "name": "Kamera 0", "device": "/dev/video0", "state": "running", "hasFrame": true }
]
}
```
`state`: `"running"` | `"idle"` | `"stopping"` — Zustand des FFmpeg-Prozesses.
---
## Deploy (Portainer)
1. Portainer → Stacks → Web editor → `docker-compose.yaml` einfügen
2. `APP_PATH` auf den absoluten Pfad des Projektverzeichnisses setzen
3. Deploy — der Container baut sich selbst (Node + FFmpeg)
```yaml
# Minimal-Konfiguration:
APP_PATH=/home/user/appRobotWebcam
```
## Architektur
```
cameras.json → server.js → CameraSwitch (/dev/videoN)
├── Live: ffmpeg → MJPEG → Browser
└── Grab: Live stoppen → hires → zurück
```
Ein FFmpeg pro Kamera, nie zwei gleichzeitig. Das `close`-Event ist der harte Beweis
„Gerät frei" — kein Race, kein 106%-CPU-Bug (der mit go2rtc aufgetreten war).
## Dokumentation
| Datei | Inhalt |
|---|---|
| `doc/01_WebcamRoadmap.md` | Ziel, Architektur, Entwicklungsgeschichte |
| `doc/05_screenShot_roadmap.md` | HD-Grab, Encode-Qualität, Kamera-Eigenheiten |
| `doc/07_multipleCam_roadmap.md` | cameras.json-Referenz, Multi-Kamera-Setup |
| `doc/09_Bug_reports.md` | Bug-Dokumentation |