Deploying vibe-live: the built-in engine
No media server at all (F59). The app's own RTMP listener takes the publish, an ffmpeg this project compiles makes the renditions and the thumbnail, and the files are served from the app's own routes -- one process, one image, nothing beside it to configure or keep alive.
Everything in all-in-one.md about required variables, sign-in, notifications, federation, data and validating the install applies here too and is not repeated. What differs is the media half, and one thing is genuinely missing: see What it does not do.
There is no compose file for this engine. compose.yaml runs
OvenMediaEngine and compose.livekit.yaml runs LiveKit, because each of
those needs a second service wired to the first; this one needs a container
and a port. The image is the deployment.
How the pieces fit
- Encoders publish to the app's own RTMP port, 1936
(
VIBE_LIVE_RTMP_LISTEN_ADDR), with the stream key the settings page shows. The app authorises the key itself -- there is no admission webhook and no shared secret here, because there is no second process to agree with. - One ffmpeg per output, started when it is wanted. The thumbnail runs
for as long as the channel is live; a rendition starts with its first
viewer and stops 30 s after its last.
VIBE_LIVE_RENDITIONSis the owner's selection, and nothing is ever encoded taller than what the streamer sent. - Live means media, not an accepted key (B17). The channel is announced
when the thumbnail process has decoded a keyframe and written
thumb.jpg. A publish that produces nothing before the deadline ends without ever having been announced, which is what keeps a dead encoder off the dashboard. - The files are disposable. Each channel's directory is emptied when its
publish starts and removed when it ends, and the whole of
VIBE_LIVE_MEDIA_DIRis cleared at startup. Put it on tmpfs (anemptyDiron Kubernetes): nothing in it is meant to survive a restart. - The public URLs are the same as every other engine.
/hls/,/cast-hls/and/thumb/are served from those files rather than proxied to a media server, so the frontend, Chromecast and the dashboard cards do not know which engine is running. - WebRTC is optional and off by default. Set
VIBE_LIVE_WEBRTC_UDP_PORTand the app runs its own WebRTC server: WHEP playback at/app/<channel>/whep, sub-second like OvenMediaEngine's, and WHIP for Go Live from the browser on the same port, with ICE over TCP on 3478 for networks that block UDP. A channel's WebRTC feed is itself one more ffmpeg, started by the first viewer asking for it and stopped 15 s after the last one leaves. Unset, viewers get the HLS stream and streamers publish from OBS.
Ports
| Why | Symptom when it is closed | |
|---|---|---|
| 80/tcp, 443/tcp | the site, and the ACME challenge if something in front gets its own certificate | as in all-in-one.md |
| 1936/tcp | RTMP from encoders | the settings page shows an address that refuses connections |
| 10000/udp | WebRTC media, only with VIBE_LIVE_WEBRTC_UDP_PORT=10000 |
streams negotiate and deliver nothing |
| 3478/tcp | WebRTC over TCP, for networks that block UDP -- only with the UDP port set | those viewers fall back to HLS; those streamers cannot go live from the browser |
With WebRTC on, the same five ports as every other engine, with the same
jobs: all-in-one.md's OvenMediaEngine and livekit.md's LiveKit use 10000/udp
and 3478/tcp too, so the router and firewall do not change with the engine.
Without WebRTC, three: the site and RTMP. The UDP port is whatever
VIBE_LIVE_WEBRTC_UDP_PORT names -- 10000 is only a convention, and the one
to keep. 3478 is VIBE_LIVE_WEBRTC_TCP_PORT's default.
Configuration
Nothing is required beyond all-in-one.md's list, and
VIBE_LIVE_OME_ADMISSION_SECRET is not in it -- that variable is
required only when the engine is ome.
| Variable | Notes |
|---|---|
VIBE_LIVE_ENGINE |
builtin. Unset infers it: no LiveKit settings, no OvenMediaEngine backend URLs, and an ffmpeg on PATH means this engine. Set it explicitly anyway if the deployment might grow an engine later -- it then refuses a second one rather than quietly switching. |
VIBE_LIVE_MEDIA_DIR |
Where segments, playlists and thumbnails are written and served from. Unset is a directory under the system temp dir, which works and is not where you want it: name a tmpfs mount. |
VIBE_LIVE_WEBRTC_UDP_PORT |
Turns on the app's WebRTC server. The ONE UDP port every viewer's media uses -- publish it, open it in the firewall, forward it from the router. Use 10000, the port every other engine uses. |
VIBE_LIVE_WEBRTC_TCP_PORT |
ICE over TCP, with the UDP port set. Unset is 3478; 0 turns it off. |
VIBE_LIVE_WEBRTC_PUBLIC_IPS |
Comma-separated addresses viewers are told to reach that port at: your public IP, plus your LAN address if viewers at home go through a router with no UDP hairpin. Unset advertises this host's own interfaces -- right on a LAN, wrong behind NAT. |
VIBE_LIVE_MAX_TRANSCODES |
How many publishes may transcode at once; one past it is refused before ffmpeg starts. Unset is floor(CPUs / 1.25), sized from a measured ~1.15 cores per 1080p60 channel -- four on a six-core node. Every channel being watched over WebRTC is one more transcode under this cap. |
VIBE_LIVE_RENDITIONS |
The owner's selection, as on every engine. Each one is an encode made only while somebody is watching it, so this is the load lever here. |
VIBE_LIVE_DVR_WINDOW |
Caps how far back viewers can seek through a live stream, 1m to 24h; unset is the whole stream. Seeking is on unless an admin turns it off, and each live channel then records its source as sent -- copied, not re-encoded, so almost no CPU -- into VIBE_LIVE_RECORDINGS_DIR for as long as it is live: roughly the streamer's bitrate times the stream's length (6 Mbit/s for an hour is about 2.7 GB), deleted when it ends unless kept. The cap bounds that. |
VIBE_LIVE_RECORDINGS_DIR |
Where recordings go: each live stream's, for seeking, and past broadcasts when an admin allows them and a streamer keeps theirs. Unset is recordings beside the SQLite database (/data/recordings in the image), so it is backed up with /data. |
VIBE_LIVE_RTMP_LISTEN_ADDR |
The listener itself, :1936 unless you move it. |
VIBE_LIVE_RTMP_PORT |
The port streamers are told, when a router publishes 1936 somewhere else. It changes the address on the settings page and nothing else. |
VIBE_LIVE_TURN_* |
Engine-independent: it advertises ICE servers to the browser. This engine bundles no TURN of its own; ICE over TCP on 3478 serves a viewer whose network blocks UDP but lets TCP out, and one whose network allows only 443 needs a TURN server here or falls back to HLS. |
VIBE_LIVE_OME_* and VIBE_LIVE_LIVEKIT_* do nothing here, and setting
either group alongside VIBE_LIVE_ENGINE=builtin is refused at startup:
one engine per deployment.
Building and running it
CI publishes this target as forgejo.gmatte.xyz/gmatte/vibe-live:ffmpeg-<version>
(and ffmpeg-<commit sha>, ffmpeg-latest) since 2026-09-28. Pin the
version or the SHA; to build it yourself instead:
docker build --target all-in-one-ffmpeg -t vibe-live:ffmpeg .
It is Alpine with the binary, a static ffmpeg, and the GPL notices and
written source offer those two oblige (deploy/ffmpeg/SOURCE-OFFER.txt).
Then, with your variables in .env and TLS terminated by whatever you
already run:
docker run -d --name vibe-live \
--env-file .env \
-p 8080:8080 -p 1936:1936 \
--tmpfs /media \
-v vibe-live-data:/data \
-e VIBE_LIVE_MEDIA_DIR=/media \
vibe-live:ffmpeg
Add -p 10000:10000/udp -p 3478:3478 -e VIBE_LIVE_WEBRTC_UDP_PORT=10000 for WebRTC. The
app serves plain HTTP; put the same reverse proxy in front of it that
all-in-one.md describes, and note that publishing from
the browser needs a secure context, so a self-signed certificate is the
minimum on a LAN.
To adapt compose.yaml instead -- worth it for the caddy service alone
-- three changes: build target: all-in-one-ffmpeg, drop the 3333 port
mapping and keep 10000/udp and 3478 only if you want WebRTC (and then set
VIBE_LIVE_WEBRTC_UDP_PORT=10000), and set VIBE_LIVE_ENGINE=builtin with
a tmpfs at VIBE_LIVE_MEDIA_DIR. VIBE_LIVE_OME_ADMISSION_SECRET can go.
What it does not do
- LL-HLS. ffmpeg's
hlsmuxer emits noEXT-X-PART, so withoutVIBE_LIVE_WEBRTC_UDP_PORTthe latency here is ordinary HLS -- several seconds -- rather than OvenMediaEngine's sub-second LL-HLS. With the UDP port set, WebRTC playback closes that gap for everyone whose network allows it, and HLS stays as the fallback. This is the one thing an engine still buys;docs/design/engines-and-players.mdhas the measurement. - Hardware transcoding. ffmpeg's NVENC and VAAPI paths are reachable in principle -- which is the reason gpu-transcoding.md records the decision as reopened rather than closed -- but nothing here uses them yet. Every encode is x264 on the CPU.
- A TURN server. ICE over TCP covers most UDP-blocking networks; for
the rest see
VIBE_LIVE_TURN_*above.
Everything else the other engines do, this one does: force-stop, viewer
counts, thumbnails on the cards, the rendition menu, Chromecast, and its own
figures in /internal/metrics on the internal listener.
Validating the install
scripts/validate-setup.sh https://your.instance
It is engine-aware. On this engine it asks /api/v1/instance which WebRTC
transport the deployment has and probes accordingly: with the UDP port set
it checks that /app/<channel>/whep reaches the app's WebRTC server and
then says plainly that it cannot test UDP from outside; without it, it
reports "no WebRTC on this engine" as information rather than a fault. The
HLS, Chromecast and thumbnail routes are checked the same way as on every
other shape.