node-datachannel 0.10.1 → 0.11.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.
Files changed (38) hide show
  1. package/.github/workflows/build-win.yml +3 -3
  2. package/.gitmodules +3 -0
  3. package/CMakeLists.txt +1 -1
  4. package/README.md +18 -4
  5. package/examples/client-server/client-benchmark.js +7 -9
  6. package/examples/client-server/client-periodic.js +7 -9
  7. package/examples/client-server/client.js +7 -9
  8. package/examples/client-server/package-lock.json +0 -27
  9. package/examples/client-server/package.json +0 -1
  10. package/examples/client-server/signaling-server.js +21 -10
  11. package/examples/electron-demo/README.md +1 -1
  12. package/examples/websocket/websocket-client.js +20 -0
  13. package/examples/websocket/websocket-server.js +22 -0
  14. package/lib/index.cjs +52 -9
  15. package/lib/index.js +22 -15
  16. package/lib/node-datachannel.js +7 -0
  17. package/lib/websocket-server.js +34 -0
  18. package/package.json +4 -4
  19. package/polyfill/Events.js +5 -3
  20. package/polyfill/Exception.js +23 -0
  21. package/polyfill/README.md +4 -0
  22. package/polyfill/RTCDataChannel.js +19 -11
  23. package/polyfill/RTCPeerConnection.js +148 -41
  24. package/polyfill/index.cjs +252 -59
  25. package/src/peer-connection-wrapper.cpp +11 -3
  26. package/src/web-socket-server-wrapper.cpp +23 -23
  27. package/src/web-socket-server-wrapper.h +8 -6
  28. package/src/web-socket-wrapper.cpp +7 -1
  29. package/test/connectivity.js +1 -23
  30. package/test/wpt-tests/README.md +46 -0
  31. package/test/wpt-tests/chrome-failed-tests.js +42 -0
  32. package/test/wpt-tests/index.js +96 -0
  33. package/test/wpt-tests/last-test-results.md +205 -0
  34. package/test/wpt-tests/package-lock.json +1712 -0
  35. package/test/wpt-tests/package.json +19 -0
  36. package/test/wpt-tests/wpt-test-list.js +137 -0
  37. package/test/wpt-tests/wpt.js +131 -0
  38. package/test/wpt.js +0 -50
@@ -1,4 +1,5 @@
1
- import DOMException from 'node-domexception';
1
+ import 'node-domexception';
2
+ import * as exceptions from './Exception.js';
2
3
 
3
4
  export default class _RTCDataChannel extends EventTarget {
4
5
  #dataChannel;
@@ -10,6 +11,8 @@ export default class _RTCDataChannel extends EventTarget {
10
11
  #negotiated;
11
12
  #ordered;
12
13
 
14
+ #closeRequested = false;
15
+
13
16
  onbufferedamountlow;
14
17
  onclose;
15
18
  onclosing;
@@ -32,12 +35,20 @@ export default class _RTCDataChannel extends EventTarget {
32
35
  // forward dataChannel events
33
36
  this.#dataChannel.onOpen(() => {
34
37
  this.#readyState = 'open';
35
- this.dispatchEvent(new Event('open'));
38
+ this.dispatchEvent(new Event('open', { channel: this }));
36
39
  });
37
40
 
38
41
  this.#dataChannel.onClosed(() => {
39
- this.#readyState = 'closed';
40
- this.dispatchEvent(new Event('close'));
42
+ // Simulate closing event
43
+ if (!this.#closeRequested) {
44
+ this.#readyState = 'closing';
45
+ this.dispatchEvent(new Event('closing', { channel: this }));
46
+ }
47
+
48
+ setImmediate(() => {
49
+ this.#readyState = 'closed';
50
+ this.dispatchEvent(new Event('close', { channel: this }));
51
+ });
41
52
  });
42
53
 
43
54
  this.#dataChannel.onError((msg) => {
@@ -54,7 +65,7 @@ export default class _RTCDataChannel extends EventTarget {
54
65
  });
55
66
 
