freehop 0.1.0-alpha.2 → 0.1.0-alpha.3

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
@@ -13,7 +13,7 @@
13
13
  ![License: Apache-2.0](https://img.shields.io/badge/code-Apache--2.0-303055)
14
14
  ![Docs: CC BY 4.0](https://img.shields.io/badge/docs-CC%20BY%204.0-303055)
15
15
  ![Home-built network test matrix: 40/40](https://img.shields.io/badge/home%E2%80%91built%20network%20tests-40%2F40-096e72)
16
- ![Unit tests: 167/167](https://img.shields.io/badge/unit%20tests-167%2F167-096e72)
16
+ ![Unit tests: 173/173](https://img.shields.io/badge/unit%20tests-173%2F173-096e72)
17
17
  ![Status: alpha](https://img.shields.io/badge/status-alpha-ef3b2c)
18
18
 
19
19
  </div>
@@ -88,8 +88,8 @@ arrive.
88
88
 
89
89
  **40/40 home-lab network test runs passed** on 2 October 2026 after security hardening,
90
90
  including gateways that relay only inside their session. That matrix has not been rerun for
91
- alpha.2. These are repeatable simulated-network results, not field reliability evidence. Other checks:
92
- - **167/167 unit tests**, including the RFC 5769 STUN vectors and the security regressions.
91
+ this revision. These are repeatable simulated-network results, not field reliability evidence. Other checks:
92
+ - **173/173 unit tests**, including the RFC 5769 STUN vectors and the security regressions.
93
93
  - **coturn's own test client** against Freehop's TURN server: 800/800 messages over UDP and 800/800 over TCP, 0 lost.
94
94
  - **Browser suites:** multi-gate with every gate shut down mid-call, kick/rekey, a public WebTorrent tracker as the only gate, and the SDK example app.
95
95
 
@@ -111,7 +111,7 @@ Freehop is an independent project; any app can use the same public SDK.
111
111
  ## Quickstart
112
112
 
113
113
  ```bash
114
- npm install freehop@alpha # current alpha: 0.1.0-alpha.2
114
+ npm install freehop@alpha # install the current alpha release
115
115
  ```
116
116
 
117
117
  **Backend:** decide who is in a room.
package/SDK.md CHANGED
@@ -55,6 +55,7 @@ session.on('path', ({ peer, kind, via }) => …); // direct | gateway |
55
55
  session.on('peer-left', ({ id, reason }) => …);
56
56
  await session.setMicrophone(false); await session.setCamera(true);
57
57
  const levels = await session.levels(); // speaking indicators
58
+ await session.setPeerMuted(peerId, true); // silence one member locally
58
59
  await session.update(newTicket, { dropped: [kickedPeerId] }); // after a kick
59
60
  await session.switchDevice('audio', deviceId); // another microphone or camera, no renegotiation
60
61
  await session.send({ type: 'chat', text: 'hi' }); // app data to everyone (or { to: peerId })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freehop",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "Open-source peer-to-peer voice, video and data SDK for browser and desktop apps, with signalling gates and session-member media gateways.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,6 +27,10 @@
27
27
  "scripts": {
28
28
  "test": "node --test test/*.test.mjs",
29
29
  "test:browsers": "node test/browser/smoke.mjs chromium,firefox,webkit --tls && node test/browser/multigate.mjs && node test/browser/rekey.mjs && node test/browser/example-app.mjs",
30
+ "unitgate": "node tools/unitgate.mjs",
31
+ "exportsgate": "node tools/exportsgate.mjs",
32
+ "docsyncgate": "node tools/docsyncgate.mjs",
33
+ "ship": "node tools/ship.mjs",
30
34
  "gate": "node bin/freehop-gate.mjs",
31
35
  "example": "node examples/minimal/server.mjs"
32
36
  },
@@ -121,6 +121,8 @@ export class Room extends Emitter {
121
121
  offerTimeouts: 0, mediaStalls: 0, appSent: 0, appReceived: 0, appDropped: 0 };
122
122
  this.appBudget = new Map(); // sender -> { tokens, at }
123
123
  this.videoQuality = new Map(); // peer -> per-link send/receive controller state
124
+ this.mutedPeers = new Set(); // peers muted locally (setPeerMuted)
125
+ this.remoteAudio = new Map(); // peer -> Set of live remote audio tracks, for flipping enabled
124
126
  // Preferred capture devices (options.devices, switchDevice); every later capture uses them.
125
127
  if (!deviceId(options.devices?.audio) || !deviceId(options.devices?.video)) throw new TypeError('Invalid capture device id');
126
128
  this.devices = { audio: options.devices?.audio ?? null, video: options.devices?.video ?? null };
@@ -346,8 +348,10 @@ export class Room extends Emitter {
346
348
  this.known.delete(peer); this.hintedAt.delete(peer); this.caps.delete(peer); this.presence.delete(peer); this.early.delete(peer); this.outbox.delete(peer);
347
349
  this.sendProgress.delete(peer); this.meshBudget.delete(peer); this.appBudget.delete(peer); this.videoQuality.delete(peer);
348
350
  this.lastContact.delete(peer); this.gatewayCreds.delete(`${this.tag8}:${peer}`);
349
- // The replay window survives a soft departure: a returning peer keeps its counter.
350
- if (reason !== 'gone') this.sequences.delete(peer);
351
+ this.remoteAudio.delete(peer);
352
+ // A softly departed ('gone') peer may return with the same id: its replay window and local
353
+ // mute survive. Terminal departures ('bye', 'dropped', 'rekey') clear both.
354
+ if (reason !== 'gone') { this.sequences.delete(peer); this.mutedPeers.delete(peer); }
351
355
  this.departed.set(peer, { at: Date.now(), reason });
352
356
  if (this.departed.size > 256) this.departed.delete(this.departed.keys().next().value);
353
357
  for (const [key, b] of [...this.bridgePending]) if (b.a === peer || b.b === peer) {
@@ -900,6 +904,18 @@ export class Room extends Emitter {
900
904
  setMicrophone(enabled) { return this.#enableTrack('audio', enabled, MIC); }
901
905
  setCamera(enabled) { return this.#enableTrack('video', enabled, CAM); }
902
906
 
907
+ /**
908
+ * Silences one member locally: enabled = false on their current and future remote audio
909
+ * tracks. Media still arrives and nothing is signalled, so the peer cannot tell. Muting
910
+ * an unknown peer is allowed and applies when their audio arrives. Video is untouched.
911
+ */
912
+ setPeerMuted(peer, muted) {
913
+ if (typeof peer !== 'string' || !PEER_ID.test(peer)) throw new TypeError('Invalid peer id');
914
+ if (typeof muted !== 'boolean') throw new TypeError('muted must be a boolean');
915
+ if (muted) this.mutedPeers.add(peer); else this.mutedPeers.delete(peer);
916
+ for (const track of this.remoteAudio.get(peer) ?? []) track.enabled = !muted;
917
+ }
918
+
903
919
  constraintsFor(kind, base) { const id = this.devices[kind]; return id ? { ...base, deviceId: { exact: id } } : base; }
904
920
 
905
921
  /**
@@ -1193,6 +1209,13 @@ export class Room extends Emitter {
1193
1209
  }
1194
1210
 
1195
1211
  onRemoteTrack(link, origin, track, stream) {
1212
+ if (track.kind === 'audio') {
1213
+ let set = this.remoteAudio.get(origin);
1214
+ if (!set) this.remoteAudio.set(origin, set = new Set());
1215
+ set.add(track);
1216
+ track.addEventListener('ended', () => set.delete(track));
1217
+ if (this.mutedPeers.has(origin)) track.enabled = false;
1218
+ }
1196
1219
  this.emit('track', { peer: origin, via: origin === link.id ? null : link.id, track, stream });
1197
1220
  if (origin !== link.id) return;
1198
1221
  // A forwarder keeps forwarding tracks the origin adds later (e.g. camera switched on).