node-rtc-connection 1.0.4 → 1.0.6

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.
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "node-rtc-connection",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "WebRTC DataChannel implementation for Node.js with STUN, TURN, NAT traversal, and encryption. Pure Node.js, no native dependencies.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
7
+ "types": "src/index.d.ts",
7
8
  "exports": {
8
9
  ".": {
10
+ "types": "./src/index.d.ts",
9
11
  "require": "./dist/index.cjs",
10
12
  "import": "./dist/index.mjs"
11
13
  }
@@ -100,6 +100,9 @@ class NativePeerConnection extends EventEmitter {
100
100
  this._secureConnection = null;
101
101
  this._udpTransport = null;
102
102
  this._iceCandidates = [];
103
+ this._remoteCandidates = [];
104
+ this._selectedLocalCandidate = null;
105
+ this._selectedRemoteCandidate = null;
103
106
 
104
107
  console.log('[NativePeerConnection] Created with STUN/ICE/Encryption support');
105
108
  console.log(` - Encryption: ${this._useEncryption ? 'enabled' : 'disabled (requires valid certs)'}`);
@@ -245,22 +248,106 @@ class NativePeerConnection extends EventEmitter {
245
248
  }
246
249
 
247
250
  if (!candidate || !candidate.candidate) {
251
+ // null candidate signals end of candidates - try to select best pair
252
+ if (this._remoteCandidates.length > 0 && !this._selectedRemoteCandidate) {
253
+ this._selectBestCandidatePair();
254
+ }
255
+ return;
256
+ }
257
+
258
+ // Parse ICE candidate
259
+ const parsed = this._parseIceCandidate(candidate.candidate);
260
+ if (!parsed) {
248
261
  return;
249
262
  }
250
263
 
251
- // Parse ICE candidate to extract address and port
252
- const parts = candidate.candidate.split(' ');
253
- if (parts.length >= 6) {
254
- this._remoteAddress = parts[4];
255
- this._remotePort = parseInt(parts[5], 10);
264
+ // Store the remote candidate
265
+ this._remoteCandidates.push(parsed);
266
+ console.log(`[NativePeerConnection] Added remote ICE candidate (${parsed.type}): ${parsed.ip}:${parsed.port}`);
267
+
268
+ // For backward compatibility, set remote address immediately if not set
269
+ if (!this._remoteAddress) {
270
+ this._remoteAddress = parsed.ip;
271
+ this._remotePort = parsed.port;
256
272
  console.log(`[NativePeerConnection] Remote address: ${this._remoteAddress}:${this._remotePort}`);
257
-
258
- // If we have both local and remote info, and we're offerer, connect
259
- if (this._isOfferer && this._localAddress && this._remoteAddress &&
260
- this._signalingState === 0 && !this._socket) {
261
- await this._connectToPeer();
273
+ }
274
+
275
+ // If we haven't selected a pair yet, try to select now
276
+ if (!this._selectedRemoteCandidate && this._iceCandidates.length > 0) {
277
+ this._selectBestCandidatePair();
278
+ }
279
+
280
+ // If we selected a pair and are offerer, connect
281
+ if (this._selectedRemoteCandidate && this._isOfferer &&
282
+ this._signalingState === 0 && !this._socket) {
283
+ await this._connectToPeer();
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Parse ICE candidate string
289
+ * @private
290
+ */
291
+ _parseIceCandidate(candidateStr) {
292
+ const parts = candidateStr.split(' ');
293
+ if (parts.length < 6) {
294
+ return null;
295
+ }
296
+
297
+ return {
298
+ foundation: parts[0].replace('candidate:', ''),
299
+ component: parts[1],
300
+ protocol: parts[2],
301
+ priority: parseInt(parts[3], 10),
302
+ ip: parts[4],
303
+ port: parseInt(parts[5], 10),
304
+ type: parts[7], // typ host/srflx/relay
305
+ relatedAddress: parts[9] || null,
306
+ relatedPort: parts[11] ? parseInt(parts[11], 10) : null
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Select best candidate pair for connection
312
+ * Prioritizes: relay > srflx > host
313
+ * @private
314
+ */
315
+ _selectBestCandidatePair() {
316
+ if (this._remoteCandidates.length === 0 || this._iceCandidates.length === 0) {
317
+ return;
318
+ }
319
+
320
+ // Priority order: relay > srflx > host
321
+ const typePriority = { 'relay': 3, 'srflx': 2, 'host': 1 };
322
+
323
+ // Find best local candidate
324
+ let bestLocal = this._iceCandidates[0];
325
+ for (const candidate of this._iceCandidates) {
326
+ if (typePriority[candidate.type] > typePriority[bestLocal.type]) {
327
+ bestLocal = candidate;
328
+ }
329
+ }
330
+
331
+ // Find best remote candidate
332
+ let bestRemote = this._remoteCandidates[0];
333
+ for (const candidate of this._remoteCandidates) {
334
+ if (typePriority[candidate.type] > typePriority[bestRemote.type]) {
335
+ bestRemote = candidate;
262
336
  }
263
337
  }
338
+
339
+ this._selectedLocalCandidate = bestLocal;
340
+ this._selectedRemoteCandidate = bestRemote;
341
+
342
+ // Update addresses for connection
343
+ this._localAddress = bestLocal.ip;
344
+ this._localPort = bestLocal.port;
345
+ this._remoteAddress = bestRemote.ip;
346
+ this._remotePort = bestRemote.port;
347
+
348
+ console.log(`[NativePeerConnection] Selected candidate pair:`);
349
+ console.log(` Local: ${bestLocal.type} ${this._localAddress}:${this._localPort}`);
350
+ console.log(` Remote: ${bestRemote.type} ${this._remoteAddress}:${this._remotePort}`);
264
351
  }
265
352
 
266
353
  /**
@@ -396,14 +483,24 @@ class NativePeerConnection extends EventEmitter {
396
483
  return;
397
484
  }
398
485
 
399
- // Tie-breaking: only connect if our port is higher than remote port
400
- // This ensures only one peer connects, avoiding the race condition
401
- if (this._localPort < this._remotePort) {
402
- console.log(`[NativePeerConnection] Not connecting (local port ${this._localPort} < remote port ${this._remotePort}), waiting for incoming`);
403
- return;
486
+ // Check if we're using relay candidates - if so, skip tie-breaking
487
+ const usingRelay = this._selectedLocalCandidate?.type === 'relay' ||
488
+ this._selectedRemoteCandidate?.type === 'relay';
489
+
490
+ if (!usingRelay) {
491
+ // Tie-breaking: only connect if our port is higher than remote port
492
+ // This ensures only one peer connects, avoiding the race condition
493
+ // Note: This only works for direct connections (host/srflx)
494
+ if (this._localPort < this._remotePort) {
495
+ console.log(`[NativePeerConnection] Not connecting (local port ${this._localPort} < remote port ${this._remotePort}), waiting for incoming`);
496
+ return;
497
+ }
404
498
  }
405
499
 
406
500
  console.log(`[NativePeerConnection] Connecting to ${this._remoteAddress}:${this._remotePort}`);
501
+ if (usingRelay) {
502
+ console.log(`[NativePeerConnection] Using TURN relay connection`);
503
+ }
407
504
 
408
505
  this._socket = new net.Socket();
409
506
 
@@ -696,7 +793,12 @@ a=max-message-size:262144
696
793
 
697
794
  // Emit each candidate
698
795
  for (const candidate of candidates) {
699
- this._iceCandidates.push(candidate);
796
+ // Parse and store the candidate
797
+ const parsed = this._parseIceCandidate(candidate.candidate);
798
+ if (parsed) {
799
+ this._iceCandidates.push(parsed);
800
+ }
801
+
700
802
  this.emit('icecandidate', {
701
803
  candidate: candidate.candidate,
702
804
  sdpMid: candidate.sdpMid,
@@ -710,8 +812,14 @@ a=max-message-size:262144
710
812
 
711
813
  // Fallback: emit only local host candidate
712
814
  if (this._localAddress && this._localPort) {
815
+ const candidateStr = `candidate:1 1 tcp 2130706431 ${this._localAddress} ${this._localPort} typ host`;
816
+ const parsed = this._parseIceCandidate(candidateStr);
817
+ if (parsed) {
818
+ this._iceCandidates.push(parsed);
819
+ }
820
+
713
821
  const candidate = {
714
- candidate: `candidate:1 1 tcp 2130706431 ${this._localAddress} ${this._localPort} typ host`,
822
+ candidate: candidateStr,
715
823
  sdpMid: 'data',
716
824
  sdpMLineIndex: 0
717
825
  };
@@ -3,6 +3,16 @@ const RTCDataChannel = require('./RTCDataChannel');
3
3
  const RTCSessionDescription = require('./RTCSessionDescription');
4
4
  const RTCIceCandidate = require('./RTCIceCandidate');
5
5
 
6
+ // Lazy-load factory to avoid circular dependency
7
+ let _defaultFactory = null;
8
+ function getDefaultFactory() {
9
+ if (!_defaultFactory) {
10
+ const NativePeerConnectionFactory = require('./NativePeerConnectionFactory');
11
+ _defaultFactory = new NativePeerConnectionFactory();
12
+ }
13
+ return _defaultFactory;
14
+ }
15
+
6
16
  /**
7
17
  * RTCPeerConnection represents a WebRTC connection between the local computer and a remote peer.
8
18
  * This is a DataChannel-only implementation ported from Chromium.
@@ -25,7 +35,8 @@ class RTCPeerConnection extends EventEmitter {
25
35
 
26
36
  // Native peer connection (would be native WebRTC binding)
27
37
  this._nativePeerConnection = null;
28
- this._nativePeerConnectionFactory = nativePeerConnectionFactory;
38
+ // Use provided factory or default factory
39
+ this._nativePeerConnectionFactory = nativePeerConnectionFactory || getDefaultFactory();
29
40
 
30
41
  // Initialize native peer connection
31
42
  this._initializeNativePeerConnection();
package/src/index.d.ts ADDED
@@ -0,0 +1,229 @@
1
+ // Type definitions for node-rtc-connection
2
+ // Project: https://github.com/nmhung1210/nodertc
3
+ // Definitions by: nmhung1210
4
+
5
+ /// <reference types="node" />
6
+
7
+ import { EventEmitter } from 'events';
8
+
9
+ // RTCConfiguration
10
+ export interface RTCIceServer {
11
+ urls: string | string[];
12
+ username?: string;
13
+ credential?: string;
14
+ credentialType?: 'password' | 'oauth';
15
+ }
16
+
17
+ export interface RTCConfiguration {
18
+ iceServers?: RTCIceServer[];
19
+ iceTransportPolicy?: 'all' | 'relay';
20
+ bundlePolicy?: 'balanced' | 'max-compat' | 'max-bundle';
21
+ rtcpMuxPolicy?: 'negotiate' | 'require';
22
+ iceCandidatePoolSize?: number;
23
+ }
24
+
25
+ // RTCSessionDescription
26
+ export type RTCSdpType = 'offer' | 'answer' | 'pranswer' | 'rollback';
27
+
28
+ export interface RTCSessionDescriptionInit {
29
+ type: RTCSdpType;
30
+ sdp?: string;
31
+ }
32
+
33
+ export class RTCSessionDescription {
34
+ constructor(descriptionInitDict?: RTCSessionDescriptionInit);
35
+ readonly type: RTCSdpType;
36
+ readonly sdp: string;
37
+ toJSON(): RTCSessionDescriptionInit;
38
+ }
39
+
40
+ // RTCIceCandidate
41
+ export type RTCIceCandidateType = 'host' | 'srflx' | 'prflx' | 'relay';
42
+ export type RTCIceProtocol = 'udp' | 'tcp';
43
+ export type RTCIceTcpCandidateType = 'active' | 'passive' | 'so';
44
+
45
+ export interface RTCIceCandidateInit {
46
+ candidate?: string;
47
+ sdpMid?: string | null;
48
+ sdpMLineIndex?: number | null;
49
+ usernameFragment?: string | null;
50
+ }
51
+
52
+ export class RTCIceCandidate {
53
+ constructor(candidateInitDict?: RTCIceCandidateInit);
54
+ readonly candidate: string;
55
+ readonly sdpMid: string | null;
56
+ readonly sdpMLineIndex: number | null;
57
+ readonly foundation: string | null;
58
+ readonly component: 'rtp' | 'rtcp' | null;
59
+ readonly priority: number | null;
60
+ readonly address: string | null;
61
+ readonly protocol: RTCIceProtocol | null;
62
+ readonly port: number | null;
63
+ readonly type: RTCIceCandidateType | null;
64
+ readonly tcpType: RTCIceTcpCandidateType | null;
65
+ readonly relatedAddress: string | null;
66
+ readonly relatedPort: number | null;
67
+ readonly usernameFragment: string | null;
68
+ toJSON(): RTCIceCandidateInit;
69
+ }
70
+
71
+ // RTCDataChannel
72
+ export type RTCDataChannelState = 'connecting' | 'open' | 'closing' | 'closed';
73
+ export type RTCBinaryType = 'blob' | 'arraybuffer';
74
+
75
+ export interface RTCDataChannelInit {
76
+ ordered?: boolean;
77
+ maxPacketLifeTime?: number;
78
+ maxRetransmits?: number;
79
+ protocol?: string;
80
+ negotiated?: boolean;
81
+ id?: number;
82
+ }
83
+
84
+ export interface RTCDataChannelEventMap {
85
+ open: Event;
86
+ message: MessageEvent;
87
+ error: RTCErrorEvent;
88
+ close: Event;
89
+ bufferedamountlow: Event;
90
+ }
91
+
92
+ export class RTCDataChannel extends EventEmitter {
93
+ readonly label: string;
94
+ readonly ordered: boolean;
95
+ readonly maxPacketLifeTime: number | null;
96
+ readonly maxRetransmits: number | null;
97
+ readonly protocol: string;
98
+ readonly negotiated: boolean;
99
+ readonly id: number | null;
100
+ readonly readyState: RTCDataChannelState;
101
+ readonly bufferedAmount: number;
102
+ bufferedAmountLowThreshold: number;
103
+ binaryType: RTCBinaryType;
104
+
105
+ close(): void;
106
+ send(data: string | ArrayBuffer | ArrayBufferView): void;
107
+
108
+ on<K extends keyof RTCDataChannelEventMap>(event: K, listener: (ev: RTCDataChannelEventMap[K]) => void): this;
109
+ once<K extends keyof RTCDataChannelEventMap>(event: K, listener: (ev: RTCDataChannelEventMap[K]) => void): this;
110
+ off<K extends keyof RTCDataChannelEventMap>(event: K, listener: (ev: RTCDataChannelEventMap[K]) => void): this;
111
+ emit<K extends keyof RTCDataChannelEventMap>(event: K, ...args: any[]): boolean;
112
+ }
113
+
114
+ // RTCPeerConnection
115
+ export type RTCSignalingState = 'stable' | 'have-local-offer' | 'have-remote-offer' |
116
+ 'have-local-pranswer' | 'have-remote-pranswer' | 'closed';
117
+
118
+ export type RTCIceGatheringState = 'new' | 'gathering' | 'complete';
119
+
120
+ export type RTCIceConnectionState = 'new' | 'checking' | 'connected' | 'completed' |
121
+ 'failed' | 'disconnected' | 'closed';
122
+
123
+ export type RTCPeerConnectionState = 'new' | 'connecting' | 'connected' |
124
+ 'disconnected' | 'failed' | 'closed';
125
+
126
+ export interface RTCOfferOptions {
127
+ iceRestart?: boolean;
128
+ offerToReceiveAudio?: boolean;
129
+ offerToReceiveVideo?: boolean;
130
+ }
131
+
132
+ export interface RTCAnswerOptions {
133
+ iceRestart?: boolean;
134
+ }
135
+
136
+ export interface RTCDataChannelEventInit {
137
+ channel: RTCDataChannel;
138
+ }
139
+
140
+ export class RTCDataChannelEvent extends Event {
141
+ constructor(type: string, eventInitDict: RTCDataChannelEventInit);
142
+ readonly channel: RTCDataChannel;
143
+ }
144
+
145
+ export interface RTCPeerConnectionIceEventInit {
146
+ candidate?: RTCIceCandidate | null;
147
+ url?: string | null;
148
+ }
149
+
150
+ export class RTCPeerConnectionIceEvent extends Event {
151
+ constructor(type: string, eventInitDict?: RTCPeerConnectionIceEventInit);
152
+ readonly candidate: RTCIceCandidate | null;
153
+ readonly url: string | null;
154
+ }
155
+
156
+ export interface RTCPeerConnectionEventMap {
157
+ connectionstatechange: Event;
158
+ datachannel: RTCDataChannelEvent;
159
+ icecandidate: RTCPeerConnectionIceEvent;
160
+ icecandidateerror: Event;
161
+ iceconnectionstatechange: Event;
162
+ icegatheringstatechange: Event;
163
+ negotiationneeded: Event;
164
+ signalingstatechange: Event;
165
+ }
166
+
167
+ export class RTCPeerConnection extends EventEmitter {
168
+ constructor(configuration?: RTCConfiguration, factory?: NativePeerConnectionFactory);
169
+
170
+ readonly signalingState: RTCSignalingState;
171
+ readonly iceGatheringState: RTCIceGatheringState;
172
+ readonly iceConnectionState: RTCIceConnectionState;
173
+ readonly connectionState: RTCPeerConnectionState;
174
+ readonly localDescription: RTCSessionDescription | null;
175
+ readonly remoteDescription: RTCSessionDescription | null;
176
+ readonly pendingLocalDescription: RTCSessionDescription | null;
177
+ readonly pendingRemoteDescription: RTCSessionDescription | null;
178
+ readonly currentLocalDescription: RTCSessionDescription | null;
179
+ readonly currentRemoteDescription: RTCSessionDescription | null;
180
+
181
+ createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescription>;
182
+ createAnswer(options?: RTCAnswerOptions): Promise<RTCSessionDescription>;
183
+ setLocalDescription(description: RTCSessionDescriptionInit): Promise<void>;
184
+ setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void>;
185
+ addIceCandidate(candidate?: RTCIceCandidateInit | null): Promise<void>;
186
+ createDataChannel(label: string, dataChannelDict?: RTCDataChannelInit): RTCDataChannel;
187
+ getConfiguration(): RTCConfiguration;
188
+ setConfiguration(configuration: RTCConfiguration): void;
189
+ close(): void;
190
+ getStats(): Promise<any>;
191
+
192
+ on<K extends keyof RTCPeerConnectionEventMap>(event: K, listener: (ev: RTCPeerConnectionEventMap[K]) => void): this;
193
+ once<K extends keyof RTCPeerConnectionEventMap>(event: K, listener: (ev: RTCPeerConnectionEventMap[K]) => void): this;
194
+ off<K extends keyof RTCPeerConnectionEventMap>(event: K, listener: (ev: RTCPeerConnectionEventMap[K]) => void): this;
195
+ emit<K extends keyof RTCPeerConnectionEventMap>(event: K, ...args: any[]): boolean;
196
+ }
197
+
198
+ // RTCError
199
+ export class RTCError extends Error {
200
+ constructor(message: string, errorDetail?: string);
201
+ readonly errorDetail: string;
202
+ }
203
+
204
+ export interface RTCErrorEventInit {
205
+ error: RTCError;
206
+ }
207
+
208
+ export class RTCErrorEvent extends Event {
209
+ constructor(type: string, eventInitDict: RTCErrorEventInit);
210
+ readonly error: RTCError;
211
+ }
212
+
213
+ // NativePeerConnectionFactory
214
+ export class NativePeerConnectionFactory {
215
+ constructor();
216
+ initialize(): void;
217
+ createPeerConnection(configuration: RTCConfiguration): any;
218
+ dispose(): void;
219
+ }
220
+
221
+ // Factory functions
222
+ export function createPeerConnection(configuration?: RTCConfiguration): RTCPeerConnection;
223
+ export function createPeerConnectionWithFactory(configuration: RTCConfiguration, factory: NativePeerConnectionFactory): RTCPeerConnection;
224
+
225
+ // Alias
226
+ export { RTCPeerConnection as RTCConnection };
227
+
228
+ // Default factory instance
229
+ export const defaultFactory: NativePeerConnectionFactory;
package/src/index.js CHANGED
@@ -40,6 +40,7 @@ function createPeerConnectionWithFactory(configuration, factory) {
40
40
  module.exports = {
41
41
  // Main API
42
42
  RTCPeerConnection,
43
+ RTCConnection: RTCPeerConnection, // Alias for convenience
43
44
  createPeerConnection,
44
45
  createPeerConnectionWithFactory,
45
46