Carrier-neutral SIP, WebRTC, RTP, and recording session gateway for backend-controlled real-time communication.
The gateway connects to Drachtio for SIP signaling, accepts inbound SIP INVITEs, routes each destination URI to an owning backend over a multiplexed WebSocket control connection or static HTTP webhook, and exposes rtpbridge-backed media sessions for SIP-less WebRTC, RTP, bridging, playback, DTMF, gathering, leave-message flows, and PCAP recording access.
- Inbound SIP routing by exact destination user or destination-user prefix.
- Pre-media inbound decisioning over the control WebSocket before rtpbridge allocation.
- Outbound SIP origination through the same command surface.
- Multiplexed WebSocket control protocol with request/response commands and gateway events.
- HTTP management API with equivalent command routes for simple integrations and operations.
- rtpbridge-backed media sessions with WebRTC, RTP, file playback, bridge/unbridge, DTMF injection, digit gathering, and leave-message support.
- Recording start/stop/list/download/delete plus gateway-side ordered PCAP segment merge.
- Docker Compose E2E tests with Drachtio, two rtpbridge backends, coturn, SIP/RTP/WebRTC media, decoded recordings, and failure cleanup. CI runs both TLS/HMAC and plaintext modes before publishing images.
VitePress documentation lives in docs/.
corepack enable
yarn install --immutable
yarn docs:devStart with:
This project uses Node.js 24 and Yarn Berry through Corepack.
corepack enable
yarn install --immutable
yarn test
yarn buildRun the service locally against an external Drachtio server:
yarn devRun the automated local E2E stack:
yarn e2e:composeThe compose runner pulls the CI-published ghcr.io/zyno-io/rtpbridge:main image by default. To build and test a local rtpbridge checkout, set RTPBRIDGE_LOCAL_CHECKOUT=/path/to/rtpbridge. An explicit RTPBRIDGE_IMAGE=registry.example.com/rtpbridge:tag takes precedence over a local checkout. Set E2E_GATEWAY_PORT when host port 3001 is already in use.
Core environment variables:
DRACHTIO_HOST: Drachtio server host, defaults to127.0.0.1.DRACHTIO_PORT: Drachtio control port, defaults to9022.DRACHTIO_SECRET: Drachtio shared secret, optional.DRACHTIO_APP_TAG: application tag advertised to a Drachtio server using outbound request routing.DRACHTIO_ROUTE_FALLBACK_URL: existing Drachtio HTTP router to use when no gateway route owns an INVITE.HTTP_PORT: HTTP/control port, defaults to3001.CONTROL_WS_PATH: backend control WebSocket path, defaults to/control.CONTROL_AUTH_MODE:bearerornone.CONTROL_AUTH_TOKEN: bearer token for control WebSocket and protected HTTP command routes.RTPBRIDGE_HOST: rtpbridge service DNS name or IP; enables media commands when set.RTPBRIDGE_PORT: rtpbridge WebSocket port, defaults to9100.RTPBRIDGE_TLS: settruefor WSS control and HTTPS recording requests.RTPBRIDGE_AUTH_HMAC_SECRET_FILE: mounted HMAC key file shared with rtpbridge.RTPBRIDGE_TLS_CA_FILE: optional PEM CA bundle for private certificates.RTPBRIDGE_TLS_SERVERNAME: optional TLS certificate name override.RECORDINGS_PATH: rtpbridge recording root, defaults to/var/lib/rtpbridge/recordings.ROUTES_JSON: static HTTP route table, defaults to[].
Example static routes:
[
{ "match": "exact", "value": "support", "url": "https://api.example.com/sip" },
{ "match": "userPrefix", "value": "dev-support-", "url": "https://dev.example.com/sip" }
]Dynamic route registration over the control WebSocket is preferred for applications that need to accept or reject inbound SIP before media allocation.
Backends connect to /control, optionally with Authorization: Bearer <token>, then register routes:
{
"type": "request",
"id": "register-1",
"method": "route.register",
"params": {
"routes": [{ "match": "exact", "value": "support" }]
}
}When an inbound SIP INVITE matches that route, the gateway sends a request before allocating rtpbridge media:
{
"type": "request",
"id": "invite-1",
"method": "sip.invite",
"params": {
"event": "invite",
"callId": "abc123@example.com",
"sipCallId": "abc123@example.com",
"destinationUri": "sip:support@example.net",
"destinationUser": "support",
"sdp": "v=0..."
}
}The backend can reject immediately:
{
"type": "response",
"id": "invite-1",
"ok": true,
"result": { "action": "reject", "status": 486, "reason": "Busy Here" }
}Or answer after creating or selecting media:
{
"type": "response",
"id": "invite-1",
"ok": true,
"result": { "action": "answer", "sdp": "v=0..." }
}Media sessions and SIP dialogs created or accepted over a control connection are owned by that connection and are torn down when it disconnects.
SIGTERM, SIGINT, POST /drain, and POST /terminate stop admission and let existing calls and media sessions finish before a configurable shutdown deadline. Use /readyz for new-traffic readiness and retain existing owner connections during drain. See operations for rolling deployment requirements.
HTTP recording routes are bearer-protected when CONTROL_AUTH_MODE=bearer.
GET /recordings?startsWith=<prefix>&skip=<n>&limit=<n>GET /recordings/:backendId/*POST /recordings/mergeDELETE /recordings/:backendId/*
Production applications should prefer deterministic recording filenames over prefix scans. If the application chooses the filePath when starting each segment, it already knows the recording path and should not need GET /recordings on the hot path. In multi-backend deployments, also retain the backendId returned by recording.start and recording.stop; direct download, delete, and merge targets are { backendId, path }. GET /recordings fans out across configured rtpbridge backends and is best reserved for diagnostics, operator browsing, or recovery/backfill flows.
POST /recordings/merge accepts an ordered target list and streams one PCAP response. The gateway writes the first global PCAP header once, validates that every source segment has the same global header, and appends packet records in request order.
{
"targets": [
{ "backendId": "rtpbridge-0", "path": "call_42__seg_0001.pcap" },
{ "backendId": "rtpbridge-1", "path": "call_42__seg_0002.pcap" }
]
}Clients should delete source segments only after the merged artifact is durably stored.
Merged or downloaded PCAP files can be decoded with rtpbridge pcap2audio; see the rtpbridge recording docs at https://zyno-io.github.io/rtpbridge/protocol/recording.html#decoding-pcap2audio.