You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
903 lines
32 KiB
903 lines
32 KiB
#!/usr/bin/env bash
|
|
# NDI Pi Gateway installer for Raspberry Pi OS Trixie / Debian 13 ARM64.
|
|
# V17: Adds configurable capture buffering and aspect-preserving HDMI canvas fitting. Integrates validated direct NDI BGRx/BGRA-to-KMS display path without videoconvert.
|
|
# Restores the manually verified NDI -> ndisrcdemux -> BGRA -> KMS live pipeline.
|
|
# Standby has explicit HDMI caps and status text; service startup tries live first
|
|
# and only grabs KMS for standby when the live pipeline is unavailable.
|
|
# Capture mode selection and NDI latency settings are unchanged; only capture queue buffering is tuned.
|
|
# Updated: installed NDI 6.x runtimes are detected without requiring the SDK archive.
|
|
set -Eeuo pipefail
|
|
|
|
# Raspberry Pi 4 / Raspberry Pi OS Trixie 64-bit
|
|
# NDI Gateway:
|
|
# USB capture + USB audio -> NDI
|
|
# NDI video -> HDMI/DRM
|
|
# NDI audio -> HDMI ALSA
|
|
#
|
|
# NDI receive design:
|
|
# - Prefer a direct NDI sender address (IP:port) when configured/resolved.
|
|
# - NDI source discovery is still used to find the service and its actual port.
|
|
# - ndi-name remains a fallback when a direct address cannot be resolved.
|
|
#
|
|
# Design:
|
|
# - GStreamer itself comes from Debian/Raspberry Pi OS.
|
|
# - The NDI GStreamer plugin is built from the gst-plugins-rs 0.14 branch/tag,
|
|
# because Debian Trixie provides GStreamer 1.26 while the NDI plugin is not
|
|
# provided as a matching Debian package.
|
|
# - NDI SDK is downloaded from the official NDI developer distribution.
|
|
#
|
|
# Environment overrides:
|
|
# NDI_SDK_URL Override the NDI SDK archive URL.
|
|
# NDI_SDK_TARBALL Use a local SDK archive instead of downloading one.
|
|
# NDI_GST_TAG Plugin tag, default 0.14.5.
|
|
#
|
|
# Existing /etc/ndi-pi.conf is preserved and migrated by adding missing keys.
|
|
#
|
|
# The script is intentionally conservative: it never assumes /dev/video0,
|
|
# never assumes an UGREEN device, and treats audio as optional.
|
|
|
|
set -Eeuo pipefail
|
|
|
|
INSTALL_DIR=/opt/ndi-pi
|
|
SRC_DIR="${INSTALL_DIR}/src"
|
|
PLUGIN_DIR="/usr/lib/aarch64-linux-gnu/gstreamer-1.0"
|
|
CONF="/etc/ndi-pi.conf"
|
|
|
|
# NDI 6.x is the current SDK line. The exact archive URL may change, so the
|
|
# default is intentionally empty: the installer asks for NDI_SDK_URL or a
|
|
# locally supplied archive. This avoids hard-coding a vendor download URL.
|
|
SDK_URL="${NDI_SDK_URL:-}"
|
|
SDK_TARBALL="${NDI_SDK_TARBALL:-${INSTALL_DIR}/ndi-sdk.tar.gz}"
|
|
|
|
PLUGIN_TAG="${NDI_GST_TAG:-0.14.5}"
|
|
PLUGIN_REPO="https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git"
|
|
|
|
log() { echo "[ndi-pi] $*"; }
|
|
warn() { echo "[ndi-pi] WARNING: $*" >&2; }
|
|
die() { echo "[ndi-pi] ERROR: $*" >&2; exit 1; }
|
|
|
|
[[ $EUID -eq 0 ]] || die "Bitte mit sudo/root ausführen."
|
|
[[ "$(dpkg --print-architecture)" == "arm64" ]] || die "Dieses Skript ist für ARM64."
|
|
|
|
. /etc/os-release
|
|
[[ "${VERSION_CODENAME:-}" == "trixie" ]] || die "Dieses Skript erwartet Debian/Raspberry Pi OS Trixie."
|
|
|
|
mkdir -p "$INSTALL_DIR" "$SRC_DIR" "$PLUGIN_DIR"
|
|
|
|
log "Installiere System-GStreamer, ALSA, V4L2, DRM und Build-Abhängigkeiten..."
|
|
apt-get update
|
|
# gstreamer1.0-plugins-good provides gst-device-monitor-1.0 on Debian Trixie.
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
ca-certificates curl wget git build-essential pkg-config \
|
|
rustc cargo \
|
|
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
|
|
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
|
gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \
|
|
gstreamer1.0-libav gstreamer1.0-alsa \
|
|
alsa-utils alsa-ucm-conf \
|
|
v4l-utils libdrm-tests libdrm-dev libudev-dev libglib2.0-dev \
|
|
avahi-daemon avahi-utils libavahi-client3 libavahi-common3
|
|
|
|
systemctl enable --now avahi-daemon || true
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# NDI SDK / already-installed NDI runtime
|
|
# ---------------------------------------------------------------------------
|
|
# The SDK archive is NOT required when libndi.so.6 is already installed.
|
|
# This is important for manual NDI SDK installations on Raspberry Pi.
|
|
#
|
|
# Accepted examples:
|
|
# /usr/local/lib/libndi.so.6
|
|
# /usr/local/lib/libndi.so.6.3.2
|
|
# /usr/lib/aarch64-linux-gnu/libndi.so.6*
|
|
#
|
|
# If libndi.so.6 is a regular file while a versioned libndi.so.6.x.y exists,
|
|
# normalize it to a symlink. This also removes the ldconfig warning:
|
|
# "libndi.so.6 is not a symbolic link"
|
|
|
|
NDI_LIB_DIR_INSTALLED=""
|
|
NDI_LIB_INSTALLED=""
|
|
|
|
find_installed_ndi() {
|
|
local candidate
|
|
for candidate in \
|
|
/usr/local/lib/libndi.so.6.* \
|
|
/usr/local/lib/libndi.so.6 \
|
|
/usr/lib/aarch64-linux-gnu/libndi.so.6.* \
|
|
/usr/lib/aarch64-linux-gnu/libndi.so.6
|
|
do
|
|
if [[ -f "$candidate" ]]; then
|
|
NDI_LIB_INSTALLED="$candidate"
|
|
NDI_LIB_DIR_INSTALLED="$(dirname "$candidate")"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
normalize_installed_ndi() {
|
|
local libdir="$1"
|
|
local major="$2"
|
|
local major_link="${libdir}/libndi.so.${major}"
|
|
local unversioned="${libdir}/libndi.so"
|
|
local versioned=""
|
|
|
|
# Prefer the most specific versioned file, e.g. libndi.so.6.3.2.
|
|
versioned="$(find "$libdir" -maxdepth 1 -type f \
|
|
-name "libndi.so.${major}.*" -print | sort -V | tail -n 1)"
|
|
|
|
if [[ -n "$versioned" ]]; then
|
|
if [[ -e "$major_link" && ! -L "$major_link" ]]; then
|
|
log "Korrigiere ${major_link}: reguläre Datei -> Sicherung + Symlink"
|
|
mv -f "$major_link" "${major_link}.manual-backup.$(date +%Y%m%d-%H%M%S)"
|
|
fi
|
|
ln -sfn "$versioned" "$major_link"
|
|
elif [[ -f "$major_link" && ! -L "$major_link" ]]; then
|
|
# A standalone libndi.so.6 is still usable. Do not overwrite it.
|
|
warn "${major_link} ist eine reguläre Datei; kein versioniertes Ziel gefunden."
|
|
fi
|
|
|
|
# Only replace the unversioned link when it is absent/broken or already a link.
|
|
if [[ -L "$unversioned" || ! -e "$unversioned" ]]; then
|
|
ln -sfn "$major_link" "$unversioned"
|
|
fi
|
|
|
|
echo "$libdir" > /etc/ld.so.conf.d/ndi.conf
|
|
ldconfig
|
|
}
|
|
|
|
if find_installed_ndi; then
|
|
log "Bereits installiertes NDI Runtime gefunden: $NDI_LIB_INSTALLED"
|
|
|
|
# Extract the SONAME major from libndi.so.6 / libndi.so.6.x.y.
|
|
NDI_MAJOR="$(basename "$NDI_LIB_INSTALLED" | sed -n 's/^libndi\.so\.\([0-9][0-9]*\).*$/\1/p')"
|
|
[[ "$NDI_MAJOR" == "6" ]] || die "Gefundene NDI Bibliothek ist nicht NDI 6.x: $NDI_LIB_INSTALLED"
|
|
|
|
normalize_installed_ndi "$NDI_LIB_DIR_INSTALLED" "$NDI_MAJOR"
|
|
|
|
# NDI_RUNTIME_DIR_V6 must point to the directory containing libndi.so.6.
|
|
NDI_RUNTIME_DIR_V6="$NDI_LIB_DIR_INSTALLED"
|
|
export NDI_RUNTIME_DIR_V6
|
|
log "NDI_RUNTIME_DIR_V6=${NDI_RUNTIME_DIR_V6}"
|
|
else
|
|
if [[ ! -s "$SDK_TARBALL" ]]; then
|
|
if [[ -z "$SDK_URL" ]]; then
|
|
die "Kein NDI 6.x Runtime und kein SDK-Archiv gefunden.
|
|
Wenn NDI bereits manuell installiert ist, muss libndi.so.6 unter
|
|
/usr/local/lib oder /usr/lib/aarch64-linux-gnu vorhanden sein.
|
|
Alternativ:
|
|
NDI_SDK_URL=<Download-URL> ./install.sh
|
|
oder
|
|
NDI_SDK_TARBALL=/pfad/zum/SDK.tar.gz ./install.sh"
|
|
fi
|
|
log "Lade NDI SDK von der angegebenen NDI-URL..."
|
|
curl -fL --retry 3 --connect-timeout 15 -o "$SDK_TARBALL" "$SDK_URL"
|
|
fi
|
|
|
|
TMP_SDK="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP_SDK"' EXIT
|
|
tar -xzf "$SDK_TARBALL" -C "$TMP_SDK"
|
|
|
|
SDK_ROOT="$(find "$TMP_SDK" -maxdepth 4 -type d -name 'NDI SDK for Linux' -print -quit)"
|
|
[[ -n "$SDK_ROOT" ]] || die "NDI SDK Verzeichnis nicht gefunden."
|
|
|
|
NDI_LIB_DIR="$SDK_ROOT/lib/aarch64-rpi4-linux-gnueabi"
|
|
if [[ ! -d "$NDI_LIB_DIR" ]]; then
|
|
NDI_LIB_DIR="$(find "$SDK_ROOT/lib" -maxdepth 2 -type d \( -name '*aarch64*rpi4*' -o -name '*aarch64*' \) -print -quit)"
|
|
fi
|
|
[[ -n "${NDI_LIB_DIR:-}" && -d "$NDI_LIB_DIR" ]] || die "Keine passende NDI ARM64/RPi4 Bibliothek im SDK gefunden."
|
|
|
|
NDI_LIB="$(find "$NDI_LIB_DIR" -maxdepth 1 -type f -name 'libndi.so.*' -print | sort -V | tail -n 1)"
|
|
[[ -n "$NDI_LIB" ]] || die "libndi.so.* nicht im SDK gefunden."
|
|
|
|
install -m 0755 "$NDI_LIB" /usr/local/lib/"$(basename "$NDI_LIB")"
|
|
NDI_BASENAME="$(basename "$NDI_LIB")"
|
|
NDI_MAJOR="$(printf '%s\n' "$NDI_BASENAME" | sed -n 's/.*\.so\.\([0-9][0-9]*\).*/\1/p')"
|
|
[[ -n "$NDI_MAJOR" ]] || NDI_MAJOR=6
|
|
ln -sfn "/usr/local/lib/$NDI_BASENAME" "/usr/local/lib/libndi.so.${NDI_MAJOR}"
|
|
ln -sfn "/usr/local/lib/libndi.so.${NDI_MAJOR}" /usr/local/lib/libndi.so
|
|
echo /usr/local/lib > /etc/ld.so.conf.d/ndi.conf
|
|
ldconfig
|
|
|
|
NDI_RUNTIME_DIR_V6=/usr/local/lib
|
|
export NDI_RUNTIME_DIR_V6
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build the NDI GStreamer plugin
|
|
# ---------------------------------------------------------------------------
|
|
cd "$SRC_DIR"
|
|
if [[ ! -d gst-plugins-rs ]]; then
|
|
git clone --depth 1 --branch "$PLUGIN_TAG" "$PLUGIN_REPO" gst-plugins-rs
|
|
fi
|
|
|
|
cd gst-plugins-rs
|
|
git fetch --depth 1 origin "refs/tags/${PLUGIN_TAG}:refs/tags/${PLUGIN_TAG}" || true
|
|
git checkout -q "$PLUGIN_TAG"
|
|
|
|
if [[ -x /root/.cargo/bin/rustc ]]; then
|
|
export PATH="/root/.cargo/bin:$PATH"
|
|
fi
|
|
|
|
log "Baue gst-plugin-ndi ${PLUGIN_TAG} gegen das installierte GStreamer..."
|
|
cargo build -p gst-plugin-ndi --release
|
|
|
|
PLUGIN_SO="$(find target -type f -name 'libgstndi.so' -not -path '*/deps/*' -print -quit)"
|
|
[[ -n "$PLUGIN_SO" ]] || die "libgstndi.so wurde nicht erzeugt."
|
|
install -m 0755 "$PLUGIN_SO" "$PLUGIN_DIR/libgstndi.so"
|
|
rm -f /root/.cache/gstreamer-1.0/registry.*.bin 2>/dev/null || true
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
HOSTNAME_DEFAULT="$(hostname -s | tr '[:lower:]' '[:upper:]')"
|
|
if [[ ! -f "$CONF" ]]; then
|
|
cat > "$CONF" <<EOF
|
|
# Raspberry Pi NDI Gateway
|
|
|
|
PI_NAME=${HOSTNAME_DEFAULT}
|
|
NDI_CAPTURE_NAME=${HOSTNAME_DEFAULT}-Capture
|
|
NDI_DISPLAY_SOURCE=PC01-Capture
|
|
# auto = resolve NDI service via mDNS and connect directly to IP:port.
|
|
# You can also set a fixed address, e.g. 192.168.180.171:5961.
|
|
NDI_DISPLAY_ADDRESS=auto
|
|
|
|
# auto = any suitable V4L2 capture device, not vendor-specific.
|
|
CAPTURE_DEVICE=auto
|
|
|
|
# Preferred capture mode. auto selection only uses modes actually advertised
|
|
# by the capture card.
|
|
CAPTURE_WIDTH=1920
|
|
CAPTURE_HEIGHT=1080
|
|
CAPTURE_FPS=30
|
|
CAPTURE_FORMAT=MJPEG
|
|
CAPTURE_AUTO_FALLBACK=true
|
|
|
|
# Capture queue after jpegdec; 2 buffers are about 33 ms at 60 fps.
|
|
# leaky=downstream discards stale frames if decoding briefly falls behind.
|
|
CAPTURE_QUEUE_BUFFERS=2
|
|
|
|
# Audio from the selected USB capture device.
|
|
ENABLE_AUDIO=true
|
|
CAPTURE_AUDIO_DEVICE=auto
|
|
CAPTURE_AUDIO_RATE=48000
|
|
CAPTURE_AUDIO_CHANNELS=2
|
|
|
|
# NDI -> HDMI audio. If no HDMI ALSA device is present, video continues.
|
|
ENABLE_HDMI_AUDIO=false
|
|
KMS_DRIVER=vc4
|
|
KMS_CONNECTOR_ID=auto
|
|
HDMI_AUDIO_DEVICE=auto
|
|
|
|
# NDI receive queue. 0 = minimal latency (validated setting, do not raise
|
|
# without a documented reason - see NDI-Pi-Gateway-Dokumentation.md, §7).
|
|
NDI_MAX_QUEUE=0
|
|
|
|
# Direct NDI receive: when true, resolve the advertised NDI service via mDNS
|
|
# and feed its actual IP:port to ndisrc url-address.
|
|
NDI_USE_DIRECT_ADDRESS=true
|
|
EOF
|
|
fi
|
|
chmod 0644 "$CONF"
|
|
|
|
# Migrate an existing configuration without overwriting user settings.
|
|
ensure_conf_key() {
|
|
local key="$1" value="$2"
|
|
if ! grep -qE "^[[:space:]]*${key}=" "$CONF"; then
|
|
printf '\n%s=%s\n' "$key" "$value" >> "$CONF"
|
|
log "Konfiguration ergänzt: ${key}=${value}"
|
|
fi
|
|
}
|
|
ensure_conf_key NDI_DISPLAY_ADDRESS auto
|
|
ensure_conf_key NDI_USE_DIRECT_ADDRESS true
|
|
ensure_conf_key KMS_DRIVER vc4
|
|
ensure_conf_key KMS_CONNECTOR_ID auto
|
|
ensure_conf_key ENABLE_HDMI_AUDIO false
|
|
if grep -qE '^NDI_MAX_QUEUE=(4|10)$' "$CONF"; then
|
|
sed -i -E 's/^NDI_MAX_QUEUE=(4|10)$/NDI_MAX_QUEUE=0/' "$CONF"
|
|
log "Fehlerhaften Zwischenstand NDI_MAX_QUEUE=4/10 auf 0 zurückgesetzt (validierte Latenz-Einstellung)."
|
|
fi
|
|
ensure_conf_key NDI_MAX_QUEUE 0
|
|
ensure_conf_key CAPTURE_QUEUE_BUFFERS 2
|
|
ensure_conf_key DISPLAY_WIDTH 1920
|
|
ensure_conf_key DISPLAY_HEIGHT 1080
|
|
ensure_conf_key DISPLAY_FPS 60
|
|
ensure_conf_key NDI_USE_DIRECT_ADDRESS true
|
|
|
|
install -d -m 0755 "$INSTALL_DIR/bin"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Capture helper
|
|
# ---------------------------------------------------------------------------
|
|
cat > "$INSTALL_DIR/bin/ndi-capture.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
source /etc/ndi-pi.conf
|
|
|
|
log() { echo "[ndi-capture] $*"; }
|
|
warn() { echo "[ndi-capture] WARNING: $*" >&2; }
|
|
|
|
is_video_capture() {
|
|
local d="$1" caps
|
|
caps="$(v4l2-ctl -d "$d" --all 2>/dev/null || true)"
|
|
grep -qiE 'Video Capture' <<<"$caps"
|
|
}
|
|
|
|
fmt_caps() {
|
|
v4l2-ctl -d "$1" --list-formats-ext 2>/dev/null || true
|
|
}
|
|
|
|
supports_mode() {
|
|
local d="$1" fmt="$2" w="$3" h="$4" fps="$5"
|
|
local caps block
|
|
caps="$(fmt_caps "$d")"
|
|
[[ -n "$caps" ]] || return 1
|
|
|
|
# Extract the complete format block. Trixie's v4l2-ctl output can use
|
|
# either [N]: 'MJPG' or a Pixel Format heading depending on the driver.
|
|
block="$(awk -v wanted="$fmt" '
|
|
function isfmt(line) {
|
|
if (wanted == "MJPEG") return line ~ /MJPG|MJPEG|Motion-JPEG/
|
|
return line ~ /YUYV|YUY2|YUV 4:2:2/
|
|
}
|
|
isfmt($0) { if (found) exit; found=1; print; next }
|
|
found {
|
|
if ($0 ~ /^[[:space:]]*\[[0-9]+\]:/ || $0 ~ /^[[:space:]]*Pixel Format:/) exit
|
|
print
|
|
}
|
|
' <<<"$caps")"
|
|
|
|
[[ -n "$block" ]] || return 1
|
|
grep -qE "${w}[[:space:]]*[x×][[:space:]]*${h}" <<<"$block" || return 1
|
|
|
|
awk -v wanted="$fps" '
|
|
{
|
|
line=$0
|
|
while (match(line, /([0-9]+([.][0-9]+)?)[[:space:]]*([/][[:space:]]*[0-9]+)?[[:space:]]*fps/)) {
|
|
val=substr(line, RSTART, RLENGTH)
|
|
gsub(/fps/, "", val); gsub(/[[:space:]]/, "", val)
|
|
if (index(val, "/")) {
|
|
split(val,a,"/")
|
|
if (a[2] != 0 && (a[1]/a[2]) >= wanted-0.01 && (a[1]/a[2]) <= wanted+0.01) ok=1
|
|
} else if ((val+0) >= wanted-0.01 && (val+0) <= wanted+0.01) {
|
|
ok=1
|
|
}
|
|
line=substr(line, RSTART+RLENGTH)
|
|
}
|
|
}
|
|
END { exit(ok ? 0 : 1) }
|
|
' <<<"$block"
|
|
}
|
|
|
|
|
|
mode_score() {
|
|
local fmt="$1" w="$2" h="$3" fps="$4"
|
|
local score=0
|
|
[[ "$fmt" == "YUYV" ]] && score=$((score + 1000000))
|
|
score=$((score + w*h))
|
|
score=$((score + fps*10000))
|
|
echo "$score"
|
|
}
|
|
|
|
select_mode() {
|
|
local d="$1"
|
|
local wanted_fmt="${CAPTURE_FORMAT:-auto}"
|
|
local desired_w="${CAPTURE_WIDTH:-1920}"
|
|
local desired_h="${CAPTURE_HEIGHT:-1080}"
|
|
local desired_fps="${CAPTURE_FPS:-60}"
|
|
local candidates=(
|
|
"YUYV|1920|1080|60"
|
|
"YUYV|1920|1080|50"
|
|
"YUYV|1920|1080|30"
|
|
"YUYV|1280|720|60"
|
|
"YUYV|1280|720|50"
|
|
"YUYV|1280|720|30"
|
|
"YUYV|720|480|60"
|
|
"YUYV|720|480|30"
|
|
"MJPEG|1920|1080|60"
|
|
"MJPEG|1920|1080|50"
|
|
"MJPEG|1920|1080|30"
|
|
"MJPEG|1280|720|60"
|
|
"MJPEG|1280|720|50"
|
|
"MJPEG|1280|720|30"
|
|
)
|
|
|
|
# First try the user's exact preferred resolution/fps.
|
|
local c fmt w h fps
|
|
for c in "${candidates[@]}"; do
|
|
IFS='|' read -r fmt w h fps <<<"$c"
|
|
[[ "$wanted_fmt" != "auto" && "$fmt" != "$wanted_fmt" ]] && continue
|
|
if supports_mode "$d" "$fmt" "$desired_w" "$desired_h" "$desired_fps"; then
|
|
echo "$fmt|$desired_w|$desired_h|$desired_fps"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
[[ "${CAPTURE_AUTO_FALLBACK:-true}" == "true" ]] || return 1
|
|
|
|
# Then use the ordered practical fallback list above.
|
|
for c in "${candidates[@]}"; do
|
|
IFS='|' read -r fmt w h fps <<<"$c"
|
|
[[ "$wanted_fmt" != "auto" && "$fmt" != "$wanted_fmt" ]] && continue
|
|
if supports_mode "$d" "$fmt" "$w" "$h" "$fps"; then
|
|
echo "$fmt|$w|$h|$fps"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
usb_key() {
|
|
local p="$1" x base
|
|
p="$(readlink -f "$p" 2>/dev/null || true)"
|
|
[[ -n "$p" ]] || return 1
|
|
IFS='/' read -ra parts <<<"${p#/}"
|
|
for x in "${parts[@]}"; do
|
|
if [[ "$x" =~ ^[0-9]+-[0-9]+([.][0-9]+)*$ ]]; then
|
|
base="$x"
|
|
fi
|
|
done
|
|
[[ -n "${base:-}" ]] && echo "$base"
|
|
}
|
|
|
|
score_device() {
|
|
local d="$1" mode="$2" score=0 bus fmt w h fps
|
|
IFS='|' read -r fmt w h fps <<<"$mode"
|
|
|
|
# Keep the generic USB preference, but score the ACTUAL mode selected by
|
|
# select_mode(). In auto mode YUYV is intentionally preferred over MJPEG.
|
|
bus="$(udevadm info -q property -n "$d" 2>/dev/null | awk -F= '$1=="ID_BUS"{print $2; exit}')"
|
|
[[ "$bus" == "usb" ]] && score=$((score + 5000000))
|
|
[[ "$fmt" == "YUYV" ]] && score=$((score + 1000000))
|
|
[[ "$fmt" == "MJPEG" ]] && score=$((score + 500000))
|
|
[[ "$w" == "$CAPTURE_WIDTH" ]] && [[ "$h" == "$CAPTURE_HEIGHT" ]] && score=$((score + 100000))
|
|
[[ "$fps" == "$CAPTURE_FPS" ]] && score=$((score + 10000))
|
|
score=$((score + w*h))
|
|
score=$((score + fps*100))
|
|
echo "$score"
|
|
}
|
|
|
|
pick_capture() {
|
|
if [[ "${CAPTURE_DEVICE:-auto}" != "auto" ]]; then
|
|
echo "$CAPTURE_DEVICE"
|
|
return
|
|
fi
|
|
|
|
local best="" best_score=-1 d caps mode score
|
|
shopt -s nullglob
|
|
for d in /dev/video*; do
|
|
[[ -e "$d" ]] || continue
|
|
is_video_capture "$d" || continue
|
|
caps="$(fmt_caps "$d")"
|
|
mode="$(select_mode "$d" 2>/dev/null || true)"
|
|
[[ -n "$mode" ]] || continue
|
|
score="$(score_device "$d" "$mode")"
|
|
[[ "$score" -gt "$best_score" ]] && { best="$d"; best_score="$score"; }
|
|
done
|
|
shopt -u nullglob
|
|
[[ -n "$best" ]] && echo "$best"
|
|
}
|
|
|
|
find_audio_capture() {
|
|
[[ "${ENABLE_AUDIO:-true}" == "true" ]] || return 1
|
|
[[ "${CAPTURE_AUDIO_DEVICE:-auto}" != "auto" ]] && { echo "$CAPTURE_AUDIO_DEVICE"; return 0; }
|
|
|
|
local vkey card idx path skey best="" fallback=""
|
|
vkey="$(usb_key "$(udevadm info -q path -n "$DEV" 2>/dev/null || true)")" || true
|
|
|
|
# Prefer a USB sound card on the same USB device as the selected video node.
|
|
while read -r idx _; do
|
|
[[ "$idx" =~ ^[0-9]+$ ]] || continue
|
|
path="$(readlink -f "/sys/class/sound/card${idx}/device" 2>/dev/null || true)"
|
|
[[ -n "$path" ]] || continue
|
|
skey="$(usb_key "$path" 2>/dev/null || true)"
|
|
if [[ -n "$vkey" && -n "$skey" && "$vkey" == "$skey" ]]; then
|
|
best="plughw:${idx},0"
|
|
break
|
|
fi
|
|
if [[ -n "$skey" && -z "$fallback" ]]; then
|
|
fallback="plughw:${idx},0"
|
|
fi
|
|
done < <(grep -E '^[[:space:]]*[0-9]+[[:space:]]+\[' /proc/asound/cards 2>/dev/null | sed -E 's/^[[:space:]]*([0-9]+).*/\1/')
|
|
|
|
[[ -n "$best" ]] && { echo "$best"; return 0; }
|
|
[[ -n "$fallback" ]] && { echo "$fallback"; return 0; }
|
|
return 1
|
|
}
|
|
|
|
DEV="$(pick_capture)" || {
|
|
warn "Kein geeignetes V4L2-Capture-Gerät gefunden; systemd startet später erneut."
|
|
for d in /dev/video*; do
|
|
[[ -e "$d" ]] || continue
|
|
warn "V4L2-Info für $d:"
|
|
v4l2-ctl -d "$d" --all 2>&1 | sed 's/^/[ndi-capture] /' || true
|
|
v4l2-ctl -d "$d" --list-formats-ext 2>&1 | sed 's/^/[ndi-capture] /' || true
|
|
done
|
|
exit 1
|
|
}
|
|
|
|
MODE="$(select_mode "$DEV")" || {
|
|
warn "Kein unterstützter Capture-Modus auf $DEV."
|
|
fmt_caps "$DEV" | sed 's/^/[ndi-capture] /' >&2 || true
|
|
exit 1
|
|
}
|
|
IFS='|' read -r FMT WIDTH HEIGHT FPS <<<"$MODE"
|
|
|
|
AUDIO_DEV="$(find_audio_capture || true)"
|
|
|
|
# MJPEG wird mit dem normalen Software-Decoder jpegdec dekodiert.
|
|
# v4l2jpegdec wird bewusst NICHT verwendet: die Testreihe zeigte dort
|
|
# wiederholt "slow copy into buffer" und sichtbare Micro-Ruckler.
|
|
log "Capture: $DEV, $FMT ${WIDTH}x${HEIGHT}@${FPS}"
|
|
[[ -n "$AUDIO_DEV" ]] && log "Capture-Audio: $AUDIO_DEV" || warn "Kein passendes USB-Audio gefunden; Video-only."
|
|
|
|
if [[ "$FMT" == "MJPEG" ]]; then
|
|
VIDEO_CAPS="image/jpeg,width=${WIDTH},height=${HEIGHT},framerate=${FPS}/1"
|
|
else
|
|
VIDEO_CAPS="video/x-raw,format=YUY2,width=${WIDTH},height=${HEIGHT},framerate=${FPS}/1"
|
|
fi
|
|
|
|
# MJPEG: v4l2src(io-mode=2) -> jpegdec -> konfigurierbare kurze Queue -> ndisink.
|
|
# Die Queue puffert kurze Decoder-/Scheduling-Schwankungen; leaky downstream
|
|
# verhindert, dass bei Überlastung zunehmend alte Frames ausgegeben werden.
|
|
CAPTURE_QUEUE_BUFFERS="${CAPTURE_QUEUE_BUFFERS:-2}"
|
|
if ! [[ "$CAPTURE_QUEUE_BUFFERS" =~ ^[1-9][0-9]*$ ]]; then
|
|
warn "CAPTURE_QUEUE_BUFFERS muss eine positive ganze Zahl sein; verwende 2."
|
|
CAPTURE_QUEUE_BUFFERS=2
|
|
fi
|
|
CAPTURE_QUEUE=(queue "max-size-buffers=${CAPTURE_QUEUE_BUFFERS}" max-size-time=0 max-size-bytes=0 leaky=downstream)
|
|
# UYVY ist nur beim YUYV-Fallback nötig, weil ndisink YUY2/YUYV nicht direkt annimmt.
|
|
if [[ -n "$AUDIO_DEV" ]]; then
|
|
if [[ "$FMT" == "MJPEG" ]]; then
|
|
exec gst-launch-1.0 -e \
|
|
ndisinkcombiner name=combiner ! \
|
|
ndisink ndi-name="${NDI_CAPTURE_NAME}" sync=false qos=true \
|
|
v4l2src device="$DEV" io-mode=2 ! \
|
|
"$VIDEO_CAPS" ! \
|
|
jpegdec ! "${CAPTURE_QUEUE[@]}" ! combiner.video \
|
|
alsasrc device="$AUDIO_DEV" do-timestamp=true ! \
|
|
queue max-size-time=100000000 max-size-buffers=0 max-size-bytes=0 ! \
|
|
audioconvert ! audioresample ! \
|
|
audio/x-raw,format=F32LE,rate="${CAPTURE_AUDIO_RATE:-48000}",channels="${CAPTURE_AUDIO_CHANNELS:-2}" ! \
|
|
combiner.audio
|
|
else
|
|
exec gst-launch-1.0 -e \
|
|
ndisinkcombiner name=combiner ! \
|
|
ndisink ndi-name="${NDI_CAPTURE_NAME}" sync=false qos=true \
|
|
v4l2src device="$DEV" io-mode=2 ! \
|
|
"$VIDEO_CAPS" ! \
|
|
videoconvert ! video/x-raw,format=UYVY ! "${CAPTURE_QUEUE[@]}" ! combiner.video \
|
|
alsasrc device="$AUDIO_DEV" do-timestamp=true ! \
|
|
queue max-size-time=100000000 max-size-buffers=0 max-size-bytes=0 ! \
|
|
audioconvert ! audioresample ! \
|
|
audio/x-raw,format=F32LE,rate="${CAPTURE_AUDIO_RATE:-48000}",channels="${CAPTURE_AUDIO_CHANNELS:-2}" ! \
|
|
combiner.audio
|
|
fi
|
|
else
|
|
if [[ "$FMT" == "MJPEG" ]]; then
|
|
exec gst-launch-1.0 -e \
|
|
v4l2src device="$DEV" io-mode=2 ! \
|
|
"$VIDEO_CAPS" ! \
|
|
jpegdec ! \
|
|
"${CAPTURE_QUEUE[@]}" ! \
|
|
ndisink ndi-name="${NDI_CAPTURE_NAME}" sync=false qos=true
|
|
else
|
|
exec gst-launch-1.0 -e \
|
|
v4l2src device="$DEV" io-mode=2 ! \
|
|
"$VIDEO_CAPS" ! \
|
|
videoconvert ! video/x-raw,format=UYVY ! \
|
|
"${CAPTURE_QUEUE[@]}" ! \
|
|
ndisink ndi-name="${NDI_CAPTURE_NAME}" sync=false qos=true
|
|
fi
|
|
fi
|
|
EOF
|
|
chmod 0755 "$INSTALL_DIR/bin/ndi-capture.sh"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Display helper
|
|
# ---------------------------------------------------------------------------
|
|
cat > "$INSTALL_DIR/bin/ndi-display.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
source /etc/ndi-pi.conf
|
|
|
|
log(){ echo "[ndi-display] $*"; }
|
|
warn(){ echo "[ndi-display] WARN: $*" >&2; }
|
|
|
|
find_kms_connector() {
|
|
local driver="${KMS_DRIVER:-vc4}" wanted="${KMS_CONNECTOR_ID:-auto}" line
|
|
if [[ "$wanted" != "auto" && "$wanted" =~ ^[0-9]+$ ]]; then echo "$wanted"; return 0; fi
|
|
command -v modetest >/dev/null 2>&1 || return 1
|
|
line="$(modetest -M "$driver" -c 2>/dev/null | awk '$3 == "connected" {print $1; exit}')"
|
|
[[ "$line" =~ ^[0-9]+$ ]] || return 1
|
|
echo "$line"
|
|
}
|
|
|
|
KMS_CONNECTOR="$(find_kms_connector || true)"
|
|
if [[ -z "$KMS_CONNECTOR" ]]; then
|
|
warn "Kein verbundener ${KMS_DRIVER:-vc4}-HDMI-Connector gefunden."
|
|
exit 1
|
|
fi
|
|
log "DRM/KMS: driver=${KMS_DRIVER:-vc4}, connector-id=${KMS_CONNECTOR}"
|
|
|
|
resolve_ndi_address() {
|
|
local source="$1"
|
|
command -v avahi-browse >/dev/null 2>&1 || return 1
|
|
timeout 8 avahi-browse -rtp _ndi._tcp 2>/dev/null |
|
|
awk -F';' -v wanted="$source" '$1 == "=" && $4 == wanted && $8 ~ /^[0-9]+([.][0-9]+){3}$/ && $9 ~ /^[0-9]+$/ {print $8 ":" $9; exit}' | head -n 1
|
|
}
|
|
|
|
NDI_ADDRESS="${NDI_DISPLAY_ADDRESS:-auto}"
|
|
if [[ "${NDI_USE_DIRECT_ADDRESS:-true}" == "true" && ( -z "$NDI_ADDRESS" || "$NDI_ADDRESS" == "auto" ) ]]; then
|
|
NDI_ADDRESS="$(resolve_ndi_address "$NDI_DISPLAY_SOURCE" || true)"
|
|
[[ -n "$NDI_ADDRESS" ]] && log "NDI-Service aufgelöst: ${NDI_DISPLAY_SOURCE} -> ${NDI_ADDRESS}" || true
|
|
elif [[ "${NDI_USE_DIRECT_ADDRESS:-true}" == "true" && -n "$NDI_ADDRESS" && "$NDI_ADDRESS" != "auto" ]]; then
|
|
log "Verwende konfigurierte NDI-Adresse: ${NDI_ADDRESS}"
|
|
fi
|
|
|
|
if [[ "${NDI_USE_DIRECT_ADDRESS:-true}" == "true" && -n "$NDI_ADDRESS" && "$NDI_ADDRESS" != "auto" ]]; then
|
|
log "Verwende ndisrc url-address=${NDI_ADDRESS}"
|
|
NDI_SRC_ARGS=(ndisrc "url-address=${NDI_ADDRESS}" "color-format=bgrx-bgra" "max-queue-length=${NDI_MAX_QUEUE:-0}" "connect-timeout=10000" "timeout=5000")
|
|
else
|
|
log "Verwende ndisrc ndi-name=${NDI_DISPLAY_SOURCE} (Fallback)"
|
|
NDI_SRC_ARGS=(ndisrc "ndi-name=${NDI_DISPLAY_SOURCE}" "color-format=bgrx-bgra" "max-queue-length=${NDI_MAX_QUEUE:-0}" "connect-timeout=10000" "timeout=5000")
|
|
fi
|
|
|
|
WIDTH="${DISPLAY_WIDTH:-1920}"
|
|
HEIGHT="${DISPLAY_HEIGHT:-1080}"
|
|
FPS="${DISPLAY_FPS:-60}"
|
|
STANDBY_PID=""
|
|
|
|
# Keep the standby self-contained and explicitly sized. Without a capsfilter,
|
|
# videotestsrc negotiates its own small default size and kmssink can display that
|
|
# image only in the middle of the HDMI output. The explicit caps below make the
|
|
# standby a real fullscreen framebuffer at the configured HDMI mode.
|
|
HOST_SHORT="$(hostname -s 2>/dev/null || hostname)"
|
|
HOST_SHORT="${HOST_SHORT^^}"
|
|
LOCAL_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
|
LOCAL_IP="${LOCAL_IP:-unknown}"
|
|
STANDBY_TEXT="NO SIGNAL ${HOST_SHORT} IP:${LOCAL_IP} HDMI:${WIDTH}x${HEIGHT}@${FPS} NDI:${NDI_DISPLAY_SOURCE}"
|
|
|
|
|
|
|
|
stop_standby() {
|
|
if [[ -n "${STANDBY_PID:-}" ]]; then
|
|
kill "$STANDBY_PID" 2>/dev/null || true
|
|
wait "$STANDBY_PID" 2>/dev/null || true
|
|
STANDBY_PID=""
|
|
fi
|
|
}
|
|
|
|
start_standby() {
|
|
stop_standby
|
|
log "Aktiviere schwarzen HDMI-Standby (Fullscreen ${WIDTH}x${HEIGHT}@${FPS})."
|
|
|
|
env NDI_RUNTIME_DIR_V6="${NDI_RUNTIME_DIR_V6:-/usr/local/lib}" \
|
|
gst-launch-1.0 -q \
|
|
videotestsrc pattern=black is-live=true ! \
|
|
"video/x-raw,format=BGRA,width=${WIDTH},height=${HEIGHT},framerate=${FPS}/1" ! \
|
|
textoverlay text="${STANDBY_TEXT}" \
|
|
halignment=center valignment=center \
|
|
font-desc="Sans Bold, 32" \
|
|
shaded-background=true \
|
|
! video/x-raw,format=BGRA ! \
|
|
kmssink driver-name="${KMS_DRIVER:-vc4}" connector-id="$KMS_CONNECTOR" sync=false \
|
|
>/tmp/ndi-display-standby.log 2>&1 &
|
|
STANDBY_PID=$!
|
|
|
|
sleep 0.8
|
|
if ! kill -0 "$STANDBY_PID" 2>/dev/null; then
|
|
warn "HDMI-Standby konnte nicht gestartet werden. Ausgabe:"
|
|
sed 's/^/[ndi-standby] /' /tmp/ndi-display-standby.log >&2 || true
|
|
STANDBY_PID=""
|
|
return 1
|
|
fi
|
|
|
|
log "HDMI-Standby ist fullscreen aktiv (PID=${STANDBY_PID})."
|
|
}
|
|
|
|
cleanup() {
|
|
stop_standby
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Important for service restarts: do not first grab the DRM connector with a
|
|
# standby kmssink and then immediately release it for the live kmssink. When the
|
|
# NDI source is available, go directly to the verified live path. Only if that
|
|
# path exits do we acquire KMS for the standby screen.
|
|
while true; do
|
|
log "Starte NDI-Livepipeline für ${NDI_DISPLAY_SOURCE}."
|
|
|
|
set +e
|
|
env NDI_RUNTIME_DIR_V6="${NDI_RUNTIME_DIR_V6:-/usr/local/lib}" \
|
|
gst-launch-1.0 -e \
|
|
"${NDI_SRC_ARGS[@]}" ! \
|
|
ndisrcdemux name=d \
|
|
d.video ! \
|
|
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
|
videoscale add-borders=true ! \
|
|
"video/x-raw,width=${WIDTH},height=${HEIGHT},pixel-aspect-ratio=1/1" ! \
|
|
kmssink driver-name="${KMS_DRIVER:-vc4}" connector-id="$KMS_CONNECTOR" sync=false \
|
|
d.audio ! queue ! fakesink sync=false
|
|
rc=$?
|
|
set -e
|
|
|
|
warn "NDI-Livepipeline beendet (rc=$rc). Rückkehr zu schwarzem Standby."
|
|
start_standby || true
|
|
|
|
# Keep the fullscreen standby active while the source is unavailable. Do not
|
|
# repeatedly tear down/recreate kmssink: that was the visible flicker reported
|
|
# during service restarts/source outages. Retry only after the NDI service can
|
|
# be resolved/reached again.
|
|
while true; do
|
|
sleep 2
|
|
candidate="$NDI_ADDRESS"
|
|
if [[ "${NDI_USE_DIRECT_ADDRESS:-true}" == "true" && ( -z "$candidate" || "$candidate" == "auto" ) ]]; then
|
|
candidate="$(resolve_ndi_address "$NDI_DISPLAY_SOURCE" || true)"
|
|
fi
|
|
|
|
if [[ -n "$candidate" && "$candidate" != "auto" && "$candidate" == *:* ]]; then
|
|
host="${candidate%:*}"
|
|
port="${candidate##*:}"
|
|
if [[ "$port" =~ ^[0-9]+$ ]] && timeout 1 bash -c "</dev/tcp/$host/$port" 2>/dev/null; then
|
|
log "NDI-Service wieder erreichbar (${candidate}); beende Standby und versuche Live erneut."
|
|
NDI_ADDRESS="$candidate"
|
|
if [[ "${NDI_USE_DIRECT_ADDRESS:-true}" == "true" ]]; then
|
|
NDI_SRC_ARGS=(ndisrc "url-address=${NDI_ADDRESS}" "color-format=bgrx-bgra" "max-queue-length=${NDI_MAX_QUEUE:-0}" "connect-timeout=10000" "timeout=5000")
|
|
fi
|
|
stop_standby
|
|
break
|
|
fi
|
|
fi
|
|
done
|
|
done
|
|
EOF
|
|
chmod 0755 "$INSTALL_DIR/bin/ndi-display.sh"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Diagnostic tool
|
|
# ---------------------------------------------------------------------------
|
|
cat > "$INSTALL_DIR/bin/ndi-pi-diagnose" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -u
|
|
source /etc/ndi-pi.conf 2>/dev/null || true
|
|
|
|
echo "=== NDI Pi Diagnose ==="
|
|
echo "Kernel: $(uname -a)"
|
|
echo "OS: $(. /etc/os-release; echo "${PRETTY_NAME:-unknown}")"
|
|
echo
|
|
echo "--- GStreamer ---"
|
|
gst-launch-1.0 --version || true
|
|
gst-inspect-1.0 ndisink ndisinkcombiner ndisrc ndisrcdemux kmssink 2>&1 | grep -E '^(Factory Details| Rank| Long-name| Description| Version| Author|Pad Templates| SINK| SRC| AUDIO| VIDEO)' || true
|
|
echo
|
|
echo "--- V4L2 ---"
|
|
v4l2-ctl --list-devices || true
|
|
for d in /dev/video*; do
|
|
[[ -e "$d" ]] || continue
|
|
echo "### $d"
|
|
v4l2-ctl -d "$d" --all 2>/dev/null | grep -E 'Card type|Driver name|Bus info|Capabilities' || true
|
|
v4l2-ctl -d "$d" --list-formats-ext 2>/dev/null | head -80 || true
|
|
done
|
|
echo
|
|
echo "--- ALSA capture ---"
|
|
arecord -l || true
|
|
echo
|
|
echo "--- ALSA playback ---"
|
|
aplay -l || true
|
|
echo
|
|
echo "--- DRM ---"
|
|
ls -l /dev/dri 2>/dev/null || true
|
|
cat /sys/class/drm/*/status 2>/dev/null || true
|
|
echo
|
|
echo "--- NDI configuration ---"
|
|
grep -E '^(NDI_DISPLAY_SOURCE|NDI_DISPLAY_ADDRESS|NDI_USE_DIRECT_ADDRESS|KMS_DRIVER|KMS_CONNECTOR_ID|ENABLE_HDMI_AUDIO|NDI_MAX_QUEUE)=' /etc/ndi-pi.conf 2>/dev/null || true
|
|
echo
|
|
echo "--- NDI discovery ---"
|
|
if command -v gst-device-monitor-1.0 >/dev/null 2>&1; then
|
|
NDI_RUNTIME_DIR_V6="${NDI_RUNTIME_DIR_V6:-/usr/local/lib}" \
|
|
gst-device-monitor-1.0 -f Source/Network:application/x-ndi 2>&1 | head -120 || true
|
|
else
|
|
echo "gst-device-monitor-1.0 nicht verfügbar"
|
|
fi
|
|
echo
|
|
echo "--- NDI mDNS services ---"
|
|
avahi-browse -rtp _ndi._tcp 2>&1 | head -80 || true
|
|
echo
|
|
echo "--- Services ---"
|
|
systemctl --no-pager --full status ndi-capture.service ndi-display.service || true
|
|
echo
|
|
echo "--- ndi-capture.service journal (letzte 80 Zeilen) ---"
|
|
journalctl -u ndi-capture.service -n 80 --no-pager || true
|
|
echo
|
|
echo "--- ndi-display.service journal (letzte 80 Zeilen) ---"
|
|
journalctl -u ndi-display.service -n 80 --no-pager || true
|
|
EOF
|
|
chmod 0755 "$INSTALL_DIR/bin/ndi-pi-diagnose"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# systemd
|
|
# ---------------------------------------------------------------------------
|
|
cat > /etc/systemd/system/ndi-capture.service <<'EOF'
|
|
[Unit]
|
|
Description=NDI Pi Gateway - USB Capture + Audio to NDI
|
|
After=network-online.target sound.target
|
|
Wants=network-online.target sound.target
|
|
ConditionPathExists=/etc/ndi-pi.conf
|
|
|
|
[Service]
|
|
Type=simple
|
|
Environment=NDI_RUNTIME_DIR_V6=/usr/local/lib
|
|
ExecStart=/opt/ndi-pi/bin/ndi-capture.sh
|
|
Restart=always
|
|
RestartSec=1
|
|
Nice=-5
|
|
LimitNOFILE=65536
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
cat > /etc/systemd/system/ndi-display.service <<'EOF'
|
|
[Unit]
|
|
Description=NDI Pi Gateway - NDI Video + Audio to HDMI/DRM
|
|
After=network-online.target sound.target
|
|
Wants=network-online.target sound.target
|
|
ConditionPathExists=/etc/ndi-pi.conf
|
|
|
|
[Service]
|
|
Type=simple
|
|
Environment=NDI_RUNTIME_DIR_V6=/usr/local/lib
|
|
ExecStart=/opt/ndi-pi/bin/ndi-display.sh
|
|
Restart=always
|
|
RestartSec=1
|
|
Nice=-5
|
|
LimitNOFILE=65536
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable ndi-capture.service ndi-display.service
|
|
|
|
log "Prüfe NDI-GStreamer-Plugin..."
|
|
NDI_RUNTIME_DIR_V6="${NDI_RUNTIME_DIR_V6:-/usr/local/lib}" \
|
|
gst-inspect-1.0 ndisink >/dev/null
|
|
NDI_RUNTIME_DIR_V6="${NDI_RUNTIME_DIR_V6:-/usr/local/lib}" \
|
|
gst-inspect-1.0 ndisrc >/dev/null
|
|
|
|
log "NDI Runtime:"
|
|
ldconfig -p | grep -E 'libndi\.so\.6(\.|$)' || true
|
|
ls -l "${NDI_RUNTIME_DIR_V6:-/usr/local/lib}"/libndi.so* 2>/dev/null || true
|
|
|
|
systemctl restart ndi-capture.service
|
|
systemctl restart ndi-display.service
|
|
|
|
echo
|
|
log "Installation abgeschlossen."
|
|
echo
|
|
echo "Konfiguration: $CONF"
|
|
echo " sudo nano $CONF"
|
|
echo
|
|
echo "NDI Display:"
|
|
echo " NDI_DISPLAY_SOURCE=${NDI_DISPLAY_SOURCE:-}"
|
|
echo " NDI_DISPLAY_ADDRESS=${NDI_DISPLAY_ADDRESS:-auto}"
|
|
echo " NDI_USE_DIRECT_ADDRESS=${NDI_USE_DIRECT_ADDRESS:-true}"
|
|
echo
|
|
echo "Diagnose:"
|
|
echo " sudo /opt/ndi-pi/bin/ndi-pi-diagnose"
|
|
echo
|
|
echo "Nach Änderungen:"
|
|
echo " sudo systemctl restart ndi-capture ndi-display"
|
|
echo
|
|
echo "Logs:"
|
|
echo " journalctl -u ndi-capture -f"
|
|
echo " journalctl -u ndi-display -f"
|
|
echo
|
|
echo "NDI-Quellen:"
|
|
if command -v gst-device-monitor-1.0 >/dev/null 2>&1; then
|
|
NDI_RUNTIME_DIR_V6="${NDI_RUNTIME_DIR_V6:-/usr/local/lib}" \
|
|
gst-device-monitor-1.0 -f Source/Network:application/x-ndi || true
|
|
else
|
|
log "gst-device-monitor-1.0 nicht verfügbar; NDI-Quellen werden nicht aufgelistet."
|
|
fi
|
|
|