slice-1: WebRTC media loopback — the media-core proof #1
74
LEARNING.md
Normal file
74
LEARNING.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# LEARNING.md — to learn concept X, read file Y
|
||||
|
||||
This index maps a Rust concept you might be learning to the file where
|
||||
slice 1 makes the concept concrete. Each entry is a worked example you
|
||||
can read in `cargo doc --open` plus the source file itself.
|
||||
|
||||
## Concepts + pointers
|
||||
|
||||
- **Newtype pattern (type-safety via single-field wrappers)** →
|
||||
`crates/rutster-call-model/src/lib.rs` — `ChannelId(Uuid)`. The newtype
|
||||
stops us from mixing up a `ChannelId` with some future `SessionId` at
|
||||
the type-system level. Compile-enforced where a comment could only ask.
|
||||
|
||||
- **`enum` for closed state sets + exhaustive `match`** →
|
||||
`crates/rutster-call-model/src/lib.rs` — `ChannelState` (New →
|
||||
Connecting → Connected → Closing → Closed). Exhaustiveness checking
|
||||
forces every `match` to consider each state; adding a state later
|
||||
surfaces every site that needs handling.
|
||||
|
||||
- **Sans-IO pattern (no I/O inside the library; input via method calls,
|
||||
output via return values)** → `crates/rutster-media/src/loop_driver.rs`
|
||||
— the str0m poll loop. `Rtc::handle_input` takes a network packet as a
|
||||
struct argument, not from a socket the library owns; `poll_output`
|
||||
returns `Transmit` packets the caller sends. Fully testable without a
|
||||
network — str0m integration tests use this property to drive faster
|
||||
than realtime.
|
||||
|
||||
- **Trait design for extension points (a futures-compatible seam)** →
|
||||
`crates/rutster-media/src/pcm.rs` — the `AudioSource` / `AudioSink`
|
||||
traits. Slice 1 wires an `EchoAudioPipe` between them; step 2 swaps
|
||||
that for a real WSS tap client without touching `RtcSession`. The
|
||||
traits describe *what* the splice point does, not *how* it's filled.
|
||||
|
||||
- **Error enums with `thiserror` + hot-path match-and-continue** →
|
||||
`crates/rutster-media/src/lib.rs` (`MediaError`) and
|
||||
`crates/rutster-media/src/opus_codec.rs` (`OpusDecoder::decode` returns
|
||||
`Option<PcmFrame>`). Cold path: `thiserror`-derived enum + `?`. Hot
|
||||
path: match-and-continue, never `?`, never panic — "drop + observe,
|
||||
don't crash" (spec §3.8).
|
||||
|
||||
- **`Arc<Mutex<T>>` vs `Arc<RwLock<T>>` — when each is right** →
|
||||
`crates/rutster/src/session_map.rs`. The `RtcSession` lives behind
|
||||
`Arc<Mutex<...>>` because every access mutates it (str0m's `&mut self`
|
||||
contract) — `RwLock`'s read-mode would be useless. Comment on the
|
||||
struct explains the trade-off.
|
||||
|
||||
- **`DashMap` for sharded concurrent maps** →
|
||||
`crates/rutster/src/session_map.rs`. `DashMap` shards its inner map so
|
||||
two handlers operating on different `ChannelId`s don't contend;
|
||||
`HashMap` wrapped in a single `Mutex` would serialize every access.
|
||||
|
||||
- **str0m 0.21's single-mutation invariant** →
|
||||
`crates/rutster-media/src/loop_driver.rs`. Mutate (handle_input /
|
||||
Writer::write) → drain `poll_output` to `Output::Timeout` → next
|
||||
mutate. Violating this leaves str0m in an inconsistent state.
|
||||
|
||||
- **tokio graceful shutdown via signal handlers** →
|
||||
`crates/rutster/src/main.rs` (`shutdown_signal`). Ctrl-C / SIGTERM
|
||||
drops the AppState; the AppState drops the DashMap; the DashMap drops
|
||||
every RtcSession. No in-flight call preservation in slice 1.
|
||||
|
||||
- **`include_str!` for embedding static assets** →
|
||||
`crates/rutster/src/routes.rs` (`include_str!("../static/index.html")`).
|
||||
The HTML test client is compiled into the binary at build time — no
|
||||
separate file to ship, no disk IO to serve it.
|
||||
|
||||
## How to read
|
||||
|
||||
1. `cargo doc --open` — every module has a `//!` doc comment; the doc
|
||||
tree is the high-level map.
|
||||
2. Pick a concept above; open the named file. The first occurrence of
|
||||
each non-obvious pattern has a `//` comment explaining *why*.
|
||||
3. Cross-ref back to the spec sections cited inline (`spec §3.8`,
|
||||
`ADR-0002`, etc.) for the architecture-level rationale.
|
||||
42
README.md
42
README.md
@@ -8,6 +8,48 @@ or its architecture.
|
||||
> technical builder uses to stand up a contact center* — and re-aims it at a category AI is
|
||||
> actively disrupting, instead of a PBX category UCaaS already ate.
|
||||
|
||||
## Slice 1 dev loop (WebRTC media loopback)
|
||||
|
||||
> Build prerequisite: install libopus (the `opus` crate links it via FFI):
|
||||
> ```bash
|
||||
> sudo apt-get install -y libopus-dev # Debian/Ubuntu
|
||||
> # Fedora: sudo dnf install -y opus-devel
|
||||
> # macOS: brew install opus
|
||||
> ```
|
||||
> This is the one system dependency in slice 1. Opus is FFI per PORT_PLAN
|
||||
> §7's "🦀 Core (FFI)" disposition — the codec surface Rust doesn't need
|
||||
> to re-implement.
|
||||
|
||||
Run the server:
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
# listening on http://0.0.0.0:8080
|
||||
```
|
||||
|
||||
Open a browser to `http://localhost:8080/`, click "Start call", grant
|
||||
microphone permission. Speak — you should hear yourself back within
|
||||
~200 ms (no perceptible delay). Click "Hang up" to tear down; server
|
||||
logs `Closing → Closed`.
|
||||
|
||||
Verbose tracing:
|
||||
|
||||
```bash
|
||||
RUST_LOG=rutster=debug cargo run
|
||||
```
|
||||
|
||||
### Slice 1 "done" checklist (spec §6.5)
|
||||
|
||||
On a clean checkout:
|
||||
1. `cargo test --all` passes.
|
||||
2. `cargo fmt --check` passes.
|
||||
3. `cargo clippy -- -D warnings` passes.
|
||||
4. `cargo deny check` passes.
|
||||
5. `cargo run` + browser manual e2e: speak → hear echo within ~200 ms.
|
||||
6. Hang-up button triggers `Closing → Closed` in server logs.
|
||||
7. Every stub crate compiles; its doc-comment names its scheduled step.
|
||||
8. `LEARNING.md` indexes at least 5 "to learn X, read Y" pointers.
|
||||
|
||||
## Why it exists
|
||||
|
||||
Asterisk won because contact centers were **built on it** (Vicidial, GOautodial, a thousand
|
||||
|
||||
21
fuzz/README.md
Normal file
21
fuzz/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# fuzz/ — cargo-fuzz harness directory (placeholder)
|
||||
|
||||
**Status:** placeholder. Not yet a cargo-fuzz project — just the directory.
|
||||
Fuzz harnesses land at spearhead step 5 (PSTN trunk) alongside the
|
||||
SIP/SDP/RTP wire parsers (PORT_PLAN §10 mandates continuous fuzzing of
|
||||
every wire parser). Slice 1 has no hostile-bytes surface (the browser is
|
||||
trusted), so no harnesses here yet — the `fuzz/` dir pre-paves the
|
||||
layout. Populating this directory with a real `cargo-fuzz` project
|
||||
(`fuzz/Cargo.toml` + `fuzz/fuzz_targets/*.rs`) happens at step 5.
|
||||
|
||||
If you're at step 5, replace this README with that structure:
|
||||
- `fuzz/Cargo.toml` — cargo-fuzz manifest.
|
||||
- `fuzz/fuzz_targets/sip_parser.rs` — fuzz the SIP parser.
|
||||
- `fuzz/fuzz_targets/sdp_parser.rs` — fuzz the SDP parser.
|
||||
- `fuzz/fuzz_targets/rtp_packet.rs` — fuzz the RTP packet parser.
|
||||
- CI job running a short fuzz burst on each PR (the cargo-fuzz integration
|
||||
lands in `.github/workflows/` at that point).
|
||||
|
||||
The hot-path "drop + observe, don't crash" policy (spec §3.8) is what the
|
||||
future harnesses assert against: throw arbitrary bytes at the parser,
|
||||
assert it returns an error or drops silently — never panics.
|
||||
Reference in New Issue
Block a user