node-datachannel 0.10.0 → 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 (40) hide show
  1. package/.github/workflows/build-mac-m1.yml +7 -6
  2. package/.github/workflows/build-mac-x64.yml +5 -5
  3. package/.github/workflows/build-win.yml +3 -3
  4. package/.gitmodules +3 -0
  5. package/CMakeLists.txt +2 -2
  6. package/README.md +33 -31
  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/websocket/websocket-client.js +20 -0
  15. package/examples/websocket/websocket-server.js +22 -0
  16. package/lib/index.cjs +52 -9
  17. package/lib/index.js +22 -15
  18. package/lib/node-datachannel.js +7 -0
  19. package/lib/websocket-server.js +34 -0
  20. package/package.json +4 -4
  21. package/polyfill/Events.js +5 -3
  22. package/polyfill/Exception.js +23 -0
  23. package/polyfill/README.md +3 -3
  24. package/polyfill/RTCDataChannel.js +19 -11
  25. package/polyfill/RTCPeerConnection.js +148 -41
  26. package/polyfill/index.cjs +252 -59
  27. package/src/peer-connection-wrapper.cpp +11 -3
  28. package/src/web-socket-server-wrapper.cpp +23 -23
  29. package/src/web-socket-server-wrapper.h +8 -6
  30. package/src/web-socket-wrapper.cpp +7 -1
  31. package/test/connectivity.js +1 -23
  32. package/test/wpt-tests/README.md +46 -0
  33. package/test/wpt-tests/chrome-failed-tests.js +42 -0
  34. package/test/wpt-tests/index.js +96 -0
  35. package/test/wpt-tests/last-test-results.md +205 -0
  36. package/test/wpt-tests/package-lock.json +1712 -0
  37. package/test/wpt-tests/package.json +19 -0
  38. package/test/wpt-tests/wpt-test-list.js +137 -0
  39. package/test/wpt-tests/wpt.js +131 -0
  40. package/test/wpt.js +0 -50
package/lib/index.js CHANGED
@@ -1,9 +1,6 @@
1
- // createRequire is native in node version >= 12
2
- import { createRequire } from 'module';
3
- const require = createRequire(import.meta.url);
4
-
5
- const nodeDataChannel = require('../build/Release/node_datachannel.node');
1
+ import nodeDataChannel from './node-datachannel.js';
6
2
  import DataChannelStream from './datachannel-stream.js';
3
+ import WebSocketServer from './websocket-server.js';
7
4
 
