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.
- package/README.md +355 -289
- package/dist/index.cjs +4318 -3095
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +4318 -3095
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/datachannel/RTCDataChannel.js +354 -0
- package/src/dtls/RTCCertificate.js +310 -0
- package/src/dtls/RTCDtlsTransport.js +247 -0
- package/src/foundation/ByteBufferQueue.js +235 -0
- package/src/foundation/RTCError.js +226 -0
- package/src/ice/RTCIceCandidate.js +301 -0
- package/src/ice/RTCIceTransport.js +956 -0
- package/src/index.d.ts +316 -145
- package/src/index.js +78 -45
- package/src/network/network-transport.js +478 -0
- package/src/peerconnection/RTCPeerConnection.js +847 -0
- package/src/sctp/RTCSctpTransport.js +253 -0
- package/src/sdp/RTCSessionDescription.js +102 -0
- package/src/sdp/sdp-utils.js +224 -0
- package/src/stun/stun-client.js +643 -0
- package/src/ICEGatherer.js +0 -341
- package/src/NativePeerConnectionFactory.js +0 -1044
- package/src/RTCDataChannel.js +0 -346
- package/src/RTCDataChannelEvent.js +0 -50
- package/src/RTCError.js +0 -66
- package/src/RTCIceCandidate.js +0 -184
- package/src/RTCPeerConnection.js +0 -505
- package/src/RTCPeerConnectionIceEvent.js +0 -58
- package/src/RTCSessionDescription.js +0 -62
- package/src/STUNClient.js +0 -222
- package/src/SecureConnection.js +0 -298
- package/src/TURNClient.js +0 -561
- package/src/UDPTransport.js +0 -236
|
@@ -1,1044 +0,0 @@
|
|
|
1
|
-
const EventEmitter = require('events');
|
|
2
|
-
const net = require('net');
|
|
3
|
-
const dgram = require('dgram');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const STUNClient = require('./STUNClient');
|
|
7
|
-
const ICEGatherer = require('./ICEGatherer');
|
|
8
|
-
const { SecureConnection } = require('./SecureConnection');
|
|
9
|
-
const UDPTransport = require('./UDPTransport');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* NativePeerConnectionFactory creates native peer connection instances.
|
|
13
|
-
* Real implementation using Node.js net package for peer-to-peer communication.
|
|
14
|
-
*/
|
|
15
|
-
class NativePeerConnectionFactory {
|
|
16
|
-
constructor() {
|
|
17
|
-
this._initialized = false;
|
|
18
|
-
this._peerConnections = new Set();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Initialize the factory
|
|
23
|
-
*/
|
|
24
|
-
initialize() {
|
|
25
|
-
if (this._initialized) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
console.log('[NativePeerConnectionFactory] Initializing with Node.js net package');
|
|
30
|
-
this._initialized = true;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Create a native peer connection
|
|
35
|
-
* @param {Object} configuration - RTCConfiguration
|
|
36
|
-
* @returns {NativePeerConnection}
|
|
37
|
-
*/
|
|
38
|
-
createPeerConnection(configuration) {
|
|
39
|
-
if (!this._initialized) {
|
|
40
|
-
this.initialize();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const nativePC = new NativePeerConnection(configuration);
|
|
44
|
-
this._peerConnections.add(nativePC);
|
|
45
|
-
|
|
46
|
-
nativePC.on('close', () => {
|
|
47
|
-
this._peerConnections.delete(nativePC);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return nativePC;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Dispose of the factory and all peer connections
|
|
55
|
-
*/
|
|
56
|
-
dispose() {
|
|
57
|
-
for (const pc of this._peerConnections) {
|
|
58
|
-
pc.close();
|
|
59
|
-
}
|
|
60
|
-
this._peerConnections.clear();
|
|
61
|
-
this._initialized = false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* NativePeerConnection - Real implementation using Node.js net package
|
|
67
|
-
* Implements peer-to-peer connection using TCP for data channels
|
|
68
|
-
*/
|
|
69
|
-
class NativePeerConnection extends EventEmitter {
|
|
70
|
-
constructor(configuration) {
|
|
71
|
-
super();
|
|
72
|
-
this._configuration = configuration || {};
|
|
73
|
-
this._signalingState = 0; // stable
|
|
74
|
-
this._iceConnectionState = 0; // new
|
|
75
|
-
this._iceGatheringState = 0; // new
|
|
76
|
-
this._connectionState = 0; // new
|
|
77
|
-
this._localDescription = null;
|
|
78
|
-
this._remoteDescription = null;
|
|
79
|
-
this._dataChannels = new Map();
|
|
80
|
-
this._closed = false;
|
|
81
|
-
|
|
82
|
-
// Networking components
|
|
83
|
-
this._server = null;
|
|
84
|
-
this._socket = null;
|
|
85
|
-
this._localAddress = null;
|
|
86
|
-
this._localPort = null;
|
|
87
|
-
this._remoteAddress = null;
|
|
88
|
-
this._remotePort = null;
|
|
89
|
-
this._isOfferer = false;
|
|
90
|
-
this._iceUsername = this._generateRandomString(8);
|
|
91
|
-
this._icePassword = this._generateRandomString(24);
|
|
92
|
-
this._fingerprint = this._generateFingerprint();
|
|
93
|
-
|
|
94
|
-
// New features
|
|
95
|
-
this._useEncryption = this._configuration.encryption === true; // Disabled by default
|
|
96
|
-
this._useUDP = this._configuration.transport === 'udp';
|
|
97
|
-
this._iceGatherer = new ICEGatherer({
|
|
98
|
-
stunServers: this._extractSTUNServers(this._configuration),
|
|
99
|
-
turnServers: this._extractTURNServers(this._configuration)
|
|
100
|
-
});
|
|
101
|
-
this._secureConnection = null;
|
|
102
|
-
this._udpTransport = null;
|
|
103
|
-
this._iceCandidates = [];
|
|
104
|
-
this._remoteCandidates = [];
|
|
105
|
-
this._selectedLocalCandidate = null;
|
|
106
|
-
this._selectedRemoteCandidate = null;
|
|
107
|
-
|
|
108
|
-
console.log('[NativePeerConnection] Created with STUN/ICE/Encryption support');
|
|
109
|
-
console.log(` - Encryption: ${this._useEncryption ? 'enabled' : 'disabled (requires valid certs)'}`);
|
|
110
|
-
console.log(` - Transport: ${this._useUDP ? 'UDP' : 'TCP'}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Extract STUN servers from configuration
|
|
115
|
-
* @private
|
|
116
|
-
*/
|
|
117
|
-
_extractSTUNServers(config) {
|
|
118
|
-
const stunServers = [];
|
|
119
|
-
|
|
120
|
-
if (config.iceServers) {
|
|
121
|
-
for (const server of config.iceServers) {
|
|
122
|
-
if (server.urls) {
|
|
123
|
-
const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
|
|
124
|
-
for (const url of urls) {
|
|
125
|
-
if (url.startsWith('stun:')) {
|
|
126
|
-
stunServers.push(url.replace('stun:', ''));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return stunServers.length > 0 ? stunServers : undefined;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Extract TURN servers from configuration
|
|
138
|
-
* @private
|
|
139
|
-
*/
|
|
140
|
-
_extractTURNServers(config) {
|
|
141
|
-
const turnServers = [];
|
|
142
|
-
|
|
143
|
-
if (config.iceServers) {
|
|
144
|
-
for (const server of config.iceServers) {
|
|
145
|
-
if (server.urls) {
|
|
146
|
-
const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
|
|
147
|
-
for (const url of urls) {
|
|
148
|
-
if (url.startsWith('turn:')) {
|
|
149
|
-
turnServers.push({
|
|
150
|
-
urls: url,
|
|
151
|
-
username: server.username,
|
|
152
|
-
credential: server.credential
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (turnServers.length > 0) {
|
|
161
|
-
console.log(`[NativePeerConnection] Configured ${turnServers.length} TURN server(s)`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return turnServers;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Create an offer
|
|
169
|
-
* @param {Object} options
|
|
170
|
-
* @returns {Promise<{type: string, sdp: string}>}
|
|
171
|
-
*/
|
|
172
|
-
async createOffer(options) {
|
|
173
|
-
if (this._closed) {
|
|
174
|
-
throw new Error('PeerConnection is closed');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
this._isOfferer = true;
|
|
178
|
-
|
|
179
|
-
// Create TCP server to listen for incoming connections
|
|
180
|
-
await this._createServer();
|
|
181
|
-
|
|
182
|
-
// Generate real SDP with actual network information
|
|
183
|
-
const sdp = this._generateSDP('offer');
|
|
184
|
-
return { type: 'offer', sdp };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Create an answer
|
|
189
|
-
* @param {Object} options
|
|
190
|
-
* @returns {Promise<{type: string, sdp: string}>}
|
|
191
|
-
*/
|
|
192
|
-
async createAnswer(options) {
|
|
193
|
-
if (this._closed) {
|
|
194
|
-
throw new Error('PeerConnection is closed');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!this._remoteDescription) {
|
|
198
|
-
throw new Error('No remote description set');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this._isOfferer = false;
|
|
202
|
-
|
|
203
|
-
// Create TCP server to listen for incoming connections (if needed)
|
|
204
|
-
if (!this._server) {
|
|
205
|
-
await this._createServer();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Generate real SDP with actual network information
|
|
209
|
-
const sdp = this._generateSDP('answer');
|
|
210
|
-
return { type: 'answer', sdp };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Set local description
|
|
215
|
-
* @param {Object} description
|
|
216
|
-
* @returns {Promise<void>}
|
|
217
|
-
*/
|
|
218
|
-
async setLocalDescription(description) {
|
|
219
|
-
if (this._closed) {
|
|
220
|
-
throw new Error('PeerConnection is closed');
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
this._localDescription = description;
|
|
224
|
-
|
|
225
|
-
// Update signaling state
|
|
226
|
-
if (description.type === 'offer') {
|
|
227
|
-
this._signalingState = 1; // have-local-offer
|
|
228
|
-
this.emit('signalingstatechange', this._signalingState);
|
|
229
|
-
} else if (description.type === 'answer') {
|
|
230
|
-
this._signalingState = 0; // stable
|
|
231
|
-
this.emit('signalingstatechange', this._signalingState);
|
|
232
|
-
|
|
233
|
-
// If we're answerer and have remote description, connect
|
|
234
|
-
if (!this._isOfferer && this._remoteDescription) {
|
|
235
|
-
await this._connectToPeer();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Start real ICE gathering
|
|
240
|
-
this._startIceGathering();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Set remote description
|
|
245
|
-
* @param {Object} description
|
|
246
|
-
* @returns {Promise<void>}
|
|
247
|
-
*/
|
|
248
|
-
async setRemoteDescription(description) {
|
|
249
|
-
if (this._closed) {
|
|
250
|
-
throw new Error('PeerConnection is closed');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Parse remote SDP to extract connection information
|
|
254
|
-
this._parseSDP(description.sdp);
|
|
255
|
-
this._remoteDescription = description;
|
|
256
|
-
|
|
257
|
-
// Update signaling state
|
|
258
|
-
if (description.type === 'offer') {
|
|
259
|
-
this._signalingState = 2; // have-remote-offer
|
|
260
|
-
this.emit('signalingstatechange', this._signalingState);
|
|
261
|
-
} else if (description.type === 'answer') {
|
|
262
|
-
this._signalingState = 0; // stable
|
|
263
|
-
this.emit('signalingstatechange', this._signalingState);
|
|
264
|
-
|
|
265
|
-
// If we're offerer and have local description, connect
|
|
266
|
-
if (this._isOfferer && this._localDescription) {
|
|
267
|
-
await this._connectToPeer();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Add ICE candidate
|
|
274
|
-
* @param {Object} candidate
|
|
275
|
-
* @returns {Promise<void>}
|
|
276
|
-
*/
|
|
277
|
-
async addIceCandidate(candidate) {
|
|
278
|
-
if (this._closed) {
|
|
279
|
-
throw new Error('PeerConnection is closed');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!candidate || !candidate.candidate) {
|
|
283
|
-
// null candidate signals end of candidates - try to select best pair
|
|
284
|
-
if (this._remoteCandidates.length > 0 && !this._selectedRemoteCandidate) {
|
|
285
|
-
this._selectBestCandidatePair();
|
|
286
|
-
}
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Parse ICE candidate
|
|
291
|
-
const parsed = this._parseIceCandidate(candidate.candidate);
|
|
292
|
-
if (!parsed) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Store the remote candidate
|
|
297
|
-
this._remoteCandidates.push(parsed);
|
|
298
|
-
console.log(`[NativePeerConnection] Added remote ICE candidate (${parsed.type}): ${parsed.ip}:${parsed.port}`);
|
|
299
|
-
|
|
300
|
-
// For backward compatibility, set remote address immediately if not set
|
|
301
|
-
if (!this._remoteAddress) {
|
|
302
|
-
this._remoteAddress = parsed.ip;
|
|
303
|
-
this._remotePort = parsed.port;
|
|
304
|
-
console.log(`[NativePeerConnection] Remote address: ${this._remoteAddress}:${this._remotePort}`);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// If we haven't selected a pair yet, try to select now
|
|
308
|
-
if (!this._selectedRemoteCandidate && this._iceCandidates.length > 0) {
|
|
309
|
-
this._selectBestCandidatePair();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// If we selected a pair and are offerer, connect
|
|
313
|
-
if (this._selectedRemoteCandidate && this._isOfferer &&
|
|
314
|
-
this._signalingState === 0 && !this._socket) {
|
|
315
|
-
await this._connectToPeer();
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Parse ICE candidate string
|
|
321
|
-
* @private
|
|
322
|
-
*/
|
|
323
|
-
_parseIceCandidate(candidateStr) {
|
|
324
|
-
const parts = candidateStr.split(' ');
|
|
325
|
-
if (parts.length < 6) {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
foundation: parts[0].replace('candidate:', ''),
|
|
331
|
-
component: parts[1],
|
|
332
|
-
protocol: parts[2],
|
|
333
|
-
priority: parseInt(parts[3], 10),
|
|
334
|
-
ip: parts[4],
|
|
335
|
-
port: parseInt(parts[5], 10),
|
|
336
|
-
type: parts[7], // typ host/srflx/relay
|
|
337
|
-
relatedAddress: parts[9] || null,
|
|
338
|
-
relatedPort: parts[11] ? parseInt(parts[11], 10) : null
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Select best candidate pair for connection
|
|
344
|
-
* Prioritizes: relay > srflx > host
|
|
345
|
-
* @private
|
|
346
|
-
*/
|
|
347
|
-
_selectBestCandidatePair() {
|
|
348
|
-
if (this._remoteCandidates.length === 0 || this._iceCandidates.length === 0) {
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Priority order: relay > srflx > host
|
|
353
|
-
const typePriority = { 'relay': 3, 'srflx': 2, 'host': 1 };
|
|
354
|
-
|
|
355
|
-
// Find best local candidate
|
|
356
|
-
let bestLocal = this._iceCandidates[0];
|
|
357
|
-
for (const candidate of this._iceCandidates) {
|
|
358
|
-
if (typePriority[candidate.type] > typePriority[bestLocal.type]) {
|
|
359
|
-
bestLocal = candidate;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Find best remote candidate
|
|
364
|
-
let bestRemote = this._remoteCandidates[0];
|
|
365
|
-
for (const candidate of this._remoteCandidates) {
|
|
366
|
-
if (typePriority[candidate.type] > typePriority[bestRemote.type]) {
|
|
367
|
-
bestRemote = candidate;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
this._selectedLocalCandidate = bestLocal;
|
|
372
|
-
this._selectedRemoteCandidate = bestRemote;
|
|
373
|
-
|
|
374
|
-
// Update addresses for connection
|
|
375
|
-
this._localAddress = bestLocal.ip;
|
|
376
|
-
this._localPort = bestLocal.port;
|
|
377
|
-
this._remoteAddress = bestRemote.ip;
|
|
378
|
-
this._remotePort = bestRemote.port;
|
|
379
|
-
|
|
380
|
-
console.log(`[NativePeerConnection] Selected candidate pair:`);
|
|
381
|
-
console.log(` Local: ${bestLocal.type} ${this._localAddress}:${this._localPort}`);
|
|
382
|
-
console.log(` Remote: ${bestRemote.type} ${this._remoteAddress}:${this._remotePort}`);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Create a data channel
|
|
387
|
-
* @param {string} label
|
|
388
|
-
* @param {Object} options
|
|
389
|
-
* @returns {NativeDataChannel}
|
|
390
|
-
*/
|
|
391
|
-
createDataChannel(label, options) {
|
|
392
|
-
if (this._closed) {
|
|
393
|
-
throw new Error('PeerConnection is closed');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const channel = new NativeDataChannel(label, options, this);
|
|
397
|
-
this._dataChannels.set(label, channel);
|
|
398
|
-
|
|
399
|
-
// If socket is already connected, open the channel
|
|
400
|
-
if (this._socket && this._socket.writable) {
|
|
401
|
-
setTimeout(() => channel._setConnected(this._socket), 0);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Emit negotiation needed
|
|
405
|
-
setTimeout(() => {
|
|
406
|
-
this.emit('negotiationneeded');
|
|
407
|
-
}, 0);
|
|
408
|
-
|
|
409
|
-
return channel;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Set configuration
|
|
414
|
-
* @param {Object} configuration
|
|
415
|
-
*/
|
|
416
|
-
setConfiguration(configuration) {
|
|
417
|
-
this._configuration = configuration;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Get stats
|
|
422
|
-
* @returns {Promise<Object>}
|
|
423
|
-
*/
|
|
424
|
-
async getStats() {
|
|
425
|
-
return {
|
|
426
|
-
type: 'peer-connection',
|
|
427
|
-
timestamp: Date.now(),
|
|
428
|
-
// Mock stats
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Close the peer connection
|
|
434
|
-
*/
|
|
435
|
-
close() {
|
|
436
|
-
if (this._closed) {
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
this._closed = true;
|
|
441
|
-
this._signalingState = 5; // closed
|
|
442
|
-
|
|
443
|
-
// Close all data channels
|
|
444
|
-
for (const [label, channel] of this._dataChannels) {
|
|
445
|
-
channel.close();
|
|
446
|
-
}
|
|
447
|
-
this._dataChannels.clear();
|
|
448
|
-
|
|
449
|
-
// Close secure connection
|
|
450
|
-
if (this._secureConnection) {
|
|
451
|
-
this._secureConnection.close();
|
|
452
|
-
this._secureConnection = null;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Close UDP transport
|
|
456
|
-
if (this._udpTransport) {
|
|
457
|
-
this._udpTransport.close();
|
|
458
|
-
this._udpTransport = null;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Close socket
|
|
462
|
-
if (this._socket) {
|
|
463
|
-
this._socket.destroy();
|
|
464
|
-
this._socket = null;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Close server
|
|
468
|
-
if (this._server) {
|
|
469
|
-
this._server.close();
|
|
470
|
-
this._server = null;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
this.emit('signalingstatechange', this._signalingState);
|
|
474
|
-
this.emit('close');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Create TCP server to listen for incoming connections
|
|
479
|
-
* @private
|
|
480
|
-
*/
|
|
481
|
-
async _createServer() {
|
|
482
|
-
return new Promise((resolve, reject) => {
|
|
483
|
-
this._server = net.createServer((socket) => {
|
|
484
|
-
console.log('[NativePeerConnection] Incoming connection accepted');
|
|
485
|
-
this._handleIncomingConnection(socket);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
this._server.listen(0, '0.0.0.0', () => {
|
|
489
|
-
const address = this._server.address();
|
|
490
|
-
this._localPort = address.port;
|
|
491
|
-
this._localAddress = this._getLocalIPAddress();
|
|
492
|
-
console.log(`[NativePeerConnection] Server listening on ${this._localAddress}:${this._localPort}`);
|
|
493
|
-
resolve();
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
this._server.on('error', (err) => {
|
|
497
|
-
console.error('[NativePeerConnection] Server error:', err);
|
|
498
|
-
reject(err);
|
|
499
|
-
});
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Connect to remote peer
|
|
505
|
-
* @private
|
|
506
|
-
*/
|
|
507
|
-
async _connectToPeer() {
|
|
508
|
-
if (!this._remoteAddress || !this._remotePort) {
|
|
509
|
-
console.log('[NativePeerConnection] Cannot connect: missing remote address');
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (this._socket) {
|
|
514
|
-
console.log('[NativePeerConnection] Already connected');
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Check if we're using relay candidates - if so, skip tie-breaking
|
|
519
|
-
const usingRelay = this._selectedLocalCandidate?.type === 'relay' ||
|
|
520
|
-
this._selectedRemoteCandidate?.type === 'relay';
|
|
521
|
-
|
|
522
|
-
if (!usingRelay) {
|
|
523
|
-
// Tie-breaking: only connect if our port is higher than remote port
|
|
524
|
-
// This ensures only one peer connects, avoiding the race condition
|
|
525
|
-
// Note: This only works for direct connections (host/srflx)
|
|
526
|
-
if (this._localPort < this._remotePort) {
|
|
527
|
-
console.log(`[NativePeerConnection] Not connecting (local port ${this._localPort} < remote port ${this._remotePort}), waiting for incoming`);
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
console.log(`[NativePeerConnection] Connecting to ${this._remoteAddress}:${this._remotePort}`);
|
|
533
|
-
if (usingRelay) {
|
|
534
|
-
console.log(`[NativePeerConnection] Using TURN relay connection`);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
this._socket = new net.Socket();
|
|
538
|
-
|
|
539
|
-
this._socket.connect(this._remotePort, this._remoteAddress, async () => {
|
|
540
|
-
console.log('[NativePeerConnection] Connected to peer');
|
|
541
|
-
|
|
542
|
-
// Apply TLS encryption if enabled
|
|
543
|
-
if (this._useEncryption) {
|
|
544
|
-
await this._wrapWithEncryption(false);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
this._iceConnectionState = 2; // connected
|
|
548
|
-
this.emit('iceconnectionstatechange', this._iceConnectionState);
|
|
549
|
-
|
|
550
|
-
// Announce existing data channels to peer and open them
|
|
551
|
-
setImmediate(() => {
|
|
552
|
-
for (const [label, channel] of this._dataChannels) {
|
|
553
|
-
this._sendChannelAnnouncement(label);
|
|
554
|
-
channel._setConnected(this._getActiveSocket());
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
this._setupSocketHandlers(this._socket);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Handle incoming connection from peer
|
|
564
|
-
* @private
|
|
565
|
-
*/
|
|
566
|
-
async _handleIncomingConnection(socket) {
|
|
567
|
-
if (this._socket) {
|
|
568
|
-
console.log('[NativePeerConnection] Connection already exists, closing new one');
|
|
569
|
-
socket.destroy();
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
console.log('[NativePeerConnection] Accepted connection from peer');
|
|
574
|
-
|
|
575
|
-
// Setup handlers BEFORE storing socket to avoid missing early data
|
|
576
|
-
this._setupSocketHandlers(socket);
|
|
577
|
-
|
|
578
|
-
this._socket = socket;
|
|
579
|
-
|
|
580
|
-
// Apply TLS encryption if enabled
|
|
581
|
-
if (this._useEncryption) {
|
|
582
|
-
await this._wrapWithEncryption(true);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
this._iceConnectionState = 2; // connected
|
|
586
|
-
this.emit('iceconnectionstatechange', this._iceConnectionState);
|
|
587
|
-
|
|
588
|
-
// Announce existing data channels to peer
|
|
589
|
-
setImmediate(() => {
|
|
590
|
-
for (const [label, channel] of this._dataChannels) {
|
|
591
|
-
this._sendChannelAnnouncement(label);
|
|
592
|
-
channel._setConnected(this._getActiveSocket());
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Wrap socket with TLS encryption
|
|
599
|
-
* @private
|
|
600
|
-
*/
|
|
601
|
-
async _wrapWithEncryption(isServer) {
|
|
602
|
-
try {
|
|
603
|
-
console.log(`[NativePeerConnection] Enabling TLS encryption (${isServer ? 'server' : 'client'})`);
|
|
604
|
-
|
|
605
|
-
this._secureConnection = new SecureConnection(this._socket, { isServer });
|
|
606
|
-
await this._secureConnection.wrap();
|
|
607
|
-
|
|
608
|
-
// Update fingerprint with real certificate
|
|
609
|
-
this._fingerprint = this._secureConnection.getFingerprint();
|
|
610
|
-
|
|
611
|
-
console.log('[NativePeerConnection] TLS encryption enabled');
|
|
612
|
-
} catch (err) {
|
|
613
|
-
console.error('[NativePeerConnection] Failed to enable encryption:', err);
|
|
614
|
-
throw err;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Get the active socket (secure or plain)
|
|
620
|
-
* @private
|
|
621
|
-
*/
|
|
622
|
-
_getActiveSocket() {
|
|
623
|
-
return this._secureConnection?.secureSocket || this._socket;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Setup socket event handlers
|
|
628
|
-
* @private
|
|
629
|
-
*/
|
|
630
|
-
_setupSocketHandlers(socket) {
|
|
631
|
-
socket.on('data', (data) => {
|
|
632
|
-
this._handleIncomingData(data);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
socket.on('close', () => {
|
|
636
|
-
console.log('[NativePeerConnection] Connection closed');
|
|
637
|
-
this._iceConnectionState = 6; // closed
|
|
638
|
-
this.emit('iceconnectionstatechange', this._iceConnectionState);
|
|
639
|
-
|
|
640
|
-
// Close all data channels
|
|
641
|
-
for (const [label, channel] of this._dataChannels) {
|
|
642
|
-
channel._handleDisconnect();
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
socket.on('error', (err) => {
|
|
647
|
-
console.error('[NativePeerConnection] Socket error:', err);
|
|
648
|
-
this._iceConnectionState = 4; // failed
|
|
649
|
-
this.emit('iceconnectionstatechange', this._iceConnectionState);
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Send channel announcement to peer (empty message to trigger remote channel creation)
|
|
655
|
-
* @private
|
|
656
|
-
*/
|
|
657
|
-
_sendChannelAnnouncement(label) {
|
|
658
|
-
if (!this._socket) {
|
|
659
|
-
console.log(`[NativePeerConnection] Cannot announce ${label}: no socket`);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const labelBuffer = Buffer.from(label, 'utf8');
|
|
664
|
-
const labelLength = labelBuffer.length;
|
|
665
|
-
const emptyData = Buffer.alloc(0);
|
|
666
|
-
|
|
667
|
-
// Message format: <length:4><label-length:2><label><data>
|
|
668
|
-
// length is the number of bytes after the length field
|
|
669
|
-
const totalLength = 2 + labelLength + emptyData.length;
|
|
670
|
-
const buffer = Buffer.allocUnsafe(4 + totalLength);
|
|
671
|
-
|
|
672
|
-
buffer.writeUInt32BE(totalLength, 0);
|
|
673
|
-
buffer.writeUInt16BE(labelLength, 4);
|
|
674
|
-
labelBuffer.copy(buffer, 6);
|
|
675
|
-
|
|
676
|
-
this._socket.write(buffer);
|
|
677
|
-
console.log(`[NativePeerConnection] Announced channel: ${label}`);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Handle incoming data from socket
|
|
682
|
-
* @private
|
|
683
|
-
*/
|
|
684
|
-
_handleIncomingData(data) {
|
|
685
|
-
try {
|
|
686
|
-
// Parse message format: <length:4><channel-label-length:2><channel-label><data>
|
|
687
|
-
let offset = 0;
|
|
688
|
-
|
|
689
|
-
while (offset < data.length) {
|
|
690
|
-
if (offset + 6 > data.length) break;
|
|
691
|
-
|
|
692
|
-
const totalLength = data.readUInt32BE(offset);
|
|
693
|
-
const labelLength = data.readUInt16BE(offset + 4);
|
|
694
|
-
|
|
695
|
-
if (offset + 6 + labelLength + (totalLength - 2 - labelLength) > data.length) break;
|
|
696
|
-
|
|
697
|
-
const label = data.toString('utf8', offset + 6, offset + 6 + labelLength);
|
|
698
|
-
const messageData = data.slice(offset + 6 + labelLength, offset + 6 + totalLength - 2);
|
|
699
|
-
|
|
700
|
-
// Find or create the data channel
|
|
701
|
-
let channel = this._dataChannels.get(label);
|
|
702
|
-
if (!channel) {
|
|
703
|
-
// Create remote data channel
|
|
704
|
-
console.log(`[NativePeerConnection] Creating remote channel: ${label}`);
|
|
705
|
-
channel = new NativeDataChannel(label, {}, this);
|
|
706
|
-
this._dataChannels.set(label, channel);
|
|
707
|
-
|
|
708
|
-
// Emit datachannel event first (before setting connected)
|
|
709
|
-
this.emit('datachannel', channel);
|
|
710
|
-
|
|
711
|
-
// Then set it as connected after a small delay to allow event handlers to be set up
|
|
712
|
-
setImmediate(() => {
|
|
713
|
-
channel._setConnected(this._socket);
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Deliver the message (if it has data - announcements are empty)
|
|
718
|
-
if (messageData.length > 0) {
|
|
719
|
-
channel._handleIncomingMessage(messageData);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
offset += 6 + totalLength - 2;
|
|
723
|
-
}
|
|
724
|
-
} catch (err) {
|
|
725
|
-
console.error('[NativePeerConnection] Error parsing incoming data:', err);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Generate real SDP with actual network information
|
|
731
|
-
* @private
|
|
732
|
-
*/
|
|
733
|
-
_generateSDP(type) {
|
|
734
|
-
const sessionId = Date.now();
|
|
735
|
-
const address = this._localAddress || '0.0.0.0';
|
|
736
|
-
const port = this._localPort || 9;
|
|
737
|
-
|
|
738
|
-
return `v=0
|
|
739
|
-
o=- ${sessionId} 2 IN IP4 ${address}
|
|
740
|
-
s=-
|
|
741
|
-
t=0 0
|
|
742
|
-
a=group:BUNDLE data
|
|
743
|
-
a=msid-semantic: WMS
|
|
744
|
-
m=application ${port} TCP/DTLS/SCTP webrtc-datachannel
|
|
745
|
-
c=IN IP4 ${address}
|
|
746
|
-
a=ice-ufrag:${this._iceUsername}
|
|
747
|
-
a=ice-pwd:${this._icePassword}
|
|
748
|
-
a=ice-options:trickle
|
|
749
|
-
a=fingerprint:sha-256 ${this._fingerprint}
|
|
750
|
-
a=setup:actpass
|
|
751
|
-
a=mid:data
|
|
752
|
-
a=sctp-port:5000
|
|
753
|
-
a=max-message-size:262144
|
|
754
|
-
`;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Parse SDP to extract connection information
|
|
759
|
-
* @private
|
|
760
|
-
*/
|
|
761
|
-
_parseSDP(sdp) {
|
|
762
|
-
const lines = sdp.split('\n');
|
|
763
|
-
|
|
764
|
-
for (const line of lines) {
|
|
765
|
-
// Parse connection line: c=IN IP4 <address>
|
|
766
|
-
if (line.startsWith('c=IN IP4 ')) {
|
|
767
|
-
const address = line.substring(9).trim();
|
|
768
|
-
if (address !== '0.0.0.0') {
|
|
769
|
-
this._remoteAddress = address;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Parse media line: m=application <port> ...
|
|
774
|
-
if (line.startsWith('m=application ')) {
|
|
775
|
-
const parts = line.split(' ');
|
|
776
|
-
if (parts.length >= 2) {
|
|
777
|
-
const port = parseInt(parts[1], 10);
|
|
778
|
-
if (port > 0 && port !== 9) {
|
|
779
|
-
this._remotePort = port;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
console.log(`[NativePeerConnection] Parsed SDP - Remote: ${this._remoteAddress}:${this._remotePort}`);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* Get local IP address
|
|
790
|
-
* @private
|
|
791
|
-
*/
|
|
792
|
-
_getLocalIPAddress() {
|
|
793
|
-
const interfaces = os.networkInterfaces();
|
|
794
|
-
|
|
795
|
-
// Try to find a non-internal IPv4 address first
|
|
796
|
-
for (const name of Object.keys(interfaces)) {
|
|
797
|
-
for (const iface of interfaces[name]) {
|
|
798
|
-
if (iface.family === 'IPv4' && !iface.internal) {
|
|
799
|
-
return iface.address;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Fall back to IPv6 if no IPv4 available
|
|
805
|
-
for (const name of Object.keys(interfaces)) {
|
|
806
|
-
for (const iface of interfaces[name]) {
|
|
807
|
-
if (iface.family === 'IPv6' && !iface.internal) {
|
|
808
|
-
return iface.address;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Fallback to localhost
|
|
814
|
-
return '127.0.0.1';
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/**
|
|
818
|
-
* Start ICE gathering with real network addresses
|
|
819
|
-
* @private
|
|
820
|
-
*/
|
|
821
|
-
/**
|
|
822
|
-
* Start ICE candidate gathering with STUN support
|
|
823
|
-
* @private
|
|
824
|
-
*/
|
|
825
|
-
async _startIceGathering() {
|
|
826
|
-
this._iceGatheringState = 1; // gathering
|
|
827
|
-
this.emit('icegatheringstatechange', this._iceGatheringState);
|
|
828
|
-
|
|
829
|
-
try {
|
|
830
|
-
// Use real ICE gatherer to get host and srflx candidates
|
|
831
|
-
const candidates = await this._iceGatherer.gatherCandidates(this._localPort);
|
|
832
|
-
|
|
833
|
-
console.log(`[NativePeerConnection] Gathered ${candidates.length} ICE candidates`);
|
|
834
|
-
|
|
835
|
-
// Emit each candidate
|
|
836
|
-
for (const candidate of candidates) {
|
|
837
|
-
// Parse and store the candidate
|
|
838
|
-
const parsed = this._parseIceCandidate(candidate.candidate);
|
|
839
|
-
if (parsed) {
|
|
840
|
-
this._iceCandidates.push(parsed);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
this.emit('icecandidate', {
|
|
844
|
-
candidate: candidate.candidate,
|
|
845
|
-
sdpMid: candidate.sdpMid,
|
|
846
|
-
sdpMLineIndex: candidate.sdpMLineIndex
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
console.log(`[NativePeerConnection] ICE candidate (${candidate.type}): ${candidate.ip}:${candidate.port}`);
|
|
850
|
-
}
|
|
851
|
-
} catch (err) {
|
|
852
|
-
console.warn('[NativePeerConnection] ICE gathering error:', err.message);
|
|
853
|
-
|
|
854
|
-
// Fallback: emit only local host candidate
|
|
855
|
-
if (this._localAddress && this._localPort) {
|
|
856
|
-
const candidateStr = `candidate:1 1 tcp 2130706431 ${this._localAddress} ${this._localPort} typ host`;
|
|
857
|
-
const parsed = this._parseIceCandidate(candidateStr);
|
|
858
|
-
if (parsed) {
|
|
859
|
-
this._iceCandidates.push(parsed);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
const candidate = {
|
|
863
|
-
candidate: candidateStr,
|
|
864
|
-
sdpMid: 'data',
|
|
865
|
-
sdpMLineIndex: 0
|
|
866
|
-
};
|
|
867
|
-
this.emit('icecandidate', candidate);
|
|
868
|
-
console.log(`[NativePeerConnection] Fallback ICE candidate: ${this._localAddress}:${this._localPort}`);
|
|
869
|
-
}
|
|
870
|
-
} finally {
|
|
871
|
-
// Complete gathering
|
|
872
|
-
setTimeout(() => {
|
|
873
|
-
this._iceGatheringState = 2; // complete
|
|
874
|
-
this.emit('icegatheringstatechange', this._iceGatheringState);
|
|
875
|
-
this.emit('icecandidate', null); // End of candidates
|
|
876
|
-
console.log('[NativePeerConnection] ICE gathering complete');
|
|
877
|
-
}, 100);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/**
|
|
882
|
-
* Generate random string
|
|
883
|
-
* @private
|
|
884
|
-
*/
|
|
885
|
-
_generateRandomString(length) {
|
|
886
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
887
|
-
let result = '';
|
|
888
|
-
for (let i = 0; i < length; i++) {
|
|
889
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
890
|
-
}
|
|
891
|
-
return result;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* Generate fingerprint
|
|
896
|
-
* @private
|
|
897
|
-
*/
|
|
898
|
-
_generateFingerprint() {
|
|
899
|
-
const bytes = [];
|
|
900
|
-
for (let i = 0; i < 32; i++) {
|
|
901
|
-
bytes.push(Math.floor(Math.random() * 256).toString(16).padStart(2, '0').toUpperCase());
|
|
902
|
-
}
|
|
903
|
-
return bytes.join(':');
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/**
|
|
908
|
-
* NativeDataChannel - Real implementation using TCP socket
|
|
909
|
-
*/
|
|
910
|
-
class NativeDataChannel extends EventEmitter {
|
|
911
|
-
constructor(label, options = {}, peerConnection) {
|
|
912
|
-
super();
|
|
913
|
-
this.label = label;
|
|
914
|
-
this.ordered = options.ordered !== undefined ? options.ordered : true;
|
|
915
|
-
this.maxPacketLifeTime = options.maxPacketLifeTime || -1;
|
|
916
|
-
this.maxRetransmits = options.maxRetransmits || -1;
|
|
917
|
-
this.protocol = options.protocol || '';
|
|
918
|
-
this.negotiated = options.negotiated || false;
|
|
919
|
-
this.id = options.id !== undefined ? options.id : -1;
|
|
920
|
-
|
|
921
|
-
this._state = 0; // connecting
|
|
922
|
-
this._bufferedAmount = 0;
|
|
923
|
-
this._closed = false;
|
|
924
|
-
this._socket = null;
|
|
925
|
-
this._peerConnection = peerConnection;
|
|
926
|
-
|
|
927
|
-
console.log(`[NativeDataChannel] Created: ${label}`);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* Set socket connection for this channel
|
|
932
|
-
* @private
|
|
933
|
-
*/
|
|
934
|
-
_setConnected(socket) {
|
|
935
|
-
if (this._closed) {
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
this._socket = socket;
|
|
940
|
-
this._state = 1; // open
|
|
941
|
-
this.emit('statechange', this._state);
|
|
942
|
-
console.log(`[NativeDataChannel] ${this.label} - Channel opened`);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/**
|
|
946
|
-
* Handle incoming message for this channel
|
|
947
|
-
* @private
|
|
948
|
-
*/
|
|
949
|
-
_handleIncomingMessage(data) {
|
|
950
|
-
if (this._state !== 1) {
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Check if data is binary or text
|
|
955
|
-
let binary = true;
|
|
956
|
-
try {
|
|
957
|
-
// Try to detect if it's valid UTF-8 text
|
|
958
|
-
const text = data.toString('utf8');
|
|
959
|
-
if (text.length > 0 && /^[\x20-\x7E\s]*$/.test(text)) {
|
|
960
|
-
binary = false;
|
|
961
|
-
}
|
|
962
|
-
} catch (e) {
|
|
963
|
-
binary = true;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
this.emit('message', { data, binary });
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* Handle socket disconnect
|
|
971
|
-
* @private
|
|
972
|
-
*/
|
|
973
|
-
_handleDisconnect() {
|
|
974
|
-
if (this._closed) {
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
this._socket = null;
|
|
979
|
-
this._state = 3; // closed
|
|
980
|
-
this.emit('statechange', this._state);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Send data over the channel
|
|
985
|
-
* @param {Object} dataBuffer - { data: Buffer, binary: boolean }
|
|
986
|
-
*/
|
|
987
|
-
send(dataBuffer) {
|
|
988
|
-
if (this._state !== 1) {
|
|
989
|
-
throw new Error('DataChannel is not open');
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
if (!this._socket || !this._socket.writable) {
|
|
993
|
-
throw new Error('Socket is not writable');
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
try {
|
|
997
|
-
const data = dataBuffer.data;
|
|
998
|
-
const labelBuffer = Buffer.from(this.label, 'utf8');
|
|
999
|
-
|
|
1000
|
-
// Message format: <length:4><label-length:2><label><data>
|
|
1001
|
-
// Length includes label-length field + label + data
|
|
1002
|
-
const totalLength = 2 + labelBuffer.length + data.length;
|
|
1003
|
-
const header = Buffer.allocUnsafe(6);
|
|
1004
|
-
header.writeUInt32BE(totalLength, 0);
|
|
1005
|
-
header.writeUInt16BE(labelBuffer.length, 4);
|
|
1006
|
-
|
|
1007
|
-
const message = Buffer.concat([header, labelBuffer, data]);
|
|
1008
|
-
|
|
1009
|
-
this._bufferedAmount += message.length;
|
|
1010
|
-
this._socket.write(message, () => {
|
|
1011
|
-
this._bufferedAmount = Math.max(0, this._bufferedAmount - message.length);
|
|
1012
|
-
if (this._bufferedAmount === 0) {
|
|
1013
|
-
this.emit('bufferedamountlow', 0);
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
console.log(`[NativeDataChannel] ${this.label} - Sent ${data.length} bytes`);
|
|
1018
|
-
} catch (err) {
|
|
1019
|
-
console.error(`[NativeDataChannel] ${this.label} - Send error:`, err);
|
|
1020
|
-
throw err;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/**
|
|
1025
|
-
* Close the channel
|
|
1026
|
-
*/
|
|
1027
|
-
close() {
|
|
1028
|
-
if (this._closed) {
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
this._closed = true;
|
|
1033
|
-
this._state = 2; // closing
|
|
1034
|
-
this.emit('statechange', this._state);
|
|
1035
|
-
|
|
1036
|
-
setTimeout(() => {
|
|
1037
|
-
this._state = 3; // closed
|
|
1038
|
-
this._socket = null;
|
|
1039
|
-
this.emit('statechange', this._state);
|
|
1040
|
-
}, 10);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
module.exports = NativePeerConnectionFactory;
|