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.
Files changed (48) hide show
  1. package/.github/workflows/build-linux.yml +7 -2
  2. package/.github/workflows/build-win.yml +3 -3
  3. package/.gitmodules +3 -0
  4. package/CMakeLists.txt +1 -1
  5. package/README.md +18 -4
  6. package/cmake/toolchain/ci.cmake +1 -1
  7. package/examples/client-server/client-benchmark.js +7 -9
  8. package/examples/client-server/client-periodic.js +7 -9
  9. package/examples/client-server/client.js +7 -9
  10. package/examples/client-server/package-lock.json +0 -27
  11. package/examples/client-server/package.json +0 -1
  12. package/examples/client-server/signaling-server.js +21 -10
  13. package/examples/electron-demo/README.md +1 -1
  14. package/examples/electron-demo/package-lock.json +316 -372
  15. package/examples/electron-demo/package.json +2 -1
  16. package/examples/electron-demo/webpack.main.config.js +10 -0
  17. package/examples/websocket/websocket-client.js +20 -0
  18. package/examples/websocket/websocket-server.js +22 -0
  19. package/jest.config.cjs +1 -1
  20. package/lib/index.cjs +52 -9
  21. package/lib/index.js +22 -15
  22. package/lib/node-datachannel.js +7 -0
  23. package/lib/websocket-server.js +34 -0
  24. package/package.json +3 -3
  25. package/polyfill/Events.js +5 -3
  26. package/polyfill/Exception.js +23 -0
  27. package/polyfill/README.md +4 -0
  28. package/polyfill/RTCDataChannel.js +19 -11
  29. package/polyfill/RTCError.d.ts +11 -0
  30. package/polyfill/RTCError.js +65 -0
  31. package/polyfill/RTCIceCandidate.js +22 -23
  32. package/polyfill/RTCPeerConnection.js +157 -42
  33. package/polyfill/index.cjs +350 -82
  34. package/polyfill/index.js +3 -0
  35. package/src/peer-connection-wrapper.cpp +11 -3
  36. package/src/web-socket-server-wrapper.cpp +23 -23
  37. package/src/web-socket-server-wrapper.h +8 -6
  38. package/src/web-socket-wrapper.cpp +7 -1
  39. package/test/connectivity.js +1 -23
  40. package/test/wpt-tests/README.md +46 -0
  41. package/test/wpt-tests/chrome-failed-tests.js +42 -0
  42. package/test/wpt-tests/index.js +96 -0
  43. package/test/wpt-tests/last-test-results.md +301 -0
  44. package/test/wpt-tests/package-lock.json +1712 -0
  45. package/test/wpt-tests/package.json +19 -0
  46. package/test/wpt-tests/wpt-test-list.js +139 -0
  47. package/test/wpt-tests/wpt.js +131 -0
  48. 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 '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,106 @@ 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 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.#config = init;
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
- 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
- });
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 dataChannel = new RTCDataChannel(channel);
81
- this.#dataChannels.add(dataChannel);
82
- this.dispatchEvent(new RTCDataChannelEvent(dataChannel));
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
- return this.#peerConnection.iceState();
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 == null || candidate.candidate == null) {
197
- throw new DOMException('Candidate invalid');
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
- this.#remoteCandidates.push(
201
- new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid || '0' }),
202
- );
203
- this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid || '0');
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: 'localcandidate',
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: 'remotecandidate',
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 == null || description.type == null) {
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
- this.#peerConnection.setLocalDescription(description.type);
457
+
458
+ this.#peerConnection.setLocalDescription(description?.type);
344
459
  }
345
460
 
346
461
  async setRemoteDescription(description) {