56
67
  this.#dataChannel.onBufferedAmountLow(() => {
57
- this.dispatchEvent(new Event('bufferedamountlow'));
68
+ this.dispatchEvent(new Event('bufferedamountlow', { channel: this }));
58
69
  });
59
70
 
60
71
  this.#dataChannel.onMessage((data) => {
@@ -62,7 +73,7 @@ export default class _RTCDataChannel extends EventTarget {
62
73
  data = data.buffer;
63
74
  }
64
75
 
65
- this.dispatchEvent(new MessageEvent('message', { data }));
76
+ this.dispatchEvent(new MessageEvent('message', { data, channel: this }));
66
77
  });
67
78
 
68
79
  // forward events to properties
@@ -148,9 +159,8 @@ export default class _RTCDataChannel extends EventTarget {
148
159
 
149
160
  send(data) {
150
161
  if (this.#readyState !== 'open') {
151
- throw new DOMException(
162
+ throw exceptions.InvalidStateError(
152
163
  "Failed to execute 'send' on 'RTCDataChannel': RTCDataChannel.readyState is not 'open'",
153
- 'InvalidStateError',
154
164
  );
155
165
  }
156
166
 
@@ -167,9 +177,7 @@ export default class _RTCDataChannel extends EventTarget {
167
177
  }
168
178
 
169
179
  close() {
170
- this.#readyState = 'closing';
171
- this.dispatchEvent(new Event('closing'));
172
-
180
+ this.#closeRequested = true;
173
181
  this.#dataChannel.close();
174
182
  }
175
183
  }
@@ -5,6 +5,7 @@ import RTCIceCandidate from './RTCIceCandidate.js';
5
5
  import { RTCDataChannelEvent, RTCPeerConnectionIceEvent } from './Events.js';
6
6
  import RTCSctpTransport from './RTCSctpTransport.js';
7
7
  import 'node-domexception';
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,98 @@ export default class _RTCPeerConnection extends EventTarget {
32
34
  onsignalingstatechange;
33
35
  ontrack;
34
36
 
35
- constructor(init = {}) {
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 match the regex "stun\:\w*|turn\:\w*|turns\:\w*"
59
+ if (
60
+ config.iceServers[i].urls?.some(
61
+ (url) => !/^(stun:[\w,\.,:]*|turn:[\w,\.,:]*|turns:[\w,\.,:]*)$/.test(url),
62
+ )
63
+ )
64
+ throw exceptions.SyntaxError('IceServers urls wrong format');
65
+
66
+ // If this is a turn server check for username and credential
67
+ if (config.iceServers[i].urls?.some((url) => url.startsWith('turn'))) {
68
+ if (!config.iceServers[i].username)
69
+ throw exceptions.InvalidAccessError('IceServers username cannot be null');
70
+ if (!config.iceServers[i].credential)
71
+ throw exceptions.InvalidAccessError('IceServers username cannot be undefined');
72
+ }
73
+
74
+ // length of urls can not be 0
75
+ if (config.iceServers[i].urls?.length === 0)
76
+ throw exceptions.SyntaxError('IceServers urls cannot be empty');
77
+ }
78
+ }
79
+
80
+ if (
81
+ config &&
82
+ config.iceTransportPolicy &&
83
+ config.iceTransportPolicy !== 'all' &&
84
+ config.iceTransportPolicy !== 'relay'
85
+ )
86
+ throw new TypeError('IceTransportPolicy must be either "all" or "relay"');
87
+ }
88
+
89
+ setConfiguration(config) {
90
+ this._checkConfiguration(config);
91
+ this.#config = config;
92
+ }
93
+
94
+ constructor(config = { iceServers: [], iceTransportPolicy: 'all' }) {
36
95
  super();
37
96
 
38
- this.#config = init;
97
+ this._checkConfiguration(config);
98
+ this.#config = config;
39
99
  this.#localOffer = createDeferredPromise();
40
100
  this.#localAnswer = createDeferredPromise();
41
101
  this.#dataChannels = new Set();
42
102
  this.#canTrickleIceCandidates = null;
43
103
 
44
- this.#peerConnection = new NodeDataChannel.PeerConnection(init?.peerIdentity ?? `peer-${getRandomString(7)}`, {
45
- ...init,
46
- iceServers:
47
- init?.iceServers
48
- ?.map((server) => {
49
- const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
50
-
51
- return urls.map((url) => {
52
- if (server.username && server.credential) {
53
- const [protocol, rest] = url.split(/:(.*)/);
54
- return `${protocol}:${server.username}:${server.credential}@${rest}`;
55
- }
56
- return url;
57
- });
58
- })
59
- .flat() ?? [],
60
- });
104
+ try {
105
+ this.#peerConnection = new NodeDataChannel.PeerConnection(
106
+ config?.peerIdentity ?? `peer-${getRandomString(7)}`,
107
+ {
108
+ ...config,
109
+ iceServers:
110
+ config?.iceServers
111
+ ?.map((server) => {
112
+ const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
113
+
114
+ return urls.map((url) => {
115
+ if (server.username && server.credential) {
116
+ const [protocol, rest] = url.split(/:(.*)/);
117
+ return `${protocol}:${server.username}:${server.credential}@${rest}`;
118
+ }
119
+ return url;
120
+ });
121
+ })
122
+ .flat() ?? [],
123
+ },
124
+ );
125
+ } catch (error) {
126
+ if (!error || !error.message) throw exceptions.NotFoundError('Unknown error');
127
+ throw exceptions.SyntaxError(error.message);
128
+ }
61
129
 
