node-datachannel 0.10.1 → 0.12.0
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/build-linux.yml +7 -2
- package/.github/workflows/build-win.yml +3 -3
- package/.gitmodules +3 -0
- package/CMakeLists.txt +1 -1
- package/README.md +18 -4
- package/cmake/toolchain/ci.cmake +1 -1
- package/examples/client-server/client-benchmark.js +7 -9
- package/examples/client-server/client-periodic.js +7 -9
- package/examples/client-server/client.js +7 -9
- package/examples/client-server/package-lock.json +0 -27
- package/examples/client-server/package.json +0 -1
- package/examples/client-server/signaling-server.js +21 -10
- package/examples/electron-demo/README.md +1 -1
- package/examples/electron-demo/package-lock.json +316 -372
- package/examples/electron-demo/package.json +2 -1
- package/examples/electron-demo/webpack.main.config.js +10 -0
- package/examples/websocket/websocket-client.js +20 -0
- package/examples/websocket/websocket-server.js +22 -0
- package/jest.config.cjs +1 -1
- package/lib/index.cjs +52 -9
- package/lib/index.js +22 -15
- package/lib/node-datachannel.js +7 -0
- package/lib/websocket-server.js +34 -0
- package/package.json +3 -3
- package/polyfill/Events.js +5 -3
- package/polyfill/Exception.js +23 -0
- package/polyfill/README.md +4 -0
- package/polyfill/RTCDataChannel.js +19 -11
- package/polyfill/RTCError.d.ts +11 -0
- package/polyfill/RTCError.js +65 -0
- package/polyfill/RTCIceCandidate.js +22 -23
- package/polyfill/RTCPeerConnection.js +157 -42
- package/polyfill/index.cjs +350 -82
- package/polyfill/index.js +3 -0
- package/src/peer-connection-wrapper.cpp +11 -3
- package/src/web-socket-server-wrapper.cpp +23 -23
- package/src/web-socket-server-wrapper.h +8 -6
- package/src/web-socket-wrapper.cpp +7 -1
- package/test/connectivity.js +1 -23
- package/test/wpt-tests/README.md +46 -0
- package/test/wpt-tests/chrome-failed-tests.js +42 -0
- package/test/wpt-tests/index.js +96 -0
- package/test/wpt-tests/last-test-results.md +301 -0
- package/test/wpt-tests/package-lock.json +1712 -0
- package/test/wpt-tests/package.json +19 -0
- package/test/wpt-tests/wpt-test-list.js +139 -0
- package/test/wpt-tests/wpt.js +131 -0
- package/test/wpt.js +0 -50
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import 'node-domexception';
|
|
1
2
|
import NodeDataChannel from '../lib/index.js';
|
|
2
3
|
import RTCSessionDescription from './RTCSessionDescription.js';
|
|
3
4
|
import RTCDataChannel from './RTCDataChannel.js';
|
|
4
5
|
import RTCIceCandidate from './RTCIceCandidate.js';
|
|
5
6
|
import { RTCDataChannelEvent, RTCPeerConnectionIceEvent } from './Events.js';
|
|
6
7
|
import RTCSctpTransport from './RTCSctpTransport.js';
|
|
7
|
-
import '
|
|
8
|
+
import * as exceptions from './Exception.js';
|
|
8
9
|
|
|
9
10
|
export default class _RTCPeerConnection extends EventTarget {
|
|
10
11
|
static async generateCertificate() {
|
|
@@ -15,6 +16,7 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
15
16
|
#localOffer;
|
|
16
17
|
#localAnswer;
|
|
17
18
|
#dataChannels;
|
|
19
|
+
#dataChannelsClosed = 0;
|
|
18
20
|
#config;
|
|
19
21
|
#canTrickleIceCandidates;
|
|
20
22
|
#sctp;
|
|
@@ -32,32 +34,106 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
32
34
|
onsignalingstatechange;
|
|
33
35
|
ontrack;
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
_checkConfiguration(config) {
|
|
38
|
+
if (config && config.iceServers === undefined) config.iceServers = [];
|
|
39
|
+
if (config && config.iceTransportPolicy === undefined) config.iceTransportPolicy = 'all';
|
|
40
|
+
|
|
41
|
+
if (config?.iceServers === null) throw new TypeError('IceServers cannot be null');
|
|
42
|
+
|
|
43
|
+
// Check for all the properties of iceServers
|
|
44
|
+
if (Array.isArray(config?.iceServers)) {
|
|
45
|
+
for (let i = 0; i < config.iceServers.length; i++) {
|
|
46
|
+
if (config.iceServers[i] === null) throw new TypeError('IceServers cannot be null');
|
|
47
|
+
if (config.iceServers[i] === undefined) throw new TypeError('IceServers cannot be undefined');
|
|
48
|
+
if (Object.keys(config.iceServers[i]).length === 0) throw new TypeError('IceServers cannot be empty');
|
|
49
|
+
|
|
50
|
+
// If iceServers is string convert to array
|
|
51
|
+
if (typeof config.iceServers[i].urls === 'string')
|
|
52
|
+
config.iceServers[i].urls = [config.iceServers[i].urls];
|
|
53
|
+
|
|
54
|
+
// urls can not be empty
|
|
55
|
+
if (config.iceServers[i].urls?.some((url) => url == ''))
|
|
56
|
+
throw exceptions.SyntaxError('IceServers urls cannot be empty');
|
|
57
|
+
|
|
58
|
+
// urls should be valid URLs and match the protocols "stun:|turn:|turns:"
|
|
59
|
+
if (
|
|
60
|
+
config.iceServers[i].urls?.some(
|
|
61
|
+
(url) => {
|
|
62
|
+
try {
|
|
63
|
+
const parsedURL = new URL(url)
|
|
64
|
+
|
|
65
|
+
return !/^(stun:|turn:|turns:)$/.test(parsedURL.protocol)
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
throw exceptions.SyntaxError('IceServers urls wrong format');
|
|
73
|
+
|
|
74
|
+
// If this is a turn server check for username and credential
|
|
75
|
+
if (config.iceServers[i].urls?.some((url) => url.startsWith('turn'))) {
|
|
76
|
+
if (!config.iceServers[i].username)
|
|
77
|
+
throw exceptions.InvalidAccessError('IceServers username cannot be null');
|
|
78
|
+
if (!config.iceServers[i].credential)
|
|
79
|
+
throw exceptions.InvalidAccessError('IceServers username cannot be undefined');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// length of urls can not be 0
|
|
83
|
+
if (config.iceServers[i].urls?.length === 0)
|
|
84
|
+
throw exceptions.SyntaxError('IceServers urls cannot be empty');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
config &&
|
|
90
|
+
config.iceTransportPolicy &&
|
|
91
|
+
config.iceTransportPolicy !== 'all' &&
|
|
92
|
+
config.iceTransportPolicy !== 'relay'
|
|
93
|
+
)
|
|
94
|
+
throw new TypeError('IceTransportPolicy must be either "all" or "relay"');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setConfiguration(config) {
|
|
98
|
+
this._checkConfiguration(config);
|
|
99
|
+
this.#config = config;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
constructor(config = { iceServers: [], iceTransportPolicy: 'all' }) {
|
|
36
103
|
super();
|
|
37
104
|
|
|
38
|
-
this
|
|
105
|
+
this._checkConfiguration(config);
|
|
106
|
+
this.#config = config;
|
|
39
107
|
this.#localOffer = createDeferredPromise();
|
|
40
108
|
this.#localAnswer = createDeferredPromise();
|
|
41
109
|
this.#dataChannels = new Set();
|
|
42
110
|
this.#canTrickleIceCandidates = null;
|
|
43
111
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
112
|
+
try {
|
|
113
|
+
this.#peerConnection = new NodeDataChannel.PeerConnection(
|
|
114
|
+
config?.peerIdentity ?? `peer-${getRandomString(7)}`,
|
|
115
|
+
{
|
|
116
|
+
...config,
|
|
117
|
+
iceServers:
|
|
118
|
+
config?.iceServers
|
|
119
|
+
?.map((server) => {
|
|
120
|
+
const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
|
|
121
|
+
|
|
122
|
+
return urls.map((url) => {
|
|
123
|
+
if (server.username && server.credential) {
|
|
124
|
+
const [protocol, rest] = url.split(/:(.*)/);
|
|
125
|
+
return `${protocol}:${server.username}:${server.credential}@${rest}`;
|
|
126
|
+
}
|
|
127
|
+
return url;
|
|
128
|
+
});
|
|
129
|
+
})
|
|
130
|
+
.flat() ?? [],
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (!error || !error.message) throw exceptions.NotFoundError('Unknown error');
|
|
135
|
+
throw exceptions.SyntaxError(error.message);
|
|
136
|
+
}
|
|
61
137
|
|
|
62
138
|
// forward peerConnection events
|
|
63
139
|
this.#peerConnection.onStateChange(() => {
|
|
@@ -77,9 +153,9 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
77
153
|
});
|
|
78
154
|
|
|
79
155
|
this.#peerConnection.onDataChannel((channel) => {
|
|
80
|
-
const
|
|
81
|
-
this.#dataChannels.add(
|
|
82
|
-
this.dispatchEvent(new RTCDataChannelEvent(
|
|
156
|
+
const dc = new RTCDataChannel(channel);
|
|
157
|
+
this.#dataChannels.add(dc);
|
|
158
|
+
this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel: dc }));
|
|
83
159
|
});
|
|
84
160
|
|
|
85
161
|
this.#peerConnection.onLocalDescription((sdp, type) => {
|
|
@@ -153,7 +229,11 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
153
229
|
}
|
|
154
230
|
|
|
155
231
|
get iceConnectionState() {
|
|
156
|
-
|
|
232
|
+
let state = this.#peerConnection.iceState();
|
|
233
|
+
// libdatachannel uses 'completed' instead of 'connected'
|
|
234
|
+
// see /webrtc/getstats.html
|
|
235
|
+
if (state == 'completed') state = 'connected';
|
|
236
|
+
return state;
|
|
157
237
|
}
|
|
158
238
|
|
|
159
239
|
get iceGatheringState() {
|
|
@@ -193,14 +273,45 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
193
273
|
}
|
|
194
274
|
|
|
195
275
|
async addIceCandidate(candidate) {
|
|
196
|
-
if (candidate
|
|
197
|
-
|
|
276
|
+
if (!candidate || !candidate.candidate) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (candidate.sdpMid === null && candidate.sdpMLineIndex === null) {
|
|
281
|
+
throw new TypeError('sdpMid must be set');
|
|
198
282
|
}
|
|
199
283
|
|
|
200
|
-
|
|
201
|
-
new
|
|
202
|
-
|
|
203
|
-
|
|
284
|
+
if (candidate.sdpMid === undefined && candidate.sdpMLineIndex == undefined) {
|
|
285
|
+
throw new TypeError('sdpMid must be set');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Reject if sdpMid format is not valid
|
|
289
|
+
// ??
|
|
290
|
+
if (candidate.sdpMid && candidate.sdpMid.length > 3) {
|
|
291
|
+
// console.log(candidate.sdpMid);
|
|
292
|
+
throw exceptions.OperationError('Invalid sdpMid format');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// We don't care about sdpMLineIndex, just for test
|
|
296
|
+
if (!candidate.sdpMid && candidate.sdpMLineIndex > 1) {
|
|
297
|
+
throw exceptions.OperationError('This is only for test case.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid || '0');
|
|
302
|
+
this.#remoteCandidates.push(
|
|
303
|
+
new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid || '0' }),
|
|
304
|
+
);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (!error || !error.message) throw exceptions.NotFoundError('Unknown error');
|
|
307
|
+
|
|
308
|
+
// Check error Message if contains specific message
|
|
309
|
+
if (error.message.includes('remote candidate without remote description'))
|
|
310
|
+
throw exceptions.InvalidStateError(error.message);
|
|
311
|
+
if (error.message.includes('Invalid candidate format')) throw exceptions.OperationError(error.message);
|
|
312
|
+
|
|
313
|
+
throw exceptions.NotFoundError(error.message);
|
|
314
|
+
}
|
|
204
315
|
}
|
|
205
316
|
|
|
206
317
|
addTrack(track, ...streams) {
|
|
@@ -215,6 +326,7 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
215
326
|
// close all channels before shutting down
|
|
216
327
|
this.#dataChannels.forEach((channel) => {
|
|
217
328
|
channel.close();
|
|
329
|
+
this.#dataChannelsClosed++;
|
|
218
330
|
});
|
|
219
331
|
|
|
220
332
|
this.#peerConnection.close();
|
|
@@ -232,6 +344,7 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
232
344
|
this.#dataChannels.add(dataChannel);
|
|
233
345
|
dataChannel.addEventListener('close', () => {
|
|
234
346
|
this.#dataChannels.delete(dataChannel);
|
|
347
|
+
this.#dataChannelsClosed++;
|
|
235
348
|
});
|
|
236
349
|
|
|
237
350
|
return dataChannel;
|
|
@@ -265,7 +378,7 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
265
378
|
let localId = 'RTCIceCandidate_' + localIdRs;
|
|
266
379
|
report.set(localId, {
|
|
267
380
|
id: localId,
|
|
268
|
-
type: '
|
|
381
|
+
type: 'local-candidate',
|
|
269
382
|
timestamp: Date.now(),
|
|
270
383
|
candidateType: cp.local.type,
|
|
271
384
|
ip: cp.local.address,
|
|
@@ -276,7 +389,7 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
276
389
|
let remoteId = 'RTCIceCandidate_' + remoteIdRs;
|
|
277
390
|
report.set(remoteId, {
|
|
278
391
|
id: remoteId,
|
|
279
|
-
type: '
|
|
392
|
+
type: 'remote-candidate',
|
|
280
393
|
timestamp: Date.now(),
|
|
281
394
|
candidateType: cp.remote.type,
|
|
282
395
|
ip: cp.remote.address,
|
|
@@ -311,6 +424,15 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
311
424
|
selectedCandidatePairChanges: 1,
|
|
312
425
|
});
|
|
313
426
|
|
|
427
|
+
// peer-connection'
|
|
428
|
+
report.set('P', {
|
|
429
|
+
id: 'P',
|
|
430
|
+
type: 'peer-connection',
|
|
431
|
+
timestamp: Date.now(),
|
|
432
|
+
dataChannelsOpened: this.#dataChannels.size,
|
|
433
|
+
dataChannelsClosed: this.#dataChannelsClosed,
|
|
434
|
+
});
|
|
435
|
+
|
|
314
436
|
return resolve(report);
|
|
315
437
|
});
|
|
316
438
|
}
|
|
@@ -327,20 +449,13 @@ export default class _RTCPeerConnection extends EventTarget {
|
|
|
327
449
|
throw new DOMException('Not implemented');
|
|
328
450
|
}
|
|
329
451
|
|
|
330
|
-
setConfiguration(config) {
|
|
331
|
-
this.#config = config;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
452
|
async setLocalDescription(description) {
|
|
335
|
-
if (description
|
|
336
|
-
throw new DOMException('Local description type must be set');
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (description.type !== 'offer') {
|
|
453
|
+
if (description?.type !== 'offer') {
|
|
340
454
|
// any other type causes libdatachannel to throw
|
|
341
455
|
return;
|
|
342
456
|
}
|
|
343
|
-
|
|
457
|
+
|
|
458
|
+
this.#peerConnection.setLocalDescription(description?.type);
|
|
344
459
|
}
|
|
345
460
|
|
|
346
461
|
async setRemoteDescription(description) {
|