Files
rutster/docs/QUICKSTART.md
Aaron D. Lee e6891f2cec docs: QUICKSTART env table + 'make a real phone call' walkthrough + README status update (slice-5 T9)
QUICKSTART gains a Twilio Media Streams section: env-var table for the four
RUTSTER_TWILIO_* vars, the run-with-twilio-live command, the point-Twilio-
at-rutster webhook/TwiML walkthrough, + the outbound-call curl example. The
/v1/trunk routes' auth-deferral (slice 6) is flagged. A 'what's different
from WebRTC' note explains the architectural reuse -- the reflex stack is
ingress-agnostic (Reflex<TapAudioPipe> + LocalVadReflex REUSED from slice-4).

README's spearhead status is corrected + extended: slices 1-4 are merged to
main (the prior status stalled at '1-3 merged, slice-4 active' -- stale);
4.5 (sim/benchmark, ADR-0010) + step 5 (PSTN via rented transport, ADR-0007)
are the active build targets. ADR-0007 honored: rutster parses zero SIP bytes.

T9 of slice-5.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 16:02:19 +00:00

7.5 KiB
Raw Permalink Blame History

Quickstart

Get Rutster running and hear your own voice echoed back in under 5 minutes.

Status: Slices 14 (WebRTC media core, WS tap, OpenAI Realtime brain, barge-in / VAD-driven playout kill) are merged to main. Slice 4½ (sim/benchmark harness) + step 5 (PSTN via rented Twilio Media Streams transport) are the active build targets, in flight. This quickstart's first section exercises the slice-1 WebRTC media loopback (the simplest end-to-end demo on main); the optional "Make a real phone call" section below covers step-5 PSTN ingress. See docs/superpowers/specs/2026-07-05-slice-5-rented-transport-design.md for the active build target's design.


Prerequisites

1. Rust toolchain

Install via rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

The repo pins a specific stable channel in rust-toolchain.tomlrustup will pick it up automatically on first cargo invocation. No manual toolchain selection needed.

2. libopus (FFI dependency)

The opus crate links system libopus via FFI (per PORT_PLAN §7's "🦀 Core (FFI)" disposition — Opus is the codec surface Rust doesn't need to re-implement). Install the dev headers:

Platform Command
Debian/Ubuntu sudo apt-get install -y libopus-dev
Fedora sudo dnf install -y opus-devel
Arch sudo pacman -S opus
macOS (Homebrew) brew install opus

Verify: pkg-config --cflags opus should print a path with no error.

That's the only system dependency in slice 1. Everything else is pure Rust from crates.io.


Run the server

cargo run
# listening on http://0.0.0.0:8080

First build takes ~2 minutes (str0m + axum + tokio compile fresh). Subsequent builds are incremental.


Hear the echo

  1. Open a browser to http://localhost:8080/.
  2. Click Start call.
  3. Grant microphone permission when the browser prompts.
  4. Speak — you should hear yourself back within ~200 ms (no perceptible delay).
  5. Click Hang up to tear down. The server logs Closing → Closed for the session.

Verbose tracing for debugging:

RUST_LOG=rutster=debug cargo run

Make a real phone call (PSTN via Twilio Media Streams, optional)

The WebRTC loopback above proves the media core + the reflex loop. Spearhead step 5 takes it to a real phone number: a PSTN caller dials your Twilio number, Twilio forks the call's audio over a WebSocket to rutster, and the same reflex loop (barge-in, brain tap) runs against the PSTN leg — no first-party SIP stack (ADR-0007).

Prerequisites

  • A Twilio account (trial works).
  • A Twilio phone number capable of Voice + Media Streams.
  • A publicly-reachable HTTPS URL for rutster (Twilio must call back to you). For local dev, use ngrok to tunnel localhost:8080 to a public URL: ngrok http 8080.

Twilio credentials

Set these environment variables to enable PSTN ingress:

Env var Purpose Example
RUTSTER_TWILIO_ACCOUNT_SID Twilio account ID ACxxx...
RUTSTER_TWILIO_AUTH_TOKEN Twilio auth token (secret; never logged) xxx...
RUTSTER_TWILIO_MEDIA_BIND Where rutster's Media Streams WSS server binds 0.0.0.0:8081
RUTSTER_TWILIO_WEBHOOK_BASE Your public URL Twilio calls back to https://your.ngrok.io

