@mertushka/webrtc-node 0.1.0-alpha.0
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/CMakeLists.txt +143 -0
- package/LICENSE +373 -0
- package/README.md +166 -0
- package/docs/README.md +14 -0
- package/docs/architecture.md +38 -0
- package/docs/conformance.md +53 -0
- package/docs/development.md +161 -0
- package/docs/divergences.md +230 -0
- package/examples/datachannel.js +57 -0
- package/index.d.ts +340 -0
- package/lib/index.js +4139 -0
- package/lib/load-native.js +42 -0
- package/package.json +94 -0
- package/scripts/check-api-surface.js +149 -0
- package/scripts/check-ci-evidence.js +124 -0
- package/scripts/check-native-integration.js +124 -0
- package/scripts/check-package-artifact.js +91 -0
- package/scripts/check-prebuilds.js +31 -0
- package/scripts/check-wpt-results.js +117 -0
- package/scripts/check-wpt-selection.js +72 -0
- package/scripts/ensure-wpt.js +118 -0
- package/scripts/install-native.js +116 -0
- package/scripts/package-prebuild.js +69 -0
- package/scripts/print-wpt-manifest.js +7 -0
- package/scripts/run-docker-linux-ci.ps1 +73 -0
- package/scripts/run-docker-linux-ci.sh +97 -0
- package/scripts/run-wpt-smoke.js +32 -0
- package/scripts/run-wpt-subset.js +1193 -0
- package/scripts/write-ci-evidence.js +118 -0
- package/scripts/write-wpt-report.js +143 -0
- package/src/native/addon.cc +1202 -0
- package/wpt-manifest.json +129 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,4139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const native = require("./load-native");
|
|
5
|
+
|
|
6
|
+
const kHandlers = new WeakMap();
|
|
7
|
+
const localDescriptionOwners = new Map();
|
|
8
|
+
const kInternalConstruct = Symbol("internalConstruct");
|
|
9
|
+
|
|
10
|
+
function descriptionPairingKey(description) {
|
|
11
|
+
if (!description || typeof description.sdp !== "string" || !description.sdp) return null;
|
|
12
|
+
return `${description.type}\n${description.sdp}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class SimpleEvent {
|
|
16
|
+
constructor(type, init = {}) {
|
|
17
|
+
this.type = String(type);
|
|
18
|
+
this.bubbles = Boolean(init.bubbles);
|
|
19
|
+
this.cancelable = Boolean(init.cancelable);
|
|
20
|
+
this.defaultPrevented = false;
|
|
21
|
+
this.target = null;
|
|
22
|
+
this.currentTarget = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
preventDefault() {
|
|
26
|
+
if (this.cancelable) this.defaultPrevented = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class SimpleMessageEvent extends SimpleEvent {
|
|
31
|
+
constructor(type, init = {}) {
|
|
32
|
+
super(type, init);
|
|
33
|
+
this.data = init.data;
|
|
34
|
+
this.origin = init.origin || "";
|
|
35
|
+
this.lastEventId = init.lastEventId || "";
|
|
36
|
+
this.source = init.source || null;
|
|
37
|
+
this.ports = init.ports || [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class SimpleEventTarget {
|
|
42
|
+
addEventListener(type, callback, options = undefined) {
|
|
43
|
+
if (callback == null) return;
|
|
44
|
+
let map = kHandlers.get(this);
|
|
45
|
+
if (!map) {
|
|
46
|
+
map = new Map();
|
|
47
|
+
kHandlers.set(this, map);
|
|
48
|
+
}
|
|
49
|
+
const key = String(type);
|
|
50
|
+
let listeners = map.get(key);
|
|
51
|
+
if (!listeners) {
|
|
52
|
+
listeners = [];
|
|
53
|
+
map.set(key, listeners);
|
|
54
|
+
}
|
|
55
|
+
const once = typeof options === "object" && options !== null && Boolean(options.once);
|
|
56
|
+
if (!listeners.some((listener) => listener.callback === callback)) {
|
|
57
|
+
listeners.push({ callback, once });
|
|
58
|
+
}
|
|
59
|
+
if (typeof this._eventListenerAdded === "function") this._eventListenerAdded(key);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
removeEventListener(type, callback) {
|
|
63
|
+
const listeners = kHandlers.get(this)?.get(String(type));
|
|
64
|
+
if (!listeners) return;
|
|
65
|
+
const index = listeners.findIndex((listener) => listener.callback === callback);
|
|
66
|
+
if (index !== -1) listeners.splice(index, 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
dispatchEvent(event) {
|
|
70
|
+
if (!event || typeof event.type !== "string") {
|
|
71
|
+
throw new TypeError("dispatchEvent requires an Event object");
|
|
72
|
+
}
|
|
73
|
+
event.target = event.target || this;
|
|
74
|
+
event.currentTarget = this;
|
|
75
|
+
const listeners = Array.from(kHandlers.get(this)?.get(event.type) || []);
|
|
76
|
+
const handler = this[`on${event.type}`];
|
|
77
|
+
for (const listener of listeners) {
|
|
78
|
+
callListener(listener.callback, this, event);
|
|
79
|
+
if (listener.once) this.removeEventListener(event.type, listener.callback);
|
|
80
|
+
}
|
|
81
|
+
if (typeof handler === "function") callListener(handler, this, event);
|
|
82
|
+
return !event.defaultPrevented;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_hasEventConsumer(type) {
|
|
86
|
+
const listeners = kHandlers.get(this)?.get(String(type));
|
|
87
|
+
return Boolean((listeners && listeners.length > 0) || typeof this[`on${type}`] === "function");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function callListener(listener, target, event) {
|
|
92
|
+
if (typeof listener === "function") {
|
|
93
|
+
listener.call(target, event);
|
|
94
|
+
} else if (listener && typeof listener.handleEvent === "function") {
|
|
95
|
+
listener.handleEvent(event);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function makeEvent(type, init) {
|
|
100
|
+
return new SimpleEvent(type, init);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function makeMessageEvent(type, init) {
|
|
104
|
+
return new SimpleMessageEvent(type, init);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function makeDOMException(message, name) {
|
|
108
|
+
if (typeof globalThis.DOMException === "function") {
|
|
109
|
+
return new globalThis.DOMException(message, name);
|
|
110
|
+
}
|
|
111
|
+
const error = new Error(message);
|
|
112
|
+
error.name = name;
|
|
113
|
+
return error;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function mapNativeError(error, fallbackName = "OperationError") {
|
|
117
|
+
if (error?.name && error.name !== "Error") return error;
|
|
118
|
+
return makeDOMException(error?.message || String(error), fallbackName);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function byteLength(value) {
|
|
122
|
+
return Buffer.byteLength(String(value), "utf8");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function enforceRange(value, name, max = 65535) {
|
|
126
|
+
const number = Number(value);
|
|
127
|
+
if (!Number.isInteger(number) || number < 0 || number > max) {
|
|
128
|
+
throw new TypeError(`${name} must be an integer between 0 and ${max}`);
|
|
129
|
+
}
|
|
130
|
+
return number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toUnsignedLong(value) {
|
|
134
|
+
if (typeof value === "bigint" || Object.prototype.toString.call(value) === "[object BigInt]") {
|
|
135
|
+
throw new TypeError("Cannot convert BigInt to unsigned long");
|
|
136
|
+
}
|
|
137
|
+
const number = Number(value);
|
|
138
|
+
if (!Number.isFinite(number) || number === 0) return 0;
|
|
139
|
+
const integer = Math.trunc(number);
|
|
140
|
+
const modulo = 2 ** 32;
|
|
141
|
+
return ((integer % modulo) + modulo) % modulo;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateByteLength(value, name) {
|
|
145
|
+
if (byteLength(value) > 65535) {
|
|
146
|
+
throw new TypeError(`${name} exceeds 65535 bytes`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function fingerprintFromBytes(bytes) {
|
|
151
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(":");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class RTCCertificate {
|
|
155
|
+
constructor({
|
|
156
|
+
expires,
|
|
157
|
+
algorithm = "ECDSA",
|
|
158
|
+
fingerprints,
|
|
159
|
+
certificatePem = null,
|
|
160
|
+
keyPem = null,
|
|
161
|
+
} = {}) {
|
|
162
|
+
this.expires = expires;
|
|
163
|
+
this._algorithm = algorithm;
|
|
164
|
+
this._fingerprints =
|
|
165
|
+
Array.isArray(fingerprints) && fingerprints.length
|
|
166
|
+
? fingerprints.map((fingerprint) => ({
|
|
167
|
+
algorithm: String(fingerprint.algorithm),
|
|
168
|
+
value: String(fingerprint.value).toLowerCase(),
|
|
169
|
+
}))
|
|
170
|
+
: [
|
|
171
|
+
{
|
|
172
|
+
algorithm: "sha-256",
|
|
173
|
+
value: fingerprintFromBytes(crypto.randomBytes(32)),
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
this._certificatePem = certificatePem;
|
|
177
|
+
this._keyPem = keyPem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getFingerprints() {
|
|
181
|
+
return this._fingerprints.map((fingerprint) => ({ ...fingerprint }));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeEnum(value, name, allowed, defaultValue) {
|
|
186
|
+
if (value === undefined) return defaultValue;
|
|
187
|
+
if (value === null || !allowed.includes(value)) {
|
|
188
|
+
throw new TypeError(`${name} must be one of: ${allowed.join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function validateIceServerUrl(url) {
|
|
194
|
+
if (typeof url !== "string") throw new TypeError("RTCIceServer.urls must contain strings");
|
|
195
|
+
const match = /^([a-z][a-z0-9+.-]*):(.*)$/i.exec(url);
|
|
196
|
+
if (!match) throw makeDOMException("Invalid ICE server URL", "SyntaxError");
|
|
197
|
+
const scheme = match[1].toLowerCase();
|
|
198
|
+
const rest = match[2];
|
|
199
|
+
if (!["stun", "stuns", "turn", "turns"].includes(scheme)) {
|
|
200
|
+
throw makeDOMException("Unsupported ICE server URL scheme", "SyntaxError");
|
|
201
|
+
}
|
|
202
|
+
if (!rest || rest.startsWith("//") || /[/\\#@]/.test(rest)) {
|
|
203
|
+
throw makeDOMException("Invalid ICE server URL", "SyntaxError");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const [hostPort, query = ""] = rest.split("?");
|
|
207
|
+
if (rest.split("?").length > 2 || !hostPort) {
|
|
208
|
+
throw makeDOMException("Invalid ICE server URL", "SyntaxError");
|
|
209
|
+
}
|
|
210
|
+
if ((scheme === "stun" || scheme === "stuns") && query) {
|
|
211
|
+
throw makeDOMException("STUN URL must not contain a query", "SyntaxError");
|
|
212
|
+
}
|
|
213
|
+
if ((scheme === "turn" || scheme === "turns") && query && !/^transport=(udp|tcp)$/i.test(query)) {
|
|
214
|
+
throw makeDOMException("Invalid TURN transport", "SyntaxError");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let host = hostPort;
|
|
218
|
+
let port = "";
|
|
219
|
+
if (hostPort.startsWith("[")) {
|
|
220
|
+
const end = hostPort.indexOf("]");
|
|
221
|
+
if (end <= 1) throw makeDOMException("Invalid ICE server host", "SyntaxError");
|
|
222
|
+
host = hostPort.slice(1, end);
|
|
223
|
+
const suffix = hostPort.slice(end + 1);
|
|
224
|
+
if (suffix) {
|
|
225
|
+
if (!suffix.startsWith(":")) throw makeDOMException("Invalid ICE server port", "SyntaxError");
|
|
226
|
+
port = suffix.slice(1);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const colonCount = (hostPort.match(/:/g) || []).length;
|
|
230
|
+
if (colonCount > 1) throw makeDOMException("Invalid ICE server host", "SyntaxError");
|
|
231
|
+
if (colonCount === 1) {
|
|
232
|
+
const index = hostPort.lastIndexOf(":");
|
|
233
|
+
host = hostPort.slice(0, index);
|
|
234
|
+
port = hostPort.slice(index + 1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!host) throw makeDOMException("Invalid ICE server host", "SyntaxError");
|
|
238
|
+
if (port !== "") {
|
|
239
|
+
const number = Number(port);
|
|
240
|
+
if (!/^\d+$/.test(port) || !Number.isInteger(number) || number < 0 || number > 65535) {
|
|
241
|
+
throw makeDOMException("Invalid ICE server port", "SyntaxError");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeIceServers(iceServers) {
|
|
247
|
+
if (iceServers === undefined) return [];
|
|
248
|
+
if (!Array.isArray(iceServers)) throw new TypeError("iceServers must be an array");
|
|
249
|
+
return iceServers.map((server) => {
|
|
250
|
+
if (server === null || typeof server !== "object")
|
|
251
|
+
throw new TypeError("RTCIceServer must be an object");
|
|
252
|
+
if (!Object.hasOwn(server, "urls")) {
|
|
253
|
+
throw new TypeError("RTCIceServer.urls is required");
|
|
254
|
+
}
|
|
255
|
+
const urls = Array.isArray(server.urls) ? server.urls.map(String) : [String(server.urls)];
|
|
256
|
+
if (urls.length === 0)
|
|
257
|
+
throw makeDOMException("RTCIceServer.urls must not be empty", "SyntaxError");
|
|
258
|
+
for (const url of urls) validateIceServerUrl(url);
|
|
259
|
+
|
|
260
|
+
const requiresCredentials = urls.some((url) => /^turns?:/i.test(url));
|
|
261
|
+
const username = server.username === undefined ? undefined : String(server.username);
|
|
262
|
+
const credential = server.credential === undefined ? undefined : String(server.credential);
|
|
263
|
+
if (requiresCredentials) {
|
|
264
|
+
if (username === undefined || credential === undefined || credential === "") {
|
|
265
|
+
throw makeDOMException(
|
|
266
|
+
"TURN servers require username and credential",
|
|
267
|
+
"InvalidAccessError",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (byteLength(username) > 509) {
|
|
271
|
+
throw makeDOMException("TURN username exceeds 509 bytes", "InvalidAccessError");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const normalized = { urls };
|
|
276
|
+
if (username !== undefined) normalized.username = username;
|
|
277
|
+
if (credential !== undefined) normalized.credential = credential;
|
|
278
|
+
return normalized;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizePeerConnectionConfiguration(configuration) {
|
|
283
|
+
if (configuration == null) configuration = {};
|
|
284
|
+
if (typeof configuration !== "object") configuration = {};
|
|
285
|
+
if (configuration.certificates === null) {
|
|
286
|
+
throw new TypeError("certificates must not be null");
|
|
287
|
+
}
|
|
288
|
+
const certificates =
|
|
289
|
+
configuration.certificates === undefined ? [] : Array.from(configuration.certificates);
|
|
290
|
+
for (const certificate of certificates) {
|
|
291
|
+
if (certificate == null) throw new TypeError("certificates must not contain null or undefined");
|
|
292
|
+
if (certificate instanceof RTCCertificate && certificate.expires <= Date.now()) {
|
|
293
|
+
throw makeDOMException("RTCCertificate has expired", "InvalidAccessError");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
iceServers: normalizeIceServers(configuration.iceServers),
|
|
298
|
+
iceTransportPolicy: normalizeEnum(
|
|
299
|
+
configuration.iceTransportPolicy,
|
|
300
|
+
"iceTransportPolicy",
|
|
301
|
+
["all", "relay"],
|
|
302
|
+
"all",
|
|
303
|
+
),
|
|
304
|
+
bundlePolicy: normalizeEnum(
|
|
305
|
+
configuration.bundlePolicy,
|
|
306
|
+
"bundlePolicy",
|
|
307
|
+
["balanced", "max-compat", "max-bundle"],
|
|
308
|
+
"balanced",
|
|
309
|
+
),
|
|
310
|
+
rtcpMuxPolicy: normalizeEnum(
|
|
311
|
+
configuration.rtcpMuxPolicy,
|
|
312
|
+
"rtcpMuxPolicy",
|
|
313
|
+
["require"],
|
|
314
|
+
"require",
|
|
315
|
+
),
|
|
316
|
+
iceCandidatePoolSize:
|
|
317
|
+
configuration.iceCandidatePoolSize === undefined
|
|
318
|
+
? 0
|
|
319
|
+
: enforceRange(configuration.iceCandidatePoolSize, "iceCandidatePoolSize", 255),
|
|
320
|
+
certificates,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function cloneConfiguration(configuration) {
|
|
325
|
+
return {
|
|
326
|
+
iceServers: configuration.iceServers.map((server) => ({ ...server, urls: [...server.urls] })),
|
|
327
|
+
iceTransportPolicy: configuration.iceTransportPolicy,
|
|
328
|
+
bundlePolicy: configuration.bundlePolicy,
|
|
329
|
+
rtcpMuxPolicy: configuration.rtcpMuxPolicy,
|
|
330
|
+
iceCandidatePoolSize: configuration.iceCandidatePoolSize,
|
|
331
|
+
certificates: [...configuration.certificates],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function sameCertificateSet(left, right) {
|
|
336
|
+
if (left.length !== right.length) return false;
|
|
337
|
+
return left.every((certificate, index) => certificate === right[index]);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function normalizeCertificateExpiration(value) {
|
|
341
|
+
if (value === undefined) return Date.now() + 30 * 24 * 60 * 60 * 1000;
|
|
342
|
+
const number = Number(value);
|
|
343
|
+
if (!Number.isFinite(number) || !Number.isInteger(number) || number < 0) {
|
|
344
|
+
throw new TypeError("expires must be a non-negative integer");
|
|
345
|
+
}
|
|
346
|
+
return Date.now() + number;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalizeCertificateAlgorithm(algorithm) {
|
|
350
|
+
if (typeof algorithm === "string") {
|
|
351
|
+
throw makeDOMException("Unsupported certificate algorithm", "NotSupportedError");
|
|
352
|
+
}
|
|
353
|
+
if (algorithm === null || typeof algorithm !== "object") {
|
|
354
|
+
throw makeDOMException("Unsupported certificate algorithm", "NotSupportedError");
|
|
355
|
+
}
|
|
356
|
+
const name = String(algorithm.name || "").toUpperCase();
|
|
357
|
+
if (name === "ECDSA") {
|
|
358
|
+
if (String(algorithm.namedCurve || "").toUpperCase() !== "P-256") {
|
|
359
|
+
throw makeDOMException("Unsupported ECDSA curve", "NotSupportedError");
|
|
360
|
+
}
|
|
361
|
+
return { name: "ECDSA" };
|
|
362
|
+
}
|
|
363
|
+
if (name === "RSASSA-PKCS1-V1_5") {
|
|
364
|
+
const hash =
|
|
365
|
+
typeof algorithm.hash === "object" && algorithm.hash !== null
|
|
366
|
+
? String(algorithm.hash.name || "")
|
|
367
|
+
: String(algorithm.hash || "");
|
|
368
|
+
if (hash.toUpperCase() !== "SHA-256") {
|
|
369
|
+
throw makeDOMException("Unsupported RSA hash", "NotSupportedError");
|
|
370
|
+
}
|
|
371
|
+
const modulusLength = Number(algorithm.modulusLength);
|
|
372
|
+
if (!Number.isInteger(modulusLength) || modulusLength < 1024) {
|
|
373
|
+
throw makeDOMException("Unsupported RSA modulus length", "NotSupportedError");
|
|
374
|
+
}
|
|
375
|
+
return { name: "RSASSA-PKCS1-v1_5", modulusLength };
|
|
376
|
+
}
|
|
377
|
+
throw makeDOMException("Unsupported certificate algorithm", "NotSupportedError");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function createNativeBackedCertificate({ normalizedAlgorithm, expires, expiresMs }) {
|
|
381
|
+
const material = native.generateCertificate({
|
|
382
|
+
algorithm: normalizedAlgorithm.name,
|
|
383
|
+
modulusLength: normalizedAlgorithm.modulusLength,
|
|
384
|
+
expiresMs,
|
|
385
|
+
});
|
|
386
|
+
return new RTCCertificate({
|
|
387
|
+
expires,
|
|
388
|
+
algorithm: normalizedAlgorithm.name,
|
|
389
|
+
fingerprints: material.fingerprints,
|
|
390
|
+
certificatePem: material.certificatePem,
|
|
391
|
+
keyPem: material.keyPem,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function createDefaultNativeCertificate() {
|
|
396
|
+
const expiresMs = 30 * 24 * 60 * 60 * 1000;
|
|
397
|
+
return createNativeBackedCertificate({
|
|
398
|
+
normalizedAlgorithm: { name: "ECDSA" },
|
|
399
|
+
expires: Date.now() + expiresMs,
|
|
400
|
+
expiresMs,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function certificatePemToArrayBuffer(pem) {
|
|
405
|
+
const raw = new crypto.X509Certificate(pem).raw;
|
|
406
|
+
return raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function generateCertificate(algorithm) {
|
|
410
|
+
try {
|
|
411
|
+
const normalizedAlgorithm = normalizeCertificateAlgorithm(algorithm);
|
|
412
|
+
const expiresInput = algorithm && typeof algorithm === "object" ? algorithm.expires : undefined;
|
|
413
|
+
const expires = normalizeCertificateExpiration(expiresInput);
|
|
414
|
+
const expiresMs = expiresInput === undefined ? 30 * 24 * 60 * 60 * 1000 : Number(expiresInput);
|
|
415
|
+
return Promise.resolve(
|
|
416
|
+
createNativeBackedCertificate({ normalizedAlgorithm, expires, expiresMs }),
|
|
417
|
+
);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
return Promise.reject(error);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function normalizeDescription(init) {
|
|
424
|
+
if (init instanceof RTCSessionDescription) return init;
|
|
425
|
+
return new RTCSessionDescription(init);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function validateSdpType(type) {
|
|
429
|
+
if (!["offer", "answer", "pranswer", "rollback"].includes(type)) {
|
|
430
|
+
throw new TypeError(`Invalid RTCSdpType: ${type}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function isNoMediaSdp(description) {
|
|
435
|
+
return (
|
|
436
|
+
typeof description?.sdp === "string" &&
|
|
437
|
+
/^v=0(?:\r?\n|$)/.test(description.sdp) &&
|
|
438
|
+
!/\r?\nm=/.test(description.sdp)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function sdpSyntaxErrorLine(sdp) {
|
|
443
|
+
const lines = String(sdp ?? "").split(/\r\n|\n|\r/);
|
|
444
|
+
if (lines[0] !== "v=0") return 1;
|
|
445
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
446
|
+
const line = lines[index];
|
|
447
|
+
if (line === "" && index === lines.length - 1) continue;
|
|
448
|
+
if (!/^[a-z]=/.test(line)) return index + 1;
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function assertValidSdpSyntax(description) {
|
|
454
|
+
if (description.type === "rollback") return;
|
|
455
|
+
const line = sdpSyntaxErrorLine(description.sdp);
|
|
456
|
+
if (line === null) return;
|
|
457
|
+
throw new RTCError(
|
|
458
|
+
{
|
|
459
|
+
errorDetail: "sdp-syntax-error",
|
|
460
|
+
sdpLineNumber: line,
|
|
461
|
+
},
|
|
462
|
+
"Invalid SDP syntax",
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function hasDataMediaSection(description) {
|
|
467
|
+
return /\r?\nm=application\b/i.test(description?.sdp || "");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function hasTrickleIceOption(description) {
|
|
471
|
+
return /\r?\na=ice-options:[^\r\n]*\btrickle\b/i.test(description?.sdp || "");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function maxMessageSizeFromSdp(description) {
|
|
475
|
+
const match = /\r?\na=max-message-size:(\d+)/i.exec(description?.sdp || "");
|
|
476
|
+
return match ? Number(match[1]) : null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function nextTask() {
|
|
480
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function delay(ms) {
|
|
484
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const CLOSE_FLUSH_DELAY_MS = 50;
|
|
488
|
+
const CLOSE_FLUSH_POLL_INTERVAL_MS = 25;
|
|
489
|
+
const CLOSE_FLUSH_TIMEOUT_MS = 60000;
|
|
490
|
+
const REMOTE_CLOSE_MESSAGE_GRACE_MS = 25;
|
|
491
|
+
const MESSAGE_CONSUMER_GATE_TIMEOUT_MS = 1000;
|
|
492
|
+
const PEER_CONNECTION_NATIVE_CLOSE_DELAY_MS = 25;
|
|
493
|
+
const SCTP_CONNECT_POLL_INTERVAL_MS = 25;
|
|
494
|
+
const SCTP_CONNECT_POLL_TIMEOUT_MS = 5000;
|
|
495
|
+
const DATA_CHANNEL_OPEN_REPAIR_INTERVAL_MS = 25;
|
|
496
|
+
const DATA_CHANNEL_OPEN_REPAIR_TIMEOUT_MS = 5000;
|
|
497
|
+
const DATA_CHANNEL_ANNOUNCEMENT_REPAIR_INTERVAL_MS = 25;
|
|
498
|
+
const DATA_CHANNEL_ANNOUNCEMENT_REPAIR_GRACE_MS = 250;
|
|
499
|
+
const DATA_CHANNEL_ANNOUNCEMENT_REPAIR_TIMEOUT_MS = 5000;
|
|
500
|
+
const DEFAULT_SCTP_MAX_MESSAGE_SIZE = 262144;
|
|
501
|
+
const LIBDATACHANNEL_SCTP_MAX_CHANNELS = 1024;
|
|
502
|
+
let nextUnsupportedDataChannelBindingId = -1;
|
|
503
|
+
|
|
504
|
+
async function delayInvalidStateIfOperationPending(peer) {
|
|
505
|
+
if (peer._operationsPending > 0) await nextTask();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const ICE_CREDENTIAL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
509
|
+
|
|
510
|
+
function randomIceCredential(length) {
|
|
511
|
+
const bytes = crypto.randomBytes(length);
|
|
512
|
+
let value = "";
|
|
513
|
+
for (const byte of bytes) value += ICE_CREDENTIAL_CHARS[byte % ICE_CREDENTIAL_CHARS.length];
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function createIceRestartCredentials() {
|
|
518
|
+
return {
|
|
519
|
+
iceUfrag: randomIceCredential(16),
|
|
520
|
+
icePwd: randomIceCredential(32),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function rewriteIceCredentials(sdp, credentials) {
|
|
525
|
+
if (!/^a=ice-ufrag:/m.test(sdp) || !/^a=ice-pwd:/m.test(sdp)) return sdp;
|
|
526
|
+
return sdp
|
|
527
|
+
.replace(/^a=ice-ufrag:.*$/gm, `a=ice-ufrag:${credentials.iceUfrag}`)
|
|
528
|
+
.replace(/^a=ice-pwd:.*$/gm, `a=ice-pwd:${credentials.icePwd}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function markJsOnlyIceRestart(description) {
|
|
532
|
+
if (description && typeof description === "object") {
|
|
533
|
+
Object.defineProperty(description, "_webrtcNodeJsOnlyIceRestart", {
|
|
534
|
+
value: true,
|
|
535
|
+
configurable: true,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return description;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function hasDifferentIceCredentials(left, right) {
|
|
542
|
+
const leftParameters = firstIceParameters(left);
|
|
543
|
+
const rightParameters = firstIceParameters(right);
|
|
544
|
+
if (!leftParameters || !rightParameters) return false;
|
|
545
|
+
return (
|
|
546
|
+
leftParameters.usernameFragment !== rightParameters.usernameFragment ||
|
|
547
|
+
leftParameters.password !== rightParameters.password
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
class RTCSessionDescription {
|
|
552
|
+
constructor(init) {
|
|
553
|
+
if (!init || typeof init !== "object")
|
|
554
|
+
throw new TypeError("RTCSessionDescriptionInit required");
|
|
555
|
+
this.type = String(init.type || "");
|
|
556
|
+
validateSdpType(this.type);
|
|
557
|
+
this.sdp = String(init.sdp || "");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
toJSON() {
|
|
561
|
+
return { type: this.type, sdp: this.sdp };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
class RTCIceCandidate {
|
|
566
|
+
constructor(init = {}) {
|
|
567
|
+
if (init == null || typeof init !== "object")
|
|
568
|
+
throw new TypeError("RTCIceCandidateInit required");
|
|
569
|
+
const sdpMid = init.sdpMid == null ? null : String(init.sdpMid);
|
|
570
|
+
const sdpMLineIndex = init.sdpMLineIndex == null ? null : Number(init.sdpMLineIndex);
|
|
571
|
+
if (sdpMid === null && sdpMLineIndex === null) {
|
|
572
|
+
throw new TypeError("Either sdpMid or sdpMLineIndex must be provided");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
this.candidate = init.candidate === undefined ? "" : String(init.candidate);
|
|
576
|
+
this.sdpMid = sdpMid;
|
|
577
|
+
this.sdpMLineIndex = sdpMLineIndex;
|
|
578
|
+
this.usernameFragment = init.usernameFragment == null ? null : String(init.usernameFragment);
|
|
579
|
+
this.relayProtocol = init.relayProtocol == null ? null : String(init.relayProtocol);
|
|
580
|
+
this.url = init.url == null ? null : String(init.url);
|
|
581
|
+
|
|
582
|
+
const parsed = parseCandidate(this.candidate);
|
|
583
|
+
this.foundation = parsed.foundation;
|
|
584
|
+
this.component = parsed.component;
|
|
585
|
+
this.priority = parsed.priority;
|
|
586
|
+
this.address = parsed.address;
|
|
587
|
+
this.protocol = parsed.protocol;
|
|
588
|
+
this.port = parsed.port;
|
|
589
|
+
this.type = parsed.type;
|
|
590
|
+
this.tcpType = parsed.tcpType;
|
|
591
|
+
this.relatedAddress = parsed.relatedAddress;
|
|
592
|
+
this.relatedPort = parsed.relatedPort;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
toJSON() {
|
|
596
|
+
return {
|
|
597
|
+
candidate: this.candidate,
|
|
598
|
+
sdpMid: this.sdpMid,
|
|
599
|
+
sdpMLineIndex: this.sdpMLineIndex,
|
|
600
|
+
usernameFragment: this.usernameFragment,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
class RTCIceCandidatePair {
|
|
606
|
+
constructor(token, local, remote) {
|
|
607
|
+
if (token !== kInternalConstruct) throw new TypeError("Illegal constructor");
|
|
608
|
+
defineReadonly(this, "local", local);
|
|
609
|
+
defineReadonly(this, "remote", remote);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function parseCandidate(candidate) {
|
|
614
|
+
const empty = {
|
|
615
|
+
foundation: null,
|
|
616
|
+
component: null,
|
|
617
|
+
priority: null,
|
|
618
|
+
address: null,
|
|
619
|
+
protocol: null,
|
|
620
|
+
port: null,
|
|
621
|
+
type: null,
|
|
622
|
+
tcpType: null,
|
|
623
|
+
relatedAddress: null,
|
|
624
|
+
relatedPort: null,
|
|
625
|
+
};
|
|
626
|
+
if (typeof candidate !== "string" || !candidate.startsWith("candidate:")) return empty;
|
|
627
|
+
const parts = candidate.slice("candidate:".length).trim().split(/\s+/);
|
|
628
|
+
if (parts.length < 8) return empty;
|
|
629
|
+
const result = { ...empty };
|
|
630
|
+
result.foundation = parts[0] || null;
|
|
631
|
+
result.component = parts[1] === "1" ? "rtp" : parts[1] === "2" ? "rtcp" : null;
|
|
632
|
+
result.protocol = parts[2] ? parts[2].toLowerCase() : null;
|
|
633
|
+
result.priority = Number(parts[3]);
|
|
634
|
+
result.address = parts[4] || null;
|
|
635
|
+
result.port = Number(parts[5]);
|
|
636
|
+
const typIndex = parts.indexOf("typ");
|
|
637
|
+
if (typIndex !== -1 && parts[typIndex + 1]) result.type = parts[typIndex + 1];
|
|
638
|
+
const tcpIndex = parts.indexOf("tcptype");
|
|
639
|
+
if (tcpIndex !== -1 && parts[tcpIndex + 1]) result.tcpType = parts[tcpIndex + 1];
|
|
640
|
+
const raddrIndex = parts.indexOf("raddr");
|
|
641
|
+
if (raddrIndex !== -1 && parts[raddrIndex + 1]) result.relatedAddress = parts[raddrIndex + 1];
|
|
642
|
+
const rportIndex = parts.indexOf("rport");
|
|
643
|
+
if (rportIndex !== -1 && parts[rportIndex + 1])
|
|
644
|
+
result.relatedPort = Number(parts[rportIndex + 1]);
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function normalizeAddIceCandidateInput(init) {
|
|
649
|
+
if (init instanceof RTCIceCandidate) return init;
|
|
650
|
+
if (init == null) {
|
|
651
|
+
return {
|
|
652
|
+
candidate: "",
|
|
653
|
+
sdpMid: null,
|
|
654
|
+
sdpMLineIndex: null,
|
|
655
|
+
usernameFragment: null,
|
|
656
|
+
toJSON() {
|
|
657
|
+
return {
|
|
658
|
+
candidate: this.candidate,
|
|
659
|
+
sdpMid: this.sdpMid,
|
|
660
|
+
sdpMLineIndex: this.sdpMLineIndex,
|
|
661
|
+
usernameFragment: this.usernameFragment,
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
if (typeof init !== "object") throw new TypeError("RTCIceCandidateInit required");
|
|
667
|
+
const candidate = init.candidate == null ? "" : String(init.candidate);
|
|
668
|
+
const sdpMid = init.sdpMid == null ? null : String(init.sdpMid);
|
|
669
|
+
const sdpMLineIndex = init.sdpMLineIndex == null ? null : Number(init.sdpMLineIndex);
|
|
670
|
+
if (candidate !== "" && sdpMid === null && sdpMLineIndex === null) {
|
|
671
|
+
throw new TypeError("Either sdpMid or sdpMLineIndex must be provided");
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
candidate,
|
|
675
|
+
sdpMid,
|
|
676
|
+
sdpMLineIndex,
|
|
677
|
+
usernameFragment: init.usernameFragment == null ? null : String(init.usernameFragment),
|
|
678
|
+
toJSON() {
|
|
679
|
+
return {
|
|
680
|
+
candidate: this.candidate,
|
|
681
|
+
sdpMid: this.sdpMid,
|
|
682
|
+
sdpMLineIndex: this.sdpMLineIndex,
|
|
683
|
+
usernameFragment: this.usernameFragment,
|
|
684
|
+
};
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function splitSdpLines(sdp) {
|
|
690
|
+
return String(sdp || "")
|
|
691
|
+
.split(/\r?\n/)
|
|
692
|
+
.filter((line) => line !== "");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function parseSdpMediaSections(sdp) {
|
|
696
|
+
const sessionLines = [];
|
|
697
|
+
const mediaSections = [];
|
|
698
|
+
let sessionIceUfrag = null;
|
|
699
|
+
let sessionIcePwd = null;
|
|
700
|
+
let current = null;
|
|
701
|
+
for (const line of splitSdpLines(sdp)) {
|
|
702
|
+
if (line.startsWith("m=")) {
|
|
703
|
+
current = { startLine: line, lines: [line], mid: null, iceUfrag: null, icePwd: null };
|
|
704
|
+
mediaSections.push(current);
|
|
705
|
+
} else if (current) {
|
|
706
|
+
current.lines.push(line);
|
|
707
|
+
if (line.startsWith("a=mid:")) current.mid = line.slice("a=mid:".length);
|
|
708
|
+
if (line.startsWith("a=ice-ufrag:")) current.iceUfrag = line.slice("a=ice-ufrag:".length);
|
|
709
|
+
if (line.startsWith("a=ice-pwd:")) current.icePwd = line.slice("a=ice-pwd:".length);
|
|
710
|
+
} else {
|
|
711
|
+
sessionLines.push(line);
|
|
712
|
+
if (line.startsWith("a=ice-ufrag:")) sessionIceUfrag = line.slice("a=ice-ufrag:".length);
|
|
713
|
+
if (line.startsWith("a=ice-pwd:")) sessionIcePwd = line.slice("a=ice-pwd:".length);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
for (const section of mediaSections) {
|
|
717
|
+
if (section.iceUfrag === null) section.iceUfrag = sessionIceUfrag;
|
|
718
|
+
if (section.icePwd === null) section.icePwd = sessionIcePwd;
|
|
719
|
+
}
|
|
720
|
+
return { sessionLines, mediaSections };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function serializeSdp(sessionLines, mediaSections) {
|
|
724
|
+
const lines = [...sessionLines];
|
|
725
|
+
for (const section of mediaSections) lines.push(...section.lines);
|
|
726
|
+
return `${lines.join("\r\n")}\r\n`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function validateCandidateSyntax(candidate) {
|
|
730
|
+
if (candidate === "") return true;
|
|
731
|
+
if (typeof candidate !== "string" || !candidate.startsWith("candidate:")) return false;
|
|
732
|
+
const parts = candidate.slice("candidate:".length).trim().split(/\s+/);
|
|
733
|
+
const typIndex = parts.indexOf("typ");
|
|
734
|
+
return parts.length >= 8 && typIndex >= 6 && Boolean(parts[typIndex + 1]);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function selectCandidateSections(description, candidate) {
|
|
738
|
+
const parsed = parseSdpMediaSections(description?.sdp || "");
|
|
739
|
+
const sections = parsed.mediaSections;
|
|
740
|
+
if (sections.length === 0) return { parsed, targets: [] };
|
|
741
|
+
|
|
742
|
+
let targets;
|
|
743
|
+
if (candidate.sdpMid !== null) {
|
|
744
|
+
const section = sections.find((entry) => entry.mid === candidate.sdpMid);
|
|
745
|
+
if (!section)
|
|
746
|
+
throw makeDOMException("sdpMid does not match a remote media section", "OperationError");
|
|
747
|
+
targets = [section];
|
|
748
|
+
} else if (candidate.sdpMLineIndex !== null) {
|
|
749
|
+
const index = Number(candidate.sdpMLineIndex);
|
|
750
|
+
if (!Number.isInteger(index) || index < 0 || index >= sections.length) {
|
|
751
|
+
throw makeDOMException(
|
|
752
|
+
"sdpMLineIndex does not match a remote media section",
|
|
753
|
+
"OperationError",
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
targets = [sections[index]];
|
|
757
|
+
} else if (candidate.usernameFragment !== null) {
|
|
758
|
+
targets = sections.filter((entry) => entry.iceUfrag === candidate.usernameFragment);
|
|
759
|
+
if (targets.length === 0) {
|
|
760
|
+
throw makeDOMException(
|
|
761
|
+
"usernameFragment does not match a remote ICE generation",
|
|
762
|
+
"OperationError",
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
targets = sections;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (candidate.usernameFragment !== null) {
|
|
770
|
+
const matches = targets.some((entry) => entry.iceUfrag === candidate.usernameFragment);
|
|
771
|
+
if (!matches) {
|
|
772
|
+
throw makeDOMException(
|
|
773
|
+
"usernameFragment does not match the selected media section",
|
|
774
|
+
"OperationError",
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return { parsed, targets };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function appendRemoteCandidateToDescription(description, candidate) {
|
|
783
|
+
if (!description?.sdp || isNoMediaSdp(description)) return description;
|
|
784
|
+
if (!validateCandidateSyntax(candidate.candidate)) {
|
|
785
|
+
throw makeDOMException("Invalid ICE candidate syntax", "OperationError");
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const { parsed, targets } = selectCandidateSections(description, candidate);
|
|
789
|
+
if (targets.length === 0) return description;
|
|
790
|
+
|
|
791
|
+
const line = candidate.candidate === "" ? "a=end-of-candidates" : `a=${candidate.candidate}`;
|
|
792
|
+
for (const section of targets) {
|
|
793
|
+
if (!section.lines.includes(line)) section.lines.push(line);
|
|
794
|
+
}
|
|
795
|
+
return new RTCSessionDescription({
|
|
796
|
+
type: description.type,
|
|
797
|
+
sdp: serializeSdp(parsed.sessionLines, parsed.mediaSections),
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function isIceCandidateLine(line) {
|
|
802
|
+
return line.startsWith("a=candidate:") || line === "a=end-of-candidates";
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function stripIceCandidateLinesFromDescription(description) {
|
|
806
|
+
if (!description?.sdp || isNoMediaSdp(description)) return description;
|
|
807
|
+
const parsed = parseSdpMediaSections(description.sdp);
|
|
808
|
+
let stripped = false;
|
|
809
|
+
for (const section of parsed.mediaSections) {
|
|
810
|
+
section.lines = section.lines.filter((line) => {
|
|
811
|
+
if (!isIceCandidateLine(line)) return true;
|
|
812
|
+
stripped = true;
|
|
813
|
+
return false;
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
if (!stripped) return description;
|
|
817
|
+
const result = new RTCSessionDescription({
|
|
818
|
+
type: description.type,
|
|
819
|
+
sdp: serializeSdp(parsed.sessionLines, parsed.mediaSections),
|
|
820
|
+
});
|
|
821
|
+
if (description._webrtcNodeJsOnlyIceRestart) markJsOnlyIceRestart(result);
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function extractIceCandidatesFromDescription(description) {
|
|
826
|
+
if (!description?.sdp || isNoMediaSdp(description)) return [];
|
|
827
|
+
const parsed = parseSdpMediaSections(description.sdp);
|
|
828
|
+
const candidates = [];
|
|
829
|
+
for (let index = 0; index < parsed.mediaSections.length; index += 1) {
|
|
830
|
+
const section = parsed.mediaSections[index];
|
|
831
|
+
for (const line of section.lines) {
|
|
832
|
+
if (!line.startsWith("a=candidate:")) continue;
|
|
833
|
+
candidates.push(
|
|
834
|
+
new RTCIceCandidate({
|
|
835
|
+
candidate: line.slice("a=".length),
|
|
836
|
+
sdpMid: section.mid,
|
|
837
|
+
sdpMLineIndex: index,
|
|
838
|
+
usernameFragment: section.iceUfrag,
|
|
839
|
+
}),
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return candidates;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function resolveCandidateMid(description, candidate) {
|
|
847
|
+
if (candidate.sdpMid !== null) return candidate.sdpMid;
|
|
848
|
+
if (candidate.sdpMLineIndex === null) return null;
|
|
849
|
+
const index = Number(candidate.sdpMLineIndex);
|
|
850
|
+
const section = parseSdpMediaSections(description?.sdp || "").mediaSections[index];
|
|
851
|
+
return section?.mid || null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function firstIceParameters(description) {
|
|
855
|
+
const section = parseSdpMediaSections(description?.sdp || "").mediaSections.find(
|
|
856
|
+
(entry) => entry.iceUfrag && entry.icePwd,
|
|
857
|
+
);
|
|
858
|
+
if (!section) return null;
|
|
859
|
+
return {
|
|
860
|
+
usernameFragment: section.iceUfrag,
|
|
861
|
+
password: section.icePwd,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function dtlsSetupFromDescription(description) {
|
|
866
|
+
const parsed = parseSdpMediaSections(description?.sdp || "");
|
|
867
|
+
const section =
|
|
868
|
+
parsed.mediaSections.find((entry) => /^m=application\b/i.test(entry.startLine)) ||
|
|
869
|
+
parsed.mediaSections[0];
|
|
870
|
+
const setupLine =
|
|
871
|
+
section?.lines.find((line) => /^a=setup:/i.test(line)) ||
|
|
872
|
+
parsed.sessionLines.find((line) => /^a=setup:/i.test(line));
|
|
873
|
+
return setupLine ? setupLine.slice("a=setup:".length).toLowerCase() : null;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function sameCandidate(left, right) {
|
|
877
|
+
return (
|
|
878
|
+
left.candidate === right.candidate &&
|
|
879
|
+
left.sdpMid === right.sdpMid &&
|
|
880
|
+
left.sdpMLineIndex === right.sdpMLineIndex &&
|
|
881
|
+
left.usernameFragment === right.usernameFragment
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function sameCandidateEndpoint(left, right) {
|
|
886
|
+
if (!left || !right) return false;
|
|
887
|
+
return (
|
|
888
|
+
left.component === right.component &&
|
|
889
|
+
left.protocol === right.protocol &&
|
|
890
|
+
left.address === right.address &&
|
|
891
|
+
left.port === right.port &&
|
|
892
|
+
left.address !== null &&
|
|
893
|
+
left.port !== null
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function selectKnownCandidateByEndpoint(candidates, selectedCandidate) {
|
|
898
|
+
return (
|
|
899
|
+
candidates.find((candidate) => sameCandidateEndpoint(candidate, selectedCandidate)) ||
|
|
900
|
+
selectedCandidate
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
class RTCDataChannelEvent extends SimpleEvent {
|
|
905
|
+
constructor(type, init) {
|
|
906
|
+
if (!init || !init.channel) throw new TypeError("RTCDataChannelEventInit.channel is required");
|
|
907
|
+
super(type, init);
|
|
908
|
+
this.channel = init.channel;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
class RTCPeerConnectionIceEvent extends SimpleEvent {
|
|
913
|
+
constructor(type, init = {}) {
|
|
914
|
+
if (arguments.length === 0) throw new TypeError("type is required");
|
|
915
|
+
if (
|
|
916
|
+
init.candidate !== undefined &&
|
|
917
|
+
init.candidate !== null &&
|
|
918
|
+
!(init.candidate instanceof RTCIceCandidate)
|
|
919
|
+
) {
|
|
920
|
+
throw new TypeError("candidate must be an RTCIceCandidate or null");
|
|
921
|
+
}
|
|
922
|
+
super(type, init);
|
|
923
|
+
this.candidate = init.candidate == null ? null : init.candidate;
|
|
924
|
+
this.url = init.url === undefined ? null : init.url;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
class RTCPeerConnectionIceErrorEvent extends SimpleEvent {
|
|
929
|
+
constructor(type, init = {}) {
|
|
930
|
+
if (arguments.length === 0) throw new TypeError("type is required");
|
|
931
|
+
super(type, init);
|
|
932
|
+
this.address =
|
|
933
|
+
init.address === undefined || init.address === null ? null : String(init.address);
|
|
934
|
+
this.port =
|
|
935
|
+
init.port === undefined || init.port === null ? null : enforceRange(init.port, "port");
|
|
936
|
+
this.url = init.url === undefined ? "" : String(init.url);
|
|
937
|
+
this.errorCode = init.errorCode === undefined ? 0 : enforceRange(init.errorCode, "errorCode");
|
|
938
|
+
this.errorText = init.errorText === undefined ? "" : String(init.errorText);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
class RTCError extends Error {
|
|
943
|
+
constructor(init, message = "") {
|
|
944
|
+
if (!init || typeof init !== "object" || init.errorDetail === undefined) {
|
|
945
|
+
throw new TypeError("RTCErrorInit.errorDetail is required");
|
|
946
|
+
}
|
|
947
|
+
const errorDetail = String(init.errorDetail);
|
|
948
|
+
if (
|
|
949
|
+
![
|
|
950
|
+
"data-channel-failure",
|
|
951
|
+
"dtls-failure",
|
|
952
|
+
"fingerprint-failure",
|
|
953
|
+
"sctp-failure",
|
|
954
|
+
"sdp-syntax-error",
|
|
955
|
+
"hardware-encoder-error",
|
|
956
|
+
"hardware-encoder-not-available",
|
|
957
|
+
].includes(errorDetail)
|
|
958
|
+
) {
|
|
959
|
+
throw new TypeError(`Invalid RTCErrorDetailType: ${errorDetail}`);
|
|
960
|
+
}
|
|
961
|
+
super(message);
|
|
962
|
+
defineReadonly(this, "name", "OperationError");
|
|
963
|
+
defineReadonly(this, "code", 0);
|
|
964
|
+
defineReadonly(this, "errorDetail", errorDetail);
|
|
965
|
+
defineReadonly(this, "sdpLineNumber", init.sdpLineNumber ?? null);
|
|
966
|
+
defineReadonly(this, "sctpCauseCode", init.sctpCauseCode ?? null);
|
|
967
|
+
defineReadonly(this, "receivedAlert", init.receivedAlert ?? null);
|
|
968
|
+
defineReadonly(this, "sentAlert", init.sentAlert ?? null);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function defineReadonly(target, name, value) {
|
|
973
|
+
Object.defineProperty(target, name, {
|
|
974
|
+
value,
|
|
975
|
+
enumerable: true,
|
|
976
|
+
configurable: true,
|
|
977
|
+
writable: false,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
class RTCErrorEvent extends SimpleEvent {
|
|
982
|
+
constructor(type, init = {}) {
|
|
983
|
+
super(type, init);
|
|
984
|
+
this.error = init.error;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
class RTCIceTransport extends SimpleEventTarget {
|
|
989
|
+
constructor(token, peerConnection) {
|
|
990
|
+
if (token !== kInternalConstruct) throw new TypeError("Illegal constructor");
|
|
991
|
+
super();
|
|
992
|
+
this._pc = peerConnection;
|
|
993
|
+
this._stateOverride = null;
|
|
994
|
+
this._connectionSequenceRequested = false;
|
|
995
|
+
this._connectionSequenceStarted = false;
|
|
996
|
+
this._connectionSequenceReplaying = false;
|
|
997
|
+
this.onstatechange = null;
|
|
998
|
+
this.ongatheringstatechange = null;
|
|
999
|
+
this.onselectedcandidatepairchange = null;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
get role() {
|
|
1003
|
+
return this._pc._iceRole;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
get component() {
|
|
1007
|
+
return "rtp";
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
get state() {
|
|
1011
|
+
if (this._pc._closed) return "closed";
|
|
1012
|
+
return this._stateOverride || this._pc.iceConnectionState;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
get gatheringState() {
|
|
1016
|
+
const state = this._pc.iceGatheringState;
|
|
1017
|
+
if (
|
|
1018
|
+
state === "new" &&
|
|
1019
|
+
(this._pc._localIceCandidates.length > 0 ||
|
|
1020
|
+
this.state === "connected" ||
|
|
1021
|
+
this.state === "completed")
|
|
1022
|
+
) {
|
|
1023
|
+
return "gathering";
|
|
1024
|
+
}
|
|
1025
|
+
return state;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
getLocalCandidates() {
|
|
1029
|
+
return this._pc._localIceCandidates.map((candidate) => new RTCIceCandidate(candidate.toJSON()));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
getRemoteCandidates() {
|
|
1033
|
+
return this._pc._remoteIceCandidates.map(
|
|
1034
|
+
(candidate) => new RTCIceCandidate(candidate.toJSON()),
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
getSelectedCandidatePair() {
|
|
1039
|
+
if (!["connected", "completed"].includes(this.state)) return null;
|
|
1040
|
+
const localCandidates = this.getLocalCandidates();
|
|
1041
|
+
const remoteCandidates = this.getRemoteCandidates();
|
|
1042
|
+
const hasExplicitlyNegotiatedPeerTransport = Boolean(
|
|
1043
|
+
this._pc._hasExplicitlyNegotiatedDataTransport() &&
|
|
1044
|
+
this._pc._pairedPeer?._hasExplicitlyNegotiatedDataTransport(),
|
|
1045
|
+
);
|
|
1046
|
+
const hasJsVisibleRemoteCandidate =
|
|
1047
|
+
remoteCandidates.length > 0 ||
|
|
1048
|
+
this._pc._sameProcessIceCandidateExchange ||
|
|
1049
|
+
this._pc._explicitIceCandidateExchange ||
|
|
1050
|
+
hasExplicitlyNegotiatedPeerTransport;
|
|
1051
|
+
if (!hasJsVisibleRemoteCandidate) return null;
|
|
1052
|
+
const nativePair = this._pc._native?.selectedCandidatePair();
|
|
1053
|
+
if (nativePair?.local && nativePair?.remote) {
|
|
1054
|
+
const nativeLocal = new RTCIceCandidate(nativePair.local);
|
|
1055
|
+
const nativeRemote = new RTCIceCandidate(nativePair.remote);
|
|
1056
|
+
return new RTCIceCandidatePair(
|
|
1057
|
+
kInternalConstruct,
|
|
1058
|
+
selectKnownCandidateByEndpoint(localCandidates, nativeLocal),
|
|
1059
|
+
selectKnownCandidateByEndpoint(remoteCandidates, nativeRemote),
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
const pairedPeer = this._pc._pairedPeer;
|
|
1063
|
+
const pairedLocalCandidates =
|
|
1064
|
+
pairedPeer?._localIceCandidates?.map(
|
|
1065
|
+
(candidate) => new RTCIceCandidate(candidate.toJSON()),
|
|
1066
|
+
) || [];
|
|
1067
|
+
const pairedRemoteCandidates =
|
|
1068
|
+
pairedPeer?._remoteIceCandidates?.map(
|
|
1069
|
+
(candidate) => new RTCIceCandidate(candidate.toJSON()),
|
|
1070
|
+
) || [];
|
|
1071
|
+
const local = localCandidates[0] || pairedRemoteCandidates[0] || null;
|
|
1072
|
+
const remote = remoteCandidates[0] || pairedLocalCandidates[0] || null;
|
|
1073
|
+
if (!local || !remote) return null;
|
|
1074
|
+
return new RTCIceCandidatePair(kInternalConstruct, local, remote);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
getLocalParameters() {
|
|
1078
|
+
return firstIceParameters(this._pc.localDescription);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
getRemoteParameters() {
|
|
1082
|
+
return firstIceParameters(this._pc.remoteDescription);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
_eventListenerAdded(type) {
|
|
1086
|
+
if (type !== "statechange") return;
|
|
1087
|
+
this._connectionSequenceRequested = true;
|
|
1088
|
+
this._queueConnectedSequenceIfNeeded();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
_handlePeerIceConnectionState(state) {
|
|
1092
|
+
if (this._connectionSequenceReplaying && (state === "connected" || state === "completed")) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if ((state === "connected" || state === "completed") && this._queueConnectedSequenceIfNeeded())
|
|
1096
|
+
return;
|
|
1097
|
+
|
|
1098
|
+
this._stateOverride = null;
|
|
1099
|
+
this.dispatchEvent(makeEvent("statechange"));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
_queueConnectedSequenceIfNeeded() {
|
|
1103
|
+
if (
|
|
1104
|
+
!this._connectionSequenceRequested ||
|
|
1105
|
+
this._connectionSequenceStarted ||
|
|
1106
|
+
!this._pc._hasNegotiatedDataTransport() ||
|
|
1107
|
+
!["connected", "completed"].includes(this._pc.iceConnectionState)
|
|
1108
|
+
) {
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
this._connectionSequenceStarted = true;
|
|
1112
|
+
this._connectionSequenceReplaying = true;
|
|
1113
|
+
setTimeout(() => {
|
|
1114
|
+
if (this._pc._closed || this._stateOverride === "disconnected") return;
|
|
1115
|
+
this._stateOverride = "checking";
|
|
1116
|
+
this.dispatchEvent(makeEvent("statechange"));
|
|
1117
|
+
this._stateOverride = "connected";
|
|
1118
|
+
setTimeout(() => {
|
|
1119
|
+
if (this._pc._closed || this._stateOverride !== "connected") return;
|
|
1120
|
+
this.dispatchEvent(makeEvent("statechange"));
|
|
1121
|
+
this._connectionSequenceReplaying = false;
|
|
1122
|
+
}, 0);
|
|
1123
|
+
}, 0);
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
_forceState(state, { dispatch = true } = {}) {
|
|
1128
|
+
this._connectionSequenceStarted = true;
|
|
1129
|
+
this._stateOverride = state;
|
|
1130
|
+
if (dispatch) this.dispatchEvent(makeEvent("statechange"));
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
class RTCDtlsTransport extends SimpleEventTarget {
|
|
1135
|
+
constructor(token, peerConnection) {
|
|
1136
|
+
if (token !== kInternalConstruct) throw new TypeError("Illegal constructor");
|
|
1137
|
+
super();
|
|
1138
|
+
this._pc = peerConnection;
|
|
1139
|
+
this.iceTransport = new RTCIceTransport(kInternalConstruct, peerConnection);
|
|
1140
|
+
this.onstatechange = null;
|
|
1141
|
+
this.onerror = null;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
get state() {
|
|
1145
|
+
if (this._pc._closed || this._pc.connectionState === "closed") return "closed";
|
|
1146
|
+
if (this._pc.connectionState === "connected" || this._pc._sctpTransport?._state === "connected")
|
|
1147
|
+
return "connected";
|
|
1148
|
+
if (this._pc.connectionState === "failed") return "failed";
|
|
1149
|
+
if (!this._pc.localDescription || !this._pc.remoteDescription) return "new";
|
|
1150
|
+
return "connecting";
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
getRemoteCertificates() {
|
|
1154
|
+
if (this.state !== "connected") return [];
|
|
1155
|
+
const certificate = this._pc._pairedPeer?._nativeCertificates?.[0];
|
|
1156
|
+
if (!certificate?._certificatePem) return [];
|
|
1157
|
+
try {
|
|
1158
|
+
return [certificatePemToArrayBuffer(certificate._certificatePem)];
|
|
1159
|
+
} catch {
|
|
1160
|
+
return [];
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
class RTCSctpTransport extends SimpleEventTarget {
|
|
1166
|
+
constructor(token, peerConnection) {
|
|
1167
|
+
if (token !== kInternalConstruct) throw new TypeError("Illegal constructor");
|
|
1168
|
+
super();
|
|
1169
|
+
this._pc = peerConnection;
|
|
1170
|
+
this._transport = new RTCDtlsTransport(kInternalConstruct, peerConnection);
|
|
1171
|
+
this._state = "connecting";
|
|
1172
|
+
this._maxMessageSize = null;
|
|
1173
|
+
this._maxChannels = null;
|
|
1174
|
+
this.onstatechange = null;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
get transport() {
|
|
1178
|
+
return this._transport;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
get state() {
|
|
1182
|
+
return this._state;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
get maxMessageSize() {
|
|
1186
|
+
return this._maxMessageSize;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
get maxChannels() {
|
|
1190
|
+
if (this._maxChannels === null && this._state === "connected") {
|
|
1191
|
+
this._maxChannels = this._pc._currentSctpMaxChannels() || LIBDATACHANNEL_SCTP_MAX_CHANNELS;
|
|
1192
|
+
}
|
|
1193
|
+
return this._maxChannels;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
_setState(state) {
|
|
1197
|
+
if (this._state === state) return;
|
|
1198
|
+
this._state = state;
|
|
1199
|
+
this.dispatchEvent(makeEvent("statechange"));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
_setLimits({ maxMessageSize, maxChannels }) {
|
|
1203
|
+
this._maxMessageSize = maxMessageSize;
|
|
1204
|
+
this._maxChannels = maxChannels;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
class UnsupportedNativeDataChannel {
|
|
1209
|
+
constructor(label, options) {
|
|
1210
|
+
this.bindingId = nextUnsupportedDataChannelBindingId--;
|
|
1211
|
+
this.label = label;
|
|
1212
|
+
this.ordered = options.ordered;
|
|
1213
|
+
this.protocol = options.protocol;
|
|
1214
|
+
this.negotiated = options.negotiated;
|
|
1215
|
+
this.maxPacketLifeTime = options.maxPacketLifeTime ?? null;
|
|
1216
|
+
this.maxRetransmits = options.maxRetransmits ?? null;
|
|
1217
|
+
this.id = options.id;
|
|
1218
|
+
this.bufferedAmount = 0;
|
|
1219
|
+
this.maxMessageSize = DEFAULT_SCTP_MAX_MESSAGE_SIZE;
|
|
1220
|
+
this.isOpen = false;
|
|
1221
|
+
this.isClosed = false;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
close() {
|
|
1225
|
+
this.isClosed = true;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
setBufferedAmountLowThreshold() {}
|
|
1229
|
+
|
|
1230
|
+
sendString() {
|
|
1231
|
+
throw new Error("RTCDataChannel stream id exceeds libdatachannel's native stream limit");
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
sendBinary() {
|
|
1235
|
+
throw new Error("RTCDataChannel stream id exceeds libdatachannel's native stream limit");
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
class SyntheticNativeDataChannel {
|
|
1240
|
+
constructor(label, options) {
|
|
1241
|
+
this.bindingId = nextUnsupportedDataChannelBindingId--;
|
|
1242
|
+
this.label = label;
|
|
1243
|
+
this.ordered = options.ordered;
|
|
1244
|
+
this.protocol = options.protocol;
|
|
1245
|
+
this.negotiated = options.negotiated;
|
|
1246
|
+
this.maxPacketLifeTime = options.maxPacketLifeTime ?? null;
|
|
1247
|
+
this.maxRetransmits = options.maxRetransmits ?? null;
|
|
1248
|
+
this.id = options.id;
|
|
1249
|
+
this.bufferedAmount = 0;
|
|
1250
|
+
this.maxMessageSize = DEFAULT_SCTP_MAX_MESSAGE_SIZE;
|
|
1251
|
+
this.isOpen = true;
|
|
1252
|
+
this.isClosed = false;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
close() {
|
|
1256
|
+
this.isOpen = false;
|
|
1257
|
+
this.isClosed = true;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
setBufferedAmountLowThreshold() {}
|
|
1261
|
+
|
|
1262
|
+
sendString() {
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
sendBinary() {
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
class RTCDataChannel extends SimpleEventTarget {
|
|
1272
|
+
static _fromNative(
|
|
1273
|
+
peerConnection,
|
|
1274
|
+
nativeChannel,
|
|
1275
|
+
initialReadyState = undefined,
|
|
1276
|
+
assignedId = null,
|
|
1277
|
+
) {
|
|
1278
|
+
const channel = new RTCDataChannel(
|
|
1279
|
+
peerConnection,
|
|
1280
|
+
nativeChannel,
|
|
1281
|
+
initialReadyState,
|
|
1282
|
+
assignedId,
|
|
1283
|
+
);
|
|
1284
|
+
peerConnection._channels.set(channel._native.bindingId, channel);
|
|
1285
|
+
peerConnection._registerDataChannelId(channel);
|
|
1286
|
+
return channel;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
constructor(peerConnection, nativeChannel, initialReadyState = undefined, assignedId = null) {
|
|
1290
|
+
super();
|
|
1291
|
+
this._pc = peerConnection;
|
|
1292
|
+
this._native = nativeChannel;
|
|
1293
|
+
this._readyState =
|
|
1294
|
+
initialReadyState ||
|
|
1295
|
+
(nativeChannel.isOpen ? "open" : nativeChannel.isClosed ? "closed" : "connecting");
|
|
1296
|
+
this._binaryType = "arraybuffer";
|
|
1297
|
+
this._bufferedAmount = 0;
|
|
1298
|
+
this._bufferedAmountLowThreshold = 0;
|
|
1299
|
+
this._sendTail = Promise.resolve();
|
|
1300
|
+
this._pendingSendCount = 0;
|
|
1301
|
+
this._nativeCloseScheduled = false;
|
|
1302
|
+
this._openEventPending = false;
|
|
1303
|
+
this._openEventDispatched = false;
|
|
1304
|
+
this._announcementPending = false;
|
|
1305
|
+
this._nativeEventDrainActive = false;
|
|
1306
|
+
this._queuedNativeEvents = [];
|
|
1307
|
+
this._queuedMessageEvents = [];
|
|
1308
|
+
this._messageEventFlushScheduled = false;
|
|
1309
|
+
this._messageEventGateActive = false;
|
|
1310
|
+
this._messageConsumerGateActive = false;
|
|
1311
|
+
this._pendingPairedDeliveryBytes = 0;
|
|
1312
|
+
this._nativeCloseEventQueued = false;
|
|
1313
|
+
this._openEventDeferredForIce = false;
|
|
1314
|
+
this._openEventDeferredForDataChannel = false;
|
|
1315
|
+
this._openEventDataChannelDeferralExpired = false;
|
|
1316
|
+
this._registeredDataChannelId = null;
|
|
1317
|
+
this._assignedId = assignedId;
|
|
1318
|
+
this._pairedChannel = null;
|
|
1319
|
+
this._createdLocally = false;
|
|
1320
|
+
this._negotiatedOverride = null;
|
|
1321
|
+
this._syntheticIncoming = false;
|
|
1322
|
+
this.onopen = null;
|
|
1323
|
+
this._onmessage = null;
|
|
1324
|
+
this.onclose = null;
|
|
1325
|
+
this.onerror = null;
|
|
1326
|
+
this.onclosing = null;
|
|
1327
|
+
this.onbufferedamountlow = null;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
get label() {
|
|
1331
|
+
return this._native.label;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
get ordered() {
|
|
1335
|
+
return this._native.ordered;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
get maxPacketLifeTime() {
|
|
1339
|
+
return this._native.maxPacketLifeTime;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
get maxRetransmits() {
|
|
1343
|
+
return this._native.maxRetransmits;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
get protocol() {
|
|
1347
|
+
return this._native.protocol;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
get negotiated() {
|
|
1351
|
+
return this._negotiatedOverride ?? this._native.negotiated;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
get id() {
|
|
1355
|
+
const nativeId = this._native.id;
|
|
1356
|
+
if (nativeId != null) this._assignedId = nativeId;
|
|
1357
|
+
const id = nativeId ?? this._assignedId;
|
|
1358
|
+
this._pc._registerDataChannelId(this, id);
|
|
1359
|
+
return id;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
get readyState() {
|
|
1363
|
+
return this._readyState;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
get bufferedAmount() {
|
|
1367
|
+
return this._bufferedAmount;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
get bufferedAmountLowThreshold() {
|
|
1371
|
+
return this._bufferedAmountLowThreshold;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
set bufferedAmountLowThreshold(value) {
|
|
1375
|
+
const threshold = toUnsignedLong(value);
|
|
1376
|
+
this._bufferedAmountLowThreshold = threshold;
|
|
1377
|
+
this._native.setBufferedAmountLowThreshold(threshold);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
get binaryType() {
|
|
1381
|
+
return this._binaryType;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
set binaryType(value) {
|
|
1385
|
+
if (value === "arraybuffer" || value === "blob") this._binaryType = value;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
get onmessage() {
|
|
1389
|
+
return this._onmessage;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
set onmessage(callback) {
|
|
1393
|
+
this._onmessage = typeof callback === "function" ? callback : null;
|
|
1394
|
+
if (this._onmessage) this._releaseMessageConsumerGate();
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
send(data) {
|
|
1398
|
+
if (this._readyState === "connecting") {
|
|
1399
|
+
throw makeDOMException("RTCDataChannel is not open", "InvalidStateError");
|
|
1400
|
+
}
|
|
1401
|
+
if (this._readyState === "closing" || this._readyState === "closed") {
|
|
1402
|
+
throw makeDOMException("RTCDataChannel is closing or closed", "InvalidStateError");
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (typeof data === "string") {
|
|
1406
|
+
const payload = data;
|
|
1407
|
+
const size = byteLength(payload);
|
|
1408
|
+
this._assertWithinMaxMessageSize(size);
|
|
1409
|
+
if (this._hasPendingSends() || !this._nativeReadyForSend()) {
|
|
1410
|
+
this._enqueueSend(() => this._sendNativeString(payload), size);
|
|
1411
|
+
} else {
|
|
1412
|
+
if (this._sendNativeString(payload)) {
|
|
1413
|
+
this._increaseBufferedAmount(size);
|
|
1414
|
+
} else {
|
|
1415
|
+
this._enqueueSend(() => this._sendNativeString(payload), size);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
1422
|
+
const blob = data;
|
|
1423
|
+
this._assertWithinMaxMessageSize(blob.size);
|
|
1424
|
+
this._enqueueSend(async () => {
|
|
1425
|
+
const buffer = await blob.arrayBuffer();
|
|
1426
|
+
return this._sendNativeBinary(new Uint8Array(buffer));
|
|
1427
|
+
}, blob.size);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const view = toUint8Array(data);
|
|
1432
|
+
const size = view.byteLength;
|
|
1433
|
+
this._assertWithinMaxMessageSize(size);
|
|
1434
|
+
if (this._hasPendingSends() || !this._nativeReadyForSend()) {
|
|
1435
|
+
const payload = new Uint8Array(view);
|
|
1436
|
+
this._enqueueSend(() => this._sendNativeBinary(payload), size);
|
|
1437
|
+
} else {
|
|
1438
|
+
const payload = new Uint8Array(view);
|
|
1439
|
+
if (this._sendNativeBinary(payload)) {
|
|
1440
|
+
this._increaseBufferedAmount(size);
|
|
1441
|
+
} else {
|
|
1442
|
+
this._enqueueSend(() => this._sendNativeBinary(payload), size);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
close() {
|
|
1448
|
+
if (this._readyState === "closing" || this._readyState === "closed") return;
|
|
1449
|
+
this._readyState = "closing";
|
|
1450
|
+
if (!this._pairedChannel) {
|
|
1451
|
+
this._pc._pairDataChannelById(this, this._effectiveId());
|
|
1452
|
+
}
|
|
1453
|
+
const pairedChannel = this._pairedChannel;
|
|
1454
|
+
const shouldDrainBeforeClose =
|
|
1455
|
+
this._bufferedAmount > 0 || this._hasPendingSends() || this._pendingPairedDeliveryBytes > 0;
|
|
1456
|
+
if (!this._nativeCloseScheduled) {
|
|
1457
|
+
this._nativeCloseScheduled = true;
|
|
1458
|
+
const closeNative = () => {
|
|
1459
|
+
const close = () => {
|
|
1460
|
+
const synthesizePairedClose =
|
|
1461
|
+
pairedChannel &&
|
|
1462
|
+
pairedChannel.readyState !== "closed" &&
|
|
1463
|
+
(pairedChannel._syntheticIncoming ||
|
|
1464
|
+
(this._bufferedAmount === 0 && !this._hasPendingSends()));
|
|
1465
|
+
this._native.close();
|
|
1466
|
+
if (synthesizePairedClose) {
|
|
1467
|
+
setTimeout(() => pairedChannel._handleRemoteChannelClose(), 0);
|
|
1468
|
+
}
|
|
1469
|
+
setTimeout(() => {
|
|
1470
|
+
if (this._readyState === "closing") this._handleClose();
|
|
1471
|
+
}, 0);
|
|
1472
|
+
};
|
|
1473
|
+
if (shouldDrainBeforeClose) {
|
|
1474
|
+
this._closeNativeAfterBufferedSends(close);
|
|
1475
|
+
} else {
|
|
1476
|
+
close();
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
if (this._hasPendingSends()) {
|
|
1480
|
+
this._sendTail.then(closeNative, closeNative);
|
|
1481
|
+
} else {
|
|
1482
|
+
closeNative();
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
_increaseBufferedAmount(size) {
|
|
1488
|
+
if (!size) return;
|
|
1489
|
+
this._bufferedAmount += size;
|
|
1490
|
+
setTimeout(() => this._decreaseBufferedAmount(size), 0);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
_hasPendingSends() {
|
|
1494
|
+
return this._pendingSendCount > 0;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
_effectiveId() {
|
|
1498
|
+
return this._native.id ?? this._assignedId;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
_assignId(id) {
|
|
1502
|
+
if (this._native.id != null || this._assignedId != null) return;
|
|
1503
|
+
this._assignedId = id;
|
|
1504
|
+
this._pc._registerDataChannelId(this, id);
|
|
1505
|
+
if (this._pairedChannel?._syntheticIncoming && this._pairedChannel._effectiveId() == null) {
|
|
1506
|
+
this._pairedChannel._assignId(id);
|
|
1507
|
+
}
|
|
1508
|
+
this._pc._pairedPeer?._scheduleDataChannelAnnouncementRepair();
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
_adoptNativeChannel(nativeChannel, initialReadyState = undefined) {
|
|
1512
|
+
const previousBindingId = this._native.bindingId;
|
|
1513
|
+
if (this._pc._channels.get(previousBindingId) === this) {
|
|
1514
|
+
this._pc._channels.delete(previousBindingId);
|
|
1515
|
+
}
|
|
1516
|
+
this._native = nativeChannel;
|
|
1517
|
+
this._syntheticIncoming = false;
|
|
1518
|
+
this._pc._channels.set(nativeChannel.bindingId, this);
|
|
1519
|
+
if (initialReadyState) {
|
|
1520
|
+
this._readyState = initialReadyState;
|
|
1521
|
+
} else if (nativeChannel.isOpen) {
|
|
1522
|
+
this._readyState = "open";
|
|
1523
|
+
} else if (nativeChannel.isClosed) {
|
|
1524
|
+
this._readyState = "closed";
|
|
1525
|
+
}
|
|
1526
|
+
this._pc._registerDataChannelId(this);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
_assertWithinMaxMessageSize(size) {
|
|
1530
|
+
const maxMessageSize = this._pc.sctp?.maxMessageSize;
|
|
1531
|
+
if (Number.isFinite(maxMessageSize) && size > maxMessageSize) {
|
|
1532
|
+
throw new TypeError("RTCDataChannel message exceeds RTCSctpTransport.maxMessageSize");
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
_enqueueSend(operation, size) {
|
|
1537
|
+
this._pendingSendCount += 1;
|
|
1538
|
+
this._bufferedAmount += size;
|
|
1539
|
+
const run = async () => {
|
|
1540
|
+
try {
|
|
1541
|
+
await this._waitForNativeSendReady();
|
|
1542
|
+
await this._waitForNativeSendAccepted(operation);
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
this.dispatchEvent(
|
|
1545
|
+
new RTCErrorEvent("error", {
|
|
1546
|
+
error: new RTCError(
|
|
1547
|
+
{ errorDetail: "data-channel-failure" },
|
|
1548
|
+
error?.message || String(error),
|
|
1549
|
+
),
|
|
1550
|
+
}),
|
|
1551
|
+
);
|
|
1552
|
+
} finally {
|
|
1553
|
+
this._pendingSendCount -= 1;
|
|
1554
|
+
this._decreaseBufferedAmount(size);
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
this._sendTail = this._sendTail.then(run, run);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
_nativeReadyForSend() {
|
|
1561
|
+
return this._usesSyntheticPairDelivery() || Boolean(this._native?.isOpen);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
async _waitForNativeSendReady() {
|
|
1565
|
+
if (this._nativeReadyForSend()) return;
|
|
1566
|
+
const deadline = Date.now() + DATA_CHANNEL_OPEN_REPAIR_TIMEOUT_MS;
|
|
1567
|
+
while (!this._nativeReadyForSend() && this._readyState !== "closed" && Date.now() < deadline) {
|
|
1568
|
+
await delay(DATA_CHANNEL_OPEN_REPAIR_INTERVAL_MS);
|
|
1569
|
+
}
|
|
1570
|
+
if (!this._nativeReadyForSend()) {
|
|
1571
|
+
throw makeDOMException("RTCDataChannel is not open", "InvalidStateError");
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
async _waitForNativeSendAccepted(operation) {
|
|
1576
|
+
const deadline = Date.now() + DATA_CHANNEL_OPEN_REPAIR_TIMEOUT_MS;
|
|
1577
|
+
while (this._readyState !== "closed" && Date.now() < deadline) {
|
|
1578
|
+
if (await operation()) return;
|
|
1579
|
+
await delay(DATA_CHANNEL_OPEN_REPAIR_INTERVAL_MS);
|
|
1580
|
+
await this._waitForNativeSendReady();
|
|
1581
|
+
}
|
|
1582
|
+
throw makeDOMException("RTCDataChannel send was not accepted", "OperationError");
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
_sendNativeString(data) {
|
|
1586
|
+
if (this._usesSyntheticPairDelivery()) {
|
|
1587
|
+
this._trackPendingPairedDelivery(byteLength(data));
|
|
1588
|
+
this._deliverSyntheticMessage({ binary: false, data });
|
|
1589
|
+
return true;
|
|
1590
|
+
}
|
|
1591
|
+
try {
|
|
1592
|
+
const sent = this._tryNativeSend(() => this._native.sendString(data));
|
|
1593
|
+
if (sent) this._trackPendingPairedDelivery(byteLength(data));
|
|
1594
|
+
return sent;
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
throw mapNativeError(error, "OperationError");
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
_sendNativeBinary(data) {
|
|
1601
|
+
if (this._usesSyntheticPairDelivery()) {
|
|
1602
|
+
const view = toUint8Array(data);
|
|
1603
|
+
const copy = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
1604
|
+
this._trackPendingPairedDelivery(copy.byteLength);
|
|
1605
|
+
this._deliverSyntheticMessage({ binary: true, data: copy });
|
|
1606
|
+
return true;
|
|
1607
|
+
}
|
|
1608
|
+
try {
|
|
1609
|
+
const view = toUint8Array(data);
|
|
1610
|
+
const sent = this._tryNativeSend(() => this._native.sendBinary(view));
|
|
1611
|
+
if (sent) this._trackPendingPairedDelivery(view.byteLength);
|
|
1612
|
+
return sent;
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
throw mapNativeError(error, "OperationError");
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
_tryNativeSend(send) {
|
|
1619
|
+
const sent = send();
|
|
1620
|
+
if (sent !== false) return true;
|
|
1621
|
+
return Boolean(this._native?.isOpen);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
_usesSyntheticPairDelivery() {
|
|
1625
|
+
return this._syntheticIncoming || this._pairedChannel?._syntheticIncoming;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
_deliverSyntheticMessage(event) {
|
|
1629
|
+
const target = this._pairedChannel;
|
|
1630
|
+
if (!target || target.readyState === "closed") return;
|
|
1631
|
+
setTimeout(() => {
|
|
1632
|
+
if (target.readyState === "closed") return;
|
|
1633
|
+
target._handleNativeEvent({ type: "message", ...event });
|
|
1634
|
+
}, 0);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
_trackPendingPairedDelivery(size) {
|
|
1638
|
+
if (!this._pairedChannel || size <= 0) return;
|
|
1639
|
+
this._pendingPairedDeliveryBytes += size;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
_markPairedDeliveryReceived(event) {
|
|
1643
|
+
const sender = this._pairedChannel;
|
|
1644
|
+
if (!sender || sender.readyState === "closed") return;
|
|
1645
|
+
const size = event.binary ? event.data?.byteLength || 0 : byteLength(event.data || "");
|
|
1646
|
+
if (size <= 0) return;
|
|
1647
|
+
sender._pendingPairedDeliveryBytes = Math.max(0, sender._pendingPairedDeliveryBytes - size);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
_decreaseBufferedAmount(size) {
|
|
1651
|
+
const previous = this._bufferedAmount;
|
|
1652
|
+
this._bufferedAmount = Math.max(0, this._bufferedAmount - size);
|
|
1653
|
+
if (
|
|
1654
|
+
previous > this._bufferedAmountLowThreshold &&
|
|
1655
|
+
this._bufferedAmount <= this._bufferedAmountLowThreshold
|
|
1656
|
+
) {
|
|
1657
|
+
this.dispatchEvent(makeEvent("bufferedamountlow"));
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
_closeNativeAfterBufferedSends(close) {
|
|
1662
|
+
const deadline = Date.now() + CLOSE_FLUSH_TIMEOUT_MS;
|
|
1663
|
+
const poll = () => {
|
|
1664
|
+
const nativeBufferedAmount = Number(this._native?.bufferedAmount || 0);
|
|
1665
|
+
const pairedDeliveryDrained = this._pendingPairedDeliveryBytes === 0;
|
|
1666
|
+
const nativeBufferDrained = this._bufferedAmount === 0 && nativeBufferedAmount === 0;
|
|
1667
|
+
const drained = this._pairedChannel ? pairedDeliveryDrained : nativeBufferDrained;
|
|
1668
|
+
if (drained || Date.now() >= deadline) {
|
|
1669
|
+
setTimeout(close, CLOSE_FLUSH_DELAY_MS);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
setTimeout(poll, CLOSE_FLUSH_POLL_INTERVAL_MS);
|
|
1673
|
+
};
|
|
1674
|
+
poll();
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
_handleNativeEvent(event, fromQueue = false) {
|
|
1678
|
+
if (!fromQueue && (this._announcementPending || this._nativeEventDrainActive)) {
|
|
1679
|
+
if (event.type === "open") {
|
|
1680
|
+
this._readyState = "open";
|
|
1681
|
+
this._openEventPending = true;
|
|
1682
|
+
} else {
|
|
1683
|
+
this._queuedNativeEvents.push(event);
|
|
1684
|
+
}
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
switch (event.type) {
|
|
1689
|
+
case "open":
|
|
1690
|
+
if (this._openEventPending) {
|
|
1691
|
+
this._readyState = "open";
|
|
1692
|
+
this._pc._registerDataChannelId(this);
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
if (this._readyState === "connecting" || this._readyState === "open") {
|
|
1696
|
+
this._readyState = "open";
|
|
1697
|
+
this._pc._registerDataChannelId(this);
|
|
1698
|
+
this._dispatchOpenEvent();
|
|
1699
|
+
}
|
|
1700
|
+
break;
|
|
1701
|
+
case "message":
|
|
1702
|
+
this._queueMessageEvent(event);
|
|
1703
|
+
break;
|
|
1704
|
+
case "bufferedamountlow":
|
|
1705
|
+
// The JS facade owns W3C bufferedAmount timing; native low-water events
|
|
1706
|
+
// use libdatachannel's transport queue and can race the JS-visible counter.
|
|
1707
|
+
break;
|
|
1708
|
+
case "error":
|
|
1709
|
+
this.dispatchEvent(
|
|
1710
|
+
new RTCErrorEvent("error", {
|
|
1711
|
+
error: new RTCError({ errorDetail: "data-channel-failure" }, event.error || ""),
|
|
1712
|
+
}),
|
|
1713
|
+
);
|
|
1714
|
+
break;
|
|
1715
|
+
case "close":
|
|
1716
|
+
this._queueNativeCloseEvent();
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
_queueNativeCloseEvent() {
|
|
1722
|
+
if (this._nativeCloseEventQueued) return;
|
|
1723
|
+
this._nativeCloseEventQueued = true;
|
|
1724
|
+
this._deferRemoteCloseUntilMessagesFlushed(() => {
|
|
1725
|
+
this._nativeCloseEventQueued = false;
|
|
1726
|
+
this._handleNativeCloseEvent();
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
_deferRemoteCloseUntilMessagesFlushed(callback) {
|
|
1731
|
+
const notBefore = Date.now() + REMOTE_CLOSE_MESSAGE_GRACE_MS;
|
|
1732
|
+
const closeAfterMessages = () => {
|
|
1733
|
+
if (this._readyState === "closed") return;
|
|
1734
|
+
if (
|
|
1735
|
+
Date.now() < notBefore ||
|
|
1736
|
+
this._messageEventGateActive ||
|
|
1737
|
+
this._messageEventFlushScheduled ||
|
|
1738
|
+
this._queuedMessageEvents.length > 0
|
|
1739
|
+
) {
|
|
1740
|
+
setTimeout(closeAfterMessages, 0);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
callback();
|
|
1744
|
+
};
|
|
1745
|
+
setTimeout(closeAfterMessages, 0);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
_handleNativeCloseEvent() {
|
|
1749
|
+
if (this._shouldSuppressSpuriousNativeClose()) return;
|
|
1750
|
+
if (this._readyState === "open") {
|
|
1751
|
+
this._readyState = "closing";
|
|
1752
|
+
this.dispatchEvent(makeEvent("closing"));
|
|
1753
|
+
setTimeout(() => this._finishRemoteClose(), 0);
|
|
1754
|
+
} else {
|
|
1755
|
+
this._handleClose();
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
_shouldSuppressSpuriousNativeClose() {
|
|
1760
|
+
return Boolean(
|
|
1761
|
+
this._pc._pairedPeer &&
|
|
1762
|
+
this._pairedChannel &&
|
|
1763
|
+
this._readyState === "open" &&
|
|
1764
|
+
this._pairedChannel.readyState === "open" &&
|
|
1765
|
+
this._pc.connectionState === "connected" &&
|
|
1766
|
+
["connected", "completed"].includes(this._pc.iceConnectionState) &&
|
|
1767
|
+
this._pc.sctp?.state === "connected",
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
_finishRemoteClose() {
|
|
1772
|
+
if (this._readyState !== "closing") return;
|
|
1773
|
+
const peerFailed =
|
|
1774
|
+
this._pc.connectionState === "closed" ||
|
|
1775
|
+
this._pc.connectionState === "disconnected" ||
|
|
1776
|
+
this._pc.connectionState === "failed" ||
|
|
1777
|
+
this._pc.iceConnectionState === "closed" ||
|
|
1778
|
+
this._pc.iceConnectionState === "disconnected" ||
|
|
1779
|
+
this._pc.iceConnectionState === "failed";
|
|
1780
|
+
if (peerFailed && this._hasEventConsumer("error") && this._hasEventConsumer("close")) {
|
|
1781
|
+
this.dispatchEvent(
|
|
1782
|
+
new RTCErrorEvent("error", {
|
|
1783
|
+
error: new RTCError(
|
|
1784
|
+
{
|
|
1785
|
+
name: "OperationError",
|
|
1786
|
+
errorDetail: "sctp-failure",
|
|
1787
|
+
sctpCauseCode: null,
|
|
1788
|
+
},
|
|
1789
|
+
"The SCTP association was closed",
|
|
1790
|
+
),
|
|
1791
|
+
}),
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
this._handleClose();
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
_handleRemoteChannelClose() {
|
|
1798
|
+
if (this._readyState === "closed") return;
|
|
1799
|
+
this._deferRemoteCloseUntilMessagesFlushed(() => {
|
|
1800
|
+
if (this._readyState === "closed") return;
|
|
1801
|
+
if (this._readyState === "open" || this._readyState === "connecting") {
|
|
1802
|
+
this._readyState = "closing";
|
|
1803
|
+
this.dispatchEvent(makeEvent("closing"));
|
|
1804
|
+
setTimeout(() => this._handleClose(), 0);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (this._readyState === "closing") {
|
|
1808
|
+
setTimeout(() => this._handleClose(), 0);
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
_handlePeerConnectionFailure() {
|
|
1814
|
+
if (this._readyState === "closed") return;
|
|
1815
|
+
if (this._readyState === "open") {
|
|
1816
|
+
this._readyState = "closing";
|
|
1817
|
+
}
|
|
1818
|
+
if (this._hasEventConsumer("error") && this._hasEventConsumer("close")) {
|
|
1819
|
+
this.dispatchEvent(
|
|
1820
|
+
new RTCErrorEvent("error", {
|
|
1821
|
+
error: new RTCError(
|
|
1822
|
+
{
|
|
1823
|
+
name: "OperationError",
|
|
1824
|
+
errorDetail: "sctp-failure",
|
|
1825
|
+
sctpCauseCode: null,
|
|
1826
|
+
},
|
|
1827
|
+
"The SCTP association was closed",
|
|
1828
|
+
),
|
|
1829
|
+
}),
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
this._handleClose();
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
_handleClose() {
|
|
1836
|
+
if (this._readyState === "closed") return;
|
|
1837
|
+
const id = this._effectiveId();
|
|
1838
|
+
this._readyState = "closed";
|
|
1839
|
+
if (this._pairedChannel?._pairedChannel === this) this._pairedChannel._pairedChannel = null;
|
|
1840
|
+
this._pairedChannel = null;
|
|
1841
|
+
this._pc._unregisterDataChannelId(this);
|
|
1842
|
+
if (!this._createdLocally && id != null) {
|
|
1843
|
+
this._pc._remoteAnnouncedDataChannelIds.delete(id);
|
|
1844
|
+
this._pc._pendingSyntheticDataChannelAnnouncements.delete(id);
|
|
1845
|
+
}
|
|
1846
|
+
this.dispatchEvent(makeEvent("close"));
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
_announceOpenAfterDataChannelEvent() {
|
|
1850
|
+
if (!this._openEventPending) return;
|
|
1851
|
+
this._openEventPending = false;
|
|
1852
|
+
setTimeout(() => {
|
|
1853
|
+
if (this._readyState === "open") this._dispatchOpenEvent();
|
|
1854
|
+
}, 0);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
_dispatchOpenEvent() {
|
|
1858
|
+
if (this._openEventDispatched || this._readyState !== "open") return;
|
|
1859
|
+
const pendingIcePeer = this._pendingIceEventPeer();
|
|
1860
|
+
if (pendingIcePeer) {
|
|
1861
|
+
if (this._openEventDeferredForIce) return;
|
|
1862
|
+
this._openEventDeferredForIce = true;
|
|
1863
|
+
pendingIcePeer._afterDeferredIceEventsFlushed(() => {
|
|
1864
|
+
this._openEventDeferredForIce = false;
|
|
1865
|
+
this._dispatchOpenEvent();
|
|
1866
|
+
});
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const pendingDataChannelPeer = this._pendingDataChannelAnnouncementPeer();
|
|
1870
|
+
if (pendingDataChannelPeer) {
|
|
1871
|
+
if (this._openEventDeferredForDataChannel) return;
|
|
1872
|
+
this._openEventDeferredForDataChannel = true;
|
|
1873
|
+
pendingDataChannelPeer._afterDataChannelAnnouncementSettled(
|
|
1874
|
+
this._effectiveId(),
|
|
1875
|
+
(settled) => {
|
|
1876
|
+
this._openEventDeferredForDataChannel = false;
|
|
1877
|
+
if (!settled) this._openEventDataChannelDeferralExpired = true;
|
|
1878
|
+
this._dispatchOpenEvent();
|
|
1879
|
+
},
|
|
1880
|
+
);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
this._openEventDispatched = true;
|
|
1884
|
+
this.dispatchEvent(makeEvent("open"));
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
_pendingIceEventPeer() {
|
|
1888
|
+
const peers = [this._pc, this._pc._pairedPeer].filter(Boolean);
|
|
1889
|
+
return peers.find((peer) => peer._hasPendingDeferredIceEvents());
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
_pendingDataChannelAnnouncementPeer() {
|
|
1893
|
+
if (this._openEventDataChannelDeferralExpired) return null;
|
|
1894
|
+
if (!this._createdLocally || this.negotiated) return null;
|
|
1895
|
+
const id = this._effectiveId();
|
|
1896
|
+
const peer = this._pc._pairedPeer;
|
|
1897
|
+
if (!peer || peer._closed || id == null || !peer._hasEventConsumer("datachannel")) return null;
|
|
1898
|
+
return peer._hasSettledDataChannelAnnouncement(id) ? null : peer;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
_repairMissedOpenEvent() {
|
|
1902
|
+
if (this._readyState !== "connecting" || this._native.isClosed || !this._native.isOpen)
|
|
1903
|
+
return false;
|
|
1904
|
+
this._readyState = "open";
|
|
1905
|
+
this._pc._registerDataChannelId(this);
|
|
1906
|
+
if (this._announcementPending) {
|
|
1907
|
+
this._openEventPending = true;
|
|
1908
|
+
} else {
|
|
1909
|
+
this._dispatchOpenEvent();
|
|
1910
|
+
}
|
|
1911
|
+
return true;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
_flushQueuedNativeEventsAfterAnnouncement() {
|
|
1915
|
+
if (!this._queuedNativeEvents.length) return;
|
|
1916
|
+
this._nativeEventDrainActive = true;
|
|
1917
|
+
const dispatchNext = () => {
|
|
1918
|
+
const event = this._queuedNativeEvents.shift();
|
|
1919
|
+
if (!event) {
|
|
1920
|
+
this._nativeEventDrainActive = false;
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
this._handleNativeEvent(event, true);
|
|
1924
|
+
if (this._queuedNativeEvents.length) setTimeout(dispatchNext, 0);
|
|
1925
|
+
else this._nativeEventDrainActive = false;
|
|
1926
|
+
};
|
|
1927
|
+
setTimeout(dispatchNext, 0);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
_queueMessageEvent(event) {
|
|
1931
|
+
this._queuedMessageEvents.push(event);
|
|
1932
|
+
this._scheduleMessageEventFlush();
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
_scheduleMessageEventFlush() {
|
|
1936
|
+
if (this._messageEventFlushScheduled) return;
|
|
1937
|
+
this._messageEventFlushScheduled = true;
|
|
1938
|
+
setTimeout(() => {
|
|
1939
|
+
this._messageEventFlushScheduled = false;
|
|
1940
|
+
this._flushQueuedMessageEvents();
|
|
1941
|
+
}, 0);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
_flushQueuedMessageEvents() {
|
|
1945
|
+
if (this._readyState === "closed") {
|
|
1946
|
+
this._queuedMessageEvents.length = 0;
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (!this._queuedMessageEvents.length) return;
|
|
1950
|
+
const pendingPeer =
|
|
1951
|
+
this._pc._operationsPending > 0
|
|
1952
|
+
? this._pc
|
|
1953
|
+
: this._pc._pairedPeer?._operationsPending > 0
|
|
1954
|
+
? this._pc._pairedPeer
|
|
1955
|
+
: null;
|
|
1956
|
+
if (pendingPeer) {
|
|
1957
|
+
pendingPeer._afterOperationsIdle(() => this._scheduleMessageEventFlush());
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
if (this._messageEventGateActive) return;
|
|
1961
|
+
if (this._messageConsumerGateActive && !this._hasEventConsumer("message")) return;
|
|
1962
|
+
if (this._messageConsumerGateActive) this._messageConsumerGateActive = false;
|
|
1963
|
+
|
|
1964
|
+
const event = this._queuedMessageEvents.shift();
|
|
1965
|
+
this.dispatchEvent(makeMessageEvent("message", { data: this._convertMessage(event) }));
|
|
1966
|
+
this._markPairedDeliveryReceived(event);
|
|
1967
|
+
if (this._queuedMessageEvents.length) this._scheduleMessageEventFlush();
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
_gateMessageEventsAfterAnnouncement() {
|
|
1971
|
+
if (this._messageEventGateActive) return;
|
|
1972
|
+
this._messageEventGateActive = true;
|
|
1973
|
+
setTimeout(() => {
|
|
1974
|
+
setTimeout(() => {
|
|
1975
|
+
this._messageEventGateActive = false;
|
|
1976
|
+
this._scheduleMessageEventFlush();
|
|
1977
|
+
}, 0);
|
|
1978
|
+
}, 0);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
_gateMessageEventsUntilConsumer() {
|
|
1982
|
+
if (this._hasEventConsumer("message")) return;
|
|
1983
|
+
this._messageConsumerGateActive = true;
|
|
1984
|
+
setTimeout(() => this._releaseMessageConsumerGate(), MESSAGE_CONSUMER_GATE_TIMEOUT_MS);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
_releaseMessageConsumerGate() {
|
|
1988
|
+
if (!this._messageConsumerGateActive) return;
|
|
1989
|
+
this._messageConsumerGateActive = false;
|
|
1990
|
+
this._scheduleMessageEventFlush();
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
_eventListenerAdded(type) {
|
|
1994
|
+
if (type === "message") this._releaseMessageConsumerGate();
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
_convertMessage(event) {
|
|
1998
|
+
if (!event.binary) return event.data;
|
|
1999
|
+
if (this._binaryType === "blob" && typeof Blob !== "undefined") {
|
|
2000
|
+
return new Blob([event.data]);
|
|
2001
|
+
}
|
|
2002
|
+
return event.data;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
function toUint8Array(data) {
|
|
2007
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
2008
|
+
if (ArrayBuffer.isView(data)) {
|
|
2009
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
2010
|
+
}
|
|
2011
|
+
throw new TypeError(
|
|
2012
|
+
"RTCDataChannel.send expects a string, Blob, ArrayBuffer, or ArrayBufferView",
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
class RTCPeerConnection extends SimpleEventTarget {
|
|
2017
|
+
static generateCertificate(algorithm) {
|
|
2018
|
+
return generateCertificate(algorithm);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
constructor(configuration = {}) {
|
|
2022
|
+
super();
|
|
2023
|
+
const normalizedConfiguration = normalizePeerConnectionConfiguration(configuration);
|
|
2024
|
+
this._configuration = normalizedConfiguration;
|
|
2025
|
+
this._channels = new Map();
|
|
2026
|
+
this._usedDataChannelIds = new Map();
|
|
2027
|
+
this._closed = false;
|
|
2028
|
+
this._localDescription = null;
|
|
2029
|
+
this._remoteDescription = null;
|
|
2030
|
+
this._currentLocalDescription = null;
|
|
2031
|
+
this._pendingLocalDescription = null;
|
|
2032
|
+
this._currentRemoteDescription = null;
|
|
2033
|
+
this._pendingRemoteDescription = null;
|
|
2034
|
+
this._localDescriptionPairingKeys = new Set();
|
|
2035
|
+
this._pairedPeer = null;
|
|
2036
|
+
this._sctpTransport = null;
|
|
2037
|
+
this._canTrickleIceCandidates = null;
|
|
2038
|
+
this._selfRemoteDescription = false;
|
|
2039
|
+
this._pendingIce = [];
|
|
2040
|
+
this._localIceCandidates = [];
|
|
2041
|
+
this._remoteIceCandidates = [];
|
|
2042
|
+
this._pendingRemoteCandidatesForNative = [];
|
|
2043
|
+
this._deferredIceEvents = [];
|
|
2044
|
+
this._iceEventFlushScheduled = false;
|
|
2045
|
+
this._processingDeferredIceEvent = false;
|
|
2046
|
+
this._iceEventIdleCallbacks = [];
|
|
2047
|
+
this._iceRole = "unknown";
|
|
2048
|
+
this._preparedLocalDescription = null;
|
|
2049
|
+
this._lastCreatedOffer = null;
|
|
2050
|
+
this._lastCreatedAnswer = null;
|
|
2051
|
+
this._localDescriptionSetByApi = false;
|
|
2052
|
+
this._iceRestartPending = false;
|
|
2053
|
+
this._pendingIceRestartCredentials = null;
|
|
2054
|
+
this._jsOnlyIceRestartOfferPending = false;
|
|
2055
|
+
this._jsOnlyIceRestartRemoteOffer = false;
|
|
2056
|
+
this._operationsPending = 0;
|
|
2057
|
+
this._operationIdleCallbacks = [];
|
|
2058
|
+
this._explicitIceCandidateExchange = false;
|
|
2059
|
+
this._sameProcessIceCandidateExchange = false;
|
|
2060
|
+
this._suppressNextNativeSignalingState = null;
|
|
2061
|
+
this._sctpTransportUpdateScheduled = false;
|
|
2062
|
+
this._sctpConnectedTransitionScheduled = false;
|
|
2063
|
+
this._sctpConnectedTransitionReady = false;
|
|
2064
|
+
this._sctpConnectPollScheduled = false;
|
|
2065
|
+
this._sctpConnectPollDeadline = 0;
|
|
2066
|
+
this._connectedStateRepairScheduled = false;
|
|
2067
|
+
this._dataChannelOpenRepairScheduled = false;
|
|
2068
|
+
this._dataChannelOpenRepairDeadline = 0;
|
|
2069
|
+
this._dataChannelAnnouncementRepairScheduled = false;
|
|
2070
|
+
this._dataChannelAnnouncementRepairReadyAt = 0;
|
|
2071
|
+
this._dataChannelAnnouncementRepairDeadline = 0;
|
|
2072
|
+
this._remoteAnnouncedDataChannelIds = new Set();
|
|
2073
|
+
this._pendingSyntheticDataChannelAnnouncements = new Map();
|
|
2074
|
+
this._localDataChannelCount = 0;
|
|
2075
|
+
this._negotiationNeeded = false;
|
|
2076
|
+
this._negotiationNeededScheduled = false;
|
|
2077
|
+
this._ondatachannel = null;
|
|
2078
|
+
this._pendingDataChannelEvents = [];
|
|
2079
|
+
this._pendingNativeDataChannelEvents = new Map();
|
|
2080
|
+
this._dataChannelFlushScheduled = false;
|
|
2081
|
+
this.onicecandidate = null;
|
|
2082
|
+
this.onicecandidateerror = null;
|
|
2083
|
+
this.onicegatheringstatechange = null;
|
|
2084
|
+
this.oniceconnectionstatechange = null;
|
|
2085
|
+
this.onconnectionstatechange = null;
|
|
2086
|
+
this.onsignalingstatechange = null;
|
|
2087
|
+
this.onnegotiationneeded = null;
|
|
2088
|
+
this._connectionState = "new";
|
|
2089
|
+
this._iceConnectionState = "new";
|
|
2090
|
+
this._iceGatheringState = "new";
|
|
2091
|
+
this._signalingState = "stable";
|
|
2092
|
+
this._native = null;
|
|
2093
|
+
this._nativeCertificates = null;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
getConfiguration() {
|
|
2097
|
+
return cloneConfiguration(this._configuration);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
_ensureNativePeerConnection() {
|
|
2101
|
+
if (this._native) return this._native;
|
|
2102
|
+
const nativeConfiguration = cloneConfiguration(this._configuration);
|
|
2103
|
+
let nativeCertificates = nativeConfiguration.certificates;
|
|
2104
|
+
if (nativeConfiguration.certificates.length === 0) {
|
|
2105
|
+
nativeCertificates = [createDefaultNativeCertificate()];
|
|
2106
|
+
nativeConfiguration.certificates = nativeCertificates;
|
|
2107
|
+
}
|
|
2108
|
+
const nativePeerConnection = new native.NativePeerConnection(nativeConfiguration, (event) =>
|
|
2109
|
+
this._handleNativeEvent(event),
|
|
2110
|
+
);
|
|
2111
|
+
this._nativeCertificates = nativeCertificates;
|
|
2112
|
+
this._native = nativePeerConnection;
|
|
2113
|
+
return this._native;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
setConfiguration(configuration = {}) {
|
|
2117
|
+
this._assertNotClosed();
|
|
2118
|
+
const hasCertificates =
|
|
2119
|
+
configuration &&
|
|
2120
|
+
typeof configuration === "object" &&
|
|
2121
|
+
Object.hasOwn(configuration, "certificates");
|
|
2122
|
+
const normalizedConfiguration = normalizePeerConnectionConfiguration(configuration);
|
|
2123
|
+
if (!hasCertificates) normalizedConfiguration.certificates = this._configuration.certificates;
|
|
2124
|
+
if (normalizedConfiguration.bundlePolicy !== this._configuration.bundlePolicy) {
|
|
2125
|
+
throw makeDOMException("bundlePolicy cannot be changed", "InvalidModificationError");
|
|
2126
|
+
}
|
|
2127
|
+
if (normalizedConfiguration.rtcpMuxPolicy !== this._configuration.rtcpMuxPolicy) {
|
|
2128
|
+
throw makeDOMException("rtcpMuxPolicy cannot be changed", "InvalidModificationError");
|
|
2129
|
+
}
|
|
2130
|
+
if (
|
|
2131
|
+
!sameCertificateSet(normalizedConfiguration.certificates, this._configuration.certificates)
|
|
2132
|
+
) {
|
|
2133
|
+
throw makeDOMException("certificates cannot be changed", "InvalidModificationError");
|
|
2134
|
+
}
|
|
2135
|
+
this._configuration = normalizedConfiguration;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
get localDescription() {
|
|
2139
|
+
return this._pendingLocalDescription || this._currentLocalDescription;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
get currentLocalDescription() {
|
|
2143
|
+
return this._currentLocalDescription;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
get pendingLocalDescription() {
|
|
2147
|
+
return this._pendingLocalDescription;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
get remoteDescription() {
|
|
2151
|
+
return this._pendingRemoteDescription || this._currentRemoteDescription;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
get currentRemoteDescription() {
|
|
2155
|
+
return this._currentRemoteDescription;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
get pendingRemoteDescription() {
|
|
2159
|
+
return this._pendingRemoteDescription;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
get signalingState() {
|
|
2163
|
+
return this._closed ? "closed" : this._signalingState;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
get iceGatheringState() {
|
|
2167
|
+
return this._iceGatheringState;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
get iceConnectionState() {
|
|
2171
|
+
return this._iceConnectionState;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
get connectionState() {
|
|
2175
|
+
return this._connectionState;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
get canTrickleIceCandidates() {
|
|
2179
|
+
return this._canTrickleIceCandidates;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
get sctp() {
|
|
2183
|
+
return this._sctpTransport;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
get ondatachannel() {
|
|
2187
|
+
return this._ondatachannel;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
set ondatachannel(callback) {
|
|
2191
|
+
this._ondatachannel = typeof callback === "function" ? callback : null;
|
|
2192
|
+
this._scheduleDataChannelFlush();
|
|
2193
|
+
this._scheduleDataChannelAnnouncementRepair();
|
|
2194
|
+
this._pairedPeer?._scheduleDataChannelAnnouncementRepair();
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
createDataChannel(label, init = {}) {
|
|
2198
|
+
this._assertNotClosed();
|
|
2199
|
+
if (arguments.length === 0) throw new TypeError("createDataChannel requires a label");
|
|
2200
|
+
const options = normalizeDataChannelInit(init);
|
|
2201
|
+
const stringLabel = String(label);
|
|
2202
|
+
validateByteLength(stringLabel, "label");
|
|
2203
|
+
validateByteLength(options.protocol, "protocol");
|
|
2204
|
+
if (options.negotiated && this._isDataChannelIdInUse(options.id)) {
|
|
2205
|
+
throw makeDOMException("RTCDataChannel id is already in use", "OperationError");
|
|
2206
|
+
}
|
|
2207
|
+
const nativeChannel = this._createNativeDataChannel(stringLabel, options);
|
|
2208
|
+
const assignedId = options.negotiated ? options.id : null;
|
|
2209
|
+
const channel = RTCDataChannel._fromNative(this, nativeChannel, undefined, assignedId);
|
|
2210
|
+
channel._createdLocally = true;
|
|
2211
|
+
this._localDataChannelCount += 1;
|
|
2212
|
+
if (this._localDataChannelCount === 1) this._markNegotiationNeeded();
|
|
2213
|
+
this._assignDataChannelIdsFromDtlsRole();
|
|
2214
|
+
this._scheduleDataChannelOpenRepair();
|
|
2215
|
+
this._pairedPeer?._scheduleDataChannelAnnouncementRepair();
|
|
2216
|
+
return channel;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
_createNativeDataChannel(label, options) {
|
|
2220
|
+
try {
|
|
2221
|
+
return this._ensureNativePeerConnection().createDataChannel(label, options);
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
const highNegotiatedStream =
|
|
2224
|
+
options.negotiated &&
|
|
2225
|
+
options.id >= LIBDATACHANNEL_SCTP_MAX_CHANNELS &&
|
|
2226
|
+
/stream id is too high/i.test(error?.message || "");
|
|
2227
|
+
if (!highNegotiatedStream) throw error;
|
|
2228
|
+
return new UnsupportedNativeDataChannel(label, options);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
async createOffer(options = undefined) {
|
|
2233
|
+
this._assertNotClosed();
|
|
2234
|
+
const optionsObject = options !== null && typeof options === "object" ? options : {};
|
|
2235
|
+
const iceRestartRequested = optionsObject.iceRestart === true;
|
|
2236
|
+
if (this._signalingState !== "stable" && this._signalingState !== "have-local-offer") {
|
|
2237
|
+
await delayInvalidStateIfOperationPending(this);
|
|
2238
|
+
throw makeDOMException("Cannot create offer in current signaling state", "InvalidStateError");
|
|
2239
|
+
}
|
|
2240
|
+
this._operationsPending += 1;
|
|
2241
|
+
try {
|
|
2242
|
+
await nextTask();
|
|
2243
|
+
if (iceRestartRequested) this._iceRestartPending = true;
|
|
2244
|
+
let offer = this._ensureNativePeerConnection().createOffer();
|
|
2245
|
+
const jsOnlyIceRestart = this._iceRestartPending && !this._canApplyNativeIceCredentials();
|
|
2246
|
+
if (this._iceRestartPending) {
|
|
2247
|
+
offer = {
|
|
2248
|
+
type: offer.type,
|
|
2249
|
+
sdp: rewriteIceCredentials(offer.sdp, this._ensureIceRestartCredentials()),
|
|
2250
|
+
};
|
|
2251
|
+
if (jsOnlyIceRestart) markJsOnlyIceRestart(offer);
|
|
2252
|
+
}
|
|
2253
|
+
this._lastCreatedOffer = new RTCSessionDescription(offer);
|
|
2254
|
+
if (jsOnlyIceRestart) markJsOnlyIceRestart(this._lastCreatedOffer);
|
|
2255
|
+
return offer;
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
throw mapNativeError(error, "InvalidStateError");
|
|
2258
|
+
} finally {
|
|
2259
|
+
this._finishPendingOperation();
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
restartIce() {
|
|
2264
|
+
if (this._closed) return;
|
|
2265
|
+
if (!this._localDescription && !this._remoteDescription) return;
|
|
2266
|
+
this._iceRestartPending = true;
|
|
2267
|
+
this._pendingIceRestartCredentials = null;
|
|
2268
|
+
if (this._signalingState === "stable") this._markNegotiationNeeded();
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
async createAnswer() {
|
|
2272
|
+
this._assertNotClosed();
|
|
2273
|
+
if (!this.remoteDescription) {
|
|
2274
|
+
await delayInvalidStateIfOperationPending(this);
|
|
2275
|
+
throw makeDOMException("Remote description is not set", "InvalidStateError");
|
|
2276
|
+
}
|
|
2277
|
+
this._operationsPending += 1;
|
|
2278
|
+
try {
|
|
2279
|
+
await nextTask();
|
|
2280
|
+
if (this._jsOnlyIceRestartRemoteOffer) {
|
|
2281
|
+
const answer = markJsOnlyIceRestart(
|
|
2282
|
+
new RTCSessionDescription(this._ensureNativePeerConnection().createAnswer()),
|
|
2283
|
+
);
|
|
2284
|
+
this._lastCreatedAnswer = answer;
|
|
2285
|
+
Object.defineProperty(answer, "_webrtcNodeAnswerer", {
|
|
2286
|
+
value: this,
|
|
2287
|
+
configurable: true,
|
|
2288
|
+
});
|
|
2289
|
+
return answer;
|
|
2290
|
+
}
|
|
2291
|
+
if (isNoMediaSdp(this.remoteDescription)) {
|
|
2292
|
+
const answer = {
|
|
2293
|
+
type: "answer",
|
|
2294
|
+
sdp: this.remoteDescription.sdp,
|
|
2295
|
+
};
|
|
2296
|
+
this._lastCreatedAnswer = new RTCSessionDescription(answer);
|
|
2297
|
+
return answer;
|
|
2298
|
+
}
|
|
2299
|
+
const answer = this._ensureNativePeerConnection().createAnswer();
|
|
2300
|
+
this._lastCreatedAnswer = new RTCSessionDescription(answer);
|
|
2301
|
+
Object.defineProperty(answer, "_webrtcNodeAnswerer", {
|
|
2302
|
+
value: this,
|
|
2303
|
+
configurable: true,
|
|
2304
|
+
});
|
|
2305
|
+
return answer;
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
throw mapNativeError(error, "InvalidStateError");
|
|
2308
|
+
} finally {
|
|
2309
|
+
this._finishPendingOperation();
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
async setLocalDescription(description = undefined) {
|
|
2314
|
+
this._assertNotClosed();
|
|
2315
|
+
const jsOnlyIceRestartDescription = Boolean(description?._webrtcNodeJsOnlyIceRestart);
|
|
2316
|
+
let normalized = description === undefined ? null : normalizeDescription(description);
|
|
2317
|
+
const type = normalized ? normalized.type : this._implicitLocalDescriptionType();
|
|
2318
|
+
if (
|
|
2319
|
+
normalized?.type === "rollback" &&
|
|
2320
|
+
this._signalingState !== "have-local-offer" &&
|
|
2321
|
+
this._operationsPending === 0
|
|
2322
|
+
) {
|
|
2323
|
+
throw makeDOMException(
|
|
2324
|
+
"Cannot roll back a local description in current signaling state",
|
|
2325
|
+
"InvalidStateError",
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
this._operationsPending += 1;
|
|
2329
|
+
try {
|
|
2330
|
+
await nextTask();
|
|
2331
|
+
if (this._closed) return new Promise(() => {});
|
|
2332
|
+
if (normalized?.type === "offer") {
|
|
2333
|
+
if (this._signalingState !== "stable" && this._signalingState !== "have-local-offer") {
|
|
2334
|
+
throw makeDOMException(
|
|
2335
|
+
"Cannot set a local offer in current signaling state",
|
|
2336
|
+
"InvalidStateError",
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
if (normalized.sdp === "" && this._lastCreatedOffer) normalized = this._lastCreatedOffer;
|
|
2340
|
+
if (!this._lastCreatedOffer || normalized.sdp !== this._lastCreatedOffer.sdp) {
|
|
2341
|
+
throw makeDOMException(
|
|
2342
|
+
"Local offer does not match the last created offer",
|
|
2343
|
+
"InvalidModificationError",
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
if (normalized?.type === "rollback") {
|
|
2348
|
+
if (this._signalingState !== "have-local-offer") {
|
|
2349
|
+
throw makeDOMException(
|
|
2350
|
+
"Cannot roll back a local description in current signaling state",
|
|
2351
|
+
"InvalidStateError",
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
const previousState = this._signalingState;
|
|
2355
|
+
const previousGatheringState = this._iceGatheringState;
|
|
2356
|
+
const rollingBackInitialOffer = !this._currentLocalDescription;
|
|
2357
|
+
const nativeBackedRollback =
|
|
2358
|
+
this._localDescription && !isNoMediaSdp(this._localDescription);
|
|
2359
|
+
if (nativeBackedRollback) {
|
|
2360
|
+
this._suppressNextNativeSignalingState = "stable";
|
|
2361
|
+
try {
|
|
2362
|
+
this._ensureNativePeerConnection().setLocalDescription("rollback");
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
this._suppressNextNativeSignalingState = null;
|
|
2365
|
+
throw error;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
this._rollbackLocalDescription();
|
|
2369
|
+
this._localDescriptionSetByApi = hasDataMediaSection(this._localDescription);
|
|
2370
|
+
this._localIceCandidates = [];
|
|
2371
|
+
this._unregisterLocalDescriptionsForPairing();
|
|
2372
|
+
this._syncStatesFromNative();
|
|
2373
|
+
this._signalingState = "stable";
|
|
2374
|
+
if (rollingBackInitialOffer) {
|
|
2375
|
+
this._iceGatheringState = "new";
|
|
2376
|
+
}
|
|
2377
|
+
this._refreshIceRole();
|
|
2378
|
+
this._updateSctpTransport();
|
|
2379
|
+
if (rollingBackInitialOffer && previousGatheringState !== "new") {
|
|
2380
|
+
this.dispatchEvent(makeEvent("icegatheringstatechange"));
|
|
2381
|
+
this._iceTransport()?.dispatchEvent(makeEvent("gatheringstatechange"));
|
|
2382
|
+
}
|
|
2383
|
+
if (previousState !== this._signalingState)
|
|
2384
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2385
|
+
await nextTask();
|
|
2386
|
+
if (this._suppressNextNativeSignalingState === "stable")
|
|
2387
|
+
this._suppressNextNativeSignalingState = null;
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
if (normalized?.type === "pranswer") {
|
|
2391
|
+
if (
|
|
2392
|
+
this._signalingState !== "have-remote-offer" &&
|
|
2393
|
+
this._signalingState !== "have-local-pranswer"
|
|
2394
|
+
) {
|
|
2395
|
+
throw makeDOMException(
|
|
2396
|
+
"Cannot set a local pranswer in current signaling state",
|
|
2397
|
+
"InvalidStateError",
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
const previousState = this._signalingState;
|
|
2401
|
+
this._setPendingLocalDescription(normalized);
|
|
2402
|
+
this._localDescriptionSetByApi = hasDataMediaSection(this._localDescription);
|
|
2403
|
+
this._registerLocalDescriptionForPairing();
|
|
2404
|
+
this._signalingState = "have-local-pranswer";
|
|
2405
|
+
this._refreshIceRole();
|
|
2406
|
+
this._updateSctpTransport();
|
|
2407
|
+
if (previousState !== this._signalingState)
|
|
2408
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2409
|
+
await nextTask();
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
let alreadyAppliedAnswer = false;
|
|
2413
|
+
if (normalized?.type === "answer") {
|
|
2414
|
+
if (normalized.sdp === "" && this._lastCreatedAnswer) normalized = this._lastCreatedAnswer;
|
|
2415
|
+
const answerCreatedByThisPeer = description && description._webrtcNodeAnswerer === this;
|
|
2416
|
+
alreadyAppliedAnswer =
|
|
2417
|
+
this._localDescription?.type === "answer" &&
|
|
2418
|
+
(this._localDescription.sdp === normalized.sdp || answerCreatedByThisPeer);
|
|
2419
|
+
if (
|
|
2420
|
+
!alreadyAppliedAnswer &&
|
|
2421
|
+
this._signalingState !== "have-remote-offer" &&
|
|
2422
|
+
this._signalingState !== "have-local-pranswer"
|
|
2423
|
+
) {
|
|
2424
|
+
throw makeDOMException(
|
|
2425
|
+
"Cannot set a local answer in current signaling state",
|
|
2426
|
+
"InvalidStateError",
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
if (!this._lastCreatedAnswer || normalized.sdp !== this._lastCreatedAnswer.sdp) {
|
|
2430
|
+
throw makeDOMException(
|
|
2431
|
+
"Local answer does not match the last created answer",
|
|
2432
|
+
"InvalidModificationError",
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
if (alreadyAppliedAnswer) {
|
|
2437
|
+
this._localDescriptionSetByApi = hasDataMediaSection(this._localDescription);
|
|
2438
|
+
this._syncStatesFromNative();
|
|
2439
|
+
this._refreshIceRole();
|
|
2440
|
+
for (const candidate of this._pendingIce.splice(0)) await this.addIceCandidate(candidate);
|
|
2441
|
+
this._flushPendingRemoteCandidatesForNative();
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
if (normalized && isNoMediaSdp(normalized)) {
|
|
2445
|
+
await this._applyNoMediaLocalDescription(normalized);
|
|
2446
|
+
this._localDescriptionSetByApi = hasDataMediaSection(this._localDescription);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
if (!normalized && type === "offer") {
|
|
2450
|
+
const offer =
|
|
2451
|
+
this._lastCreatedOffer ||
|
|
2452
|
+
new RTCSessionDescription(this._ensureNativePeerConnection().createOffer());
|
|
2453
|
+
if (isNoMediaSdp(offer)) {
|
|
2454
|
+
await this._applyNoMediaLocalDescription(offer);
|
|
2455
|
+
this._localDescriptionSetByApi = hasDataMediaSection(this._localDescription);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (!normalized && type === "answer" && isNoMediaSdp(this.remoteDescription)) {
|
|
2460
|
+
const answer =
|
|
2461
|
+
this._lastCreatedAnswer ||
|
|
2462
|
+
new RTCSessionDescription({
|
|
2463
|
+
type: "answer",
|
|
2464
|
+
sdp: this.remoteDescription.sdp,
|
|
2465
|
+
});
|
|
2466
|
+
await this._applyNoMediaLocalDescription(answer);
|
|
2467
|
+
this._localDescriptionSetByApi = hasDataMediaSection(this._localDescription);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
if (type === "answer" && this._jsOnlyIceRestartRemoteOffer) {
|
|
2471
|
+
const answer =
|
|
2472
|
+
normalized ||
|
|
2473
|
+
this._lastCreatedAnswer ||
|
|
2474
|
+
new RTCSessionDescription(this._ensureNativePeerConnection().createAnswer());
|
|
2475
|
+
await this._applyJsOnlyLocalAnswer(answer);
|
|
2476
|
+
this._localDescriptionSetByApi = hasDataMediaSection(this._localDescription);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
const previousIceGatheringState = this._iceGatheringState;
|
|
2480
|
+
const previousSignalingState = this._signalingState;
|
|
2481
|
+
let appliedIceRestart = false;
|
|
2482
|
+
let manuallyUpdatedSignalingState = false;
|
|
2483
|
+
let usedJsOnlyIceRestart = false;
|
|
2484
|
+
if (this._localDescription?.type === type && type !== "offer") {
|
|
2485
|
+
this._syncStatesFromNative();
|
|
2486
|
+
} else if (this._preparedLocalDescription && type === this._preparedLocalDescription.type) {
|
|
2487
|
+
this._refreshCurrentOrPendingLocalDescription(this._preparedLocalDescription);
|
|
2488
|
+
this._preparedLocalDescription = null;
|
|
2489
|
+
this._syncStatesFromNative();
|
|
2490
|
+
} else {
|
|
2491
|
+
const hadIceRestartRequest =
|
|
2492
|
+
(this._iceRestartPending || jsOnlyIceRestartDescription) && type === "offer";
|
|
2493
|
+
const useJsOnlyIceRestart = hadIceRestartRequest && !this._canApplyNativeIceCredentials();
|
|
2494
|
+
if (useJsOnlyIceRestart) {
|
|
2495
|
+
let localDescription = new RTCSessionDescription(
|
|
2496
|
+
normalized ||
|
|
2497
|
+
this._lastCreatedOffer ||
|
|
2498
|
+
this._ensureNativePeerConnection().createOffer(),
|
|
2499
|
+
);
|
|
2500
|
+
localDescription = new RTCSessionDescription({
|
|
2501
|
+
type: localDescription.type,
|
|
2502
|
+
sdp: rewriteIceCredentials(localDescription.sdp, this._ensureIceRestartCredentials()),
|
|
2503
|
+
});
|
|
2504
|
+
this._localDescription = markJsOnlyIceRestart(localDescription);
|
|
2505
|
+
this._jsOnlyIceRestartOfferPending = true;
|
|
2506
|
+
this._signalingState = "have-local-offer";
|
|
2507
|
+
appliedIceRestart = true;
|
|
2508
|
+
manuallyUpdatedSignalingState = true;
|
|
2509
|
+
usedJsOnlyIceRestart = true;
|
|
2510
|
+
} else {
|
|
2511
|
+
const localDescriptionInit = this._localOfferInit(type);
|
|
2512
|
+
this._ensureNativePeerConnection().setLocalDescription(type, localDescriptionInit);
|
|
2513
|
+
appliedIceRestart = hadIceRestartRequest;
|
|
2514
|
+
const nativeDescription = this._ensureNativePeerConnection().localDescription();
|
|
2515
|
+
this._localDescription = nativeDescription
|
|
2516
|
+
? new RTCSessionDescription(nativeDescription)
|
|
2517
|
+
: null;
|
|
2518
|
+
this._syncStatesFromNative();
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
if (type === "offer") {
|
|
2522
|
+
this._setPendingLocalDescription(this._localDescription);
|
|
2523
|
+
} else if (type === "answer") {
|
|
2524
|
+
this._commitRemoteDescription();
|
|
2525
|
+
this._commitLocalDescription(this._localDescription);
|
|
2526
|
+
for (const candidate of this._pendingIce.splice(0)) await this.addIceCandidate(candidate);
|
|
2527
|
+
this._flushPendingRemoteCandidatesForNative();
|
|
2528
|
+
}
|
|
2529
|
+
this._localDescriptionSetByApi = hasDataMediaSection(this._localDescription);
|
|
2530
|
+
this._clearNegotiationNeededIfDataMLineIsPresent();
|
|
2531
|
+
this._refreshIceRole();
|
|
2532
|
+
if (manuallyUpdatedSignalingState && previousSignalingState !== this._signalingState)
|
|
2533
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2534
|
+
if (appliedIceRestart) {
|
|
2535
|
+
if (previousIceGatheringState === "complete") this._queueSyntheticIceRestartGathering();
|
|
2536
|
+
this._clearIceRestartRequest();
|
|
2537
|
+
}
|
|
2538
|
+
await nextTask();
|
|
2539
|
+
if (usedJsOnlyIceRestart) {
|
|
2540
|
+
this._registerLocalDescriptionForPairing();
|
|
2541
|
+
} else {
|
|
2542
|
+
await this._refreshLocalDescriptionAfterGatheringWindow();
|
|
2543
|
+
this._registerLocalDescriptionForPairing();
|
|
2544
|
+
}
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
throw mapNativeError(error, "InvalidStateError");
|
|
2547
|
+
} finally {
|
|
2548
|
+
this._finishPendingOperation();
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
async setRemoteDescription(description) {
|
|
2553
|
+
this._assertNotClosed();
|
|
2554
|
+
const jsOnlyIceRestartDescription = Boolean(description?._webrtcNodeJsOnlyIceRestart);
|
|
2555
|
+
const normalized = normalizeDescription(description);
|
|
2556
|
+
if (
|
|
2557
|
+
normalized.type === "answer" &&
|
|
2558
|
+
this._signalingState !== "have-local-offer" &&
|
|
2559
|
+
this._signalingState !== "have-remote-pranswer" &&
|
|
2560
|
+
this._operationsPending === 0
|
|
2561
|
+
) {
|
|
2562
|
+
throw makeDOMException(
|
|
2563
|
+
"Cannot set a remote answer in current signaling state",
|
|
2564
|
+
"InvalidStateError",
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
if (
|
|
2568
|
+
normalized.type === "pranswer" &&
|
|
2569
|
+
this._signalingState !== "have-local-offer" &&
|
|
2570
|
+
this._signalingState !== "have-remote-pranswer" &&
|
|
2571
|
+
this._operationsPending === 0
|
|
2572
|
+
) {
|
|
2573
|
+
throw makeDOMException(
|
|
2574
|
+
"Cannot set a remote pranswer in current signaling state",
|
|
2575
|
+
"InvalidStateError",
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
if (
|
|
2579
|
+
normalized.type === "rollback" &&
|
|
2580
|
+
this._signalingState !== "have-remote-offer" &&
|
|
2581
|
+
this._operationsPending === 0
|
|
2582
|
+
) {
|
|
2583
|
+
throw makeDOMException(
|
|
2584
|
+
"Cannot roll back a remote description in current signaling state",
|
|
2585
|
+
"InvalidStateError",
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
this._operationsPending += 1;
|
|
2589
|
+
try {
|
|
2590
|
+
await nextTask();
|
|
2591
|
+
if (this._closed) return new Promise(() => {});
|
|
2592
|
+
if (
|
|
2593
|
+
normalized.type === "answer" &&
|
|
2594
|
+
this._signalingState !== "have-local-offer" &&
|
|
2595
|
+
this._signalingState !== "have-remote-pranswer"
|
|
2596
|
+
) {
|
|
2597
|
+
throw makeDOMException(
|
|
2598
|
+
"Cannot set a remote answer in current signaling state",
|
|
2599
|
+
"InvalidStateError",
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
if (normalized.type === "pranswer") {
|
|
2603
|
+
if (
|
|
2604
|
+
this._signalingState !== "have-local-offer" &&
|
|
2605
|
+
this._signalingState !== "have-remote-pranswer"
|
|
2606
|
+
) {
|
|
2607
|
+
throw makeDOMException(
|
|
2608
|
+
"Cannot set a remote pranswer in current signaling state",
|
|
2609
|
+
"InvalidStateError",
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
const previousState = this._signalingState;
|
|
2613
|
+
this._setPendingRemoteDescription(normalized);
|
|
2614
|
+
this._pairWithRemoteDescription(this._remoteDescription);
|
|
2615
|
+
this._canTrickleIceCandidates = hasTrickleIceOption(normalized);
|
|
2616
|
+
this._signalingState = "have-remote-pranswer";
|
|
2617
|
+
this._refreshIceRole();
|
|
2618
|
+
this._updateSctpTransport();
|
|
2619
|
+
if (previousState !== this._signalingState)
|
|
2620
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2621
|
+
await nextTask();
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (
|
|
2625
|
+
normalized.type === "offer" &&
|
|
2626
|
+
(this._signalingState === "have-local-offer" ||
|
|
2627
|
+
this._signalingState === "have-remote-pranswer")
|
|
2628
|
+
) {
|
|
2629
|
+
await this._implicitRollbackLocalDescription();
|
|
2630
|
+
if (this._closed) return new Promise(() => {});
|
|
2631
|
+
}
|
|
2632
|
+
assertValidSdpSyntax(normalized);
|
|
2633
|
+
if (normalized.type === "rollback") {
|
|
2634
|
+
if (this._signalingState !== "have-remote-offer") {
|
|
2635
|
+
throw makeDOMException(
|
|
2636
|
+
"Cannot roll back a remote description in current signaling state",
|
|
2637
|
+
"InvalidStateError",
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
const previousState = this._signalingState;
|
|
2641
|
+
const nativeBackedRollback =
|
|
2642
|
+
this._remoteDescription && !isNoMediaSdp(this._remoteDescription);
|
|
2643
|
+
if (nativeBackedRollback) {
|
|
2644
|
+
this._suppressNextNativeSignalingState = "stable";
|
|
2645
|
+
try {
|
|
2646
|
+
this._ensureNativePeerConnection().setRemoteDescription({ type: "rollback", sdp: "" });
|
|
2647
|
+
} catch (error) {
|
|
2648
|
+
this._suppressNextNativeSignalingState = null;
|
|
2649
|
+
throw error;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
this._rollbackRemoteDescription();
|
|
2653
|
+
this._remoteIceCandidates = [];
|
|
2654
|
+
if (this._pairedPeer?._pairedPeer === this) this._pairedPeer._pairedPeer = null;
|
|
2655
|
+
this._pairedPeer = null;
|
|
2656
|
+
this._selfRemoteDescription = false;
|
|
2657
|
+
this._syncStatesFromNative();
|
|
2658
|
+
this._signalingState = "stable";
|
|
2659
|
+
this._refreshIceRole();
|
|
2660
|
+
this._updateSctpTransport();
|
|
2661
|
+
if (previousState !== this._signalingState)
|
|
2662
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2663
|
+
await nextTask();
|
|
2664
|
+
if (this._suppressNextNativeSignalingState === "stable")
|
|
2665
|
+
this._suppressNextNativeSignalingState = null;
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
if (
|
|
2669
|
+
normalized.type === "offer" &&
|
|
2670
|
+
this._isJsOnlyIceRestartRemoteOffer(normalized, jsOnlyIceRestartDescription)
|
|
2671
|
+
) {
|
|
2672
|
+
const previousState = this._signalingState;
|
|
2673
|
+
this._setPendingRemoteDescription(markJsOnlyIceRestart(normalized));
|
|
2674
|
+
this._pairWithRemoteDescription(this._remoteDescription);
|
|
2675
|
+
this._canTrickleIceCandidates = hasTrickleIceOption(normalized);
|
|
2676
|
+
this._signalingState = "have-remote-offer";
|
|
2677
|
+
this._jsOnlyIceRestartRemoteOffer = true;
|
|
2678
|
+
this._refreshIceRole();
|
|
2679
|
+
this._updateSctpTransport();
|
|
2680
|
+
if (previousState !== this._signalingState)
|
|
2681
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2682
|
+
await nextTask();
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
if (
|
|
2686
|
+
normalized.type === "answer" &&
|
|
2687
|
+
(jsOnlyIceRestartDescription || this._jsOnlyIceRestartOfferPending)
|
|
2688
|
+
) {
|
|
2689
|
+
const previousState = this._signalingState;
|
|
2690
|
+
let remoteDescription = normalized;
|
|
2691
|
+
if (description && description._webrtcNodeAnswerer) {
|
|
2692
|
+
remoteDescription = await description._webrtcNodeAnswerer._ensureLocalAnswerApplied();
|
|
2693
|
+
}
|
|
2694
|
+
this._setPendingRemoteDescription(markJsOnlyIceRestart(remoteDescription));
|
|
2695
|
+
this._commitLocalDescription();
|
|
2696
|
+
this._commitRemoteDescription(this._remoteDescription);
|
|
2697
|
+
this._canTrickleIceCandidates = hasTrickleIceOption(remoteDescription);
|
|
2698
|
+
this._signalingState = "stable";
|
|
2699
|
+
this._jsOnlyIceRestartOfferPending = false;
|
|
2700
|
+
this._clearIceRestartRequest();
|
|
2701
|
+
this._refreshIceRole();
|
|
2702
|
+
this._assignDataChannelIdsFromDtlsRole();
|
|
2703
|
+
this._updateSctpTransport();
|
|
2704
|
+
if (previousState !== this._signalingState)
|
|
2705
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2706
|
+
await nextTask();
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
if (isNoMediaSdp(normalized)) {
|
|
2710
|
+
if (normalized.type === "offer") {
|
|
2711
|
+
this._rollbackLocalDescription();
|
|
2712
|
+
this._setPendingRemoteDescription(normalized);
|
|
2713
|
+
} else if (normalized.type === "answer") {
|
|
2714
|
+
this._commitLocalDescription();
|
|
2715
|
+
this._commitRemoteDescription(normalized);
|
|
2716
|
+
} else {
|
|
2717
|
+
this._setPendingRemoteDescription(normalized);
|
|
2718
|
+
}
|
|
2719
|
+
this._pairWithRemoteDescription(this._remoteDescription);
|
|
2720
|
+
this._canTrickleIceCandidates = hasTrickleIceOption(normalized);
|
|
2721
|
+
if (normalized.type === "offer") {
|
|
2722
|
+
this._signalingState = "have-remote-offer";
|
|
2723
|
+
} else if (normalized.type === "answer") {
|
|
2724
|
+
this._signalingState = "stable";
|
|
2725
|
+
}
|
|
2726
|
+
this._refreshIceRole();
|
|
2727
|
+
this._updateSctpTransport();
|
|
2728
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2729
|
+
await nextTask();
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
if (normalized.type === "offer" && this._signalingState === "have-remote-offer") {
|
|
2733
|
+
this._setPendingRemoteDescription(normalized);
|
|
2734
|
+
this._pairWithRemoteDescription(this._remoteDescription);
|
|
2735
|
+
this._canTrickleIceCandidates = hasTrickleIceOption(normalized);
|
|
2736
|
+
this._refreshIceRole();
|
|
2737
|
+
this._updateSctpTransport();
|
|
2738
|
+
await nextTask();
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
let remoteDescription = normalized;
|
|
2742
|
+
if (normalized.type === "answer" && description && description._webrtcNodeAnswerer) {
|
|
2743
|
+
remoteDescription = await description._webrtcNodeAnswerer._ensureLocalAnswerApplied();
|
|
2744
|
+
}
|
|
2745
|
+
let nativeRemoteDescription = remoteDescription;
|
|
2746
|
+
let deferredCandidatesForNative = [];
|
|
2747
|
+
if (normalized.type === "offer") {
|
|
2748
|
+
deferredCandidatesForNative = extractIceCandidatesFromDescription(remoteDescription);
|
|
2749
|
+
if (deferredCandidatesForNative.length > 0) {
|
|
2750
|
+
nativeRemoteDescription = stripIceCandidateLinesFromDescription(remoteDescription);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
this._ensureNativePeerConnection().setRemoteDescription(nativeRemoteDescription.toJSON());
|
|
2754
|
+
this._queuePendingRemoteCandidatesForNative(deferredCandidatesForNative);
|
|
2755
|
+
this._canTrickleIceCandidates = hasTrickleIceOption(remoteDescription);
|
|
2756
|
+
this._remoteDescription = new RTCSessionDescription(remoteDescription);
|
|
2757
|
+
this._pairWithRemoteDescription(this._remoteDescription);
|
|
2758
|
+
this._syncStatesFromNative();
|
|
2759
|
+
if (normalized.type === "offer") {
|
|
2760
|
+
this._rollbackLocalDescription();
|
|
2761
|
+
this._setPendingRemoteDescription(this._remoteDescription);
|
|
2762
|
+
} else if (normalized.type === "answer") {
|
|
2763
|
+
this._commitLocalDescription();
|
|
2764
|
+
this._commitRemoteDescription(this._remoteDescription);
|
|
2765
|
+
}
|
|
2766
|
+
this._refreshIceRole();
|
|
2767
|
+
this._assignDataChannelIdsFromDtlsRole();
|
|
2768
|
+
if (normalized.type !== "offer") {
|
|
2769
|
+
for (const candidate of this._pendingIce.splice(0)) await this.addIceCandidate(candidate);
|
|
2770
|
+
this._flushPendingRemoteCandidatesForNative();
|
|
2771
|
+
}
|
|
2772
|
+
await nextTask();
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
throw mapNativeError(error, "InvalidStateError");
|
|
2775
|
+
} finally {
|
|
2776
|
+
this._finishPendingOperation();
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
async addIceCandidate(candidate = null) {
|
|
2781
|
+
this._assertNotClosed();
|
|
2782
|
+
const hasArgument = arguments.length > 0;
|
|
2783
|
+
if (!hasArgument && !this.remoteDescription && this._operationsPending === 0) {
|
|
2784
|
+
throw makeDOMException("Remote description is not set", "InvalidStateError");
|
|
2785
|
+
}
|
|
2786
|
+
if (candidate === null && hasArgument && !this.remoteDescription) return;
|
|
2787
|
+
if (candidate instanceof RTCIceCandidate && candidate._webrtcNodeLocalCandidate) {
|
|
2788
|
+
await this._addExchangedLocalCandidate(candidate);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
const normalized = normalizeAddIceCandidateInput(candidate);
|
|
2792
|
+
if (!this.remoteDescription) {
|
|
2793
|
+
await delayInvalidStateIfOperationPending(this);
|
|
2794
|
+
if (!this.remoteDescription) {
|
|
2795
|
+
throw makeDOMException("Remote description is not set", "InvalidStateError");
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
this._operationsPending += 1;
|
|
2799
|
+
try {
|
|
2800
|
+
await nextTask();
|
|
2801
|
+
const remoteDescription = appendRemoteCandidateToDescription(
|
|
2802
|
+
this._remoteDescription,
|
|
2803
|
+
normalized,
|
|
2804
|
+
);
|
|
2805
|
+
if (normalized.candidate === "") {
|
|
2806
|
+
this._refreshCurrentOrPendingRemoteDescription(remoteDescription);
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
const nativeCandidate = normalized.toJSON();
|
|
2810
|
+
if (nativeCandidate.sdpMid === null) {
|
|
2811
|
+
nativeCandidate.sdpMid = resolveCandidateMid(this._remoteDescription, normalized);
|
|
2812
|
+
}
|
|
2813
|
+
if (nativeCandidate.candidate) {
|
|
2814
|
+
this._rememberRemoteIceCandidate(
|
|
2815
|
+
new RTCIceCandidate({
|
|
2816
|
+
candidate: nativeCandidate.candidate,
|
|
2817
|
+
sdpMid: nativeCandidate.sdpMid,
|
|
2818
|
+
sdpMLineIndex: nativeCandidate.sdpMLineIndex,
|
|
2819
|
+
usernameFragment: nativeCandidate.usernameFragment,
|
|
2820
|
+
}),
|
|
2821
|
+
);
|
|
2822
|
+
this._markExplicitIceCandidateExchange();
|
|
2823
|
+
}
|
|
2824
|
+
if (this._shouldDeferRemoteCandidateUntilLocalAnswer()) {
|
|
2825
|
+
this._queuePendingRemoteCandidatesForNative([normalized]);
|
|
2826
|
+
this._refreshCurrentOrPendingRemoteDescription(remoteDescription);
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
this._ensureNativePeerConnection().addRemoteCandidate(nativeCandidate);
|
|
2830
|
+
this._refreshCurrentOrPendingRemoteDescription(remoteDescription);
|
|
2831
|
+
} catch (error) {
|
|
2832
|
+
throw mapNativeError(error, "OperationError");
|
|
2833
|
+
} finally {
|
|
2834
|
+
this._finishPendingOperation();
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
async _addExchangedLocalCandidate(candidate) {
|
|
2839
|
+
if (!candidate.candidate) return;
|
|
2840
|
+
this._sameProcessIceCandidateExchange = true;
|
|
2841
|
+
if (this._pairedPeer) this._pairedPeer._sameProcessIceCandidateExchange = true;
|
|
2842
|
+
this._rememberRemoteIceCandidate(candidate);
|
|
2843
|
+
this._markExplicitIceCandidateExchange();
|
|
2844
|
+
if (!this.remoteDescription || this._shouldDeferRemoteCandidateUntilLocalAnswer()) {
|
|
2845
|
+
if (!this._pendingIce.some((entry) => sameCandidate(entry, candidate))) {
|
|
2846
|
+
this._pendingIce.push(candidate);
|
|
2847
|
+
}
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
if (this._remoteDescription?.sdp?.includes(candidate.candidate)) return;
|
|
2852
|
+
const nativeCandidate = candidate.toJSON();
|
|
2853
|
+
if (nativeCandidate.sdpMid === null) {
|
|
2854
|
+
nativeCandidate.sdpMid = resolveCandidateMid(this._remoteDescription, candidate);
|
|
2855
|
+
}
|
|
2856
|
+
try {
|
|
2857
|
+
this._ensureNativePeerConnection().addRemoteCandidate(nativeCandidate);
|
|
2858
|
+
this._refreshCurrentOrPendingRemoteDescription(
|
|
2859
|
+
appendRemoteCandidateToDescription(this._remoteDescription, candidate),
|
|
2860
|
+
);
|
|
2861
|
+
} catch {
|
|
2862
|
+
// Exchanged local candidates are delivered from event handlers that do
|
|
2863
|
+
// not await addIceCandidate(); keep them best-effort like browser ICE.
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
close() {
|
|
2868
|
+
if (this._closed) return;
|
|
2869
|
+
this._closed = true;
|
|
2870
|
+
const pairedPeer = this._pairedPeer;
|
|
2871
|
+
this._unregisterLocalDescriptionsForPairing();
|
|
2872
|
+
const nativePeer = this._native;
|
|
2873
|
+
if (nativePeer) {
|
|
2874
|
+
setTimeout(() => {
|
|
2875
|
+
try {
|
|
2876
|
+
nativePeer.close();
|
|
2877
|
+
} catch {
|
|
2878
|
+
// JS-visible close state is already final; native teardown is best-effort.
|
|
2879
|
+
}
|
|
2880
|
+
}, PEER_CONNECTION_NATIVE_CLOSE_DELAY_MS);
|
|
2881
|
+
}
|
|
2882
|
+
setTimeout(() => {
|
|
2883
|
+
if (pairedPeer && !pairedPeer._closed) pairedPeer._handleRemotePeerClosed();
|
|
2884
|
+
}, 0);
|
|
2885
|
+
this._connectionState = "closed";
|
|
2886
|
+
this._iceConnectionState = "closed";
|
|
2887
|
+
this._deferredIceEvents = [];
|
|
2888
|
+
this._signalingState = "closed";
|
|
2889
|
+
this._updateSctpTransport();
|
|
2890
|
+
for (const channel of this._channels.values()) {
|
|
2891
|
+
if (channel.readyState !== "closed") channel._handleClose();
|
|
2892
|
+
}
|
|
2893
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
2894
|
+
this.dispatchEvent(makeEvent("iceconnectionstatechange"));
|
|
2895
|
+
this.dispatchEvent(makeEvent("connectionstatechange"));
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
_registerLocalDescriptionForPairing() {
|
|
2899
|
+
const key = descriptionPairingKey(this._localDescription);
|
|
2900
|
+
if (!key) return;
|
|
2901
|
+
localDescriptionOwners.set(key, new WeakRef(this));
|
|
2902
|
+
this._localDescriptionPairingKeys.add(key);
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
_unregisterLocalDescriptionsForPairing() {
|
|
2906
|
+
for (const key of this._localDescriptionPairingKeys) {
|
|
2907
|
+
const owner = localDescriptionOwners.get(key)?.deref();
|
|
2908
|
+
if (!owner || owner === this) localDescriptionOwners.delete(key);
|
|
2909
|
+
}
|
|
2910
|
+
this._localDescriptionPairingKeys.clear();
|
|
2911
|
+
if (this._pairedPeer?._pairedPeer === this) this._pairedPeer._pairedPeer = null;
|
|
2912
|
+
this._pairedPeer = null;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
_pairWithRemoteDescription(description) {
|
|
2916
|
+
const key = descriptionPairingKey(description);
|
|
2917
|
+
if (!key) return;
|
|
2918
|
+
const peerRef = localDescriptionOwners.get(key);
|
|
2919
|
+
const peer = peerRef?.deref();
|
|
2920
|
+
const ownCreatedOffer =
|
|
2921
|
+
description?.type === "offer" && this._lastCreatedOffer?.sdp === description.sdp;
|
|
2922
|
+
if (!peer) {
|
|
2923
|
+
if (peerRef) localDescriptionOwners.delete(key);
|
|
2924
|
+
this._selfRemoteDescription = ownCreatedOffer;
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
if (peer === this) {
|
|
2928
|
+
this._selfRemoteDescription = true;
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
if (peer._closed) return;
|
|
2932
|
+
this._selfRemoteDescription = false;
|
|
2933
|
+
this._pairedPeer = peer;
|
|
2934
|
+
peer._pairedPeer = this;
|
|
2935
|
+
if (this._explicitIceCandidateExchange || peer._explicitIceCandidateExchange) {
|
|
2936
|
+
this._explicitIceCandidateExchange = true;
|
|
2937
|
+
peer._explicitIceCandidateExchange = true;
|
|
2938
|
+
}
|
|
2939
|
+
if (this._sameProcessIceCandidateExchange || peer._sameProcessIceCandidateExchange) {
|
|
2940
|
+
this._sameProcessIceCandidateExchange = true;
|
|
2941
|
+
peer._sameProcessIceCandidateExchange = true;
|
|
2942
|
+
}
|
|
2943
|
+
this._pairExistingDataChannels();
|
|
2944
|
+
peer._pairExistingDataChannels();
|
|
2945
|
+
this._scheduleDataChannelAnnouncementRepair();
|
|
2946
|
+
peer._scheduleDataChannelAnnouncementRepair();
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
_markExplicitIceCandidateExchange() {
|
|
2950
|
+
this._explicitIceCandidateExchange = true;
|
|
2951
|
+
this._updateSctpTransport();
|
|
2952
|
+
if (this._pairedPeer && !this._pairedPeer._explicitIceCandidateExchange) {
|
|
2953
|
+
this._pairedPeer._explicitIceCandidateExchange = true;
|
|
2954
|
+
this._pairedPeer._scheduleSctpTransportUpdate();
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
_rememberLocalIceCandidate(candidate) {
|
|
2959
|
+
if (!candidate?.candidate) return;
|
|
2960
|
+
if (!this._localIceCandidates.some((entry) => sameCandidate(entry, candidate))) {
|
|
2961
|
+
this._localIceCandidates.push(candidate);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
_rememberRemoteIceCandidate(candidate) {
|
|
2966
|
+
if (!candidate?.candidate) return;
|
|
2967
|
+
if (!this._remoteIceCandidates.some((entry) => sameCandidate(entry, candidate))) {
|
|
2968
|
+
this._remoteIceCandidates.push(candidate);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
_refreshIceRole() {
|
|
2973
|
+
if (this._localDescription?.type === "offer" && this._remoteDescription?.type === "answer") {
|
|
2974
|
+
this._iceRole = "controlling";
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
if (this._remoteDescription?.type === "offer" && this._localDescription?.type === "answer") {
|
|
2978
|
+
this._iceRole = "controlled";
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
this._iceRole = "unknown";
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
_iceTransport() {
|
|
2985
|
+
return this._sctpTransport?.transport?.iceTransport || null;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
_pairExistingDataChannels() {
|
|
2989
|
+
for (const channel of this._channels.values()) {
|
|
2990
|
+
const id = channel.id;
|
|
2991
|
+
if (id != null) this._pairDataChannelById(channel, id);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
_pairDataChannelById(channel, id) {
|
|
2996
|
+
if (!this._pairedPeer || id == null || channel.readyState === "closed") return;
|
|
2997
|
+
for (const peerChannel of this._pairedPeer._channels.values()) {
|
|
2998
|
+
if (peerChannel.readyState === "closed") continue;
|
|
2999
|
+
if (peerChannel.id === id) {
|
|
3000
|
+
channel._pairedChannel = peerChannel;
|
|
3001
|
+
peerChannel._pairedChannel = channel;
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
_handleRemotePeerClosed() {
|
|
3008
|
+
if (this._closed) return;
|
|
3009
|
+
this._connectionState = "disconnected";
|
|
3010
|
+
this._iceConnectionState = "disconnected";
|
|
3011
|
+
this._updateSctpTransport();
|
|
3012
|
+
this._iceTransport()?._forceState("disconnected");
|
|
3013
|
+
this.dispatchEvent(makeEvent("iceconnectionstatechange"));
|
|
3014
|
+
this.dispatchEvent(makeEvent("connectionstatechange"));
|
|
3015
|
+
this._closeChannelsOnPeerFailure({ includeDisconnected: true });
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
_updateSctpTransport() {
|
|
3019
|
+
const hasData =
|
|
3020
|
+
hasDataMediaSection(this._localDescription) || hasDataMediaSection(this._remoteDescription);
|
|
3021
|
+
if (!hasData) {
|
|
3022
|
+
this._sctpConnectedTransitionReady = false;
|
|
3023
|
+
this._sctpTransport = null;
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
if (!this._sctpTransport) this._sctpTransport = new RTCSctpTransport(kInternalConstruct, this);
|
|
3028
|
+
const closed =
|
|
3029
|
+
this._closed ||
|
|
3030
|
+
this._connectionState === "closed" ||
|
|
3031
|
+
this._iceConnectionState === "closed" ||
|
|
3032
|
+
this._connectionState === "failed" ||
|
|
3033
|
+
this._iceConnectionState === "failed" ||
|
|
3034
|
+
this._connectionState === "disconnected" ||
|
|
3035
|
+
this._iceConnectionState === "disconnected";
|
|
3036
|
+
const dataTransportNegotiated =
|
|
3037
|
+
hasDataMediaSection(this._localDescription) && hasDataMediaSection(this._remoteDescription);
|
|
3038
|
+
const pairedDataTransportNegotiated =
|
|
3039
|
+
this._pairedPeer &&
|
|
3040
|
+
hasDataMediaSection(this._pairedPeer._localDescription) &&
|
|
3041
|
+
hasDataMediaSection(this._pairedPeer._remoteDescription);
|
|
3042
|
+
const nativeTransportConnected =
|
|
3043
|
+
this._connectionState === "connected" ||
|
|
3044
|
+
this._iceConnectionState === "connected" ||
|
|
3045
|
+
this._iceConnectionState === "completed";
|
|
3046
|
+
const pairedNativeTransportConnected =
|
|
3047
|
+
this._pairedPeer &&
|
|
3048
|
+
(this._pairedPeer._connectionState === "connected" ||
|
|
3049
|
+
this._pairedPeer._iceConnectionState === "connected" ||
|
|
3050
|
+
this._pairedPeer._iceConnectionState === "completed");
|
|
3051
|
+
const nativeConnected =
|
|
3052
|
+
nativeTransportConnected && dataTransportNegotiated && !this._selfRemoteDescription;
|
|
3053
|
+
const sameProcessConnected =
|
|
3054
|
+
this._sameProcessIceCandidateExchange &&
|
|
3055
|
+
dataTransportNegotiated &&
|
|
3056
|
+
pairedDataTransportNegotiated &&
|
|
3057
|
+
(nativeTransportConnected || pairedNativeTransportConnected);
|
|
3058
|
+
const operationsSettled =
|
|
3059
|
+
(this._operationsPending === 0 &&
|
|
3060
|
+
(!this._pairedPeer || this._pairedPeer._operationsPending === 0)) ||
|
|
3061
|
+
this._sctpTransport.state === "connected";
|
|
3062
|
+
const shouldConnect = (nativeConnected || sameProcessConnected) && operationsSettled;
|
|
3063
|
+
let connected = shouldConnect;
|
|
3064
|
+
if (closed || !shouldConnect) this._sctpConnectedTransitionReady = false;
|
|
3065
|
+
if (
|
|
3066
|
+
shouldConnect &&
|
|
3067
|
+
this._sctpTransport.state !== "connected" &&
|
|
3068
|
+
!this._sctpConnectedTransitionReady
|
|
3069
|
+
) {
|
|
3070
|
+
this._scheduleSctpConnectedTransition();
|
|
3071
|
+
connected = false;
|
|
3072
|
+
}
|
|
3073
|
+
this._sctpTransport._setLimits({
|
|
3074
|
+
maxMessageSize: this._currentSctpMaxMessageSize(),
|
|
3075
|
+
maxChannels: connected ? this._currentSctpMaxChannels() : null,
|
|
3076
|
+
});
|
|
3077
|
+
this._sctpTransport._setState(closed ? "closed" : connected ? "connected" : "connecting");
|
|
3078
|
+
if (connected || closed) this._sctpConnectPollDeadline = 0;
|
|
3079
|
+
if (connected) {
|
|
3080
|
+
this._scheduleConnectedStateRepair();
|
|
3081
|
+
this._scheduleDataChannelOpenRepair();
|
|
3082
|
+
this._scheduleDataChannelAnnouncementRepair();
|
|
3083
|
+
this._pairedPeer?._scheduleDataChannelAnnouncementRepair();
|
|
3084
|
+
}
|
|
3085
|
+
this._scheduleSctpConnectPollIfNeeded();
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
_scheduleSctpConnectedTransition() {
|
|
3089
|
+
if (this._sctpConnectedTransitionScheduled) return;
|
|
3090
|
+
this._sctpConnectedTransitionScheduled = true;
|
|
3091
|
+
setTimeout(() => {
|
|
3092
|
+
this._sctpConnectedTransitionScheduled = false;
|
|
3093
|
+
if (this._closed || !this._sctpTransport) return;
|
|
3094
|
+
this._sctpConnectedTransitionReady = true;
|
|
3095
|
+
this._updateSctpTransport();
|
|
3096
|
+
}, 0);
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
_shouldRepairConnectedState() {
|
|
3100
|
+
return Boolean(
|
|
3101
|
+
!this._closed &&
|
|
3102
|
+
this._sctpTransport?.state === "connected" &&
|
|
3103
|
+
this._sameProcessIceCandidateExchange &&
|
|
3104
|
+
this._hasNegotiatedDataTransport() &&
|
|
3105
|
+
this._pairedPeer?._hasNegotiatedDataTransport() &&
|
|
3106
|
+
this._operationsPending === 0 &&
|
|
3107
|
+
this._pairedPeer._operationsPending === 0 &&
|
|
3108
|
+
this._connectionState !== "failed" &&
|
|
3109
|
+
this._connectionState !== "disconnected" &&
|
|
3110
|
+
this._iceConnectionState !== "failed" &&
|
|
3111
|
+
this._iceConnectionState !== "disconnected" &&
|
|
3112
|
+
(this._connectionState !== "connected" ||
|
|
3113
|
+
!["connected", "completed"].includes(this._iceConnectionState)),
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
_scheduleConnectedStateRepair() {
|
|
3118
|
+
if (this._connectedStateRepairScheduled || !this._shouldRepairConnectedState()) return;
|
|
3119
|
+
this._connectedStateRepairScheduled = true;
|
|
3120
|
+
setTimeout(() => {
|
|
3121
|
+
this._connectedStateRepairScheduled = false;
|
|
3122
|
+
if (!this._shouldRepairConnectedState()) return;
|
|
3123
|
+
|
|
3124
|
+
if (this._iceConnectionState === "new") {
|
|
3125
|
+
this._iceConnectionState = "checking";
|
|
3126
|
+
this.dispatchEvent(makeEvent("iceconnectionstatechange"));
|
|
3127
|
+
this._iceTransport()?._handlePeerIceConnectionState("checking");
|
|
3128
|
+
}
|
|
3129
|
+
if (this._connectionState === "new") {
|
|
3130
|
+
this._connectionState = "connecting";
|
|
3131
|
+
this.dispatchEvent(makeEvent("connectionstatechange"));
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
setTimeout(() => {
|
|
3135
|
+
if (!this._shouldRepairConnectedState()) return;
|
|
3136
|
+
if (!["connected", "completed"].includes(this._iceConnectionState)) {
|
|
3137
|
+
this._iceConnectionState = "connected";
|
|
3138
|
+
this.dispatchEvent(makeEvent("iceconnectionstatechange"));
|
|
3139
|
+
this._iceTransport()?._handlePeerIceConnectionState("connected");
|
|
3140
|
+
}
|
|
3141
|
+
if (this._connectionState !== "connected") {
|
|
3142
|
+
this._connectionState = "connected";
|
|
3143
|
+
this._updateSctpTransport();
|
|
3144
|
+
this.dispatchEvent(makeEvent("connectionstatechange"));
|
|
3145
|
+
}
|
|
3146
|
+
}, 0);
|
|
3147
|
+
}, 0);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
_hasNegotiatedDataTransport() {
|
|
3151
|
+
return (
|
|
3152
|
+
hasDataMediaSection(this._localDescription) && hasDataMediaSection(this._remoteDescription)
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
_hasExplicitlyNegotiatedDataTransport() {
|
|
3157
|
+
return this._localDescriptionSetByApi && this._hasNegotiatedDataTransport();
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
_shouldDeferRemoteCandidateUntilLocalAnswer() {
|
|
3161
|
+
return (
|
|
3162
|
+
this._remoteDescription?.type === "offer" &&
|
|
3163
|
+
(this._signalingState === "have-remote-offer" ||
|
|
3164
|
+
this._pairedPeer?.signalingState === "have-local-offer" ||
|
|
3165
|
+
this._operationsPending > 0 ||
|
|
3166
|
+
this._pairedPeer?._operationsPending > 0)
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
_queuePendingRemoteCandidatesForNative(candidates) {
|
|
3171
|
+
for (const candidate of candidates) {
|
|
3172
|
+
if (!candidate?.candidate) continue;
|
|
3173
|
+
if (
|
|
3174
|
+
!this._pendingRemoteCandidatesForNative.some((entry) => sameCandidate(entry, candidate))
|
|
3175
|
+
) {
|
|
3176
|
+
this._pendingRemoteCandidatesForNative.push(candidate);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
_flushPendingRemoteCandidatesForNative() {
|
|
3182
|
+
if (!this._remoteDescription || this._shouldDeferRemoteCandidateUntilLocalAnswer()) return;
|
|
3183
|
+
const candidates = this._pendingRemoteCandidatesForNative.splice(0);
|
|
3184
|
+
for (const candidate of candidates) {
|
|
3185
|
+
try {
|
|
3186
|
+
const nativeCandidate = candidate.toJSON();
|
|
3187
|
+
if (nativeCandidate.sdpMid === null) {
|
|
3188
|
+
nativeCandidate.sdpMid = resolveCandidateMid(this._remoteDescription, candidate);
|
|
3189
|
+
}
|
|
3190
|
+
this._ensureNativePeerConnection().addRemoteCandidate(nativeCandidate);
|
|
3191
|
+
} catch {
|
|
3192
|
+
// Same-process and inline SDP candidate races are best-effort.
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
_finishPendingOperation() {
|
|
3198
|
+
this._operationsPending = Math.max(0, this._operationsPending - 1);
|
|
3199
|
+
if (this._operationsPending === 0) {
|
|
3200
|
+
this._scheduleSctpTransportUpdate();
|
|
3201
|
+
if (this._pairedPeer && !this._pairedPeer._closed) {
|
|
3202
|
+
this._pairedPeer._scheduleSctpTransportUpdate();
|
|
3203
|
+
}
|
|
3204
|
+
this._flushPendingRemoteCandidatesForNative();
|
|
3205
|
+
this._pairedPeer?._flushPendingRemoteCandidatesForNative();
|
|
3206
|
+
this._scheduleDeferredIceEventFlush();
|
|
3207
|
+
const callbacks = this._operationIdleCallbacks.splice(0);
|
|
3208
|
+
for (const callback of callbacks) setTimeout(callback, 0);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
_afterOperationsIdle(callback) {
|
|
3213
|
+
if (this._closed || this._operationsPending === 0) {
|
|
3214
|
+
setTimeout(callback, 0);
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
this._operationIdleCallbacks.push(callback);
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
_shouldDeferIceEvent(event) {
|
|
3221
|
+
if (this._processingDeferredIceEvent || this._operationsPending === 0) return false;
|
|
3222
|
+
return event.type === "localcandidate" || event.type === "icegatheringstatechange";
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
_scheduleDeferredIceEventFlush() {
|
|
3226
|
+
if (this._iceEventFlushScheduled || this._deferredIceEvents.length === 0) return;
|
|
3227
|
+
this._iceEventFlushScheduled = true;
|
|
3228
|
+
setTimeout(() => {
|
|
3229
|
+
this._iceEventFlushScheduled = false;
|
|
3230
|
+
if (this._closed) {
|
|
3231
|
+
this._deferredIceEvents = [];
|
|
3232
|
+
this._flushIceEventIdleCallbacks();
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
if (this._operationsPending > 0) {
|
|
3236
|
+
this._scheduleDeferredIceEventFlush();
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
const event = this._deferredIceEvents.shift();
|
|
3240
|
+
this._processingDeferredIceEvent = true;
|
|
3241
|
+
try {
|
|
3242
|
+
if (event) this._handleNativeEvent(event);
|
|
3243
|
+
} finally {
|
|
3244
|
+
this._processingDeferredIceEvent = false;
|
|
3245
|
+
}
|
|
3246
|
+
if (this._deferredIceEvents.length) this._scheduleDeferredIceEventFlush();
|
|
3247
|
+
else this._flushIceEventIdleCallbacks();
|
|
3248
|
+
}, 0);
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
_hasPendingDeferredIceEvents() {
|
|
3252
|
+
return (
|
|
3253
|
+
this._deferredIceEvents.length > 0 ||
|
|
3254
|
+
this._iceEventFlushScheduled ||
|
|
3255
|
+
this._processingDeferredIceEvent
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
_afterDeferredIceEventsFlushed(callback) {
|
|
3260
|
+
if (this._closed || !this._hasPendingDeferredIceEvents()) {
|
|
3261
|
+
setTimeout(callback, 0);
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
this._iceEventIdleCallbacks.push(callback);
|
|
3265
|
+
this._scheduleDeferredIceEventFlush();
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
_flushIceEventIdleCallbacks() {
|
|
3269
|
+
if (this._hasPendingDeferredIceEvents()) return;
|
|
3270
|
+
const callbacks = this._iceEventIdleCallbacks.splice(0);
|
|
3271
|
+
for (const callback of callbacks) setTimeout(callback, 0);
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
_queueSyntheticIceRestartGathering() {
|
|
3275
|
+
const hasGatheringStateEvent = this._deferredIceEvents.some(
|
|
3276
|
+
(event) => event.type === "icegatheringstatechange",
|
|
3277
|
+
);
|
|
3278
|
+
if (hasGatheringStateEvent) return;
|
|
3279
|
+
this._deferredIceEvents.push(
|
|
3280
|
+
{ type: "icegatheringstatechange", state: "gathering" },
|
|
3281
|
+
{ type: "icegatheringstatechange", state: "complete" },
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
_scheduleSctpTransportUpdate() {
|
|
3286
|
+
if (this._sctpTransportUpdateScheduled) return;
|
|
3287
|
+
this._sctpTransportUpdateScheduled = true;
|
|
3288
|
+
setTimeout(() => {
|
|
3289
|
+
this._sctpTransportUpdateScheduled = false;
|
|
3290
|
+
if (
|
|
3291
|
+
!this._closed &&
|
|
3292
|
+
this._connectionState !== "closed" &&
|
|
3293
|
+
this._iceConnectionState !== "closed" &&
|
|
3294
|
+
this._native
|
|
3295
|
+
) {
|
|
3296
|
+
this._connectionState = this._native.connectionState;
|
|
3297
|
+
this._iceConnectionState = this._native.iceConnectionState;
|
|
3298
|
+
}
|
|
3299
|
+
this._updateSctpTransport();
|
|
3300
|
+
}, 0);
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
_scheduleSctpConnectPollIfNeeded() {
|
|
3304
|
+
if (
|
|
3305
|
+
this._closed ||
|
|
3306
|
+
this._operationsPending > 0 ||
|
|
3307
|
+
this._pairedPeer?._operationsPending > 0 ||
|
|
3308
|
+
this._sctpTransport?.state !== "connecting" ||
|
|
3309
|
+
!hasDataMediaSection(this._localDescription) ||
|
|
3310
|
+
!hasDataMediaSection(this._remoteDescription)
|
|
3311
|
+
) {
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
const now = Date.now();
|
|
3316
|
+
if (!this._sctpConnectPollDeadline) {
|
|
3317
|
+
this._sctpConnectPollDeadline = now + SCTP_CONNECT_POLL_TIMEOUT_MS;
|
|
3318
|
+
}
|
|
3319
|
+
if (this._sctpConnectPollScheduled || now >= this._sctpConnectPollDeadline) return;
|
|
3320
|
+
this._sctpConnectPollScheduled = true;
|
|
3321
|
+
setTimeout(() => {
|
|
3322
|
+
this._sctpConnectPollScheduled = false;
|
|
3323
|
+
if (this._closed || this._sctpTransport?.state !== "connecting") return;
|
|
3324
|
+
this._scheduleSctpTransportUpdate();
|
|
3325
|
+
}, SCTP_CONNECT_POLL_INTERVAL_MS);
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
_scheduleDataChannelOpenRepair() {
|
|
3329
|
+
if (this._closed || this._dataChannelOpenRepairScheduled) return;
|
|
3330
|
+
if (this._sctpTransport?.state !== "connected") return;
|
|
3331
|
+
const now = Date.now();
|
|
3332
|
+
if (!this._dataChannelOpenRepairDeadline) {
|
|
3333
|
+
this._dataChannelOpenRepairDeadline = now + DATA_CHANNEL_OPEN_REPAIR_TIMEOUT_MS;
|
|
3334
|
+
}
|
|
3335
|
+
if (now >= this._dataChannelOpenRepairDeadline) return;
|
|
3336
|
+
this._dataChannelOpenRepairScheduled = true;
|
|
3337
|
+
setTimeout(() => {
|
|
3338
|
+
this._dataChannelOpenRepairScheduled = false;
|
|
3339
|
+
if (this._closed || this._sctpTransport?.state !== "connected") return;
|
|
3340
|
+
let hasConnectingChannel = false;
|
|
3341
|
+
for (const channel of this._channels.values()) {
|
|
3342
|
+
if (channel.readyState === "connecting") {
|
|
3343
|
+
hasConnectingChannel = true;
|
|
3344
|
+
channel._repairMissedOpenEvent();
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
if (hasConnectingChannel) {
|
|
3348
|
+
this._scheduleDataChannelOpenRepair();
|
|
3349
|
+
} else {
|
|
3350
|
+
this._dataChannelOpenRepairDeadline = 0;
|
|
3351
|
+
}
|
|
3352
|
+
}, DATA_CHANNEL_OPEN_REPAIR_INTERVAL_MS);
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
_scheduleDataChannelAnnouncementRepair() {
|
|
3356
|
+
if (this._closed || this._dataChannelAnnouncementRepairScheduled) return;
|
|
3357
|
+
if (!this._hasDataChannelAnnouncementRepairCandidates()) {
|
|
3358
|
+
this._dataChannelAnnouncementRepairReadyAt = 0;
|
|
3359
|
+
this._dataChannelAnnouncementRepairDeadline = 0;
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
const now = Date.now();
|
|
3363
|
+
const canRepair = this._canRepairDataChannelAnnouncements();
|
|
3364
|
+
if (canRepair && !this._dataChannelAnnouncementRepairDeadline) {
|
|
3365
|
+
this._dataChannelAnnouncementRepairReadyAt = now + DATA_CHANNEL_ANNOUNCEMENT_REPAIR_GRACE_MS;
|
|
3366
|
+
this._dataChannelAnnouncementRepairDeadline =
|
|
3367
|
+
now + DATA_CHANNEL_ANNOUNCEMENT_REPAIR_TIMEOUT_MS;
|
|
3368
|
+
}
|
|
3369
|
+
if (canRepair && now >= this._dataChannelAnnouncementRepairDeadline) {
|
|
3370
|
+
this._dataChannelAnnouncementRepairReadyAt = 0;
|
|
3371
|
+
this._dataChannelAnnouncementRepairDeadline = 0;
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
this._dataChannelAnnouncementRepairScheduled = true;
|
|
3375
|
+
setTimeout(() => {
|
|
3376
|
+
this._dataChannelAnnouncementRepairScheduled = false;
|
|
3377
|
+
if (this._closed) return;
|
|
3378
|
+
const repairable = this._canRepairDataChannelAnnouncements();
|
|
3379
|
+
if (repairable && !this._dataChannelAnnouncementRepairDeadline) {
|
|
3380
|
+
const now = Date.now();
|
|
3381
|
+
this._dataChannelAnnouncementRepairReadyAt =
|
|
3382
|
+
now + DATA_CHANNEL_ANNOUNCEMENT_REPAIR_GRACE_MS;
|
|
3383
|
+
this._dataChannelAnnouncementRepairDeadline =
|
|
3384
|
+
now + DATA_CHANNEL_ANNOUNCEMENT_REPAIR_TIMEOUT_MS;
|
|
3385
|
+
}
|
|
3386
|
+
const canCreateSyntheticAnnouncement =
|
|
3387
|
+
this._pendingSyntheticDataChannelAnnouncements.size > 0 ||
|
|
3388
|
+
Date.now() >= this._dataChannelAnnouncementRepairReadyAt;
|
|
3389
|
+
const hasWork = repairable
|
|
3390
|
+
? canCreateSyntheticAnnouncement
|
|
3391
|
+
? this._repairMissingDataChannelAnnouncements()
|
|
3392
|
+
: this._hasDataChannelAnnouncementRepairCandidates()
|
|
3393
|
+
: this._hasDataChannelAnnouncementRepairCandidates();
|
|
3394
|
+
if (hasWork) {
|
|
3395
|
+
this._scheduleDataChannelAnnouncementRepair();
|
|
3396
|
+
} else {
|
|
3397
|
+
this._dataChannelAnnouncementRepairReadyAt = 0;
|
|
3398
|
+
this._dataChannelAnnouncementRepairDeadline = 0;
|
|
3399
|
+
}
|
|
3400
|
+
}, DATA_CHANNEL_ANNOUNCEMENT_REPAIR_INTERVAL_MS);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
_canRepairDataChannelAnnouncements() {
|
|
3404
|
+
return Boolean(
|
|
3405
|
+
this._pairedPeer &&
|
|
3406
|
+
!this._pairedPeer._closed &&
|
|
3407
|
+
this._operationsPending === 0 &&
|
|
3408
|
+
this._pairedPeer._operationsPending === 0 &&
|
|
3409
|
+
this._hasNegotiatedDataTransport() &&
|
|
3410
|
+
this._pairedPeer._hasNegotiatedDataTransport(),
|
|
3411
|
+
);
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
_hasDataChannelAnnouncementRepairCandidates() {
|
|
3415
|
+
if (!this._pairedPeer || this._pairedPeer._closed) return false;
|
|
3416
|
+
if (this._pendingSyntheticDataChannelAnnouncements.size > 0) return true;
|
|
3417
|
+
for (const sourceChannel of this._pairedPeer._channels.values()) {
|
|
3418
|
+
if (
|
|
3419
|
+
!sourceChannel._createdLocally ||
|
|
3420
|
+
sourceChannel.negotiated ||
|
|
3421
|
+
sourceChannel.readyState === "closed"
|
|
3422
|
+
) {
|
|
3423
|
+
continue;
|
|
3424
|
+
}
|
|
3425
|
+
const id = sourceChannel._effectiveId();
|
|
3426
|
+
if (id == null || !this._hasRemoteDataChannelForId(id)) return true;
|
|
3427
|
+
}
|
|
3428
|
+
return false;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
_repairMissingDataChannelAnnouncements() {
|
|
3432
|
+
if (!this._pairedPeer || this._pairedPeer._closed) return false;
|
|
3433
|
+
let hasPendingWork = false;
|
|
3434
|
+
|
|
3435
|
+
for (const [id, channel] of this._pendingSyntheticDataChannelAnnouncements) {
|
|
3436
|
+
if (channel.readyState === "closed") {
|
|
3437
|
+
this._pendingSyntheticDataChannelAnnouncements.delete(id);
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
if (!channel._native.isOpen) {
|
|
3441
|
+
hasPendingWork = true;
|
|
3442
|
+
continue;
|
|
3443
|
+
}
|
|
3444
|
+
channel._readyState = "open";
|
|
3445
|
+
channel._openEventPending = true;
|
|
3446
|
+
channel._announcementPending = true;
|
|
3447
|
+
const sourceChannel = channel._pairedChannel;
|
|
3448
|
+
if (sourceChannel?.readyState === "connecting") {
|
|
3449
|
+
sourceChannel._readyState = "open";
|
|
3450
|
+
sourceChannel._pc._registerDataChannelId(sourceChannel);
|
|
3451
|
+
sourceChannel._dispatchOpenEvent();
|
|
3452
|
+
}
|
|
3453
|
+
this._remoteAnnouncedDataChannelIds.add(id);
|
|
3454
|
+
this._pendingSyntheticDataChannelAnnouncements.delete(id);
|
|
3455
|
+
this._pendingDataChannelEvents.push(new RTCDataChannelEvent("datachannel", { channel }));
|
|
3456
|
+
this._scheduleDataChannelFlush();
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
for (const sourceChannel of this._pairedPeer._channels.values()) {
|
|
3460
|
+
if (
|
|
3461
|
+
!sourceChannel._createdLocally ||
|
|
3462
|
+
sourceChannel.negotiated ||
|
|
3463
|
+
sourceChannel.readyState === "closed"
|
|
3464
|
+
) {
|
|
3465
|
+
continue;
|
|
3466
|
+
}
|
|
3467
|
+
let id = sourceChannel.id;
|
|
3468
|
+
if (id == null) {
|
|
3469
|
+
this._assignSyntheticDataChannelIdIfNeeded(sourceChannel);
|
|
3470
|
+
id = sourceChannel.id;
|
|
3471
|
+
}
|
|
3472
|
+
if (id == null) {
|
|
3473
|
+
hasPendingWork = true;
|
|
3474
|
+
continue;
|
|
3475
|
+
}
|
|
3476
|
+
if (this._hasRemoteDataChannelForId(id)) continue;
|
|
3477
|
+
const channel = this._createSyntheticIncomingDataChannel(sourceChannel, id);
|
|
3478
|
+
if (channel) hasPendingWork = true;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
return hasPendingWork || this._pendingSyntheticDataChannelAnnouncements.size > 0;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
_assignSyntheticDataChannelIdIfNeeded(channel) {
|
|
3485
|
+
if (channel.negotiated || channel._effectiveId() != null || channel.readyState === "closed") {
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
const parity = channel._pc._dataChannelIdParityFromDtlsRole() ?? 1;
|
|
3489
|
+
const id = channel._pc._nextAvailableDataChannelId(parity);
|
|
3490
|
+
if (id != null) channel._assignId(id);
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
_hasRemoteDataChannelForId(id) {
|
|
3494
|
+
if (
|
|
3495
|
+
this._remoteAnnouncedDataChannelIds.has(id) ||
|
|
3496
|
+
this._pendingSyntheticDataChannelAnnouncements.has(id)
|
|
3497
|
+
) {
|
|
3498
|
+
return true;
|
|
3499
|
+
}
|
|
3500
|
+
for (const channel of this._channels.values()) {
|
|
3501
|
+
if (!channel._createdLocally && channel.readyState !== "closed" && channel.id === id)
|
|
3502
|
+
return true;
|
|
3503
|
+
}
|
|
3504
|
+
return false;
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
_syntheticIncomingDataChannelForId(id) {
|
|
3508
|
+
const pending = this._pendingSyntheticDataChannelAnnouncements.get(id);
|
|
3509
|
+
if (pending && pending.readyState !== "closed") return pending;
|
|
3510
|
+
for (const event of this._pendingDataChannelEvents) {
|
|
3511
|
+
const channel = event.channel;
|
|
3512
|
+
if (channel?._syntheticIncoming && channel.readyState !== "closed" && channel.id === id) {
|
|
3513
|
+
return channel;
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
for (const channel of this._channels.values()) {
|
|
3517
|
+
if (channel._syntheticIncoming && channel.readyState !== "closed" && channel.id === id) {
|
|
3518
|
+
return channel;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
return null;
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
_hasPendingDataChannelEventForId(id) {
|
|
3525
|
+
return this._pendingDataChannelEvents.some((event) => {
|
|
3526
|
+
const channel = event.channel;
|
|
3527
|
+
const channelId =
|
|
3528
|
+
typeof channel?._effectiveId === "function" ? channel._effectiveId() : channel?.id;
|
|
3529
|
+
return channelId === id;
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
_hasSettledDataChannelAnnouncement(id) {
|
|
3534
|
+
if (id == null) return false;
|
|
3535
|
+
if (
|
|
3536
|
+
this._pendingSyntheticDataChannelAnnouncements.has(id) ||
|
|
3537
|
+
this._hasPendingDataChannelEventForId(id)
|
|
3538
|
+
) {
|
|
3539
|
+
return false;
|
|
3540
|
+
}
|
|
3541
|
+
if (this._remoteAnnouncedDataChannelIds.has(id)) return true;
|
|
3542
|
+
for (const channel of this._channels.values()) {
|
|
3543
|
+
if (!channel._createdLocally && channel.readyState !== "closed" && channel.id === id) {
|
|
3544
|
+
return true;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
return false;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
_afterDataChannelAnnouncementSettled(id, callback) {
|
|
3551
|
+
const deadline = Date.now() + DATA_CHANNEL_ANNOUNCEMENT_REPAIR_TIMEOUT_MS;
|
|
3552
|
+
const poll = () => {
|
|
3553
|
+
const settled = this._hasSettledDataChannelAnnouncement(id);
|
|
3554
|
+
if (
|
|
3555
|
+
settled ||
|
|
3556
|
+
this._closed ||
|
|
3557
|
+
!this._hasEventConsumer("datachannel") ||
|
|
3558
|
+
Date.now() >= deadline
|
|
3559
|
+
) {
|
|
3560
|
+
setTimeout(() => callback(settled), 0);
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
this._scheduleDataChannelAnnouncementRepair();
|
|
3564
|
+
this._scheduleDataChannelFlush();
|
|
3565
|
+
setTimeout(poll, DATA_CHANNEL_ANNOUNCEMENT_REPAIR_INTERVAL_MS);
|
|
3566
|
+
};
|
|
3567
|
+
setTimeout(poll, 0);
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
_createSyntheticIncomingDataChannel(sourceChannel, id) {
|
|
3571
|
+
const options = {
|
|
3572
|
+
ordered: sourceChannel.ordered,
|
|
3573
|
+
maxPacketLifeTime: sourceChannel.maxPacketLifeTime,
|
|
3574
|
+
maxRetransmits: sourceChannel.maxRetransmits,
|
|
3575
|
+
protocol: sourceChannel.protocol,
|
|
3576
|
+
negotiated: true,
|
|
3577
|
+
id,
|
|
3578
|
+
};
|
|
3579
|
+
const nativeChannel = new SyntheticNativeDataChannel(sourceChannel.label, options);
|
|
3580
|
+
const channel = RTCDataChannel._fromNative(this, nativeChannel, undefined, id);
|
|
3581
|
+
channel._createdLocally = false;
|
|
3582
|
+
channel._negotiatedOverride = false;
|
|
3583
|
+
channel._syntheticIncoming = true;
|
|
3584
|
+
channel._pairedChannel = sourceChannel;
|
|
3585
|
+
sourceChannel._pairedChannel = channel;
|
|
3586
|
+
this._pendingSyntheticDataChannelAnnouncements.set(id, channel);
|
|
3587
|
+
return channel;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
_currentSctpMaxMessageSize() {
|
|
3591
|
+
if (!this._localDescription || !this._remoteDescription) return null;
|
|
3592
|
+
if (
|
|
3593
|
+
!hasDataMediaSection(this._localDescription) ||
|
|
3594
|
+
!hasDataMediaSection(this._remoteDescription)
|
|
3595
|
+
)
|
|
3596
|
+
return null;
|
|
3597
|
+
if (this._selfRemoteDescription && !this._explicitIceCandidateExchange) return null;
|
|
3598
|
+
const localLimit =
|
|
3599
|
+
maxMessageSizeFromSdp(this._localDescription) || DEFAULT_SCTP_MAX_MESSAGE_SIZE;
|
|
3600
|
+
const remoteLimit = maxMessageSizeFromSdp(this._remoteDescription);
|
|
3601
|
+
if (remoteLimit === null) return Math.min(65536, localLimit);
|
|
3602
|
+
if (remoteLimit === 0) return localLimit || Number.POSITIVE_INFINITY;
|
|
3603
|
+
return localLimit === 0 ? remoteLimit : Math.min(remoteLimit, localLimit);
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
_currentSctpMaxChannels() {
|
|
3607
|
+
const connected =
|
|
3608
|
+
this._connectionState === "connected" || this._sctpTransport?.state === "connected";
|
|
3609
|
+
if (!connected || this._selfRemoteDescription) return null;
|
|
3610
|
+
if (!this._native) return null;
|
|
3611
|
+
const maxId = Number(this._native.maxDataChannelId);
|
|
3612
|
+
return Number.isFinite(maxId) && maxId >= 0 ? maxId + 1 : LIBDATACHANNEL_SCTP_MAX_CHANNELS;
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
_implicitLocalDescriptionType() {
|
|
3616
|
+
return this._signalingState === "have-remote-offer" ? "answer" : "offer";
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
_setPendingLocalDescription(description) {
|
|
3620
|
+
this._pendingLocalDescription = description;
|
|
3621
|
+
this._localDescription = description || this._currentLocalDescription;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
_commitLocalDescription(description = this._pendingLocalDescription || this._localDescription) {
|
|
3625
|
+
if (description) this._currentLocalDescription = description;
|
|
3626
|
+
this._pendingLocalDescription = null;
|
|
3627
|
+
this._localDescription = this._currentLocalDescription;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
_rollbackLocalDescription() {
|
|
3631
|
+
this._pendingLocalDescription = null;
|
|
3632
|
+
this._localDescription = this._currentLocalDescription;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
_setPendingRemoteDescription(description) {
|
|
3636
|
+
this._pendingRemoteDescription = description;
|
|
3637
|
+
this._remoteDescription = description || this._currentRemoteDescription;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
_commitRemoteDescription(
|
|
3641
|
+
description = this._pendingRemoteDescription || this._remoteDescription,
|
|
3642
|
+
) {
|
|
3643
|
+
if (description) this._currentRemoteDescription = description;
|
|
3644
|
+
this._pendingRemoteDescription = null;
|
|
3645
|
+
this._remoteDescription = this._currentRemoteDescription;
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
_rollbackRemoteDescription() {
|
|
3649
|
+
this._pendingRemoteDescription = null;
|
|
3650
|
+
this._remoteDescription = this._currentRemoteDescription;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
async _implicitRollbackLocalDescription() {
|
|
3654
|
+
const previousState = this._signalingState;
|
|
3655
|
+
const previousGatheringState = this._iceGatheringState;
|
|
3656
|
+
const rollingBackInitialOffer = !this._currentLocalDescription;
|
|
3657
|
+
const nativeBackedRollback = this._localDescription && !isNoMediaSdp(this._localDescription);
|
|
3658
|
+
if (nativeBackedRollback) {
|
|
3659
|
+
this._suppressNextNativeSignalingState = "stable";
|
|
3660
|
+
try {
|
|
3661
|
+
this._ensureNativePeerConnection().setLocalDescription("rollback");
|
|
3662
|
+
} catch (error) {
|
|
3663
|
+
this._suppressNextNativeSignalingState = null;
|
|
3664
|
+
throw error;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
this._rollbackLocalDescription();
|
|
3668
|
+
this._localIceCandidates = [];
|
|
3669
|
+
this._unregisterLocalDescriptionsForPairing();
|
|
3670
|
+
this._syncStatesFromNative();
|
|
3671
|
+
this._signalingState = "stable";
|
|
3672
|
+
if (rollingBackInitialOffer) this._iceGatheringState = "new";
|
|
3673
|
+
this._refreshIceRole();
|
|
3674
|
+
this._updateSctpTransport();
|
|
3675
|
+
if (rollingBackInitialOffer && previousGatheringState !== "new") {
|
|
3676
|
+
this.dispatchEvent(makeEvent("icegatheringstatechange"));
|
|
3677
|
+
this._iceTransport()?.dispatchEvent(makeEvent("gatheringstatechange"));
|
|
3678
|
+
}
|
|
3679
|
+
if (previousState !== this._signalingState)
|
|
3680
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
3681
|
+
await nextTask();
|
|
3682
|
+
if (this._suppressNextNativeSignalingState === "stable")
|
|
3683
|
+
this._suppressNextNativeSignalingState = null;
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
_refreshCurrentOrPendingLocalDescription(description) {
|
|
3687
|
+
if (!description) return;
|
|
3688
|
+
this._localDescription = description;
|
|
3689
|
+
if (this._pendingLocalDescription?.type === description.type) {
|
|
3690
|
+
this._pendingLocalDescription = description;
|
|
3691
|
+
} else {
|
|
3692
|
+
this._currentLocalDescription = description;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
_refreshCurrentOrPendingRemoteDescription(description) {
|
|
3697
|
+
if (!description) return;
|
|
3698
|
+
this._remoteDescription = description;
|
|
3699
|
+
if (this._pendingRemoteDescription?.type === description.type) {
|
|
3700
|
+
this._pendingRemoteDescription = description;
|
|
3701
|
+
} else {
|
|
3702
|
+
this._currentRemoteDescription = description;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
_ensureIceRestartCredentials() {
|
|
3707
|
+
if (!this._pendingIceRestartCredentials) {
|
|
3708
|
+
this._pendingIceRestartCredentials = createIceRestartCredentials();
|
|
3709
|
+
}
|
|
3710
|
+
return this._pendingIceRestartCredentials;
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
_localOfferInit(type) {
|
|
3714
|
+
if (type !== "offer" || !this._iceRestartPending || !this._canApplyNativeIceCredentials()) {
|
|
3715
|
+
return undefined;
|
|
3716
|
+
}
|
|
3717
|
+
return this._ensureIceRestartCredentials();
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
_canApplyNativeIceCredentials() {
|
|
3721
|
+
return (
|
|
3722
|
+
this._iceGatheringState === "new" &&
|
|
3723
|
+
!this._localDescription &&
|
|
3724
|
+
!this._currentLocalDescription &&
|
|
3725
|
+
!this._pendingLocalDescription
|
|
3726
|
+
);
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
_isJsOnlyIceRestartRemoteOffer(description, explicitMarker) {
|
|
3730
|
+
if (explicitMarker) return true;
|
|
3731
|
+
if (description.type !== "offer" || !this._currentRemoteDescription) return false;
|
|
3732
|
+
if (!hasDataMediaSection(description) || !hasDataMediaSection(this._currentRemoteDescription)) {
|
|
3733
|
+
return false;
|
|
3734
|
+
}
|
|
3735
|
+
return hasDifferentIceCredentials(description, this._currentRemoteDescription);
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
_clearIceRestartRequest() {
|
|
3739
|
+
this._iceRestartPending = false;
|
|
3740
|
+
this._pendingIceRestartCredentials = null;
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
_assertNotClosed() {
|
|
3744
|
+
if (this._closed) throw makeDOMException("RTCPeerConnection is closed", "InvalidStateError");
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
async _ensureLocalAnswerApplied() {
|
|
3748
|
+
if (this._localDescription?.type === "answer") return this._localDescription;
|
|
3749
|
+
if (this._jsOnlyIceRestartRemoteOffer) {
|
|
3750
|
+
const answer =
|
|
3751
|
+
this._lastCreatedAnswer ||
|
|
3752
|
+
markJsOnlyIceRestart(
|
|
3753
|
+
new RTCSessionDescription(this._ensureNativePeerConnection().createAnswer()),
|
|
3754
|
+
);
|
|
3755
|
+
await this._applyJsOnlyLocalAnswer(answer);
|
|
3756
|
+
return this._localDescription;
|
|
3757
|
+
}
|
|
3758
|
+
if (isNoMediaSdp(this.remoteDescription)) {
|
|
3759
|
+
const answer =
|
|
3760
|
+
this._lastCreatedAnswer ||
|
|
3761
|
+
new RTCSessionDescription({
|
|
3762
|
+
type: "answer",
|
|
3763
|
+
sdp: this.remoteDescription.sdp,
|
|
3764
|
+
});
|
|
3765
|
+
await this._applyNoMediaLocalDescription(answer);
|
|
3766
|
+
return this._localDescription;
|
|
3767
|
+
}
|
|
3768
|
+
this._ensureNativePeerConnection().setLocalDescription("answer");
|
|
3769
|
+
const nativeDescription = this._ensureNativePeerConnection().localDescription();
|
|
3770
|
+
this._localDescription = nativeDescription
|
|
3771
|
+
? new RTCSessionDescription(nativeDescription)
|
|
3772
|
+
: null;
|
|
3773
|
+
this._syncStatesFromNative();
|
|
3774
|
+
for (const candidate of this._pendingIce.splice(0)) await this.addIceCandidate(candidate);
|
|
3775
|
+
this._flushPendingRemoteCandidatesForNative();
|
|
3776
|
+
await nextTask();
|
|
3777
|
+
await this._refreshLocalDescriptionAfterGatheringWindow();
|
|
3778
|
+
this._registerLocalDescriptionForPairing();
|
|
3779
|
+
return this._localDescription;
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
async _applyJsOnlyLocalAnswer(description) {
|
|
3783
|
+
const previousState = this._signalingState;
|
|
3784
|
+
const localDescription = markJsOnlyIceRestart(new RTCSessionDescription(description));
|
|
3785
|
+
this._commitRemoteDescription();
|
|
3786
|
+
this._commitLocalDescription(localDescription);
|
|
3787
|
+
this._registerLocalDescriptionForPairing();
|
|
3788
|
+
this._jsOnlyIceRestartRemoteOffer = false;
|
|
3789
|
+
this._clearNegotiationNeededIfDataMLineIsPresent();
|
|
3790
|
+
this._signalingState = "stable";
|
|
3791
|
+
this._refreshIceRole();
|
|
3792
|
+
this._updateSctpTransport();
|
|
3793
|
+
if (previousState !== this._signalingState)
|
|
3794
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
3795
|
+
await nextTask();
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
async _applyNoMediaLocalDescription(description) {
|
|
3799
|
+
const localDescription = new RTCSessionDescription(description);
|
|
3800
|
+
if (localDescription.type === "offer") {
|
|
3801
|
+
this._setPendingLocalDescription(localDescription);
|
|
3802
|
+
} else if (localDescription.type === "answer") {
|
|
3803
|
+
this._commitRemoteDescription();
|
|
3804
|
+
this._commitLocalDescription(localDescription);
|
|
3805
|
+
} else {
|
|
3806
|
+
this._setPendingLocalDescription(localDescription);
|
|
3807
|
+
}
|
|
3808
|
+
this._registerLocalDescriptionForPairing();
|
|
3809
|
+
this._signalingState = description.type === "offer" ? "have-local-offer" : "stable";
|
|
3810
|
+
this._clearNegotiationNeededIfDataMLineIsPresent();
|
|
3811
|
+
this._refreshIceRole();
|
|
3812
|
+
this._updateSctpTransport();
|
|
3813
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
3814
|
+
await nextTask();
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
async _refreshLocalDescriptionAfterGatheringWindow() {
|
|
3818
|
+
if (this._closed || !this._localDescription) return;
|
|
3819
|
+
if (this._iceGatheringState !== "complete") {
|
|
3820
|
+
await delay(50);
|
|
3821
|
+
}
|
|
3822
|
+
if (!this._native) return;
|
|
3823
|
+
const nativeDescription = this._native.localDescription();
|
|
3824
|
+
if (nativeDescription) {
|
|
3825
|
+
this._refreshCurrentOrPendingLocalDescription(new RTCSessionDescription(nativeDescription));
|
|
3826
|
+
this._registerLocalDescriptionForPairing();
|
|
3827
|
+
}
|
|
3828
|
+
this._syncStatesFromNative();
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
_syncStatesFromNative() {
|
|
3832
|
+
if (this._closed) return;
|
|
3833
|
+
if (!this._native) return;
|
|
3834
|
+
this._connectionState = this._native.connectionState;
|
|
3835
|
+
this._iceConnectionState = this._native.iceConnectionState;
|
|
3836
|
+
this._signalingState = this._native.signalingState;
|
|
3837
|
+
this._refreshIceRole();
|
|
3838
|
+
this._updateSctpTransport();
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
_handleNativeEvent(event) {
|
|
3842
|
+
if (this._closed && event.type !== "datachannel") return;
|
|
3843
|
+
if (this._shouldDeferIceEvent(event)) {
|
|
3844
|
+
this._deferredIceEvents.push(event);
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
if (event.target === "datachannel") {
|
|
3849
|
+
const channel = this._channels.get(event.channelId);
|
|
3850
|
+
if (channel) {
|
|
3851
|
+
channel._handleNativeEvent(event);
|
|
3852
|
+
} else if (event.channelId) {
|
|
3853
|
+
const events = this._pendingNativeDataChannelEvents.get(event.channelId) || [];
|
|
3854
|
+
events.push(event);
|
|
3855
|
+
this._pendingNativeDataChannelEvents.set(event.channelId, events);
|
|
3856
|
+
}
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
switch (event.type) {
|
|
3861
|
+
case "datachannel": {
|
|
3862
|
+
const incomingId = event.channel?.id ?? null;
|
|
3863
|
+
let channel =
|
|
3864
|
+
incomingId == null ? null : this._syntheticIncomingDataChannelForId(incomingId);
|
|
3865
|
+
const adoptedSynthetic = Boolean(channel && channel.readyState !== "closed");
|
|
3866
|
+
if (adoptedSynthetic) {
|
|
3867
|
+
this._pendingSyntheticDataChannelAnnouncements.delete(incomingId);
|
|
3868
|
+
channel._adoptNativeChannel(event.channel, event.channelReadyState);
|
|
3869
|
+
} else {
|
|
3870
|
+
channel = RTCDataChannel._fromNative(this, event.channel, event.channelReadyState);
|
|
3871
|
+
}
|
|
3872
|
+
const id = channel.id;
|
|
3873
|
+
if (id != null) {
|
|
3874
|
+
if (adoptedSynthetic && this._remoteAnnouncedDataChannelIds.has(id)) {
|
|
3875
|
+
this._flushPendingNativeDataChannelEvents(channel, event.channel.bindingId);
|
|
3876
|
+
channel._flushQueuedNativeEventsAfterAnnouncement();
|
|
3877
|
+
if (!channel._announcementPending) channel._announceOpenAfterDataChannelEvent();
|
|
3878
|
+
break;
|
|
3879
|
+
}
|
|
3880
|
+
if (this._remoteAnnouncedDataChannelIds.has(id)) {
|
|
3881
|
+
channel.close();
|
|
3882
|
+
break;
|
|
3883
|
+
}
|
|
3884
|
+
this._remoteAnnouncedDataChannelIds.add(id);
|
|
3885
|
+
}
|
|
3886
|
+
channel._createdLocally = false;
|
|
3887
|
+
channel._openEventPending = channel.readyState === "open";
|
|
3888
|
+
channel._announcementPending = true;
|
|
3889
|
+
this._pendingDataChannelEvents.push(new RTCDataChannelEvent("datachannel", { channel }));
|
|
3890
|
+
this._flushPendingNativeDataChannelEvents(channel, event.channelId);
|
|
3891
|
+
this._scheduleDataChannelFlush();
|
|
3892
|
+
break;
|
|
3893
|
+
}
|
|
3894
|
+
case "localdescription":
|
|
3895
|
+
this._refreshCurrentOrPendingLocalDescription(new RTCSessionDescription(event.description));
|
|
3896
|
+
this._registerLocalDescriptionForPairing();
|
|
3897
|
+
this._refreshIceRole();
|
|
3898
|
+
this._updateSctpTransport();
|
|
3899
|
+
break;
|
|
3900
|
+
case "localcandidate": {
|
|
3901
|
+
const candidateInit = {
|
|
3902
|
+
candidate: event.candidate.candidate,
|
|
3903
|
+
sdpMid: event.candidate.sdpMid || "0",
|
|
3904
|
+
};
|
|
3905
|
+
const candidate = new RTCIceCandidate(candidateInit);
|
|
3906
|
+
Object.defineProperty(candidate, "_webrtcNodeLocalCandidate", {
|
|
3907
|
+
value: true,
|
|
3908
|
+
configurable: true,
|
|
3909
|
+
});
|
|
3910
|
+
this._rememberLocalIceCandidate(candidate);
|
|
3911
|
+
this._refreshCurrentOrPendingLocalDescription(
|
|
3912
|
+
appendRemoteCandidateToDescription(this._localDescription, candidate),
|
|
3913
|
+
);
|
|
3914
|
+
this.dispatchEvent(new RTCPeerConnectionIceEvent("icecandidate", { candidate }));
|
|
3915
|
+
break;
|
|
3916
|
+
}
|
|
3917
|
+
case "icegatheringstatechange":
|
|
3918
|
+
this._iceGatheringState = event.state;
|
|
3919
|
+
this.dispatchEvent(makeEvent("icegatheringstatechange"));
|
|
3920
|
+
this._iceTransport()?.dispatchEvent(makeEvent("gatheringstatechange"));
|
|
3921
|
+
if (event.state === "complete") {
|
|
3922
|
+
this.dispatchEvent(new RTCPeerConnectionIceEvent("icecandidate", { candidate: null }));
|
|
3923
|
+
}
|
|
3924
|
+
break;
|
|
3925
|
+
case "iceconnectionstatechange":
|
|
3926
|
+
this._iceConnectionState = event.state;
|
|
3927
|
+
this._updateSctpTransport();
|
|
3928
|
+
this.dispatchEvent(makeEvent("iceconnectionstatechange"));
|
|
3929
|
+
this._iceTransport()?._handlePeerIceConnectionState(event.state);
|
|
3930
|
+
this._closeChannelsOnPeerFailure();
|
|
3931
|
+
break;
|
|
3932
|
+
case "connectionstatechange":
|
|
3933
|
+
this._connectionState = event.state;
|
|
3934
|
+
this._updateSctpTransport();
|
|
3935
|
+
this.dispatchEvent(makeEvent("connectionstatechange"));
|
|
3936
|
+
this._closeChannelsOnPeerFailure();
|
|
3937
|
+
break;
|
|
3938
|
+
case "signalingstatechange":
|
|
3939
|
+
this._signalingState = event.state;
|
|
3940
|
+
if (this._suppressNextNativeSignalingState === event.state) {
|
|
3941
|
+
this._suppressNextNativeSignalingState = null;
|
|
3942
|
+
break;
|
|
3943
|
+
}
|
|
3944
|
+
this.dispatchEvent(makeEvent("signalingstatechange"));
|
|
3945
|
+
break;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
_registerDataChannelId(channel, id = channel._effectiveId()) {
|
|
3950
|
+
if (id == null) return;
|
|
3951
|
+
if (channel._registeredDataChannelId === id) return;
|
|
3952
|
+
this._unregisterDataChannelId(channel);
|
|
3953
|
+
const existing = this._usedDataChannelIds.get(id);
|
|
3954
|
+
if (existing && existing !== channel && existing.readyState !== "closed") return;
|
|
3955
|
+
channel._registeredDataChannelId = id;
|
|
3956
|
+
this._usedDataChannelIds.set(id, channel);
|
|
3957
|
+
this._pairDataChannelById(channel, id);
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
_flushPendingNativeDataChannelEvents(channel, channelId) {
|
|
3961
|
+
const events = this._pendingNativeDataChannelEvents.get(channelId);
|
|
3962
|
+
if (!events) return;
|
|
3963
|
+
this._pendingNativeDataChannelEvents.delete(channelId);
|
|
3964
|
+
for (const event of events) {
|
|
3965
|
+
if (channel._announcementPending || channel._nativeEventDrainActive) {
|
|
3966
|
+
channel._queuedNativeEvents.push(event);
|
|
3967
|
+
} else {
|
|
3968
|
+
channel._handleNativeEvent(event, true);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
_unregisterDataChannelId(channel) {
|
|
3974
|
+
const id = channel._registeredDataChannelId;
|
|
3975
|
+
if (id == null) return;
|
|
3976
|
+
if (this._usedDataChannelIds.get(id) === channel) {
|
|
3977
|
+
this._usedDataChannelIds.delete(id);
|
|
3978
|
+
}
|
|
3979
|
+
channel._registeredDataChannelId = null;
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
_isDataChannelIdInUse(id) {
|
|
3983
|
+
for (const channel of this._channels.values()) {
|
|
3984
|
+
this._registerDataChannelId(channel, channel._effectiveId());
|
|
3985
|
+
}
|
|
3986
|
+
const channel = this._usedDataChannelIds.get(id);
|
|
3987
|
+
return Boolean(channel && channel.readyState !== "closed");
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
_dataChannelIdParityFromDtlsRole() {
|
|
3991
|
+
if (this._remoteDescription?.type !== "answer") return null;
|
|
3992
|
+
if (this._localDescription?.type !== "offer") return null;
|
|
3993
|
+
const setup = dtlsSetupFromDescription(this._remoteDescription);
|
|
3994
|
+
if (setup === "passive") return 0;
|
|
3995
|
+
if (setup === "active") return 1;
|
|
3996
|
+
return null;
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
_nextAvailableDataChannelId(parity) {
|
|
4000
|
+
for (let id = parity; id <= 65534; id += 2) {
|
|
4001
|
+
if (!this._isDataChannelIdInUse(id)) return id;
|
|
4002
|
+
}
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
_assignDataChannelIdsFromDtlsRole() {
|
|
4007
|
+
const parity = this._dataChannelIdParityFromDtlsRole();
|
|
4008
|
+
if (parity === null) return;
|
|
4009
|
+
for (const channel of this._channels.values()) {
|
|
4010
|
+
if (channel.negotiated || channel._effectiveId() != null || channel.readyState === "closed")
|
|
4011
|
+
continue;
|
|
4012
|
+
const id = this._nextAvailableDataChannelId(parity);
|
|
4013
|
+
if (id === null) {
|
|
4014
|
+
channel._handlePeerConnectionFailure();
|
|
4015
|
+
continue;
|
|
4016
|
+
}
|
|
4017
|
+
channel._assignId(id);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
_closeChannelsOnPeerFailure({ includeDisconnected = false } = {}) {
|
|
4022
|
+
if (this._closed) return;
|
|
4023
|
+
const failed =
|
|
4024
|
+
this._connectionState === "closed" ||
|
|
4025
|
+
this._connectionState === "failed" ||
|
|
4026
|
+
this._iceConnectionState === "closed" ||
|
|
4027
|
+
this._iceConnectionState === "failed" ||
|
|
4028
|
+
(includeDisconnected &&
|
|
4029
|
+
(this._connectionState === "disconnected" || this._iceConnectionState === "disconnected"));
|
|
4030
|
+
if (!failed) return;
|
|
4031
|
+
for (const channel of this._channels.values()) {
|
|
4032
|
+
channel._handlePeerConnectionFailure();
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
_markNegotiationNeeded() {
|
|
4037
|
+
if (this._closed || this._signalingState !== "stable" || this._negotiationNeeded) return;
|
|
4038
|
+
this._negotiationNeeded = true;
|
|
4039
|
+
if (this._negotiationNeededScheduled) return;
|
|
4040
|
+
this._negotiationNeededScheduled = true;
|
|
4041
|
+
setTimeout(() => {
|
|
4042
|
+
this._negotiationNeededScheduled = false;
|
|
4043
|
+
if (this._closed || this._signalingState !== "stable" || !this._negotiationNeeded) return;
|
|
4044
|
+
this.dispatchEvent(makeEvent("negotiationneeded"));
|
|
4045
|
+
}, 0);
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
_clearNegotiationNeededIfDataMLineIsPresent() {
|
|
4049
|
+
const sdp = this._localDescription?.sdp || this._remoteDescription?.sdp || "";
|
|
4050
|
+
if (/\r?\nm=application\b/i.test(sdp)) {
|
|
4051
|
+
this._negotiationNeeded = false;
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
_eventListenerAdded(type) {
|
|
4056
|
+
if (type === "datachannel") {
|
|
4057
|
+
this._scheduleDataChannelFlush();
|
|
4058
|
+
this._scheduleDataChannelAnnouncementRepair();
|
|
4059
|
+
this._pairedPeer?._scheduleDataChannelAnnouncementRepair();
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
_scheduleDataChannelFlush() {
|
|
4064
|
+
if (this._dataChannelFlushScheduled || !this._pendingDataChannelEvents.length) return;
|
|
4065
|
+
this._dataChannelFlushScheduled = true;
|
|
4066
|
+
setTimeout(() => {
|
|
4067
|
+
this._dataChannelFlushScheduled = false;
|
|
4068
|
+
if (this._closed || !this._hasEventConsumer("datachannel")) return;
|
|
4069
|
+
const events = this._pendingDataChannelEvents.splice(0);
|
|
4070
|
+
for (const event of events) {
|
|
4071
|
+
if (this._closed) return;
|
|
4072
|
+
this.dispatchEvent(event);
|
|
4073
|
+
event.channel._announcementPending = false;
|
|
4074
|
+
event.channel._announceOpenAfterDataChannelEvent();
|
|
4075
|
+
event.channel._gateMessageEventsAfterAnnouncement();
|
|
4076
|
+
event.channel._gateMessageEventsUntilConsumer();
|
|
4077
|
+
event.channel._flushQueuedNativeEventsAfterAnnouncement();
|
|
4078
|
+
}
|
|
4079
|
+
if (this._pendingDataChannelEvents.length) this._scheduleDataChannelFlush();
|
|
4080
|
+
}, 0);
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
function normalizeDataChannelInit(init) {
|
|
4085
|
+
const options = {
|
|
4086
|
+
ordered: init.ordered !== undefined ? Boolean(init.ordered) : true,
|
|
4087
|
+
maxPacketLifeTime: init.maxPacketLifeTime,
|
|
4088
|
+
maxRetransmits: init.maxRetransmits,
|
|
4089
|
+
protocol: init.protocol === undefined ? "" : String(init.protocol),
|
|
4090
|
+
negotiated: init.negotiated !== undefined ? Boolean(init.negotiated) : false,
|
|
4091
|
+
id: init.id,
|
|
4092
|
+
};
|
|
4093
|
+
|
|
4094
|
+
if (options.maxPacketLifeTime != null) {
|
|
4095
|
+
options.maxPacketLifeTime = enforceRange(options.maxPacketLifeTime, "maxPacketLifeTime");
|
|
4096
|
+
}
|
|
4097
|
+
if (options.maxRetransmits != null) {
|
|
4098
|
+
options.maxRetransmits = enforceRange(options.maxRetransmits, "maxRetransmits");
|
|
4099
|
+
}
|
|
4100
|
+
if (options.maxPacketLifeTime != null && options.maxRetransmits != null) {
|
|
4101
|
+
throw new TypeError("maxPacketLifeTime and maxRetransmits are mutually exclusive");
|
|
4102
|
+
}
|
|
4103
|
+
if (options.id != null) {
|
|
4104
|
+
options.id = enforceRange(options.id, "id", 65535);
|
|
4105
|
+
}
|
|
4106
|
+
if (options.negotiated && options.id == null) {
|
|
4107
|
+
throw new TypeError("negotiated RTCDataChannel requires an id");
|
|
4108
|
+
}
|
|
4109
|
+
if (options.negotiated && options.id > 65534) {
|
|
4110
|
+
throw new TypeError("id must be an integer between 0 and 65534");
|
|
4111
|
+
}
|
|
4112
|
+
if (!options.negotiated) {
|
|
4113
|
+
options.id = undefined;
|
|
4114
|
+
}
|
|
4115
|
+
return options;
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
module.exports = {
|
|
4119
|
+
RTCPeerConnection,
|
|
4120
|
+
RTCDataChannel,
|
|
4121
|
+
RTCSessionDescription,
|
|
4122
|
+
RTCIceCandidate,
|
|
4123
|
+
RTCIceCandidatePair,
|
|
4124
|
+
RTCCertificate,
|
|
4125
|
+
RTCDataChannelEvent,
|
|
4126
|
+
RTCPeerConnectionIceEvent,
|
|
4127
|
+
RTCPeerConnectionIceErrorEvent,
|
|
4128
|
+
RTCError,
|
|
4129
|
+
RTCErrorEvent,
|
|
4130
|
+
RTCIceTransport,
|
|
4131
|
+
RTCSctpTransport,
|
|
4132
|
+
RTCDtlsTransport,
|
|
4133
|
+
EventTarget: SimpleEventTarget,
|
|
4134
|
+
Event: SimpleEvent,
|
|
4135
|
+
MessageEvent: SimpleMessageEvent,
|
|
4136
|
+
nonstandard: {
|
|
4137
|
+
native,
|
|
4138
|
+
},
|
|
4139
|
+
};
|