Skip to content

carlbarrdahl/sipstream

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sipstream

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)

Quickstart

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.ts

Point a SIP client at <your-public-ip>:5060 and call.

Configuration

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

Stream Protocol

sipstream communicates with your WebSocket server using simple JSON messages.

Gateway → Your Server

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"}

Your Server → Gateway

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"}

HTTP Endpoints

  • GET /health{"status": "ok", "calls": 2}
  • GET /calls — list active calls with RTP details

Examples

OpenAI Realtime API

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.ts

Configurable via env vars — see examples/openai/README.md.

Write Your Own

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 }))
      }
    },
  },
})

Tenant Routing

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.

Tests

bun test          # run once
bun test --watch  # watch mode

Deploy

sipstream 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.

VPS (DigitalOcean, Hetzner, etc.)

# 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.ts

For 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.target
systemctl enable sipstream && systemctl start sipstream

Docker

docker build -t sipstream .
docker run --network host \
  -e STREAM_URL=ws://localhost:8080 \
  -e PUBLIC_IP=<server-ip> \
  sipstream

Use --network host so SIP/RTP UDP ports are directly reachable. Bridge networking with individual port mappings works but is tedious for the RTP range.

Configure 46elks

  1. Contact 46elks support to whitelist sipstream's public IP for SIP connect
  2. 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>"}'

Network requirements

Port Protocol Purpose
5060 UDP SIP signaling
10000-10100 UDP RTP media (one port per active call)
3000 TCP HTTP health/admin

Scaling

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.

Project Structure

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

About

SIP/RTP-to-WebSocket bridge — connect phone calls to any AI backend

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors