node-datachannel 0.27.0 → 0.28.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 (96) hide show
  1. package/.eslintrc.json +25 -25
  2. package/.prettierignore +4 -0
  3. package/.prettierrc +1 -0
  4. package/API.md +4 -4
  5. package/BULDING.md +11 -2
  6. package/CMakeLists.txt +9 -4
  7. package/README.md +22 -18
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/lib/datachannel-stream.cjs +13 -13
  10. package/dist/cjs/lib/datachannel-stream.cjs.map +1 -1
  11. package/dist/cjs/lib/index.cjs.map +1 -1
  12. package/dist/cjs/lib/websocket-server.cjs.map +1 -1
  13. package/dist/cjs/lib/websocket.cjs.map +1 -1
  14. package/dist/cjs/polyfill/Events.cjs +52 -3
  15. package/dist/cjs/polyfill/Events.cjs.map +1 -1
  16. package/dist/cjs/polyfill/Exception.cjs.map +1 -1
  17. package/dist/cjs/polyfill/RTCCertificate.cjs +5 -2
  18. package/dist/cjs/polyfill/RTCCertificate.cjs.map +1 -1
  19. package/dist/cjs/polyfill/RTCDataChannel.cjs +27 -19
  20. package/dist/cjs/polyfill/RTCDataChannel.cjs.map +1 -1
  21. package/dist/cjs/polyfill/RTCDtlsTransport.cjs +6 -5
  22. package/dist/cjs/polyfill/RTCDtlsTransport.cjs.map +1 -1
  23. package/dist/cjs/polyfill/RTCError.cjs +9 -2
  24. package/dist/cjs/polyfill/RTCError.cjs.map +1 -1
  25. package/dist/cjs/polyfill/RTCIceCandidate.cjs +13 -8
  26. package/dist/cjs/polyfill/RTCIceCandidate.cjs.map +1 -1
  27. package/dist/cjs/polyfill/RTCIceTransport.cjs +26 -16
  28. package/dist/cjs/polyfill/RTCIceTransport.cjs.map +1 -1
  29. package/dist/cjs/polyfill/RTCPeerConnection.cjs +80 -70
  30. package/dist/cjs/polyfill/RTCPeerConnection.cjs.map +1 -1
  31. package/dist/cjs/polyfill/RTCSctpTransport.cjs +9 -11
  32. package/dist/cjs/polyfill/RTCSctpTransport.cjs.map +1 -1
  33. package/dist/cjs/polyfill/RTCSessionDescription.cjs +10 -2
  34. package/dist/cjs/polyfill/RTCSessionDescription.cjs.map +1 -1
  35. package/dist/cjs/polyfill/index.cjs.map +1 -1
  36. package/dist/esm/index.mjs.map +1 -1
  37. package/dist/esm/lib/datachannel-stream.mjs.map +1 -1
  38. package/dist/esm/lib/index.mjs.map +1 -1
  39. package/dist/esm/lib/websocket-server.mjs.map +1 -1
  40. package/dist/esm/lib/websocket.mjs.map +1 -1
  41. package/dist/esm/polyfill/Events.mjs +52 -4
  42. package/dist/esm/polyfill/Events.mjs.map +1 -1
  43. package/dist/esm/polyfill/Exception.mjs.map +1 -1
  44. package/dist/esm/polyfill/RTCCertificate.mjs +5 -2
  45. package/dist/esm/polyfill/RTCCertificate.mjs.map +1 -1
  46. package/dist/esm/polyfill/RTCDataChannel.mjs +27 -19
  47. package/dist/esm/polyfill/RTCDataChannel.mjs.map +1 -1
  48. package/dist/esm/polyfill/RTCDtlsTransport.mjs +6 -5
  49. package/dist/esm/polyfill/RTCDtlsTransport.mjs.map +1 -1
  50. package/dist/esm/polyfill/RTCError.mjs +9 -2
  51. package/dist/esm/polyfill/RTCError.mjs.map +1 -1
  52. package/dist/esm/polyfill/RTCIceCandidate.mjs +13 -8
  53. package/dist/esm/polyfill/RTCIceCandidate.mjs.map +1 -1
  54. package/dist/esm/polyfill/RTCIceTransport.mjs +26 -17
  55. package/dist/esm/polyfill/RTCIceTransport.mjs.map +1 -1
  56. package/dist/esm/polyfill/RTCPeerConnection.mjs +80 -70
  57. package/dist/esm/polyfill/RTCPeerConnection.mjs.map +1 -1
  58. package/dist/esm/polyfill/RTCSctpTransport.mjs +9 -11
  59. package/dist/esm/polyfill/RTCSctpTransport.mjs.map +1 -1
  60. package/dist/esm/polyfill/RTCSessionDescription.mjs +10 -2
  61. package/dist/esm/polyfill/RTCSessionDescription.mjs.map +1 -1
  62. package/dist/esm/polyfill/index.mjs.map +1 -1
  63. package/dist/types/lib/types.d.ts +6 -6
  64. package/dist/types/polyfill/Events.d.ts +4 -1
  65. package/dist/types/polyfill/RTCCertificate.d.ts +1 -0
  66. package/dist/types/polyfill/RTCDataChannel.d.ts +6 -6
  67. package/dist/types/polyfill/RTCDtlsTransport.d.ts +5 -4
  68. package/dist/types/polyfill/RTCError.d.ts +1 -0
  69. package/dist/types/polyfill/RTCIceCandidate.d.ts +1 -1
  70. package/dist/types/polyfill/RTCIceTransport.d.ts +17 -8
  71. package/dist/types/polyfill/RTCPeerConnection.d.ts +22 -11
  72. package/dist/types/polyfill/RTCSctpTransport.d.ts +4 -3
  73. package/dist/types/polyfill/RTCSessionDescription.d.ts +1 -0
  74. package/jest.config.ts +8 -8
  75. package/package.json +4 -2
  76. package/rollup.config.mjs +1 -1
  77. package/src/cpp/data-channel-wrapper.cpp +11 -0
  78. package/src/index.ts +0 -1
  79. package/src/lib/datachannel-stream.ts +83 -84
  80. package/src/lib/index.ts +163 -140
  81. package/src/lib/types.ts +97 -72
  82. package/src/lib/websocket-server.ts +25 -25
  83. package/src/lib/websocket.ts +19 -19
  84. package/src/polyfill/Events.ts +67 -17
  85. package/src/polyfill/Exception.ts +24 -24
  86. package/src/polyfill/README.md +8 -8
  87. package/src/polyfill/RTCCertificate.ts +16 -12
  88. package/src/polyfill/RTCDataChannel.ts +198 -192
  89. package/src/polyfill/RTCDtlsTransport.ts +39 -39
  90. package/src/polyfill/RTCError.ts +63 -56
  91. package/src/polyfill/RTCIceCandidate.ts +120 -116
  92. package/src/polyfill/RTCIceTransport.ts +72 -64
  93. package/src/polyfill/RTCPeerConnection.ts +436 -420
  94. package/src/polyfill/RTCSctpTransport.ts +44 -47
  95. package/src/polyfill/RTCSessionDescription.ts +26 -17
  96. package/src/polyfill/index.ts +22 -22
