node-rtc-connection 1.0.19 → 2.0.5

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 (47) hide show
  1. package/README.md +94 -85
  2. package/index.cjs +1 -0
  3. package/index.mjs +1 -0
  4. package/package.json +14 -46
  5. package/types/crypto/der.d.ts +107 -0
  6. package/types/crypto/x509.d.ts +56 -0
  7. package/types/datachannel/RTCDataChannel.d.ts +179 -0
  8. package/types/dtls/RTCCertificate.d.ts +163 -0
  9. package/types/dtls/cipher.d.ts +81 -0
  10. package/types/dtls/connection.d.ts +81 -0
  11. package/types/dtls/prf.d.ts +29 -0
  12. package/types/dtls/protocol.d.ts +127 -0
  13. package/types/foundation/ByteBufferQueue.d.ts +71 -0
  14. package/types/foundation/RTCError.d.ts +152 -0
  15. package/types/ice/RTCIceCandidate.d.ts +161 -0
  16. package/types/ice/ice-agent.d.ts +154 -0
  17. package/types/ice/stun-message.d.ts +92 -0
  18. package/types/index.d.ts +29 -0
  19. package/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  20. package/types/sctp/association.d.ts +77 -0
  21. package/types/sctp/chunks.d.ts +200 -0
  22. package/types/sctp/crc32c.d.ts +24 -0
  23. package/types/sctp/datachannel-manager.d.ts +51 -0
  24. package/types/sctp/dcep.d.ts +56 -0
  25. package/types/sdp/RTCSessionDescription.d.ts +73 -0
  26. package/types/sdp/sdp-utils.d.ts +103 -0
  27. package/types/stun/stun-client.d.ts +119 -0
  28. package/types/transport-stack.d.ts +68 -0
  29. package/dist/index.cjs +0 -5618
  30. package/dist/index.cjs.map +0 -1
  31. package/dist/index.mjs +0 -5616
  32. package/dist/index.mjs.map +0 -1
  33. package/src/datachannel/RTCDataChannel.js +0 -354
  34. package/src/dtls/RTCCertificate.js +0 -310
  35. package/src/dtls/RTCDtlsTransport.js +0 -247
  36. package/src/foundation/ByteBufferQueue.js +0 -235
  37. package/src/foundation/RTCError.js +0 -226
  38. package/src/ice/RTCIceCandidate.js +0 -301
  39. package/src/ice/RTCIceTransport.js +0 -1018
  40. package/src/index.d.ts +0 -400
  41. package/src/index.js +0 -92
  42. package/src/network/network-transport.js +0 -478
  43. package/src/peerconnection/RTCPeerConnection.js +0 -875
  44. package/src/sctp/RTCSctpTransport.js +0 -253
  45. package/src/sdp/RTCSessionDescription.js +0 -102
  46. package/src/sdp/sdp-utils.js +0 -224
  47. package/src/stun/stun-client.js +0 -777
