display-pi setup guide — fresh Pi to Sunday-morning ready

End-to-end procedure for turning a blank Raspberry Pi into a working RTMP-driven kiosk display. Written for a single-Pi deployment receiving an RTMP push from an ATEM Mini Pro; adapt the obvious bits if your upstream is different.

If you already have a working Pi and just want to push code changes, skip to Day-to-day operations.

What you need

Hardware

Item Pi 4 (recommended) Pi 5 (also supported, see caveats)
Board Pi 4 Model B, 2 GB is enough (4/8 GB for headroom) Pi 5, 4 GB or 8 GB
Power supply Official 5 V / 3 A USB-C Official 5 V / 5 A USB-C-PD
Cooling Passive heatsink fine Active cooler strongly recommended
Storage 32 GB+ SD card (Samsung Pro / SanDisk Extreme — avoid no-name)  
HDMI Micro-HDMI to HDMI cable, port 0 Same
Network Ethernet preferred — RTMP video on Wi-Fi is unreliable  
Capture (optional) USB3 HDMI capture (e.g. MACROSILICON 345f:2131) on the workstation for visual debugging  

On RAM: a 2 GB Pi 4 is fine for the relay-only kiosk role. nginx relays the RTMP push (no transcode) and mpv decodes in hardware (--hwdec=v4l2m2m-copy), so the whole stack sits well under 1 GB. The real reliability levers are cooling, SD-card quality, and Ethernet — not memory. Step up to 4/8 GB only if you add a second job to the same Pi (recording, a browser overlay, a MediaMTX relay).

HDMI 0 is the micro-HDMI port closer to the USB-C power input. The script and player are pinned to vc4hdmi0 (HDMI 0); use HDMI 1 only if you change the wiring documented in docs/dev-journal/2026-04-25-hdmi-audio-routing.md.

Software you’ll be installing

Network plan

Have an answer for these before you start:

Installation procedure

Fast path: once steps 1–4 are done (flash, first boot, SSH config, repo checkout on the workstation), a single command takes the Pi the rest of the way — base kiosk, full deploy, volunteer web manager, and shortcut files:

make provision HOST=displaypi STREAM_KEY=restoration

It runs setupdeploysetup-webvolunteer-web-url in that order (the order matters — see make help), and every step is idempotent so it’s safe to re-run. Steps 5–8 below do the same work by hand and explain each piece; read them to understand what provision automates, or to run a step on its own.

1. Flash the SD card

Use the Raspberry Pi Imager.

2. First boot

3. SSH config on the workstation

Add an alias so all the project’s tooling (make deploy, make logs, etc.) just works:

# ~/.ssh/config
Host displaypi
    HostName 192.168.0.106    # or displaypi.local, or whatever
    User rpi
    IdentityFile ~/.ssh/id_ed25519

Smoke test:

ssh displaypi true && echo OK

If you didn’t pre-load your SSH key during imaging:

ssh-copy-id displaypi

4. Clone the repo onto the workstation

git clone git@github.com:dmcbane/display-pi.git
cd display-pi

5. Run setup-kiosk.sh on the Pi

The Pi needs a copy of the repo to run setup-kiosk.sh. Ship it over:

# From the workstation (in the display-pi/ checkout):
rsync -avz --exclude='.git/' . displaypi:~/display-pi/
ssh displaypi
cd ~/display-pi

Edit configuration if you need to (the defaults in install/setup-kiosk.sh are sane for a 192.168.x.x LAN with a restoration stream key — read the top of the file and adjust if not):

$EDITOR install/setup-kiosk.sh

Run it:

bash install/setup-kiosk.sh

You’ll be prompted for your sudo password once. The script is idempotent — safe to re-run. It does, in order:

  1. Installs packages (cage, mpv, nginx-rtmp, pipewire, watchdog, …)
  2. Creates the kiosk user
  3. Enables seatd
  4. Configures nginx with the RTMP module
  5. Updates /boot/firmware/config.txt and cmdline.txt (watchdog, vc4.force_hotplug=1, consoleblank=0) 5b. If STATIC_IP is set, binds an extra static IP on Ethernet alongside DHCP (see Optional: a static fallback IP). Skipped when STATIC_IP is empty.
  6. Installs the splash image at /home/kiosk/splash.png. If the repo ships images/splash.png, that file is used as-is. If it’s absent but other images are present in images/, setup prompts you to pick one (on a non-interactive run it skips the prompt). With no usable image, it generates a placeholder from $SPLASH_TEXT.
  7. Installs a minimal bootstrap player script (overwritten by the full one when you make deploy)
  8. Installs the systemd user service
  9. Configures the hardware watchdog
  10. Configures PipeWire client.conf 10b. Installs the deploy sudoers whitelist (/etc/sudoers.d/kiosk-deploy) 10c. Generates the system locale (DISPLAY_LOCALE, default en_US.UTF-8), makes it the default, and strips LANG/LC_* from sshd’s AcceptEnv so an SSH login never shows the cannot change locale warning — no matter what the client forwards (see Optional: change the system locale).
  11. Installs logrotate config
  12. Installs the healthcheck cron stub