@@ -11,495 +11,511 @@ import RTCCertificate from './RTCCertificate';
11
11
 
12
12
  // extend RTCConfiguration with peerIdentity
13
13
  interface RTCConfiguration extends globalThis.RTCConfiguration {
14
- peerIdentity?: string;
15
- peerConnection?: PeerConnection;
14
+ peerIdentity?: string;
15
+ peerConnection?: PeerConnection;
16
16
  }
17
17
 
18
18
  export default class RTCPeerConnection extends EventTarget implements globalThis.RTCPeerConnection {
19
- static async generateCertificate(): Promise<RTCCertificate> {
20
- throw new DOMException('Not implemented');
21
- }
19
+ static async generateCertificate(): Promise<RTCCertificate> {
20
+ throw new DOMException('Not implemented');
21
+ }
22
+
23
+ #peerConnection: PeerConnection;
24
+ #localOffer: ReturnType<typeof createDeferredPromise>;
25
+ #localAnswer: ReturnType<typeof createDeferredPromise>;
26
+ #dataChannels: Set<globalThis.RTCDataChannel>;
27
+ #dataChannelsClosed = 0;
28
+ #config: globalThis.RTCConfiguration;
29
+ #canTrickleIceCandidates: boolean | null = null;
30
+ #sctp: globalThis.RTCSctpTransport;
31
+ #announceNegotiation = false;
32
+
33
+ #localCandidates: globalThis.RTCIceCandidate[] = [];
34
+ #remoteCandidates: globalThis.RTCIceCandidate[] = [];
35
+
36
+ // events
37
+ onconnectionstatechange: globalThis.RTCPeerConnection['onconnectionstatechange'] = null;
38
+ // For ondatachannel we need to define type manually
39
+ ondatachannel: ((this: globalThis.RTCPeerConnection, ev: RTCDataChannelEvent) => any) | null;
40
+ onicecandidate: globalThis.RTCPeerConnection['onicecandidate'] = null;
41
+ onicecandidateerror: globalThis.RTCPeerConnection['onicecandidateerror'] = null;
42
+ oniceconnectionstatechange: globalThis.RTCPeerConnection['oniceconnectionstatechange'] = null;
43
+ onicegatheringstatechange: globalThis.RTCPeerConnection['onicegatheringstatechange'] = null;
44
+ onnegotiationneeded: globalThis.RTCPeerConnection['onnegotiationneeded'] = null;
45
+ onsignalingstatechange: globalThis.RTCPeerConnection['onsignalingstatechange'] = null;
46
+ ontrack: globalThis.RTCPeerConnection['ontrack'] = null;
47
+
48
+ private _checkConfiguration(config: globalThis.RTCConfiguration): void {
49
+ if (config && config.iceServers === undefined) config.iceServers = [];
50
+ if (config && config.iceTransportPolicy === undefined) config.iceTransportPolicy = 'all';
51
+
52
+ if (config?.iceServers === null) throw new TypeError('IceServers cannot be null');
53
+
54
+ // Check for all the properties of iceServers
55
+ if (Array.isArray(config?.iceServers)) {
56
+ for (let i = 0; i < config.iceServers.length; i++) {
57
+ if (config.iceServers[i] === null) throw new TypeError('IceServers cannot be null');
58
+ if (config.iceServers[i] === undefined)
59
+ throw new TypeError('IceServers cannot be undefined');
60
+ if (Object.keys(config.iceServers[i]).length === 0)
61
+ throw new TypeError('IceServers cannot be empty');
62
+
63
+ // If iceServers is string convert to array
64
+ if (typeof config.iceServers[i].urls === 'string')
65
+ config.iceServers[i].urls = [config.iceServers[i].urls as string];
66
+
67
+ // urls can not be empty
68
+ if ((config.iceServers[i].urls as string[])?.some((url) => url == ''))
69
+ throw new exceptions.SyntaxError('IceServers urls cannot be empty');
70
+
71
+ // urls should be valid URLs and match the protocols "stun:|turn:|turns:"
72
+ if (
73
+ (config.iceServers[i].urls as string[])?.some((url) => {
74
+ try {
75
+ const parsedURL = new URL(url);
22
76
 
23
- #peerConnection: PeerConnection;
24
- #localOffer: any;
25
- #localAnswer: any;
26
- #dataChannels: Set<globalThis.RTCDataChannel>;
27
- #dataChannelsClosed = 0;
28
- #config: globalThis.RTCConfiguration;
29
- #canTrickleIceCandidates: boolean | null;
30
- #sctp: globalThis.RTCSctpTransport;
31
-
32
- #localCandidates: globalThis.RTCIceCandidate[] = [];
33
- #remoteCandidates: globalThis.RTCIceCandidate[] = [];
34
-
35
- // events
36
- onconnectionstatechange: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null;
37
- ondatachannel: ((this: globalThis.RTCPeerConnection, ev: globalThis.RTCDataChannelEvent) => any) | null;
38
- onicecandidate: ((this: globalThis.RTCPeerConnection, ev: globalThis.RTCPeerConnectionIceEvent) => any) | null;
39
- onicecandidateerror: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null;
40
- oniceconnectionstatechange: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null;
41
- onicegatheringstatechange: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null;
42
- onnegotiationneeded: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null;
43
- onsignalingstatechange: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null;
44
- ontrack: ((this: globalThis.RTCPeerConnection, ev: globalThis.RTCTrackEvent) => any) | null;
45
-
46
- private _checkConfiguration(config: globalThis.RTCConfiguration): void {
47
- if (config && config.iceServers === undefined) config.iceServers = [];
48
- if (config && config.iceTransportPolicy === undefined) config.iceTransportPolicy = 'all';
49
-
50
- if (config?.iceServers === null) throw new TypeError('IceServers cannot be null');
51
-
52
- // Check for all the properties of iceServers
53
- if (Array.isArray(config?.iceServers)) {
54
- for (let i = 0; i < config.iceServers.length; i++) {
55
- if (config.iceServers[i] === null) throw new TypeError('IceServers cannot be null');
56
- if (config.iceServers[i] === undefined) throw new TypeError('IceServers cannot be undefined');
57
- if (Object.keys(config.iceServers[i]).length === 0) throw new TypeError('IceServers cannot be empty');
58
-
59
- // If iceServers is string convert to array
60
- if (typeof config.iceServers[i].urls === 'string')
61
- config.iceServers[i].urls = [config.iceServers[i].urls as string];
62
-
63
- // urls can not be empty
64
- if ((config.iceServers[i].urls as string[])?.some((url) => url == ''))
65
- throw new exceptions.SyntaxError('IceServers urls cannot be empty');
66
-
67
- // urls should be valid URLs and match the protocols "stun:|turn:|turns:"
68
- if (
69
- (config.iceServers[i].urls as string[])?.some(
70
- (url) => {
71
- try {
72
- const parsedURL = new URL(url)
73
-
74
- return !/^(stun:|turn:|turns:)$/.test(parsedURL.protocol)
75
- } catch (error) {
76
- return true
77
- }
78
- },
79
- )
80
- )
81
- throw new exceptions.SyntaxError('IceServers urls wrong format');
82
-
83
- // If this is a turn server check for username and credential
84
- if ((config.iceServers[i].urls as string[])?.some((url) => url.startsWith('turn'))) {
85
- if (!config.iceServers[i].username)
86
- throw new exceptions.InvalidAccessError('IceServers username cannot be null');
87
- if (!config.iceServers[i].credential)
88
- throw new exceptions.InvalidAccessError('IceServers username cannot be undefined');
89
- }
90
-
91
- // length of urls can not be 0
92
- if (config.iceServers[i].urls?.length === 0)
93
- throw new exceptions.SyntaxError('IceServers urls cannot be empty');
77
+ return !/^(stun:|turn:|turns:)$/.test(parsedURL.protocol);
78
+ } catch (error) {
79
+ return true;
94
80
  }
95
- }
96
-
97
- if (
98
- config &&
99
- config.iceTransportPolicy &&
100
- config.iceTransportPolicy !== 'all' &&
101
- config.iceTransportPolicy !== 'relay'
81
+ })
102
82
  )
103
- throw new TypeError('IceTransportPolicy must be either "all" or "relay"');
104
- }
105
-
106
- setConfiguration(config: globalThis.RTCConfiguration): void {
107
- this._checkConfiguration(config);
108
- this.#config = config;
109
- }
110
-
111
-
112
-
113
- constructor(config: RTCConfiguration = { iceServers: [], iceTransportPolicy: 'all' }) {
114
- super();
115
-
116
- this._checkConfiguration(config);
117
- this.#config = config;
118
- this.#localOffer = createDeferredPromise();
119
- this.#localAnswer = createDeferredPromise();
120
- this.#dataChannels = new Set();
121
- this.#canTrickleIceCandidates = null;
122
-
123
- try {
124
- const peerIdentity = (config as any)?.peerIdentity ?? `peer-${getRandomString(7)}`;
125
- this.#peerConnection = config?.peerConnection ?? new PeerConnection(peerIdentity,
126
- {
127
- ...config,
128
- iceServers:
129
- config?.iceServers
130
- ?.map((server) => {
131
- const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
132
-
133
- return urls.map((url) => {
134
- if (server.username && server.credential) {
135
- const [protocol, rest] = url.split(/:(.*)/);
136
- return `${protocol}:${server.username}:${server.credential}@${rest}`;
137
- }
138
- return url;
139
- });
140
- })
141
- .flat() ?? [],
142
- },
143
- );
144
- } catch (error) {
145
- if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error');
146
- throw new exceptions.SyntaxError(error.message);
83
+ throw new exceptions.SyntaxError('IceServers urls wrong format');
84
+
85
+ // If this is a turn server check for username and credential
86
+ if ((config.iceServers[i].urls as string[])?.some((url) => url.startsWith('turn'))) {
87
+ if (!config.iceServers[i].username)
88
+ throw new exceptions.InvalidAccessError('IceServers username cannot be null');
89
+ if (!config.iceServers[i].credential)
90
+ throw new exceptions.InvalidAccessError('IceServers username cannot be undefined');
147
91
  }
148
92
 
149
- // forward peerConnection events
150
- this.#peerConnection.onStateChange(() => {
151
- this.dispatchEvent(new Event('connectionstatechange'));
152
- });
153
-
154
- this.#peerConnection.onIceStateChange(() => {
155
- this.dispatchEvent(new Event('iceconnectionstatechange'));
156
- });
157
-
158
- this.#peerConnection.onSignalingStateChange(() => {
159
- this.dispatchEvent(new Event('signalingstatechange'));
93
+ // length of urls can not be 0
94
+ if (config.iceServers[i].urls?.length === 0)
95
+ throw new exceptions.SyntaxError('IceServers urls cannot be empty');
96
+ }
97
+ }
98
+
99
+ if (
100
+ config &&
101
+ config.iceTransportPolicy &&
102
+ config.iceTransportPolicy !== 'all' &&
103
+ config.iceTransportPolicy !== 'relay'
104
+ )
105
+ throw new TypeError('IceTransportPolicy must be either "all" or "relay"');
106
+ }
107
+
108
+ setConfiguration(config: globalThis.RTCConfiguration): void {
109
+ this._checkConfiguration(config);
110
+ this.#config = config;
111
+ }
112
+
113
+ constructor(config: RTCConfiguration = { iceServers: [], iceTransportPolicy: 'all' }) {
114
+ super();
115
+
116
+ this._checkConfiguration(config);
117
+ this.#config = config;
118
+ this.#localOffer = createDeferredPromise();
119
+ this.#localAnswer = createDeferredPromise();
120
+ this.#dataChannels = new Set();
121
+ this.#canTrickleIceCandidates = null;
122
+
123
+ try {
124
+ const peerIdentity = (config as any)?.peerIdentity ?? `peer-${getRandomString(7)}`;
125
+ this.#peerConnection =
126
+ config?.peerConnection ??
127
+ new PeerConnection(peerIdentity, {
128
+ ...config,
129
+ iceServers:
130
+ config?.iceServers
131
+ ?.map((server) => {
132
+ const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
133
+
134
+ return urls.map((url) => {
135
+ if (server.username && server.credential) {
136
+ const [protocol, rest] = url.split(/:(.*)/);
137
+ return `${protocol}:${server.username}:${server.credential}@${rest}`;
138
+ }
139
+ return url;
140
+ });
141
+ })
142
+ .flat() ?? [],
160
143
  });
144
+ } catch (error) {
145
+ if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error');
146
+ throw new exceptions.SyntaxError(error.message);
147
+ }
161
148
 
162
- this.#peerConnection.onGatheringStateChange(() => {
163
- this.dispatchEvent(new Event('icegatheringstatechange'));
164
- });
149
+ // forward peerConnection events
150
+ this.#peerConnection.onStateChange(() => {
151
+ this.dispatchEvent(new Event('connectionstatechange'));
152
+ });
165
153
 
