@yz-social/webrtc 0.1.9 → 0.1.11
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/index.js +17 -12
- package/package.json +1 -1
- package/spec/portal.js +14 -8
- package/spec/webrtcCapacitySpec.js +26 -24
package/index.js
CHANGED
|
@@ -4,14 +4,14 @@ const wrtc = (typeof(process) === 'undefined') ? globalThis : (await import('#wr
|
|
|
4
4
|
export class WebRTC {
|
|
5
5
|
static iceServers = [ // Some default stun and even turn servers.
|
|
6
6
|
|
|
7
|
-
{ urls: 'stun:stun.l.google.com:19302'},
|
|
7
|
+
{ urls: 'stun:stun.l.google.com:19302'}, // As of 5/5/26, Edge and Firefox still require iceServers to be non-empty, even for localhost peers.
|
|
8
8
|
// https://freestun.net/ Currently 50 KBit/s. (2.5 MBit/s fors $9/month)
|
|
9
|
-
{ urls: 'stun:freestun.net:3478' },
|
|
9
|
+
//{ urls: 'stun:freestun.net:3478' },
|
|
10
10
|
|
|
11
11
|
//{ urls: 'turn:freestun.net:3478', username: 'free', credential: 'free' },
|
|
12
12
|
// Presumably traffic limited. Can generate new credentials at https://speed.cloudflare.com/turn-creds
|
|
13
13
|
// Also https://developers.cloudflare.com/calls/ 1 TB/month, and $0.05 /GB after that.
|
|
14
|
-
{ urls: 'turn:turn.speed.cloudflare.com:50000', username: '826226244cd6e5edb3f55749b796235f420fe5ee78895e0dd7d2baa45e1f7a8f49e9239e78691ab38b72ce016471f7746f5277dcef84ad79fc60f8020b132c73', credential: 'aba9b169546eb6dcc7bfb1cdf34544cf95b5161d602e3b5fa7c8342b2e9802fb' }
|
|
14
|
+
//{ urls: 'turn:turn.speed.cloudflare.com:50000', username: '826226244cd6e5edb3f55749b796235f420fe5ee78895e0dd7d2baa45e1f7a8f49e9239e78691ab38b72ce016471f7746f5277dcef84ad79fc60f8020b132c73', credential: 'aba9b169546eb6dcc7bfb1cdf34544cf95b5161d602e3b5fa7c8342b2e9802fb' }
|
|
15
15
|
|
|
16
16
|
// See also:
|
|
17
17
|
// https://fastturn.net/ Currently 500MB/month? (25 GB/month for $9/month)
|
|
@@ -20,7 +20,7 @@ export class WebRTC {
|
|
|
20
20
|
];
|
|
21
21
|
cleanup() { // Attempt to allow everything to be garbage-collected.
|
|
22
22
|
if (!this.pc) return;
|
|
23
|
-
this.pc.onicecandidate = this.pc.ondatachannel = this.pc.onnegotiationneeded = this.pc.onconnectionstatechange = this.pc.oniceconnectionstatechange = null;
|
|
23
|
+
this.pc.onicecandidate = this.pc.ondatachannel = this.pc.onnegotiationneeded = this.pc.onconnectionstatechange = this.pc.oniceconnectionstatechange = this.pc.onicecandidateerror = null;
|
|
24
24
|
delete this.pc;
|
|
25
25
|
delete this.dataChannelPromises;
|
|
26
26
|
delete this.dataChannelOursPromises;
|
|
@@ -28,9 +28,11 @@ export class WebRTC {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Number of instances at a time (if previous have been garbage collected), as of 1/27/26:
|
|
31
|
-
static suggestedInstancesLimit =
|
|
32
|
-
globalThis.navigator.
|
|
33
|
-
|
|
31
|
+
static suggestedInstancesLimit =
|
|
32
|
+
globalThis.navigator.vendor?.startsWith('Apple') ? 150 : // Safari will open 256, but it won't reliably keep them all open at once.
|
|
33
|
+
globalThis.navigator.userAgent?.includes('Firefox') ? 150 : // I can sometimes get maybe a dozen more, but not with refresh.
|
|
34
|
+
(typeof(globalThis.process) !== 'undefined') ? 249 : // NodeJS
|
|
35
|
+
256;
|
|
34
36
|
constructor({configuration = {iceServers: WebRTC.iceServers}, ...properties}) {
|
|
35
37
|
Object.assign(this, properties);
|
|
36
38
|
|
|
@@ -63,9 +65,12 @@ export class WebRTC {
|
|
|
63
65
|
this.signal({ candidate: e.candidate });
|
|
64
66
|
};
|
|
65
67
|
this.pc.ondatachannel = e => this.ondatachannel(e.channel);
|
|
68
|
+
this.pc.onicecandidateerror = error => {
|
|
69
|
+
const {errorCode, errorText, url, address, port} = error;
|
|
70
|
+
console.error(errorText, {errorCode, url, address, port});
|
|
71
|
+
};
|
|
66
72
|
this.pc.oniceconnectionstatechange = () => {
|
|
67
73
|
if (!this.pc) return;
|
|
68
|
-
//this.flog('iceConnectionState', this.pc.iceConnectionState);
|
|
69
74
|
switch (this.pc.iceConnectionState) {
|
|
70
75
|
case 'completed':
|
|
71
76
|
this._resolveIceCompleted?.(true);
|
|
@@ -169,7 +174,7 @@ export class WebRTC {
|
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
if (description.type === "offer" && this.pc && ['have-remote-offer', 'have-local-pranswer'].includes(this.pc
|
|
177
|
+
if (description.type === "offer" && this.pc && ['have-remote-offer', 'have-local-pranswer'].includes(this.pc?.signalingState)) {
|
|
173
178
|
const answer = await this.pc.createAnswer();
|
|
174
179
|
await this.pc.setLocalDescription(answer)
|
|
175
180
|
.catch(e => this.log(this.name, 'ignoring error setLocalDescription of answer', e));
|
|
@@ -178,13 +183,13 @@ export class WebRTC {
|
|
|
178
183
|
|
|
179
184
|
} else if (candidate) {
|
|
180
185
|
//this.log('add ice');
|
|
181
|
-
if (this.pc
|
|
182
|
-
this.log('icecandidate, connection:', this.pc
|
|
186
|
+
if (this.pc?.connectionState === 'closed' || !this.pc?.remoteDescription?.type) { // Adding ice without a proceessed offer/answer will crash. Log and drop the candidate.
|
|
187
|
+
this.log('icecandidate, connection:', this.pc?.connectionState, 'signaling:', this.pc?.signalingState, 'ice connection:', this.pc?.iceConnectionState, 'gathering:', this.pc?.iceGatheringState);
|
|
183
188
|
return;
|
|
184
189
|
}
|
|
185
190
|
await this.pc.addIceCandidate(candidate)
|
|
186
191
|
.catch(e => {
|
|
187
|
-
if (!this.ignoreOffer && this.pc
|
|
192
|
+
if (!this.ignoreOffer && this.pc?.connectionState !== 'closed') throw e;
|
|
188
193
|
});
|
|
189
194
|
}
|
|
190
195
|
}
|
package/package.json
CHANGED
package/spec/portal.js
CHANGED
|
@@ -13,8 +13,8 @@ import { WebRTC } from '../index.js';
|
|
|
13
13
|
// The WebRTC does nothing except say 'Welcome!' on the data channel opened by the client.
|
|
14
14
|
// For a more complete example, see https://github.com/YZ-social/kdht/blob/main/spec/portal.js
|
|
15
15
|
|
|
16
|
-
const nPortals = parseInt(process.argv[2] ||
|
|
17
|
-
const perPortalDelay = parseInt(process.argv[3] ||
|
|
16
|
+
const nPortals = parseInt(process.argv[2] || 256);
|
|
17
|
+
const perPortalDelay = parseInt(process.argv[3] || 200);
|
|
18
18
|
const port = parseInt(process.argv[4] || 3000);
|
|
19
19
|
|
|
20
20
|
if (cluster.isPrimary) { // Parent process with portal webserver through which clienta can bootstrap
|
|
@@ -23,14 +23,22 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
|
|
|
23
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
24
|
const __dirname = path.dirname(__filename);
|
|
25
25
|
console.log('launching', nPortals, 'portals');
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const workers = [];
|
|
27
|
+
function makeWorker(i) {
|
|
28
|
+
const worker = workers[i] = cluster.fork();
|
|
29
|
+
console.log(new Date(), 'creating worker', i, 'at', worker.id);
|
|
28
30
|
worker.on('message', signals => { // Message from a worker, in response to a POST.
|
|
29
31
|
worker.requestResolver?.(signals);
|
|
30
32
|
});
|
|
33
|
+
}
|
|
34
|
+
for (let i = 0; i < nPortals; i++) {
|
|
35
|
+
makeWorker(i);
|
|
31
36
|
await new Promise(resolve => setTimeout(resolve, perPortalDelay));
|
|
32
37
|
}
|
|
33
|
-
|
|
38
|
+
cluster.on('exit', (worker, code, signal) => { // Replace the worker at the same index in workers[].
|
|
39
|
+
const index = workers.indexOf(worker);
|
|
40
|
+
makeWorker(index);
|
|
41
|
+
});
|
|
34
42
|
app.use(logger(':date[iso] :status :method :url :res[content-length] - :response-time ms'));
|
|
35
43
|
app.use(express.json());
|
|
36
44
|
app.use(express.static(path.resolve(__dirname, '..'))); // Serve files needed for testing browsers.
|
|
@@ -61,7 +69,6 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
|
|
|
61
69
|
process.send(response);
|
|
62
70
|
});
|
|
63
71
|
function setup() {
|
|
64
|
-
console.log(new Date(), 'launched bot', cluster.worker.id);
|
|
65
72
|
portal = new WebRTC({name: 'portal'});
|
|
66
73
|
portal.getDataChannelPromise('data').then(dc => {
|
|
67
74
|
console.log(new Date(), 'connected bot', cluster.worker.id);
|
|
@@ -69,8 +76,7 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
|
|
|
69
76
|
});
|
|
70
77
|
portal.closed.then(() => { // Without any explicit message, this is 15 seconds after the other end goes away.
|
|
71
78
|
console.log('disconnected', cluster.worker.id);
|
|
72
|
-
//
|
|
73
|
-
// setup());
|
|
79
|
+
process.exit(0); // cleaner for testing than repeating setup();
|
|
74
80
|
});
|
|
75
81
|
}
|
|
76
82
|
setup();
|
|
@@ -6,19 +6,26 @@ function delay(ms) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
describe("WebRTC capacity", function () {
|
|
9
|
-
let
|
|
10
|
-
let perPortalDelay = 1e3;
|
|
9
|
+
let perPortalDelay = 200;
|
|
11
10
|
let portalSlopDelay = 2e3;
|
|
12
11
|
let perConnectionDelay = 100;
|
|
13
|
-
let connectionSlopDelay =
|
|
12
|
+
let connectionSlopDelay = 4e3;
|
|
14
13
|
let port = 3000;
|
|
15
14
|
let baseURL = `http://localhost:${port}`;
|
|
16
15
|
// Alas, I can't seem to get more than about 150-160 nodes through ngrok, even on a machine that can handle 200 directly.
|
|
17
16
|
//let baseURL = 'https://dorado.ngrok.dev'; // if E.g., node spec/portal.js 200 100; ngrok http 3000 --url https://dorado.ngrok.dev
|
|
18
17
|
|
|
19
18
|
// Uncomment this line if running a stand-alone capacity test.
|
|
20
|
-
// (And also likely comment out the import './webrtcSpec.js' in test.html
|
|
21
|
-
//
|
|
19
|
+
// (And also likely comment out the import './webrtcSpec.js' in test.html,
|
|
20
|
+
// and "resignals on restartIce..." tests.)
|
|
21
|
+
let nNodes = WebRTC.suggestedInstancesLimit;
|
|
22
|
+
let reNegotiate = false; // Should we attempt to test restartIce/re-negotiation?
|
|
23
|
+
if (reNegotiate) {
|
|
24
|
+
if (globalThis.navigator.userAgent?.includes('Firefox') ||
|
|
25
|
+
globalThis.navigator.vendor?.startsWith('Apple') ||
|
|
26
|
+
(typeof(globalThis.process) !== 'undefined'))
|
|
27
|
+
nNodes = Math.floor(0.5 * nNodes);
|
|
28
|
+
}
|
|
22
29
|
|
|
23
30
|
const isNodeJS = typeof(globalThis.process) !== 'undefined';
|
|
24
31
|
const portalIsLocal = isNodeJS && baseURL.startsWith('http://localhost');
|
|
@@ -60,18 +67,13 @@ describe("WebRTC capacity", function () {
|
|
|
60
67
|
const dataOpened = node.getDataChannelPromise('data')
|
|
61
68
|
.then(dc => node.dataReceived = new Promise(resolve => dc.onmessage = event => resolve(event.data)));
|
|
62
69
|
node.createChannel('data', {negotiated: false});
|
|
63
|
-
await dataOpened;
|
|
70
|
+
await Promise.race([dataOpened, delay(perConnectionDelay)]);
|
|
64
71
|
console.log('opened', index);
|
|
65
|
-
// if (!portalIsLocal) {
|
|
66
|
-
// const maxConnectionsPerNode = 3;
|
|
67
|
-
// const maxNgrokConnectionsPerSecond = 120 / 60;
|
|
68
|
-
// const secondsPerNode = maxConnectionsPerNode / maxNgrokConnectionsPerSecond;
|
|
69
|
-
// await delay(secondsPerNode * 1.5e3); // fudge factor milliseconds/second
|
|
70
|
-
// }
|
|
71
72
|
}
|
|
73
|
+
console.log(new Date(), 'pause after setup');
|
|
72
74
|
await delay(connectionSlopDelay);
|
|
73
75
|
console.log(new Date(), 'finished setup');
|
|
74
|
-
}, nNodes * perPortalDelay + portalSlopDelay + nNodes * perConnectionDelay +
|
|
76
|
+
}, nNodes * perPortalDelay + portalSlopDelay + 2 * nNodes * perConnectionDelay + 2 * connectionSlopDelay);
|
|
75
77
|
for (let index = 0; index < nNodes; index++) {
|
|
76
78
|
it('opened connection ' + index, function () {
|
|
77
79
|
expect(nodes[index].pc.connectionState).toBe('connected');
|
|
@@ -79,17 +81,17 @@ describe("WebRTC capacity", function () {
|
|
|
79
81
|
it('got data ' + index, async function () {
|
|
80
82
|
expect(await nodes[index].dataReceived).toBe('Welcome!');
|
|
81
83
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
84
|
+
if (reNegotiate) {
|
|
85
|
+
it('resignals on restartIce ' + index, async function () {
|
|
86
|
+
const node = nodes[index];
|
|
87
|
+
expect(['connected', 'completed']).toContain(node.pc.iceConnectionState);
|
|
88
|
+
node.nFetches = 0;
|
|
89
|
+
const promise = node.renegotiate();
|
|
90
|
+
await Promise.race([promise, delay(1e3)]);
|
|
91
|
+
expect(['connected', 'completed']).toContain(node.pc.iceConnectionState);
|
|
92
|
+
expect(node.nFetches).toBeGreaterThan(0); // We will have re-signalled.
|
|
93
|
+
});
|
|
94
|
+
}
|
|
93
95
|
}
|
|
94
96
|
afterAll(async function () {
|
|
95
97
|
console.log(new Date(), 'starting teardown');
|