matsandClaude Sonnet 5 78d97ee733
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 25s
Add a second, LAN-only TURN relay to avoid hairpin NAT entirely
The existing coturn instance advertises the router's public IP as its
relay address (external-ip=94.255.139.35) — correct for real external
users, but when both peers are on the same home network as the router,
relay traffic has to hairpin through the router's own WAN IP. That only
worked with hwnat (hardware NAT offload) disabled, which pegged the
router's CPU at 93% for well under half its rated throughput — not an
acceptable trade for the whole household's network.

Instead of touching hwnat, added a second coturn deployment
(coturn-lan, own secret, own relay port range 49210-49220) exposed via
its own MetalLB LoadBalancer IP (10.10.3.46) on the same LAN segment.
That address is answered directly via L2 ARP — no NAT, no hairpin,
hwnat never touches it — and it's deliberately not port-forwarded from
WAN, so it's LAN-only by construction.

iceConfig() now appends this as an extra iceServers entry whenever
TURN_LAN_HOST/TURN_LAN_SECRET are set. It's harmless for anyone not on
that network — the address is simply unreachable and ICE ignores it —
and WebRTC tries every configured server, using whichever candidates
actually connect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:13:25 +02:00

gibbz voice — POC

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):

npm install
PORT=8081 npm start
# open http://localhost:8081, click a voice channel, allow mic

What works

  • Mesh WebRTC voice (perfect negotiation; lowest peer id initiates)
  • Screen share with audio (mic is spatialized, screen audio plays direct)
  • Spatial audio: drag your avatar on the plane; remote mics run through a PannerNode (HRTF, inverse distance). Positions sync over signaling.
  • 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.

ICE configuration (server side)

Env var Meaning
STUN_URLS comma-separated stun: urls (Google defaults)
TURN_URLS comma-separated turn:/turns: urls
TURN_USERNAME / TURN_CREDENTIAL static TURN creds
ICE_SERVERS_JSON full {"iceServers":[...]} override, wins over the above
PORT / HOST listen address (default 8081 / 0.0.0.0)

Example with coturn:

TURN_URLS='turn:turn.example.com:3478?transport=udp,turns:turn.example.com:5349' \
TURN_USERNAME=mats TURN_CREDENTIAL=secret npm start

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.
  • Text channels are decorative. No persistence, no rate limiting.
  • Screen-share negotiation storms on join/leave are expected at small scale.
S
Description
No description provided
Readme
38 MiB
0 Stars 1 Watchers 0 Forks
Languages
JavaScript 75.1%
CSS 16.2%
HTML 7.5%
Makefile 0.9%
Dockerfile 0.3%