Skip to main content

Radio Engine (radio-engine.js)

radio-engine.js is the bridge between the control plane and the broadcast node. It owns the channel abstraction, the sync of playlists/media, and the realtime now-playing telemetry that feeds every frontend.

Channel abstraction

The engine exposes radio channels (e.g. main). Each channel has:

  • a now-playing item (radio.nowPlaying)
  • a queue
  • a channel id and its own Socket.io namespace

radio.nowPlaying is the canonical state emitted in status events (see Socket events).

Sync to production

Since 2026-08-07 the engine uploads over HTTP pull, not scp. The full mechanism (endpoints, SSH_OPTS, fingerprints, verification) is in Media sync; the short version:

  1. remoteWrite() opens one ssh per file to run a tiny control command on the remote: mkdir -p … && curl -sf -o '<tmp>' "http://10.40.3.174:5000/api/sync/media?rel=…&token=…" && cat '<tmp>' > '<final>' && rm -f '<tmp>' && stat -c %s.
  2. The remote pulls the bytes over plain HTTP (token + path-guarded), writes the tmp in place (cat tmp > final) so Liquidsoap's watch reload fires, and returns the final size.
  3. 5 retries with exponential backoff + a 190s watchdog SIGKILL.
  4. SSH_OPTS = -o BatchMode=yes -o PreferredAuthentications=publickey on every ssh (including ensureProcess/remoteExec) so a keyless SYSTEM-session engine fails fast instead of hanging on a password prompt.

updateRemoteStream is serialized via a _syncInFlight lock — a heartbeat and a refresh-resync can no longer double-push/rewind the remote.

Content-fingerprint manifest

remoteSynced (persisted to E:\radionew\logs\remote_synced.json) stores path#size#mtimeMs fingerprints, so re-fetched/retagged files re-upload and a restart never re-pushes the whole 120-file rotation.

Rotation verification (verifyRemoteRotation)

After each playlist push the engine size-checks every remote file (stat -c %s | grep -qx <localSize>), chunked 20 paths per ssh to stay under the Windows 32 KB command-line limit. Boot logs [Sync] Rotation verified on remote (N files present, size-checked) with zero MISS when healthy.

Playlist push (finalizeSync)

The M3U is pushed only when its content actually changed:

  • finalizeSync reads the local M3U and compares it to lastPushedM3uContent. If unchanged, it skips the upload entirely and just verifies Liquidsoap is alive (ensureProcess). This prevents a reload churn: Liquidsoap watches the file, and a needless rewrite would reload the rotation and rewind the stream onto tracks that just played.
  • When it does push, it uploads in place. M3U files are written with dd if='<tmp>' of='<final>' conv=notrunc bs=1M — a single write syscall (one IN_MODIFY), whereas media files use cat tmp > final. Because the m3u is padded to a fixed 32768 bytes by the scheduler (FIXED_M3U_SIZE), dd-notrunc fully overwrites it with no stale tail, and Liquidsoap reloads exactly once per push instead of twice (see Double-reload fix).
  • An empty M3U is never pushed — an empty overwrite makes Liquidsoap emit Fetch failed: empty and go silent; the last good rundown is preserved.

Remote configuration

ConstantValue
UBUNTU_IP10.10.8.230
UBUNTU_USERsms
REMOTE_PATH/home/sms/radio
ICECAST_PASSWORDIcecast source password (installed at deployment)
Local stream port8005 (Icecast source port)

Failover behavior

  • If a track fails mid-play, the engine emits a fresh status (server keeps listeners updated) and moves to the next queued item.
  • Sync failures are retried, then logged — they never crash the scheduler loop.
  • The listener-count source is MediaMTX HLS, not Icecast: updateStats() polls the read-only listener proxy at http://10.10.8.230:19353/v3/paths/list every 5 s → emitStats(readers.length, 128, 'mediamtx'). Icecast's countIcecast() is only a fallback when MediaMTX is unreachable (source = mediamtx | icecast in stats_update).

How it connects to the server

server.js instantiates the engine and passes it the Socket.io server so the engine can emit channel events. This is why every frontend can display now-playing without polling.

See also