Synchronised video playback for tiny theatres, on a budget.
Got a bunch of old PCs or Raspberry Pis lying around? Want to play video across multiple projectors/screens in sync, with crossfades, from one operator workstation? That's what this does.
Controller (your laptop)
│
├── Client 1 → projector 1
├── Client 2 → projector 2
└── Client 3 → projector 3 ... etc
No internet, no accounts, no cloud. Just your LAN and a switch.
- GO a cue — all clients start playing in sync (~50–150ms drift).
- Crossfade between videos (two layers, fade the alpha between them).
- Fade to/from solid-colour screens for blackouts/transitions.
- Preflight check — the controller checks every client has the right media files before show time.
- Survives network drops — a client that loses the network keeps playing the current cue until reconnection.
One Controller machine (your laptop — runs the show UI) and as many Client machines as you like (each with a screen/projector).
Supported platforms:
| Machine | Works as |
|---|---|
| Windows x86_64 | Controller + Client |
| macOS (Intel & Apple Silicon) | Controller + Client |
| Linux x86_64 (any PC) | Controller + Client |
| Linux aarch64 (Pi 4/5) | Controller + Client |
| (pending) Linux armv7 (Pi 3) | Client only (720p30 ceiling) |
-
Install GStreamer on each machine (runtime dependency — see below).
-
Run the controller using the bundled run-controller.sh script (or the downloaded binary on linux)
-
Create a show file using the controller GUI or manually following the example in this repo.
-
Run each client: using the bundled run-client.sh script (or the downloaded binary on linux)
-
Copy your media to each client into a folder of your choice (or use the push-media feature).
Clients auto-discover the controller on the LAN, so you should be able to just play the first cue.
MultiPlex uses GStreamer for all media playback. It's not statically linked, so you need the runtime:
-
Linux (Debian/Ubuntu/Pi OS):
sudo apt install libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 \ libgstreamer-plugins-bad1.0-0 gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly
-
Windows / macOS: Download the official GStreamer runtime from gstreamer.freedesktop.org (or use the bundled installer if available).
Cues are defined in a TOML file. Here's the gist:
[[cues]]
name = "Opening"
type = "video"
file = "opening.mp4"
fade_in_ms = 500 # fade up from black over 500ms — also the crossfade duration into this cue
[[cues]]
name = "Fade to Black"
type = "color"
color = "#000000"
fade_in_ms = 1500See examples/example_show.multiplex.toml for a real example.
git clone https://github.com/davidroberthoare/multiplex
cd multiplex
cargo build --releaseBinaries end up in target/release/:
multiplex-controllermultiplex-client
You don't need a pile of Pis to try MultiPlex out — you can run the controller and several clients as separate processes on one machine.
Each client persists a stable id (client_id) to a config file so cue
targeting survives restarts — but that means every client on the same
machine reads the same file by default and ends up sharing one id. Give
each instance its own identity file with MULTIPLEX_IDENTITY_PATH:
# Terminal 1: controller
cargo run --bin multiplex-controller
# Terminal 2: client A
MULTIPLEX_IDENTITY_PATH=/tmp/mp-client-a.toml \
MULTIPLEX_NAME=client-a \
MULTIPLEX_CANVAS=640x360@30 \
cargo run --bin multiplex-client
# Terminal 3: client B
MULTIPLEX_IDENTITY_PATH=/tmp/mp-client-b.toml \
MULTIPLEX_NAME=client-b \
MULTIPLEX_CANVAS=640x360@30 \
cargo run --bin multiplex-clientNotes:
MULTIPLEX_MEDIA_ROOTcan stay the default/shared across instances — preflight pushes every show file to every client regardless of which cues target it, so there's no need for separate media folders just to test.MULTIPLEX_CANVASkeeps the client windows small enough to tile on one screen. They'll likely spawn stacked on top of each other — there's no window-position env var, so just drag one aside.- To try per-client cue targeting: open the controller's show editor, set a
cue's Targets column to Whitelist and check only
client-a, Apply, then GO — only the client-a window should play it. The If excluded column controls what the other client does instead (Ignore/Poster/Color). - To try renaming: click the pencil icon next to a client in the roster,
rename it, then restart that client (same
MULTIPLEX_IDENTITY_PATH) and confirm the name sticks even without settingMULTIPLEX_NAME.
Updates are operator-triggered from the controller — nothing updates itself behind your back, and clients never need internet:
- Update controller (toolbar): when the controller machine has internet,
it downloads the latest signed release, verifies it, and asks you to
confirm the restart. It also caches the client binaries for every platform
into an
updates/folder next to the controller, so step 2 works with no internet at all. (No internet ever? Copy anupdates/bundle in by hand.) - Update fleet (toolbar, or per-client in the roster): sends the right binary to each out-of-date client over the LAN. Clients verify the cryptographic signature and stage the update — playback is never touched. You then click Apply (per client or fleet-wide) when nothing is on stage; a client that is playing politely refuses.
The updater only swaps the MultiPlex binaries. On Windows/macOS, a release that needs a newer bundled GStreamer runtime will tell you to reinstall instead.
Recommended: H.264 video in an MP4 container, 30fps. Hardware decoding works on almost everything. Avoid AV1 — most machines don't have hardware decoders for it and the software decoder is too slow for smooth playback.
MultiPlex is built on the shoulders of many excellent projects. Thank you!
- Rust — the language
- GStreamer — the media engine that does all the actual work (decoding, compositing, crossfading)
- Tokio — async runtime
- tokio-tungstenite — WebSocket transport
- egui / eframe — immediate-mode GUI
- mdns-sd — service discovery (no mDNS daemon needed)
- serde — serialisation (JSON on the wire, TOML on disk)
- sha2 — content hashing for preflight checks
- tracing — structured logging
- anyhow / thiserror — error handling
Very much a work in progress. Things will break, change, and improve. If you try it out, I'd love to hear how it went.
MIT licensed — do whatever you want with it.