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

6.9 KiB
Raw Permalink Blame History

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.

{
  "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:

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
// 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):

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.

{
  "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).

// 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

{
  "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)
# 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