Streaming Delivery (Icecast + MediaMTX)
Listeners receive the broadcast through two delivery paths fed from the same Liquidsoap output.
Icecast — broadcast source
| Setting | Value |
|---|---|
| Server | files.mediahubnetwork.net |
| Port | 8005 |
| Mount | /main |
| Public URL | https://files.mediahubnetwork.net/main (or :8005/main) |
| Codec | OGG Vorbis 500 kbps, 48 kHz (worker source stream) |
- Liquidsoap sources its output to Icecast on
localhost:8005/main; this is the master signal everything else derives from. - The master OGG URL
https://files.mediahubnetwork.net/mainreturnsAccess-Control-Allow-Origin: *, so the web player can use it as a CORS-safe fallback (the player falls back to OGG if HLS auth fails). - Icecast publishes metadata (artist/title) on the mount so players display now-playing automatically.
- The source password is configured on the production node (
hackmein the worker invocation); the engine holds the same value in its remote config.
HLS bridge (start_loklok_bridge.sh)
MediaMTX does not ingest from Liquidsoap directly. A dedicated ffmpeg bridge pulls the Icecast signal and re-publishes it into MediaMTX:
| Setting | Value |
|---|---|
| Ingest | Icecast OGG http://localhost:8005/main (with -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 10 so transient Icecast errors are retried inside the same process) |
| Encode | AAC 256 kbps, 48 kHz, stereo (raised from 128k 2026-08-12 — the 128k AAC re-encode of the 500k OGG master was generational loss; master playlist shows ~275k) |
| Push | FLV → rtmp://localhost:19350/loklok (MediaMTX RTMP ingest) |
| Deliver | HLS https://api.mediahubnetwork.net/loklok/index.m3u8 |
| Watchdog | start_loklok_bridge.sh — restarts the ffmpeg bridge if it dies. To change the encoder you must also pkill -9 -f start_loklok_bridge.sh and relaunch via the @reboot cron command — the running watchdog re-spawns from its old in-memory loop forever |
| Log | /home/sms/radio/loklok_bridge.log |
Changing the bridge bitrate: killing just the ffmpeg is NOT enough — the watchdog holds the old
start_bridgefunction in memory and re-spawns the old bitrate. Kill the watchdog (pkill -9 -f start_loklok_bridge.sh) and relaunch via the cron@reboot sleep 20command. (Two watchdogs may coexist; harmless while ffmpeg is alive, but a double-spawn could push two RTMP sources — pkill all and relaunch once if bridge behavior is ever weird.)
Why the bridge must never die (-reconnect)
If the bridge process restarts, MediaMTX recreates the HLS muxer with a new
random segment-hash prefix and a media-sequence reset. Connected players still
holding the previous child playlist then 404 on every old-generation segment
and keep playing stale buffered audio (cross-contamination) until they re-sync.
-reconnect keeps the RTMP publish alive through Icecast hiccups so the muxer
generation never changes; the client page additionally tracks the muxer
generation and self-heals if it ever does change (see below).
HLS delivery & the cookie flow
MediaMTX protects HLS playlists with a cookieCheck session:
GET /loklok/index.m3u8→ 302 →Location: ?cookieCheck=1+Set-Cookie: cookieCheck=1.- Following the redirect returns the master, which references plain
audio1_stream.m3u8— no session UUID. - Child playlist + init + segments all return 200 with just the cookie (same-origin, so the cookie persists across every request). hls.js re-fetches the child continuously → auth is renewed forever → self-sustaining, zero 401s.
Player page must be same-origin as the HLS URL. The page is served at
api.mediahubnetwork.net; pointing HLS_URL at files.mediahubnetwork.net made
the cookie third-party (blocked → intermittent 401 → stutter). The fix (2026-08-13):
HLS_URL = https://api.mediahubnetwork.net/loklok/index.m3u8 (same origin). The
APK uses the same HTTPS standard-port URL (libmpv persists the cookie inside the
TLS session).
ABR ladder — tried, reverted (2026-08-13)
A 3-rung ABR (256k/128k/64k → /abr_loklok.m3u8 dynamic master in
radio_server.py) was built and deployed, then reverted for the web player:
MediaMTX's session UUIDs are bound to the minting connection and die after
~60–100 s idle; hls.js only re-fetches the active variant's playlist, so the
other variants' sessions died → 401s on ABR probes → stutter loop. The cookie flow
needs no session UUID at all, so single-stream 256k is the stable production
path. /abr_loklok.m3u8 still exists but is unused. Do not enable
lowLatencyMode:true in hls.js — this MediaMTX build returns 400 on all
_HLS_msn blocking-reload requests (that 400-loop is a documented bug source).
Choosing a path
| Listener type | Use |
|---|---|
| Classic pages / desktop players | Icecast /main (https://files.mediahubnetwork.net/main) |
| Next.js player / modern web | MediaMTX HLS https://api.mediahubnetwork.net/loklok/index.m3u8 (same-origin with the page) |
| iOS / Safari | HLS (native) via MediaMTX |
| Mobile live broadcast | WHIP /go-live page + /whip-info (see Live ingest) |
| Video (LOKLOK TV) | HLS …:19351/loklokvideo/index.m3u8 · RTSP rtsp://…:19354/loklokvideo · WebRTC :19355 |
The video program (loklokvideo) is a second MediaMTX path fed by the
video_feed ffmpeg renderer; the transcode farm + freetube ABR auto-build
multi-bitrate ladders for it (see LOKLOK TV).
Client resilience (web player index.html)
The page self-heals the failure modes above:
- Muxer-generation tracker —
currentMuxerGen()reads the 12-hex segment-hash prefix from the active level's last fragment; onLEVEL_UPDATEDa changed hash triggers ONE rate-limited (once/30s,reconnectSeq-guarded)applySource('hls')+resumePlayback()to flush stale buffered audio. - Error backstop — catches 401/403 (session death) and 404/410 (muxer restart) on fatal OR non-fatal errors, debounced 400ms / rate-limited once per 30s, and re-mints the master.
lowLatencyMode:false+enableWorker:true— near-live-edge play without the_HLS_msnblocking-reload 400s this MediaMTX build always rejects.- Buffer tuning —
maxBufferLength 30/maxMaxBufferLength 60/backBufferLength 15keep the active session warm. startLevel:0— open on the top variant and only downshift when the link genuinely can't sustain it.- OGG failover — if HLS is unreachable the player falls back to the CORS-safe
Icecast master (
https://files.mediahubnetwork.net/main). userStoppedflag — only a real user pause sets it; a buffer-starvationpauseevent no longer blocks the watchdog/backstop/reconnect recovery paths (the "plays part then silent forever" deadlock fix).
Stale-page trap:
radio_server.pyserves/and/index.htmlwithCache-Control: no-store, no-cache, must-revalidate, max-age=0so browsers can never run stale player JS again. If a page misbehaves after a fix, hard-refresh (Ctrl+F5) to drop the old bundle. (The earlier attempt to set these headers viasend_headerbeforesend_responsecrashed the page — they are applied through anend_headers()override aftersend_response.)
Operational notes
- The master signal is Icecast
/main; if it is down, both paths die (including the video program, which ingests the same OGG). - The HLS path depends on the ffmpeg bridge — if
19351is dead, checkstart_loklok_bridge.sh//home/sms/radio/loklok_bridge.logand the MediaMTX process. - The
stats_updatetelemetry reflects MediaMTX HLS listener counts (polled from the read-only proxy:19353), with Icecast as fallback — see Radio engine.
See also
- Liquidsoap — the source feeding both servers.
- Production runbook — restarting/verifying the chain.