When it finishes, reboot:

sudo reboot

After ~30 s the Pi will come back into kiosk mode: a brief boot diagnostics screen, then the splash image. From here on, you do not need to log into the Pi for routine work.

Optional: a static fallback IP

On a normal LAN the Pi gets its address from DHCP and you never think about this. But sometimes there’s no DHCP server — you’ve patched a laptop straight into the Pi with one cable, plugged it into a dumb switch, or set it up in the field. Set STATIC_IP and the Pi binds a fixed IPv4 address in addition to its DHCP lease, so it’s reachable at a known address in both situations:

# During setup (or provision), from the workstation:
make setup STATIC_IP=192.168.50.1/24 HOST=displaypi

Pick a subnet outside your normal LAN range (e.g. 192.168.50.0/24) so the static address never collides with a real DHCP lease. NetworkManager keeps requesting DHCP as usual, so this is purely additive — it does not change how the Pi behaves on your regular network.

To reach the Pi over the fallback address, give the connecting machine another address in the same subnet and SSH across:

# On the laptop, once cabled to the Pi (macOS/Linux example):
sudo ip addr add 192.168.50.2/24 dev eth0
ssh rpi@192.168.50.1

Notes:

Optional: change the system locale

By default the Pi is set to en_US.UTF-8, and — importantly — sshd is told to ignore the locale your SSH client forwards. That combination means every login is clean and identical no matter which machine you connect from: no more

-bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)

You don’t have to do anything to get this; it’s part of the base setup. If your region isn’t US English, set DISPLAY_LOCALE so the Pi’s default matches:

make setup DISPLAY_LOCALE=en_GB.UTF-8 HOST=displaypi

Why the warning happened in the first place: a fresh Raspberry Pi OS image has almost no locales generated, but most SSH clients forward LANG/LC_* from the desktop they run on. When the Pi doesn’t have that locale, every program that reads it complains. Setup fixes both halves — it generates the default locale and stops sshd from importing the forwarded values — so the result no longer depends on the client at all.

6. First deploy from the workstation

Back on your workstation, in the display-pi/ checkout:

make deploy

Should finish without prompting for any password (the sudoers whitelist from step 10b is already in place). This pushes the full repo to the Pi, symlinks the real player.sh, restarts the kiosk service, and reloads nginx.

7. Configure your RTMP source

The ATEM Mini Pro is what we use. In ATEM Software Control:

  1. Settings → Output → Stream Service. Add a custom destination using Blackmagic’s Streaming.xml generator or by editing the Streaming.xml file directly.
  2. Server URL: rtmp://<pi-ip>/live (e.g. rtmp://192.168.0.106/live)
  3. Stream key: restoration (or whatever you set in install/setup-kiosk.sh).
  4. Press the LIVE button on the ATEM. The Pi should switch from the splash to your live feed within a few seconds.

For OBS Studio: same URL/key, plug into the Stream settings under Custom.

For ad-hoc testing without the ATEM, see step 8.

8. Verify with a test stream

From the workstation:

make test-stream    # 60 s of 1080p test pattern + 440 Hz tone

The Pi should switch from splash to the test pattern within a few seconds, you should hear a 440 Hz tone, and make test-stream-long gives you 5 minutes if you need more time at the receiver.

If you can’t hear audio: docs/dev-journal/2026-04-25-hdmi-audio-routing.md walks through the diagnosis.

Day-to-day operations

All from the workstation, in the display-pi/ checkout:

Command What it does
make deploy Push repo + restart kiosk service. No password prompts.
make test-stream 60 s test pattern with audio.
make test-stream-long 5 min version, for in-place AV testing.
make ssh Interactive shell on the Pi.
make logs Tail kiosk + nginx logs.
make status Show kiosk service status.
make diag Run diagnostics on the Pi (text output).
make ssh-password Toggle SSH password login: STATE=on (key OR password), STATE=off (key-only), STATE=status (default, just report).
make ping 3 pings to verify the Pi is reachable.
make reboot Reboot the Pi.
make shutdown Power off the Pi (sudo poweroff).
make sudoers One-time: install the deploy sudoers whitelist (only needed if you skip step 5 or rebuild the Pi without re-running setup-kiosk.sh).

