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.
- package/README.md +94 -85
- package/index.cjs +1 -0
- package/index.mjs +1 -0
- package/package.json +14 -46
- package/types/crypto/der.d.ts +107 -0
- package/types/crypto/x509.d.ts +56 -0
- package/types/datachannel/RTCDataChannel.d.ts +179 -0
- package/types/dtls/RTCCertificate.d.ts +163 -0
- package/types/dtls/cipher.d.ts +81 -0
- package/types/dtls/connection.d.ts +81 -0
- package/types/dtls/prf.d.ts +29 -0
- package/types/dtls/protocol.d.ts +127 -0
- package/types/foundation/ByteBufferQueue.d.ts +71 -0
- package/types/foundation/RTCError.d.ts +152 -0
- package/types/ice/RTCIceCandidate.d.ts +161 -0
- package/types/ice/ice-agent.d.ts +154 -0
- package/types/ice/stun-message.d.ts +92 -0
- package/types/index.d.ts +29 -0
- package/types/peerconnection/RTCPeerConnection.d.ts +74 -0
- package/types/sctp/association.d.ts +77 -0
- package/types/sctp/chunks.d.ts +200 -0
- package/types/sctp/crc32c.d.ts +24 -0
- package/types/sctp/datachannel-manager.d.ts +51 -0
- package/types/sctp/dcep.d.ts +56 -0
- package/types/sdp/RTCSessionDescription.d.ts +73 -0
- package/types/sdp/sdp-utils.d.ts +103 -0
- package/types/stun/stun-client.d.ts +119 -0
- package/types/transport-stack.d.ts +68 -0
- package/dist/index.cjs +0 -5618
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs +0 -5616
- package/dist/index.mjs.map +0 -1
- package/src/datachannel/RTCDataChannel.js +0 -354
- package/src/dtls/RTCCertificate.js +0 -310
- package/src/dtls/RTCDtlsTransport.js +0 -247
- package/src/foundation/ByteBufferQueue.js +0 -235
- package/src/foundation/RTCError.js +0 -226
- package/src/ice/RTCIceCandidate.js +0 -301
- package/src/ice/RTCIceTransport.js +0 -1018
- package/src/index.d.ts +0 -400
- package/src/index.js +0 -92
- package/src/network/network-transport.js +0 -478
- package/src/peerconnection/RTCPeerConnection.js +0 -875
- package/src/sctp/RTCSctpTransport.js +0 -253
- package/src/sdp/RTCSessionDescription.js +0 -102
- package/src/sdp/sdp-utils.js +0 -224
- 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
|
-
};
|