@@ -1,875 +0,0 @@
1
- /**
2
- * @file RTCPeerConnection.js
3
- * @description WebRTC Peer Connection implementation
4
- * @module peerconnection/RTCPeerConnection
5
- *
6
- * Ported from Chromium's RTCPeerConnection implementation:
7
- * - cc/rtc_peer_connection.idl
8
- * - cc/rtc_peer_connection.h
9
- */
10
-
11
- 'use strict';
12
-
13
- const EventEmitter = require('events');
14
- const { RTCIceTransport } = require('../ice/RTCIceTransport');
15
- const { RTCDtlsTransport } = require('../dtls/RTCDtlsTransport');
16
- const { RTCSctpTransport } = require('../sctp/RTCSctpTransport');
17
- const { RTCDataChannel } = require('../datachannel/RTCDataChannel');
18
- const RTCCertificate = require('../dtls/RTCCertificate');
19
- const { RTCSessionDescription, RTCSdpType } = require('../sdp/RTCSessionDescription');
20
- const sdpUtils = require('../sdp/sdp-utils');
21
- const { DataChannelTransport } = require('../network/network-transport');
22
-
23
- /**
24
- * RTCSignalingState - Signaling state of the peer connection
25
- * @readonly
26
- * @enum {string}
27
- */
28
- const RTCSignalingState = Object.freeze({
29
- STABLE: 'stable',
30
- HAVE_LOCAL_OFFER: 'have-local-offer',
31
- HAVE_REMOTE_OFFER: 'have-remote-offer',
32
- HAVE_LOCAL_PRANSWER: 'have-local-pranswer',
33
- HAVE_REMOTE_PRANSWER: 'have-remote-pranswer',
34
- CLOSED: 'closed'
35
- });
36
-
37
- /**
38
- * RTCIceGatheringState - ICE gathering state
39
- * @readonly
40
- * @enum {string}
41
- */
42
- const RTCIceGatheringState = Object.freeze({
43
- NEW: 'new',
44
- GATHERING: 'gathering',
45
- COMPLETE: 'complete'
46
- });
47
-
48
- /**
49
- * RTCPeerConnectionState - Overall connection state
50
- * @readonly
51
- * @enum {string}
52
- */
53
- const RTCPeerConnectionState = Object.freeze({
54
- NEW: 'new',
55
- CONNECTING: 'connecting',
56
- CONNECTED: 'connected',
57
- DISCONNECTED: 'disconnected',
58
- FAILED: 'failed',
59
- CLOSED: 'closed'
60
- });
61
-
62
- /**
63
- * @class RTCPeerConnection
64
- * @extends EventEmitter
65
- * @description Main class for WebRTC peer-to-peer connections
66
- *
67
- * Events:
68
- * - 'negotiationneeded': Negotiation is needed
69
- * - 'icecandidate': New ICE candidate gathered
70
- * - 'icegatheringstatechange': ICE gathering state changed
71
- * - 'iceconnectionstatechange': ICE connection state changed
72
- * - 'connectionstatechange': Overall connection state changed
73
- * - 'signalingstatechange': Signaling state changed
74
- * - 'datachannel': New data channel received
75
- *
76
- * @example
77
- * const pc = new RTCPeerConnection({
78
- * iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
79
- * });
80
- *
81
- * const channel = pc.createDataChannel('myChannel');
82
- * const offer = await pc.createOffer();
83
- * await pc.setLocalDescription(offer);
84
- */
85
- class RTCPeerConnection extends EventEmitter {
86
- /**
87
- * Create an RTCPeerConnection instance.
88
- * @param {Object} [configuration] - Configuration options
89
- * @param {Array<Object>} [configuration.iceServers] - STUN/TURN servers
90
- * @param {string} [configuration.iceTransportPolicy='all'] - ICE transport policy
91
- * @param {string} [configuration.bundlePolicy='balanced'] - Bundle policy
92
- */
93
- constructor(configuration = {}) {
94
- super();
95
-
96
- this._configuration = configuration;
97
- this._signalingState = RTCSignalingState.STABLE;
98
- this._iceGatheringState = RTCIceGatheringState.NEW;
99
- this._connectionState = RTCPeerConnectionState.NEW;
100
-
101
- this._localDescription = null;
102
- this._remoteDescription = null;
103
- this._pendingLocalDescription = null;
104
- this._pendingRemoteDescription = null;
105
-
106
- this._dataChannels = new Map();
107
- this._nextChannelId = 0;
108
-
109
- // Transport components
110
- this._certificate = null;
111
- this._iceTransport = null;
112
- this._dtlsTransport = null;
113
- this._sctpTransport = null;
114
- this._networkTransport = null;
115
-
116
- this._isClosed = false;
117
- this._localIceCandidates = [];
118
- this._remoteIceCandidates = [];
119
-
120
- // Network state
121
- this._isOfferer = false;
122
- this._remoteConnectionInfo = null;
123
-
124
- // Initialize transports lazily
125
- this._initializePromise = null;
126
- }
127
-
128
- /**
129
- * Initialize transports (lazy initialization)
130
- * @private
131
- */
132
- async _initialize() {
133
- if (this._initializePromise) {
134
- return this._initializePromise;
135
- }
136
-
137
- this._initializePromise = (async () => {
138
- // Generate certificate if not provided
139
- if (!this._certificate) {
140
- this._certificate = await RTCCertificate.generateCertificate({
141
- name: 'RSASSA-PKCS1-v1_5',
142
- modulusLength: 2048,
143
- hash: 'SHA-256'
144
- });
145
- }
146
-
147
- // Create transport stack
148
- this._iceTransport = new RTCIceTransport();
149
- this._dtlsTransport = new RTCDtlsTransport(this._iceTransport, [this._certificate]);
150
- this._sctpTransport = new RTCSctpTransport(this._dtlsTransport);
151
-
152
- // Create network transport
153
- this._networkTransport = new DataChannelTransport();
154
-
155
- // Setup event handlers
156
- this._setupTransportEvents();
157
- this._setupNetworkTransport();
158
- })();
159
-
160
- return this._initializePromise;
161
- }
162
-
163
- /**
164
- * Setup transport event handlers
165
- * @private
166
- */
167
- _setupTransportEvents() {
168
- // ICE events
169
- this._iceTransport.on('icecandidate', (candidate) => {
170
- const candidateInit = {
171
- candidate: candidate.candidate,
172
- sdpMid: '0',
173
- sdpMLineIndex: 0,
174
- usernameFragment: candidate.usernameFragment
175
- };
176
- this._localIceCandidates.push(candidateInit);
177
- this.emit('icecandidate', { candidate: candidateInit });
178
- });
179
-
180
- this._iceTransport.on('gatheringstatechange', () => {
181
- const state = this._iceTransport.gatheringState;
182
- this._iceGatheringState = state;
183
- this.emit('icegatheringstatechange');
184
- });
185
-
186
- this._iceTransport.on('statechange', () => {
187
- this._updateConnectionState();
188
- this.emit('iceconnectionstatechange');
189
- });
190
-
191
- // DTLS events
192
- this._dtlsTransport.on('statechange', () => {
193
- this._updateConnectionState();
194
- });
195
-
196
- // SCTP events
197
- this._sctpTransport.on('statechange', () => {
198
- this._updateConnectionState();
199
- });
200
- }
201
-
202
- /**
203
- * Setup network transport event handlers
204
- * @private
205
- */
206
- _setupNetworkTransport() {
207
- this._networkTransport.on('message', (message, rinfo) => {
208
- this._handleIncomingMessage(message, rinfo);
209
- });
210
-
211
- this._networkTransport.on('connection', (connectionId, rinfo) => {
212
- // Connection established - open data channels
213
- setImmediate(() => {
214
- this._openDataChannels();
215
- });
216
- });
217
-
218
- this._networkTransport.on('error', (error) => {
219
- console.error('Network transport error:', error);
220
- });
221
- }
222
-
223
- /**
224
- * Open all data channels
225
- * @private
226
- */
227
- _openDataChannels() {
228
- // Check if network transport has active connections
229
- const hasConnections = this._networkTransport &&
230
- this._networkTransport.tcpTransport &&
231
- this._networkTransport.tcpTransport.connections.size > 0;
232
-
233
- if (!hasConnections) {
234
- // Network not ready yet, channels will be opened when connection establishes
235
- return;
236
- }
237
-
238
- for (const channel of this._dataChannels.values()) {
239
- if (channel.readyState === 'connecting') {
240
- this._connectChannelToNetwork(channel);
241
- channel._setStateToOpen();
242
- }
243
- }
244
- }
245
-
246
- /**
247
- * Handle incoming network message
248
- * @private
249
- */
250
- _handleIncomingMessage(message, rinfo) {
251
- try {
252
- // Try to parse as JSON first (for data channel messages)
253
- const data = JSON.parse(message.toString());
254
-
255
- if (data.type === 'datachannel') {
256
- const channelLabel = data.label;
257
- const channelData = data.data;
258
-
259
- // Find or create data channel
260
- let channel = Array.from(this._dataChannels.values())
261
- .find(ch => ch.label === channelLabel);
262
-
263
- if (!channel) {
264
- // Remote peer created a new data channel
265
- channel = new RTCDataChannel(channelLabel, {
266
- id: data.id || this._nextChannelId++
267
- });
268
- this._dataChannels.set(channel.id, channel);
269
-
270
- // Connect channel to network before opening
271
- this._connectChannelToNetwork(channel);
272
-
273
- channel._setStateToOpen();
274
- this.emit('datachannel', { channel });
275
- }
276
-
277
- // Deliver message to channel
278
- if (channel.readyState === 'open') {
279
- channel._receiveMessage(channelData);
280
- }
281
- }
282
- } catch (error) {
283
- // Not JSON, might be raw binary data
284
- console.error('Error parsing network message:', error);
285
- }
286
- }
287
-
288
- /**
289
- * Update overall connection state based on transport states
290
- * @private
291
- */
292
- _updateConnectionState() {
293
- if (this._isClosed) {
294
- this._connectionState = RTCPeerConnectionState.CLOSED;
295
- this.emit('connectionstatechange');
296
- return;
297
- }
298
-
299
- const iceState = this._iceTransport?.state || 'new';
300
- const dtlsState = this._dtlsTransport?.state || 'new';
301
- const sctpState = this._sctpTransport?.state || 'connecting';
302
-
303
- let newState;
304
-
305
- if (iceState === 'failed' || dtlsState === 'failed') {
306
- newState = RTCPeerConnectionState.FAILED;
307
- } else if (iceState === 'connected' && dtlsState === 'connected' && sctpState === 'connected') {
308
- newState = RTCPeerConnectionState.CONNECTED;
309
- } else if (iceState === 'checking' || dtlsState === 'connecting' || sctpState === 'connecting') {
310
- newState = RTCPeerConnectionState.CONNECTING;
311
- } else if (iceState === 'disconnected') {
312
- newState = RTCPeerConnectionState.DISCONNECTED;
313
- } else {
314
- newState = RTCPeerConnectionState.NEW;
315
- }
316
-
317
- if (newState !== this._connectionState) {
318
- this._connectionState = newState;
319
- this.emit('connectionstatechange');
320
- }
321
- }
322
-
323
- /**
324
- * Create a data channel.
325
- * @param {string} label - Channel label
326
- * @param {Object} [options] - Channel options
327
- * @returns {RTCDataChannel} Data channel
328
- */
329
- createDataChannel(label, options = {}) {
330
- if (this._isClosed) {
331
- throw new Error('RTCPeerConnection is closed');
332
- }
333
-
334
- const channelOptions = {
335
- ...options,
336
- negotiated: options.negotiated || false
337
- };
338
-
339
- if (!channelOptions.negotiated) {
340
- channelOptions.id = this._nextChannelId++;
341
- }
342
-
343
- const channel = new RTCDataChannel(label, channelOptions);
344
- this._dataChannels.set(channelOptions.id, channel);
345
-
346
- // Emit negotiation needed
347
- setImmediate(() => {
348
- if (!this._isClosed) {
349
- this.emit('negotiationneeded');
350
- }
351
- });
352
-
353
- return channel;
354
- }
355
-
356
- /**
357
- * Create an SDP offer.
358
- * @param {Object} [options] - Offer options
359
- * @returns {Promise<RTCSessionDescription>} Offer description
360
- */
361
- async createOffer(options = {}) {
362
- if (this._isClosed) {
363
- throw new Error('RTCPeerConnection is closed');
364
- }
365
-
366
- await this._initialize();
367
-
368
- // Start listening to get actual port
369
- const { address, port } = await this._networkTransport.listen(0, '0.0.0.0');
370
-
371
- // Get local address (try to find non-localhost)
372
- const os = require('os');
373
- const interfaces = os.networkInterfaces();
374
- let localAddress = '127.0.0.1';
375
-
376
- for (const [name, addrs] of Object.entries(interfaces)) {
377
- for (const addr of addrs) {
378
- if (addr.family === 'IPv4' && !addr.internal) {
379
- localAddress = addr.address;
380
- break;
381
- }
382
- }
383
- if (localAddress !== '127.0.0.1') break;
384
- }
385
-
386
- // Generate ICE credentials
387
- const iceCredentials = sdpUtils.generateIceCredentials();
388
-
389
- // Get fingerprints
390
- const fingerprints = this._certificate.getFingerprints();
391
-
392
- // Generate SDP offer with actual connection info
393
- const sdp = sdpUtils.generateOffer({
394
- iceUfrag: iceCredentials.usernameFragment,
395
- icePwd: iceCredentials.password,
396
- fingerprints,
397
- candidates: this._localIceCandidates,
398
- setup: 'actpass',
399
- connectionAddress: localAddress,
400
- connectionPort: port
401
- });
402
-
403
- return new RTCSessionDescription({
404
- type: RTCSdpType.OFFER,
405
- sdp
406
- });
407
- }
408
-
409
- /**
410
- * Create an SDP answer.
411
- * @param {Object} [options] - Answer options
412
- * @returns {Promise<RTCSessionDescription>} Answer description
413
- */
414
- async createAnswer(options = {}) {
415
- if (this._isClosed) {
416
- throw new Error('RTCPeerConnection is closed');
417
- }
418
-
419
- if (!this._remoteDescription || this._remoteDescription.type !== 'offer') {
420
- throw new Error('Cannot create answer without remote offer');
421
- }
422
-
423
- await this._initialize();
424
-
425
- // Generate ICE credentials
426
- const iceCredentials = sdpUtils.generateIceCredentials();
427
-
428
- // Get fingerprints
429
- const fingerprints = this._certificate.getFingerprints();
430
-
431
- // Generate SDP answer
432
- const sdp = sdpUtils.generateAnswer({
433
- iceUfrag: iceCredentials.usernameFragment,
434
- icePwd: iceCredentials.password,
435
- fingerprints,
436
- candidates: this._localIceCandidates,
437
- setup: 'active'
438
- });
439
-
440
- return new RTCSessionDescription({
441
- type: RTCSdpType.ANSWER,
442
- sdp
443
- });
444
- }
445
-
446
- /**
447
- * Set the local description.
448
- * @param {RTCSessionDescription} [description] - Local description
449
- * @returns {Promise<void>}
450
- */
451
- async setLocalDescription(description) {
452
- if (this._isClosed) {
453
- throw new Error('RTCPeerConnection is closed');
454
- }
455
-
456
- // If no description provided, create one based on signaling state
457
- if (!description) {
458
- if (this._signalingState === RTCSignalingState.HAVE_REMOTE_OFFER) {
459
- description = await this.createAnswer();
460
- } else {
461
- description = await this.createOffer();
462
- }
463
- }
464
-
465
- await this._initialize();
466
-
467
- this._localDescription = new RTCSessionDescription(description);
468
- this._pendingLocalDescription = this._localDescription;
469
-
470
- // Update signaling state
471
- if (description.type === 'offer') {
472
- this._signalingState = RTCSignalingState.HAVE_LOCAL_OFFER;
473
- } else if (description.type === 'answer') {
474
- this._signalingState = RTCSignalingState.STABLE;
475
- this._pendingLocalDescription = null;
476
-
477
- // Answerer: start connection when setting local answer
478
- if (this._remoteDescription) {
479
- const iceParams = sdpUtils.parseIceParameters(this._remoteDescription.sdp);
480
- const dtlsParams = sdpUtils.parseDtlsParameters(this._remoteDescription.sdp);
481
- const sctpParams = sdpUtils.parseSctpParameters(this._remoteDescription.sdp);
482
- await this._startConnection(iceParams, dtlsParams, sctpParams);
483
- }
484
- }
485
-
486
- // Parse and apply ICE parameters
487
- const iceParams = sdpUtils.parseIceParameters(description.sdp);
488
-
489
- // Start ICE gathering with configured servers
490
- this._iceGatheringState = RTCIceGatheringState.GATHERING;
491
- this.emit('icegatheringstatechange');
492
-
493
- // Gather candidates with ICE servers
494
- if (this._iceTransport) {
495
- await this._iceTransport.gather({
496
- iceServers: this._configuration.iceServers || []
497
- });
498
- }
499
-
500
- this.emit('signalingstatechange');
501
- }
502
-
503
- /**
504
- * Set the remote description.
505
- * @param {RTCSessionDescription} description - Remote description
506
- * @returns {Promise<void>}
507
- */
508
- async setRemoteDescription(description) {
509
- if (this._isClosed) {
510
- throw new Error('RTCPeerConnection is closed');
511
- }
512
-
513
- if (!description || !description.sdp) {
514
- throw new Error('Invalid session description');
515
- }
516
-
517
- await this._initialize();
518
-
519
- this._remoteDescription = new RTCSessionDescription(description);
520
- this._pendingRemoteDescription = this._remoteDescription;
521
-
522
- // Update signaling state
523
- if (description.type === 'offer') {
524
- this._signalingState = RTCSignalingState.HAVE_REMOTE_OFFER;
525
- } else if (description.type === 'answer') {
526
- this._signalingState = RTCSignalingState.STABLE;
527
- this._pendingRemoteDescription = null;
528
- this._pendingLocalDescription = null;
529
- }
530
-
531
- // Parse remote parameters
532
- const iceParams = sdpUtils.parseIceParameters(description.sdp);
533
- const dtlsParams = sdpUtils.parseDtlsParameters(description.sdp);
534
- const sctpParams = sdpUtils.parseSctpParameters(description.sdp);
535
-
536
- // Parse remote candidates and extract connection info
537
- const remoteCandidates = sdpUtils.parseCandidates(description.sdp);
538
- this._remoteIceCandidates = remoteCandidates;
539
-
540
- // Extract connection info from SDP
541
- this._extractConnectionInfo(description.sdp);
542
-
543
- this.emit('signalingstatechange');
544
-
545
- // Start connection establishment if we have both descriptions
546
- if (this._signalingState === RTCSignalingState.STABLE) {
547
- await this._startConnection(iceParams, dtlsParams, sctpParams);
548
- }
549
- }
550
-
551
- /**
552
- * Extract connection information from SDP
553
- * @param {string} sdp - SDP string
554
- * @private
555
- */
556
- _extractConnectionInfo(sdp) {
557
- // Look for connection line: c=IN IP4 <address>
558
- const cLineMatch = sdp.match(/^c=IN IP4 ([^\s]+)/m);
559
- if (cLineMatch && cLineMatch[1] !== '0.0.0.0') {
560
- const address = cLineMatch[1];
561
-
562
- // Look for port in media line: m=application <port>
563
- const mLineMatch = sdp.match(/^m=application (\d+)/m);
564
- if (mLineMatch && mLineMatch[1] !== '9') {
565
- const port = parseInt(mLineMatch[1], 10);
566
- this._remoteConnectionInfo = { address, port };
567
- }
568
- }
569
- }
570
-
571
- /**
572
- * Start connection establishment
573
- * @param {Object} iceParams - ICE parameters
574
- * @param {Object} dtlsParams - DTLS parameters
575
- * @param {Object} sctpParams - SCTP parameters
576
- * @private
577
- */
578
- async _startConnection(iceParams, dtlsParams, sctpParams) {
579
- // Determine if we're the offerer based on setup attribute
580
- this._isOfferer = this._localDescription?.type === 'offer';
581
-
582
- // Start network transport
583
- try {
584
- if (this._isOfferer) {
585
- // Offerer already started listening in createOffer()
586
- // Wait for incoming connection
587
- } else {
588
- // Answerer connects to offerer
589
- if (this._remoteConnectionInfo) {
590
- await this._networkTransport.connect(
591
- this._remoteConnectionInfo.address,
592
- this._remoteConnectionInfo.port
593
- );
594
-
595
- // Connection established - open channels
596
- setImmediate(() => {
597
- this._openDataChannels();
598
- });
599
- }
600
- }
601
- } catch (error) {
602
- console.error('Failed to establish network connection:', error);
603
- }
604
-
605
- // Snapshot the candidates BEFORE starting ICE to avoid race condition
606
- // If addIceCandidate runs while start() is yielding, it will see transport running and add the candidate.
607
- // If we snapshot after start(), we might add the same candidate twice.
608
- const candidatesToAdd = [...this._remoteIceCandidates];
609
-
610
- // Start ICE
611
- if (iceParams.usernameFragment && iceParams.password) {
612
- try {
613
- await this._iceTransport.start(iceParams, this._isOfferer ? 'controlling' : 'controlled');
614
- } catch (error) {
615
- console.error('Failed to start ICE:', error);
616
- }
617
- }
618
-
619
- // Add remote candidates
620
- for (const candidate of candidatesToAdd) {
621
- try {
622
- // Parse candidate string (simplified)
623
- await this._iceTransport.addRemoteCandidate(candidate);
624
- } catch (error) {
625
- console.error('Failed to add remote candidate:', error);
626
- }
627
- }
628
-
629
- // Open data channels when connection is established
630
- this._sctpTransport.once('statechange', () => {
631
- if (this._sctpTransport.state === 'connected') {
632
- // Wait for network to be ready before opening channels
633
- this._openDataChannels();
634
- }
635
- });
636
- }
637
-
638
- /**
639
- * Connect data channel to network transport
640
- * @param {RTCDataChannel} channel - Data channel
641
- * @private
642
- */
643
- _connectChannelToNetwork(channel) {
644
- // Store original _send method
645
- const originalInternalSend = channel._send ? channel._send.bind(channel) : null;
646
-
647
- // Override the internal send to use network transport
648
- channel._send = async (data) => {
649
- try {
650
- const message = JSON.stringify({
651
- type: 'datachannel',
652
- label: channel.label,
653
- id: channel.id,
654
- data: data
655
- });
656
- await this._networkTransport.sendMessage(message);
657
-
658
- // Update buffered amount tracking
659
- if (channel._bufferedAmount > 0) {
660
- channel._bufferedAmount = Math.max(0, channel._bufferedAmount - (typeof data === 'string' ? data.length : 0));
661
- channel._emitBufferedAmountLow();
662
- }
663
- } catch (error) {
664
- console.error('Failed to send via network:', error);
665
- throw error;
666
- }
667
- };
668
- }
669
-
670
- /**
671
- * Add an ICE candidate.
672
- * @param {Object} [candidate] - ICE candidate
673
- * @returns {Promise<void>}
674
- */
675
- async addIceCandidate(candidate) {
676
- if (this._isClosed) {
677
- throw new Error('RTCPeerConnection is closed');
678
- }
679
-
680
- if (!candidate || (candidate.candidate === '')) {
681
- // End of candidates signal
682
- this._iceGatheringState = RTCIceGatheringState.COMPLETE;
683
- this.emit('icegatheringstatechange');
684
- return;
685
- }
686
-
687
- await this._initialize();
688
-
689
- if (this._iceTransport) {
690
- this._remoteIceCandidates.push(candidate);
691
-
692
- // If connection is already started, add candidate immediately
693
- // Check if transport is actually started (not just initialized)
694
- // We can infer this if we have remote description AND the transport state is not 'new'
695
- // Or simply try/catch and ignore "not started" error, but better to check.
696
- // Since we don't have public access to _started, we rely on state.
697
- // However, state might be 'new' but start() was called? No, start() sets state to checking.
698
-
699
- // Actually, _startConnection calls start().
700
- // If we are Answerer, we set remote offer. _startConnection is NOT called yet.
701
- // It is called when we set local answer.
702
- // So we should NOT add candidates yet.
703
-
704
- // If we are Offerer, we set remote answer. _startConnection IS called.
705
-
706
- // So we should only add if we have both descriptions (Stable state)?
707
- // Or if the transport is started.
708
-
709
- // Let's check if we are in a state where transport should be running.
710
- const isTransportRunning = this._iceTransport.state !== 'new' && this._iceTransport.state !== 'closed';
711
-
712
- if (isTransportRunning) {
713
- try {
714
- await this._iceTransport.addRemoteCandidate(candidate);
715
- } catch (error) {
716
- console.error('Failed to add ICE candidate:', error);
717
- }
718
- }
719
- }
720
- }
721
-
722
- /**
723
- * Get the current configuration.
724
- * @returns {Object} Configuration
725
- */
726
- getConfiguration() {
727
- return { ...this._configuration };
728
- }
729
-
730
- /**
731
- * Set the configuration.
732
- * @param {Object} configuration - New configuration
733
- */
734
- setConfiguration(configuration) {
735
- if (this._isClosed) {
736
- throw new Error('RTCPeerConnection is closed');
737
- }
738
- this._configuration = { ...configuration };
739
- }
740
-
741
- /**
742
- * Close the peer connection.
743
- */
744
- close() {
745
- if (this._isClosed) {
746
- return;
747
- }
748
-
749
- this._isClosed = true;
750
- this._signalingState = RTCSignalingState.CLOSED;
751
- this._connectionState = RTCPeerConnectionState.CLOSED;
752
-
753
- // Close all data channels
754
- for (const channel of this._dataChannels.values()) {
755
- channel.close();
756
- }
757
-
758
- // Close transports
759
- if (this._sctpTransport) {
760
- this._sctpTransport.close();
761
- }
762
- if (this._dtlsTransport) {
763
- this._dtlsTransport.close();
764
- }
765
- if (this._iceTransport) {
766
- this._iceTransport.stop();
767
- }
768
-
769
- this.emit('signalingstatechange');
770
- this.emit('connectionstatechange');
771
- }
772
-
773
- /**
774
- * Get the signaling state.
775
- * @returns {string} Signaling state
776
- */
777
- get signalingState() {
778
- return this._signalingState;
779
- }
780
-
781
- /**
782
- * Get the ICE gathering state.
783
- * @returns {string} ICE gathering state
784
- */
785
- get iceGatheringState() {
786
- return this._iceGatheringState;
787
- }
788
-
789
- /**
790
- * Get the ICE connection state.
791
- * @returns {string} ICE connection state
792
- */
793
- get iceConnectionState() {
794
- return this._iceTransport?.state || 'new';
795
- }
796
-
797
- /**
798
- * Get the overall connection state.
799
- * @returns {string} Connection state
800
- */
801
- get connectionState() {
802
- return this._connectionState;
803
- }
804
-
805
- /**
806
- * Get the local description.
807
- * @returns {RTCSessionDescription|null} Local description
808
- */
809
- get localDescription() {
810
- return this._localDescription;
811
- }
812
-
813
- /**
814
- * Get the remote description.
815
- * @returns {RTCSessionDescription|null} Remote description
816
- */
817
- get remoteDescription() {
818
- return this._remoteDescription;
819
- }
820
-
821
- /**
822
- * Get the current local description.
823
- * @returns {RTCSessionDescription|null} Current local description
824
- */
825
- get currentLocalDescription() {
826
- return this._signalingState === RTCSignalingState.STABLE ? this._localDescription : null;
827
- }
828
-
829
- /**
830
- * Get the pending local description.
831
- * @returns {RTCSessionDescription|null} Pending local description
832
- */
833
- get pendingLocalDescription() {
834
- return this._pendingLocalDescription;
835
- }
836
-
837
- /**
838
- * Get the current remote description.
839
- * @returns {RTCSessionDescription|null} Current remote description
840
- */
841
- get currentRemoteDescription() {
842
- return this._signalingState === RTCSignalingState.STABLE ? this._remoteDescription : null;
843
- }
844
-
845
- /**
846
- * Get the pending remote description.
847
- * @returns {RTCSessionDescription|null} Pending remote description
848
- */
849
- get pendingRemoteDescription() {
850
- return this._pendingRemoteDescription;
851
- }
852
-
853
- /**
854
- * Check if ICE candidate trickling is supported.
855
- * @returns {boolean} Always true
856
- */
857
- get canTrickleIceCandidates() {
858
- return true;
859
- }
860
-
861
- /**
862
- * Get the SCTP transport.
863
- * @returns {RTCSctpTransport|null} SCTP transport
864
- */
865
- get sctp() {
866
- return this._sctpTransport;
867
- }
868
- }
869
-
870
- module.exports = {
871
- RTCPeerConnection,
872
- RTCSignalingState,
873
- RTCIceGatheringState,
874
- RTCPeerConnectionState
875
- };