Files
rutster/docs/QUICKSTART.md
Aaron D. Lee 486af84af1 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 03:10:13 -04:00

200 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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](https://rustup.rs/):
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
The repo pins a specific stable channel in `rust-toolchain.toml``rustup`
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
```bash
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:
```bash
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](adr/0007-trunk-rented-transport.md)).
### Prerequisites
- A [Twilio account](https://www.twilio.com/) (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](https://ngrok.com/) 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:
```bash
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
```bash
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`](https://docs.rs/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`](ARCHITECTURE.md). For the dev loop,
see [`DEVELOPMENT.md`](DEVELOPMENT.md).