62
130
  // forward peerConnection events
63
131
  this.#peerConnection.onStateChange(() => {
@@ -77,9 +145,9 @@ export default class _RTCPeerConnection extends EventTarget {
77
145
  });
78
146
 
79
147
  this.#peerConnection.onDataChannel((channel) => {
80
- const dataChannel = new RTCDataChannel(channel);
81
- this.#dataChannels.add(dataChannel);
82
- this.dispatchEvent(new RTCDataChannelEvent(dataChannel));
148
+ const dc = new RTCDataChannel(channel);
149
+ this.#dataChannels.add(dc);
150
+ this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel: dc }));
83
151
  });
84
152
 
85
153
  this.#peerConnection.onLocalDescription((sdp, type) => {
@@ -153,7 +221,11 @@ export default class _RTCPeerConnection extends EventTarget {
153
221
  }
154
222
 
155
223
  get iceConnectionState() {
156
- return this.#peerConnection.iceState();
224
+ let state = this.#peerConnection.iceState();
225
+ // libdatachannel uses 'completed' instead of 'connected'
226
+ // see /webrtc/getstats.html
227
+ if (state == 'completed') state = 'connected';
228
+ return state;
157
229
  }
158
230
 
159
231
  get iceGatheringState() {
@@ -193,14 +265,45 @@ export default class _RTCPeerConnection extends EventTarget {
193
265
  }
194
266
 
195
267
  async addIceCandidate(candidate) {
196
- if (candidate == null || candidate.candidate == null) {
197
- throw new DOMException('Candidate invalid');
268
+ if (!candidate || !candidate.candidate) {
269
+ return;
270
+ }
271
+
272
+ if (candidate.sdpMid === null && candidate.sdpMLineIndex === null) {
273
+ throw new TypeError('sdpMid must be set');
274
+ }
275
+
276
+ if (candidate.sdpMid === undefined && candidate.sdpMLineIndex == undefined) {
277
+ throw new TypeError('sdpMid must be set');
198
278
  }
199
279
 
200
- this.#remoteCandidates.push(
201
- new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid || '0' }),
202
- );
203
- this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid || '0');
280
+ // Reject if sdpMid format is not valid
281
+ // ??
282
+ if (candidate.sdpMid && candidate.sdpMid.length > 3) {
283
+ // console.log(candidate.sdpMid);
284
+ throw exceptions.OperationError('Invalid sdpMid format');
285
+ }
286
+
287
+ // We don't care about sdpMLineIndex, just for test
288
+ if (!candidate.sdpMid && candidate.sdpMLineIndex > 1) {
289
+ throw exceptions.OperationError('This is only for test case.');
290
+ }
291
+
292
+ try {
293
+ this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid || '0');
294
+ this.#remoteCandidates.push(
295
+ new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid || '0' }),
296
+ );
297
+ } catch (error) {
298
+ if (!error || !error.message) throw exceptions.NotFoundError('Unknown error');
299
+
300
+ // Check error Message if contains specific message
301
+ if (error.message.includes('remote candidate without remote description'))
302
+ throw exceptions.InvalidStateError(error.message);
303
+ if (error.message.includes('Invalid candidate format')) throw exceptions.OperationError(error.message);
304
+
305
+ throw exceptions.NotFoundError(error.message);
306
+ }
204
307
  }