Splash images

The kiosk cycles through the images in /home/kiosk/splash.d/ whenever the stream is idle, advancing one image each time the splash comes back up (when the stream drops, or the kiosk service restarts). There is no timer — a single continuous idle period shows one image until the splash is re-entered. The cursor is persisted to /home/kiosk/.splash-index, so it keeps moving across restarts instead of snapping back to the first slide (this is what makes make restart step to the next image during testing).

/home/kiosk/splash.d and /home/kiosk/splash.png are symlinks into the deployed repo (/home/kiosk/display-pi/images/…), exactly like the bin/ scripts — there are no separate copies to keep in sync.

The volunteer web manager

make provision (or make setup-web) installs a browser-based manager that lets volunteers swap splash images, restart or reboot the Pi, and watch a live System Status board — all from one bookmarked link, no SSH key required. The link carries an access token you can rotate in one click if it leaks.

The manager is served over HTTPS by default with a certificate signed by a small CA generated on the Pi — no domain needed. Trust that CA once per device for a warning-free padlock:

make web-ca HOST=displaypi   # saves display-pi-rootCA.crt — import it as a trusted root

If you control a domain, make setup-web-tls DOMAIN=… swaps in a publicly-trusted Let’s Encrypt cert instead (no per-device import). Full walkthrough: Web Manager — Splash, Status, HTTPS & Tokens.

SSH password login

setup-kiosk.sh configures the Pi to accept SSH login by public key OR password. The setting lives in a single drop-in, /etc/ssh/sshd_config.d/00-display-pi-auth.conf, whose 00- prefix makes it sort first and win sshd’s first-value-wins resolution over any later drop-in (including the key-only file rpi-imager writes) and the stock config.

Flip it without hand-editing config:

# From the workstation:
make ssh-password STATE=off      # key-only (hardened)
make ssh-password STATE=on       # allow public key OR password again
make ssh-password                # STATE=status — show the effective setting

Public-key auth is always kept enabled, so STATE=off can never lock out key-based logins. The toggle validates with sshd -t and applies with a reload (not a restart), so the SSH session you run it from stays up even if the new config were rejected. On the Pi directly: sudo bash install/sshd-password-toggle.sh on|off|status.

To change the stream key, RTMP allow-list, or any other config: re-run bash install/setup-kiosk.sh on the Pi after editing the config block at the top. The script is idempotent and backs up everything it touches.

Pi 4 vs Pi 5 — known differences

setup-kiosk.sh was developed against a Pi 4 and is the supported reference platform. The Pi 5 should work, with these caveats:

If you’re deploying a Pi 5 and hit something not covered here, please add a note to docs/dev-journal/.

Troubleshooting

“No HDMI signal” on first boot

Stream is live but no audio on HDMI

This was the original sin that started the journal. See docs/dev-journal/2026-04-25-hdmi-audio-routing.md. Quick check:

ssh displaypi 'sudo grep audio-device /home/kiosk/bin/player.sh'
# Should show: --audio-device=alsa/plughw:CARD=vc4hdmi0,DEV=0

Stream pushes from the ATEM but kiosk doesn’t switch

Kiosk service keeps crashing

make status        # see Restart=always firing
make ssh
journalctl --user -u kiosk.service --since '5 min ago'
sudo tail -100 /tmp/player.log

The most common causes are nginx not running (publisher path broken) or the splash image missing/corrupted. The boot assessment screen will show which check failed — wait for it to display on HDMI before the splash takes over.

make deploy asks for a password

The deploy sudoers whitelist isn’t installed (or got deleted). Run:

make sudoers

This is the same step setup-kiosk.sh does as part of step 10b.

Pi gets stuck at the rainbow boot screen

Almost always a cmdline.txt corruption — setup-kiosk.sh defends against this with a backup-and-restore-on-error path, but if something else edited the file:

  1. Power off, pull the SD card.
  2. On your workstation, mount the boot partition, fix cmdline.txt (must be exactly one non-empty line).
  3. Re-insert and boot.

The script keeps timestamped backups at /boot/firmware/cmdline.txt.bak-* so you can always restore.

Where to look next