name: approbotwebcam # ════════════════════════════════════════════════════════════════════════════ # AppRobotWebcam – Node-MJPEG-Schalter # ════════════════════════════════════════════════════════════════════════════ # # Node besitzt jede Kamera direkt (eine CameraSwitch-Instanz pro /dev/videoN). # Live: FFmpeg → MJPEG multipart → Browser . Latenz: ~139 ms. # HD-Grab: Live-FFmpeg stoppen (close-Event = FD frei) → hires-FFmpeg → # JPEG an Client → Live zurück. Auflösungen in cameras.json konfiguriert. # # Kameras (aktuell, by-id = stabil über Reboots): # cam0 C270 /dev/video0 Live 640×480, Hires 1280×960 # cam1 C270 /dev/video2 Live 640×480, Hires 1280×960 # cam2 C920 /dev/video4 Live 640×480, Hires 1920×1080 # # Portainer: Stack → Web editor → dieses YAML → Deploy. # APP_PATH = /absoluter/pfad/zum/appRobotWebcam # # Netz: hängt am externen Bridge-Netz "approbot_default" → vom HTTPS-Proxy im # selben Netz erreichbar als http://AppRobotWebcam:8444 (oder webcam:8444). # Firewall: TCP 8444 am Host nur für direkten LAN-Zugriff (ports:-Mapping). Läuft # alles über den Proxy → ports:-Zeile raus, dann ist am Host nichts offen. # # Zugriff: # Viewer: http://:8444/ # Live-Stream: http://:8444/api/stream/cam0 # Snapshot: http://:8444/api/snapshot/cam0 # HD-Snapshot: http://:8444/api/snapshot/cam0/hires # Kamera-Liste: http://:8444/api/cameras # Status: http://:8444/health # ════════════════════════════════════════════════════════════════════════════ services: webcam: build: context: /tmp dockerfile_inline: | FROM node:lts-bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \ && rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/app EXPOSE 8444 image: approbotwebcam:latest container_name: AppRobotWebcam restart: unless-stopped # Hängt am externen Netz approbot_default (statt host-Mode) → der HTTPS-Proxy im # selben Netz erreicht den Container per Name (http://AppRobotWebcam:8444). networks: - approbot_default # 8444 am Host veröffentlicht → direkter LAN-Zugriff (http://:8444) bleibt. # Wenn ALLES über den Proxy läuft, diesen ports-Block entfernen → proxy-only. ports: - "8444:8444" command: sh -c "npm install --omit=dev && node server.js" volumes: - ${APP_PATH:-.}:/usr/src/app devices: # by-id (Host) → /dev/videoN (Container) – stabil über Reboots und USB-Re-Plugs. # Rechte Seite = Pfad den cameras.json + FFmpeg im Container sehen. - /dev/v4l/by-id/usb-046d_0825_3BB3FE20-video-index0:/dev/video0 # cam0 – C270 (046d:0825) - /dev/v4l/by-id/usb-046d_081b_342D4F40-video-index0:/dev/video2 # cam1 – C270 (046d:081b) - /dev/v4l/by-id/usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0:/dev/video4 # cam2 – C920 # GPU-Renderknoten für H.264-Encoding (VAAPI Intel/AMD). Nur nötig, wenn eine # Kamera encode='h264' nutzt. Auf der Intel-Box bestätigt: /dev/dri/renderD128. - /dev/dri:/dev/dri group_add: - video - render # Zugriff auf /dev/dri/renderD128 (VAAPI). GID via `getent group render`. environment: - NODE_ENV=production - PORT=8444 # Kamera-Konfiguration (Gerät, Name, Auflösung) → cameras.json im APP_PATH # Globale Fallback-Werte (gelten wenn cameras.json keinen Wert hat): # - LIVE_SIZE=640x480 # - LIVE_FPS=30 # - HIRES_SIZE=1280x960 # - HIRES_FPS=15 # - ENCODE_MODE=copybsf # copybsf = Bitstream-Copy, niedrige CPU (Default) # # mjpeg = Re-Encode (~50%, Fallback falls copybsf zickt) # # h264 = GPU-H.264 → MSE (Bandbreite sparen, braucht GPU) # - ON_DEMAND=true # Live nur bei Zuschauern (Default); 'false' = dauerhaft an # - IDLE_GRACE_MS=15000 # Karenz nach letztem Zuschauer vor dem Stop # # ── H.264-Hardware-Encoding (nur relevant für encode='h264') ────────────── # - GPU=intel # intel|amd → VAAPI (gemeinsamer Pfad) · none → libx264 (CPU-Test) # # → HIER die Maschine wählen: 'intel' (UHD 630) oder 'amd' (680M) # - HWENC=vaapi # vaapi|qsv|libx264 – Encoder erzwingen (überschreibt GPU) # - HWENC_DEVICE=/dev/dri/renderD128 # VAAPI/QSV-Renderknoten # - H264_BITRATE=3M # Zielbitrate # - H264_GOP= # Keyframe-Abstand (Default ~2×fps); kleiner = schnellerer Einstieg, mehr Bitrate # - H264_PROFILE=main # constrained_baseline|main|high (muss zum Treiber passen) # - H264_FRAG_MS=200 # fMP4-Fragmentlänge in ms # - H264_JPEG_FPS=2 # Bildrate des MJPEG-Nebenausgangs (für /api/snapshot) # - H264_MSE_CODEC= # MSE-Codec-String überschreiben, falls der Browser meckert (z.B. avc1.640020) # ── Netzwerk ──────────────────────────────────────────────────────────────────── # Externes, bereits existierendes Bridge-Netz (vom Stack "approbot"). Wird hier nur # referenziert, nicht erstellt. Prüfen: docker network inspect approbot_default networks: approbot_default: external: true name: approbot_default # ── Hinweise ──────────────────────────────────────────────────────────────────── # • Neue oder geänderte Kamera: cameras.json anpassen + Redeploy (kein Code-Änderung). # by-id-Namen ermitteln: ls -la /dev/v4l/by-id/ # Neues Device hier eintragen (by-id → /dev/videoN), dann cameras.json-Eintrag. # # • Bleibt eine Kamera schwarz oder liefert falsche Auflösung? # Direkt auf dem Host testen (ohne Docker, beweist was die Kamera real kann): # node tools/hires-probe.js /dev/video4 1920x1080 copybsf # Alternativ manuell: # v4l2-ctl --list-formats-ext -d /dev/video4 # MJPG-Auflösungen anzeigen # Nur MJPEG-native Auflösungen in cameras.json verwenden (YUYV = Software-Encode = ~50% CPU). # # • HD-Grab liefert schlechte Qualität? # Standard ist copybsf (Kamera-JPEG pur, keine zweite Kompression). # "hiresEncode": "mjpeg" in cameras.json nur als Fallback, erzeugt Re-Encode-Artefakte. # # • Code-Stand im Container prüfen: # docker exec AppRobotWebcam grep -n "Grab (minWidth" src/cameraSwitch.js # Zeigt die neue Log-Zeile → aktueller Code läuft. "1280-Grab" → staler Code. # # • Compose v2 ist Pflicht (dockerfile_inline). Bei Portainer-Warnung: Docker-Engine updaten. # ────────────────────────────────────────────────────────────────────────────────