A high-performance RTP media routing server in Rust, loosely inspired by rtpengine.
rtpbridge sits between VoIP endpoints, routing audio between them with support for WebRTC, plain RTP/SRTP, codec transcoding, DTMF, recording, voice activity detection, and file playback. It is controlled via a WebSocket JSON interface where each connection maps 1:1 to a media session.
- WebRTC endpoints via str0m (DTLS, ICE lite, SRTP)
- Plain RTP/SRTP endpoints with SDES key exchange
- Codec transcoding between PCMU (G.711), G.722, and Opus
- DTMF detection, forwarding, and injection (RFC 4733 telephone-event)
- PCAP recording of decrypted media (RTP + RTCP) with accurate timestamps
- Voice Activity Detection via earshot (independent of recording)
- File playback from local files or URLs (WAV, MP3, OGG, FLAC) with seek, pause, loop
- Cross-session endpoint transfer — move endpoints between sessions with zero overt media disruption (
endpoint.transfer) - Session bridging — bidirectional audio bridge between sessions using decoded PCM (L16 at 48kHz) with no lossy re-encoding at the bridge boundary (
session.bridge) - Session management with orphan timeout, reconnection (
session.attach), and empty session auto-destroy - Graceful shutdown with configurable drain wait (k8s compatible)
- Split interface binding for separate control and media networks
- Native TLS/WSS control listener on the existing control port, with optional HMAC authorization for privileged control operations
- Symmetric RTP with first-packet tuple latching for NAT traversal
# Requires Rust 1.94+, libopus headers, a C compiler, make, Perl and curl
sh scripts/build-openssl.sh target/openssl
export OPENSSL_DIR="$PWD/target/openssl" OPENSSL_STATIC=1
cargo build --release# Default: WebSocket on 127.0.0.1:9100, media on 127.0.0.1
./target/release/rtpbridge --listen 127.0.0.1:9100 --media-ip 203.0.113.5
# With config file
./target/release/rtpbridge --config rtpbridge.tomlNon-loopback control listeners require TLS and HMAC configuration. A trusted TLS proxy may use allow_plaintext_control = true on its protected upstream; unauthenticated remote control requires the separate explicit development exception allow_unauthenticated_control = true. The HMAC key authorizes all sessions.
Connect a WebSocket client and send JSON messages:
# Using websocat
websocat ws://localhost:9100{"id":"1","method":"session.create","params":{}}| Argument | Default | Description |
|---|---|---|
--listen |
127.0.0.1:9100 |
WebSocket/HTTP control plane address(es), comma-separated |
--media-ip |
127.0.0.1 |
IP for all media sockets (RTP/WebRTC) |
--config |
— | Path to TOML config file |
--log-level |
info |
Log level (trace/debug/info/warn/error) |
listen = "127.0.0.1:9100"
media_ip = "203.0.113.5" # or "203.0.113.5, 2001:db8::5" for dual-stack
rtp_port_range = [30000, 39999]
disconnect_timeout_secs = 30
shutdown_max_wait_secs = 300
max_sessions = 10000
max_endpoints_per_session = 20
max_recordings_per_session = 100
session_idle_timeout_secs = 0 # 0 = disabled
recording_dir = "/var/lib/rtpbridge/recordings"
media_dir = "/var/lib/rtpbridge/media"
cache_dir = "/tmp/rtpbridge-cache"
cache_cleanup_interval_secs = 300
ws_ping_interval_secs = 30
log_level = "info"
# See rtpbridge.toml.example for all available optionsSet [tls] to serve HTTPS/WSS on the existing listen port; rtpbridge does
not mix plaintext and TLS on that port. Set auth_hmac_secret_file to require
short-lived HMAC-SHA256 signatures for control WebSocket upgrades and sensitive
HTTP routes. /audio/<connect_token> remains a separate single-use audio
capability, so AI/media consumers do not receive the control signing key. See
the configuration guide
for the exact configuration and wire format.
All communication uses JSON over WebSocket. Each connection is bound to exactly one session.
The control API is intended for backend clients. Without HMAC, privileged requests carrying a browser Origin are rejected, and loopback clients must use a loopback IP or localhost in Host. This protects local development listeners from cross-site requests and DNS rebinding. Browser audio uses the separate /audio/<connect_token> capability.
| Method | Description |
|---|---|
session.create |
Create a new session |
session.attach |
Reattach to an orphaned session |
session.destroy |
Destroy the current session |
session.info |
Get session details |
session.list |
List all sessions on the server |
server.info |
Get server hostname and configured media IP |
endpoint.create_from_offer |
Create endpoint from remote SDP offer (auto-detects WebRTC vs RTP) |
endpoint.create_offer |
Create a new endpoint and generate SDP offer |
endpoint.accept_answer |
Accept remote SDP answer |
endpoint.accept_offer |
Accept remote SDP offer for an existing endpoint |
endpoint.create_with_file |
Create file playback endpoint |
endpoint.create_tone |
Create generated tone endpoint |
endpoint.create_websocket |
Create raw PCM WebSocket audio endpoint |
endpoint.remove |
Remove an endpoint |
endpoint.ice_restart |
ICE restart (WebRTC only) |
endpoint.update_direction |
Override or restore endpoint routing direction |
endpoint.update_remote_sdp |
Update remote RTP address/SRTP state without codec changes |
endpoint.file.seek |
Seek file playback position |
endpoint.file.pause |
Pause file playback |
endpoint.file.resume |
Resume file playback |
endpoint.dtmf.inject |
Inject DTMF digit into endpoint |
endpoint.dtmf.set_sensitive |
Redact DTMF logs, tag control events, and omit DTMF packets from recordings for an endpoint |
endpoint.srtp_rekey |
Initiate SRTP rekey (plain RTP/SRTP only) |
endpoint.transfer |
Transfer an endpoint to a different session |
session.bridge |
Bidirectional audio bridge between two sessions |
recording.start |
Start recording (full session or single leg) |
recording.stop |
Stop recording |
vad.start |
Start voice activity detection on endpoint |
vad.stop |
Stop voice activity detection |
fax_detect.start |
Start fax tone detection (CNG/CED) on endpoint |
fax_detect.stop |
Stop fax tone detection |
stats.subscribe |
Subscribe to periodic session statistics |
stats.unsubscribe |
Unsubscribe from statistics |
recording.start, recording.stop, endpoint removal, and the file-playback lifecycle events carry
media-host epoch milliseconds. These use the same host clock as PCAP packet records, so callers
can safely align explicitly injected media to a recording without comparing control-plane clocks.
endpoint.file.started is emitted only immediately before the first file RTP is routed and carries
that same epoch; ledger consumers must use it rather than endpoint creation, which can remain buffering.
pcap2audio --metadata <path> writes its earliest decodable-RTP sample-zero epoch and rendered WAV
duration as JSON for conversion services.
| Event | Description |
|---|---|
dtmf |
DTMF digit detected from remote endpoint |
endpoint.state_changed |
Endpoint state transition |
endpoint.ice_state_changed |
WebRTC ICE state transition |
endpoint.file.started |
First file RTP packet entered media routing |
endpoint.file.finished |
File playback completed |
endpoint.tone.finished |
Tone generation completed |
endpoint.ws.connected |
WebSocket audio socket attached |
endpoint.ws.disconnected |
WebSocket audio socket closed |
endpoint.ws.connect_timeout |
WebSocket audio endpoint was not dialed in before timeout |
endpoint.media_timeout |
No media received from remote endpoint |
endpoint.rtcp_bye |
RTCP BYE received from remote endpoint |
endpoint.transferred_out |
Endpoint transferred to another session |
endpoint.transferred_in |
Endpoint transferred in from another session |
events.dropped |
Events dropped due to client backpressure |
recording.stopped |
Recording stopped externally |
session.idle_timeout |
Session destroyed due to inactivity |
session.empty_timeout |
Session destroyed due to zero endpoints timeout |
session.orphaned |
Control connection dropped, timeout running |
stats |
Periodic session statistics |
vad.speech_started |
Speech detected after silence |
vad.silence |
Periodic silence notification |
vad.error |
VAD analysis decoder could not be created |
fax.cng_detected |
Fax calling tone (CNG, 1100 Hz) detected |
fax.ced_detected |
Fax/modem answer tone (CED, 2100 Hz) detected |
fax.error |
Fax analysis decoder could not be created |
See Events Reference for detailed payload schemas.
rtpbridge also serves HTTP endpoints on the same listen address(es). See Configuration — HTTP REST API for full details including status codes and pagination.
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check — returns {"status":"ok"} |
/metrics |
GET | Prometheus-format metrics |
/sessions |
GET | List all active sessions |
/sessions/{id} |
GET | Get session details |
/recordings |
GET | List PCAP recording files |
/recordings/{path} |
GET | Download a recording file |
/recordings/{path} |
DELETE | Delete a recording file |
WebSocket Control (JSON)
|
+------v------+
| SessionMgr | DashMap<SessionId, Session>
+------+------+
|
+------------v------------+
| Session Task | (one tokio task per session)
| |
| +--------+ +--------+ |
| | WebRTC | | RTP/ | |
| | str0m | | SRTP | | +--------+ +----------+
| +---+----+ +---+----+ | | File | | Recording|
| | | | |Playback | | (PCAP) |
| +----+-----+ | +----+----+ +----------+
| | | |
| Routing Table | |
| (sendrecv/recvonly/ | |
| sendonly directions) | |
+-------------------------+-------+
|
+------------v------------+
| Per-Endpoint UDP |
| Sockets |
+-------------------------+
# Unit tests
cargo test
# Integration tests (serial — spawns server processes)
cargo test -- --test-threads=1MIT