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 +4 -4
- package/SDK.md +1 -0
- package/package.json +5 -1
- package/src/client/room.mjs +25 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|

|
|
14
14
|

|
|
15
15
|

|
|
16
|
-

|
|
17
17
|

|
|
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
|
-
|
|
92
|
-
- **
|
|
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
|
|
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.
|
|
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
|
},
|
package/src/client/room.mjs
CHANGED
|
@@ -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
|
-
|
|
350
|
-
|
|
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).
|