node-rtc-connection 1.0.3

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,494 @@
1
+ const EventEmitter = require('events');
2
+ const RTCDataChannel = require('./RTCDataChannel');
3
+ const RTCSessionDescription = require('./RTCSessionDescription');
4
+ const RTCIceCandidate = require('./RTCIceCandidate');
5
+
6
+ /**
7
+ * RTCPeerConnection represents a WebRTC connection between the local computer and a remote peer.
8
+ * This is a DataChannel-only implementation ported from Chromium.
9
+ */
10
+ class RTCPeerConnection extends EventEmitter {
11
+ constructor(configuration, nativePeerConnectionFactory) {
12
+ super();
13
+
14
+ this._configuration = this._parseConfiguration(configuration || {});
15
+ this._signalingState = 'stable';
16
+ this._iceGatheringState = 'new';
17
+ this._iceConnectionState = 'new';
18
+ this._connectionState = 'new';
19
+ this._pendingLocalDescription = null;
20
+ this._currentLocalDescription = null;
21
+ this._pendingRemoteDescription = null;
22
+ this._currentRemoteDescription = null;
23
+ this._dataChannels = new Map();
24
+ this._closed = false;
25
+
26
+ // Native peer connection (would be native WebRTC binding)
27
+ this._nativePeerConnection = null;
28
+ this._nativePeerConnectionFactory = nativePeerConnectionFactory;
29
+
30
+ // Initialize native peer connection
31
+ this._initializeNativePeerConnection();
32
+ }
33
+
34
+ /**
35
+ * Parse and validate configuration
36
+ * @private
37
+ */
38
+ _parseConfiguration(config) {
39
+ const configuration = {
40
+ iceServers: [],
41
+ iceTransportPolicy: 'all',
42
+ bundlePolicy: 'balanced',
43
+ rtcpMuxPolicy: 'require',
44
+ iceCandidatePoolSize: 0
45
+ };
46
+
47
+ if (config.iceServers) {
48
+ configuration.iceServers = config.iceServers.map(server => ({
49
+ urls: Array.isArray(server.urls) ? server.urls : [server.urls],
50
+ username: server.username || '',
51
+ credential: server.credential || ''
52
+ }));
53
+ }
54
+
55
+ if (config.iceTransportPolicy) {
56
+ configuration.iceTransportPolicy = config.iceTransportPolicy;
57
+ }
58
+
59
+ if (config.bundlePolicy) {
60
+ configuration.bundlePolicy = config.bundlePolicy;
61
+ }
62
+
63
+ if (config.rtcpMuxPolicy) {
64
+ configuration.rtcpMuxPolicy = config.rtcpMuxPolicy;
65
+ }
66
+
67
+ if (config.iceCandidatePoolSize !== undefined) {
68
+ configuration.iceCandidatePoolSize = config.iceCandidatePoolSize;
69
+ }
70
+
71
+ return configuration;
72
+ }
73
+
74
+ /**
75
+ * Initialize native peer connection with factory
76
+ * @private
77
+ */
78
+ _initializeNativePeerConnection() {
79
+ if (!this._nativePeerConnectionFactory) {
80
+ throw new Error('Native PeerConnection factory not provided');
81
+ }
82
+
83
+ // Create native peer connection
84
+ this._nativePeerConnection = this._nativePeerConnectionFactory.createPeerConnection(
85
+ this._configuration
86
+ );
87
+
88
+ if (!this._nativePeerConnection) {
89
+ throw new Error('Failed to create native peer connection');
90
+ }
91
+
92
+ // Setup observers
93
+ this._setupObservers();
94
+ }
95
+
96
+ /**
97
+ * Setup observers for native peer connection events
98
+ * @private
99
+ */
100
+ _setupObservers() {
101
+ if (!this._nativePeerConnection) {
102
+ return;
103
+ }
104
+
105
+ // Signaling state change
106
+ this._nativePeerConnection.on('signalingstatechange', (state) => {
107
+ this._signalingState = this._convertSignalingState(state);
108
+ this.emit('signalingstatechange');
109
+ });
110
+
111
+ // ICE connection state change
112
+ this._nativePeerConnection.on('iceconnectionstatechange', (state) => {
113
+ this._iceConnectionState = this._convertIceConnectionState(state);
114
+ this.emit('iceconnectionstatechange');
115
+ });
116
+
117
+ // ICE gathering state change
118
+ this._nativePeerConnection.on('icegatheringstatechange', (state) => {
119
+ this._iceGatheringState = this._convertIceGatheringState(state);
120
+ this.emit('icegatheringstatechange');
121
+ });
122
+
123
+ // Connection state change
124
+ this._nativePeerConnection.on('connectionstatechange', (state) => {
125
+ this._connectionState = this._convertConnectionState(state);
126
+ this.emit('connectionstatechange');
127
+ });
128
+
129
+ // ICE candidate
130
+ this._nativePeerConnection.on('icecandidate', (candidate) => {
131
+ const iceCandidate = candidate ? new RTCIceCandidate(candidate) : null;
132
+ this.emit('icecandidate', { candidate: iceCandidate });
133
+ });
134
+
135
+ // Data channel (remote)
136
+ this._nativePeerConnection.on('datachannel', (nativeChannel) => {
137
+ const dataChannel = new RTCDataChannel(nativeChannel, this);
138
+ this._dataChannels.set(dataChannel.label, dataChannel);
139
+ this.emit('datachannel', { channel: dataChannel });
140
+ });
141
+
142
+ // Negotiation needed
143
+ this._nativePeerConnection.on('negotiationneeded', () => {
144
+ this.emit('negotiationneeded');
145
+ });
146
+ }
147
+
148
+ /**
149
+ * The current signaling state
150
+ */
151
+ get signalingState() {
152
+ return this._signalingState;
153
+ }
154
+
155
+ /**
156
+ * The current ICE gathering state
157
+ */
158
+ get iceGatheringState() {
159
+ return this._iceGatheringState;
160
+ }
161
+
162
+ /**
163
+ * The current ICE connection state
164
+ */
165
+ get iceConnectionState() {
166
+ return this._iceConnectionState;
167
+ }
168
+
169
+ /**
170
+ * The current connection state
171
+ */
172
+ get connectionState() {
173
+ return this._connectionState;
174
+ }
175
+
176
+ /**
177
+ * The local description
178
+ */
179
+ get localDescription() {
180
+ return this._currentLocalDescription || this._pendingLocalDescription;
181
+ }
182
+
183
+ /**
184
+ * The remote description
185
+ */
186
+ get remoteDescription() {
187
+ return this._currentRemoteDescription || this._pendingRemoteDescription;
188
+ }
189
+
190
+ /**
191
+ * The pending local description
192
+ */
193
+ get pendingLocalDescription() {
194
+ return this._pendingLocalDescription;
195
+ }
196
+
197
+ /**
198
+ * The pending remote description
199
+ */
200
+ get pendingRemoteDescription() {
201
+ return this._pendingRemoteDescription;
202
+ }
203
+
204
+ /**
205
+ * The current local description
206
+ */
207
+ get currentLocalDescription() {
208
+ return this._currentLocalDescription;
209
+ }
210
+
211
+ /**
212
+ * The current remote description
213
+ */
214
+ get currentRemoteDescription() {
215
+ return this._currentRemoteDescription;
216
+ }
217
+
218
+ /**
219
+ * Create an offer
220
+ * @param {Object} options - Offer options
221
+ * @returns {Promise<RTCSessionDescriptionInit>}
222
+ */
223
+ async createOffer(options = {}) {
224
+ this._checkClosed();
225
+
226
+ if (!this._nativePeerConnection) {
227
+ throw new Error('Native peer connection not available');
228
+ }
229
+
230
+ try {
231
+ const nativeDescription = await this._nativePeerConnection.createOffer(options);
232
+ return new RTCSessionDescription({
233
+ type: nativeDescription.type,
234
+ sdp: nativeDescription.sdp
235
+ });
236
+ } catch (error) {
237
+ throw new Error(`Failed to create offer: ${error.message}`);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Create an answer
243
+ * @param {Object} options - Answer options
244
+ * @returns {Promise<RTCSessionDescriptionInit>}
245
+ */
246
+ async createAnswer(options = {}) {
247
+ this._checkClosed();
248
+
249
+ if (!this._nativePeerConnection) {
250
+ throw new Error('Native peer connection not available');
251
+ }
252
+
253
+ try {
254
+ const nativeDescription = await this._nativePeerConnection.createAnswer(options);
255
+ return new RTCSessionDescription({
256
+ type: nativeDescription.type,
257
+ sdp: nativeDescription.sdp
258
+ });
259
+ } catch (error) {
260
+ throw new Error(`Failed to create answer: ${error.message}`);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Set the local description
266
+ * @param {RTCSessionDescriptionInit} description
267
+ * @returns {Promise<void>}
268
+ */
269
+ async setLocalDescription(description) {
270
+ this._checkClosed();
271
+
272
+ if (!this._nativePeerConnection) {
273
+ throw new Error('Native peer connection not available');
274
+ }
275
+
276
+ try {
277
+ await this._nativePeerConnection.setLocalDescription(description);
278
+
279
+ if (description.type === 'offer') {
280
+ this._pendingLocalDescription = new RTCSessionDescription(description);
281
+ } else if (description.type === 'answer') {
282
+ this._currentLocalDescription = new RTCSessionDescription(description);
283
+ this._pendingLocalDescription = null;
284
+ } else if (description.type === 'rollback') {
285
+ this._pendingLocalDescription = null;
286
+ }
287
+ } catch (error) {
288
+ throw new Error(`Failed to set local description: ${error.message}`);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Set the remote description
294
+ * @param {RTCSessionDescriptionInit} description
295
+ * @returns {Promise<void>}
296
+ */
297
+ async setRemoteDescription(description) {
298
+ this._checkClosed();
299
+
300
+ if (!this._nativePeerConnection) {
301
+ throw new Error('Native peer connection not available');
302
+ }
303
+
304
+ try {
305
+ await this._nativePeerConnection.setRemoteDescription(description);
306
+
307
+ if (description.type === 'offer') {
308
+ this._pendingRemoteDescription = new RTCSessionDescription(description);
309
+ } else if (description.type === 'answer') {
310
+ this._currentRemoteDescription = new RTCSessionDescription(description);
311
+ this._pendingRemoteDescription = null;
312
+ } else if (description.type === 'rollback') {
313
+ this._pendingRemoteDescription = null;
314
+ }
315
+ } catch (error) {
316
+ throw new Error(`Failed to set remote description: ${error.message}`);
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Add an ICE candidate
322
+ * @param {RTCIceCandidateInit} candidate
323
+ * @returns {Promise<void>}
324
+ */
325
+ async addIceCandidate(candidate) {
326
+ this._checkClosed();
327
+
328
+ if (!this._nativePeerConnection) {
329
+ throw new Error('Native peer connection not available');
330
+ }
331
+
332
+ if (!candidate) {
333
+ // End of candidates
334
+ return;
335
+ }
336
+
337
+ try {
338
+ await this._nativePeerConnection.addIceCandidate(candidate);
339
+ } catch (error) {
340
+ throw new Error(`Failed to add ICE candidate: ${error.message}`);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Create a data channel
346
+ * @param {string} label - Channel label
347
+ * @param {Object} dataChannelDict - Channel options
348
+ * @returns {RTCDataChannel}
349
+ */
350
+ createDataChannel(label, dataChannelDict = {}) {
351
+ this._checkClosed();
352
+
353
+ if (!this._nativePeerConnection) {
354
+ throw new Error('Native peer connection not available');
355
+ }
356
+
357
+ const options = {
358
+ ordered: dataChannelDict.ordered !== undefined ? dataChannelDict.ordered : true,
359
+ maxPacketLifeTime: dataChannelDict.maxPacketLifeTime,
360
+ maxRetransmits: dataChannelDict.maxRetransmits,
361
+ protocol: dataChannelDict.protocol || '',
362
+ negotiated: dataChannelDict.negotiated || false,
363
+ id: dataChannelDict.id
364
+ };
365
+
366
+ try {
367
+ const nativeChannel = this._nativePeerConnection.createDataChannel(label, options);
368
+ const dataChannel = new RTCDataChannel(nativeChannel, this);
369
+ this._dataChannels.set(label, dataChannel);
370
+ return dataChannel;
371
+ } catch (error) {
372
+ throw new Error(`Failed to create data channel: ${error.message}`);
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Get configuration
378
+ * @returns {Object}
379
+ */
380
+ getConfiguration() {
381
+ return { ...this._configuration };
382
+ }
383
+
384
+ /**
385
+ * Set configuration
386
+ * @param {Object} configuration
387
+ */
388
+ setConfiguration(configuration) {
389
+ this._checkClosed();
390
+ this._configuration = this._parseConfiguration(configuration);
391
+
392
+ if (this._nativePeerConnection) {
393
+ this._nativePeerConnection.setConfiguration(this._configuration);
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Close the peer connection
399
+ */
400
+ close() {
401
+ if (this._closed) {
402
+ return;
403
+ }
404
+
405
+ this._closed = true;
406
+ this._signalingState = 'closed';
407
+
408
+ // Close all data channels
409
+ for (const [label, channel] of this._dataChannels) {
410
+ channel.close();
411
+ channel.dispose();
412
+ }
413
+ this._dataChannels.clear();
414
+
415
+ // Close native peer connection
416
+ if (this._nativePeerConnection) {
417
+ this._nativePeerConnection.close();
418
+ this._nativePeerConnection.removeAllListeners();
419
+ this._nativePeerConnection = null;
420
+ }
421
+
422
+ this.emit('signalingstatechange');
423
+ this.removeAllListeners();
424
+ }
425
+
426
+ /**
427
+ * Get stats
428
+ * @returns {Promise<Object>}
429
+ */
430
+ async getStats() {
431
+ this._checkClosed();
432
+
433
+ if (!this._nativePeerConnection) {
434
+ throw new Error('Native peer connection not available');
435
+ }
436
+
437
+ try {
438
+ return await this._nativePeerConnection.getStats();
439
+ } catch (error) {
440
+ throw new Error(`Failed to get stats: ${error.message}`);
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Check if connection is closed
446
+ * @private
447
+ */
448
+ _checkClosed() {
449
+ if (this._closed || this._signalingState === 'closed') {
450
+ throw new Error("The RTCPeerConnection's signalingState is 'closed'");
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Convert native signaling state to string
456
+ * @private
457
+ */
458
+ _convertSignalingState(state) {
459
+ const states = ['stable', 'have-local-offer', 'have-remote-offer',
460
+ 'have-local-pranswer', 'have-remote-pranswer', 'closed'];
461
+ return states[state] || 'stable';
462
+ }
463
+
464
+ /**
465
+ * Convert native ICE connection state to string
466
+ * @private
467
+ */
468
+ _convertIceConnectionState(state) {
469
+ const states = ['new', 'checking', 'connected', 'completed',
470
+ 'failed', 'disconnected', 'closed'];
471
+ return states[state] || 'new';
472
+ }
473
+
474
+ /**
475
+ * Convert native ICE gathering state to string
476
+ * @private
477
+ */
478
+ _convertIceGatheringState(state) {
479
+ const states = ['new', 'gathering', 'complete'];
480
+ return states[state] || 'new';
481
+ }
482
+
483
+ /**
484
+ * Convert native connection state to string
485
+ * @private
486
+ */
487
+ _convertConnectionState(state) {
488
+ const states = ['new', 'connecting', 'connected', 'disconnected',
489
+ 'failed', 'closed'];
490
+ return states[state] || 'new';
491
+ }
492
+ }
493
+
494
+ module.exports = RTCPeerConnection;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * RTCPeerConnectionIceEvent is fired when an ICE candidate is available.
3
+ * Ported from Chromium's implementation.
4
+ */
5
+ class RTCPeerConnectionIceEvent {
6
+ constructor(type, eventInitDict = {}) {
7
+ this._type = type;
8
+ this._candidate = eventInitDict.candidate || null;
9
+ this._url = eventInitDict.url || null;
10
+ this._bubbles = eventInitDict.bubbles || false;
11
+ this._cancelable = eventInitDict.cancelable || false;
12
+ this._timestamp = Date.now();
13
+ }
14
+
15
+ /**
16
+ * The event type
17
+ */
18
+ get type() {
19
+ return this._type;
20
+ }
21
+
22
+ /**
23
+ * The RTCIceCandidate associated with the event
24
+ */
25
+ get candidate() {
26
+ return this._candidate;
27
+ }
28
+
29
+ /**
30
+ * The URL of the TURN or STUN server
31
+ */
32
+ get url() {
33
+ return this._url;
34
+ }
35
+
36
+ /**
37
+ * Whether the event bubbles
38
+ */
39
+ get bubbles() {
40
+ return this._bubbles;
41
+ }
42
+
43
+ /**
44
+ * Whether the event is cancelable
45
+ */
46
+ get cancelable() {
47
+ return this._cancelable;
48
+ }
49
+
50
+ /**
51
+ * The timestamp when the event was created
52
+ */
53
+ get timeStamp() {
54
+ return this._timestamp;
55
+ }
56
+ }
57
+
58
+ module.exports = RTCPeerConnectionIceEvent;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * RTCSessionDescription represents a session description.
3
+ * Ported from Chromium's implementation.
4
+ */
5
+ class RTCSessionDescription {
6
+ constructor(descriptionInitDict = {}) {
7
+ this._type = descriptionInitDict.type || '';
8
+ this._sdp = descriptionInitDict.sdp || '';
9
+
10
+ // Validate type
11
+ const validTypes = ['offer', 'answer', 'pranswer', 'rollback'];
12
+ if (this._type && !validTypes.includes(this._type)) {
13
+ throw new Error(`Invalid type: ${this._type}`);
14
+ }
15
+ }
16
+
17
+ /**
18
+ * The type of session description
19
+ * Values: 'offer', 'answer', 'pranswer', 'rollback'
20
+ */
21
+ get type() {
22
+ return this._type;
23
+ }
24
+
25
+ set type(value) {
26
+ const validTypes = ['offer', 'answer', 'pranswer', 'rollback'];
27
+ if (value && !validTypes.includes(value)) {
28
+ throw new Error(`Invalid type: ${value}`);
29
+ }
30
+ this._type = value;
31
+ }
32
+
33
+ /**
34
+ * The SDP string
35
+ */
36
+ get sdp() {
37
+ return this._sdp;
38
+ }
39
+
40
+ set sdp(value) {
41
+ this._sdp = value || '';
42
+ }
43
+
44
+ /**
45
+ * Convert to JSON
46
+ */
47
+ toJSON() {
48
+ return {
49
+ type: this._type,
50
+ sdp: this._sdp
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Convert to string
56
+ */
57
+ toString() {
58
+ return `RTCSessionDescription { type: "${this._type}", sdp: "${this._sdp.substring(0, 50)}..." }`;
59
+ }
60
+ }
61
+
62
+ module.exports = RTCSessionDescription;