8
5
  const {
9
6
  initLogger,
@@ -17,9 +14,16 @@ const {
17
14
  DataChannel,
18
15
  PeerConnection,
19
16
  WebSocket,
20
- WebSocketServer,
21
17
  } = nodeDataChannel;
22
18
 
19
+ export const DescriptionType = {
20
+ Unspec: 'unspec',
21
+ Offer: 'offer',
22
+ Answer: 'answer',
23
+ Pranswer: 'pranswer',
24
+ Rollback: 'rollback',
25
+ };
26
+
23
27
  export {
24
28
  initLogger,
25
29
  cleanup,
@@ -38,14 +42,17 @@ export {
38
42
  };
39
43
 
40
44
  export default {
41
- ...nodeDataChannel,
45
+ initLogger,
46
+ cleanup,
47
+ preload,
48
+ setSctpSettings,
49
+ RtcpReceivingSession,
50
+ Track,
51
+ Video,
52
+ Audio,
53
+ DataChannel,
54
+ PeerConnection,
55
+ WebSocket,
56
+ WebSocketServer,
42
57
  DataChannelStream,
43
58
  };
44
-
45
- export const DescriptionType = {
46
- Unspec: 'unspec',
47
- Offer: 'offer',
48
- Answer: 'answer',
49
- Pranswer: 'pranswer',
50
- Rollback: 'rollback',
51
- };
@@ -0,0 +1,7 @@
1
+ // createRequire is native in node version >= 12
2
+ import { createRequire } from 'module';
3
+ const require = createRequire(import.meta.url);
4
+
5
+ const nodeDataChannel = require('../build/Release/node_datachannel.node');
6
+
7
+ export default nodeDataChannel;
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from 'events';
2
+ import nodeDataChannel from './node-datachannel.js';
3
+
4
+ export default class WebSocketServer extends EventEmitter {
5
+ #server;
6
+ #clients = [];
7
+
8
+ constructor(options) {
9
+ super();
10
+ this.#server = new nodeDataChannel.WebSocketServer(options);
11
+
12
+ this.#server.onClient((client) => {
13
+ this.emit('client', client);
14
+ this.#clients.push(client);
15
+ });
16
+ }
17
+
18
+ port() {
19
+ return this.#server?.port() || 0;
20
+ }
21
+
22
+ stop() {
23
+ this.#clients.forEach((client) => {
24
+ client?.close();
25
+ });
26
+ this.#server?.stop();
27
+ this.#server = null;
28
+ this.removeAllListeners();
29
+ }
30
+
31
+ onClient(cb) {
32
+ if (this.#server) this.on('client', cb);
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-datachannel",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "libdatachannel node bindings",
5
5
  "type": "module",
6
6
  "types": "./lib/index.d.ts",
@@ -93,11 +93,11 @@
93
93
  "node-addon-api": "^7.0.0",
94
94
  "prebuild": "^12.0.0",
95
95
  "prettier": "^2.8.8",
96
- "typescript": "^5.3.2",
97
- "rollup": "^4.14.1"
96
+ "rollup": "^4.14.1",
97
+ "typescript": "^5.3.2"
98
98
  },
99
99
  "dependencies": {
100
100
  "node-domexception": "^2.0.1",
101
101
  "prebuild-install": "^7.0.1"
102
102
  }
103
- }
103
+ }
@@ -15,10 +15,12 @@ export class RTCPeerConnectionIceEvent extends Event {
15
15
  export class RTCDataChannelEvent extends Event {
16
16
  #channel;
17
17
 
18
- constructor(channel) {
19
- super('datachannel');
18
+ constructor(type, eventInitDict) {
19
+ super(type);
20
20
 
21
- this.#channel = channel;
21
+ if (type && !eventInitDict.channel) throw new TypeError('channel member is required');
22
+
23
+ this.#channel = eventInitDict?.channel;
22
24
  }
23
25
 
24
26
  get channel() {
@@ -0,0 +1,23 @@
1
+ export const InvalidStateError = (msg) => {
2
+ return new DOMException(msg, 'InvalidStateError');
3
+ };
4
+
5
+ export const InvalidAccessError = (msg) => {
6
+ return new DOMException(msg, 'InvalidAccessError');
7
+ };
8
+
9
+ export const NotFoundError = (msg) => {
10
+ return new DOMException(msg, 'NotFoundError');
11
+ };
12
+
13
+ export const OperationError = (msg) => {
14
+ return new DOMException(msg, 'OperationError');
15
+ };
16
+
17
+ export const TypeError = (msg) => {
18
+ return new DOMException(msg, 'TypeError');
19
+ };
20
+
21
+ export const SyntaxError = (msg) => {
22
+ return new DOMException(msg, 'SyntaxError');
23
+ };
@@ -1,10 +1,10 @@
1
1
  # WebRTC Polyfills
2
2
 
3
- > This part is ongoing work and is considered to be in beta phase.
3
+ WebRTC polyfills to be used for libraries like `simple-peer`.
4
4
 
5
- > Any feedback and contributions are welcomed.
5
+ # web-platform-tests
6
6
 
7
- WebRTC polyfills to be used for libraries like `simple-peer`.
7
+ Please check actual situation [here](/test/wpt-tests/)
8
8
 
9
9
  ## Example Usage
10
10
 
@@ -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) {