166
- this.#peerConnection.onDataChannel((channel) => {
167
- const dc = new RTCDataChannel(channel);
168
- this.#dataChannels.add(dc);
169
- this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel: dc }));
170
- });
154
+ this.#peerConnection.onIceStateChange(() => {
155
+ this.dispatchEvent(new Event('iceconnectionstatechange'));
156
+ });
171
157
 
172
- this.#peerConnection.onLocalDescription((sdp, type) => {
173
- if (type === 'offer') {
174
- this.#localOffer.resolve({ sdp, type });
175
- }
158
+ this.#peerConnection.onSignalingStateChange(() => {
159
+ this.dispatchEvent(new Event('signalingstatechange'));
160
+ });
176
161
 
177
- if (type === 'answer') {
178
- this.#localAnswer.resolve({ sdp, type });
179
- }
180
- });
162
+ this.#peerConnection.onGatheringStateChange(() => {
163
+ this.dispatchEvent(new Event('icegatheringstatechange'));
164
+ });
181
165
 
182
- this.#peerConnection.onLocalCandidate((candidate, sdpMid) => {
183
- if (sdpMid === 'unspec') {
184
- this.#localAnswer.reject(new Error(`Invalid description type ${sdpMid}`));
185
- return;
186
- }
166
+ this.#peerConnection.onDataChannel((channel) => {
167
+ const dc = new RTCDataChannel(channel);
168
+ this.#dataChannels.add(dc);
169
+ this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel: dc }));
170
+ });
187
171
 
