wireweave 0.3.46 → 0.3.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -151,6 +151,31 @@ voice.addEventListener('participants', e => console.log(e.detail.list));
151
151
 
152
152
  Voice carries every empirically-discovered reliability pattern: perfect negotiation (RFC 8840), ICE restart on disconnect, track-stall detection, SFU hub election (mesh→star at 3+ peers), exponential-backoff reconnect.
153
153
 
154
+ ### voice quality/input settings
155
+
156
+ `VoiceSession` (and `ensureVoice`) accepts real, constructor-configurable options for input mode, mic sensitivity, echo/noise handling, and Opus quality — every one of them threaded into an actual `getUserMedia`/`RTCRtpSender`/SDP call site, not just stored:
157
+
158
+ ```js
159
+ const voice = ww.ensureVoice({
160
+ serverId: 'abc:xyz',
161
+ displayName: 'you',
162
+ pttMode: true, // push-to-talk (default) vs. open-mic/VAD mode
163
+ micSensitivity: 0.045, // RMS threshold the speaker-activity detector uses
164
+ noiseSuppression: true, // getUserMedia audio constraints — real MediaTrackConstraints
165
+ echoCancellation: true,
166
+ autoGainControl: true,
167
+ audioQuality: 'high', // Opus bitrate tier: 'low' (16kbps) | 'medium' (32kbps) | 'high' (48kbps) | 'max' (64kbps)
168
+ dtx: true // discontinuous transmission (silence suppression), SDP fmtp usedtx=1
169
+ });
170
+ ```
171
+
172
+ - **`pttMode`** (push-to-talk vs. voice-activity/open-mic): `true` (default) starts the session muted; the caller opens the mic via `setMuted(false)`/`requestTransmit()`. `false` starts unmuted — the mic stays live and the speaker-activity detector (RMS threshold) drives the `isSpeaking` UI flag instead of gating transmission. Live-togglable: `voice.setPttMode(false)`.
173
+ - **`micSensitivity`** — the RMS level (0–1) above which a stream counts as "speaking" in the speaker-activity poller. Live-togglable: `voice.setMicSensitivity(0.09)`.
174
+ - **`noiseSuppression` / `echoCancellation` / `autoGainControl`** — real [`MediaTrackConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints) passed straight into the `getUserMedia({ audio: {...} })` call `connect()` makes.
175
+ - **`audioQuality`** — an Opus bitrate ladder tier (`low`/`medium`/`high`/`max`, mapping to 16/32/48/64 kbps) applied via the real `RTCRtpSender.setParameters()` call on every connected peer's audio sender. Live-togglable and re-applies immediately to every open connection: `voice.setAudioQuality('low')`.
176
+ - **`dtx`** — discontinuous transmission (silence suppression), negotiated per the real WebRTC spec via the Opus `a=fmtp` SDP line (`usedtx=1`), not an `RTCRtpEncodingParameters` field (that isn't where DTX lives in the real spec). Applied at every offer/answer/ICE-restart. Live-togglable: `voice.setDtx(false)` (takes effect on the next SDP exchange for already-open peers).
177
+ - **Per-room bandwidth shaping for large rooms** — this module is audio-only (no video sender exists anywhere in it), so real per-layer RTP simulcast (which is video-only in every browser implementation) doesn't apply. The honest, reachable analog is what's already built: SFU hub election (`_sfuElect`/`_sfuRankCandidates`) picks the highest-uplink participant as the hub and fans out audio to everyone via zero-copy `replaceTrack`, so bandwidth concentrates on whoever the room is actually routing through, combined with the per-connection Opus bitrate ladder above.
178
+
154
179
  ## node usage (relay + auth only)
155
180
 
156
181
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wireweave",
3
- "version": "0.3.46",
3
+ "version": "0.3.48",
4
4
  "description": "nostr + webrtc voice + data SDK. networking layer for 247420 projects.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/relay-pool.js CHANGED
@@ -64,7 +64,15 @@ class RelayHealth {
64
64
  this.rank = 50;
65
65
  }
66
66
 
67
- recordConnectAttempt() { this.attempts++; }
67
+ // Recomputes rank on every attempt, not just on a successful signal —
68
+ // otherwise a relay that NEVER once connects (rank frozen at the ctor's
69
+ // neutral 50 forever, since recordConnectLatency/recordSustainedConnection/
70
+ // recordEoseLatency — the only other rank-recomputing call sites — never
71
+ // fire for it) reads as perpetually "average" no matter how many failed
72
+ // attempts pile up, defeating both healthReport() ranking and
73
+ // _maybeRotate's rank-gap rotation trigger for the exact "never connects
74
+ // at all" relay auto-rotation exists to route around.
75
+ recordConnectAttempt() { this.attempts++; this.rank = computeRank(this); }
68
76
 
69
77
  recordConnectLatency(ms) {
70
78
  this.connectLatencyMs = ewma(this.connectLatencyMs, ms);
@@ -250,7 +258,13 @@ export class RelayPool extends EventTarget {
250
258
  // Swap the worst currently-active relay for the best-ranked unused
251
259
  // candidate (fallback pool, or a previously-tried relay we disconnected
252
260
  // from) when the gap is large enough to be worth the churn of a new
253
- // connection. Never rotates below MIN_ACTIVE_RELAYS active URLs.
261
+ // connection. Never rotates below MIN_ACTIVE_RELAYS active URLs. Called
262
+ // from every ws.onclose (both the sustained-then-dropped branch AND the
263
+ // never-connected/failed-fast branch) — a relay that never manages to
264
+ // connect at all still needs evaluating here, since that IS the
265
+ // "consistently unhealthy" case rotation exists to route around, and its
266
+ // rank already reflects the failures via recordConnectAttempt's own
267
+ // computeRank call once attempts >= 2 (the sample floor enforced below).
254
268
  _maybeRotate() {
255
269
  if (!this.autoRotate || this._closed) return;
256
270
  const MIN_ACTIVE_RELAYS = 2;
@@ -378,11 +392,17 @@ export class RelayPool extends EventTarget {
378
392
  relay.reconnectDelay = 1000;
379
393
  health.recordSustainedConnection();
380
394
  this._scheduleSaveHealth();
381
- this._maybeRotate();
382
395
  } else {
383
396
  relay.failCount++;
384
397
  relay.reconnectDelay = Math.min(relay.reconnectDelay * 2, 30000);
385
398
  }
399
+ // Evaluate rotation on EVERY close, not just a sustained-then-dropped
400
+ // connection — a relay that never manages to connect at all (the
401
+ // `else` branch above) is exactly the "consistently unhealthy"
402
+ // relay auto-rotation exists to route around, and its rank already
403
+ // reflects that via computeRank's uptime/latency components once
404
+ // `attempts >= 2` (the sample floor _maybeRotate itself enforces).
405
+ this._maybeRotate();
386
406
  relay._openedAt = null;
387
407
  if (this._closed) return;
388
408
  const t = setTimeout(() => this._open(url), jitter(relay.reconnectDelay));