English | 日本語
moip-toolkit moves the video transport of the DJ event "The Utopia Tone" onto an IP network using NDI. It receives NDI sources and shows them fullscreen with minimal latency, sends capture devices as NDI sources, shows several sources in a multiview grid, and displays still images as digital signage. Everything can be controlled from a browser UI or a REST API.
flowchart TD
browser["Browser<br/>Nuxt / Vue web UI"]
server["<b>moip web</b><br/>Go server, server/"]
engine["<b>moip-engine</b><br/>Rust media engine, engine/<br/>rx · tx · multiview · signage · preview · discover · devices"]
ndi["libndi.so.6<br/>NDI SDK runtime<br/>installed separately, not in this repository"]
display["Display<br/>KMS/DRM or desktop via SDL2 / OpenGL"]
capture["Capture devices<br/>V4L2 video, ALSA audio"]
lan["NDI sources and receivers on the LAN"]
browser -- "HTTP · Server-Sent Events · MJPEG · WebRTC" --> server
server -- "JSON lines over stdin/stdout<br/>one process per job" --> engine
engine -- "dlopen at run time" --> ndi
ndi <-- "NDI" --> lan
engine --> display
capture --> engine
- engine/ is written in Rust and does all the media work. It calls the NDI SDK directly, so there is no interpreter lock and no framesync buffering in the receive path: frames are uploaded to the GPU as soon as they arrive, and UYVY is converted to RGB in a shader. A stalled receiver is replaced at once and the old one is torn down on a background thread.
- server/ is written in Go. It serves the web UI and API, starts and stops engine processes, and forwards browser previews without transcoding them: H.264 over WebRTC via pion, with MJPEG as a fallback.
- frontend/ is the browser UI, built with Nuxt 3 and Vue 3.
- Linux on x86_64 or arm64
- The NDI SDK runtime
libndi.so.6, see Install the NDI SDK avahi-daemon, so that NDI sources can be discovered over mDNS- To build: stable Rust installed with rustup, Go 1.24 or newer, a C/C++ compiler, and the SDL2 and ALSA development headers
- To build the web UI: Node.js 20 or newer
# Debian / Ubuntu
$ sudo apt install git curl build-essential pkg-config libsdl2-dev libasound2-dev avahi-daemon
# Fedora
$ sudo dnf install git curl gcc gcc-c++ pkgconf SDL2-devel alsa-lib-devel avahiThe NDI SDK is licensed by Vizrt NDI AB and must not be committed to this repository. Each machine needs its own copy of the runtime library. moip-toolkit does not need the SDK to build: it loads libndi.so.6 when it starts.
Install it with the bundled script, which is based on DistroAV's libndi-get.sh:
$ scripts/install-ndi-sdk.shThe script:
- downloads the official installer from
https://downloads.ndi.tv/SDK/NDI_SDK_Linux/Install_NDI_SDK_v6_Linux.tar.gz; - shows the NDI SDK license and asks you to accept it;
- copies
libndi.so.6*for your CPU to/usr/local/libwithsudo. On arm64 the library is taken fromlib/aarch64-*. If the dynamic linker does not search that directory yet, as on Fedora and RHEL, the script registers it, then runsldconfig.
Options, set as environment variables:
| Variable | Description |
|---|---|
PREFIX=/opt/ndi |
Install to $PREFIX/lib instead of /usr/local/lib |
NDI_ACCEPT_LICENSE=yes |
Accept the license without the prompt. Only use this after you have read and accepted it, for example when setting up more machines |
After building, check that the runtime is found:
$ ./bin/moip version
NDI SDK LINUX ... 6.x.xIf you would rather not install system-wide, point NDI_RUNTIME_DIR_V6 at the directory that contains libndi.so.6.
$ git clone https://github.com/TechnoTUT/moip-toolkit.git
$ cd moip-toolkit
$ make # builds bin/moip, the server and CLI, and bin/moip-engine
$ make frontend # builds the web UI into frontend/.output/publicmake test runs the Rust and Go tests.
RX, multiview and signage render straight to the display through KMS/DRM by default. Bypassing the desktop compositor saves one to two frames of latency. This needs:
- no desktop session running on that display.
sudo systemctl set-default multi-user.targetmakes the machine boot to a console instead of a GUI; - the user in the
videogroup for the display and theinputgroup for the Esc key:sudo usermod -aG video,input $USER.
Output is always fullscreen, and WAYLAND_DISPLAY / DISPLAY are not needed.
If KMS/DRM is not available because a desktop session owns the display, moip-toolkit prints a warning and opens a window on the desktop instead. To use the desktop from the start, pass --video-driver auto, wayland or x11.
Tip
Showing windows on a desktop when started over SSH A process started over SSH needs the desktop session's environment variables.
- Check the session type:
$ loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type - For
Type=wayland:$ WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) ./bin/moip web --video-driver auto - For
Type=x11:$ DISPLAY=:0 XAUTHORITY=$HOME/.Xauthority ./bin/moip web --video-driver auto
The moip command is the single entry point. moip web starts the server. The other subcommands run the media engine directly, and each one accepts -h to list its options.
$ ./bin/moip web --host 0.0.0.0 --port 8000- Open
http://<host>:8000/in a browser. - The UI lets you discover sources, start and switch RX, start and stop TX, run multiview and signage, preview sources in the browser, and set presets with auto-start.
- Settings are stored in
~/.config/utone-ndi/settings.json. Signage images are stored indata/signage/; change the location with--data.
Useful flags:
| Flag | Description |
|---|---|
--video-driver kmsdrm |
Display driver for RX, multiview and signage started from the UI: kmsdrm by default, auto, wayland or x11 |
--extra-ips 10.0.0.5,10.0.0.6 |
Query these hosts for sources directly, for networks without mDNS |
--max-webrtc 4 |
Maximum number of concurrent WebRTC previews |
--static DIR / --engine PATH |
Override where the UI build and moip-engine are found |
$ ./bin/moip rx -s "<NDI source name>"Give the source as its full name, HOST (STREAM), or as the stream name only. If you leave out -s, the network is searched and you pick a source from a list. Press Esc to quit.
Other options:
| Option | Description |
|---|---|
--recv-fmt uyvy |
Receive format: uyvy, the default and fastest because it is converted on the GPU, rgb or bgr |
--recv-bandwidth highest |
highest or lowest |
--banner-seconds 30 |
How long the IP address banner is shown |
--fullscreen |
Fullscreen window on a desktop. KMS/DRM output is always fullscreen |
$ ./bin/moip devices # list cameras, capture cards and audio inputs
$ ./bin/moip tx -n TX -d 0 --width 1920 --height 1080 --fps 30 -a "plughw:CARD=PCH,DEV=0"
$ ./bin/moip tx -n BARS -d -1 --fps 59.94 --no-audio # colour bars with a clock-dtakes a V4L2 device index for/dev/videoN, a device path, or-1for the built-in test pattern.--input-format auto|yuyv|uyvy|mjpgpicks the capture format.autouses uncompressed video when the device can deliver the requested size and frame rate, and MJPEG otherwise.- Video is always sent as UYVY, NDI's native format. Audio comes from ALSA;
-a defaultuses the system default input.
$ ./bin/moip multiview # every source found, re-arranged as sources come and go
$ ./bin/moip multiview -s CAM1 -s CAM2 # only these sourcesEach tile shows the source name and L/R audio meters. With more than two tiles, the low-bandwidth NDI proxy streams are used to save CPU; --recv-bandwidth highest overrides this.
Upload images in the web UI and show them fullscreen on the connected display, or show an image from the command line:
$ ./bin/moip signage /path/to/image.png
$ ./bin/moip signage /path/to/image.png --framebuffer # Linux framebuffer, no GPU needed--fb-device selects the framebuffer device, /dev/fb0 by default. Writing to the framebuffer requires membership of the video group.
Example user units are in systemd-example/: moip-rx.service, moip-tx.service and moip-web.service. Edit ExecStart to match your setup.
$ mkdir -p ~/.config/systemd/user
$ cp systemd-example/moip-rx.service ~/.config/systemd/user/
$ vim ~/.config/systemd/user/moip-rx.service
$ systemctl --user daemon-reload
$ systemctl --user enable --now moip-rx.serviceTo start the units at boot without logging in, enable lingering:
$ sudo loginctl enable-linger $USERWith moip-web.service, you can instead turn on auto-start for RX / TX in the web UI's settings.
All endpoints are under /api. Errors are returned as {"detail": "..."}.
| Method | Path | Description |
|---|---|---|
| GET | /api/events |
Server-Sent Events carrying the full state: sources, rx, tx, multiview, signage, system and settings. Sent when it changes |
| GET | /api/ndi/sources |
Sources found on the network |
| GET | /api/ndi/preview?source=&fps=&width= |
MJPEG preview |
| POST | /api/webrtc/offer |
WebRTC preview: send {source, sdp, type}, receive an SDP answer with an H.264 track |
| GET | /api/devices/video, /api/devices/audio |
Capture devices |
| GET/POST | /api/rx/status, /api/rx/start, /api/rx/switch, /api/rx/stop |
Receiver |
| GET/POST | /api/tx/status, /api/tx/start, /api/tx/stop |
Sender |
| GET/POST | /api/multiview/status, /api/multiview/start, /api/multiview/stop |
Multiview |
| GET/POST/DELETE | /api/signage/images, /api/signage/upload, /api/signage/image/{filename} |
Signage images |
| GET/POST | /api/signage/status, /api/signage/start, /api/signage/stop |
Signage display |
| GET/POST | /api/system/status, /api/settings |
System load, presets and auto-start |
RX, multiview and signage share the display, so starting one of them stops the others.
moip-toolkit/
├── engine/ # Rust workspace
│ ├── ndi/ # NDI SDK bindings, loaded at run time
│ └── moip-engine/src/
│ ├── rx.rs # fullscreen viewer
│ ├── recv.rs # capture thread, stall recovery, audio meter
│ ├── gfx.rs # SDL2/OpenGL window, UYVY shader, labels
│ ├── tx.rs # sender: V4L2 or test pattern, plus ALSA
│ ├── multiview.rs # grid viewer
│ ├── signage.rs # image display on GL or the framebuffer
│ ├── preview.rs # H.264 / JPEG preview for the browser
│ └── control.rs # stdin/stdout JSON protocol with the server
├── server/ # Go module
│ ├── cmd/moip/ # moip command: web server and engine launcher
│ └── internal/ # api, jobs, preview, sources, settings, sysmon, engine
├── frontend/ # Nuxt 3 / Vue 3 web UI
├── scripts/ # install-ndi-sdk.sh
├── data/signage/ # uploaded signage images
├── systemd-example/ # example systemd user units
└── Makefile
moip-toolkit is released under the MIT License. The NDI SDK and runtime are subject to the NDI SDK license and are not part of this repository. NDI® is a registered trademark of Vizrt NDI AB.