188
- this.#localCandidates.push(new RTCIceCandidate({ candidate, sdpMid }));
189
- this.dispatchEvent(new RTCPeerConnectionIceEvent(new RTCIceCandidate({ candidate, sdpMid })));
190
- });
172
+ this.#peerConnection.onLocalDescription((sdp, type) => {
173
+ if (type === 'offer') {
174
+ this.#localOffer.resolve(new RTCSessionDescription({ sdp, type }));
175
+ }
191
176
 
192
- // forward events to properties
193
- this.addEventListener('connectionstatechange', (e) => {
194
- if (this.onconnectionstatechange) this.onconnectionstatechange(e);
195
- });
196
- this.addEventListener('signalingstatechange', (e) => {
197
- if (this.onsignalingstatechange) this.onsignalingstatechange(e);
198
- });
199
- this.addEventListener('iceconnectionstatechange', (e) => {
200
- if (this.oniceconnectionstatechange) this.oniceconnectionstatechange(e);
201
- });
202
- this.addEventListener('icegatheringstatechange', (e) => {
203
- if (this.onicegatheringstatechange) this.onicegatheringstatechange(e);
204
- });
205
- this.addEventListener('datachannel', (e) => {
206
- if (this.ondatachannel) this.ondatachannel(e as globalThis.RTCDataChannelEvent);
207
- });
208
- this.addEventListener('icecandidate', (e) => {
209
- if (this.onicecandidate) this.onicecandidate(e as globalThis.RTCPeerConnectionIceEvent);
210
- });
177
+ if (type === 'answer') {
178
+ this.#localAnswer.resolve(new RTCSessionDescription({ sdp, type }));
179
+ }
180
+ });
211
181
 
