Skip to content

IFM Station — Broadcaster Studio Web App

Status: ✅ Core demo implemented (packages/sdk/examples/station/) — this document is the full design spec for the production app.

Implemented today: frequency creation (Number/Name toggle, validated, auto-assigned free numbers that never collide with your own stations), a persisted “Your stations” manager (created stations survive reloads, reopenable/removable from the setup page), a reload-safe studio (the on-air station is re-tuned + re-announced automatically), a broadcast console (unlimited mixer channels with device-real sources — enumerated microphones/interfaces, browser-tab &...

The Station app is a professional broadcasting interface for originating live content into the IFM mesh. Think OBS Studio + Streamlabs + Restream built natively for the IFM protocol — browser-based, zero-install, entering the mesh through a WebSocket relay. The desktop app is the first-class participant; the web is the interface from which you enter and use the mesh.


Target Audience

  • Radio stations — Internet radio, community radio, campus radio
  • Podcasters — Live podcast recording with audience interaction
  • Live streamers — Music, talk, gaming, events
  • Event producers — Conferences, concerts, sports commentary
  • Emergency broadcasters — Public alerts, community coordination

Core Features

1. Stream Setup (/station/setup)

Pre-flight configuration before going live:

SectionFields
FrequencySelect existing or create new (name, namespace, channel); access type: public/protected/hidden
MetadataTitle, description, tags (genre, language, region), cover art upload
ScheduleGo live now / schedule for later; recurrence rules; auto-end timer
SimulcastAdd secondary frequencies (cross-post audio + chat)
Monetization HooksDonation link, subscription tier, sponsor slots (extensible)

2. Studio (/station/studio)

Main broadcasting interface — the cockpit:

Audio Source Mixer

  • Per-source: Gain slider, mute, push-to-talk (mic), pause, solo, monitor
  • Master: Master gain, limiter indicator, VU meter
  • Input selection: Browser device picker (MediaDevices API), virtual audio cables supported
  • Noise suppression / echo cancellation / auto-gain toggles per mic input (WebRTC AudioWorklet)

Live Dashboard (Right Sidebar)

Stream Controls (Bottom Bar)

ControlActionShortcut
Start/StopGo live / end broadcastSpace
Push-to-TalkMomentary mic activationHold Ctrl
Cough ButtonTemporary mute (3s)C
Mute AllKill all sources instantlyM
AnnounceSend pre-recorded/Typed announcement to chatA
Clip Last 30sSave highlight locallyShift+C

3. Chat Moderation (/station/moderate)

Dedicated moderation view (can be popped out to second monitor):

  • Message queue — Real-time feed with auto-scroll, pause on hover
  • Bulk actions — Multi-select → timeout/ban/delete/pin
  • User cards — Click username → history, warnings, notes, ban record
  • Automod rules — Regex blocks, link filtering, caps/spam thresholds, slow mode
  • Moderator panel — Add/remove mods, assign roles, mod-only chat
  • Overlay alerts — Configure follower/donation/raid/sub alerts (webhook endpoints)

4. Recordings & Clips (/station/library)

Post-stream asset management:

  • Full recordings — Auto-saved (MediaRecorder → WebM/Opus), indexed by stream
  • Auto-clips — Detect silence breaks, high chat velocity, manual markers → suggested clips
  • Manual clips — "Clip that" button marks last 15/30/60s
  • Export — Download WebM, transcode to MP3 (WASM ffmpeg), upload to IPFS/S3
  • Metadata editor — Title, description, chapters, timestamps, tags

Broadcast Console (implemented)

The studio ships with a working broadcast console (src/mixer/) that puts the SOURCES → CHANNELS → PROCESSING → BUSES → OUTPUTS model in the browser. The console is shared VERBATIM with the desktop IFM Station app (packages/ifm-station/src/mixer/) — this demo FOLLOWS the desktop implementation, always, never the contrary (Architectural Invariant 9): the desktop app is the source of truth for shared UI, and this example mirrors it byte-for-byte; when a shared file changes, the change lands in the desktop app first and is copied here. The audio architecture is the modular pipeline from docs/audio/README.md: an AudioSource abstraction (sources.ts: MicSource / TabSource / DesktopSource / FileSource) feeds the mixer, and every MediaRecorder goes through the independent record component (recorder.ts). The only app-specific seam is where the finished mix is saved — the desktop app uses the native save dialog, this demo downloads the mix:

  • Unlimited channels+ Add channel opens a source picker that reflects THIS device: it enumerates your real microphones/interfaces (each becomes a strip bound to that input), offers browser-tab capture and desktop/system audio (both via the system getDisplayMedia picker — enable “share tab audio” / “entire screen with audio”), an uploaded audio file (looped), or an empty strip. A mic strip is live immediately, a capture strip opens the system picker right away. Every strip has a name, icon, accent color, input kind and a horizontal volume fader; they persist per operator (localStorage)
  • Inputs — enumerated microphones & interfaces (getUserMedia + enumerateDevices, device-bound), window audio and desktop audio (getDisplayMedia), audio file (uploaded, looped, with strip transport — play/pause · stop · speed), or no input. The picker also lists your audio outputs so you can choose which speakers/headphones the console monitors through (AudioContext.setSinkId; broadcast is unaffected). Honest limits: a web page cannot enumerate which apps are currently playing audio — the desktop source is “share the entire screen with audio”, and per-application capture is native-layer only
  • Power switch + console interaction — all sources run together; a single click on a channel disables/enables that source (a power state, silent everywhere when off), and a double-click opens the channel's detail panel as a modal (Escape / ✕ / backdrop to close). The modal holds the input, processing, state, output matrix and MIDI mapping sections
  • Per-channel processing — fader (tapered volume), trim gain (±12 dB), pan, 3-band EQ (low shelf / peak / high shelf), compressor (DynamicsCompressor, dry/wet bypass), mute, solo, PFL (pre-fader cue → monitor), and an explicit ON AIR / STANDBY broadcast state
  • Create-on-success — a channel strip appears only once its source is really attached: cancelling the mic permission prompt or the tab/screen picker leaves no leftover slot source
  • Output matrix — each channel routes to IFM Broadcast, Monitor (local speakers) and Record independently; PFL taps pre-fader into Monitor
  • Master — broadcast + monitor levels with live meters (AnalyserNode RMS), and a Record bus recorder (downloadable WebM mix via MediaRecorderRecorder, the single place the station touches MediaRecorder; a slow or failing recorder never stalls the live graph)
  • ON AIR = the master bus — while the studio is open, the broadcast bus is continuously recorded (MediaRecorder, Opus/WebM) and published on the station frequency as voice packets: whatever is mixed is what listeners hear
  • Scenes — save / load / delete the full mixer state (strips, EQ, compressor, routing, on-air states, master); PFL is a live cue and never recalled
  • Soundboard — playable sound sources into broadcast + monitor with play / pause / resume / stop (a paused clip resumes where it left off; stop restarts it) and a duration readout (two built-in synthesized samples + uploaded clips)
  • MIDI control layer — Web MIDI (Chrome/Edge): per-parameter LEARN (move a knob/button → message detected → Assign), CC → continuous / Note → toggle mappings persisted per operator, and feedback echoed to output devices. MIDI never touches the audio graph — it moves the same parameters the UI does

Browser-real limits: a web page cannot enumerate running applications (so there is no “show apps that are playing” list — the desktop source captures the whole system output instead), and capturing individual applications requires the native layer; the console is the web implementation of that model (network/remote-station channels arrive with the native IFM transport).


Audio Pipeline (Browser)

Key implementation details:

  • AudioWorklet (not ScriptProcessorNode) — runs on audio thread, zero main-thread jitter
  • Opus encoding in WASM@ifm/opus-wasm compiles libopus to WASM, runs in AudioWorklet
  • Adaptive bitrate — Monitor peer bandwidth via REMB/TWCC, adjust encoder bitrate dynamically
  • FEC + RED — Forward error correction for lossy mesh paths
  • Jitter buffer on receive side — Listener handles de-jitter; sender just encodes + forwards

Transport Layer

The web app is an interface into the mesh: it connects over WebSocket to a relay hub, which is a full libp2p node on the same network the desktop apps join. The browser never dials native transports directly — the relay is the bridge.

ModeUse CaseImplementation
WebSocketBrowser ↔ Relay hub (the web's path into the mesh)@ifm/sdk JSON protocol over WS to the hub
QUIC (native)Desktop ↔ Mesh (first-class)crates/transport libp2p QUIC, via the desktop apps
WebTransportFuture: lower latency, DATAGRAMWhen browser support matures

Connection flow:

  1. Station loads → initializes @ifm/sdk (WebSocket)
  2. SDK connects to the relay hub (the same mesh desktop nodes join)
  3. On join: announce frequency, start broadcasting
  4. The hub fans out to the native mesh and to other browser sessions

Project Structure

packages/sdk/examples/station/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.js
├── public/
│   ├── wasm/                    # @ifm/sdk + @ifm/opus-wasm
│   └── icons/
├── src/
│   ├── main.tsx                 # App entry + providers
│   ├── App.tsx                  # Router: /setup, /studio, /moderate, /library
│   ├── styles/globals.css
│   ├── components/
│   │   ├── ui/                  # Radix primitives (Button, Slider, Dialog, ...)
│   │   ├── layout/              # Header, Sidebar, BottomBar, PopoutWindow
│   │   ├── studio/
│   │   │   ├── SourceMixer.tsx      # Per-source gain/mute/PTT
│   │   │   ├── MasterBus.tsx        # Master gain, limiter, VU
│   │   │   ├── DevicePicker.tsx     # MediaDevices selector
│   │   │   ├── LiveDashboard.tsx    # Viewers, peers, chat stats
│   │   │   ├── StreamControls.tsx   # Start/Stop, PTT, Cough, Clip
│   │   │   └── AnnouncementBar.tsx  # Typed/pre-recorded announces
│   │   ├── moderation/
│   │   │   ├── MessageQueue.tsx
│   │   │   ├── UserCard.tsx
│   │   │   ├── AutomodRules.tsx
│   │   │   ├── ModeratorPanel.tsx
│   │   │   └── AlertConfig.tsx
│   │   └── library/
│   │       ├── RecordingList.tsx
│   │       ├── ClipEditor.tsx
│   │       └── ExportDialog.tsx
│   ├── pages/
│   │   ├── Setup.tsx
│   │   ├── Studio.tsx
│   │   ├── Moderate.tsx
│   │   └── Library.tsx
│   ├── hooks/
│   │   ├── useStudio.ts           # Audio graph lifecycle
│   │   ├── useAudioCapture.ts     # getUserMedia + constraints
│   │   ├── useOpusEncoder.ts      # AudioWorklet encoder bridge
│   │   ├── useStream.ts           # IFM broadcast/subscribe
│   │   ├── usePeerHealth.ts       # Peer connection quality
│   │   ├── useChatMod.ts          # Moderation actions
│   │   └── useRecordings.ts       # MediaRecorder management
│   ├── services/
│   │   ├── ifm.ts                 # SDK wrapper (connect, tune, broadcast)
│   │   ├── webrtc.ts              # WebRTC signaling, ICE
│   │   ├── audioWorklet.ts        # Worklet registration, messaging
│   │   ├── recording.ts           # MediaRecorder + IndexedDB
│   │   └── notifications.ts       # Web Push API
│   ├── stores/
│   │   ├── studioStore.ts         # Sources, master, stream state
│   │   ├── moderationStore.ts     # Messages, users, rules
│   │   └── libraryStore.ts        # Recordings, clips, metadata
│   └── types/
│       ├── studio.ts
│       ├── moderation.ts
│       └── audio.ts

Key User Flows

1. First-Time Broadcaster

2. Scheduled Broadcast

3. Multi-Source Production


Responsive Breakpoints

BreakpointWidthLayout
Mobile< 640pxStacked: Setup only (Studio needs desktop); view-only dashboard
Tablet640–1024pxStudio compressed: mixer collapsible, dashboard sidebar overlay
Desktop1024–1440pxFull layout: 3-col (Mixer
Wide> 1440px4-col: add Moderation panel; popout windows for 2nd monitor

Note: Station is desktop-first. Mobile shows read-only dashboard + chat moderation. Full studio requires desktop for audio device access and screen real estate.


Accessibility

  • Keyboard navigation — All controls reachable, logical tab order, visible focus rings
  • Screen readers — ARIA labels on all controls, live regions for viewer count/alerts
  • High contrast — Theme supports WCAG AA; colorblind-safe palette
  • Reduced motion — Respects prefers-reduced-motion; disables visualizer animations
  • Audio ducking — Screen reader announcement ducks stream audio momentarily

Configuration

toml
# station.config.toml (optional, loaded from public/config.toml or localStorage)

[station]
default_frequency = "music.general"
auto_connect = true
recording_enabled = true
recording_format = "webm"  # "webm" | "mp3" (via WASM ffmpeg)

[audio]
sample_rate = 48000
frame_size = 960           # 20ms @ 48kHz
opus_bitrate_kbps = 128    # adaptive range: 64-256
opus_complexity = 10       # 0-10
fec = true
dtx = false                # discontinuous transmission

[webrtc]
ice_servers = [
  { urls = "stun:stun.l.google.com:19302" },
  { urls = "turn:turn.example.com", username = "...", credential = "..." }
]
signaling_url = "wss://signaling.ifm.app"

[mesh]
fallback_ws_url = "wss://relay.ifm.app/ifm"

[moderation]
slow_mode_default_ms = 5000
automod_links = true
automod_invites = true
max_message_length = 4000

[alerts]
webhook_url = ""
events = ["follow", "donation", "raid", "sub"]

Dependencies (Key)

PackagePurpose
react, react-domUI framework
react-router-domRouting
@tanstack/react-queryServer state
zustandClient state
@radix-ui/*Accessible primitives
tailwindcssStyling
@ifm/sdkIFM protocol (WASM)
@ifm/opus-wasmOpus encode/decode (WASM)
viteBuild tool
vitest, playwrightTesting

Implementation Phases

Phase 1: Foundation

  • [ ] Vite + React + TypeScript + Tailwind scaffold
  • [ ] @ifm/sdk WASM integration (connect, tune, broadcast)
  • [ ] Basic routing: Setup → Studio → Library
  • [ ] AudioWorklet scaffold (passthrough → Opus encoder)

Phase 2: Studio Core

  • [ ] Source mixer (mic, screen, app, file) with gain/mute
  • [ ] Opus encoder in AudioWorklet (@ifm/opus-wasm)
  • [ ] Live broadcast → IFM mesh
  • [ ] Live dashboard (viewer count, peer health)

Phase 3: Production Features

  • [ ] Push-to-talk, cough button, mute all
  • [ ] Stream scheduling, simulcast
  • [ ] Recording (MediaRecorder) + library
  • [ ] Clip editor + export

Phase 4: Moderation & Polish

  • [ ] Chat moderation panel
  • [ ] Automod rules
  • [ ] Alert overlays (webhooks)
  • [ ] Themes, keyboard shortcuts, accessibility audit
  • [ ] Popout windows for 2nd monitor

Phase 5: Advanced

  • [ ] Adaptive bitrate (REMB/TWCC)
  • [ ] Multi-frequency broadcast
  • [ ] Guest invite (WebRTC join link)
  • [ ] Restream to RTMP/SRT (via native relay)
  • [ ] Monetization hooks (donations, subs)

  • Relay Dashboard — Operator view of the mesh (Station broadcasts appear here)
  • Listener App — End-user consumption (Station content consumed here)
  • SDK API — Core protocol library
  • Node UI — Native node operator terminal

Released under the MIT License.