Lightweight FastAPI web service and UI to discover NDI sources on your network and route a selected stream to one or more output devices over SSH.
The app discovers NDI sources using cyndilib and, on request, connects to selected devices to launch yuri_simple with the chosen NDI stream.
- NDI discovery: Enumerates live NDI sources using
cyndilib - Web UI: Simple page to pick a source and target devices
- Multi-device routing: Sends the same stream to multiple hosts concurrently
- Stateless config: Output devices are defined in a single JSON file
- REST API: Endpoints for sources, devices, and routing actions
- Python 3.11+
- Platform dependencies suitable for
cyndilib(NDI runtime/SDK as required by your OS) - Passwordless SSH access (public key auth) from the router host to each output device
- On each output device: an executable
run_yuri.shscript available in the default login directory
- Clone and install
git clone https://github.com/yourusername/ndi_router.git
cd ndi_router
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt- Configure SSH access to devices
- Ensure the router host can SSH to each device without a password.
ssh-keygen -t ed25519 -C "ndi_router"
ssh-copy-id user@DEVICE_IP
ssh user@DEVICE_IP "echo ok"- Define output devices in
src/output_devices.json
[
{ "name": "Display-1", "host": "192.168.1.10", "user": "youruser" },
{ "name": "Display-2", "host": "192.168.1.11", "user": "youruser" }
]- Prepare
run_yuri.shon each output device
Place an executable script named run_yuri.sh in the default login directory of the remote user (so that ./run_yuri.sh works). Example content:
#!/usr/bin/env bash
set -euo pipefail
STREAM_NAME="${1:?NDI stream name required}"
pkill -f 'yuri_simple' || true
nohup yuri_simple "ndi_input[stream=${STREAM_NAME}]" "glx_window[fullscreen=True]" \
> /tmp/yuri_simple.log 2>&1 &
echo "launched yuri_simple for ${STREAM_NAME}"chmod +x run_yuri.sh- Run the web app
uvicorn src.main:app --reloadOpen http://127.0.0.1:8000 and use the UI to route a stream.
Environment variables (optional):
HOST(default127.0.0.1)PORT(default8000)
-
GET /api/ndi-sources- Response:
{ "sources": [{ "name": str, "host": str, "stream": str }] }
- Response:
-
GET /api/output-devices- Response:
{ "devices": [{ "name": str, "host": str, "user": str }] }
- Response:
-
POST /api/route- Form fields:
stream_name: string (exact NDI source name as displayed)devices: repeated string values of host addresses (e.g.,192.168.1.10)
- Response:
{ "status": "ok" }on success
- Form fields:
- Discovery is implemented in
ndi_discovery.pyusingcyndilib.Finderand exposed via FastAPI insrc/main.py. - The UI (
src/templates/index.html) fetches sources/devices and posts the routing request. - For each selected device, the server SSHes in via
paramikoand runs./run_yuri.sh "<stream>".
- No NDI sources appear: Ensure the NDI runtime is installed and your machine is on the same network segment. Check host firewall rules.
- SSH errors / prompts for password: Confirm your public key is installed on each device and
sshdallows public key auth. Test withssh user@DEVICE_IP "echo ok". run_yuri.shnot found: Place the script in the remote user's default directory andchmod +x run_yuri.sh. The server invokes./run_yuri.sh.- Display doesn’t update: Confirm
yuri_simpleexists and runs on the target device. Inspect/tmp/yuri_simple.logon the device.
Project layout:
src/main.py: FastAPI app, API endpoints, and simple HTML UI routendi_discovery.py: NDI discovery helpers backed bycyndilibsrc/templates/index.html: Minimal UI for selecting sources and devicessrc/output_devices.json: Device configuration consumed by the API/UIndi_test.py: Example extended discovery including resolution/framerate
Run the discovery helper directly:
python ndi_discovery.pyThis service performs remote command execution over SSH with no built-in authentication or authorization. Run it only on a trusted network or place it behind an authenticated reverse proxy.
MIT License