212
- this.#sctp = new RTCSctpTransport({
213
- pc: this,
214
- extraFunctions: {
215
- maxDataChannelId: (): number => {
216
- return this.#peerConnection.maxDataChannelId();
217
- },
218
- maxMessageSize: (): number => {
219
- return this.#peerConnection.maxMessageSize();
220
- },
221
- localCandidates: (): globalThis.RTCIceCandidate[] => {
222
- return this.#localCandidates;
223
- },
224
- remoteCandidates: (): globalThis.RTCIceCandidate[] => {
225
- return this.#remoteCandidates;
226
- },
227
- selectedCandidatePair: (): { local: SelectedCandidateInfo; remote: SelectedCandidateInfo } | null => {
228
- return this.#peerConnection.getSelectedCandidatePair();
229
- },
230
- },
231
- });
232
- }
182
+ this.#peerConnection.onLocalCandidate((candidate, sdpMid) => {
183
+ if (sdpMid === 'unspec') {
184
+ this.#localAnswer.reject(new Error(`Invalid description type ${sdpMid}`));
185
+ return;
186
+ }
233
187
 
234
- get canTrickleIceCandidates(): boolean | null {
235
- return this.#canTrickleIceCandidates;
236
- }
188
+ this.#localCandidates.push(new RTCIceCandidate({ candidate, sdpMid }));
189
+ this.dispatchEvent(new RTCPeerConnectionIceEvent(new RTCIceCandidate({ candidate, sdpMid })));
190
+ });
237
191
 
238
- get connectionState(): globalThis.RTCPeerConnectionState {
239
- return this.#peerConnection.state();
240
- }
192
+ // forward events to properties
193
+ this.addEventListener('connectionstatechange', (e) => {
194
+ this.onconnectionstatechange?.(e);
195
+ });
196
+ this.addEventListener('signalingstatechange', (e) => {
197
+ this.onsignalingstatechange?.(e);
198
+ });
199
+ this.addEventListener('iceconnectionstatechange', (e) => {
200
+ this.oniceconnectionstatechange?.(e);
201
+ });
202
+ this.addEventListener('icegatheringstatechange', (e) => {
203
+ this.onicegatheringstatechange?.(e);
204
+ });
205
+ this.addEventListener('datachannel', (e) => {
206
+ this.ondatachannel?.(e as RTCDataChannelEvent);
207
+ });
208
+ this.addEventListener('icecandidate', (e) => {
209
+ this.onicecandidate?.(e as globalThis.RTCPeerConnectionIceEvent);
210
+ });
211
+ this.addEventListener('track', (e) => {
212
+ this.ontrack?.(e as RTCTrackEvent);
213
+ });
214
+ this.addEventListener('negotiationneeded', (e) => {
215
+ this.#announceNegotiation = true;
216
+ this.onnegotiationneeded?.(e);
217
+ });
241
218
 
242
- get iceConnectionState(): globalThis.RTCIceConnectionState {
243
- let state = this.#peerConnection.iceState();
244
- // libdatachannel uses 'completed' instead of 'connected'
245
- // see /webrtc/getstats.html
246
- if (state == 'completed') state = 'connected';
247
- return state;
248
- }
219
+ this.#sctp = new RTCSctpTransport({
220
+ pc: this,
221
+ });
222
+ }
249
223
 
250
- get iceGatheringState(): globalThis.RTCIceGatheringState {
251
- return this.#peerConnection.gatheringState();
252
- }
224
+ // Extra FUnctions
225
+ get ext_maxDataChannelId(): number {
226
+ return this.#peerConnection.maxDataChannelId();
227
+ }
253
228
 
254
- get currentLocalDescription(): globalThis.RTCSessionDescription {
255
- return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
256
- }
229
+ get ext_maxMessageSize(): number {
230
+ return this.#peerConnection.maxMessageSize();
231
+ }
257
232
 
258
- get currentRemoteDescription(): globalThis.RTCSessionDescription {
259
- return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
260
- }
233
+ get ext_localCandidates(): globalThis.RTCIceCandidate[] {
234
+ return this.#localCandidates;
235
+ }
261
236
 
262
- get localDescription(): globalThis.RTCSessionDescription {
263
- return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
264
- }
237
+ get ext_remoteCandidates(): globalThis.RTCIceCandidate[] {
238
+ return this.#remoteCandidates;
239
+ }
265
240
 
266
- get pendingLocalDescription(): globalThis.RTCSessionDescription {
267
- return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
268
- }
241
+ selectedCandidatePair(): {
242
+ local: SelectedCandidateInfo;
243
+ remote: SelectedCandidateInfo;
244
+ } | null {
245
+ return this.#peerConnection.getSelectedCandidatePair();
246
+ }
269
247
 
270
- get pendingRemoteDescription():globalThis.RTCSessionDescription {
271
- return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
272
- }
248
+ get canTrickleIceCandidates(): boolean | null {
249
+ return this.#canTrickleIceCandidates;
250
+ }
273
251
 
274
- get remoteDescription(): globalThis.RTCSessionDescription {
275
- return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
276
- }
252
+ get connectionState(): globalThis.RTCPeerConnectionState {
253
+ return this.#peerConnection.state();
254
+ }
277
255
 