Without these set, the binary runs WebRTC-only (slices 14 ingress + barge-in). With them set, the binary accepts PSTN fork calls against /twilio/media-stream.

Run with Twilio enabled

The live Twilio REST client is feature-gated behind twilio-live — the routine CI gate uses the in-process mock; you only need the live client for a real call:

export RUTSTER_TWILIO_ACCOUNT_SID=AC...
export RUTSTER_TWILIO_AUTH_TOKEN=...
export RUTSTER_TWILIO_MEDIA_BIND=0.0.0.0:8081
export RUTSTER_TWILIO_WEBHOOK_BASE=https://your.ngrok.io
cargo run --features=twilio-live

Point Twilio at rutster

  1. In the Twilio console, configure your phone number's A CALL COMES IN webhook to POST to https://your.ngrok.io/v1/trunk/webhook.
  2. Twilio answers an inbound call, hits your webhook; rutster responds with TwiML instructing Twilio to <Connect><Stream> against wss://your.ngrok.io/twilio/media-stream.
  3. Twilio opens the WSS; rutster's TwilioMediaStreamsServer receives the call's audio (µ-law @ 8 kHz), decodes via G711Codec to 24 kHz PCM, and feeds the same reflex loop + brain tap as a WebRTC leg.

Make an outbound call

curl -X POST http://localhost:8080/v1/trunk/sessions \
  -H 'Content-Type: application/json' \
  -d '{"to":"+15551234567","from":"+15550000000"}'

The handler calls TwilioCallControlClient::originate; Twilio places the call and forks the audio back — the PSTN caller reaches the reflex loop.

Auth note: the /v1/trunk/* routes are not authenticated in slice 5 (authn defers to slice 6, the spend cap). Do not expose them to the public internet without a reverse-proxy auth layer.

What's different from WebRTC

Nothing, architecturally. The PSTN leg enters the same MediaThread tick loop via a MediaLeg::Trunk(TrunkSession) variant (the WebRTC leg is MediaLeg::WebRTC). The reflex stack (Reflex<TapAudioPipe> + LocalVadReflex) is reused verbatim from slice 4 — barge-in fires on PSTN caller speech the same way it fires on WebRTC caller speech. The only difference is the ingress: str0m's RTP decode (WebRTC) vs the Media Streams WSS pump + G.711 codec (trunk).


Troubleshooting

Symptom Likely cause / fix
error: linking with cc failed / could not find opus libopus dev headers not installed. Re-run the install command above.
Browser shows no mic prompt Another tab/app holding the mic, or mic permissions disabled for localhost. Check browser settings.
ICE connection failed in the browser Shouldn't happen on loopback (host candidates only). If it does, check the server console for the str0m error.
Click Start call, nothing happens Open the browser console (F12). The page logs ICE state + connection state to a <pre> element. Look for the failure there.
Port 8080 already in use Set RUTSTER_HTTP_BIND, e.g. RUTSTER_HTTP_BIND=0.0.0.0:8090 cargo run -p rutster

The browser test page at GET / is a single self-contained HTML file with inline JS — no build step. View source to see exactly what the client side is doing.


What's happening

When you click "Start call":

  1. Browser captures microphone audio via getUserMedia.
  2. Browser creates an RTCPeerConnection and generates an SDP offer (audio-only, Opus codec).
  3. Browser POSTs the offer to POST /v1/sessions/:id/offer.
  4. The Rutster core (built on str0m, a sans-IO WebRTC implementation) accepts the offer, generates an SDP answer with its DTLS fingerprint + ICE credentials.
  5. Browser sets the answer as remote description; ICE + DTLS handshake completes.
  6. RTP starts flowing: browser → core terminates DTLS-SRTP → decodes Opus to 16-bit PCM @ 24 kHz mono → echoes PCM back → re-encodes to Opus → DTLS-SRTP → browser plays it.

The "codec-to-PCM boundary" is the canonical point where, in a future slice, the audio tap for an external AI brain splices in. Slice 1 just echoes; step 2 of the spearhead swaps the echo for a real tap.

For the why, see ARCHITECTURE.md. For the dev loop, see DEVELOPMENT.md.