This project is an RTSP audio streamer for ESP32 boards built as an ESPHome external component. It is designed to pair with cheap IΒ²S MEMS microphones like the INMP441. The stream is standard RTSP/RTP, so anything that speaks RTSP can consume it. (VLC, ffmpeg, Frigate, BirdNET-Go, and most NVRs). Designed for local streaming on a LAN, not the public internet.
Microphone audio streaming from anywhere there is WiFi using a tiny footprint and even tinier budget. Built so my wife can identify birds in the garden with BirdNET-Go.
See CHANGELOG.md for release notes and the configuration reference for all available options.
- Stream microphone audio off an ESP32 over standard RTSP.
- Simple: single stream, fixed 32 kHz / mono / 16-bit audio.
- Pair cleanly with ESPHome's mic stack β works with any
microphone:platform that delivers 32 kHz mono 16-bit (e.g.i2s_audio). - Lightweight: Work on most ESP32s
Protocols:
- RTP over UDP (
RTP/AVP) β the classic transport. Works with VLC and most media players. - RTP over TCP (
RTP/AVP/TCP, interleaved) β RTP framed on the RTSP connection. Works with FFmpeg, BirdNET-Go, Frigate, and most NVRs. - Uncompressed L16 PCM audio β 32 kHz mono 16-bit, RTP payload type 96 (
L16/32000/1). See Audio format.
Audio processing:
-
Audio cleanup chain β an always-on DC blocker (kills the MEMS capsule's DC bias before it eats headroom) plus complementary user-tunable 2nd-order Butterworth (biquad) low-cut and high-cut filters from Home Assistant β β12 dB/oct rolloff, maximally-flat passband. Low-cut on by default at 100 Hz; high-cut off by default. See docs/configuration.md#cut-filters.
Bypass conventions: drag the low-cut slider to 0 Hz to disable the low-cut entirely; drag the high-cut slider to 16 kHz (its max, Nyquist for 32 kHz audio) to disable the high-cut. With both off the chain is bit-identical to a build without those features (DC blocker always runs).
The β3 dB point is the half-power point β the universally agreed convention for defining a filter's cutoff frequency. See Why is the cutoff frequency at β3 dB? for more.
-
Audio gain in dB, tunable from Home Assistant β software level adjustment applied after the cut filters. Default 0 dB (unity, bit-identical fast path), range β20 to +40 dB, persisted across reboots. Saturating clamp on overflow, never wraps. See docs/configuration.md#audio-gain.
Diagnostics and metrics:
- Diagnostic sensors for Home Assistant β opt-in
binary_sensor/text_sensor/sensorplatforms expose client-connected state, client IP, and bytes sent, so you can debug the stream from HA without tailing logs. See docs/configuration.md#diagnostic-sensors. - CPU-use sensor β opt-in
cpu_use_pctreports how much of wall-clock the audio path is consuming, so you can tell whether your board has headroom. See docs/configuration.md#diagnostic-sensors.
Other nice touches:
- Idle nodes carry zero audio buffer overhead. When no one's listening, no audio buffers exist. They're allocated on
PLAYand freed onTEARDOWN/ Wi-Fi loss / peer close. A node that's "ready to stream" 99 % of the time doesn't burn 64 KB of RAM doing nothing, which lets the component share a chip withvoice_assistant, displays, wake word, etc. without crowding them out. - Self-recovery on Wi-Fi drop. The active session is torn down cleanly and the RTSP listener stays up ready for the next client.
- Session inactivity timeout. A client that vanishes without sending
TEARDOWN(due to crash, sleep, peer Wi-Fi drop) is reaped after 60 s so the single client slot frees up, ready for the next connection. - OOM-safe RTP send path. The transmit buffer is reserved up front so no-PSRAM boards do not run out of memory mid-stream.
- Only a single client at a time. Only one RTSP client is served at a time. A second TCP connection is closed immediately (no RTSP-level response) and the device logs
Reject second RTSP client. A single ring buffer drains into a single transport. Lifting this would mean per-client packet pacing, SSRC, and sequence numbering. Keep it simple. - No sample-rate conversion. The microphone source is fixed at 32 kHz mono 16-bit so PCM passes straight into RTP with no resampler pulled in. Keep it efficient.
- No Arduino framework support. The component is gated to ESP-IDF. ESPHome itself is moving to ESP-IDF as the default and Arduino as the legacy path. We follow that direction rather than carry a second build configuration. Keep it targeted.
- IPv4 only for UDP. TCP-interleaved RTP works on IPv4 and IPv6, but the UDP media path is IPv4-only. A UDP
SETUPfrom an IPv6 client is rejected. If your network is IPv6-only, use TCP transport.
This assumes you are comfortable with ESP boards and ESPHome. If not, now is a good time to learn!
-
Wire up an IΒ²S microphone to an ESP32. Defaults assume an INMP441 on an ESP32-S2. The example YAML uses
GPIO11(WS),GPIO9(SCK),GPIO12(SD) but change this as needed. Wire 3.3 V power and ground, then those three signals.INMP441 wiring + what each pin does
INMP441 ESP32 GPIO (example YAML) VDD 3.3 V GND GND WS GPIO11 ( i2s_lrclk_pin)SCK GPIO9 ( i2s_bclk_pin)SD GPIO12 ( i2s_din_pin)L/R GND or VDD (selects channel) - L/R β Channel-select. Tie to GND for the left slot or VDD for the right; the
channel:value in YAML must match. - WS β Word Select (LRCLK). Timing when the left or right channel data is being sent.
- SCK β Serial Clock (BCLK). Clocks bits out of the mic. Frequency =
sample_rate Γ bits_per_sample Γ channels. - SD β Serial Data. The mic's audio output. The ESP32 reads this on
i2s_din_pin. - MCLK β Not used by the INMP441. Leave
i2s_mclk_pinunset in YAML.
WS and SCK are outputs from the ESP32 (it's the IΒ²S master). SD is the mic's only output.
- L/R β Channel-select. Tie to GND for the left slot or VDD for the right; the
-
Grab the example YAML. Copy
example_rtsp_audio.yamlinto your ESPHome config directory as e.g.rtsp-audio.yaml. The example already pulls this component from GitHub viaexternal_components(referencing the latest released tag), so there's nothing to install separately.Edit two things before flashing:
- Your Wi-Fi credentials (or set up
secrets.yaml). - The three IΒ²S GPIO pins, if your wiring differs from the defaults.
- Your Wi-Fi credentials (or set up
-
Validate, compile, flash.
Compile and flash like you would any other ESPHome device.
Via web interface or
esphome config rtsp-audio.yaml # Validate esphome compile rtsp-audio.yaml # Compile esphome run rtsp-audio.yaml # Install (USB or OTA)
-
Listen. Point an RTSP client such as VLC at the device's IP. Any path the client asks for is accepted, so
rtsp://<node-ip>:554/works just as well asrtsp://<node-ip>:554/audio.Find
<node-ip>in the ESPHome dashboard, your router's DHCP table, or by using the device's mDNS hostname βrtsp://<esphome-name>.local:554/works on most networks. Confirm the server is up by looking forRTSP listening on port 554 (L16/32000/1, PT 96)in the ESPHome logs, or by enabling the client-connected diagnostic sensor and watching it flip in Home Assistant.-
VLC (GUI) β File β Open Networkβ¦ and enter
rtsp://<node-ip>:554/. -
ffplay (CLI) β TCP transport is the more reliable default; drop the
-rtsp_transportflag to use UDP.ffplay -rtsp_transport tcp rtsp://<node-ip>:554/
-
Transport (UDP or TCP-interleaved) is negotiated per client at SETUP β VLC defaults to UDP, FFmpeg / Frigate / BirdNET-Go / most NVRs default to TCP. Both work, no configuration needed.
Component not found: rtsp_audioβ theexternal_componentsblock didn't resolve. Double-check theurl:andref:values in your YAML, and that the build host can reach GitHub.Component requires framework esp-idfβ setesp32: framework: type: esp-idf. The component intentionally rejects Arduino (see Intentional non-goals).Microphone sample rate must be 32000β setsample_rate: 32000on themicrophone:block. The constraint is enforced byfinal_validate_microphone_source_schema.
The component is built and verified on both esp32-s2-idf and
esp32-s3-idf. A dual-core ESP32 is recommended, e.g. the ESP32-S3, but the ESP32-S2 that I'm testing on works surprisingly well.
On single-core chips the Wi-Fi stack and the audio loop share one core, so you'll see occasional brief (~1 s) audio gaps when Wi-Fi gets busy. A dual-core chip puts Wi-Fi and the audio loop on separate cores and avoids them.
- Works for the current implementation (32 kHz sample rate, mono,
20 ms packets, single viewer). RAM is the tight resource:
~10β11 % of internal RAM is used while idle, plus ~64 kB ring buffer
- ~1.3 kB RTP packet during an active stream.
- No PSRAM on most S2 boards β buffers fall back to internal heap. Verified to fit, but there's less headroom for additional audio / voice components on the same node.
- Single Xtensa core β the Wi-Fi stack and the audio loop share one core, so expect occasional brief (~1 s) audio gaps when Wi-Fi is busy (e.g. a roam scan). They are harmless and self-recovering, but a dual-core S3 avoids them. No margin for adding wake-word / voice_assistant either.
- Best for: dedicated "RTSP mic" nodes.
- Recommended when reliability or coexistence with other ESPHome audio components matters.
- PSRAM lands the ring buffer + RTP packet buffer in external RAM automatically (no YAML changes needed). Internal RAM is freed up for the rest of the firmware.
- Dual core β
loop()and the IΒ²S DMA pipeline can sit on different cores via ESP-IDF's task affinity, giving consistent jitter even under load. - Best for: nodes that combine RTSP audio with
voice_assistant, wake word, displays, etc.
- Original ESP32 (Xtensa LX6): should also work for the MVP shape
but isn't part of the build matrix today. If you have one, run the
S2 test config with
board:swapped β the code path is the same. - ESP32-C3 / C6 (RISC-V): untested. There's nothing intentionally Xtensa-only in the code, but the audio components targeted by this matrix all assume Xtensa today.
Enable the cpu_use_pct diagnostic sensor (see docs/configuration.md#diagnostic-sensors) to confirm whether your chosen board has enough headroom for your configuration before committing to it β the recommended way to decide between staying on what you have, upgrading to an ESP32-S3, or disabling features.
- CI builds the component for both
esp32-s2-idfandesp32-s3-idfon every change (seetests/components/rtsp_audio/). - Hardware-verified on a LOLIN S2 Mini (ESP32-S2) with an INMP441 MEMS microphone module.
The mic source is set to L16 PCM, 32 kHz sample rate, mono, 16-bit. The format is fixed β no resampler is pulled in, so PCM flows straight from the mic into RTP.
- Usable audio bandwidth β the sample rate is how many times per second the mic is measured; the highest audio frequency that can be faithfully captured is half of that, per the NyquistβShannon sampling theorem (you need at least two samples per cycle to reconstruct a wave). So at a 32 kHz sample rate we can capture audio content up to 16 kHz. This is currently not configurable.
- Why 32 kHz sample rate, not higher or lower β the MEMS mics this targets are not high-fidelity capsules and don't faithfully reproduce content above ~16 kHz, so going higher (e.g. 48 kHz, giving 24 kHz of audio bandwidth) would just spend CPU and Wi-Fi on headroom the mic can't fill. Going lower (e.g. 16 kHz sample rate β 8 kHz audio bandwidth) would lose the upper octave of voice sibilance and most bird-call detail.
- CPU cost β On an ESP32-S3 the audio path still sits well under the comfort budget described in the CPU-use sensor section above.
- Wi-Fi / packet size β RTP packets are ~1.3 KB at the default 20 ms packet duration, still well under MTU. Both UDP and TCP transports are unaffected.
We want this to "just work" on any ESPHome-capable ESP32 β including the cheap WROOM boards everyone has in a drawer β so the buffer is sized for the no-PSRAM case. The RTSP client adds its own jitter buffer (VLC, ffmpeg, NVRs all hold ~500 msβ1 s before playback starts), so end-to-end resilience is closer to 1.5β2 s.
The jitter ring buffer is sized for 1 second of audio (~64 KB at 32 kHz mono 16-bit). That's a deliberate compromise: 2 s would absorb longer Wi-Fi stalls, but it would also push the buffer past what a bare ESP32 can hand out as a single contiguous chunk of internal RAM.
The ESP32 family is split on PSRAM:
- No PSRAM β bare
ESP32-WROOM, mostESP32-S3-WROOM-1SKUs without anRin the part number, and all currentESP32-C3/-C6/-H2parts (the RISC-V chips don't even support PSRAM). These boards have ~320 KB of internal SRAM total, most of it claimed by Wi-Fi, LwIP, and the ESPHome runtime β a 128 KB contiguous allocation routinely fails on them. - With PSRAM β
WROVERvariants,ESP32-S3-WROOM-1-Nβ¦R8/-R2, and PSRAM-equipped S2 modules (e.g. LOLIN S2 Mini). 2 MB or 8 MB external RAM is plenty.
A future enhancement would be to detect PSRAM at runtime and grow the ring buffer to 2 s when available, giving PSRAM boards the extra safety margin without breaking the bare-WROOM path.
All tests and firmware builds run inside Docker β only docker is required on the host. Use the Makefile:
make help # list targets
make config # esphome config on each test YAML
make compile # full firmware build for each test YAML
make compile BOARD=s3-idf
make test # host C++ unit tests (gtest, ctest) in Docker
make lint # pre-commit hooks (clang-format, ruff, ...)
YAML smoke configs live under tests/components/rtsp_audio/ for both esp32-s2-idf and esp32-s3-idf.
See CONTRIBUTING.md for more details.