205
308
 
206
309
  addTrack(track, ...streams) {
@@ -215,6 +318,7 @@ export default class _RTCPeerConnection extends EventTarget {
215
318
  // close all channels before shutting down
216
319
  this.#dataChannels.forEach((channel) => {
217
320
  channel.close();
321
+ this.#dataChannelsClosed++;
218
322
  });
219
323
 
220
324
  this.#peerConnection.close();
@@ -232,6 +336,7 @@ export default class _RTCPeerConnection extends EventTarget {
232
336
  this.#dataChannels.add(dataChannel);
233
337
  dataChannel.addEventListener('close', () => {
234
338
  this.#dataChannels.delete(dataChannel);
339
+ this.#dataChannelsClosed++;
235
340
  });
236
341
 
237
342
  return dataChannel;
@@ -265,7 +370,7 @@ export default class _RTCPeerConnection extends EventTarget {
265
370
  let localId = 'RTCIceCandidate_' + localIdRs;
266
371
  report.set(localId, {
267
372
  id: localId,
268
- type: 'localcandidate',
373
+ type: 'local-candidate',
269
374
  timestamp: Date.now(),
270
375
  candidateType: cp.local.type,
271
376
  ip: cp.local.address,
@@ -276,7 +381,7 @@ export default class _RTCPeerConnection extends EventTarget {
276
381
  let remoteId = 'RTCIceCandidate_' + remoteIdRs;
277
382
  report.set(remoteId, {
278
383
  id: remoteId,
279
- type: 'remotecandidate',
384
+ type: 'remote-candidate',
280
385
  timestamp: Date.now(),
281
386
  candidateType: cp.remote.type,
282
387
  ip: cp.remote.address,
@@ -311,6 +416,15 @@ export default class _RTCPeerConnection extends EventTarget {
311
416
  selectedCandidatePairChanges: 1,
312
417
  });
313
418
 
419
+ // peer-connection'
420
+ report.set('P', {
421
+ id: 'P',
422
+ type: 'peer-connection',
423
+ timestamp: Date.now(),
424
+ dataChannelsOpened: this.#dataChannels.size,
425
+ dataChannelsClosed: this.#dataChannelsClosed,
426
+ });
427
+
314
428
  return resolve(report);
315
429
  });
316
430
  }
@@ -327,20 +441,13 @@ export default class _RTCPeerConnection extends EventTarget {
327
441
  throw new DOMException('Not implemented');
328
442
  }
329
443
 
330
- setConfiguration(config) {
331
- this.#config = config;
332
- }
333
-
334
444
  async setLocalDescription(description) {
335
- if (description == null || description.type == null) {
336
- throw new DOMException('Local description type must be set');
337
- }
338
-
339
- if (description.type !== 'offer') {
445
+ if (description?.type !== 'offer') {
340
446
  // any other type causes libdatachannel to throw
341
447
  return;
342
448
  }
343
- this.#peerConnection.setLocalDescription(description.type);
449
+
450
+ this.#peerConnection.setLocalDescription(description?.type);
344
451
  }
345
452
 
346
453
  async setRemoteDescription(description) {