A SIP/RTP-to-WebSocket bridge that connects telephone calls to any WebSocket backend. Receives SIP calls (e.g. from 46elks), extracts G.711 μ-law audio from RTP, and streams it bidirectionally over a simple JSON/WebSocket protocol.
Your backend is a WebSocket server. sipstream connects to it, sends you audio, and you send audio back. What you do with that audio is up to you — pipe it to OpenAI, run your own STT/TTS, record it, whatever.
Phone ──SIP/RTP──▶ sipstream ──WebSocket──▶ Your Server
(this) (you build)
bun install
# Terminal 1: your WebSocket backend (OpenAI example included)
OPENAI_API_KEY=sk-... bun run examples/openai/server.ts
# Terminal 2: the gateway
STREAM_URL=ws://localhost:8080 PUBLIC_IP=<your-public-ip> bun run src/index.tsPoint a SIP client at <your-public-ip>:5060 and call.
| Variable | Default | Description |
|---|---|---|
STREAM_URL |
(required) | WebSocket URL of your backend server |
PUBLIC_IP |
(required) | Public IP advertised in SDP for RTP |
SIP_PORT |
5060 |
UDP port for SIP signaling |
HTTP_PORT |
3000 |
TCP port for health/status HTTP endpoints |
RTP_PORT_MIN |
10000 |
Start of RTP port range |
RTP_PORT_MAX |
10100 |
End of RTP port range |
sipstream communicates with your WebSocket server using simple JSON messages.
connected — sent when a new call arrives:
{"event": "connected", "callId": "call-1@46elks.com", "from": "+46766861234", "to": "+46700000000"}media — caller audio, base64-encoded G.711 μ-law (8kHz, 20ms chunks):
{"event": "media", "payload": "//NQYH..."}disconnected — caller hung up:
{"event": "disconnected", "callId": "call-1@46elks.com"}media — audio to play back to caller (same format):
{"event": "media", "payload": "//NQYH..."}clear — discard buffered outgoing audio (e.g. when the caller interrupts):
{"event": "clear"}hangup — end the call from your side:
{"event": "hangup"}GET /health—{"status": "ok", "calls": 2}GET /calls— list active calls with RTP details
The included example bridges calls to OpenAI's Realtime API with G.711 μ-law passthrough (zero transcoding, minimal latency):
OPENAI_API_KEY=sk-... bun run examples/openai/server.tsConfigurable via env vars — see examples/openai/README.md.
A minimal echo server that plays audio back to the caller:
Bun.serve({
port: 8080,
fetch(req, server) {
if (server.upgrade(req)) return
return new Response("ws")
},
websocket: {
message(ws, data) {
const msg = JSON.parse(String(data))
if (msg.event === "media") {
ws.send(JSON.stringify({ event: "media", payload: msg.payload }))
}
},
},
})sipstream can route calls to different WebSocket backends based on the dialed number (To header). The tenant map lives in src/tenants.ts:
const db: Record<string, Tenant> = {
"46700000000": {
name: "Acme Corp",
streamUrl: "ws://localhost:8080",
},
}lookupTenant is async — swap the in-memory map for a DB query when ready. Calls that don't match any tenant fall back to STREAM_URL.
bun test # run once
bun test --watch # watch modesipstream needs a public IP with UDP ports open for SIP/RTP. A VPS is the simplest option — no NAT, no port mapping, all UDP ports reachable by default.
# On the server
curl -fsSL https://bun.sh/install | bash
source ~/.bashrc
git clone <repo-url> /opt/sipstream
cd /opt/sipstream
bun install
# Test it
STREAM_URL=ws://localhost:8080 PUBLIC_IP=<server-ip> bun run src/index.tsFor persistent operation, create a systemd service:
# /etc/systemd/system/sipstream.service
[Unit]
Description=sipstream
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/sipstream
ExecStart=/root/.bun/bin/bun run src/index.ts
Restart=always
Environment=STREAM_URL=ws://localhost:8080
Environment=PUBLIC_IP=<server-ip>
[Install]
WantedBy=multi-user.targetsystemctl enable sipstream && systemctl start sipstreamdocker build -t sipstream .
docker run --network host \
-e STREAM_URL=ws://localhost:8080 \
-e PUBLIC_IP=<server-ip> \
sipstreamUse --network host so SIP/RTP UDP ports are directly reachable. Bridge networking with individual port mappings works but is tedious for the RTP range.
- Contact 46elks support to whitelist sipstream's public IP for SIP connect
- Configure the phone number:
curl -X POST https://api.46elks.com/a1/Numbers/n... \
-u API_USERNAME:API_PASSWORD \
-d 'voice_start={"connect":"sip:ai@<server-ip>"}'| Port | Protocol | Purpose |
|---|---|---|
| 5060 | UDP | SIP signaling |
| 10000-10100 | UDP | RTP media (one port per active call) |
| 3000 | TCP | HTTP health/admin |
Each instance handles multiple concurrent calls (one RTP port per call). For the default port range of 10000-10100, that's ~100 concurrent calls per instance.
src/
config.ts # env var loading
index.ts # entry point
bridge.ts # SIP+RTP ↔ WebSocket orchestration
session.ts # per-call state management
tenants.ts # tenant routing map (dialed number → stream URL)
sip/
parser.ts # SIP message parse/build
sdp.ts # SDP parse/build
server.ts # UDP socket for SIP
rtp/
packet.ts # RTP header encode/decode
buffer.ts # send buffer (packetizer)
stream/
protocol.ts # WebSocket stream protocol messages
examples/
openai/
server.ts # OpenAI Realtime API bridge
test/ # bun:test specs for all modules