A lightweight H.264 video player for Linux SBCs, built as a modern replacement for the discontinued omxplayer. Uses the V4L2 M2M hardware decoder, DRM/KMS display, and ALSA audio — zero CPU video decode, zero X11 dependency.
have a nice day ;)
ZeroPlay runs on any Linux device with a V4L2 M2M hardware decoder and DRM/KMS display. Tested on:
| Device | OS |
|---|---|
| Pi Zero W | Raspberry Pi OS Lite 32-bit (Trixie) |
| Pi Zero 2 W | Raspberry Pi OS Lite 32/64-bit (Trixie) |
| Pi Zero 2 W | balenaOS (Bookworm) |
| Pi 3 / 3+ | Raspberry Pi OS Lite 32/64-bit (Trixie) |
| Pi 4 | Raspberry Pi OS Lite 32/64-bit (Trixie) |
| Pi 1 Model B |
Both 32-bit and 64-bit builds are supported. The install script builds from source automatically for the correct architecture.
ZeroPlay decodes H.264 only. Other codecs will fail with an "unsupported codec" error — transcode them to H.264 first.
| Codec | Profiles | Container |
|---|---|---|
| H.264 | Baseline to High, up to level 4.2 | MP4, MKV, MOV, HLS (.m3u8) |
H.264 is hardware decoded via the bcm2835 VPU on Pi Zero W, Pi Zero 2W, and Pi 3, and the V4L2 stateful decoder on Pi 4.
| Codec | Notes |
|---|---|
| AAC / HE-AAC | SBR rate mismatch auto-detected and corrected |
| MP3 | |
| AC3 | |
| FLAC | |
| Opus |
curl -fsSL https://raw.githubusercontent.com/HorseyofCoursey/zeroplay/main/install.sh | sudo bashThis installs dependencies, builds from source, and places the binary at /usr/local/bin/zeroplay.
sudo apt install git gcc make pkgconf \
libavformat-dev libavcodec-dev libavutil-dev libswresample-dev libswscale-dev \
libdrm-dev libasound2-dev libcjson-dev libfreetype-dev
git clone https://github.com/HorseyofCoursey/zeroplay.git
cd zeroplay
make
sudo make installlibfreetype-dev is optional but recommended — without it subtitles use a built-in bitmap font fallback.
To play YouTube URLs directly, install yt-dlp:
sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp \
-o /usr/local/bin/yt-dlp && sudo chmod +x /usr/local/bin/yt-dlpKeep yt-dlp up to date — YouTube changes frequently and old versions stop working.
sudo apt install libwebsockets-dev
make WS=1
sudo make install./cross-build.shUses Docker with buildx to produce a native ARM binary. Copy the result to your Pi with scp.
zeroplay [options] <path> [path2 ...]
Each path can be a video file, image, .txt/.m3u playlist, directory, URL, or YouTube URL. Up to 4 paths may be given — each is assigned to a connected display in DRM enumeration order.
| Flag | Description |
|---|---|
--loop |
Loop playback indefinitely |
--loop-seamless |
Loop a single track indefinitely without a pipeline restart between loops |
--shuffle |
Randomise playlist order |
--recursive |
Load files from folder recursively |
--no-audio |
Disable audio |
--vol n |
Initial volume, 0–200 (default: 100) |
--pos n |
Start position in seconds |
--audio-device dev |
ALSA device override |
--sub path |
External subtitle file (.srt) |
--hls-bitrate bps |
Cap HLS variant bitrate in bps (or HLS_MAX_BANDWIDTH env) |
--yt-quality n |
YouTube stream height: 360, 480, 720, 1080 (default: 480) |
--image-duration n |
Seconds per image (default: 10, 0 = hold forever) |
--spi-fill |
SPI/DBI panel: crop video to fill instead of the default letterboxed fit — see SPI/DBI Panels |
--verbose |
Print decoder and driver info |
--help |
Show usage |
# Play a local file
zeroplay movie.mp4
# Play a YouTube video (requires yt-dlp)
zeroplay "https://www.youtube.com/watch?v=..."
# Play YouTube at 720p (Pi 4/5)
zeroplay --yt-quality 720 "https://www.youtube.com/watch?v=..."
# Play YouTube at 360p (Pi Zero W)
zeroplay --yt-quality 360 "https://www.youtube.com/watch?v=..."
# Play an HLS stream
zeroplay https://example.com/stream.m3u8
# Play separate video and audio streams (advanced)
zeroplay "(https://example.com/video.m3u8)(https://example.com/audio.m3u8)"
# Play with subtitles (auto-detected if .srt has the same name as the video)
zeroplay movie.mp4
# Play with an explicit subtitle file
zeroplay --sub subtitles.srt movie.mp4
# Play all media in a directory
zeroplay /home/pi/media/
# Play all media in a directory and its subdirectories
zeroplay --recursive /home/pi/media/
# Play a playlist file, loop and shuffle
zeroplay --loop --shuffle playlist.txt
# Mix images and videos in a directory, 15 seconds per image
zeroplay --loop --image-duration 15 /home/pi/media/
# Display a static image indefinitely
zeroplay --image-duration 0 photo.jpg
# Loop a directory of media
zeroplay --loop /home/pi/media/
# Shuffle a playlist
zeroplay --loop --shuffle playlist.txt
# Dual display on Pi 4
zeroplay file1.mp4 file2.mp4
# Start at 1h 30min
zeroplay --pos 5400 movie.mp4A plain .txt or .m3u file with one path or URL per line. Lines starting with # are ignored.
# My playlist
/home/pi/media/intro.mp4
/home/pi/media/photo.jpg
https://example.com/stream.m3u8
| Key | Action |
|---|---|
p / Space |
Pause / resume |
| ← / → | Seek −/+ 1 minute |
| ↑ / ↓ | Seek −/+ 5 minutes |
+ / = |
Volume up 10% |
- |
Volume down 10% |
m |
Mute / unmute |
n |
Next playlist item |
b |
Previous playlist item |
i / o |
Previous / next chapter |
q / Esc |
Quit |
ZeroPlay renders subtitles via a DRM overlay plane — composited by the display hardware with no CPU overhead.
- Auto-detection — a
.srtfile with the same name as the video is loaded automatically - Explicit file — use
--sub subtitles.srt - Embedded — MKV files with embedded SRT/subrip subtitle tracks work automatically
- Font — DejaVu Sans Bold 36px when
libfreetype-devis installed; built-in bitmap font otherwise
ZeroPlay detects YouTube URLs automatically and resolves them via yt-dlp:
zeroplay "https://www.youtube.com/watch?v=..."
zeroplay "https://youtu.be/..."
zeroplay "https://www.youtube.com/shorts/..."Use --yt-quality to set the maximum stream height. The default is 480p which works well on Pi Zero 2W. Pi 4/5 users can use 720p or 1080p.
| Device | Recommended quality |
|---|---|
| Pi Zero W | 360p |
| Pi Zero 2 W | 480p |
| Pi 3 | 480p |
| Pi 4 / 5 | 720p or 1080p |
Note: Rapid seeking and pausing during YouTube playback may cause instability due to the dual-stream sync mechanism. Normal playback and occasional seeking works reliably.
ZeroPlay auto-detects the HDMI audio device and routes through the hdmi: ALSA device, which uses the IEC958 plugin chain built into vc4-hdmi. Using plughw: directly bypasses this chain and produces noise.
The hardware's native sample rate is probed at startup to avoid pitch distortion from resampling.
# Override the audio device
zeroplay --audio-device plughw:CARD=Headphones,DEV=0 movie.mp4
# List available devices
aplay -LZeroPlay can drive small SPI TFT panels (ILI9341, ST7789V via panel-mipi-dbi, and
other drm/tiny MIPI DBI controllers) with no HDMI attached — the decoder hands the
ISP an RGB565 frame instead of NV12, and it's presented on the panel's own plane.
Requirements:
- The panel must bind through DRM/KMS, not
fbtft. fbtft gives you/dev/fb1and no DRM node at all, which ZeroPlay can't use. Check withls /dev/dri/— you need acardNentry. If you only get a framebuffer, switch the panel's overlay from anfbtft-style one to adrm/tinyone (a dedicated overlay for your controller, or the genericdtoverlay=mipi-dbi-spifor ST7789 and other panels without a dedicated driver). - A reasonably fast SPI clock. A full-panel refresh at low frame rates needs real bandwidth — 40 MHz worked cleanly on the panels this was tested on; push it as high as your panel and wiring tolerate without flickering.
Two display modes, picked with --spi-fill (default is fit):
| Mode | Behaviour | Requirements |
|---|---|---|
| Fit (default) | Whole picture, letterboxed to the panel's aspect. CPU-scales the decoded frame into a panel-sized buffer. | None — works on any kernel. |
--spi-fill |
Crops to fill the panel edge-to-edge, no CPU scaling (zero-copy: the decoder's own buffer is scanned out directly, cropped by the plane's source rectangle). | Needs the panel's drm/tiny driver to accept a framebuffer larger than the panel and honour a non-zero plane source offset — merged in raspberrypi/linux rpi-7.2.y+ and submitted upstream dri-devel, waiting on merge status as of 9/15/26. On an older kernel this mode will fail to allocate the framebuffer; fit mode still works everywhere. |
If the panel can't keep the requested frame rate (a slow SPI clock, a large panel, or a high-frame-rate source), ZeroPlay drops late frames rather than falling into slow motion — you'll see fewer frames displayed, at the correct real-time speed.
# Default: whole picture, letterboxed
zeroplay movie.mp4
# Crop to fill the panel (needs a current kernel, see table above)
zeroplay --spi-fill movie.mp4To play a video file seamlessly and indefinitely, start zeroplay with the --loop-seamless flag. Instead of tearing the pipeline down and rebuilding it at the end of every pass, the demuxer seeks back to the start and keeps feeding packets, so there is no gap between loops.
Restriction:
- Does not support external audio or subtitle files.
MP4 Requirements:
- If the audio stream is longer than the video stream, it will be trimmed to match the video duration.
- If the video stream is longer than the audio stream, zeroplay will fail.
- For best results, ensure the MP4 file has matching audio and video durations.
In --control mode the flag applies per clip: loadloop loops seamlessly, while load plays once and still emits ended.
Requires
make WS=1. No libwebsockets dependency in the base build.
Runs ZeroPlay as a remotely controlled player, receiving commands from a backend and reporting state every 5 seconds.
zeroplay --ws-url ws://backend.local:8080/ws --device-token <token>Runs ZeroPlay as a long-lived process controlled via stdin/stdout. Unlike standard mode, the DRM display is held open between clips — no console flash between videos and the last frame stays on screen until the next clip starts.
zeroplay --control [initial_path]If an initial path is given it auto-loops at startup so the screen is live immediately.
| Command | Description |
|---|---|
load <path> |
Play a file once, emit ended on stdout at end |
loadloop <path> |
Play a file, seamlessly re-looping at end |
pause |
Pause playback |
resume |
Resume playback |
stop |
Stop and hold last frame |
quit |
Exit cleanly |
| Event | Description |
|---|---|
ready |
Emitted at startup when display is initialised |
ended |
Emitted when a non-looping clip finishes |
Useful for Python scripts, kiosk controllers, or any local process that needs seamless clip switching without WebSocket overhead.
| Flag | Env var | Description |
|---|---|---|
--ws-url URL |
BACKEND_WS_URL |
Backend WebSocket URL (ws:// or wss://) |
--device-token TOKEN |
DEVICE_TOKEN |
Device auth token (required) |
--health-port PORT |
HEALTH_PORT |
HTTP health endpoint port (default: 3000) |
| Command | Fields | Description |
|---|---|---|
load |
url |
Load and play a media URL |
play |
— | Resume |
pause |
— | Pause |
stop |
— | Stop and unload |
seek |
positionMs |
Seek to position in milliseconds |
Reconnects automatically with exponential backoff (1s → 30s).
sudo nano /etc/systemd/system/zeroplay.service[Unit]
Description=ZeroPlay video player
After=multi-user.target
[Service]
User=pi
Group=video
Environment=HOME=/home/pi
ExecStart=/usr/local/bin/zeroplay --loop /home/pi/media/
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now zeroplay
sudo usermod -aG video pi- Demux — libavformat reads the container and routes packets (local files, HLS, network streams, YouTube via yt-dlp)
- Video decode — V4L2 M2M hardware decoder
- Display — DRM/KMS atomic modesetting with DMABUF zero-copy from decoder to scanout
- Subtitles — rendered into an ARGB8888 DRM overlay plane, composited by the display hardware
- Audio — libavcodec → libswresample (hardware rate probing, HE-AAC SBR correction) → ALSA
- Sync — wall-clock pacing against video PTS; A/V sync throttling for separate stream playback
No X11, no Wayland, no GPU compositing. Runs from a TTY or SSH session.
| Feature | omxplayer | ZeroPlay |
|---|---|---|
| Hardware decode | OpenMAX (deprecated) | V4L2 M2M |
| Display | dispmanx (deprecated) | DRM/KMS |
| Dual display | No | Yes (Pi 4) |
| Playlist / directory | No | Yes |
| Image display | No | Yes |
| HLS streaming | No | Yes |
| YouTube | No | Yes (via yt-dlp) |
| Subtitles (SRT) | Yes | Yes |
| Subtitles (embedded MKV) | No | Yes |
| WebSocket remote control | No | Yes (opt-in) |
| Chapter navigation | No | Yes |
| Seeking | Yes | Yes |
| Volume control | Yes | Yes |
| Loop | Yes | Yes |
| Runs on modern OS | No | Yes |