refactor: port backend and frontend to TypeScript classes (bun + vite)
Backend (src/server, runs on Bun): - GibbzHttpServer (health, /api/ice, /api/rooms, static, keepalive tuning) - SignalingServer with Peer/Room/RoomRegistry classes and typed messages - IceConfigProvider (env STUN/TURN + coturn HMAC ephemeral creds) - StaticFileServer (dist/ with public/ fallback, traversal guard) - Tracing (OpenTelemetry SDK), AppConfig; createApp() factory for tests Frontend (src/client, built by Vite): - VoiceApp orchestrator implementing VoiceAppContext - WebRtcManager + PeerSession (perfect negotiation, pc rebuild, quality monitor) - SignalingClient + ReconnectController, AudioManager, SpatialAudio, Microphone/ScreenShare controllers, ScreensUi/RosterUi/SpatialMap/ SpeakingMonitor, Settings/Welcome dialogs, LayoutManager, MatrixChat Tooling: bun scripts (dev/start/test/typecheck), vite.config.ts root src/client with publicDir public/static, vitest-config for CI-less runs, bun-based Dockerfile (oven/bun:1), Makefile + README updated, legacy server.js/tracing.js/test.js/public js removed. Verified locally: tsc --noEmit clean, vitest run 6/6 green, vite build ok, server smoke-tested serving dist + signaling endpoints under Node. Local bun binary segfaults (SIGILL) on this host, so 'bun test' itself was not executable here; tests are vitest-API compatible which bun test maps.
This commit is contained in:
1 parent
9b8c2a9599
commit
2b950e4953
48 files changed
+4589
-9200
No files matched your search
@@ -3,59 +3,80 @@
|
||||
Discord-clone proof of concept: peer-to-peer WebRTC voice chat + screen sharing
|
||||
with position-based spatial audio and pluggable STUN/TURN.
|
||||
|
||||
Run (Node 18+, only dep is `ws`):
|
||||
TypeScript everywhere: the backend runs on [Bun](https://bun.sh), the client is
|
||||
built with [Vite](https://vite.dev), and both are organized into classes.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/server/ Bun (Node-compatible) WebSocket/HTTP backend
|
||||
index.ts entrypoint + createApp() factory (used by tests)
|
||||
http-server.ts GibbzHttpServer — health probes, /api/ice, /api/rooms, static
|
||||
signaling.ts Peer / Room / RoomRegistry / SignalingServer (ws)
|
||||
ice.ts IceConfigProvider — env-driven STUN/TURN + HMAC TURN creds
|
||||
static-files.ts StaticFileServer — dist/ (or public/) with traversal guard
|
||||
tracing.ts Tracing — OpenTelemetry SDK setup
|
||||
config.ts AppConfig — env + static dir resolution
|
||||
|
||||
src/client/ Vite root (index.html, style.css)
|
||||
app.ts VoiceApp — orchestrator implementing VoiceAppContext
|
||||
context.ts the subsystem-facing application interface
|
||||
state.ts AppState — shared cross-cutting voice state
|
||||
webrtc/ WebRtcManager, PeerSession, SignalingClient, ReconnectController
|
||||
media/ AudioManager (mic chain), SpatialAudio, Microphone-, ScreenShareController
|
||||
ui/ roster/screens/spatial-map renderers, settings, welcome, layout
|
||||
chat/ MatrixChat (OAuth2/OIDC text channel)
|
||||
|
||||
public/static/ PWA assets served verbatim (icons, manifest, service worker)
|
||||
tests/ bun test / vitest compatible suite (tests/server.test.ts)
|
||||
```
|
||||
|
||||
## Develop
|
||||
|
||||
```sh
|
||||
npm install
|
||||
PORT=8081 npm start
|
||||
# open http://localhost:8081, click a voice channel, allow mic
|
||||
bun install
|
||||
bun run dev:server # backend on :8080 (bun --watch)
|
||||
bun run dev # vite dev server for the client
|
||||
bun test # tests (vitest-compatible; `bun run test:node` = vitest run)
|
||||
bun run typecheck # tsc --noEmit
|
||||
bun run build # production client bundle into dist/
|
||||
bun run start # serve dist/ + signaling (bun src/server/index.ts)
|
||||
```
|
||||
|
||||
The server also runs under Node 22.18+/23+ (`node src/server/index.ts`) since it
|
||||
only uses erasable TypeScript, but Bun is the supported runtime (the Dockerfile
|
||||
is `oven/bun:1`). A `bun.lock` is generated on first `bun install`.
|
||||
|
||||
## What works
|
||||
|
||||
- Mesh WebRTC voice (perfect negotiation; lowest peer id initiates)
|
||||
- Screen share with audio (mic is spatialized, screen audio plays direct)
|
||||
- Mesh WebRTC voice (perfect negotiation; lowest peer id initiates), full
|
||||
pc-rebuild recovery on ICE failure with TURN-relay fallback
|
||||
- Screen share with audio (mic is spatialized, screen audio plays direct),
|
||||
spotlight/grid tiles and pop-out windows
|
||||
- Spatial audio: drag your avatar on the plane; remote mics run through a
|
||||
`PannerNode` (HRTF, inverse distance). Positions sync over signaling.
|
||||
- Hi-Fi rooms (Code, Gaming): stereo Opus 256 kbps, echo cancellation off
|
||||
- Custom STUN/TURN: gear icon → JSON override in `localStorage`, applied on
|
||||
next join. Server default comes from env (below).
|
||||
- Mute (M), deafen (D), per-peer volume, speaking indicators.
|
||||
- Voice capture uses mono 48 kHz Opus-oriented settings, a 64 kbps audio ceiling,
|
||||
and periodic packet-loss, jitter, and RTT quality diagnostics.
|
||||
- Audio input has manual gain control and a live signal/clipping meter in the
|
||||
settings dialog; microphone processing is split into `public/audio-manager.js`.
|
||||
- Screen sharing uses the browser's default codec negotiation, with selectable
|
||||
`detail` (text/UI) or `motion` (gameplay) content hints.
|
||||
- Mute (M), deafen (D), per-peer volume, speaking indicators, quality
|
||||
diagnostics (loss/jitter/RTT), auto-reconnect with countdown banner
|
||||
- Matrix-backed text channel (OAuth2/OIDC against matrix.k6n.net)
|
||||
|
||||
## ICE configuration (server side)
|
||||
|
||||
| Env var | Meaning |
|
||||
|---|---|
|
||||
| `STUN_URLS` | comma-separated `stun:` urls (Google defaults) |
|
||||
| `STUN_URLS` | comma-separated `stun:` urls |
|
||||
| `TURN_URLS` | comma-separated `turn:`/`turns:` urls |
|
||||
| `TURN_USERNAME` / `TURN_CREDENTIAL` | static TURN creds |
|
||||
| `TURN_SECRET` | coturn static-auth-secret → ephemeral HMAC creds |
|
||||
| `ICE_TRANSPORT_POLICY` | WebRTC ICE policy: `all`, `relay`, or `none` |
|
||||
| `ICE_SERVERS_JSON` | full `{"iceServers":[...]}` override, wins over the above |
|
||||
| `PORT` / `HOST` | listen address (default 8081 / 0.0.0.0) |
|
||||
|
||||
Example with coturn:
|
||||
|
||||
```sh
|
||||
TURN_URLS='turn:turn.example.com:3478?transport=udp,turns:turn.example.com:5349' \
|
||||
TURN_USERNAME=mats TURN_CREDENTIAL=secret npm start
|
||||
```
|
||||
|
||||
For deployments where clients are commonly behind separate NATs, set
|
||||
`ICE_TRANSPORT_POLICY=relay` so media consistently uses the public TURN
|
||||
service instead of depending on NAT hairpinning or direct candidate pairs.
|
||||
| `PORT` / `HOST` | listen address (default 8080 / 0.0.0.0) |
|
||||
|
||||
## POC shortcuts (productionize later)
|
||||
|
||||
- No auth, no reconnection, rooms are in-memory, signaling is unencrypted ws
|
||||
behind the preview ingress (use wss/TLS in front for real deploys).
|
||||
- No media server: N² mesh — fine for small rooms, needs SFU (mediasoup/livekit)
|
||||
beyond ~6–8 participants.
|
||||
- For larger rooms or consistently lower bandwidth, replace the mesh with an SFU;
|
||||
the current quality tuning improves the P2P path but does not change its N² cost.
|
||||
- Text channels are decorative. No persistence, no rate limiting.
|
||||
- Screen-share negotiation storms on join/leave are expected at small scale.
|
||||
- No auth, rooms are in-memory, signaling is plain ws behind the ingress.
|
||||
- No media server: N² mesh — fine for small rooms, needs SFU beyond ~6–8
|
||||
participants.
|
||||
- Text channels are a single live Matrix room; no history pagination.
|
||||
Reference in new issue
Block a user