Skip to main content

ADR-0006: Transmitter Audio Uplink Uses a Dedicated WebSocket, Not MQTT

Status

Accepted

Context

A transmitter can capture live audio on its amplifier/mic input and needs to send it back to the backend to be fanned out as a live broadcast. A transmitter already holds an open MQTT session for status/heartbeat/config/ cmd traffic, which raised the question of whether audio should ride that same channel.

Two things ruled that out:

  • EMQX's per-device rate and message-size limits (emqx/emqx.conf.template) are sized for chatty status/heartbeat/cmd traffic, not sustained audio.
  • The one precedent for audio-over-MQTT in this codebase — the admin panel's own mic mirrored as base64 JSON chunks on the now-retired mosque-wide trigger topic — was unauthenticated, and was judged not to be a pattern worth extending.

Decision

Audio uplink uses a dedicated, authenticated WebSocket (wss://host/ws/transmitter-broadcast), the same shape the admin panel's own live broadcast connection already uses, reusing the exact same FFmpeg → Icecast pipeline and the exact same started/ended fan-out function (core/live_broadcast.py:notify_icecast_broadcast_started) — not a second implementation of either.

Alternatives Considered

Extending the existing MQTT session to carry base64-encoded audio chunks, following the retired mosque-wide trigger topic's precedent — rejected as both a broker sizing mismatch and a pattern that was already known to be unauthenticated and not worth repeating.

Consequences

  • Audio delivery reuses proven infrastructure (FFmpeg → Icecast, the same fan-out as any other live broadcast) instead of building a second streaming path — a transmitter uplink behaves identically to an admin's own microphone broadcast from the fan-out's perspective, including authorization-gating per recipient.
  • The connection authenticates with the same per-device secret every transmitter already has (devices.device_secret_hash, returned once as broadcast_secret), not a second credential system.
  • The transmitter's own hardware decides when to start (VOX/level-detection or a physical button) and opens the connection itself — there is no MQTT cmd that tells a unit to begin capturing, and no explicit "stop": the receiving devices detect end-of-stream themselves, same as every other live broadcast.
  • The source transmitter must be explicitly excluded from its own fan-out to avoid looping its amplifier output back through itself — an implementation detail directly caused by reusing the generic fan-out path for a device-originated broadcast.