name: approbotwebcam

# ════════════════════════════════════════════════════════════════════════════
#  AppRobotWebcam  –  Node-MJPEG-Schalter
# ════════════════════════════════════════════════════════════════════════════
#
#  Node besitzt jede Kamera direkt (eine CameraSwitch-Instanz pro /dev/videoN).
#  Live: FFmpeg → MJPEG multipart → Browser <img>.  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://<host>:8444/
#    Live-Stream:       http://<host>:8444/api/stream/cam0
#    Snapshot:          http://<host>:8444/api/snapshot/cam0
#    HD-Snapshot:       http://<host>:8444/api/snapshot/cam0/hires
#    Kamera-Liste:      http://<host>:8444/api/cameras
#    Status:            http://<host>: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://<host>: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.
# ────────────────────────────────────────────────────────────────────────────────
