fix(ts-config,proxybridge,voicebox): align voicebox config types and add missing proxy bridge command definitions
This commit is contained in:
114
CLAUDE.md
114
CLAUDE.md
@@ -1,41 +1,103 @@
|
||||
# Project Notes
|
||||
|
||||
## Architecture: Hub Model (Call as Centerpiece)
|
||||
## Architecture: Hub Model in Rust (Call as Centerpiece)
|
||||
|
||||
All call logic lives in `ts/call/`. The Call is the central entity with N legs.
|
||||
The call hub lives in the Rust proxy-engine (`rust/crates/proxy-engine/`). TypeScript is the **control plane only** — it configures the engine, sends high-level commands (`hangup`, `make_call`, `webrtc_offer`, etc.), and receives events (`incoming_call`, `call_answered`, `device_registered`, `webrtc_audio_rx`, …). No raw SIP/RTP ever touches TypeScript.
|
||||
|
||||
### Key Files
|
||||
- `ts/call/call-manager.ts` — singleton registry, factory methods, SIP routing
|
||||
- `ts/call/call.ts` — the hub: owns legs, media forwarding
|
||||
- `ts/call/sip-leg.ts` — SIP device/provider connection (wraps SipDialog)
|
||||
- `ts/call/webrtc-leg.ts` — browser WebRTC connection (wraps werift PeerConnection)
|
||||
- `ts/call/rtp-port-pool.ts` — unified RTP port pool
|
||||
- `ts/sipproxy.ts` — thin bootstrap wiring everything together
|
||||
- `ts/webrtcbridge.ts` — browser device registration (signaling only)
|
||||
The `Call` is still the central entity: it owns N legs and a central mixer task that provides mix-minus audio to all participants. Legs can be `SipProvider`, `SipDevice`, `WebRtc`, or `Tool` (recording/transcription observer).
|
||||
|
||||
### WebRTC Browser Call Flow (Critical)
|
||||
### Key Rust files (`rust/crates/proxy-engine/src/`)
|
||||
|
||||
The browser call flow has a specific signaling order that MUST be followed:
|
||||
- `call_manager.rs` — singleton registry, call factory methods, SIP routing (inbound/outbound/passthrough), B2BUA state machine, inbound route resolution
|
||||
- `call.rs` — the `Call` hub + `LegInfo` struct, owns legs and the mixer task
|
||||
- `sip_leg.rs` — full SIP dialog management for B2BUA legs (INVITE, 407 auth retry, BYE, CANCEL, early media)
|
||||
- `rtp.rs` — RTP port pool (uses `Weak<UdpSocket>` so calls auto-release ports on drop) + RTP header helpers
|
||||
- `mixer.rs` — 20 ms-tick mix-minus engine (48 kHz f32 internal, per-leg transcoding via `codec-lib`, per-leg denoising)
|
||||
- `jitter_buffer.rs` — per-leg reordering/packet-loss compensation
|
||||
- `leg_io.rs` — spawns inbound/outbound RTP I/O tasks per SIP leg
|
||||
- `webrtc_engine.rs` — browser WebRTC sessions (werift-rs based), ICE/DTLS/SRTP
|
||||
- `provider.rs` — SIP trunk registrations, public-IP detection via Via `received=`
|
||||
- `registrar.rs` — accepts REGISTER from SIP phones, tracks contacts (push-based device status)
|
||||
- `config.rs` — `AppConfig` deserialized from TS, route resolvers (`resolve_outbound_route`, `resolve_inbound_route`)
|
||||
- `main.rs` — IPC command dispatcher (`handle_command`), event emitter, top-level SIP packet router
|
||||
- `sip_transport.rs` — owning wrapper around the main SIP UDP socket
|
||||
- `voicemail.rs` / `recorder.rs` / `audio_player.rs` / `tts.rs` — media subsystems
|
||||
- `tool_leg.rs` — per-source observer audio for recording/transcription tools
|
||||
- `ipc.rs` — event-emission helper used throughout
|
||||
|
||||
1. `POST /api/call` with browser deviceId → CallManager creates Call, saves pending state, notifies browser via `webrtc-incoming`
|
||||
2. Browser sends `webrtc-offer` (with its own `sessionId`) → CallManager creates a **standalone** WebRtcLeg (NOT attached to any call yet)
|
||||
3. Browser sends `webrtc-accept` (with `callId` + `sessionId`) → CallManager links the standalone WebRtcLeg to the Call, then starts the SIP provider leg
|
||||
### Key TS files (control plane)
|
||||
|
||||
**The WebRtcLeg CANNOT be created at call creation time** because the browser's session ID is unknown until the `webrtc-offer` arrives.
|
||||
- `ts/sipproxy.ts` — entrypoint, wires the proxy engine bridge + web UI + WebRTC signaling
|
||||
- `ts/proxybridge.ts` — `@push.rocks/smartrust` bridge to the Rust binary, typed `TProxyCommands` map
|
||||
- `ts/config.ts` — JSON config loader (`IAppConfig`, `IProviderConfig`, etc.), sent to Rust via `configure`
|
||||
- `ts/voicebox.ts` — voicemail metadata persistence (WAV files live in `.nogit/voicemail/{boxId}/`)
|
||||
- `ts/webrtcbridge.ts` — browser WebSocket signaling, browser device registry (`deviceIdToWs`)
|
||||
- `ts/call/prompt-cache.ts` — the only remaining file under `ts/call/` (IVR prompt caching)
|
||||
|
||||
### WebRTC Audio Return Channel (Critical)
|
||||
### Rust SIP protocol library
|
||||
|
||||
The SIP→browser audio path works through the Call hub:
|
||||
`rust/crates/sip-proto/` is a zero-dependency SIP data library (parse/build/mutate/serialize messages, dialog management, SDP helpers, digest auth). Do not add transport or timer logic there — it's purely data-level.
|
||||
|
||||
1. Provider sends RTP to SipLeg's socket
|
||||
2. SipLeg's `onRtpReceived` fires → Call hub's `forwardRtp`
|
||||
3. Call hub calls `webrtcLeg.sendRtp(data)` → which calls `forwardToBrowser()`
|
||||
4. `forwardToBrowser` transcodes (G.722→Opus) and sends via `sender.sendRtp()` (WebRTC PeerConnection)
|
||||
## Event-push architecture for device status
|
||||
|
||||
**`WebRtcLeg.sendRtp()` MUST feed into `forwardToBrowser()`** (the WebRTC PeerConnection path), NOT send to a UDP address. This was a bug that caused one-way audio.
|
||||
Device status flows **via push events**, not pull-based IPC queries:
|
||||
|
||||
The browser→SIP direction works independently: `ontrack.onReceiveRtp` → `forwardToSip()` → transcodes → sends directly to provider's media endpoint via UDP.
|
||||
1. Rust emits `device_registered` when a phone REGISTERs
|
||||
2. TS `sipproxy.ts` maintains a `deviceStatuses` Map, updated from the event
|
||||
3. Map snapshot goes into the WebSocket `status` broadcast
|
||||
4. Web UI (`ts_web/elements/sipproxy-devices.ts`) reads it from the push stream
|
||||
|
||||
### SIP Protocol Library
|
||||
There used to be a `get_status` pull IPC for this, but it was never called from TS and has been removed. If a new dashboard ever needs a pull-based snapshot, the push Map is the right source to read from.
|
||||
|
||||
`ts/sip/` is a zero-dependency SIP protocol library. Do not add transport or timer logic there — it's purely data-level (parse/build/mutate/serialize).
|
||||
## Inbound routing (wired in Commit 4 of the cleanup PR)
|
||||
|
||||
Inbound route resolution goes through `config.resolve_inbound_route(provider_id, called_number, caller_number)` inside `create_inbound_call` (call_manager.rs). The result carries a `ring_browsers` flag that propagates to the `incoming_call` event; `ts/sipproxy.ts` gates the `webrtc-incoming` browser fan-out behind that flag.
|
||||
|
||||
**Known limitations / TODOs** (documented in code at `create_inbound_call`):
|
||||
- Multi-target inbound fork is not yet implemented — only the first registered device from `route.device_ids` is rung.
|
||||
- `ring_browsers` is **informational only**: browsers see a toast but do not race the SIP device to answer. True first-to-answer-wins requires a multi-leg fork + per-leg CANCEL, which is not built yet.
|
||||
- `voicemail_box`, `ivr_menu_id`, `no_answer_timeout` are resolved but not yet honored downstream.
|
||||
|
||||
## WebRTC Browser Call Flow (Critical)
|
||||
|
||||
The browser call signaling order is strict:
|
||||
|
||||
1. Browser initiates outbound via a TS API (e.g. `POST /api/call`) — TS creates a pending call in the Rust engine via `make_call` and notifies the browser with a `webrtc-incoming` push.
|
||||
2. Browser sends `webrtc-offer` (with its own `sessionId`) → Rust `handle_webrtc_offer` creates a **standalone** WebRTC session (NOT attached to any call yet).
|
||||
3. Browser sends `webrtc_link` (with `callId` + `sessionId`) → Rust links the standalone session to the Call and wires the WebRTC leg through the mixer.
|
||||
|
||||
**The WebRTC leg cannot be fully attached at call-creation time** because the browser's session ID is unknown until the `webrtc-offer` arrives.
|
||||
|
||||
### WebRTC audio return channel (Critical)
|
||||
|
||||
The SIP→browser audio path goes through the mixer, not a direct RTP relay:
|
||||
|
||||
1. Provider sends RTP → received on the provider leg's UDP socket (`leg_io::spawn_sip_inbound`)
|
||||
2. Packet flows through `jitter_buffer` → mixer's inbound mpsc channel
|
||||
3. Mixer decodes/resamples/denoises, computes mix-minus per leg
|
||||
4. WebRTC leg receives its mix-minus frame, encodes to Opus, and pushes via the WebRTC engine's peer connection sender
|
||||
|
||||
Browser→SIP works symmetrically: `ontrack.onReceiveRtp` → WebRTC leg's outbound mpsc → mixer → other legs' inbound channels.
|
||||
|
||||
## SDP/Record-Route NAT (fixed in Commit 3 of the cleanup PR)
|
||||
|
||||
The proxy tracks a `public_ip: Option<String>` on every `LegInfo` (populated from provider-leg construction sites). When `route_passthrough_message` rewrites SDP (`c=` line) or emits a `Record-Route`, it picks `advertise_ip` based on the destination leg's kind:
|
||||
|
||||
- `SipProvider` → `other.public_ip.unwrap_or(lan_ip)` (provider reaches us via public IP)
|
||||
- `SipDevice` / `WebRtc` / `Tool` / `Media` → `lan_ip` (everything else is LAN or proxy-internal)
|
||||
|
||||
This fixed a real NAT-traversal bug where the proxy advertised its RFC1918 LAN IP to the provider in SDP, causing one-way or no audio for device-originated inbound traffic behind NAT.
|
||||
|
||||
## Build & development
|
||||
|
||||
- **Build:** `pnpm run buildRust` (never `cargo build` directly — tsrust cross-compiles for both `x86_64-unknown-linux-gnu` and `aarch64-unknown-linux-gnu`)
|
||||
- **Cross-compile setup:** the aarch64 target requires `gcc-aarch64-linux-gnu` + `libstdc++6-arm64-cross` (Debian/Ubuntu). See `rust/.cargo/config.toml` for the linker wiring. A committed symlink at `rust/.cargo/crosslibs/aarch64/libstdc++.so` → `/usr/aarch64-linux-gnu/lib/libstdc++.so.6` avoids needing the `libstdc++-13-dev-arm64-cross` package.
|
||||
- **Bundle web UI:** `pnpm run bundle` (esbuild, output: `dist_ts_web/bundle.js`)
|
||||
- **Full build:** `pnpm run build` (= `buildRust && bundle`)
|
||||
- **Start server:** `pnpm run start` (runs `tsx ts/sipproxy.ts`)
|
||||
|
||||
## Persistent files
|
||||
|
||||
- `.nogit/config.json` — app config (providers, devices, routes, voiceboxes, IVR menus)
|
||||
- `.nogit/voicemail/{boxId}/` — voicemail WAV files + `messages.json` index
|
||||
- `.nogit/prompts/` — cached TTS prompts for IVR menus
|
||||
|
||||
Reference in New Issue
Block a user