Rádio Alvorada TV · Platform 3 of 4 · 2025–2026

audio.radioalvoradatv.com.br

Zero-install radio broadcasting from any browser

Volunteer operators open a web page, pick a mic, paste a token, and go live. A WebSocket-to-ffmpeg bridge on the server handles transcoding to MP3 so Icecast can serve listeners directly.

Role

Product Designer & Design EngineerOperator UI + bridge + VM

Origin

Icecast on Oracle VMnginx TLS front

Ingest

WebSocket + ffmpegNode bridge service

Output

MP3 128k stereoBroad HTMLAudio support

  • $0

    Origin monthly cost

    Oracle Always Free

  • 128k

    MP3 output

    ffmpeg normalization

  • 0

    Desktop apps required

    Browser operator panel

Operator panel with microphone source picker, broadcast token field, level meter, and start control

The operator UI replaces desktop encoder software — three steps: pick input, paste token, start.

Level meter uses Web Audio AnalyserNode so volunteers confirm signal before going live.

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

Operator browser sends WebM chunks through nginx to ffmpeg and IcecastFIG 1 — BROWSER-TO-ICECAST PIPELINEOperator UIMediaRecorder1s WebM chunkswss://…/broadcasttoken authnginx TLS · no access_logNode bridge:8090 systemdsingle session · 409 if busyffmpegstdin → MP3128k stereoIcecast127.0.0.1/liveLISTENERShttps://stream.radioalvoradatv.com.br/live → HTMLAudioElement on audio.radioalvoradatv.com.brVercel health API/api/health → Icecast status-json.xslok: true only when source connected · off-air = “Sem sinal”Security boundaryIcecast never public · port 8000 blockednginx blocks /admin · bridge runs as radio-bridge user

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 connected

Technical 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

Mobile listener page showing off-air Sem sinal state when no Icecast source is connected

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