@yz-social/webrtc 0.1.0 → 0.1.1
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 +31 -19
- package/package.json +1 -1
- package/spec/webrtcSpec.js +60 -40
|
@@ -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
|
@@ -181,40 +181,44 @@ export class WebRTC {
|
|
|
181
181
|
if (!this.transferSignals) return; // Just keep collecting until the next call to respond();
|
|
182
182
|
this.sendPending();
|
|
183
183
|
}
|
|
184
|
-
followupTimer = null;
|
|
184
|
+
//followupTimer = null;
|
|
185
185
|
sendPending(force = false) { // Send over any signals we have, and process the response.
|
|
186
186
|
this.lastOutboundSignal = Date.now();
|
|
187
|
-
clearTimeout(this.followupTimer);
|
|
187
|
+
//clearTimeout(this.followupTimer);
|
|
188
188
|
this.transferSerializer = this.transferSerializer.then(() => {
|
|
189
189
|
const signals = this.collectPendingSignals();
|
|
190
190
|
if (!force && !signals.length) return null; // A stack of pending signals got rolled together and pending is now empty.
|
|
191
191
|
this.lastOutboundSend = Date.now();
|
|
192
192
|
this.log('sending', signals.length, 'signals');
|
|
193
193
|
return this.transferSignals(signals).then(async response => {
|
|
194
|
-
clearTimeout(this.followupTimer);
|
|
194
|
+
//clearTimeout(this.followupTimer);
|
|
195
195
|
this.lastResponse = Date.now();
|
|
196
196
|
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
|
-
|
|
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
|
+
// if (this.pc.connectionState === 'connected') return;
|
|
200
|
+
// Note: if we bring this back, stop after closed!
|
|
201
|
+
// this.log('************** nothing new to send', this.pc.connectionState, ' ************************');
|
|
202
|
+
// this.sendPending(true);
|
|
203
|
+
// }, 500);
|
|
203
204
|
});
|
|
204
205
|
});
|
|
205
206
|
}
|
|
206
207
|
transferSerializer = Promise.resolve();
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
setupChannel(dc) { // Arrange for the data channel promise to resolve open, and do other setup.
|
|
210
|
+
// Called by explicit createChannel ('ours' opened) and also by ondatachannel ('theirs' opened).
|
|
211
|
+
const { label, readyState } = dc;
|
|
212
|
+
const isTheirs = readyState === 'open'; // Came via ondatachannel.
|
|
213
|
+
this.log('setupChannel:', label, dc.id, readyState, 'negotiated:', dc.negotiated);
|
|
214
|
+
const kind = isTheirs ? 'Theirs' : 'Ours';
|
|
212
215
|
dc.webrtc = this;
|
|
213
|
-
dc.onopen = async () => {
|
|
214
|
-
this.log('channel onopen:',
|
|
215
|
-
this.
|
|
216
|
+
dc.onopen = async () => { // Idempotent (except for logging), if we do not bash dataChannePromises[label] multiple times.
|
|
217
|
+
this.log('channel onopen:', label, dc.id, readyState, 'negotiated:', dc.negotiated);
|
|
218
|
+
this[this.restrictablePromiseKey()][label]?.resolve(dc);
|
|
219
|
+
this[this.restrictablePromiseKey(kind)][label]?.resolve(dc);
|
|
216
220
|
};
|
|
217
|
-
if (
|
|
221
|
+
if (isTheirs) dc.onopen();
|
|
218
222
|
return dc;
|
|
219
223
|
}
|
|
220
224
|
ondatachannel(dc) {
|
|
@@ -229,10 +233,18 @@ export class WebRTC {
|
|
|
229
233
|
return this.setupChannel(this.pc.createDataChannel(name, {negotiated, id, ...options}));
|
|
230
234
|
}
|
|
231
235
|
dataChannelPromises = {};
|
|
232
|
-
|
|
236
|
+
dataChannelOursPromises = {};
|
|
237
|
+
dataChannelTheirsPromises = {};
|
|
238
|
+
restrictablePromiseKey(restriction = '') { // The property in which store dataChannel promises of the specified restriction.
|
|
239
|
+
return `dataChannel${restriction}Promises`;
|
|
240
|
+
}
|
|
241
|
+
getDataChannelPromise(name = 'data', restriction = '') { // Promise to resolve when opened, WITHOUT actually creating one.
|
|
242
|
+
// The application can restrict this to being only a channel of 'ours' or 'theirs' (see setupChannel), which is useful for
|
|
243
|
+
// non-negotiated channels.
|
|
244
|
+
const key = this.restrictablePromiseKey(restriction);
|
|
233
245
|
const {promise, resolve, reject} = Promise.withResolvers();
|
|
234
246
|
Object.assign(promise, {resolve, reject});
|
|
235
|
-
return this
|
|
247
|
+
return this[key][name] = promise;
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
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/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,17 @@ 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
|
+
// maximums
|
|
79
|
+
// 1 channel pair, without negotiated on first signal: nodejs:83, firefox:150+, safari:85+ but gets confused with closing, chrome/edge:50(?)
|
|
80
|
+
// 50 works across the board with one channel pair
|
|
81
|
+
// On Safari (only), anything more than 32 pair starts to loose messages on the SECOND channel.
|
|
82
|
+
const nPairs = includeSecondChannel ? 32 : 62; //32;
|
|
72
83
|
beforeAll(async function () {
|
|
73
84
|
const start = Date.now();
|
|
74
|
-
console.log('start setup');
|
|
85
|
+
console.log(new Date(), 'start setup', nPairs, 'pairs');
|
|
75
86
|
for (let index = 0; index < nPairs; index++) {
|
|
76
87
|
await setup({index});
|
|
77
88
|
}
|
|
@@ -81,8 +92,10 @@ describe("WebRTC", function () {
|
|
|
81
92
|
for (let index = 0; index < nPairs; index++) {
|
|
82
93
|
it(`connects ${index}.`, function () {
|
|
83
94
|
const {A, B} = connections[index];
|
|
84
|
-
expect(A.
|
|
85
|
-
expect(B.
|
|
95
|
+
expect(A.ours.readyState).toBe('open');
|
|
96
|
+
expect(B.ours.readyState).toBe('open');
|
|
97
|
+
expect(A.theirs.readyState).toBe('open');
|
|
98
|
+
expect(B.theirs.readyState).toBe('open');
|
|
86
99
|
});
|
|
87
100
|
it(`receives ${index}.`, async function () {
|
|
88
101
|
const {A, B} = connections[index];
|
|
@@ -143,6 +156,7 @@ describe("WebRTC", function () {
|
|
|
143
156
|
}
|
|
144
157
|
describe("one side opens", function () {
|
|
145
158
|
describe('non-negotiated', function () {
|
|
159
|
+
beforeAll(function () {console.log('one-sided non-negotiated'); });
|
|
146
160
|
standardBehavior(async function ({index}) {
|
|
147
161
|
const {A, B, bothOpen} = makePair({index});
|
|
148
162
|
A.createChannel('data', {negotiated: false});
|
|
@@ -150,6 +164,7 @@ describe("WebRTC", function () {
|
|
|
150
164
|
}, {includeConflictCheck: false, includeSecondChannel: false});
|
|
151
165
|
});
|
|
152
166
|
describe("negotiated on first signal", function () {
|
|
167
|
+
beforeAll(function () {console.log('one-sided negotiated'); });
|
|
153
168
|
standardBehavior(async function ({index}) {
|
|
154
169
|
const {A, B, bothOpen} = makePair({index});
|
|
155
170
|
// There isn't really a direct, automated way to have one side open another with negotiated:true,
|
|
@@ -173,6 +188,7 @@ describe("WebRTC", function () {
|
|
|
173
188
|
describe("simultaneous two-sided", function () {
|
|
174
189
|
describe("negotiated single full-duplex-channel", function () {
|
|
175
190
|
describe("impolite first", function () {
|
|
191
|
+
beforeAll(function () {console.log('two-sided negotiated impolite-first'); });
|
|
176
192
|
standardBehavior(async function ({index}) {
|
|
177
193
|
const {A, B, bothOpen} = makePair({index});
|
|
178
194
|
A.createChannel("data", {negotiated: true});
|
|
@@ -181,6 +197,7 @@ describe("WebRTC", function () {
|
|
|
181
197
|
});
|
|
182
198
|
});
|
|
183
199
|
describe("polite first", function () {
|
|
200
|
+
beforeAll(function () {console.log('two-sided negotiated polite-first');});
|
|
184
201
|
standardBehavior(async function ({index}) {
|
|
185
202
|
const {A, B, bothOpen} = makePair({index});
|
|
186
203
|
B.createChannel("data", {negotiated: true});
|
|
@@ -190,18 +207,21 @@ describe("WebRTC", function () {
|
|
|
190
207
|
});
|
|
191
208
|
});
|
|
192
209
|
describe("non-negotiated dual half-duplex channels", function () {
|
|
193
|
-
const delay =
|
|
210
|
+
const delay = 200;
|
|
211
|
+
const debug = false;
|
|
194
212
|
describe("impolite first", function () {
|
|
213
|
+
beforeAll(function () {console.log('two-sided non-negotiated impolite-first');});
|
|
195
214
|
standardBehavior(async function ({index}) {
|
|
196
|
-
const {A, B, bothOpen} = makePair({delay, index});
|
|
215
|
+
const {A, B, bothOpen} = makePair({delay, index, debug});
|
|
197
216
|
A.createChannel("data", {negotiated: false});
|
|
198
217
|
B.createChannel("data", {negotiated: false});
|
|
199
218
|
await bothOpen;
|
|
200
219
|
});
|
|
201
220
|
});
|
|
202
221
|
describe("polite first", function () {
|
|
222
|
+
beforeAll(function () {console.log('two-sided non-negotiated polite-first');});
|
|
203
223
|
standardBehavior(async function ({index}) {
|
|
204
|
-
const {A, B, bothOpen} = makePair({delay, index});
|
|
224
|
+
const {A, B, bothOpen} = makePair({delay, index, debug});
|
|
205
225
|
B.createChannel("data", {negotiated: false});
|
|
206
226
|
A.createChannel("data", {negotiated: false});
|
|
207
227
|
await bothOpen;
|