278
- get sctp(): globalThis.RTCSctpTransport {
279
- return this.#sctp;
280
- }
256
+ get iceConnectionState(): globalThis.RTCIceConnectionState {
257
+ let state = this.#peerConnection.iceState();
258
+ // libdatachannel uses 'completed' instead of 'connected'
259
+ // see /webrtc/getstats.html
260
+ if (state == 'completed') state = 'connected';
261
+ return state;
262
+ }
281
263
 
282
- get signalingState(): globalThis.RTCSignalingState {
283
- return this.#peerConnection.signalingState();
284
- }
264
+ get iceGatheringState(): globalThis.RTCIceGatheringState {
265
+ return this.#peerConnection.gatheringState();
266
+ }
285
267
 
286
- async addIceCandidate(candidate?: globalThis.RTCIceCandidateInit | null): Promise<void> {
287
- if (!candidate || !candidate.candidate) {
288
- return;
289
- }
268
+ get currentLocalDescription(): globalThis.RTCSessionDescription {
269
+ return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
270
+ }
290
271
 
291
- if (candidate.sdpMid === null && candidate.sdpMLineIndex === null) {
292
- throw new TypeError('sdpMid must be set');
293
- }
272
+ get currentRemoteDescription(): globalThis.RTCSessionDescription {
273
+ return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
274
+ }
294
275
 
295
- if (candidate.sdpMid === undefined && candidate.sdpMLineIndex == undefined) {
296
- throw new TypeError('sdpMid must be set');
297
- }
276
+ get localDescription(): globalThis.RTCSessionDescription {
277
+ return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
278
+ }
298
279
 
299
- // Reject if sdpMid format is not valid
300
- // ??
301
- if (candidate.sdpMid && candidate.sdpMid.length > 3) {
302
- // console.log(candidate.sdpMid);
303
- throw new exceptions.OperationError('Invalid sdpMid format');
304
- }
280
+ get pendingLocalDescription(): globalThis.RTCSessionDescription {
281
+ return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
282
+ }
305
283
 
306
- // We don't care about sdpMLineIndex, just for test
307
- if (!candidate.sdpMid && candidate.sdpMLineIndex > 1) {
308
- throw new exceptions.OperationError('This is only for test case.');
309
- }
284
+ get pendingRemoteDescription(): globalThis.RTCSessionDescription {
285
+ return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
286
+ }
310
287
 
311
- try {
312
- this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid || '0');
313
- this.#remoteCandidates.push(
314
- new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid || '0' }),
315
- );
316
- } catch (error) {
317
- if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error');
288
+ get remoteDescription(): globalThis.RTCSessionDescription {
289
+ return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
290
+ }
318
291
 
319
- // Check error Message if contains specific message
320
- if (error.message.includes('remote candidate without remote description'))
321
- throw new exceptions.InvalidStateError(error.message);
322
- if (error.message.includes('Invalid candidate format')) throw new exceptions.OperationError(error.message);
292
+ get sctp(): globalThis.RTCSctpTransport {
293
+ return this.#sctp;
294
+ }
323
295
 
324
- throw new exceptions.NotFoundError(error.message);
325
- }
326
- }
296
+ get signalingState(): globalThis.RTCSignalingState {
297
+ return this.#peerConnection.signalingState();
298
+ }
327
299
 
