Self-hosted digital signage and narrowcasting that mirrors one playlist across every screen. Runs on any Linux desktop: Raspberry Pi, Debian, or Ubuntu.
FleetSign turns a small Linux box (a Raspberry Pi, or any Debian/Ubuntu mini-PC) and a wall-mounted screen into an unattended, always-on digital signage display that loops your images and videos fullscreen. You manage everything (what plays, in what order, for how long, and when) from a small password-protected web page on the same network. There are no console commands, no accounts, and nothing to babysit: it brings itself back up after a crash, a power cut, or a reboot.
It was built to drive information screens on gym walls, but nothing about it is gym-specific. It suits any narrowcasting setup that needs a few screens looping the same content: lobbies, retail, offices, classrooms, events, menus.
New to FleetSign? Read the User Manual. It is the complete day-to-day guide to the web interface: first-time setup, uploading media, scheduling by day and time, playback and maintenance controls, and running a master/slave fleet.
The master web UI is the entire control surface: upload media, set per-image
durations and weekday/time schedules, reorder the playlist, run playback
controls, and manage the fleet from one password-protected page. (Click to enlarge.)
Playback
- Loops images and videos fullscreen via mpv, in the order you set.
- Per-image display duration, with a global default; videos play to their end.
- Mute videos on/off; selectable hardware-decode mode for awkward GPUs.
- Common formats out of the box. Images: JPG, PNG, GIF, WebP, BMP; video: MP4, MKV, MOV, AVI, WebM, MPG, WMV, FLV, and more.
Scheduling and curation
- Enable / disable individual items without deleting them.
- Time-of-day and weekday schedules per item (e.g. show this only Mon-Fri, 09:00-17:00). Out-of-schedule items drop out of the loop automatically.
- Reorder the playlist with up/down controls; top-to-bottom is play order.
Operations
- A single password-protected web UI, the only control surface. No SSH, no accounts, no config files to edit.
- Upload large videos (up to 4 GiB) straight from the browser.
- Blank / resume the screen and restart playback on demand.
- Maintenance mode (web button or F12 on the Pi) drops mpv out of fullscreen and pauses so you can use the desktop; Resume signage (or F12 again) brings playback cleanly back to fullscreen, and a reboot always returns to fullscreen signage.
- Each screen shows its own
http://<ip>:<port>address small in the bottom-right corner, so you can always find its web UI. - A clock panel warns when the Pi's time looks unset (it has no battery-backed clock), since schedules depend on a correct time.
Multiple screens (master / slave fleet)
- Run one master that hosts the management UI and any number of slaves that mirror its playlist and media over the LAN every ~2 minutes.
- Slaves catch up automatically after downtime; deletions on the master propagate to every screen.
- Add screens by configuring one slave and cloning its SD card, with no per-Pi setup.
- Promote any slave to master (Become master) if the master fails.
- Role is just configuration: there is one codebase and one install. A Pi with a master address set is a slave, otherwise it's a master.
Reliability
- Two layers of self-healing:
systemdrestarts the service if the process dies, and the service relaunches mpv if mpv dies. - Atomic writes for all state, and a corrupt playlist is backed up and reset rather than crashing.
- The player tolerates bad data; one malformed entry can never black out the display.
- The signage window stays always-on-top, so a stray terminal or dialog can't slip in front of the display.
A single Python process (started by systemd --user with Restart=always)
hosts three cooperating parts that share one in-process playlist store:
- Web UI (Flask + Waitress): the management surface and, on a master, the sync endpoints that slaves pull from.
- Player: supervises one persistent mpv window over mpv's JSON IPC socket, re-reading the playlist each loop so your edits take effect within the current item, and relaunching mpv if it dies.
- Sync client (slaves only): polls the master, validates the manifest, downloads new/changed media, and mirrors it locally.
State lives in data/manifest.json (playlist + settings) and uploads in
media/. A master's manifest is the source of truth; a slave's is a synced
mirror. The sync channel is treated as untrusted and validated at the boundary.
┌──────────────── Master Pi ────────────────┐
browser ─▶ │ Web UI ──▶ PlaylistStore ──▶ Player ─▶ mpv │ ─▶ screen
│ │ │
└─────────────────┼──────────────────────────┘
│ HTTP pull (~2 min)
┌─────────────────▼──── Slave Pi ───────────┐
│ SyncClient ─▶ PlaylistStore ─▶ Player ─▶ mpv│ ─▶ screen
└────────────────────────────────────────────┘
FleetSign is a small, simple tool. Here is what it does not do:
- One playlist for the whole fleet. Every screen mirrors the master's single playlist, so you can't show different content on different screens, or group them. Enable/disable and per-item schedules apply fleet-wide, not per screen. To run more than one playlist, run a separate master for each (each with its own slaves).
- Fullscreen media only. It plays one image or video at a time, edge to edge. There are no overlays: no text/captions, tickers, logos, layout zones, transitions, web pages, or live data. If you need a designed layout, this isn't it.
- One screen per device. Each host drives a single fullscreen display.
- One shared password, no accounts. A single admin password gates the whole UI; there are no per-user logins, roles, or audit trail.
- Trusted LAN only, no TLS. The web UI and the master↔slave sync run over plain HTTP (the sync is token-authenticated but not encrypted). Keep it on a private network and don't expose it to the internet; put a VPN or reverse proxy in front if you need remote access.
- Manual failover. If the master goes down, slaves keep playing their last sync, but you can't edit anything until you manually promote a slave to master.
FleetSign is a plain Python + mpv program; nothing in it is Pi-specific, so it runs on any modern Linux desktop:
- A Linux desktop session (X11 or Wayland) that auto-logs in, in which mpv can draw a fullscreen window. The reference platform is a Raspberry Pi 4/5 on Raspberry Pi OS (Bookworm) with the desktop, but a Debian or Ubuntu mini-PC works the same way.
- Python 3.11+ (shipped with Bookworm; your distro's
python3elsewhere). - System dependencies: mpv, xdotool, and wmctrl. mpv renders the media; xdotool/wmctrl are used by the foreground guard on XWayland sessions.
- systemd for the bundled service supervision and autostart; the app itself
is just a
python -m fleetsignprocess, so this is optional if you supervise it another way. - Network access on the LAN. A device with no real-time clock (like a Pi) relies on the network to set its time at boot; schedules depend on a correct clock.
Runtime Python dependencies are flask, werkzeug, and waitress. The bundled
installer pulls in the system packages and builds the virtualenv for you on
Debian-family systems (see Quick start).
install.sh is written for Raspberry Pi OS: it installs packages with apt
and wires autostart through the Pi's default labwc compositor.
git clone https://github.com/JasperE84/FleetSign.git ~/fleetsign # the path must be ~/fleetsign
bash ~/fleetsign/install.sh # run as your normal desktop user, NOT with sudoThe installer adds mpv, xdotool, and wmctrl, creates a virtualenv, installs
the service, wires up labwc autostart, and starts it. Then, from any browser on
the same network:
- Open
http://<host-ip>:8080(the address also appears in the screen's bottom-right corner). - On first visit you're sent to a setup page; choose the admin password. That single password is the only credential.
- Upload media and configure playback entirely from the web UI. See the User Manual for a full walkthrough of every screen.
Plain Debian or Ubuntu also use apt, so the installer's package, venv, and
service steps run fine and it starts the service for the current session. But
their default desktop is GNOME, not labwc, so the installer skips the labwc
autostart hook (it writes ~/.config/labwc/autostart only when labwc is present)
and playback won't return on reboot. Add an autostart hook for your actual
desktop instead; see
Managing the service.
Non-apt distros (Fedora, Arch, …) skip the installer entirely: install
mpv, xdotool, wmctrl, and a Python 3.11 venv with your package manager,
pip install -e ~/fleetsign, copy systemd/fleetsign.service into
~/.config/systemd/user/, then add an autostart hook as above.
For the full deployment guide (service management, the master/slave fleet setup, an on-host verification checklist, updating, and troubleshooting) see INSTALL.md.
Install normally on the master and give it a static IP. On each slave, open its web UI once, choose "This screen joins a master," and enter the master's address and the sync token shown on the master's Screens & sync card. Clone that slave's SD card to add more screens with no further setup. Full walkthrough (including failover) is in INSTALL.md → Multiple screens.
A slave runs a reduced UI: it shows what it's mirroring from the master and
the last sync, keeps local playback/maintenance controls and a per-Pi video
decoder, and can be promoted to master if the master fails. (Click to enlarge.)
FleetSign runs as a systemd --user service named fleetsign. Day-to-day you
shouldn't need any of this (playback is controlled from the web UI and the
service self-heals), but for the admin on the host:
systemctl --user start fleetsign # start it now
systemctl --user restart fleetsign # restart the daemon (e.g. after an update)
systemctl --user stop fleetsign # stop it
systemctl --user status fleetsign # is it running?
journalctl --user -u fleetsign -f # live logs (errors, mpv relaunches)Logs go to the journal at INFO by default (startup, role, mpv relaunches,
uploads, logins, sync results, warnings). Filter to problems with
journalctl --user -u fleetsign -p warning. For verbose troubleshooting set
FLEETSIGN_LOG_LEVEL=debug (via systemctl --user edit fleetsign, then restart);
see Log verbosity in INSTALL.md.
systemd supervises the process and restarts it if it dies (Restart=always),
but it does not start it at boot; that's the autostart hook below.
On Raspberry Pi OS the desktop session's graphical-session.target isn't
reliably reached for --user units, so systemctl --user enable fleetsign
would not launch it on login. Instead, install.sh appends a block to the
labwc compositor's autostart file, ~/.config/labwc/autostart, which labwc
runs at session start with the Wayland environment available:
# ~/.config/labwc/autostart
systemctl --user import-environment WAYLAND_DISPLAY XDG_RUNTIME_DIR DISPLAY 2>/dev/null
systemctl --user start fleetsign.serviceThe first line hands the running display's environment to the systemd user
manager (so mpv can find the screen); DISPLAY is included because mpv renders
through XWayland to stay always-on-top and so the foreground guard can use
xdotool/wmctrl. The second starts the supervised unit.
Autostart therefore depends on this file: remove the lines and the player
won't come back after a reboot, even though systemctl --user start fleetsign
still works by hand.
# Disable autostart (leave the service installed, just don't launch on boot):
sed -i '/# Start the FleetSign player/,+2d' ~/.config/labwc/autostart
# Re-enable autostart: re-run the installer (it re-adds the block idempotently)
bash ~/fleetsign/install.shThe labwc file is specific to Raspberry Pi OS Bookworm's default (Wayland)
session. On a different desktop, add the equivalent lines to its autostart
and FleetSign starts the same way. The key step is always: import the session's
display variables into the systemd user manager, then start the unit. On a Wayland
session import both WAYLAND_DISPLAY and DISPLAY (mpv runs under XWayland
for always-on-top and focus guarding); on X11, DISPLAY.
| Session | Autostart location | Lines to add |
|---|---|---|
| labwc (Pi OS, Wayland, default) | ~/.config/labwc/autostart |
systemctl --user import-environment WAYLAND_DISPLAY XDG_RUNTIME_DIR DISPLAYsystemctl --user start fleetsign.service |
| X11 openbox / LXDE (older Pi OS) | ~/.config/openbox/autostart or ~/.config/lxsession/LXDE-pi/autostart |
systemctl --user import-environment DISPLAY XDG_RUNTIME_DIRsystemctl --user start fleetsign.service |
| Any XDG-compliant desktop | ~/.config/autostart/fleetsign.desktop |
Exec=sh -c 'systemctl --user import-environment WAYLAND_DISPLAY XDG_RUNTIME_DIR DISPLAY; systemctl --user start fleetsign.service' |
The service unit itself is portable across desktops; only the autostart hook differs.
No Pi or mpv is needed to run the tests; the player's mpv and socket interactions are dependency-injected, so the suite runs on any platform (including Windows/CI).
python -m pytest # full suite (~208 tests)
python -m pytest tests/test_store.py -v # one file
python -m fleetsign --root . --port 8080 # run the daemon locally (needs mpv for real playback)| Path | What |
|---|---|
fleetsign/store.py |
PlaylistStore, the source of truth (atomic, thread-safe) |
fleetsign/player.py |
PlayerController, supervises the mpv window over JSON IPC |
fleetsign/web.py |
Flask apps: master management UI, slave UI, and sync endpoints |
fleetsign/sync.py |
SyncClient, the slave-side mirror; manifest_payload for the master |
fleetsign/schedule.py |
weekday + time-window activeness check |
fleetsign/model.py |
data model (MediaItem, Schedule, Settings) |
fleetsign/config.py |
per-host config + auth; role (master_url/sync_token) |
fleetsign/mpv_ipc.py |
thin mpv JSON-IPC client |
fleetsign/__main__.py |
entry point, picks the master or slave app by role |
tests/ |
the test suite |
install.sh / systemd/ |
one-time deployment (Raspberry Pi OS; apt + venv steps also fit Debian/Ubuntu) |
Released into the public domain under The Unlicense; see LICENSE. This is about as permissive as licensing gets: copy, modify, publish, use, compile, sell, or distribute it for any purpose, with no conditions and no attribution required.
Every dependency permits this: Flask, Werkzeug, and Jinja2 are BSD-3-Clause, Waitress is ZPL-2.1, pytest is MIT, all permissive and none copyleft. mpv (GPL) runs as a separate process over IPC, so its license does not propagate to this code.