node-rtc-connection 1.0.19 → 2.0.4

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 (64) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5606
  3. package/dist/index.mjs +25 -5598
  4. package/dist/types/crypto/der.d.ts +107 -0
  5. package/dist/types/crypto/x509.d.ts +56 -0
  6. package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
  7. package/dist/types/dtls/RTCCertificate.d.ts +163 -0
  8. package/dist/types/dtls/cipher.d.ts +81 -0
  9. package/dist/types/dtls/connection.d.ts +81 -0
  10. package/dist/types/dtls/prf.d.ts +29 -0
  11. package/dist/types/dtls/protocol.d.ts +127 -0
  12. package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
  13. package/dist/types/foundation/RTCError.d.ts +152 -0
  14. package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
  15. package/dist/types/ice/ice-agent.d.ts +154 -0
  16. package/dist/types/ice/stun-message.d.ts +92 -0
  17. package/dist/types/index.d.ts +29 -0
  18. package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  19. package/dist/types/sctp/association.d.ts +77 -0
  20. package/dist/types/sctp/chunks.d.ts +200 -0
  21. package/dist/types/sctp/crc32c.d.ts +24 -0
  22. package/dist/types/sctp/datachannel-manager.d.ts +51 -0
  23. package/dist/types/sctp/dcep.d.ts +56 -0
  24. package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
  25. package/dist/types/sdp/sdp-utils.d.ts +103 -0
  26. package/dist/types/stun/stun-client.d.ts +119 -0
  27. package/dist/types/transport-stack.d.ts +68 -0
  28. package/package.json +26 -21
  29. package/src/crypto/der.ts +205 -0
  30. package/src/crypto/x509.ts +146 -0
  31. package/src/datachannel/RTCDataChannel.ts +388 -0
  32. package/src/dtls/RTCCertificate.ts +396 -0
  33. package/src/dtls/cipher.ts +198 -0
  34. package/src/dtls/connection.ts +974 -0
  35. package/src/dtls/prf.ts +62 -0
  36. package/src/dtls/protocol.ts +204 -0
  37. package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
  38. package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
  39. package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
  40. package/src/ice/ice-agent.ts +609 -0
  41. package/src/ice/stun-message.ts +260 -0
  42. package/src/index.ts +72 -0
  43. package/src/peerconnection/RTCPeerConnection.ts +430 -0
  44. package/src/sctp/association.ts +523 -0
  45. package/src/sctp/chunks.ts +350 -0
  46. package/src/sctp/crc32c.ts +57 -0
  47. package/src/sctp/datachannel-manager.ts +187 -0
  48. package/src/sctp/dcep.ts +94 -0
  49. package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
  50. package/src/sdp/sdp-utils.ts +229 -0
  51. package/src/stun/{stun-client.js → stun-client.ts} +346 -187
  52. package/src/transport-stack.ts +165 -0
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.mjs.map +0 -1
  55. package/src/datachannel/RTCDataChannel.js +0 -354
  56. package/src/dtls/RTCCertificate.js +0 -310
  57. package/src/dtls/RTCDtlsTransport.js +0 -247
  58. package/src/ice/RTCIceTransport.js +0 -1018
  59. package/src/index.d.ts +0 -400
  60. package/src/index.js +0 -92
  61. package/src/network/network-transport.js +0 -478
  62. package/src/peerconnection/RTCPeerConnection.js +0 -875
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. package/src/sdp/sdp-utils.js +0 -224
@@ -1,1018 +0,0 @@
1
- /**
2
- * @file RTCIceTransport.js
3
- * @description ICE transport implementation for establishing connectivity.
4
- * @module ice/RTCIceTransport
5
- *
6
- * Ported from Chromium's RTCIceTransport implementation:
7
- * - cc/rtc_ice_transport.h
8
- * - cc/rtc_ice_transport.cc
9
- * - cc/rtc_ice_transport.idl
10
- */
11
-
12
- const EventEmitter = require('events');
13
- const STUNClient = require('../stun/stun-client');
14
- const dgram = require('dgram');
15
- const crypto = require('crypto');
16
-
17
- /**
18
- * RTCIceRole - The role in the ICE process
19
- * @readonly
20
- * @enum {string}
21
- */
22
- const RTCIceRole = Object.freeze({
23
- CONTROLLING: 'controlling',
24
- CONTROLLED: 'controlled'
25
- });
26
-
27
- /**
28
- * RTCIceTransportState - Current state of the ICE transport
29
- * @readonly
30
- * @enum {string}
31
- */
32
- const RTCIceTransportState = Object.freeze({
33
- NEW: 'new',
34
- CHECKING: 'checking',
35
- CONNECTED: 'connected',
36
- COMPLETED: 'completed',
37
- DISCONNECTED: 'disconnected',
38
- FAILED: 'failed',
39
- CLOSED: 'closed'
40
- });
41
-
42
- /**
43
- * RTCIceGatheringState - ICE candidate gathering state
44
- * @readonly
45
- * @enum {string}
46
- */
47
- const RTCIceGatheringState = Object.freeze({
48
- NEW: 'new',
49
- GATHERING: 'gathering',
50
- COMPLETE: 'complete'
51
- });
52
-
53
- /**
54
- * RTCIceParameters - ICE username fragment and password
55
- * @typedef {Object} RTCIceParameters
56
- * @property {string} usernameFragment - ICE username fragment
57
- * @property {string} password - ICE password
58
- */
59
-
60
- /**
61
- * RTCIceCandidatePair - A pair of local and remote ICE candidates
62
- * @typedef {Object} RTCIceCandidatePair
63
- * @property {RTCIceCandidate} local - The local candidate
64
- * @property {RTCIceCandidate} remote - The remote candidate
65
- */
66
-
67
- /**
68
- * RTCIceGatherOptions - Options for ICE candidate gathering
69
- * @typedef {Object} RTCIceGatherOptions
70
- * @property {RTCIceGatherPolicy} [gatherPolicy='all'] - Candidate gathering policy
71
- * @property {Array<RTCIceServer>} [iceServers] - STUN/TURN servers to use
72
- */
73
-
74
- /**
75
- * @class RTCIceTransport
76
- * @extends EventEmitter
77
- * @description Represents the ICE transport layer for a peer connection.
78
- * Manages ICE candidate gathering, connectivity checks, and state transitions.
79
- *
80
- * Events:
81
- * - 'statechange': Fired when the transport state changes
82
- * - 'gatheringstatechange': Fired when the gathering state changes
83
- * - 'selectedcandidatepairchange': Fired when the selected candidate pair changes
84
- * - 'icecandidate': Fired when a new local candidate is gathered
85
- *
86
- * @example
87
- * const transport = new RTCIceTransport();
88
- * transport.on('statechange', () => {
89
- * console.log('State:', transport.state);
90
- * });
91
- * transport.on('icecandidate', (candidate) => {
92
- * // Send candidate to remote peer
93
- * });
94
- */
95
- class RTCIceTransport extends EventEmitter {
96
- /**
97
- * Create an RTCIceTransport instance.
98
- */
99
- constructor() {
100
- super();
101
-
102
- // Internal state
103
- this._role = null; // RTCIceRole or null
104
- this._state = RTCIceTransportState.NEW;
105
- this._gatheringState = RTCIceGatheringState.NEW;
106
-
107
- // Candidate lists
108
- this._localCandidates = [];
109
- this._remoteCandidates = [];
110
-
111
- // ICE parameters
112
- this._localParameters = null;
113
- this._remoteParameters = null;
114
-
115
- // Selected candidate pair
116
- this._selectedCandidatePair = null;
117
-
118
- // Started flag
119
- this._started = false;
120
-
121
- // Closed flag
122
- this._closed = false;
123
-
124
- // ICE servers (STUN/TURN)
125
- this._iceServers = [];
126
-
127
- // STUN clients
128
- this._stunClients = [];
129
-
130
- // UDP sockets for candidates
131
- this._sockets = new Map(); // Map of candidate foundation -> socket
132
-
133
- // Candidate pairs and connectivity checks
134
- this._candidatePairs = [];
135
- this._validPairs = [];
136
- }
137
-
138
- /**
139
- * Get the ICE role (controlling or controlled).
140
- * @returns {string|null} The ICE role or null if not started
141
- */
142
- get role() {
143
- return this._role;
144
- }
145
-
146
- /**
147
- * Get the current ICE transport state.
148
- * @returns {string} The transport state
149
- */
150
- get state() {
151
- return this._state;
152
- }
153
-
154
- /**
155
- * Get the current ICE gathering state.
156
- * @returns {string} The gathering state
157
- */
158
- get gatheringState() {
159
- return this._gatheringState;
160
- }
161
-
162
- /**
163
- * Get the local ICE candidates.
164
- * @returns {Array<RTCIceCandidate>} Array of local candidates
165
- */
166
- getLocalCandidates() {
167
- return [...this._localCandidates];
168
- }
169
-
170
- /**
171
- * Get the remote ICE candidates.
172
- * @returns {Array<RTCIceCandidate>} Array of remote candidates
173
- */
174
- getRemoteCandidates() {
175
- return [...this._remoteCandidates];
176
- }
177
-
178
- /**
179
- * Get the selected candidate pair.
180
- * @returns {RTCIceCandidatePair|null} The selected candidate pair or null
181
- */
182
- getSelectedCandidatePair() {
183
- return this._selectedCandidatePair ? { ...this._selectedCandidatePair } : null;
184
- }
185
-
186
- /**
187
- * Get the local ICE parameters.
188
- * @returns {RTCIceParameters|null} The local parameters or null
189
- */
190
- getLocalParameters() {
191
- return this._localParameters ? { ...this._localParameters } : null;
192
- }
193
-
194
- /**
195
- * Get the remote ICE parameters.
196
- * @returns {RTCIceParameters|null} The remote parameters or null
197
- */
198
- getRemoteParameters() {
199
- return this._remoteParameters ? { ...this._remoteParameters } : null;
200
- }
201
-
202
- /**
203
- * Generate random local ICE parameters.
204
- * Creates a random username fragment and password.
205
- * @private
206
- */
207
- _generateLocalParameters() {
208
- const crypto = require('crypto');
209
-
210
- // Generate random username fragment (16 characters)
211
- const usernameFragment = crypto.randomBytes(8).toString('hex');
212
-
213
- // Generate random password (22 characters, base64)
214
- const password = crypto.randomBytes(16).toString('base64').substring(0, 22);
215
-
216
- this._localParameters = {
217
- usernameFragment,
218
- password
219
- };
220
- }
221
-
222
- /**
223
- * Start the ICE transport with remote parameters and role.
224
- * @param {RTCIceParameters} remoteParameters - The remote ICE parameters
225
- * @param {string} role - The ICE role ('controlling' or 'controlled')
226
- * @throws {Error} If transport is closed or already started
227
- * @throws {TypeError} If parameters are invalid
228
- */
229
- start(remoteParameters, role) {
230
- if (this._closed) {
231
- throw new Error('RTCIceTransport is closed');
232
- }
233
-
234
- if (this._started) {
235
- throw new Error('RTCIceTransport already started');
236
- }
237
-
238
- // Validate role
239
- if (role !== RTCIceRole.CONTROLLING && role !== RTCIceRole.CONTROLLED) {
240
- throw new TypeError(`Invalid role: ${role}`);
241
- }
242
-
243
- // Validate remote parameters
244
- if (!remoteParameters || typeof remoteParameters !== 'object') {
245
- throw new TypeError('Remote parameters must be an object');
246
- }
247
-
248
- if (typeof remoteParameters.usernameFragment !== 'string' ||
249
- remoteParameters.usernameFragment.length === 0) {
250
- throw new TypeError('usernameFragment must be a non-empty string');
251
- }
252
-
253
- if (typeof remoteParameters.password !== 'string' ||
254
- remoteParameters.password.length === 0) {
255
- throw new TypeError('password must be a non-empty string');
256
- }
257
-
258
- // Generate local parameters if not already done
259
- if (!this._localParameters) {
260
- this._generateLocalParameters();
261
- }
262
-
263
- // Store remote parameters and role
264
- this._remoteParameters = {
265
- usernameFragment: remoteParameters.usernameFragment,
266
- password: remoteParameters.password
267
- };
268
- this._role = role;
269
- this._started = true;
270
-
271
- // Transition to checking state
272
- this._setState(RTCIceTransportState.CHECKING);
273
-
274
- // Start connectivity checks if we have candidates
275
- if (this._remoteCandidates.length > 0 && this._localCandidates.length > 0) {
276
- this._startConnectivityChecks();
277
- }
278
- }
279
-
280
- /**
281
- * Start gathering ICE candidates.
282
- * @param {RTCIceGatherOptions} [options] - Gathering options
283
- * @throws {Error} If transport is closed
284
- */
285
- async gather(options = {}) {
286
- if (this._closed) {
287
- throw new Error('RTCIceTransport is closed');
288
- }
289
-
290
- // Generate local parameters if not already done
291
- if (!this._localParameters) {
292
- this._generateLocalParameters();
293
- }
294
-
295
- // Store ICE servers
296
- if (options.iceServers) {
297
- this._iceServers = options.iceServers;
298
- }
299
-
300
- // Transition to gathering state
301
- this._setGatheringState(RTCIceGatheringState.GATHERING);
302
-
303
- // Gather host candidates (local addresses)
304
- await this._gatherHostCandidates();
305
-
306
- // Gather server reflexive candidates (STUN)
307
- await this._gatherServerReflexiveCandidates();
308
-
309
- // Gather relay candidates (TURN)
310
- await this._gatherRelayCandidates();
311
-
312
- // Complete gathering
313
- setImmediate(() => {
314
- if (!this._closed) {
315
- this._setGatheringState(RTCIceGatheringState.COMPLETE);
316
- }
317
- });
318
- }
319
-
320
- /**
321
- * Add a remote ICE candidate.
322
- * @param {RTCIceCandidate} candidate - The remote candidate to add
323
- * @throws {Error} If transport is closed or not started
324
- * @throws {TypeError} If candidate is invalid
325
- */
326
- addRemoteCandidate(candidate) {
327
- if (this._closed) {
328
- throw new Error('RTCIceTransport is closed');
329
- }
330
-
331
- if (!this._started) {
332
- throw new Error('RTCIceTransport not started');
333
- }
334
-
335
- if (!candidate || typeof candidate !== 'object') {
336
- throw new TypeError('Candidate must be an object');
337
- }
338
-
339
- // Ensure candidate is properly formatted
340
- // If it's a plain object with a candidate string, parse it
341
- let parsedCandidate = candidate;
342
- const RTCIceCandidate = require('./RTCIceCandidate');
343
-
344
- if (!(candidate instanceof RTCIceCandidate)) {
345
- // Try to create an RTCIceCandidate to parse and validate
346
- try {
347
- parsedCandidate = new RTCIceCandidate(candidate);
348
- } catch (err) {
349
- console.warn('Failed to parse remote candidate:', err.message);
350
- // Store the original anyway for compatibility
351
- this._remoteCandidates.push(candidate);
352
- return;
353
- }
354
- } else {
355
- parsedCandidate = candidate;
356
- }
357
-
358
- // Validate that candidate has required properties for connectivity checks
359
- if (!parsedCandidate.address || parsedCandidate.port === null || parsedCandidate.port === undefined) {
360
- // Only warn if it's not an empty candidate (which signals end-of-candidates)
361
- if (parsedCandidate.candidate !== '') {
362
- console.warn('Remote candidate missing address or port, skipping connectivity checks');
363
- }
364
- // Store the original candidate for compatibility
365
- this._remoteCandidates.push(candidate);
366
- return;
367
- }
368
-
369
- // Store the original candidate (for getRemoteCandidates compatibility)
370
- this._remoteCandidates.push(candidate);
371
-
372
- // Start connectivity checks if we're in checking state
373
- if (this._state === RTCIceTransportState.CHECKING && this._localCandidates.length > 0) {
374
- // Create pairs using the parsed candidate for connectivity checks
375
- for (const localCandidate of this._localCandidates) {
376
- const pair = {
377
- local: localCandidate,
378
- remote: parsedCandidate,
379
- state: 'waiting'
380
- };
381
- this._candidatePairs.push(pair);
382
- this._sendConnectivityCheck(pair);
383
- }
384
- }
385
- }
386
-
387
- /**
388
- * Stop the ICE transport.
389
- * Transitions to closed state and stops all ICE processing.
390
- */
391
- stop() {
392
- if (this._closed) {
393
- return;
394
- }
395
-
396
- this._close('stopped');
397
- }
398
-
399
- /**
400
- * Internal close method.
401
- * @param {string} reason - Reason for closing
402
- * @private
403
- */
404
- _close(reason) {
405
- if (this._closed) {
406
- return;
407
- }
408
-
409
- this._closed = true;
410
- this._setState(RTCIceTransportState.CLOSED);
411
-
412
- // Close all UDP sockets
413
- this._closeSockets();
414
-
415
- // Clean up STUN/TURN clients
416
- if (this._stunClients) {
417
- for (const client of this._stunClients) {
418
- client.close();
419
- }
420
- this._stunClients = [];
421
- }
422
-
423
- // Clear refresh timers
424
- if (this._refreshTimers) {
425
- for (const timer of this._refreshTimers) {
426
- clearInterval(timer);
427
- }
428
- this._refreshTimers = [];
429
- }
430
-
431
- // Clean up
432
- this._localCandidates = [];
433
- this._remoteCandidates = [];
434
- this._selectedCandidatePair = null;
435
- }
436
-
437
- /**
438
- * Set the transport state and emit event if changed.
439
- * @param {string} newState - The new state
440
- * @private
441
- */
442
- _setState(newState) {
443
- if (this._state !== newState) {
444
- this._state = newState;
445
- this.emit('statechange');
446
- }
447
- }
448
-
449
- /**
450
- * Set the gathering state and emit event if changed.
451
- * @param {string} newState - The new gathering state
452
- * @private
453
- */
454
- _setGatheringState(newState) {
455
- if (this._gatheringState !== newState) {
456
- this._gatheringState = newState;
457
- this.emit('gatheringstatechange');
458
- }
459
- }
460
-
461
- /**
462
- * Add a local candidate (called internally during gathering).
463
- * @param {RTCIceCandidate} candidate - The local candidate
464
- * @private
465
- */
466
- _addLocalCandidate(candidate) {
467
- this._localCandidates.push(candidate);
468
- this.emit('icecandidate', candidate);
469
- }
470
-
471
- /**
472
- * Set the selected candidate pair.
473
- * @param {RTCIceCandidatePair} pair - The selected pair
474
- * @private
475
- */
476
- _setSelectedCandidatePair(pair) {
477
- this._selectedCandidatePair = pair;
478
- this.emit('selectedcandidatepairchange');
479
- }
480
-
481
- /**
482
- * Check if the transport is in a terminal state.
483
- * @returns {boolean} True if closed, false otherwise
484
- */
485
- isClosed() {
486
- return this._state === RTCIceTransportState.CLOSED;
487
- }
488
-
489
- /**
490
- * Gather host candidates (local network interfaces)
491
- * @private
492
- */
493
- async _gatherHostCandidates() {
494
- const os = require('os');
495
- const interfaces = os.networkInterfaces();
496
-
497
- for (const [name, addrs] of Object.entries(interfaces)) {
498
- for (const addr of addrs) {
499
- // Skip internal and IPv6 for now
500
- if (addr.internal || addr.family !== 'IPv4') {
501
- continue;
502
- }
503
-
504
- // Create UDP socket for this candidate
505
- const socket = dgram.createSocket('udp4');
506
-
507
- await new Promise((resolve, reject) => {
508
- socket.on('error', reject);
509
-
510
- // Bind to the interface address with port 0 (auto-assign)
511
- socket.bind(0, addr.address, () => {
512
- const address = socket.address();
513
-
514
- const RTCIceCandidate = require('./RTCIceCandidate');
515
- const foundation = crypto.randomBytes(4).toString('hex');
516
-
517
- // Generate SDP candidate string
518
- const candidateString = `candidate:${foundation} 1 udp ${2130706431} ${address.address} ${address.port} typ host`;
519
-
520
- const candidate = new RTCIceCandidate({
521
- candidate: candidateString,
522
- sdpMid: '0',
523
- sdpMLineIndex: 0,
524
- foundation,
525
- priority: 2130706431, // Host candidates have highest priority
526
- address: address.address,
527
- protocol: 'udp',
528
- port: address.port,
529
- type: 'host',
530
- component: 'rtp',
531
- usernameFragment: this._localParameters.usernameFragment
532
- });
533
-
534
- // Store socket associated with this candidate
535
- this._sockets.set(foundation, socket);
536
-
537
- // Setup socket message handler for ICE connectivity checks
538
- socket.on('message', (msg, rinfo) => {
539
- this._handleSocketMessage(msg, rinfo, candidate);
540
- });
541
-
542
- socket.on('error', (err) => {
543
- console.error(`Socket error for ${address.address}:${address.port}:`, err);
544
- });
545
-
546
- this._addLocalCandidate(candidate);
547
- resolve();
548
- });
549
- });
550
- }
551
- }
552
- }
553
-
554
- /**
555
- * Gather server reflexive candidates using STUN
556
- * @private
557
- */
558
- async _gatherServerReflexiveCandidates() {
559
- if (!this._iceServers || this._iceServers.length === 0) {
560
- return;
561
- }
562
-
563
- // Use the first host candidate's socket for STUN queries
564
- const hostCandidates = this._localCandidates.filter(c => c.type === 'host');
565
- if (hostCandidates.length === 0) {
566
- return; // No host candidates to use
567
- }
568
-
569
- for (const server of this._iceServers) {
570
- const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
571
-
572
- for (const url of urls) {
573
- if (!url.startsWith('stun:')) {
574
- continue; // Skip non-STUN servers
575
- }
576
-
577
- try {
578
- const parsed = this._parseServerUrl(url);
579
- const stunClient = new STUNClient({
580
- server: parsed.host,
581
- port: parsed.port,
582
- username: server.username,
583
- credential: server.credential,
584
- transport: parsed.transport,
585
- params: parsed.params
586
- });
587
-
588
- this._stunClients.push(stunClient);
589
-
590
- const reflexiveAddr = await stunClient.getReflexiveAddress();
591
-
592
- const RTCIceCandidate = require('./RTCIceCandidate');
593
- const foundation = crypto.randomBytes(4).toString('hex');
594
- const hostCandidate = hostCandidates[0];
595
-
596
- // Generate SDP candidate string for server reflexive candidate
597
- const candidateString = `candidate:${foundation} 1 udp ${1694498815} ${reflexiveAddr.address} ${reflexiveAddr.port} typ srflx raddr ${hostCandidate.address} rport ${hostCandidate.port}`;
598
-
599
- const candidate = new RTCIceCandidate({
600
- candidate: candidateString,
601
- sdpMid: '0',
602
- sdpMLineIndex: 0,
603
- foundation,
604
- priority: 1694498815, // Server reflexive candidates
605
- address: reflexiveAddr.address,
606
- protocol: 'udp',
607
- port: reflexiveAddr.port,
608
- type: 'srflx',
609
- component: 'rtp',
610
- relatedAddress: hostCandidate.address,
611
- relatedPort: hostCandidate.port,
612
- usernameFragment: this._localParameters.usernameFragment
613
- });
614
-
615
- // Store the socket (same as host candidate's socket)
616
- this._sockets.set(foundation, this._sockets.get(hostCandidate.foundation));
617
-
618
- this._addLocalCandidate(candidate);
619
- } catch (error) {
620
- console.error(`Failed to gather from STUN server ${url}:`, error.message);
621
- }
622
- }
623
- }
624
- }
625
-
626
- /**
627
- * Gather relay candidates using TURN
628
- * @private
629
- */
630
- async _gatherRelayCandidates() {
631
- if (!this._iceServers || this._iceServers.length === 0) {
632
- return;
633
- }
634
-
635
- for (const server of this._iceServers) {
636
- const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
637
-
638
- for (const url of urls) {
639
- if (!url.startsWith('turn:') && !url.startsWith('turns:')) {
640
- continue; // Skip non-TURN servers
641
- }
642
-
643
- // TURN requires authentication
644
- if (!server.username || !server.credential) {
645
- console.warn(`TURN server ${url} requires username and credential`);
646
- continue;
647
- }
648
-
649
- try {
650
- const parsed = this._parseServerUrl(url);
651
- const turnClient = new STUNClient({
652
- server: parsed.host,
653
- port: parsed.port,
654
- username: server.username,
655
- credential: server.credential,
656
- transport: parsed.transport,
657
- params: parsed.params
658
- });
659
-
660
- this._stunClients.push(turnClient);
661
-
662
- // Allocate relay address
663
- const allocation = await turnClient.allocateRelay(600);
664
-
665
- const RTCIceCandidate = require('./RTCIceCandidate');
666
- const foundation = crypto.randomBytes(4).toString('hex');
667
-
668
- // Generate SDP candidate string for relay candidate
669
- const candidateString = `candidate:${foundation} 1 udp ${16777215} ${allocation.relayedAddress} ${allocation.relayedPort} typ relay raddr ${parsed.host} rport ${parsed.port}`;
670
-
671
- const candidate = new RTCIceCandidate({
672
- candidate: candidateString,
673
- sdpMid: '0',
674
- sdpMLineIndex: 0,
675
- foundation,
676
- priority: 16777215, // Relay candidates have lowest priority
677
- address: allocation.relayedAddress,
678
- protocol: 'udp',
679
- port: allocation.relayedPort,
680
- type: 'relay',
681
- component: 'rtp',
682
- relatedAddress: parsed.host, // TURN server address
683
- relatedPort: parsed.port,
684
- usernameFragment: this._localParameters.usernameFragment
685
- });
686
-
687
- // Store TURN client as the "socket" for this relay candidate
688
- this._sockets.set(foundation, { type: 'turn', client: turnClient });
689
-
690
- // Handle incoming data from TURN
691
- turnClient.on('data', (data, peer) => {
692
- this._handleSocketMessage(data, peer, candidate);
693
- });
694
-
695
- this._addLocalCandidate(candidate);
696
-
697
- // Keep allocation alive
698
- this._keepAllocAlive(turnClient, allocation.lifetime);
699
- } catch (error) {
700
- console.error(`Failed to allocate from TURN server ${url}:`, error.message);
701
- }
702
- }
703
- }
704
- }
705
-
706
- /**
707
- * Keep TURN allocation alive by sending periodic refresh requests
708
- * @param {STUNClient} client - TURN client
709
- * @param {number} lifetime - Allocation lifetime
710
- * @private
711
- */
712
- _keepAllocAlive(client, lifetime) {
713
- // Refresh 30 seconds before expiry
714
- const refreshInterval = (lifetime - 30) * 1000;
715
-
716
- const refreshTimer = setInterval(async () => {
717
- if (this._closed) {
718
- clearInterval(refreshTimer);
719
- return;
720
- }
721
-
722
- try {
723
- await client.refreshAllocation(600);
724
- } catch (error) {
725
- console.error('Failed to refresh TURN allocation:', error.message);
726
- clearInterval(refreshTimer);
727
- }
728
- }, refreshInterval);
729
-
730
- // Store timer for cleanup
731
- if (!this._refreshTimers) {
732
- this._refreshTimers = [];
733
- }
734
- this._refreshTimers.push(refreshTimer);
735
- }
736
-
737
- /**
738
- * Start ICE connectivity checks
739
- * @private
740
- */
741
- _startConnectivityChecks() {
742
- // Form candidate pairs (simplified: just pair first local with first remote)
743
- for (const localCandidate of this._localCandidates) {
744
- for (const remoteCandidate of this._remoteCandidates) {
745
- this._candidatePairs.push({
746
- local: localCandidate,
747
- remote: remoteCandidate,
748
- state: 'waiting'
749
- });
750
- }
751
- }
752
-
753
- // Send connectivity checks for each pair
754
- for (const pair of this._candidatePairs) {
755
- this._sendConnectivityCheck(pair);
756
- }
757
- }
758
-
759
- /**
760
- * Send a connectivity check (STUN Binding Request) to a candidate pair
761
- * @param {Object} pair - Candidate pair
762
- * @private
763
- */
764
- /**
765
- * Send ICE connectivity check to remote candidate
766
- * @param {Object} pair - Candidate pair
767
- * @private
768
- */
769
- _sendConnectivityCheck(pair) {
770
- const socket = this._sockets.get(pair.local.foundation);
771
- if (!socket) {
772
- return;
773
- }
774
-
775
- // Validate remote candidate has required properties
776
- if (!pair.remote.address || !pair.remote.port) {
777
- console.warn('Cannot send connectivity check: remote candidate missing address or port', {
778
- address: pair.remote.address,
779
- port: pair.remote.port,
780
- candidate: pair.remote.candidate
781
- });
782
- return;
783
- }
784
-
785
- const transactionId = crypto.randomBytes(12);
786
- const request = this._createBindingRequest(transactionId);
787
-
788
- try {
789
- if (socket.type === 'turn') {
790
- const turnClient = socket.client;
791
- // Create permission and send indication
792
- turnClient.createPermission(pair.remote.address)
793
- .then(() => {
794
- return turnClient.sendIndication(pair.remote.address, pair.remote.port, request);
795
- })
796
- .catch(err => {
797
- // Suppress errors for now as this happens frequently during connection
798
- });
799
- } else {
800
- socket.send(request, pair.remote.port, pair.remote.address, (err) => {
801
- if (err) {
802
- console.error(`Connectivity check failed for ${pair.remote.address}:${pair.remote.port}:`, err);
803
- }
804
- });
805
- }
806
- } catch (err) {
807
- console.error(`Error sending connectivity check to ${pair.remote.address || 'unknown'}:${pair.remote.port || 'unknown'}:`, err);
808
- }
809
- }
810
-
811
- /**
812
- * Create a STUN Binding Request for connectivity checks
813
- * @param {Buffer} transactionId - Transaction ID
814
- * @returns {Buffer} STUN Binding Request
815
- * @private
816
- */
817
- _createBindingRequest(transactionId) {
818
- const MAGIC_COOKIE = 0x2112A442;
819
-
820
- // Simple binding request with no attributes
821
- const header = Buffer.alloc(20);
822
- header.writeUInt16BE(0x0001, 0); // Binding Request
823
- header.writeUInt16BE(0, 2); // Message length (no attributes for now)
824
- header.writeUInt32BE(MAGIC_COOKIE, 4);
825
- transactionId.copy(header, 8);
826
-
827
- return header;
828
- }
829
-
830
- /**
831
- * Parse STUN/TURN server URL
832
- * @param {string} url - Server URL
833
- * @returns {Object} Parsed URL
834
- * @private
835
- */
836
- /**
837
- * Parse server URL with query string support
838
- * @param {string} url - Server URL (e.g., turn:host:port?transport=udp&ttl=86400)
839
- * @returns {Object} Parsed server info
840
- * @private
841
- */
842
- _parseServerUrl(url) {
843
- // Match: (stun|turn|turns)://host:port?query or (stun|turn|turns):host:port?query
844
- const match = url.match(/^(stun|turn|turns):\/?\/?([^:?]+):?(\d+)?(\?(.+))?/);
845
- if (!match) {
846
- throw new Error(`Invalid server URL: ${url}`);
847
- }
848
-
849
- const protocol = match[1];
850
- const host = match[2];
851
- const port = match[3];
852
- const queryString = match[5];
853
-
854
- // Parse query parameters
855
- const params = {};
856
- if (queryString) {
857
- queryString.split('&').forEach(param => {
858
- const [key, value] = param.split('=');
859
- if (key) {
860
- // If value is undefined (no = sign), set to true
861
- // If value is empty string, keep it as empty string
862
- params[key] = value !== undefined ? value : true;
863
- }
864
- });
865
- }
866
-
867
- return {
868
- protocol,
869
- host,
870
- port: parseInt(port || (protocol === 'turns' ? '5349' : '3478'), 10),
871
- transport: params.transport || 'udp', // Default to UDP
872
- params // Include all query parameters for future use
873
- };
874
- }
875
-
876
- /**
877
- * Handle incoming messages on a socket (ICE connectivity checks)
878
- * @param {Buffer} msg - The message buffer
879
- * @param {Object} rinfo - Remote address info
880
- * @param {RTCIceCandidate} localCandidate - The local candidate that received the message
881
- * @private
882
- */
883
- _handleSocketMessage(msg, rinfo, localCandidate) {
884
- // Check if this is a STUN message (magic cookie check)
885
- if (msg.length < 20) return;
886
-
887
- const magicCookie = msg.readUInt32BE(4);
888
- if (magicCookie !== 0x2112A442) return; // Not a STUN message
889
-
890
- const messageType = msg.readUInt16BE(0);
891
-
892
- // STUN Binding Request (0x0001) - this is an ICE connectivity check
893
- if (messageType === 0x0001) {
894
- this._handleBindingRequest(msg, rinfo, localCandidate);
895
- }
896
- // STUN Binding Response (0x0101) - response to our connectivity check
897
- else if (messageType === 0x0101) {
898
- this._handleBindingResponse(msg, rinfo, localCandidate);
899
- }
900
- }
901
-
902
- /**
903
- * Handle STUN Binding Request (incoming connectivity check)
904
- * @param {Buffer} msg - The STUN message
905
- * @param {Object} rinfo - Remote address info
906
- * @param {RTCIceCandidate} localCandidate - The local candidate
907
- * @private
908
- */
909
- _handleBindingRequest(msg, rinfo, localCandidate) {
910
- // Send Binding Response
911
- const transactionId = msg.slice(8, 20);
912
- const response = this._createBindingResponse(transactionId, rinfo.address, rinfo.port);
913
-
914
- const socket = this._sockets.get(localCandidate.foundation);
915
- if (socket) {
916
- socket.send(response, rinfo.port, rinfo.address);
917
-
918
- // If we haven't selected a candidate pair yet and this check succeeded,
919
- // this could be our selected pair
920
- if (!this._selectedCandidatePair && this._state === RTCIceTransportState.CHECKING) {
921
- this._setState(RTCIceTransportState.CONNECTED);
922
- }
923
- }
924
- }
925
-
926
- /**
927
- * Handle STUN Binding Response (response to our connectivity check)
928
- * @param {Buffer} msg - The STUN message
929
- * @param {Object} rinfo - Remote address info
930
- * @param {RTCIceCandidate} localCandidate - The local candidate
931
- * @private
932
- */
933
- _handleBindingResponse(msg, rinfo, localCandidate) {
934
- // Mark this candidate pair as valid
935
- // In a full implementation, we would track which pair this belongs to
936
- if (!this._selectedCandidatePair && this._state === RTCIceTransportState.CHECKING) {
937
- this._setState(RTCIceTransportState.CONNECTED);
938
- }
939
- }
940
-
941
- /**
942
- * Create a STUN Binding Response
943
- * @param {Buffer} transactionId - Transaction ID from the request
944
- * @param {string} address - XOR-mapped address
945
- * @param {number} port - XOR-mapped port
946
- * @returns {Buffer} STUN Binding Response message
947
- * @private
948
- */
949
- _createBindingResponse(transactionId, address, port) {
950
- const MAGIC_COOKIE = 0x2112A442;
951
-
952
- // XOR the port with magic cookie high 16 bits
953
- const xorPort = port ^ (MAGIC_COOKIE >> 16);
954
-
955
- // XOR the address with magic cookie
956
- const addrParts = address.split('.').map(Number);
957
- const addrNum = (addrParts[0] << 24) | (addrParts[1] << 16) | (addrParts[2] << 8) | addrParts[3];
958
- const xorAddr = addrNum ^ MAGIC_COOKIE;
959
-
960
- // Build XOR-MAPPED-ADDRESS attribute
961
- const attr = Buffer.alloc(12);
962
- attr.writeUInt16BE(0x0020, 0); // XOR-MAPPED-ADDRESS
963
- attr.writeUInt16BE(8, 2); // Length
964
- attr.writeUInt8(0, 4); // Reserved
965
- attr.writeUInt8(0x01, 5); // Family (IPv4)
966
- attr.writeUInt16BE(xorPort, 6);
967
- attr.writeUInt32BE(xorAddr, 8);
968
-
969
- // Build message
970
- const header = Buffer.alloc(20);
971
- header.writeUInt16BE(0x0101, 0); // Binding Response
972
- header.writeUInt16BE(12, 2); // Message length (attribute size)
973
- header.writeUInt32BE(MAGIC_COOKIE, 4);
974
- transactionId.copy(header, 8);
975
-
976
- return Buffer.concat([header, attr]);
977
- }
978
-
979
- /**
980
- * Check if start() has been called.
981
- * @returns {boolean} True if started, false otherwise
982
- */
983
- isStarted() {
984
- return this._started;
985
- }
986
-
987
- /**
988
- * Get a socket for data transmission (returns the first available socket)
989
- * @returns {Object|null} UDP socket or null
990
- */
991
- getSocket() {
992
- const sockets = Array.from(this._sockets.values());
993
- return sockets.length > 0 ? sockets[0] : null;
994
- }
995
-
996
- /**
997
- * Close all sockets
998
- * @private
999
- */
1000
- _closeSockets() {
1001
- for (const socket of this._sockets.values()) {
1002
- try {
1003
- socket.close();
1004
- } catch (err) {
1005
- // Ignore errors when closing
1006
- }
1007
- }
1008
- this._sockets.clear();
1009
- }
1010
- }
1011
-
1012
- // Export the class and enums
1013
- module.exports = {
1014
- RTCIceTransport,
1015
- RTCIceRole,
1016
- RTCIceTransportState,
1017
- RTCIceGatheringState
1018
- };