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:
| Section | Fields |
|---|---|
| Frequency | Select existing or create new (name, namespace, channel); access type: public/protected/hidden |
| Metadata | Title, description, tags (genre, language, region), cover art upload |
| Schedule | Go live now / schedule for later; recurrence rules; auto-end timer |
| Simulcast | Add secondary frequencies (cross-post audio + chat) |
| Monetization Hooks | Donation 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)
| Control | Action | Shortcut |
|---|---|---|
| Start/Stop | Go live / end broadcast | Space |
| Push-to-Talk | Momentary mic activation | Hold Ctrl |
| Cough Button | Temporary mute (3s) | C |
| Mute All | Kill all sources instantly | M |
| Announce | Send pre-recorded/Typed announcement to chat | A |
| Clip Last 30s | Save highlight locally | Shift+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 channelopens 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 systemgetDisplayMediapicker — 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 touchesMediaRecorder; 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
voicepackets: 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-wasmcompileslibopusto 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.
| Mode | Use Case | Implementation |
|---|---|---|
| WebSocket | Browser ↔ 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 |
| WebTransport | Future: lower latency, DATAGRAM | When browser support matures |
Connection flow:
- Station loads → initializes
@ifm/sdk(WebSocket) - SDK connects to the relay hub (the same mesh desktop nodes join)
- On join: announce frequency, start broadcasting
- 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.tsKey User Flows
1. First-Time Broadcaster
2. Scheduled Broadcast
3. Multi-Source Production
Responsive Breakpoints
| Breakpoint | Width | Layout |
|---|---|---|
| Mobile | < 640px | Stacked: Setup only (Studio needs desktop); view-only dashboard |
| Tablet | 640–1024px | Studio compressed: mixer collapsible, dashboard sidebar overlay |
| Desktop | 1024–1440px | Full layout: 3-col (Mixer |
| Wide | > 1440px | 4-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
# 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)
| Package | Purpose |
|---|---|
react, react-dom | UI framework |
react-router-dom | Routing |
@tanstack/react-query | Server state |
zustand | Client state |
@radix-ui/* | Accessible primitives |
tailwindcss | Styling |
@ifm/sdk | IFM protocol (WASM) |
@ifm/opus-wasm | Opus encode/decode (WASM) |
vite | Build tool |
vitest, playwright | Testing |
Implementation Phases
Phase 1: Foundation
- [ ] Vite + React + TypeScript + Tailwind scaffold
- [ ]
@ifm/sdkWASM 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)
Related Docs
- 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