328
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
329
- addTrack(_track, ..._streams): globalThis.RTCRtpSender {
330
- throw new DOMException('Not implemented');
300
+ async addIceCandidate(candidate?: globalThis.RTCIceCandidateInit | null): Promise<void> {
301
+ if (!candidate || !candidate.candidate) {
302
+ return;
331
303
  }
332
304
 
333
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
334
- addTransceiver(_trackOrKind, _init): globalThis.RTCRtpTransceiver {
335
- throw new DOMException('Not implemented');
305
+ if (candidate.sdpMid === null && candidate.sdpMLineIndex === null) {
306
+ throw new TypeError('sdpMid must be set');
336
307
  }
337
308
 
338
- close(): void {
339
- // close all channels before shutting down
340
- this.#dataChannels.forEach((channel) => {
341
- channel.close();
342
- this.#dataChannelsClosed++;
343
- });
344
-
345
- this.#peerConnection.close();
309
+ if (candidate.sdpMid === undefined && candidate.sdpMLineIndex == undefined) {
310
+ throw new TypeError('sdpMid must be set');
346
311
  }
347
312
 
348
- createAnswer(): Promise<globalThis.RTCSessionDescriptionInit | any> {
349
- return this.#localAnswer;
313
+ // Reject if sdpMid format is not valid
314
+ // ??
315
+ if (candidate.sdpMid && candidate.sdpMid.length > 3) {
316
+ // console.log(candidate.sdpMid);
317
+ throw new exceptions.OperationError('Invalid sdpMid format');
350
318
  }
351
319
 
352
-
353
- createDataChannel(label, opts = {}): globalThis.RTCDataChannel {
354
- const channel = this.#peerConnection.createDataChannel(label, opts);
355
- const dataChannel = new RTCDataChannel(channel, opts);
356
-
357
- // ensure we can close all channels when shutting down
358
- this.#dataChannels.add(dataChannel);
359
- dataChannel.addEventListener('close', () => {
360
- this.#dataChannels.delete(dataChannel);
361
- this.#dataChannelsClosed++;
362
- });
363
-
364
- return dataChannel;
320
+ // We don't care about sdpMLineIndex, just for test
321
+ if (!candidate.sdpMid && candidate.sdpMLineIndex > 1) {
322
+ throw new exceptions.OperationError('This is only for test case.');
365
323
  }
366
324
 
367
- createOffer(): Promise<globalThis.RTCSessionDescriptionInit | any> {
368
- return this.#localOffer;
325
+ try {
326
+ this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid ?? '0');
327
+ this.#remoteCandidates.push(
328
+ new RTCIceCandidate({
329
+ candidate: candidate.candidate,
330
+ sdpMid: candidate.sdpMid ?? '0',
331
+ }),
332
+ );
333
+ } catch (error) {
334
+ if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error');
335
+
336
+ // Check error Message if contains specific message
337
+ if (error.message.includes('remote candidate without remote description'))
338
+ throw new exceptions.InvalidStateError(error.message);
339
+ if (error.message.includes('Invalid candidate format'))
340
+ throw new exceptions.OperationError(error.message);
341
+
342
+ throw new exceptions.NotFoundError(error.message);
369
343
  }
344
+ }
370
345
 
371
- getConfiguration(): globalThis.RTCConfiguration {
372
- return this.#config;
373
- }
346
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
347
+ addTrack(_track, ..._streams): globalThis.RTCRtpSender {
348
+ throw new DOMException('Not implemented');
349
+ }
374
350
 
375
- getReceivers(): globalThis.RTCRtpReceiver[] {
376
- throw new DOMException('Not implemented');
377
- }
351
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
352
+ addTransceiver(_trackOrKind, _init): globalThis.RTCRtpTransceiver {
353
+ throw new DOMException('Not implemented');
354
+ }
378
355
 
379
- getSenders(): globalThis.RTCRtpSender[] {
380
- throw new DOMException('Not implemented');
381
- }
356
+ close(): void {
357
+ this.#peerConnection.close();
358
+ }
382
359
 
383
- getStats(): Promise<globalThis.RTCStatsReport> {
384
- return new Promise((resolve) => {
385
- const report = new Map();
386
- const cp = this.#peerConnection?.getSelectedCandidatePair();
387
- const bytesSent = this.#peerConnection?.bytesSent();
388
- const bytesReceived = this.#peerConnection?.bytesReceived();
389
- const rtt = this.#peerConnection?.rtt();
360
+ createAnswer(): Promise<globalThis.RTCSessionDescriptionInit | any> {
361
+ return this.#localAnswer;
362
+ }
390
363
 
391
- if(!cp) {
392
- return resolve(report);
393
- }
364
+ createDataChannel(label: string, opts: globalThis.RTCDataChannelInit = {}): RTCDataChannel {
365
+ const channel = this.#peerConnection.createDataChannel(label, opts);
366
+ const dataChannel = new RTCDataChannel(channel, opts);
394
367
 
395
- const localIdRs = getRandomString(8);
396
- const localId = 'RTCIceCandidate_' + localIdRs;
397
- report.set(localId, {
398
- id: localId,
399
- type: 'local-candidate',
400
- timestamp: Date.now(),
401
- candidateType: cp.local.type,
402
- ip: cp.local.address,
403
- port: cp.local.port,
404
- });
405
-
406
- const remoteIdRs = getRandomString(8);
407
- const remoteId = 'RTCIceCandidate_' + remoteIdRs;
408
- report.set(remoteId, {
409
- id: remoteId,
410
- type: 'remote-candidate',
411
- timestamp: Date.now(),
412
- candidateType: cp.remote.type,
413
- ip: cp.remote.address,
414
- port: cp.remote.port,
415
- });
416
-
417
- const candidateId = 'RTCIceCandidatePair_' + localIdRs + '_' + remoteIdRs;
418
- report.set(candidateId, {
419
- id: candidateId,
420
- type: 'candidate-pair',
421
- timestamp: Date.now(),
422
- localCandidateId: localId,
423
- remoteCandidateId: remoteId,
424
- state: 'succeeded',
425
- nominated: true,
426
- writable: true,
427
- bytesSent: bytesSent,
428
- bytesReceived: bytesReceived,
429
- totalRoundTripTime: rtt,
430
- currentRoundTripTime: rtt,
431
- });
432
-
433
- const transportId = 'RTCTransport_0_1';
434
- report.set(transportId, {
435
- id: transportId,
436
- timestamp: Date.now(),
437
- type: 'transport',
438
- bytesSent: bytesSent,
439
- bytesReceived: bytesReceived,
440
- dtlsState: 'connected',
441
- selectedCandidatePairId: candidateId,
442
- selectedCandidatePairChanges: 1,
443
- });
444
-
445
- // peer-connection'
446
- report.set('P', {
447
- id: 'P',
448
- type: 'peer-connection',
449
- timestamp: Date.now(),
450
- dataChannelsOpened: this.#dataChannels.size,
451
- dataChannelsClosed: this.#dataChannelsClosed,
452
- });
453
-
454
- return resolve(report);
455
- });
456
- }
368
+ // ensure we can close all channels when shutting down
369
+ this.#dataChannels.add(dataChannel);
370
+ dataChannel.addEventListener('close', () => {
371
+ this.#dataChannels.delete(dataChannel);
372
+ this.#dataChannelsClosed++;
373
+ });
457
374
 
458
- getTransceivers(): globalThis.RTCRtpTransceiver[] {
459
- return []; // throw new DOMException('Not implemented');
460
- }
375
+ if (this.#announceNegotiation == null) {
376
+ this.#announceNegotiation = false;
377
+ this.dispatchEvent(new Event('negotiationneeded'));
378
+ }
379
+
380
+ return dataChannel;
381
+ }
382
+
383
+ createOffer(): Promise<globalThis.RTCSessionDescriptionInit | any> {
384
+ return this.#localOffer;
385
+ }
386
+
387
+ getConfiguration(): globalThis.RTCConfiguration {
388
+ return this.#config;
389
+ }
390
+
391
+ getReceivers(): globalThis.RTCRtpReceiver[] {
392
+ throw new DOMException('Not implemented');
393
+ }
394
+
395
+ getSenders(): globalThis.RTCRtpSender[] {
396
+ throw new DOMException('Not implemented');
397
+ }
398
+
399
+ getStats(): Promise<globalThis.RTCStatsReport> | any {
400
+ return new Promise((resolve) => {
401
+ const report = new Map();
402
+ const cp = this.#peerConnection?.getSelectedCandidatePair();
403
+ const bytesSent = this.#peerConnection?.bytesSent();
404
+ const bytesReceived = this.#peerConnection?.bytesReceived();
405
+ const rtt = this.#peerConnection?.rtt();
406
+
407
+ if (!cp) {
408
+ return resolve(report);
409
+ }
410
+
411
+ const localIdRs = getRandomString(8);
412
+ const localId = 'RTCIceCandidate_' + localIdRs;
413
+ report.set(localId, {
414
+ id: localId,
415
+ type: 'local-candidate',
416
+ timestamp: Date.now(),
417
+ candidateType: cp.local.type,
418
+ ip: cp.local.address,
419
+ port: cp.local.port,
420
+ });
421
+
422
+ const remoteIdRs = getRandomString(8);
423
+ const remoteId = 'RTCIceCandidate_' + remoteIdRs;
424
+ report.set(remoteId, {
425
+ id: remoteId,
426
+ type: 'remote-candidate',
427
+ timestamp: Date.now(),
428
+ candidateType: cp.remote.type,
429
+ ip: cp.remote.address,
430
+ port: cp.remote.port,
431
+ });
432
+
433
+ const candidateId = 'RTCIceCandidatePair_' + localIdRs + '_' + remoteIdRs;
434
+ report.set(candidateId, {
435
+ id: candidateId,
436
+ type: 'candidate-pair',
437
+ timestamp: Date.now(),
438
+ localCandidateId: localId,
439
+ remoteCandidateId: remoteId,
440
+ state: 'succeeded',
441
+ nominated: true,
442
+ writable: true,
443
+ bytesSent: bytesSent,
444
+ bytesReceived: bytesReceived,
445
+ totalRoundTripTime: rtt,
446
+ currentRoundTripTime: rtt,
447
+ });
448
+
449
+ const transportId = 'RTCTransport_0_1';
450
+ report.set(transportId, {
451
+ id: transportId,
452
+ timestamp: Date.now(),
453
+ type: 'transport',
454
+ bytesSent: bytesSent,
455
+ bytesReceived: bytesReceived,
456
+ dtlsState: 'connected',
457
+ selectedCandidatePairId: candidateId,
458
+ selectedCandidatePairChanges: 1,
459
+ });
460
+
461
+ // peer-connection'
462
+ report.set('P', {
463
+ id: 'P',
464
+ type: 'peer-connection',
465
+ timestamp: Date.now(),
466
+ dataChannelsOpened: this.#dataChannels.size,
467
+ dataChannelsClosed: this.#dataChannelsClosed,
468
+ });
469
+
470
+ return resolve(report);
471
+ });
472
+ }
461
473
 
462
- removeTrack(): void {
463
- throw new DOMException('Not implemented');
464
- }
474
+ getTransceivers(): globalThis.RTCRtpTransceiver[] {
475
+ return []; // throw new DOMException('Not implemented');
476
+ }
465
477
 
466
- restartIce(): Promise<void> {
467
- throw new DOMException('Not implemented');
468
- }
478
+ removeTrack(): void {
479
+ throw new DOMException('Not implemented');
480
+ }
469
481
 
470
- async setLocalDescription(description: globalThis.RTCSessionDescriptionInit): Promise<void> {
471
- if (description?.type !== 'offer') {
472
- // any other type causes libdatachannel to throw
473
- return;
474
- }
482
+ restartIce(): Promise<void> {
483
+ throw new DOMException('Not implemented');
484
+ }
475
485
 
476
- this.#peerConnection.setLocalDescription(description?.type as any);
486
+ async setLocalDescription(description: globalThis.RTCSessionDescriptionInit): Promise<void> {
487
+ if (description?.type !== 'offer') {
488
+ // any other type causes libdatachannel to throw
489
+ return;
477
490
  }
478
491
 
479
- async setRemoteDescription(description: globalThis.RTCSessionDescriptionInit): Promise<void> {
480
- if (description.sdp == null) {
481
- throw new DOMException('Remote SDP must be set');
482
- }
492
+ this.#peerConnection.setLocalDescription(description?.type as any);
493
+ }
483
494
 
484
- this.#peerConnection.setRemoteDescription(description.sdp, description.type as any);
495
+ async setRemoteDescription(description: globalThis.RTCSessionDescriptionInit): Promise<void> {
496
+ if (description.sdp == null) {
497
+ throw new DOMException('Remote SDP must be set');
485
498
  }
499
+
500
+ this.#peerConnection.setRemoteDescription(description.sdp, description.type as any);
501
+ }
486
502
  }
487
503
 
488
504
  function createDeferredPromise(): any {
489
- let resolve: any, reject: any;
505
+ let resolve: any, reject: any;
490
506
 
491
- const promise = new Promise(function (_resolve, _reject) {
492
- resolve = _resolve;
493
- reject = _reject;
494
- });
507
+ const promise = new Promise(function (_resolve, _reject) {
508
+ resolve = _resolve;
509
+ reject = _reject;
510
+ });
495
511
 
496
- (promise as any).resolve = resolve;
497
- (promise as any).reject = reject;
498
- return promise;
512
+ (promise as any).resolve = resolve;
513
+ (promise as any).reject = reject;
514
+ return promise;
499
515
  }
500
516
 
501
- function getRandomString(length): string {
502
- return Math.random()
503
- .toString(36)
504
- .substring(2, 2 + length);
517
+ function getRandomString(length: number): string {
518
+ return Math.random()
519
+ .toString(36)
520
+ .substring(2, 2 + length);
505
521
  }