Skip to main content

Streaming Delivery (Icecast + MediaMTX)

Listeners receive the broadcast through two delivery paths fed from the same Liquidsoap output.

Icecast — broadcast source

SettingValue
Serverfiles.mediahubnetwork.net
Port8005
Mount/main
Public URLhttps://files.mediahubnetwork.net/main (or :8005/main)
CodecOGG 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/main returns Access-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 (hackme in 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:

SettingValue
IngestIcecast 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)
EncodeAAC 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)
PushFLV → rtmp://localhost:19350/loklok (MediaMTX RTMP ingest)
DeliverHLS https://api.mediahubnetwork.net/loklok/index.m3u8
Watchdogstart_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_bridge function 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 20 command. (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).

MediaMTX protects HLS playlists with a cookieCheck session:

  1. GET /loklok/index.m3u8302Location: ?cookieCheck=1 + Set-Cookie: cookieCheck=1.
  2. Following the redirect returns the master, which references plain audio1_stream.m3u8 — no session UUID.
  3. 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 typeUse
Classic pages / desktop playersIcecast /main (https://files.mediahubnetwork.net/main)
Next.js player / modern webMediaMTX HLS https://api.mediahubnetwork.net/loklok/index.m3u8 (same-origin with the page)
iOS / SafariHLS (native) via MediaMTX
Mobile live broadcastWHIP /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 trackercurrentMuxerGen() reads the 12-hex segment-hash prefix from the active level's last fragment; on LEVEL_UPDATED a 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_msn blocking-reload 400s this MediaMTX build always rejects.
  • Buffer tuningmaxBufferLength 30 / maxMaxBufferLength 60 / backBufferLength 15 keep 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).
  • userStopped flag — only a real user pause sets it; a buffer-starvation pause event no longer blocks the watchdog/backstop/reconnect recovery paths (the "plays part then silent forever" deadlock fix).

Stale-page trap: radio_server.py serves / and /index.html with Cache-Control: no-store, no-cache, must-revalidate, max-age=0 so 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 via send_header before send_response crashed the page — they are applied through an end_headers() override after send_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 19351 is dead, check start_loklok_bridge.sh / /home/sms/radio/loklok_bridge.log and the MediaMTX process.
  • The stats_update telemetry reflects MediaMTX HLS listener counts (polled from the read-only proxy :19353), with Icecast as fallback — see Radio engine.

See also