Use Realtime SFU to connect embedded hardware to browser listeners. Pocket Radio shows how firmware connects over WebRTC, publishes audio and telemetry, and receives authorized control commands from a browser.
Pocket Radio runs on an ESP32-S3. It reads a playlist from flash, publishes Opus audio, and sends spectrum data and device state to browsers. One listener can control playback and the board's LED.
The example is experimental and targets the documented board and toolchain. It does not implement microphone capture, device speaker output, a camera, or a complete robotics stack.
Open the example
Follow the firmware walkthrough
The firmware and each browser maintain a separate WebRTC peer. A Worker holds the SFU credentials and handles signaling. A Durable Object tracks the device, listeners, controller lease, and cleanup.
robot + spectrum
controller only
offer, answer, IDs, liveness
sessions, tracks, channels
join, SDP, control lease
The example uses these paths:
| Path | Purpose |
|---|---|
| Device audio track to browser listeners | Publish pre-encoded Opus audio for playback |
Reliable, ordered robot DataChannel |
Send telemetry, metadata, commands, and command acknowledgments |
Unordered spectrum DataChannel with zero retransmissions |
Send replaceable spectrum samples |
Controller replies on robot |
Return authorized browser commands to the device using canReply |
| Device and browser signaling to the backend | Authenticate, allocate SFU resources, exchange Session Description Protocol (SDP) offers and answers, and manage application state |
SFU App Secrets stay on the Worker. The firmware receives its device credential, Wi-Fi configuration, and signaling origin. This example uses a Worker as its backend. Other application servers can use the same SFU interfaces.
The firmware/SFU walkthrough ↗ explains the protocol sequence and identifies the code responsible for each step:
- Create and configure the firmware's WebRTC peer.
- Exchange a session description through the application backend.
- Publish the audio track and establish negotiated DataChannels.
- Connect a browser listener and acknowledge DataChannel readiness.
- Grant the controller reply access and handle device commands.
- Retain resource identifiers for teardown and retries.
Keep one owner for the WebRTC peer and its SDP state. Adapt hardware I/O and media timing for your device. You can replace HTTPS device-to-backend signaling with another application protocol, such as Constrained Application Protocol (CoAP). The backend must still call the SFU over HTTPS.
The example targets the ESP32-S3-DevKitC-1 N32R16V, with 32 MiB flash, 16 MiB PSRAM, and the documented LED pin. To build it, you need a Linux x86_64 toolchain, Node.js 24, Python, Rust, and ESP-IDF. The board requires outbound IPv4 UDP and HTTPS. Refer to the complete prerequisites ↗ for exact tool versions and hardware requirements.
-
Prepare the checkout. Run:
git clone https://github.com/cloudflare/realtime-examples.git cd realtime-examples/esp32-radio make setup cp .credential.env.example .credential.env -
Configure and deploy the backend. Create an SFU app. Follow the example's setup procedure ↗ to provision credentials, configure your hostname and account, and deploy the Worker. Keep
.credential.envprivate. -
Prepare the device. Follow Back up and flash ↗. Use audio you have permission to distribute. Back up the board before replacing its software. Flashing resets the device.
-
Listen and control. Open the Worker URL, sign in with the configured viewer password, and select Start listening. Open another listener tab. Both tabs should receive audio, spectrum, and telemetry.
In one tab, select Take control, then change the LED or pause playback. The other tab keeps listening. Select Release control before transferring control to another listener.
Browsers rejoin manually after a board restart or terminal connection failure. When the board is online, select Start listening again. The example uses a shared viewer password for application authentication. For a deployment that serves distinct users, per-user identity and abuse controls remain part of the integration.
The example allows eight listeners per board and one controller. The application sets the eight-listener cap. It does not define the SFU subscriber limit.
To stop, release control and disconnect listeners, then power off the board. Keep the backend available for as long as required cleanup remains pending. The example's operations guide ↗ explains publisher replacement, state expiry, and resource cleanup before removing the Worker or SFU app.
For a shared camera or microscope monitor, keep the observer and controller model, then add camera capture, supported video encoding, and browser video playback. Use hardware or a gateway capable of supplying that video.
For a sensor dashboard, replace spectrum samples with your measurements. Add sensor capture, units, timestamps, and behavior for stale readings.
Apply the observer/operator model to a robot gateway using these substitutions:
| Radio component | Robotics adaptation |
|---|---|
| Playlist audio source | Camera, microphone, or other supported media source |
| Device telemetry and spectrum samples | Sensor state, diagnostics, and replaceable measurements |
| Playback and LED commands | Validated commands to a robot's local control interface |
| Listener membership and controller lease | Observer and operator roles for the selected robot |
| Firmware platform layer | The robot's hardware or Robot Operating System (ROS) integration |
Implement camera capture, ROS integration, and actuator behavior separately. Include command expiry, stale-command rejection, and local watchdog or stopping behavior in that integration. Emergency stopping and actuator limits belong on the device or its local controller.
For a related implementation with a native video publisher, the cloud-gaming example uses separate channels for reliable and transient input.
Inspect the architecture guide ↗ for authentication, controller ownership, and reconnect behavior.
For a smaller browser-only exercise, run the DataChannel example.