From bebdeab0d580533d4304e6b69df3e2c295de1f12 Mon Sep 17 00:00:00 2001 From: Maik Jurischka Date: Tue, 20 Oct 2026 14:45:42 +0200 Subject: [PATCH] add helper scripts --- scripts/README.md | 34 +++ scripts/start_all_cameras.sh | 453 +++++++++++++++++++++++++++++++++++ 2 files changed, 487 insertions(+) create mode 100755 scripts/start_all_cameras.sh diff --git a/scripts/README.md b/scripts/README.md index bba96a5..6d8a91a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -103,6 +103,40 @@ Zeigt: Zeigt alle verfügbaren Kamera-Auflösungen und Formate. +## Multi-Camera: alle Kameras auf einmal + +`start_all_cameras.sh` erkennt alle angeschlossenen, von VIZION unterstützten +Kameras (via `vizion-ctl -l`, Fallback: `lsusb`/Cypress 04b4) und startet für +jede der Reihe nach eine `vizionStreamer`-Instanz mit eigenem Socket +(`/tmp/vizion_cam.sock`) und startet den Stream. + +```bash +# Erkannte Kameras auflisten +./scripts/start_all_cameras.sh --list + +# Alle Kameras per UDP streamen (Port 5000 + Kamera-Index) +./scripts/start_all_cameras.sh --udp 192.168.1.100 5000 + +# Alle Kameras als MJPEG-HTTP-Stream (Port 8080 + Kamera-Index) +./scripts/start_all_cameras.sh --mjpeg 8080 + +# Alle Kameras in Dateien aufnehmen (/tmp/cam.mp4) +./scripts/start_all_cameras.sh --file /home/user/recordings + +# Lokale Anzeige +./scripts/start_all_cameras.sh --display + +# Eigene Pipeline ({port} wird durch 5000+Kamera-Index ersetzt) +./scripts/start_all_cameras.sh --pipeline "videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=192.168.1.100 port={port}" + +# Alle gestarteten Instanzen stoppen +./scripts/start_all_cameras.sh --stop +``` + +Ohne Modus-Option wird standardmäßig per UDP nach `127.0.0.1` gestreamt. +Das Script läuft im Vordergrund und stoppt mit `Ctrl+C` alle Instanzen +sauber (Stream stoppen + Prozess beenden). Logs: `/tmp/vizion_cam.log`. + ## Typischer Workflow ```bash diff --git a/scripts/start_all_cameras.sh b/scripts/start_all_cameras.sh new file mode 100755 index 0000000..c92e921 --- /dev/null +++ b/scripts/start_all_cameras.sh @@ -0,0 +1,453 @@ +#!/bin/bash +# +# start_all_cameras.sh +# +# Sucht alle angeschlossenen, von VIZION unterstützten Kameras und startet +# für jede (der Reihe nach) eine vizionStreamer-Instanz mit eigenem +# Control-Socket und startet den Stream. +# +# Erkennung: +# 1. vizion-ctl -l (VizionSDK, /opt/vizionsdk/bin/vizion-ctl) - offizielle +# Kameradetection (USB + MIPI) +# 2. Fallback: lsusb (Cypress USB-Geräte, Vendor 04b4) +# +# Pro Kamera werden angelegt: +# /tmp/vizion_cam.sock Control-Socket +# /tmp/vizion_cam.pid PID-Datei +# /tmp/vizion_cam.log Log der Instanz +# +# Copyright (c) 2025 Maik Jurischka +# Lizenziert unter CC BY-NC-SA 4.0 - https://creativecommons.org/licenses/by-nc-sa/4.0/ + +set -u + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +BINARY="$ROOT_DIR/build/vizionStreamer" + +# VizionSDK-Tool (offizielle Kameradetection) +VIZION_CTL="$(command -v vizion-ctl 2>/dev/null || true)" +[ -n "$VIZION_CTL" ] || VIZION_CTL="/opt/vizionsdk/bin/vizion-ctl" + +SOCKET_PREFIX="/tmp/vizion_cam" # /tmp/vizion_cam.sock +PID_PREFIX="/tmp/vizion_cam" # /tmp/vizion_cam.pid +LOG_PREFIX="/tmp/vizion_cam" # /tmp/vizion_cam.log +WAIT_TIMEOUT=15 # Sekunden, die auf den Socket gewartet wird + +# --------------------------------------------------------------------------- +# Ausgabe-Helfer +# --------------------------------------------------------------------------- +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } +err() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; } + +usage() { + cat <<'EOF' +Nutzung: start_all_cameras.sh [Optionen] + +Sucht alle angeschlossenen VIZION-Kameras und startet für jede (der Reihe +nach) einen Stream. + +Optionen: + --list Nur erkannte Kameras auflisten + --udp [port] UDP H.264-Stream (Standard: 127.0.0.1, Port 5000+Kamera-Index) + --mjpeg [port] MJPEG HTTP-Stream (Standard: Port 8080+Kamera-Index) + --file [dir] Aufnahme in Datei (Standard: /tmp/cam.mp4) + --display Lokale Anzeige (autovideosink) + --pipeline Eigene GStreamer-Pipeline (für {port} wird 5000+Kamera-Index eingesetzt) + --stop Alle von diesem Script gestarteten Instanzen stoppen + -h, --help Diese Hilfe anzeigen + +Beispiele: + ./scripts/start_all_cameras.sh --list + ./scripts/start_all_cameras.sh --udp 192.168.1.100 5000 + ./scripts/start_all_cameras.sh --mjpeg 8080 + ./scripts/start_all_cameras.sh --file /home/user/recordings + ./scripts/start_all_cameras.sh --pipeline "videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=192.168.1.100 port={port}" + +Empfangen (UDP, auf dem Zielrechner): + gst-launch-1.0 udpsrc port=5000 ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! avdec_h264 ! videoconvert ! autovideosink +EOF +} + +# --------------------------------------------------------------------------- +# Socket-Helfer +# --------------------------------------------------------------------------- + +# send_cmd - JSON-Command an eine Instanz senden +send_cmd() { + echo "$2" | socat - "UNIX-CONNECT:$1" 2>/dev/null +} + +# wait_for_socket - bis der Socket existiert warten +wait_for_socket() { + local sock="$1" + local i + for ((i = 0; i < WAIT_TIMEOUT * 2; i++)); do + [ -S "$sock" ] && return 0 + sleep 0.5 + done + return 1 +} + +# json_escape - String für JSON-Strings escapen +json_escape() { + local s="$1" + s=${s//\\/\\\\} + s=${s//\"/\\\"} + printf '%s' "$s" +} + +# --------------------------------------------------------------------------- +# Kamera-Erkennung +# --------------------------------------------------------------------------- +CAMERAS=() +DETECT_OUTPUT="" + +detect_cameras() { + if [ -x "$VIZION_CTL" ]; then + DETECT_OUTPUT="$("$VIZION_CTL" -l 2>&1)" + + # Indizes aus Zeilen wie "[0] VCI-AR0234 ..." extrahieren + local idxs + idxs="$(echo "$DETECT_OUTPUT" | grep -oE '^[[:space:]]*\[[0-9]+\]' | tr -d '[]' | sort -n -u)" + if [ -n "$idxs" ]; then + while IFS= read -r i; do + CAMERAS+=("$i") + done <<< "$idxs" + else + # Fallback: Anzahl aus "Found N camera(s)" o.ä. parsen + local n + n="$(echo "$DETECT_OUTPUT" | grep -oiE '[0-9]+[[:space:]]*camera' | grep -oE '^[0-9]+' | head -1)" + if [ -n "$n" ]; then + local i + for ((i = 0; i < n; i++)); do + CAMERAS+=("$i") + done + fi + fi + else + log "vizion-ctl nicht gefunden ($VIZION_CTL) - verwende lsusb als Fallback" + fi + + # Fallback: Cypress USB-Geräte (Vendor 04b4) zählen + if [ ${#CAMERAS[@]} -eq 0 ] && command -v lsusb >/dev/null 2>&1; then + local nusb + nusb="$(lsusb 2>/dev/null | grep -cE 'ID 04b4:')" + if [ "$nusb" -gt 0 ]; then + log "lsusb hat $nusb Cypress-USB-Gerät(e) (Vendor 04b4) gefunden" + local i + for ((i = 0; i < nusb; i++)); do + CAMERAS+=("$i") + done + fi + fi +} + +# --------------------------------------------------------------------------- +# Pipeline je nach Modus (Parameter: Kamera-Index) +# --------------------------------------------------------------------------- +make_pipeline() { + local i="$1" + case "$MODE" in + udp) + echo "videoconvert ! x264enc tune=zerolatency bitrate=2000 ! rtph264pay ! udpsink host=$UDP_HOST port=$((UDP_PORT + i))" + ;; + mjpeg) + echo "videoconvert ! jpegenc ! multipartmux ! tcpserversink host=0.0.0.0 port=$((MJPEG_PORT + i))" + ;; + file) + echo "videoconvert ! x264enc ! mp4mux ! filesink location=$FILE_DIR/cam${i}.mp4" + ;; + display) + echo "videoconvert ! autovideosink" + ;; + custom) + local p="$CUSTOM_PIPELINE" + if [[ "$p" == *"{port}"* ]]; then + p="${p//\{port\}/$((5000 + i))}" + fi + echo "$p" + ;; + *) + echo "" + ;; + esac +} + +# --------------------------------------------------------------------------- +# Instanz starten (Parameter: Kamera-Index) +# --------------------------------------------------------------------------- +PIDS=() + +start_camera() { + local i="$1" + local sock="$SOCKET_PREFIX${i}.sock" + local pidfile="$PID_PREFIX${i}.pid" + local logf="$LOG_PREFIX${i}.log" + local pipeline resp pid + + # Läuft für diese Kamera bereits eine Instanz? + if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile" 2>/dev/null)" 2>/dev/null; then + log "Kamera $i: läuft bereits (PID $(cat "$pidfile")) - wird übersprungen" + return 0 + fi + + pipeline="$(make_pipeline "$i")" + if [ -z "$pipeline" ]; then + err "Kamera $i: keine Pipeline konfiguriert" + return 1 + fi + + # Eventuelle alte Socket-Datei entfernen (Instanz ist laut PID-Check nicht aktiv) + rm -f "$sock" + + log "Kamera $i: starte vizionStreamer (Socket: $sock)" + "$BINARY" --camera "$i" --socket "$sock" >> "$logf" 2>&1 & + pid=$! + echo "$pid" > "$pidfile" + PIDS+=("$pid") + + if ! wait_for_socket "$sock"; then + err "Kamera $i: Socket ist nicht aufgetaucht (Kamera nicht verfügbar? Log: $logf)" + wait "$pid" 2>/dev/null + rm -f "$pidfile" + return 1 + fi + + # Pipeline setzen + resp="$(send_cmd "$sock" "{\"command\":\"set_pipeline\",\"params\":{\"pipeline\":\"$(json_escape "$pipeline")\"}}")" + if ! echo "$resp" | grep -q '"status"[[:space:]]*:[[:space:]]*"success"'; then + err "Kamera $i: set_pipeline fehlgeschlagen: $resp" + return 1 + fi + + # Stream starten + send_cmd "$sock" '{"command":"start_stream"}' >/dev/null 2>&1 + sleep 1 + + # Status prüfen + resp="$(send_cmd "$sock" '{"command":"get_status"}')" + if echo "$resp" | grep -q '"streaming"[[:space:]]*:[[:space:]]*true'; then + log "Kamera $i: [OK] Stream läuft | $pipeline" + return 0 + else + err "Kamera $i: Stream konnte nicht gestartet werden: $resp" + return 1 + fi +} + +# --------------------------------------------------------------------------- +# Alle Instanzen stoppen +# --------------------------------------------------------------------------- +stop_all() { + local found=0 + local pidfile idx sock pid + for pidfile in "$PID_PREFIX"*.pid; do + [ -e "$pidfile" ] || continue + idx="${pidfile#"$PID_PREFIX"}" + idx="${idx%.pid}" + sock="$SOCKET_PREFIX$idx.sock" + pid="$(cat "$pidfile" 2>/dev/null || true)" + + # Zuerst den Stream stoppen (best effort) + if [ -S "$sock" ]; then + send_cmd "$sock" '{"command":"stop_stream"}' >/dev/null 2>&1 + fi + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null + log "Kamera $idx: gestoppt (PID $pid)" + fi + rm -f "$pidfile" + found=1 + done + [ "$found" -eq 1 ] || log "Keine laufenden Instanzen gefunden." +} + +cleanup() { + echo + log "Stoppe alle Streams..." + stop_all + log "Fertig." + exit 0 +} + +# --------------------------------------------------------------------------- +# Argumente parsen +# --------------------------------------------------------------------------- +MODE="" +UDP_HOST="127.0.0.1" +UDP_PORT=5000 +MJPEG_PORT=8080 +FILE_DIR="/tmp" +CUSTOM_PIPELINE="" +LIST_ONLY=0 +STOP_ONLY=0 + +while [ $# -gt 0 ]; do + case "$1" in + --list) + LIST_ONLY=1 + ;; + --stop) + STOP_ONLY=1 + ;; + --udp) + MODE="udp" + if [ $# -ge 2 ] && [[ "$2" != --* ]]; then UDP_HOST="$2"; shift; fi + if [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+$ ]]; then UDP_PORT="$2"; shift; fi + ;; + --mjpeg) + MODE="mjpeg" + if [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+$ ]]; then MJPEG_PORT="$2"; shift; fi + ;; + --file) + MODE="file" + if [ $# -ge 2 ] && [[ "$2" != --* ]]; then FILE_DIR="$2"; shift; fi + ;; + --display) + MODE="display" + ;; + --pipeline) + MODE="custom" + if [ $# -lt 2 ]; then + err "--pipeline benötigt ein Argument" + exit 1 + fi + CUSTOM_PIPELINE="$2" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + err "Unbekannte Option: $1" + usage + exit 1 + ;; + esac + shift +done + +# --------------------------------------------------------------------------- +# Hauptablauf +# --------------------------------------------------------------------------- + +# Nur stoppen? +if [ $STOP_ONLY -eq 1 ]; then + stop_all + exit 0 +fi + +# Nur auflisten? +if [ $LIST_ONLY -eq 1 ]; then + detect_cameras + if [ ${#CAMERAS[@]} -eq 0 ]; then + echo "Keine VIZION-Kameras gefunden." + exit 1 + fi + echo "Gefunden: ${#CAMERAS[@]} VIZION-Kamera(n)" + if [ -n "$DETECT_OUTPUT" ]; then + echo "$DETECT_OUTPUT" + else + for i in "${CAMERAS[@]}"; do + echo " [$i] (Cypress USB-Gerät)" + done + fi + exit 0 +fi + +# Standardmodus +if [ -z "$MODE" ]; then + MODE="udp" + log "Kein Modus angegeben - Standard: UDP nach 127.0.0.1 (mit --udp an einen Remote-Rechner streamen)" +fi + +# Abhängigkeiten prüfen +if [ ! -x "$BINARY" ]; then + err "vizionStreamer nicht gefunden: $BINARY" + err "Erst bauen: cmake -B build -S . && cmake --build build" + exit 1 +fi +if ! command -v socat >/dev/null 2>&1; then + err "socat nicht gefunden (installieren mit: sudo apt install socat)" + exit 1 +fi +if [ "$MODE" = "file" ]; then + mkdir -p "$FILE_DIR" || { err "Kann Verzeichnis $FILE_DIR nicht anlegen"; exit 1; } +fi + +# Kameras erkennen +detect_cameras +if [ ${#CAMERAS[@]} -eq 0 ]; then + err "Keine VIZION-Kameras gefunden." + err "Verbindung prüfen (dmesg) und ggf. /opt/vizionsdk/init_driver.sh ausführen." + exit 1 +fi + +log "Gefunden: ${#CAMERAS[@]} VIZION-Kamera(n): ${CAMERAS[*]}" + +# Ctrl+C -> alles sauber stoppen +trap cleanup INT TERM + +# Der Reihe nach: für jede Kamera Instanz starten + Stream +OK_COUNT=0 +FAIL_COUNT=0 +for i in "${CAMERAS[@]}"; do + if start_camera "$i"; then + OK_COUNT=$((OK_COUNT + 1)) + else + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +done + +# Zusammenfassung +echo +echo "========================================" +echo "Streams gestartet: $OK_COUNT / ${#CAMERAS[@]}" +case "$MODE" in + udp) + for i in "${CAMERAS[@]}"; do + echo " Kamera $i -> udp://$UDP_HOST:$((UDP_PORT + i))" + done + echo "Empfangen (Zielrechner, Kamera 0):" + echo " gst-launch-1.0 udpsrc port=$UDP_PORT ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! avdec_h264 ! videoconvert ! autovideosink" + ;; + mjpeg) + for i in "${CAMERAS[@]}"; do + echo " Kamera $i -> http://:$((MJPEG_PORT + i))" + done + echo "Im Browser oder mit: ffplay http://:$MJPEG_PORT" + ;; + file) + for i in "${CAMERAS[@]}"; do + echo " Kamera $i -> $FILE_DIR/cam${i}.mp4" + done + ;; + display) + echo " Fenster werden lokal angezeigt" + ;; + custom) + echo " Pipeline: $CUSTOM_PIPELINE" + ;; +esac +if [ $FAIL_COUNT -gt 0 ]; then + echo " ($FAIL_COUNT Kamera(n) fehlgeschlagen - Logs unter /tmp/vizion_cam.log)" +fi +echo "========================================" +echo "Drücke Ctrl+C, um alle Streams zu stoppen." + +# Überwachung: melden, wenn eine Instanz stirbt +while true; do + sleep 5 + alive=0 + for pid in ${PIDS[@]+"${PIDS[@]}"}; do + if kill -0 "$pid" 2>/dev/null; then + alive=$((alive + 1)) + fi + done + if [ "$alive" -eq 0 ]; then + log "Alle Instanzen haben sich beendet." + break + fi +done