Setting up RTP network audio streaming between PipeWire/PulseAudio systems is surprisingly difficult due to broken or incomplete module implementations. We stepped on all the landmines so you don’t have to… This guide provides a tested, working configuration for streaming audio from a desktop Debian system to a headless Raspbian Raspberry Pi.
TL;DR: PulseAudio’s module-rtp-send (via pipewire-pulse) → GStreamer RTP receiver
Despite being “standard” solutions, PipeWire’s native RTP modules are not production-ready, and PulseAudio’s RTP receiver is unreliable for unicast streams. The combination of PulseAudio’s sender (via pipewire-pulse) and GStreamer’s receiver provides a robust, infrastructure-grade solution for network audio streaming.
Press enter or click to view image in full size
Current State of RTP Support (as of PipeWire 0.3.65)
What Should Work (according to documentation)
PipeWire Native:
- libpipewire-module-rtp-sink and libpipewire-module-rtp-source claim to provide RTP streaming
- Documented in PipeWire Wiki as functional modules
PulseAudio Compatibility:
- module-rtp-send and module-rtp-recv are mature modules from PulseAudio
- Should auto-discover streams via SAP (Session Announcement Protocol)
- Documented as supporting both multicast and unicast
Known Issues (Prior to Testing)
- PipeWire’s RTP modules are marked as experimental in some distributions
- PulseAudio’s module-rtp-recv has long-standing issues with unicast detection
- Format negotiation between sender and receiver can be problematic
Combinations That Don’t Work
1. Native PipeWire RTP Modules
Configuration Attempted:
# Sender
context.modules = [
{ name = libpipewire-module-rtp-sink
args = { destination.ip = "192.168.1.8" ... }}
]# Receiver
context.modules = [
{ name = libpipewire-module-rtp-source
args = { source.port = 46000 ... }}
]Why It Fails:
- libpipewire-module-rtp-sink cannot accept float32le audio from PipeWire’s null-audio-sink
- Returns “Operation not supported” (error -95) when attempting to start
- The null-audio-sink adapter ignores format specifications and always outputs float32le
- RTP modules only accept integer formats (S16BE/S16LE)
2. PulseAudio module-rtp-recv
Configuration Attempted:
pactl load-module module-rtp-recv port=46000Why It Fails:
- Designed for multicast with SAP discovery
- Fails to create audio sources for unicast streams
- Even with multicast, often fails to detect valid RTP streams
- No useful error messages when detection fails
3. Mixed native PipeWire configurations
Configuration Attempted:
- Various combinations of PipeWire loopback modules to bridge format gaps
- WirePlumber routing rules for automatic connections
Why It Fails:
- PipeWire’s RTP modules create nodes with unpredictable names
- Channel mapping issues (numbered outputs instead of FL/FR)
- Complex module chains introduce multiple failure points
The Working Solution
Sender Configuration (desktop with PipeWire/PulseAudio)
Create ~/.config/pipewire/pipewire-pulse.conf.d/rtp.conf:
pulse.cmd = [
{
cmd = "load-module"
args = "module-null-sink sink_name=rtp_sink sink_properties=device.description=RTP_Network_Stream"
}
{
cmd = "load-module"
args = "module-rtp-send source=rtp_sink.monitor destination_ip=192.168.1.8 port=46000"
}
]Restart services:
systemctl --user restart pipewire pipewire-pulseReceiver Configuration (Raspberry Pi)
Install GStreamer:
sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-alsaCreate /etc/systemd/user/rtp-receiver.service:
[Unit]
Description=RTP Audio Receiver
After=sound.target network-online.target[Service]
Type=simple
Restart=always
RestartSec=5
ExecStart=/usr/bin/gst-launch-1.0 udpsrc port=46000 buffer-size=65536 ! \
application/x-rtp,media=audio,payload=127,clock-rate=48000,encoding-name=L16,channels=2 ! \
rtpjitterbuffer latency=200 ! \
rtpL16depay ! \
audioconvert ! \
alsasink device=hw:0,0 buffer-time=200000 latency-time=100000[Install]
WantedBy=default.target
Enable service:
systemctl --user enable --now rtp-receiverCritical configuration details
Why This Works
- PulseAudio’s module-rtp-send handles format conversion: Accepts float32le from the null sink monitor, converts to S16BE for transmission
- GStreamer properly decodes RTP: More robust than PulseAudio’s receiver modules
- Explicit payload type: module-rtp-send uses payload 127, not the commonly assumed 96
- Adequate buffering: Network audio requires larger buffers than local playback
Key Parameters
- Payload type 127: PulseAudio uses dynamic payload types (96–127)
- Buffer sizes: Essential for preventing audio dropouts on Raspberry Pi
- UDP buffer: 65536 bytes
- ALSA buffer-time: 200ms
- RTP jitter buffer: 200ms
- Device specification: Use hw:0,0 for direct ALSA access (adjust based on the specific audio interface on your hardware configuration)
Troubleshooting
Verify audio transmission network traffic
# On receiver
sudo tcpdump -i any -n port 46000 -c 5Check audio format
# Capture and analyze RTP payload
timeout 5 gst-launch-1.0 udpsrc port=46000 ! filesink location=/tmp/rtp.dump
hexdump -C /tmp/rtp.dump | head -20Test GStreamer pipeline
# Local audio test on receiver
gst-launch-1.0 audiotestsrc ! alsasink device=hw:0,0