node-rtc-connection 1.0.18 → 2.0.4
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/dist/index.cjs +20 -5421
- package/dist/index.mjs +25 -5413
- package/dist/types/crypto/der.d.ts +107 -0
- package/dist/types/crypto/x509.d.ts +56 -0
- package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
- package/dist/types/dtls/RTCCertificate.d.ts +163 -0
- package/dist/types/dtls/cipher.d.ts +81 -0
- package/dist/types/dtls/connection.d.ts +81 -0
- package/dist/types/dtls/prf.d.ts +29 -0
- package/dist/types/dtls/protocol.d.ts +127 -0
- package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
- package/dist/types/foundation/RTCError.d.ts +152 -0
- package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
- package/dist/types/ice/ice-agent.d.ts +154 -0
- package/dist/types/ice/stun-message.d.ts +92 -0
- package/dist/types/index.d.ts +29 -0
- package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
- package/dist/types/sctp/association.d.ts +77 -0
- package/dist/types/sctp/chunks.d.ts +200 -0
- package/dist/types/sctp/crc32c.d.ts +24 -0
- package/dist/types/sctp/datachannel-manager.d.ts +51 -0
- package/dist/types/sctp/dcep.d.ts +56 -0
- package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
- package/dist/types/sdp/sdp-utils.d.ts +103 -0
- package/dist/types/stun/stun-client.d.ts +119 -0
- package/dist/types/transport-stack.d.ts +68 -0
- package/package.json +26 -21
- package/src/crypto/der.ts +205 -0
- package/src/crypto/x509.ts +146 -0
- package/src/datachannel/RTCDataChannel.ts +388 -0
- package/src/dtls/RTCCertificate.ts +396 -0
- package/src/dtls/cipher.ts +198 -0
- package/src/dtls/connection.ts +974 -0
- package/src/dtls/prf.ts +62 -0
- package/src/dtls/protocol.ts +204 -0
- package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
- package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
- package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
- package/src/ice/ice-agent.ts +609 -0
- package/src/ice/stun-message.ts +260 -0
- package/src/index.ts +72 -0
- package/src/peerconnection/RTCPeerConnection.ts +430 -0
- package/src/sctp/association.ts +523 -0
- package/src/sctp/chunks.ts +350 -0
- package/src/sctp/crc32c.ts +57 -0
- package/src/sctp/datachannel-manager.ts +187 -0
- package/src/sctp/dcep.ts +94 -0
- package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
- package/src/sdp/sdp-utils.ts +229 -0
- package/src/stun/stun-client.ts +936 -0
- package/src/transport-stack.ts +165 -0
- package/dist/index.cjs.map +0 -1
- 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/ice/RTCIceTransport.js +0 -998
- 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 -851
- package/src/sctp/RTCSctpTransport.js +0 -253
- package/src/sdp/sdp-utils.js +0 -224
- package/src/stun/stun-client.js +0 -643
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RTCPeerConnection.ts
|
|
3
|
+
* @description WebRTC peer connection driving a real ICE/DTLS/SCTP/DCEP stack.
|
|
4
|
+
* @module peerconnection/RTCPeerConnection
|
|
5
|
+
*
|
|
6
|
+
* This orchestrates signaling (offer/answer + ICE candidate trickle) on top of
|
|
7
|
+
* src/transport-stack.ts, which implements the actual on-the-wire protocols.
|
|
8
|
+
* It interoperates with browser RTCPeerConnection for data channels.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
import RTCCertificate from '../dtls/RTCCertificate';
|
|
15
|
+
import { RTCSessionDescription, RTCSdpType, RTCSessionDescriptionInit } from '../sdp/RTCSessionDescription';
|
|
16
|
+
import { RTCDataChannel, RTCDataChannelInit } from '../datachannel/RTCDataChannel';
|
|
17
|
+
import * as sdpUtils from '../sdp/sdp-utils';
|
|
18
|
+
import { IceCredentials, Fingerprint } from '../sdp/sdp-utils';
|
|
19
|
+
import { TransportStack } from '../transport-stack';
|
|
20
|
+
import type { OpenRequestInfo } from '../sctp/datachannel-manager';
|
|
21
|
+
|
|
22
|
+
/** Configuration accepted by {@link RTCPeerConnection}. */
|
|
23
|
+
export interface RTCConfiguration {
|
|
24
|
+
iceServers?: unknown[];
|
|
25
|
+
iceTransportPolicy?: 'all' | 'relay';
|
|
26
|
+
certificates?: RTCCertificate[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A queued local channel awaiting an established SCTP association. */
|
|
30
|
+
interface PendingChannel {
|
|
31
|
+
channel: RTCDataChannel;
|
|
32
|
+
init: RTCDataChannelInit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Candidate descriptor emitted by the ICE agent through the stack. */
|
|
36
|
+
interface StackCandidate {
|
|
37
|
+
sdp: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const RTCSignalingState = Object.freeze({
|
|
41
|
+
STABLE: 'stable',
|
|
42
|
+
HAVE_LOCAL_OFFER: 'have-local-offer',
|
|
43
|
+
HAVE_REMOTE_OFFER: 'have-remote-offer',
|
|
44
|
+
HAVE_LOCAL_PRANSWER: 'have-local-pranswer',
|
|
45
|
+
HAVE_REMOTE_PRANSWER: 'have-remote-pranswer',
|
|
46
|
+
CLOSED: 'closed',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const RTCIceGatheringState = Object.freeze({ NEW: 'new', GATHERING: 'gathering', COMPLETE: 'complete' });
|
|
50
|
+
|
|
51
|
+
export const RTCPeerConnectionState = Object.freeze({
|
|
52
|
+
NEW: 'new',
|
|
53
|
+
CONNECTING: 'connecting',
|
|
54
|
+
CONNECTED: 'connected',
|
|
55
|
+
DISCONNECTED: 'disconnected',
|
|
56
|
+
FAILED: 'failed',
|
|
57
|
+
CLOSED: 'closed',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export class RTCPeerConnection extends EventEmitter {
|
|
61
|
+
#configuration: RTCConfiguration;
|
|
62
|
+
#signalingState: string;
|
|
63
|
+
#iceGatheringState: string;
|
|
64
|
+
#connectionState: string;
|
|
65
|
+
|
|
66
|
+
#localDescription: RTCSessionDescription | null;
|
|
67
|
+
#remoteDescription: RTCSessionDescription | null;
|
|
68
|
+
|
|
69
|
+
#certificate: RTCCertificate | null;
|
|
70
|
+
#localIce: IceCredentials;
|
|
71
|
+
#remoteIce: sdpUtils.IceParameters | null;
|
|
72
|
+
#remoteFingerprints: Fingerprint[];
|
|
73
|
+
#remoteSetup: string | null;
|
|
74
|
+
|
|
75
|
+
#stack: TransportStack | null;
|
|
76
|
+
#isOfferer: boolean;
|
|
77
|
+
#isClosed: boolean;
|
|
78
|
+
|
|
79
|
+
#pendingChannels: PendingChannel[];
|
|
80
|
+
#channels: Set<RTCDataChannel>;
|
|
81
|
+
#localCandidates: RTCIceCandidateInit[];
|
|
82
|
+
|
|
83
|
+
constructor(configuration: RTCConfiguration = {}) {
|
|
84
|
+
super();
|
|
85
|
+
this.#configuration = configuration;
|
|
86
|
+
this.#signalingState = RTCSignalingState.STABLE;
|
|
87
|
+
this.#iceGatheringState = RTCIceGatheringState.NEW;
|
|
88
|
+
this.#connectionState = RTCPeerConnectionState.NEW;
|
|
89
|
+
|
|
90
|
+
this.#localDescription = null;
|
|
91
|
+
this.#remoteDescription = null;
|
|
92
|
+
|
|
93
|
+
this.#certificate = null;
|
|
94
|
+
this.#localIce = sdpUtils.generateIceCredentials();
|
|
95
|
+
this.#remoteIce = null;
|
|
96
|
+
this.#remoteFingerprints = [];
|
|
97
|
+
this.#remoteSetup = null;
|
|
98
|
+
|
|
99
|
+
this.#stack = null;
|
|
100
|
+
this.#isOfferer = false;
|
|
101
|
+
this.#isClosed = false;
|
|
102
|
+
|
|
103
|
+
// Data channels created locally before the stack is ready are queued and
|
|
104
|
+
// opened once SCTP is established.
|
|
105
|
+
this.#pendingChannels = [];
|
|
106
|
+
this.#channels = new Set();
|
|
107
|
+
this.#localCandidates = [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- lazy init ----------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
async #ensureCertificate(): Promise<RTCCertificate> {
|
|
113
|
+
if (!this.#certificate) {
|
|
114
|
+
if (this.#configuration.certificates && this.#configuration.certificates[0]) {
|
|
115
|
+
this.#certificate = this.#configuration.certificates[0];
|
|
116
|
+
} else {
|
|
117
|
+
this.#certificate = await RTCCertificate.generateCertificate();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return this.#certificate;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create the transport stack once roles are known.
|
|
125
|
+
* @param {'controlling'|'controlled'} iceRole
|
|
126
|
+
* @param {'client'|'server'} dtlsRole
|
|
127
|
+
*/
|
|
128
|
+
#createStack(iceRole: 'controlling' | 'controlled', dtlsRole: 'client' | 'server'): TransportStack {
|
|
129
|
+
if (this.#stack) return this.#stack;
|
|
130
|
+
|
|
131
|
+
const certificate = this.#certificate;
|
|
132
|
+
if (!certificate) {
|
|
133
|
+
throw new Error('Certificate not initialized');
|
|
134
|
+
}
|
|
135
|
+
const certDer = certificate.getCertificateDer();
|
|
136
|
+
if (!certDer) {
|
|
137
|
+
throw new Error('Certificate has no DER encoding');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const stack = new TransportStack({
|
|
141
|
+
iceRole,
|
|
142
|
+
dtlsRole,
|
|
143
|
+
localUfrag: this.#localIce.usernameFragment,
|
|
144
|
+
localPwd: this.#localIce.password,
|
|
145
|
+
certDer,
|
|
146
|
+
privateKey: certificate.getPrivateKeyObject(),
|
|
147
|
+
verifyFingerprint: (fp: { algorithm: string; value: string }) => this.#verifyRemoteFingerprint(fp),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
stack.on('candidate', (c: StackCandidate) => {
|
|
151
|
+
const init: RTCIceCandidateInit = { candidate: c.sdp, sdpMid: '0', sdpMLineIndex: 0, usernameFragment: this.#localIce.usernameFragment };
|
|
152
|
+
this.#localCandidates.push(init);
|
|
153
|
+
this.emit('icecandidate', { candidate: init });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
stack.on('iceconnected', () => this.#setConnectionState(RTCPeerConnectionState.CONNECTING));
|
|
157
|
+
stack.on('sctpconnected', () => this.#setConnectionState(RTCPeerConnectionState.CONNECTED));
|
|
158
|
+
stack.on('error', (e: unknown) => {
|
|
159
|
+
this.emit('error', e);
|
|
160
|
+
this.#setConnectionState(RTCPeerConnectionState.FAILED);
|
|
161
|
+
});
|
|
162
|
+
stack.on('close', () => this.#setConnectionState(RTCPeerConnectionState.DISCONNECTED));
|
|
163
|
+
|
|
164
|
+
// Inbound (remotely-initiated) data channels.
|
|
165
|
+
stack.on('datachannel-request', (info: OpenRequestInfo) => {
|
|
166
|
+
const channel = new RTCDataChannel(info.label, {
|
|
167
|
+
ordered: info.ordered,
|
|
168
|
+
protocol: info.protocol,
|
|
169
|
+
id: info.streamId,
|
|
170
|
+
});
|
|
171
|
+
stack.acceptChannel(channel, info);
|
|
172
|
+
this.#channels.add(channel);
|
|
173
|
+
this.emit('datachannel', { channel });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Open any queued local channels when SCTP is ready.
|
|
177
|
+
stack.on('ready', () => {
|
|
178
|
+
for (const { channel, init } of this.#pendingChannels) {
|
|
179
|
+
stack.openChannel(channel, init);
|
|
180
|
+
}
|
|
181
|
+
this.#pendingChannels = [];
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this.#stack = stack;
|
|
185
|
+
return stack;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#verifyRemoteFingerprint(fp: { algorithm: string; value: string }): boolean {
|
|
189
|
+
if (this.#remoteFingerprints.length === 0) return true; // not yet known
|
|
190
|
+
return this.#remoteFingerprints.some(
|
|
191
|
+
(rf) => rf.algorithm === fp.algorithm && rf.value.toUpperCase() === fp.value.toUpperCase()
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---- data channels ------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
createDataChannel(label: string, options: RTCDataChannelInit = {}): RTCDataChannel {
|
|
198
|
+
if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
|
|
199
|
+
const channel = new RTCDataChannel(label, options);
|
|
200
|
+
this.#channels.add(channel);
|
|
201
|
+
|
|
202
|
+
const init: RTCDataChannelInit = {
|
|
203
|
+
ordered: options.ordered !== false,
|
|
204
|
+
maxRetransmits: options.maxRetransmits,
|
|
205
|
+
maxPacketLifeTime: options.maxPacketLifeTime,
|
|
206
|
+
protocol: options.protocol || '',
|
|
207
|
+
negotiated: options.negotiated || false,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (this.#stack && this.#stack.isReady()) {
|
|
211
|
+
this.#stack.openChannel(channel, init);
|
|
212
|
+
} else {
|
|
213
|
+
this.#pendingChannels.push({ channel, init });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
setImmediate(() => { if (!this.#isClosed) this.emit('negotiationneeded'); });
|
|
217
|
+
return channel;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- signaling ----------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
async createOffer(): Promise<RTCSessionDescription> {
|
|
223
|
+
if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
|
|
224
|
+
await this.#ensureCertificate();
|
|
225
|
+
this.#isOfferer = true;
|
|
226
|
+
|
|
227
|
+
const fp = this.#pickFingerprint();
|
|
228
|
+
const sdp = sdpUtils.generateOffer({
|
|
229
|
+
iceUfrag: this.#localIce.usernameFragment,
|
|
230
|
+
icePwd: this.#localIce.password,
|
|
231
|
+
fingerprint: fp,
|
|
232
|
+
setup: 'actpass',
|
|
233
|
+
candidates: [],
|
|
234
|
+
});
|
|
235
|
+
return new RTCSessionDescription({ type: RTCSdpType.OFFER, sdp });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async createAnswer(): Promise<RTCSessionDescription> {
|
|
239
|
+
if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
|
|
240
|
+
if (!this.#remoteDescription || this.#remoteDescription.type !== 'offer') {
|
|
241
|
+
throw new Error('Cannot create answer without remote offer');
|
|
242
|
+
}
|
|
243
|
+
await this.#ensureCertificate();
|
|
244
|
+
|
|
245
|
+
// Answerer takes the active (DTLS client) role when offer is actpass.
|
|
246
|
+
const fp = this.#pickFingerprint();
|
|
247
|
+
const sdp = sdpUtils.generateAnswer({
|
|
248
|
+
iceUfrag: this.#localIce.usernameFragment,
|
|
249
|
+
icePwd: this.#localIce.password,
|
|
250
|
+
fingerprint: fp,
|
|
251
|
+
setup: 'active',
|
|
252
|
+
candidates: [],
|
|
253
|
+
});
|
|
254
|
+
return new RTCSessionDescription({ type: RTCSdpType.ANSWER, sdp });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#pickFingerprint(): Fingerprint | undefined {
|
|
258
|
+
const certificate = this.#certificate;
|
|
259
|
+
if (!certificate) {
|
|
260
|
+
throw new Error('Certificate not initialized');
|
|
261
|
+
}
|
|
262
|
+
const fps = certificate.getFingerprints();
|
|
263
|
+
return fps.find((f) => f.algorithm === 'sha-256') || fps[0];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async setLocalDescription(description?: RTCSessionDescriptionInit | RTCSessionDescription): Promise<void> {
|
|
267
|
+
if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
|
|
268
|
+
const desc: RTCSessionDescriptionInit | RTCSessionDescription = description
|
|
269
|
+
? description
|
|
270
|
+
: this.#signalingState === RTCSignalingState.HAVE_REMOTE_OFFER
|
|
271
|
+
? await this.createAnswer()
|
|
272
|
+
: await this.createOffer();
|
|
273
|
+
await this.#ensureCertificate();
|
|
274
|
+
this.#localDescription = new RTCSessionDescription({ type: desc.type ?? undefined, sdp: desc.sdp ?? undefined });
|
|
275
|
+
|
|
276
|
+
if (desc.type === 'offer') {
|
|
277
|
+
this.#signalingState = RTCSignalingState.HAVE_LOCAL_OFFER;
|
|
278
|
+
} else if (desc.type === 'answer') {
|
|
279
|
+
this.#signalingState = RTCSignalingState.STABLE;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Determine roles and bring up the stack so we start gathering immediately.
|
|
283
|
+
this.#setupRolesAndStack(desc, /*local*/ true);
|
|
284
|
+
|
|
285
|
+
this.#iceGatheringState = RTCIceGatheringState.GATHERING;
|
|
286
|
+
this.emit('icegatheringstatechange');
|
|
287
|
+
if (this.#stack) {
|
|
288
|
+
await this.#stack.gather({
|
|
289
|
+
iceServers: this.#configuration.iceServers || [],
|
|
290
|
+
iceTransportPolicy: this.#configuration.iceTransportPolicy || 'all',
|
|
291
|
+
});
|
|
292
|
+
this.#iceGatheringState = RTCIceGatheringState.COMPLETE;
|
|
293
|
+
this.emit('icegatheringstatechange');
|
|
294
|
+
// Signal end-of-candidates.
|
|
295
|
+
this.emit('icecandidate', { candidate: null });
|
|
296
|
+
this.#maybeStartChecks();
|
|
297
|
+
}
|
|
298
|
+
this.emit('signalingstatechange');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void> {
|
|
302
|
+
if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
|
|
303
|
+
if (!description || !description.sdp) throw new Error('Invalid session description');
|
|
304
|
+
await this.#ensureCertificate();
|
|
305
|
+
|
|
306
|
+
this.#remoteDescription = new RTCSessionDescription(description);
|
|
307
|
+
if (description.type === 'offer') {
|
|
308
|
+
this.#signalingState = RTCSignalingState.HAVE_REMOTE_OFFER;
|
|
309
|
+
} else if (description.type === 'answer') {
|
|
310
|
+
this.#signalingState = RTCSignalingState.STABLE;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.#remoteIce = sdpUtils.parseIceParameters(description.sdp);
|
|
314
|
+
const dtls = sdpUtils.parseDtlsParameters(description.sdp);
|
|
315
|
+
this.#remoteFingerprints = dtls.fingerprints;
|
|
316
|
+
this.#remoteSetup = dtls.setup ?? null;
|
|
317
|
+
|
|
318
|
+
this.#setupRolesAndStack(description, /*local*/ false);
|
|
319
|
+
|
|
320
|
+
// Apply remote credentials + any in-SDP candidates, then start checks.
|
|
321
|
+
if (this.#stack && this.#remoteIce.usernameFragment) {
|
|
322
|
+
this.#stack.setRemote(this.#remoteIce.usernameFragment, this.#remoteIce.password ?? '');
|
|
323
|
+
for (const c of sdpUtils.parseCandidates(description.sdp)) {
|
|
324
|
+
this.#stack.addRemoteCandidate(c);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
this.#maybeStartChecks();
|
|
328
|
+
this.emit('signalingstatechange');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Decide ICE controlling/controlled and DTLS client/server, then create the
|
|
333
|
+
* stack. Offerer is ICE-controlling. DTLS roles follow a=setup: the side that
|
|
334
|
+
* ends up 'active' is the DTLS client.
|
|
335
|
+
*/
|
|
336
|
+
#setupRolesAndStack(_description: RTCSessionDescriptionInit | RTCSessionDescription, _isLocal: boolean): void {
|
|
337
|
+
if (this.#stack) return;
|
|
338
|
+
|
|
339
|
+
const iceRole: 'controlling' | 'controlled' = this.#isOfferer ? 'controlling' : 'controlled';
|
|
340
|
+
|
|
341
|
+
// Determine our DTLS role.
|
|
342
|
+
let dtlsRole: 'client' | 'server';
|
|
343
|
+
if (this.#isOfferer) {
|
|
344
|
+
// We offered actpass; the answerer chooses. We learn it from their answer
|
|
345
|
+
// (setup:active => they are client => we are server). Until we see the
|
|
346
|
+
// answer we default to server (passive), which matches answerer=active.
|
|
347
|
+
if (this.#remoteSetup === 'active') dtlsRole = 'server';
|
|
348
|
+
else if (this.#remoteSetup === 'passive') dtlsRole = 'client';
|
|
349
|
+
else dtlsRole = 'server';
|
|
350
|
+
} else {
|
|
351
|
+
// We are the answerer; we chose 'active' in createAnswer => DTLS client.
|
|
352
|
+
dtlsRole = 'client';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.#createStack(iceRole, dtlsRole);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#maybeStartChecks(): void {
|
|
359
|
+
// Once both local gathering started and remote creds exist, checks run.
|
|
360
|
+
if (this.#stack && this.#remoteIce && this.#remoteIce.usernameFragment) {
|
|
361
|
+
this.#stack.setRemote(this.#remoteIce.usernameFragment, this.#remoteIce.password ?? '');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async addIceCandidate(candidate: RTCIceCandidateInit | string): Promise<void> {
|
|
366
|
+
if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
|
|
367
|
+
if (!candidate || (typeof candidate !== 'string' && candidate.candidate === '')) {
|
|
368
|
+
return; // end-of-candidates
|
|
369
|
+
}
|
|
370
|
+
const candidateStr = typeof candidate === 'string' ? candidate : (candidate.candidate || '');
|
|
371
|
+
const parsed = sdpUtils.parseCandidateLine(candidateStr);
|
|
372
|
+
if (parsed && this.#stack) {
|
|
373
|
+
this.#stack.addRemoteCandidate(parsed);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---- state --------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
#setConnectionState(state: string): void {
|
|
380
|
+
if (this.#connectionState !== state) {
|
|
381
|
+
this.#connectionState = state;
|
|
382
|
+
this.emit('connectionstatechange');
|
|
383
|
+
this.emit('iceconnectionstatechange');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
getConfiguration(): RTCConfiguration { return { ...this.#configuration }; }
|
|
388
|
+
setConfiguration(configuration: RTCConfiguration): void {
|
|
389
|
+
if (this.#isClosed) throw new Error('RTCPeerConnection is closed');
|
|
390
|
+
this.#configuration = { ...configuration };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
close(): void {
|
|
394
|
+
if (this.#isClosed) return;
|
|
395
|
+
this.#isClosed = true;
|
|
396
|
+
this.#signalingState = RTCSignalingState.CLOSED;
|
|
397
|
+
for (const channel of this.#channels) {
|
|
398
|
+
try { channel.close(); } catch (_) { /* best-effort */ }
|
|
399
|
+
}
|
|
400
|
+
if (this.#stack) try { this.#stack.close(); } catch (_) { /* best-effort */ }
|
|
401
|
+
this.#setConnectionState(RTCPeerConnectionState.CLOSED);
|
|
402
|
+
this.emit('signalingstatechange');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
get signalingState(): string { return this.#signalingState; }
|
|
406
|
+
get iceGatheringState(): string { return this.#iceGatheringState; }
|
|
407
|
+
get iceConnectionState(): string {
|
|
408
|
+
return this.#connectionState === RTCPeerConnectionState.CONNECTED ? 'connected'
|
|
409
|
+
: this.#connectionState === RTCPeerConnectionState.CONNECTING ? 'checking'
|
|
410
|
+
: this.#connectionState === RTCPeerConnectionState.FAILED ? 'failed'
|
|
411
|
+
: 'new';
|
|
412
|
+
}
|
|
413
|
+
get connectionState(): string { return this.#connectionState; }
|
|
414
|
+
get localDescription(): RTCSessionDescription | null { return this.#localDescription; }
|
|
415
|
+
get remoteDescription(): RTCSessionDescription | null { return this.#remoteDescription; }
|
|
416
|
+
get currentLocalDescription(): RTCSessionDescription | null { return this.#localDescription; }
|
|
417
|
+
get currentRemoteDescription(): RTCSessionDescription | null { return this.#remoteDescription; }
|
|
418
|
+
get pendingLocalDescription(): RTCSessionDescription | null { return this.#signalingState === RTCSignalingState.STABLE ? null : this.#localDescription; }
|
|
419
|
+
get pendingRemoteDescription(): RTCSessionDescription | null { return this.#signalingState === RTCSignalingState.STABLE ? null : this.#remoteDescription; }
|
|
420
|
+
get canTrickleIceCandidates(): boolean { return true; }
|
|
421
|
+
get sctp(): TransportStack['sctp'] { return this.#stack ? this.#stack.sctp : null; }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** ICE candidate init shape (subset of the W3C dictionary). */
|
|
425
|
+
interface RTCIceCandidateInit {
|
|
426
|
+
candidate: string;
|
|
427
|
+
sdpMid?: string;
|
|
428
|
+
sdpMLineIndex?: number;
|
|
429
|
+
usernameFragment?: string;
|
|
430
|
+
}
|