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

audio.radioalvoradatv.com.br

Broadcast from the browser, not a desktop encoder

Volunteer operators shouldn't need BUTT or OBS for radio. This stack accepts browser audio over WebSocket, transcodes on the server, and serves MP3 to listeners through Icecast.

Role

Design Engineer & Full Stack DeveloperOperator UX + streaming bridge + ops

Stack

Next.js · React · TypeScriptNode bridge · ffmpeg

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 are operational debt

Traditional internet radio expects operators to run BUTT, Nicecast, or OBS alongside a stream URL and mount point. That is fine for engineers — fragile for church volunteers on unfamiliar laptops.

The product bet: move encoder complexity to a server-side bridge the operator never sees. They 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

Move complexity away from the operator, not out of the system

I owned the operator experience, listener surface, streaming-origin architecture, WebSocket bridge, Icecast/nginx provisioning scripts, health APIs, and the runbook. The technical decision was not “browser audio is cool.” The product decision was that 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 the video case study. 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 tool is live-ingest infrastructure, not an automated playlist pretending to be 24/7 programming.

Architecture

Browser → bridge → Icecast → listeners

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

Operator path

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 see signal before going live, reducing 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

Why ffmpeg sits in the middle

Browser audio is not one format. Depending on the browser, MediaRecorder may output 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 therefore 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. In plain language: the browser sends whatever it can record reliably; the server turns it into the thing radios and browsers understand.

Security

Icecast never faces the public internet

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

What happens when things go wrong

The bridge has a single active broadcast session by design. 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 can stop pretending audio is still going out. If the bridge process crashes, systemd restarts it.

The health model is intentionally 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. That distinction matters because HTTP failure would describe infrastructure, while Sem sinal describes broadcast state.

Health & off-air

Honest live semantics

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

What shipped — and what it proves

This project proves I can design an operationally safer broadcast workflow and then implement the infrastructure needed to make that workflow real. The value is not the novelty of WebSockets; it is the way browser capture, ffmpeg, Icecast, nginx, Vercel APIs, and operator copy combine into one coherent experience.

If you're hiring for a remote Design Engineer, Frontend Developer, Full Stack Developer, or Product Engineer role, this is the kind of work I do: design the workflow, ship the UI, and own the operational details that keep it reliable.

  • 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

Searchable expertise

Keywords this work should rank for

The work is intentionally described with the phrases hiring teams and founders actually search for: browser audio broadcasting, WebSocket audio streaming, Icecast operator UI, self-hosted radio infrastructure, ffmpeg audio bridge, live audio product design, Oracle Always Free streaming, and design engineering for broadcast tools.