@yz-social/webrtc 0.1.0 → 0.1.2
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/.github/workflows/ci.yml +27 -0
- package/index.js +58 -26
- package/package.json +1 -1
- package/spec/portal.js +64 -0
- package/spec/test.html +1 -0
- package/spec/webrtcCapacitySpec.js +68 -0
- package/spec/webrtcSpec.js +73 -42
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI/CD Build and Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
workflow_dispatch: # Allows manual running
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build-and-test:
|
|
12
|
+
runs-on: ubuntu-latest # Uses the latest Ubuntu runner
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout Code
|
|
16
|
+
uses: actions/checkout@v5 # Action to check out your repository code
|
|
17
|
+
|
|
18
|
+
- name: Set up Node.js
|
|
19
|
+
uses: actions/setup-node@v4 # Action to set up Node.js environment
|
|
20
|
+
with:
|
|
21
|
+
node-version: '22.x'
|
|
22
|
+
|
|
23
|
+
- name: Install Dependencies
|
|
24
|
+
run: npm i # Some folks recommend 'npm ci', but I don't like the required package lock.
|
|
25
|
+
|
|
26
|
+
- name: Run Tests
|
|
27
|
+
run: npm test # Executes the 'test' script defined in your package.json
|
package/index.js
CHANGED
|
@@ -18,6 +18,19 @@ export class WebRTC {
|
|
|
18
18
|
// https://xirsys.com/pricing/ 500 MB/month (50 GB/month for $33/month)
|
|
19
19
|
// Also https://www.npmjs.com/package/node-turn or https://meetrix.io/blog/webrtc/coturn/installation.html
|
|
20
20
|
];
|
|
21
|
+
cleanup() { // Attempt to allow everything to be garbage-collected.
|
|
22
|
+
if (!this.pc) return;
|
|
23
|
+
this.pc.onicecandidate = this.pc.ondatachannel = this.pc.onnegotiationneeded = this.pc.onconnectionstatechange = null;
|
|
24
|
+
delete this.pc;
|
|
25
|
+
delete this.dataChannelPromises;
|
|
26
|
+
delete this.dataChannelOursPromises;
|
|
27
|
+
delete this.dataChannelTheirsPromises;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Number of instances at a time (if previous have been garbage collected), as of 1/27/26:
|
|
31
|
+
static suggestedInstancesLimit = globalThis.navigator.vendor?.startsWith('Apple') ? 95 : // Seems to be hardcoded?
|
|
32
|
+
globalThis.navigator.userAgent?.includes('Firefox') ? 190 :
|
|
33
|
+
200;
|
|
21
34
|
constructor({configuration = {iceServers: WebRTC.iceServers}, ...properties}) {
|
|
22
35
|
Object.assign(this, properties);
|
|
23
36
|
|
|
@@ -26,14 +39,17 @@ export class WebRTC {
|
|
|
26
39
|
promise.resolve = resolve;
|
|
27
40
|
this.closed = promise;
|
|
28
41
|
// Safari doesn't fire signalingstatechange for closing activity.
|
|
29
|
-
this.pc.
|
|
30
|
-
const
|
|
42
|
+
this.pc.onconnectionstatechange = () => { // Only fires by action from other side.
|
|
43
|
+
const pc = this.pc;
|
|
44
|
+
if (!pc) return null;
|
|
45
|
+
const state = pc.connectionState;
|
|
31
46
|
if (state === 'connected') return this.signalsReadyResolver?.();
|
|
32
47
|
if (['new', 'connecting'].includes(state)) return null;
|
|
33
48
|
// closed, disconnected, failed: resolve this.closed promise.
|
|
34
|
-
this.log('connectionstatechange signaling/connection:',
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
this.log('connectionstatechange signaling/connection:', pc.signalingState, state);
|
|
50
|
+
this.cleanup();
|
|
51
|
+
return resolve(pc);
|
|
52
|
+
};
|
|
37
53
|
|
|
38
54
|
this.connectionStartTime = Date.now();
|
|
39
55
|
this.makingOffer = false;
|
|
@@ -62,8 +78,11 @@ export class WebRTC {
|
|
|
62
78
|
}
|
|
63
79
|
async close() {
|
|
64
80
|
// Do not try to close or wait for data channels. It confuses Safari.
|
|
65
|
-
this.pc
|
|
66
|
-
|
|
81
|
+
const pc = this.pc;
|
|
82
|
+
if (!pc) return null;
|
|
83
|
+
pc.close();
|
|
84
|
+
this.closed.resolve(pc); // We do not automatically receive 'connectionstatechange' when our side explicitly closes. (Only if the other does.)
|
|
85
|
+
this.cleanup();
|
|
67
86
|
return this.closed;
|
|
68
87
|
}
|
|
69
88
|
flog(...rest) {
|
|
@@ -80,6 +99,7 @@ export class WebRTC {
|
|
|
80
99
|
explicitRollback = typeof(globalThis.process) !== 'undefined';
|
|
81
100
|
async onSignal({ description, candidate }) {
|
|
82
101
|
// Most of this and onnegotiationneeded is from https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
|
|
102
|
+
if (!this.pc) return;
|
|
83
103
|
if (description) {
|
|
84
104
|
const offerCollision =
|
|
85
105
|
description.type === "offer" &&
|
|
@@ -181,40 +201,44 @@ export class WebRTC {
|
|
|
181
201
|
if (!this.transferSignals) return; // Just keep collecting until the next call to respond();
|
|
182
202
|
this.sendPending();
|
|
183
203
|
}
|
|
184
|
-
followupTimer = null;
|
|
204
|
+
//followupTimer = null;
|
|
185
205
|
sendPending(force = false) { // Send over any signals we have, and process the response.
|
|
186
206
|
this.lastOutboundSignal = Date.now();
|
|
187
|
-
clearTimeout(this.followupTimer);
|
|
207
|
+
//clearTimeout(this.followupTimer);
|
|
188
208
|
this.transferSerializer = this.transferSerializer.then(() => {
|
|
189
209
|
const signals = this.collectPendingSignals();
|
|
190
210
|
if (!force && !signals.length) return null; // A stack of pending signals got rolled together and pending is now empty.
|
|
191
211
|
this.lastOutboundSend = Date.now();
|
|
192
212
|
this.log('sending', signals.length, 'signals');
|
|
193
213
|
return this.transferSignals(signals).then(async response => {
|
|
194
|
-
clearTimeout(this.followupTimer);
|
|
214
|
+
//clearTimeout(this.followupTimer);
|
|
195
215
|
this.lastResponse = Date.now();
|
|
196
216
|
await this.onSignals(response);
|
|
197
|
-
if (this.pc.connectionState === 'connected') return;
|
|
198
|
-
this.followupTimer = setTimeout(() => { // We may have sent everything we had, but still need to poke in order to get more ice from them.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
217
|
+
// if (this.pc.connectionState === 'connected') return;
|
|
218
|
+
// this.followupTimer = setTimeout(() => { // We may have sent everything we had, but still need to poke in order to get more ice from them.
|
|
219
|
+
// if (this.pc.connectionState === 'connected') return;
|
|
220
|
+
// Note: if we bring this back, stop after closed!
|
|
221
|
+
// this.log('************** nothing new to send', this.pc.connectionState, ' ************************');
|
|
222
|
+
// this.sendPending(true);
|
|
223
|
+
// }, 500);
|
|
203
224
|
});
|
|
204
225
|
});
|
|
205
226
|
}
|
|
206
227
|
transferSerializer = Promise.resolve();
|
|
207
228
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
229
|
+
setupChannel(dc) { // Arrange for the data channel promise to resolve open, and do other setup.
|
|
230
|
+
// Called by explicit createChannel ('ours' opened) and also by ondatachannel ('theirs' opened).
|
|
231
|
+
const { label, readyState } = dc;
|
|
232
|
+
const isTheirs = readyState === 'open'; // Came via ondatachannel.
|
|
233
|
+
this.log('setupChannel:', label, dc.id, readyState, 'negotiated:', dc.negotiated);
|
|
234
|
+
const kind = isTheirs ? 'Theirs' : 'Ours';
|
|
212
235
|
dc.webrtc = this;
|
|
213
|
-
dc.onopen = async () => {
|
|
214
|
-
this.log('channel onopen:',
|
|
215
|
-
this.
|
|
236
|
+
dc.onopen = async () => { // Idempotent (except for logging), if we do not bash dataChannePromises[label] multiple times.
|
|
237
|
+
this.log('channel onopen:', label, dc.id, readyState, 'negotiated:', dc.negotiated);
|
|
238
|
+
this[this.restrictablePromiseKey()][label]?.resolve(dc);
|
|
239
|
+
this[this.restrictablePromiseKey(kind)][label]?.resolve(dc);
|
|
216
240
|
};
|
|
217
|
-
if (
|
|
241
|
+
if (isTheirs) dc.onopen();
|
|
218
242
|
return dc;
|
|
219
243
|
}
|
|
220
244
|
ondatachannel(dc) {
|
|
@@ -229,10 +253,18 @@ export class WebRTC {
|
|
|
229
253
|
return this.setupChannel(this.pc.createDataChannel(name, {negotiated, id, ...options}));
|
|
230
254
|
}
|
|
231
255
|
dataChannelPromises = {};
|
|
232
|
-
|
|
256
|
+
dataChannelOursPromises = {};
|
|
257
|
+
dataChannelTheirsPromises = {};
|
|
258
|
+
restrictablePromiseKey(restriction = '') { // The property in which store dataChannel promises of the specified restriction.
|
|
259
|
+
return `dataChannel${restriction}Promises`;
|
|
260
|
+
}
|
|
261
|
+
getDataChannelPromise(name = 'data', restriction = '') { // Promise to resolve when opened, WITHOUT actually creating one.
|
|
262
|
+
// The application can restrict this to being only a channel of 'ours' or 'theirs' (see setupChannel), which is useful for
|
|
263
|
+
// non-negotiated channels.
|
|
264
|
+
const key = this.restrictablePromiseKey(restriction);
|
|
233
265
|
const {promise, resolve, reject} = Promise.withResolvers();
|
|
234
266
|
Object.assign(promise, {resolve, reject});
|
|
235
|
-
return this
|
|
267
|
+
return this[key][name] = promise;
|
|
236
268
|
}
|
|
237
269
|
|
|
238
270
|
async reportConnection(doLogging = false) { // Update self with latest wrtc stats (and log them if doLogging true). See Object.assign for properties.
|
package/package.json
CHANGED
package/spec/portal.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import cluster from 'node:cluster';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { WebRTC } from '../index.js';
|
|
7
|
+
|
|
8
|
+
// A minimal webrtc web portal server.
|
|
9
|
+
// Peers may POST signals to /join/<n> in order to connect (one at a time!) to a WebRTC node running at id <n>.
|
|
10
|
+
// Two such POSTs are typically required.
|
|
11
|
+
// The WebRTC does nothing except say 'Welcome!' on the data channel opened by the client.
|
|
12
|
+
// For a more complete example, see https://github.com/YZ-social/kdht/blob/main/spec/portal.js
|
|
13
|
+
|
|
14
|
+
const nPortals = parseInt(process.argv[2] || WebRTC.suggestedInstancesLimit);
|
|
15
|
+
|
|
16
|
+
if (cluster.isPrimary) { // Parent process with portal webserver through which clienta can bootstrap
|
|
17
|
+
process.title = 'webrtc-test-portal';
|
|
18
|
+
const app = express();
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
console.log('launching', nPortals, 'portals');
|
|
22
|
+
for (let i = 0; i < nPortals; i++) {
|
|
23
|
+
const worker = cluster.fork();
|
|
24
|
+
worker.on('message', signals => { // Message from a worker, in response to a POST.
|
|
25
|
+
worker.requestResolver?.(signals);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const workers = Object.values(cluster.workers);
|
|
29
|
+
app.use(express.json());
|
|
30
|
+
app.use(express.static(path.resolve(__dirname, '..'))); // Serve files needed for testing browsers.
|
|
31
|
+
app.post('/join/:to', async (req, res, next) => { // Handler for JSON POST requests that provide an array of signals and get signals back.
|
|
32
|
+
const {params, body} = req;
|
|
33
|
+
// Find the specifed worker, or pick one at random. TODO CLEANUP: Remove. We now use as separate /name/:label to pick one.
|
|
34
|
+
const worker = workers[params.to];
|
|
35
|
+
if (!worker) {
|
|
36
|
+
console.warn('no worker', params.to);
|
|
37
|
+
return res.sendStatus(404);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pass the POST body to the worker and await the response.
|
|
41
|
+
const promise = new Promise(resolve => worker.requestResolver = resolve);
|
|
42
|
+
worker.send(body, undefined, undefined, error =>
|
|
43
|
+
error && console.log(`Error communicating with portal worker ${worker.id}:`, error));
|
|
44
|
+
let response = await promise;
|
|
45
|
+
delete worker.requestResolver; // Now that we have the response.
|
|
46
|
+
|
|
47
|
+
return res.send(response);
|
|
48
|
+
});
|
|
49
|
+
app.listen(3000);
|
|
50
|
+
} else {
|
|
51
|
+
process.title = 'webrtc-test-bot-' + cluster.worker.id;
|
|
52
|
+
let portal;
|
|
53
|
+
process.on('message', async incomingSignals => { // Signals from a sender through the server.
|
|
54
|
+
const response = await portal.respond(incomingSignals);
|
|
55
|
+
process.send(response);
|
|
56
|
+
});
|
|
57
|
+
function setup() {
|
|
58
|
+
console.log(new Date(), 'launched bot', cluster.worker.id);
|
|
59
|
+
portal = new WebRTC({name: 'portal'});
|
|
60
|
+
portal.getDataChannelPromise('data').then(dc => dc.send('Welcome!'));
|
|
61
|
+
portal.closed.then(setup); // Without any explicit message, this is 15 seconds after the other end goes away.
|
|
62
|
+
}
|
|
63
|
+
setup();
|
|
64
|
+
}
|
package/spec/test.html
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach} = globalThis; // For linters.
|
|
2
|
+
import { WebRTC } from '../index.js';
|
|
3
|
+
|
|
4
|
+
describe("WebRTC capacity", function () {
|
|
5
|
+
let nNodes = 20; // When running all webrtc tests at once, it is important to keep this low. (Memory leak?)
|
|
6
|
+
|
|
7
|
+
// Uncomment this line if running a stand-alone capacity test.
|
|
8
|
+
// (And also likely comment out the import './webrtcSpec.js' in test.html.)
|
|
9
|
+
// nNodes = WebRTC.suggestedInstancesLimit;
|
|
10
|
+
|
|
11
|
+
const isNodeJS = typeof(globalThis.process) !== 'undefined';
|
|
12
|
+
let nodes = [];
|
|
13
|
+
beforeAll(async function () {
|
|
14
|
+
|
|
15
|
+
if (isNodeJS) {
|
|
16
|
+
const { spawn } = await import('node:child_process');
|
|
17
|
+
const path = await import('path');
|
|
18
|
+
const { fileURLToPath } = await import('url');
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = path.dirname(__filename);
|
|
22
|
+
function echo(data) { data = data.slice(0, -1); console.log(data.toString()); }
|
|
23
|
+
const portalProcess = spawn('node', [path.resolve(__dirname, 'portal.js'), nNodes]);
|
|
24
|
+
portalProcess.stdout.on('data', echo);
|
|
25
|
+
portalProcess.stderr.on('data', echo);
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, 6e3));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(new Date(), 'creating', nNodes, 'nodes');
|
|
30
|
+
for (let index = 0; index < nNodes; index++) {
|
|
31
|
+
const node = nodes[index] = new WebRTC({name: 'node'});
|
|
32
|
+
node.transferSignals = messages => fetch(`http://localhost:3000/join/${index}`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json', 'Connection': 'close' },
|
|
35
|
+
body: JSON.stringify(messages)
|
|
36
|
+
}).then(response => response.json());
|
|
37
|
+
node.closed.then(() => console.log('closed', index)); // Just for debugging.
|
|
38
|
+
const dataOpened = node.getDataChannelPromise('data')
|
|
39
|
+
.then(dc => node.dataReceived = new Promise(resolve => dc.onmessage = event => resolve(event.data)));
|
|
40
|
+
node.createChannel('data', {negotiated: false});
|
|
41
|
+
await dataOpened;
|
|
42
|
+
console.log('opened', index);
|
|
43
|
+
}
|
|
44
|
+
console.log(new Date(), 'finished setup');
|
|
45
|
+
}, Math.max(20e3, nNodes * (500 + 300)));
|
|
46
|
+
for (let index = 0; index < nNodes; index++) {
|
|
47
|
+
it('opened connection ' + index, function () {
|
|
48
|
+
expect(nodes[index].pc.connectionState).toBe('connected');
|
|
49
|
+
});
|
|
50
|
+
it('got data ' + index, async function () {
|
|
51
|
+
expect(await nodes[index].dataReceived).toBe('Welcome!');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
afterAll(async function () {
|
|
55
|
+
console.log(new Date(), 'starting teardown');
|
|
56
|
+
for (let index = 0; index < nNodes; index++) {
|
|
57
|
+
const node = nodes[index];
|
|
58
|
+
expect(node.pc.connectionState).toBe('connected');
|
|
59
|
+
await node.close().then(pc =>
|
|
60
|
+
expect(pc.connectionState).toBe('closed'));
|
|
61
|
+
delete nodes[index];
|
|
62
|
+
}
|
|
63
|
+
if (isNodeJS) {
|
|
64
|
+
const { exec } = await import('node:child_process');
|
|
65
|
+
exec('pkill webrtc-test-');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
package/spec/webrtcSpec.js
CHANGED
|
@@ -3,11 +3,6 @@ import { WebRTC } from '../index.js';
|
|
|
3
3
|
|
|
4
4
|
describe("WebRTC", function () {
|
|
5
5
|
const isBrowser = typeof(process) === 'undefined';
|
|
6
|
-
// maximums
|
|
7
|
-
// 1 channel pair, without negotiated on first signal: nodejs:83, firefox:150+, safari:85+ but gets confused with closing, chrome/edge:50(?)
|
|
8
|
-
// 50 works across the board with one channel pair
|
|
9
|
-
// On Safari (only), anything more than 32 pair starts to loose messages on the SECOND channel.
|
|
10
|
-
const nPairs = 32; //32;
|
|
11
6
|
let connections = [];
|
|
12
7
|
describe("direct in-process signaling", function () {
|
|
13
8
|
function makePair({debug = false, delay = 0, index = 0} = {}) {
|
|
@@ -15,25 +10,13 @@ describe("WebRTC", function () {
|
|
|
15
10
|
const configuration = { iceServers: WebRTC.iceServers };
|
|
16
11
|
const A = new WebRTC({name: `A (impolite) ${index}`, polite: false, debug, configuration});
|
|
17
12
|
const B = new WebRTC({name: `B (polite) ${index}`, polite: true, debug, configuration});
|
|
18
|
-
async function sendingSetup(dc) { // Given an open channel, set up to receive a message and then send a test message.
|
|
19
|
-
const webrtc = dc.webrtc;
|
|
20
|
-
webrtc.receivedMessageCount = webrtc.sentMessageCount = 0;
|
|
21
|
-
webrtc.gotData = new Promise(resolve => {
|
|
22
|
-
dc.onmessage = e => {
|
|
23
|
-
webrtc.receivedMessageCount++;
|
|
24
|
-
webrtc.log('got message on', dc.label, dc.id, e.data);
|
|
25
|
-
resolve(e.data);
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
webrtc.sentMessageCount++;
|
|
29
|
-
|
|
30
13
|
// Subtle:
|
|
31
14
|
//
|
|
32
15
|
// An explicit createChannel() arranges for the promise created by getDataChannelPromise() of the same name to
|
|
33
16
|
// resolve on that side of the connection when the channel is open. The other side will internally receive a
|
|
34
17
|
// datachannel event and we arrange for that side to then also resolve a getDataChannelPromise for that name.
|
|
35
18
|
//
|
|
36
|
-
// If only
|
|
19
|
+
// If only one side creates the channel, things work out smoothly. Each side ends up resolving the promise for a
|
|
37
20
|
// channel of the given name. The id is as defaulted or specified if negotiated:true, else an internally determined
|
|
38
21
|
// unique id is used.
|
|
39
22
|
//
|
|
@@ -41,24 +24,47 @@ describe("WebRTC", function () {
|
|
|
41
24
|
// are overlappaing offers. One side backs down and there ends up being one channel with the specified id. (The default
|
|
42
25
|
// id will be the same on both sides IFF any multiple data channels are created in the same order on both sides.)
|
|
43
26
|
//
|
|
44
|
-
// However, when negotiated:
|
|
45
|
-
// RTCPeerConnection arranges for the
|
|
27
|
+
// However, when negotiated:FALSE, and both sides call createChannel with a delay in betwen, things get complex. Counterintuitively,
|
|
28
|
+
// RTCPeerConnection arranges for the internally determined id to be unique between the two sides, so both get
|
|
46
29
|
// two channels with the same name and different id. There will be two internal open events on each side: first for the
|
|
47
|
-
// channel that it created, and second for one that the other side created. There isn't any reason to do this
|
|
30
|
+
// channel that it created, and second for one that the other side created. (There likely isn't any reason to do this, as
|
|
48
31
|
// opposed to specifying negotiated:true, unless the application needs each side to listen on one channel and send
|
|
49
|
-
// on the other.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
// on the other.)
|
|
33
|
+
async function receivingSetup(dc) {
|
|
34
|
+
const webrtc = dc.webrtc;
|
|
35
|
+
webrtc.log('got open recieving channel', dc.label, dc.id);
|
|
36
|
+
webrtc.ours = dc;
|
|
37
|
+
webrtc.receivedMessageCount = 0;
|
|
38
|
+
webrtc.gotData = new Promise(resolve => webrtc.gotDataResolver = resolve);
|
|
39
|
+
dc.onmessage = e => {
|
|
40
|
+
webrtc.receivedMessageCount++;
|
|
41
|
+
webrtc.log('got message on', dc.label, dc.id, e.data);
|
|
42
|
+
webrtc.gotDataResolver(e.data);
|
|
43
|
+
};
|
|
44
|
+
return dc;
|
|
45
|
+
}
|
|
46
|
+
async function sendingSetup(dc) {
|
|
47
|
+
const webrtc = dc.webrtc;
|
|
48
|
+
webrtc.log('got open sending channel', dc.label, dc.id);
|
|
49
|
+
webrtc.theirs = dc;
|
|
50
|
+
webrtc.sentMessageCount ||= 0;
|
|
51
|
+
webrtc.sentMessageCount++;
|
|
55
52
|
await WebRTC.delay(delay);
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
dc.send(`Hello from ${webrtc.name}`);
|
|
54
|
+
return dc;
|
|
55
|
+
}
|
|
56
|
+
const promises = [];
|
|
57
|
+
if (!delay) {
|
|
58
|
+
promises.push(A.getDataChannelPromise('data').then(receivingSetup).then(sendingSetup));
|
|
59
|
+
promises.push(B.getDataChannelPromise('data').then(receivingSetup).then(sendingSetup));
|
|
60
|
+
} else {
|
|
61
|
+
promises.push(A.getDataChannelPromise('data', 'Ours').then(receivingSetup));
|
|
62
|
+
promises.push(B.getDataChannelPromise('data', 'Ours').then(receivingSetup));
|
|
63
|
+
promises.push(A.getDataChannelPromise('data', 'Theirs').then(sendingSetup));
|
|
64
|
+
promises.push(B.getDataChannelPromise('data', 'Theirs').then(sendingSetup));
|
|
58
65
|
}
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const direct = false; // Does signal work direct/one-sided to the other? False makes a request that waits for a response.
|
|
66
|
+
|
|
67
|
+
const direct = true; // Does signal work direct/one-sided to the other? False makes a request that waits for a response.
|
|
62
68
|
if (direct) {
|
|
63
69
|
A.signal = message => B.onSignal(message);
|
|
64
70
|
B.signal = message => A.onSignal(message);
|
|
@@ -66,12 +72,27 @@ describe("WebRTC", function () {
|
|
|
66
72
|
A.transferSignals = messages => B.respond(messages);
|
|
67
73
|
B.transferSignals = messages => A.respond(messages);
|
|
68
74
|
}
|
|
69
|
-
return connections[index] = {A, B, bothOpen: Promise.all(
|
|
75
|
+
return connections[index] = {A, B, bothOpen: Promise.all(promises)};
|
|
70
76
|
}
|
|
71
|
-
function standardBehavior(setup, {includeConflictCheck = isBrowser, includeSecondChannel =
|
|
77
|
+
function standardBehavior(setup, {includeConflictCheck = isBrowser, includeSecondChannel = false} = {}) {
|
|
78
|
+
// The nPairs does NOT seem to be a reliable way to determine how many webrtc peers can be active in the same Javascript.
|
|
79
|
+
// I have had numbers that work for every one of the cases DESCRIBEd below, and even in combinations,
|
|
80
|
+
// but it seems to get upset when all are run together, and it seemms to depend on the state of the machine or phases of the moon.
|
|
81
|
+
// When it fails, it just locks up with no indication of what is happening.
|
|
82
|
+
// Maybe a memory / memory-leak issue?
|
|
83
|
+
//
|
|
84
|
+
// Some observed behaviors, at some point in time, include:
|
|
85
|
+
// 1 channel pair, without negotiated on first signal: nodejs:83, firefox:150+, safari:85+ but gets confused with closing, chrome/edge:50(?)
|
|
86
|
+
// 50 works across the board with one channel pair
|
|
87
|
+
// On Safari (only), anything more than 32 pair starts to loose messages on the SECOND channel.
|
|
88
|
+
// In NodeJS with includeSecondChannel: 32
|
|
89
|
+
// In NodeJS witout includeSecondChannel: 62, 75, even 85, but not consistently.
|
|
90
|
+
//
|
|
91
|
+
// webrtcCapacitySpec.js may be a better test for capacity.
|
|
92
|
+
const nPairs = 10;
|
|
72
93
|
beforeAll(async function () {
|
|
73
94
|
const start = Date.now();
|
|
74
|
-
console.log('start setup');
|
|
95
|
+
console.log(new Date(), 'start setup', nPairs, 'pairs');
|
|
75
96
|
for (let index = 0; index < nPairs; index++) {
|
|
76
97
|
await setup({index});
|
|
77
98
|
}
|
|
@@ -81,8 +102,10 @@ describe("WebRTC", function () {
|
|
|
81
102
|
for (let index = 0; index < nPairs; index++) {
|
|
82
103
|
it(`connects ${index}.`, function () {
|
|
83
104
|
const {A, B} = connections[index];
|
|
84
|
-
expect(A.
|
|
85
|
-
expect(B.
|
|
105
|
+
expect(A.ours.readyState).toBe('open');
|
|
106
|
+
expect(B.ours.readyState).toBe('open');
|
|
107
|
+
expect(A.theirs.readyState).toBe('open');
|
|
108
|
+
expect(B.theirs.readyState).toBe('open');
|
|
86
109
|
});
|
|
87
110
|
it(`receives ${index}.`, async function () {
|
|
88
111
|
const {A, B} = connections[index];
|
|
@@ -132,8 +155,9 @@ describe("WebRTC", function () {
|
|
|
132
155
|
expect(apc.signalingState).toBe('closed');
|
|
133
156
|
const bpc = await B.closed; // Waiting for B to notice.
|
|
134
157
|
await B.close(); // Resources are not necessarilly freed when the other side closes. An explicit close() is needed.
|
|
135
|
-
expect(['closed', 'disconnected', 'failed']).toContain(
|
|
136
|
-
expect(bpc.signalingState)
|
|
158
|
+
expect(['closed', 'disconnected', 'failed']).toContain(bpc.connectionState);
|
|
159
|
+
expect(['closed', 'stable']).toContain(bpc.signalingState);
|
|
160
|
+
delete connections[index];
|
|
137
161
|
});
|
|
138
162
|
promises.push(promise);
|
|
139
163
|
}
|
|
@@ -143,6 +167,7 @@ describe("WebRTC", function () {
|
|
|
143
167
|
}
|
|
144
168
|
describe("one side opens", function () {
|
|
145
169
|
describe('non-negotiated', function () {
|
|
170
|
+
beforeAll(function () {console.log('one-sided non-negotiated'); });
|
|
146
171
|
standardBehavior(async function ({index}) {
|
|
147
172
|
const {A, B, bothOpen} = makePair({index});
|
|
148
173
|
A.createChannel('data', {negotiated: false});
|
|
@@ -150,6 +175,7 @@ describe("WebRTC", function () {
|
|
|
150
175
|
}, {includeConflictCheck: false, includeSecondChannel: false});
|
|
151
176
|
});
|
|
152
177
|
describe("negotiated on first signal", function () {
|
|
178
|
+
beforeAll(function () {console.log('one-sided negotiated'); });
|
|
153
179
|
standardBehavior(async function ({index}) {
|
|
154
180
|
const {A, B, bothOpen} = makePair({index});
|
|
155
181
|
// There isn't really a direct, automated way to have one side open another with negotiated:true,
|
|
@@ -173,6 +199,7 @@ describe("WebRTC", function () {
|
|
|
173
199
|
describe("simultaneous two-sided", function () {
|
|
174
200
|
describe("negotiated single full-duplex-channel", function () {
|
|
175
201
|
describe("impolite first", function () {
|
|
202
|
+
beforeAll(function () {console.log('two-sided negotiated impolite-first'); });
|
|
176
203
|
standardBehavior(async function ({index}) {
|
|
177
204
|
const {A, B, bothOpen} = makePair({index});
|
|
178
205
|
A.createChannel("data", {negotiated: true});
|
|
@@ -181,6 +208,7 @@ describe("WebRTC", function () {
|
|
|
181
208
|
});
|
|
182
209
|
});
|
|
183
210
|
describe("polite first", function () {
|
|
211
|
+
beforeAll(function () {console.log('two-sided negotiated polite-first');});
|
|
184
212
|
standardBehavior(async function ({index}) {
|
|
185
213
|
const {A, B, bothOpen} = makePair({index});
|
|
186
214
|
B.createChannel("data", {negotiated: true});
|
|
@@ -190,18 +218,21 @@ describe("WebRTC", function () {
|
|
|
190
218
|
});
|
|
191
219
|
});
|
|
192
220
|
describe("non-negotiated dual half-duplex channels", function () {
|
|
193
|
-
const delay =
|
|
221
|
+
const delay = 200;
|
|
222
|
+
const debug = false;
|
|
194
223
|
describe("impolite first", function () {
|
|
224
|
+
beforeAll(function () {console.log('two-sided non-negotiated impolite-first');});
|
|
195
225
|
standardBehavior(async function ({index}) {
|
|
196
|
-
const {A, B, bothOpen} = makePair({delay, index});
|
|
226
|
+
const {A, B, bothOpen} = makePair({delay, index, debug});
|
|
197
227
|
A.createChannel("data", {negotiated: false});
|
|
198
228
|
B.createChannel("data", {negotiated: false});
|
|
199
229
|
await bothOpen;
|
|
200
230
|
});
|
|
201
231
|
});
|
|
202
232
|
describe("polite first", function () {
|
|
233
|
+
beforeAll(function () {console.log('two-sided non-negotiated polite-first');});
|
|
203
234
|
standardBehavior(async function ({index}) {
|
|
204
|
-
const {A, B, bothOpen} = makePair({delay, index});
|
|
235
|
+
const {A, B, bothOpen} = makePair({delay, index, debug});
|
|
205
236
|
B.createChannel("data", {negotiated: false});
|
|
206
237
|
A.createChannel("data", {negotiated: false});
|
|
207
238
|
await bothOpen;
|