node-rtc-connection 1.0.11 → 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.
@@ -0,0 +1,847 @@
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
+ for (const channel of this._dataChannels.values()) {
229
+ if (channel.readyState === 'connecting') {
230
+ this._connectChannelToNetwork(channel);
231
+ channel._setStateToOpen();
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Handle incoming network message
238
+ * @private
239
+ */
240
+ _handleIncomingMessage(message, rinfo) {
241
+ try {
242
+ // Try to parse as JSON first (for data channel messages)
243
+ const data = JSON.parse(message.toString());
244
+
245
+ if (data.type === 'datachannel') {
246
+ const channelLabel = data.label;
247
+ const channelData = data.data;
248
+
249
+ // Find or create data channel
250
+ let channel = Array.from(this._dataChannels.values())
251
+ .find(ch => ch.label === channelLabel);
252
+
253
+ if (!channel) {
254
+ // Remote peer created a new data channel
255
+ channel = new RTCDataChannel(channelLabel, {
256
+ id: data.id || this._nextChannelId++
257
+ });
258
+ this._dataChannels.set(channel.id, channel);
259
+
260
+ // Connect channel to network before opening
261
+ this._connectChannelToNetwork(channel);
262
+
263
+ channel._setStateToOpen();
264
+ this.emit('datachannel', { channel });
265
+ }
266
+
267
+ // Deliver message to channel
268
+ if (channel.readyState === 'open') {
269
+ channel._receiveMessage(channelData);
270
+ }
271
+ }
272
+ } catch (error) {
273
+ // Not JSON, might be raw binary data
274
+ console.error('Error parsing network message:', error);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Update overall connection state based on transport states
280
+ * @private
281
+ */
282
+ _updateConnectionState() {
283
+ if (this._isClosed) {
284
+ this._connectionState = RTCPeerConnectionState.CLOSED;
285
+ this.emit('connectionstatechange');
286
+ return;
287
+ }
288
+
289
+ const iceState = this._iceTransport?.state || 'new';
290
+ const dtlsState = this._dtlsTransport?.state || 'new';
291
+ const sctpState = this._sctpTransport?.state || 'connecting';
292
+
293
+ let newState;
294
+
295
+ if (iceState === 'failed' || dtlsState === 'failed') {
296
+ newState = RTCPeerConnectionState.FAILED;
297
+ } else if (iceState === 'connected' && dtlsState === 'connected' && sctpState === 'connected') {
298
+ newState = RTCPeerConnectionState.CONNECTED;
299
+ } else if (iceState === 'checking' || dtlsState === 'connecting' || sctpState === 'connecting') {
300
+ newState = RTCPeerConnectionState.CONNECTING;
301
+ } else if (iceState === 'disconnected') {
302
+ newState = RTCPeerConnectionState.DISCONNECTED;
303
+ } else {
304
+ newState = RTCPeerConnectionState.NEW;
305
+ }
306
+
307
+ if (newState !== this._connectionState) {
308
+ this._connectionState = newState;
309
+ this.emit('connectionstatechange');
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Create a data channel.
315
+ * @param {string} label - Channel label
316
+ * @param {Object} [options] - Channel options
317
+ * @returns {RTCDataChannel} Data channel
318
+ */
319
+ createDataChannel(label, options = {}) {
320
+ if (this._isClosed) {
321
+ throw new Error('RTCPeerConnection is closed');
322
+ }
323
+
324
+ const channelOptions = {
325
+ ...options,
326
+ negotiated: options.negotiated || false
327
+ };
328
+
329
+ if (!channelOptions.negotiated) {
330
+ channelOptions.id = this._nextChannelId++;
331
+ }
332
+
333
+ const channel = new RTCDataChannel(label, channelOptions);
334
+ this._dataChannels.set(channelOptions.id, channel);
335
+
336
+ // Emit negotiation needed
337
+ setImmediate(() => {
338
+ if (!this._isClosed) {
339
+ this.emit('negotiationneeded');
340
+ }
341
+ });
342
+
343
+ return channel;
344
+ }
345
+
346
+ /**
347
+ * Create an SDP offer.
348
+ * @param {Object} [options] - Offer options
349
+ * @returns {Promise<RTCSessionDescription>} Offer description
350
+ */
351
+ async createOffer(options = {}) {
352
+ if (this._isClosed) {
353
+ throw new Error('RTCPeerConnection is closed');
354
+ }
355
+
356
+ await this._initialize();
357
+
358
+ // Start listening to get actual port
359
+ const { address, port } = await this._networkTransport.listen(0, '0.0.0.0');
360
+
361
+ // Get local address (try to find non-localhost)
362
+ const os = require('os');
363
+ const interfaces = os.networkInterfaces();
364
+ let localAddress = '127.0.0.1';
365
+
366
+ for (const [name, addrs] of Object.entries(interfaces)) {
367
+ for (const addr of addrs) {
368
+ if (addr.family === 'IPv4' && !addr.internal) {
369
+ localAddress = addr.address;
370
+ break;
371
+ }
372
+ }
373
+ if (localAddress !== '127.0.0.1') break;
374
+ }
375
+
376
+ // Generate ICE credentials
377
+ const iceCredentials = sdpUtils.generateIceCredentials();
378
+
379
+ // Get fingerprints
380
+ const fingerprints = this._certificate.getFingerprints();
381
+
382
+ // Generate SDP offer with actual connection info
383
+ const sdp = sdpUtils.generateOffer({
384
+ iceUfrag: iceCredentials.usernameFragment,
385
+ icePwd: iceCredentials.password,
386
+ fingerprints,
387
+ candidates: this._localIceCandidates,
388
+ setup: 'actpass',
389
+ connectionAddress: localAddress,
390
+ connectionPort: port
391
+ });
392
+
393
+ return new RTCSessionDescription({
394
+ type: RTCSdpType.OFFER,
395
+ sdp
396
+ });
397
+ }
398
+
399
+ /**
400
+ * Create an SDP answer.
401
+ * @param {Object} [options] - Answer options
402
+ * @returns {Promise<RTCSessionDescription>} Answer description
403
+ */
404
+ async createAnswer(options = {}) {
405
+ if (this._isClosed) {
406
+ throw new Error('RTCPeerConnection is closed');
407
+ }
408
+
409
+ if (!this._remoteDescription || this._remoteDescription.type !== 'offer') {
410
+ throw new Error('Cannot create answer without remote offer');
411
+ }
412
+
413
+ await this._initialize();
414
+
415
+ // Generate ICE credentials
416
+ const iceCredentials = sdpUtils.generateIceCredentials();
417
+
418
+ // Get fingerprints
419
+ const fingerprints = this._certificate.getFingerprints();
420
+
421
+ // Generate SDP answer
422
+ const sdp = sdpUtils.generateAnswer({
423
+ iceUfrag: iceCredentials.usernameFragment,
424
+ icePwd: iceCredentials.password,
425
+ fingerprints,
426
+ candidates: this._localIceCandidates,
427
+ setup: 'active'
428
+ });
429
+
430
+ return new RTCSessionDescription({
431
+ type: RTCSdpType.ANSWER,
432
+ sdp
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Set the local description.
438
+ * @param {RTCSessionDescription} [description] - Local description
439
+ * @returns {Promise<void>}
440
+ */
441
+ async setLocalDescription(description) {
442
+ if (this._isClosed) {
443
+ throw new Error('RTCPeerConnection is closed');
444
+ }
445
+
446
+ // If no description provided, create one based on signaling state
447
+ if (!description) {
448
+ if (this._signalingState === RTCSignalingState.HAVE_REMOTE_OFFER) {
449
+ description = await this.createAnswer();
450
+ } else {
451
+ description = await this.createOffer();
452
+ }
453
+ }
454
+
455
+ await this._initialize();
456
+
457
+ this._localDescription = new RTCSessionDescription(description);
458
+ this._pendingLocalDescription = this._localDescription;
459
+
460
+ // Update signaling state
461
+ if (description.type === 'offer') {
462
+ this._signalingState = RTCSignalingState.HAVE_LOCAL_OFFER;
463
+ } else if (description.type === 'answer') {
464
+ this._signalingState = RTCSignalingState.STABLE;
465
+ this._pendingLocalDescription = null;
466
+
467
+ // Answerer: start connection when setting local answer
468
+ if (this._remoteDescription) {
469
+ const iceParams = sdpUtils.parseIceParameters(this._remoteDescription.sdp);
470
+ const dtlsParams = sdpUtils.parseDtlsParameters(this._remoteDescription.sdp);
471
+ const sctpParams = sdpUtils.parseSctpParameters(this._remoteDescription.sdp);
472
+ await this._startConnection(iceParams, dtlsParams, sctpParams);
473
+ }
474
+ }
475
+
476
+ // Parse and apply ICE parameters
477
+ const iceParams = sdpUtils.parseIceParameters(description.sdp);
478
+
479
+ // Start ICE gathering with configured servers
480
+ this._iceGatheringState = RTCIceGatheringState.GATHERING;
481
+ this.emit('icegatheringstatechange');
482
+
483
+ // Gather candidates with ICE servers
484
+ if (this._iceTransport) {
485
+ await this._iceTransport.gather({
486
+ iceServers: this._configuration.iceServers || []
487
+ });
488
+ }
489
+
490
+ this.emit('signalingstatechange');
491
+ }
492
+
493
+ /**
494
+ * Set the remote description.
495
+ * @param {RTCSessionDescription} description - Remote description
496
+ * @returns {Promise<void>}
497
+ */
498
+ async setRemoteDescription(description) {
499
+ if (this._isClosed) {
500
+ throw new Error('RTCPeerConnection is closed');
501
+ }
502
+
503
+ if (!description || !description.sdp) {
504
+ throw new Error('Invalid session description');
505
+ }
506
+
507
+ await this._initialize();
508
+
509
+ this._remoteDescription = new RTCSessionDescription(description);
510
+ this._pendingRemoteDescription = this._remoteDescription;
511
+
512
+ // Update signaling state
513
+ if (description.type === 'offer') {
514
+ this._signalingState = RTCSignalingState.HAVE_REMOTE_OFFER;
515
+ } else if (description.type === 'answer') {
516
+ this._signalingState = RTCSignalingState.STABLE;
517
+ this._pendingRemoteDescription = null;
518
+ this._pendingLocalDescription = null;
519
+ }
520
+
521
+ // Parse remote parameters
522
+ const iceParams = sdpUtils.parseIceParameters(description.sdp);
523
+ const dtlsParams = sdpUtils.parseDtlsParameters(description.sdp);
524
+ const sctpParams = sdpUtils.parseSctpParameters(description.sdp);
525
+
526
+ // Parse remote candidates and extract connection info
527
+ const remoteCandidates = sdpUtils.parseCandidates(description.sdp);
528
+ this._remoteIceCandidates = remoteCandidates;
529
+
530
+ // Extract connection info from SDP
531
+ this._extractConnectionInfo(description.sdp);
532
+
533
+ this.emit('signalingstatechange');
534
+
535
+ // Start connection establishment if we have both descriptions
536
+ if (this._signalingState === RTCSignalingState.STABLE) {
537
+ await this._startConnection(iceParams, dtlsParams, sctpParams);
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Extract connection information from SDP
543
+ * @param {string} sdp - SDP string
544
+ * @private
545
+ */
546
+ _extractConnectionInfo(sdp) {
547
+ // Look for connection line: c=IN IP4 <address>
548
+ const cLineMatch = sdp.match(/^c=IN IP4 ([^\s]+)/m);
549
+ if (cLineMatch && cLineMatch[1] !== '0.0.0.0') {
550
+ const address = cLineMatch[1];
551
+
552
+ // Look for port in media line: m=application <port>
553
+ const mLineMatch = sdp.match(/^m=application (\d+)/m);
554
+ if (mLineMatch && mLineMatch[1] !== '9') {
555
+ const port = parseInt(mLineMatch[1], 10);
556
+ this._remoteConnectionInfo = { address, port };
557
+ }
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Start connection establishment
563
+ * @param {Object} iceParams - ICE parameters
564
+ * @param {Object} dtlsParams - DTLS parameters
565
+ * @param {Object} sctpParams - SCTP parameters
566
+ * @private
567
+ */
568
+ async _startConnection(iceParams, dtlsParams, sctpParams) {
569
+ // Determine if we're the offerer based on setup attribute
570
+ this._isOfferer = this._localDescription?.type === 'offer';
571
+
572
+ // Start network transport
573
+ try {
574
+ if (this._isOfferer) {
575
+ // Offerer already started listening in createOffer()
576
+ // Wait for incoming connection
577
+ } else {
578
+ // Answerer connects to offerer
579
+ if (this._remoteConnectionInfo) {
580
+ await this._networkTransport.connect(
581
+ this._remoteConnectionInfo.address,
582
+ this._remoteConnectionInfo.port
583
+ );
584
+
585
+ // Connection established - open channels
586
+ setImmediate(() => {
587
+ this._openDataChannels();
588
+ });
589
+ }
590
+ }
591
+ } catch (error) {
592
+ console.error('Failed to establish network connection:', error);
593
+ }
594
+
595
+ // Start ICE
596
+ if (iceParams.usernameFragment && iceParams.password) {
597
+ try {
598
+ await this._iceTransport.start(iceParams, this._isOfferer ? 'controlling' : 'controlled');
599
+ } catch (error) {
600
+ console.error('Failed to start ICE:', error);
601
+ }
602
+ }
603
+
604
+ // Add remote candidates
605
+ for (const candidate of this._remoteIceCandidates) {
606
+ try {
607
+ // Parse candidate string (simplified)
608
+ await this._iceTransport.addRemoteCandidate(candidate);
609
+ } catch (error) {
610
+ console.error('Failed to add remote candidate:', error);
611
+ }
612
+ }
613
+
614
+ // Open data channels when connection is established
615
+ this._sctpTransport.once('statechange', () => {
616
+ if (this._sctpTransport.state === 'connected') {
617
+ for (const channel of this._dataChannels.values()) {
618
+ if (channel.readyState === 'connecting') {
619
+ channel._setStateToOpen();
620
+
621
+ // Hook up channel to network transport
622
+ this._connectChannelToNetwork(channel);
623
+ }
624
+ }
625
+ }
626
+ });
627
+ }
628
+
629
+ /**
630
+ * Connect data channel to network transport
631
+ * @param {RTCDataChannel} channel - Data channel
632
+ * @private
633
+ */
634
+ _connectChannelToNetwork(channel) {
635
+ // Store original _send method
636
+ const originalInternalSend = channel._send ? channel._send.bind(channel) : null;
637
+
638
+ // Override the internal send to use network transport
639
+ channel._send = async (data) => {
640
+ try {
641
+ const message = JSON.stringify({
642
+ type: 'datachannel',
643
+ label: channel.label,
644
+ id: channel.id,
645
+ data: data
646
+ });
647
+ await this._networkTransport.sendMessage(message);
648
+
649
+ // Update buffered amount tracking
650
+ if (channel._bufferedAmount > 0) {
651
+ channel._bufferedAmount = Math.max(0, channel._bufferedAmount - (typeof data === 'string' ? data.length : 0));
652
+ channel._emitBufferedAmountLow();
653
+ }
654
+ } catch (error) {
655
+ console.error('Failed to send via network:', error);
656
+ throw error;
657
+ }
658
+ };
659
+ }
660
+
661
+ /**
662
+ * Add an ICE candidate.
663
+ * @param {Object} [candidate] - ICE candidate
664
+ * @returns {Promise<void>}
665
+ */
666
+ async addIceCandidate(candidate) {
667
+ if (this._isClosed) {
668
+ throw new Error('RTCPeerConnection is closed');
669
+ }
670
+
671
+ if (!candidate) {
672
+ // End of candidates signal
673
+ this._iceGatheringState = RTCIceGatheringState.COMPLETE;
674
+ this.emit('icegatheringstatechange');
675
+ return;
676
+ }
677
+
678
+ await this._initialize();
679
+
680
+ if (this._iceTransport) {
681
+ this._remoteIceCandidates.push(candidate);
682
+
683
+ // If connection is already started, add candidate immediately
684
+ if (this._remoteDescription) {
685
+ try {
686
+ await this._iceTransport.addRemoteCandidate(candidate);
687
+ } catch (error) {
688
+ console.error('Failed to add ICE candidate:', error);
689
+ }
690
+ }
691
+ }
692
+ }
693
+
694
+ /**
695
+ * Get the current configuration.
696
+ * @returns {Object} Configuration
697
+ */
698
+ getConfiguration() {
699
+ return { ...this._configuration };
700
+ }
701
+
702
+ /**
703
+ * Set the configuration.
704
+ * @param {Object} configuration - New configuration
705
+ */
706
+ setConfiguration(configuration) {
707
+ if (this._isClosed) {
708
+ throw new Error('RTCPeerConnection is closed');
709
+ }
710
+ this._configuration = { ...configuration };
711
+ }
712
+
713
+ /**
714
+ * Close the peer connection.
715
+ */
716
+ close() {
717
+ if (this._isClosed) {
718
+ return;
719
+ }
720
+
721
+ this._isClosed = true;
722
+ this._signalingState = RTCSignalingState.CLOSED;
723
+ this._connectionState = RTCPeerConnectionState.CLOSED;
724
+
725
+ // Close all data channels
726
+ for (const channel of this._dataChannels.values()) {
727
+ channel.close();
728
+ }
729
+
730
+ // Close transports
731
+ if (this._sctpTransport) {
732
+ this._sctpTransport.close();
733
+ }
734
+ if (this._dtlsTransport) {
735
+ this._dtlsTransport.close();
736
+ }
737
+ if (this._iceTransport) {
738
+ this._iceTransport.stop();
739
+ }
740
+
741
+ this.emit('signalingstatechange');
742
+ this.emit('connectionstatechange');
743
+ }
744
+
745
+ /**
746
+ * Get the signaling state.
747
+ * @returns {string} Signaling state
748
+ */
749
+ get signalingState() {
750
+ return this._signalingState;
751
+ }
752
+
753
+ /**
754
+ * Get the ICE gathering state.
755
+ * @returns {string} ICE gathering state
756
+ */
757
+ get iceGatheringState() {
758
+ return this._iceGatheringState;
759
+ }
760
+
761
+ /**
762
+ * Get the ICE connection state.
763
+ * @returns {string} ICE connection state
764
+ */
765
+ get iceConnectionState() {
766
+ return this._iceTransport?.state || 'new';
767
+ }
768
+
769
+ /**
770
+ * Get the overall connection state.
771
+ * @returns {string} Connection state
772
+ */
773
+ get connectionState() {
774
+ return this._connectionState;
775
+ }
776
+
777
+ /**
778
+ * Get the local description.
779
+ * @returns {RTCSessionDescription|null} Local description
780
+ */
781
+ get localDescription() {
782
+ return this._localDescription;
783
+ }
784
+
785
+ /**
786
+ * Get the remote description.
787
+ * @returns {RTCSessionDescription|null} Remote description
788
+ */
789
+ get remoteDescription() {
790
+ return this._remoteDescription;
791
+ }
792
+
793
+ /**
794
+ * Get the current local description.
795
+ * @returns {RTCSessionDescription|null} Current local description
796
+ */
797
+ get currentLocalDescription() {
798
+ return this._signalingState === RTCSignalingState.STABLE ? this._localDescription : null;
799
+ }
800
+
801
+ /**
802
+ * Get the pending local description.
803
+ * @returns {RTCSessionDescription|null} Pending local description
804
+ */
805
+ get pendingLocalDescription() {
806
+ return this._pendingLocalDescription;
807
+ }
808
+
809
+ /**
810
+ * Get the current remote description.
811
+ * @returns {RTCSessionDescription|null} Current remote description
812
+ */
813
+ get currentRemoteDescription() {
814
+ return this._signalingState === RTCSignalingState.STABLE ? this._remoteDescription : null;
815
+ }
816
+
817
+ /**
818
+ * Get the pending remote description.
819
+ * @returns {RTCSessionDescription|null} Pending remote description
820
+ */
821
+ get pendingRemoteDescription() {
822
+ return this._pendingRemoteDescription;
823
+ }
824
+
825
+ /**
826
+ * Check if ICE candidate trickling is supported.
827
+ * @returns {boolean} Always true
828
+ */
829
+ get canTrickleIceCandidates() {
830
+ return true;
831
+ }
832
+
833
+ /**
834
+ * Get the SCTP transport.
835
+ * @returns {RTCSctpTransport|null} SCTP transport
836
+ */
837
+ get sctp() {
838
+ return this._sctpTransport;
839
+ }
840
+ }
841
+
842
+ module.exports = {
843
+ RTCPeerConnection,
844
+ RTCSignalingState,
845
+ RTCIceGatheringState,
846
+ RTCPeerConnectionState
847
+ };