Problem
Desktop Encoders Create Operational Debt for Volunteers
Traditional internet radio expects operators to run BUTT, Nicecast, or OBS alongside a stream URL and mount point. That works for engineers — it breaks for church volunteers on unfamiliar laptops.
The product bet: move encoder complexity to a server-side bridge the operator never sees. Open a web page, pick microphone or tab audio, paste a token, click start. Video streaming in this same monorepo deliberately avoided a bridge; audio accepts it because the UX win is larger.
My role and product bet
Server-Side Complexity, Browser-Side Simplicity
I owned the operator experience, listener surface, streaming-origin architecture, WebSocket bridge, Icecast/nginx provisioning scripts, health APIs, and the runbook. The product decision: the person starting a devotional broadcast should not debug mount points, source passwords, encoder formats, or local audio routing.
This is where the audio case study deliberately differs from video. For video, the simplest operator path was OBS directly into nginx-rtmp, so I removed the bridge. For audio, requiring BUTT or OBS would create unnecessary friction, so I accepted a small server-side bridge. Same philosophy, opposite architectural choice: put complexity where the user's mental model is least harmed.
Operator goal
Open a browser, choose microphone or tab audio, paste a token, watch the meter, and start broadcast.
System goal
Normalize inconsistent browser audio containers into MP3 128k stereo for the broadest listener compatibility.
Security goal
Fail closed by default, keep Icecast off the public internet, and prevent one operator from accidentally displacing another.
Truth goal
Show Sem sinal when no source is connected. This is live-ingest infrastructure, not an automated playlist pretending to be 24/7 programming.
Architecture
Four-Stage Audio Pipeline: Browser to Listener
FIG 1 — Browser sends WebM chunks; the VM normalizes to MP3 128k. Operators never install BUTT.
Token auth fails closed. Second connection returns 409. Icecast binds localhost only.
Operator browser
MediaRecorder (WebM/Opus chunks every 1s)
→ wss://stream…/broadcast?token=…
→ nginx TLS (access_log off on ingest — token in query)
→ Node bridge :8090
→ ffmpeg stdin → MP3 128k → icecast://127.0.0.1:8000/live
→ https://stream…/live (public listeners)
Vercel app (audio.radioalvoradatv.com.br)
/api/now-playing → Icecast status-json.xsl
/api/health → ok when source connectedTechnical deep dive
How the Operator Panel Replaces Desktop Software
MediaRecorder MIME negotiation
Tries audio/webm;codecs=opus, then webm, ogg, mp4 — first supported wins. Chunks emit every 1000ms as ArrayBuffer over WebSocket. Tab/system audio uses getDisplayMedia (Chromium); Safari falls back to microphone-only with explicit UI copy.
Level meter
Web Audio AnalyserNode on the local preview stream — operators confirm signal before going live, preventing silent broadcasts.
Fail-closed token auth
If BROADCAST_TOKEN is unset on the VM, all connections reject — no accidental open ingest. Comparison uses timingSafeEqual. Second simultaneous connection gets HTTP 409 (one operator at a time).
Implementation detail
ffmpeg as Format Normalizer: One Bridge, Every Browser
Browser audio is not one format. Depending on the browser, MediaRecorder outputs WebM/Opus, Ogg/Opus, or MP4. Icecast can serve many formats, but the safest listener target for ordinary HTML audio players is still MP3. The bridge treats browser chunks as an ingest format, not a distribution format.
ffmpeg reads the WebSocket binary stream through stdin, rate-limits input with -re, converts to libmp3lame, and pushes to the Icecast source mount. The browser sends whatever it can record reliably; the server normalizes it into what radios and browsers universally understand.
Security
Defense in Depth: Localhost Icecast Behind nginx TLS
Icecast binds 127.0.0.1:8000 only. nginx terminates TLS on 443 and proxies /live for listeners and /broadcast for ingest. Port 8000 is blocked in Oracle security lists. Admin UI returns 403 at nginx.
Bridge runs as unprivileged radio-bridge user with systemd sandboxing (ProtectSystem=strict, memory cap 256M). Long proxy timeouts (3600s) keep live sessions alive.
Reliability model
Graceful Failure: Single-Session Locks and Auto-Recovery
The bridge enforces a single active broadcast session. A second operator receives HTTP 409 instead of replacing the current program. If ffmpeg exits, the WebSocket closes with an encoder error so the UI stops pretending audio is still going out. If the bridge process crashes, systemd restarts it.
The health model is simple: Icecast is the source of truth. If status-json.xslreports a source on /live, the API returns ok: true. If no source exists, the app is not "down"; it is off-air. HTTP failure describes infrastructure; Sem sinal describes broadcast state.
Health & off-air
Honest Off-Air State: Sem Sinal, Not a Fake Loop

Off-air is honest — Sem sinal, not a fake loop. Infrastructure runs 24/7; audio exists only when someone broadcasts.
Same status-json.xsl semantics power /api/health on Vercel and the operator panel badge.
/api/health always returns HTTP 200 — the ok boolean reflects whether Icecast reports an active source on /live. Off-air UI shows Sem sinal, not a fake loop (unlike video's YouTube fallback — a deliberate product difference).
Infrastructure is 24/7; content is live-only. The VM and bridge run continuously, but audio exists only when someone is broadcasting. That honesty matches volunteer-operated radio.
Outcome
Production Results and Engineering Evidence
This project demonstrates end-to-end ownership: designing an operationally safer broadcast workflow, then building the infrastructure to make it real. The value is not WebSocket novelty — it is how browser capture, ffmpeg transcoding, Icecast distribution, nginx security, Vercel APIs, and operator copy combine into one coherent product.
- One-script VM bootstrap: Icecast + nginx + certbot + bridge systemd unit
- Operator panel with device picker, token field, meter, and public status poll (10s)
- Listener page with same now-playing API as operator health semantics
- Documented external-encoder path (BUTT/OBS) for advanced operators who prefer it
- Security posture: fail-closed token, localhost Icecast, nginx admin block, single-session ingest
