node-rtc-connection 1.0.12 → 1.0.14

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.
@@ -1,341 +0,0 @@
1
- /**
2
- * ICE Candidate Gatherer
3
- * Discovers local, reflexive (STUN), and relay (TURN) candidates
4
- */
5
-
6
- const os = require('os');
7
- const dgram = require('dgram');
8
- const STUNClient = require('./STUNClient');
9
- const TURNClient = require('./TURNClient');
10
-
11
- class ICEGatherer {
12
- constructor(options = {}) {
13
- this.stunServers = options.stunServers || [
14
- 'stun.l.google.com:19302',
15
- 'stun1.l.google.com:19302',
16
- 'stun2.l.google.com:19302'
17
- ];
18
- this.turnServers = options.turnServers || [];
19
- this.gatherTimeout = options.gatherTimeout || 5000;
20
- }
21
-
22
- /**
23
- * Gather all ICE candidates
24
- * @param {number} localPort - Local port being used
25
- * @returns {Promise<Array>} Array of ICE candidates
26
- */
27
- async gatherCandidates(localPort) {
28
- const candidates = [];
29
-
30
- // 1. Gather host candidates (local interfaces)
31
- const hostCandidates = this._getHostCandidates(localPort);
32
- candidates.push(...hostCandidates);
33
-
34
- // 2. Gather server reflexive candidates (via STUN)
35
- try {
36
- const srflxCandidates = await this._getServerReflexiveCandidates(localPort);
37
- candidates.push(...srflxCandidates);
38
- } catch (err) {
39
- console.warn('Failed to gather STUN candidates:', err.message);
40
- }
41
-
42
- // 3. Gather relay candidates (via TURN)
43
- try {
44
- const relayCandidates = await this._getRelayCandidates(localPort);
45
- candidates.push(...relayCandidates);
46
- } catch (err) {
47
- console.warn('Failed to gather TURN candidates:', err.message);
48
- }
49
-
50
- // 4. Sort by priority (host > srflx > relay)
51
- candidates.sort((a, b) => b.priority - a.priority);
52
-
53
- return candidates;
54
- }
55
-
56
- /**
57
- * Get host candidates from local network interfaces
58
- * @private
59
- */
60
- _getHostCandidates(port) {
61
- const candidates = [];
62
- const interfaces = os.networkInterfaces();
63
- let foundation = 1;
64
-
65
- for (const [name, addrs] of Object.entries(interfaces)) {
66
- for (const addr of addrs) {
67
- // Skip internal/loopback addresses
68
- if (addr.internal) {
69
- continue;
70
- }
71
-
72
- // Support both IPv4 and IPv6
73
- if (addr.family !== 'IPv4' && addr.family !== 'IPv6') {
74
- continue;
75
- }
76
-
77
- // Calculate priority (IPv4 slightly higher than IPv6)
78
- const typePreference = addr.family === 'IPv4' ? 65535 : 65534;
79
- const priority = this._calculatePriority('host', typePreference, foundation);
80
-
81
- candidates.push({
82
- candidate: `candidate:${foundation} 1 udp ${priority} ${addr.address} ${port} typ host`,
83
- sdpMLineIndex: 0,
84
- sdpMid: 'data',
85
- foundation: String(foundation),
86
- component: 1,
87
- protocol: 'udp',
88
- priority,
89
- ip: addr.address,
90
- port,
91
- type: 'host',
92
- tcpType: null,
93
- relatedAddress: null,
94
- relatedPort: null
95
- });
96
-
97
- foundation++;
98
- }
99
- }
100
-
101
- return candidates;
102
- }
103
-
104
- /**
105
- * Get server reflexive candidates via STUN
106
- * @private
107
- */
108
- async _getServerReflexiveCandidates(localPort) {
109
- const candidates = [];
110
- const stunPromises = [];
111
-
112
- // Try multiple STUN servers in parallel
113
- for (const stunServer of this.stunServers) {
114
- const promise = this._querySTUNServer(stunServer, localPort)
115
- .catch(err => null); // Ignore individual failures
116
- stunPromises.push(promise);
117
- }
118
-
119
- // Wait for first successful response
120
- const results = await Promise.race([
121
- Promise.any(stunPromises.filter(p => p)),
122
- new Promise(resolve => setTimeout(() => resolve(null), this.gatherTimeout))
123
- ]);
124
-
125
- if (results) {
126
- const foundation = 100;
127
- const priority = this._calculatePriority('srflx', 65535, foundation);
128
-
129
- candidates.push({
130
- candidate: `candidate:${foundation} 1 udp ${priority} ${results.ip} ${results.port} typ srflx raddr ${results.localIp} rport ${localPort}`,
131
- sdpMLineIndex: 0,
132
- sdpMid: 'data',
133
- foundation: String(foundation),
134
- component: 1,
135
- protocol: 'udp',
136
- priority,
137
- ip: results.ip,
138
- port: results.port,
139
- type: 'srflx',
140
- tcpType: null,
141
- relatedAddress: results.localIp,
142
- relatedPort: localPort
143
- });
144
- }
145
-
146
- return candidates;
147
- }
148
-
149
- /**
150
- * Query a STUN server
151
- * @private
152
- */
153
- async _querySTUNServer(stunServer, localPort) {
154
- const client = new STUNClient();
155
- try {
156
- const result = await client.getReflexiveAddress(stunServer);
157
-
158
- // Get local IP that would be used to reach STUN server
159
- const localIp = this._getLocalIPForRemote();
160
-
161
- return {
162
- ...result,
163
- localIp
164
- };
165
- } finally {
166
- client.close();
167
- }
168
- }
169
-
170
- /**
171
- * Get local IP address that would be used for external connections
172
- * @private
173
- */
174
- _getLocalIPForRemote() {
175
- const interfaces = os.networkInterfaces();
176
-
177
- // Prefer non-internal IPv4 addresses first
178
- for (const [name, addrs] of Object.entries(interfaces)) {
179
- for (const addr of addrs) {
180
- if (!addr.internal && addr.family === 'IPv4') {
181
- return addr.address;
182
- }
183
- }
184
- }
185
-
186
- // Fall back to IPv6 if no IPv4 available
187
- for (const [name, addrs] of Object.entries(interfaces)) {
188
- for (const addr of addrs) {
189
- if (!addr.internal && addr.family === 'IPv6') {
190
- return addr.address;
191
- }
192
- }
193
- }
194
-
195
- return '0.0.0.0';
196
- }
197
-
198
- /**
199
- * Get relay candidates via TURN
200
- * @private
201
- */
202
- async _getRelayCandidates(localPort) {
203
- const candidates = [];
204
-
205
- if (this.turnServers.length === 0) {
206
- console.log('[ICEGatherer] No TURN servers configured');
207
- return candidates;
208
- }
209
-
210
- console.log(`[ICEGatherer] Querying ${this.turnServers.length} TURN server(s) for relay candidates...`);
211
-
212
- const turnPromises = [];
213
-
214
- // Try TURN servers
215
- for (const turnConfig of this.turnServers) {
216
- const promise = this._queryTURNServer(turnConfig, localPort)
217
- .catch(err => {
218
- console.warn(`[ICEGatherer] TURN query failed: ${err.message}`);
219
- return null;
220
- });
221
- turnPromises.push(promise);
222
- }
223
-
224
- // Wait for first successful response or timeout
225
- const results = await Promise.race([
226
- ...turnPromises,
227
- new Promise(resolve => setTimeout(() => resolve(null), this.gatherTimeout))
228
- ]);
229
-
230
- if (results) {
231
- const foundation = 200;
232
- const priority = this._calculatePriority('relay', 65535, foundation);
233
-
234
- const localIp = this._getLocalIPForRemote();
235
-
236
- console.log(`[ICEGatherer] Got TURN relay: ${results.relayedAddress}:${results.relayedPort}`);
237
-
238
- candidates.push({
239
- candidate: `candidate:${foundation} 1 udp ${priority} ${results.relayedAddress} ${results.relayedPort} typ relay raddr ${localIp} rport ${localPort}`,
240
- sdpMLineIndex: 0,
241
- sdpMid: 'data',
242
- foundation: String(foundation),
243
- component: 1,
244
- protocol: 'udp',
245
- priority,
246
- ip: results.relayedAddress,
247
- port: results.relayedPort,
248
- type: 'relay',
249
- tcpType: null,
250
- relatedAddress: localIp,
251
- relatedPort: localPort
252
- });
253
- } else {
254
- console.log('[ICEGatherer] No relay candidates obtained from TURN servers');
255
- }
256
-
257
- return candidates;
258
- }
259
-
260
- /**
261
- * Query a TURN server
262
- * @private
263
- */
264
- async _queryTURNServer(turnConfig, localPort) {
265
- const client = new TURNClient({
266
- server: turnConfig.urls || turnConfig.url,
267
- username: turnConfig.username,
268
- password: turnConfig.credential,
269
- transport: 'udp'
270
- });
271
-
272
- try {
273
- const result = await client.allocate();
274
- return result;
275
- } finally {
276
- client.close();
277
- }
278
- }
279
-
280
- /**
281
- * Calculate ICE candidate priority (RFC 5245)
282
- * @private
283
- */
284
- _calculatePriority(type, localPref, foundation) {
285
- const typePreference = {
286
- 'host': 126,
287
- 'srflx': 100,
288
- 'prflx': 110,
289
- 'relay': 0
290
- };
291
-
292
- const typePref = typePreference[type] || 0;
293
- const componentId = 1; // RTP component
294
-
295
- // Priority = (2^24)*(type preference) + (2^8)*(local preference) + (256 - component ID)
296
- return (typePref << 24) + (localPref << 8) + (256 - componentId);
297
- }
298
-
299
- /**
300
- * Parse ICE candidate string
301
- * @param {string} candidateStr - ICE candidate string
302
- * @returns {Object} Parsed candidate object
303
- */
304
- static parseCandidate(candidateStr) {
305
- // Remove "candidate:" prefix if present
306
- const str = candidateStr.replace(/^candidate:/, '');
307
- const parts = str.split(' ');
308
-
309
- if (parts.length < 8) {
310
- throw new Error('Invalid candidate string');
311
- }
312
-
313
- const candidate = {
314
- foundation: parts[0],
315
- component: parseInt(parts[1], 10),
316
- protocol: parts[2].toLowerCase(),
317
- priority: parseInt(parts[3], 10),
318
- ip: parts[4],
319
- port: parseInt(parts[5], 10),
320
- type: parts[7]
321
- };
322
-
323
- // Parse optional fields (raddr, rport, etc.)
324
- for (let i = 8; i < parts.length; i += 2) {
325
- const key = parts[i];
326
- const value = parts[i + 1];
327
-
328
- if (key === 'raddr') {
329
- candidate.relatedAddress = value;
330
- } else if (key === 'rport') {
331
- candidate.relatedPort = parseInt(value, 10);
332
- } else if (key === 'tcptype') {
333
- candidate.tcpType = value;
334
- }
335
- }
336
-
337
- return candidate;
338
- }
339
- }
340
-
341
- module.exports = ICEGatherer;