@q9labsai/chalk-client 5.2.4 → 5.2.6
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/CHANGELOG.md +4 -0
- package/dist/access/grant.d.ts +2 -1
- package/dist/access/manager.d.ts +1 -0
- package/dist/{chunk-AZONR3Q3.js → chunk-CJVLTSIO.js} +120 -8
- package/dist/{chunk-LDPC2JZB.js → chunk-I7Y4HR2P.js} +711 -194
- package/dist/{chunk-SYUGRHZ5.js → chunk-WFRTUBN6.js} +1 -1
- package/dist/{chunk-3ASKGMD3.js → chunk-YIDKETOX.js} +410 -232
- package/dist/client.d.ts +74 -2
- package/dist/connection/dependencies.d.ts +4 -1
- package/dist/connection/diagnostics.d.ts +4 -0
- package/dist/connection/episode-status.d.ts +7 -0
- package/dist/connection/index.d.ts +2 -0
- package/dist/connection/lifecycle.d.ts +2 -0
- package/dist/connection/production.d.ts +1 -1
- package/dist/effect.js +19 -3
- package/dist/generated/http-api.d.ts +73 -2
- package/dist/generated/schemas.d.ts +126 -4
- package/dist/index.js +4 -4
- package/dist/media/client.d.ts +4 -3
- package/dist/media/failure-diagnostics.d.ts +10 -0
- package/dist/media/index.js +1 -1
- package/dist/media/plane.d.ts +7 -2
- package/dist/media/types.d.ts +4 -2
- package/dist/public-invites/index.js +2 -2
- package/dist/server/access-refresh.d.cts +1 -1
- package/dist/server/access-refresh.d.ts +1 -1
- package/dist/server/index.cjs +15 -5
- package/dist/server/index.js +15 -5
- package/dist/server/types.d.cts +2 -1
- package/dist/server/types.d.ts +2 -1
- package/dist/space-client/media-projection-monitor.d.ts +26 -0
- package/dist/space-client/promise-facade.d.ts +5 -5
- package/dist/space-client/types.d.ts +4 -0
- package/dist/transport-diagnostics.d.ts +1 -1
- package/package.json +1 -1
|
@@ -176,6 +176,61 @@ function matchRemotePullTracks(publications, tracks) {
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// src/media/failure-diagnostics.ts
|
|
180
|
+
var steps = /* @__PURE__ */ new WeakMap();
|
|
181
|
+
var fetchFailures = /* @__PURE__ */ new WeakSet();
|
|
182
|
+
var domErrorKinds = /* @__PURE__ */ new Map([
|
|
183
|
+
["NotAllowedError", "NotAllowedError"],
|
|
184
|
+
["NotReadableError", "NotReadableError"],
|
|
185
|
+
["NotFoundError", "NotFoundError"],
|
|
186
|
+
["OverconstrainedError", "OverconstrainedError"],
|
|
187
|
+
["AbortError", "AbortError"],
|
|
188
|
+
["SecurityError", "SecurityError"],
|
|
189
|
+
["InvalidStateError", "InvalidStateError"],
|
|
190
|
+
["OperationError", "OperationError"],
|
|
191
|
+
["NotSupportedError", "NotSupportedError"],
|
|
192
|
+
["TimeoutError", "timeout"],
|
|
193
|
+
["NetworkError", "network"]
|
|
194
|
+
]);
|
|
195
|
+
function grantedMediaFromStream(stream) {
|
|
196
|
+
const liveTracks = stream.getTracks().filter((track) => track.readyState === "live");
|
|
197
|
+
return {
|
|
198
|
+
granted_audio: liveTracks.some((track) => track.kind === "audio") ? "granted" : "not_granted",
|
|
199
|
+
granted_video: liveTracks.some((track) => track.kind === "video") ? "granted" : "not_granted"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function markMediaFetchFailure(error) {
|
|
203
|
+
if (typeof error === "object" && error !== null) fetchFailures.add(error);
|
|
204
|
+
}
|
|
205
|
+
function markMediaFailure(error, step) {
|
|
206
|
+
if (typeof error === "object" && error !== null && !steps.has(error)) steps.set(error, step);
|
|
207
|
+
}
|
|
208
|
+
function unwrapMediaFailure(error, step) {
|
|
209
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
210
|
+
if (typeof error !== "object" || error === null) break;
|
|
211
|
+
step = steps.get(error) ?? step;
|
|
212
|
+
if (fetchFailures.has(error)) break;
|
|
213
|
+
if (!("cause" in error) || error.cause === void 0) break;
|
|
214
|
+
error = error.cause;
|
|
215
|
+
}
|
|
216
|
+
return { error, step };
|
|
217
|
+
}
|
|
218
|
+
function classifyMediaFailure(cause, fallbackStep) {
|
|
219
|
+
const { error, step } = unwrapMediaFailure(cause, fallbackStep);
|
|
220
|
+
if (error instanceof CloudflareSFUError) {
|
|
221
|
+
const status = error.options.status;
|
|
222
|
+
return {
|
|
223
|
+
media_error_kind: error.code === "signaling_timeout" || error.code === "negotiation_timeout" ? "timeout" : "CloudflareSFUError",
|
|
224
|
+
media_error_step: step,
|
|
225
|
+
...status !== void 0 && Number.isInteger(status) && status >= 400 && status <= 599 ? { http_status: status } : {}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (typeof DOMException !== "undefined" && error instanceof DOMException) {
|
|
229
|
+
return { media_error_kind: domErrorKinds.get(error.name) ?? "unknown", media_error_step: step };
|
|
230
|
+
}
|
|
231
|
+
return { media_error_kind: error instanceof TypeError && fetchFailures.has(error) ? "network" : "unknown", media_error_step: step };
|
|
232
|
+
}
|
|
233
|
+
|
|
179
234
|
// src/media/client.ts
|
|
180
235
|
var EMPTY_LOCAL = Object.freeze([]);
|
|
181
236
|
var EMPTY_REMOTE = Object.freeze([]);
|
|
@@ -183,6 +238,7 @@ var INVALID_PUBLICATION_SIGNATURE = "\0invalid";
|
|
|
183
238
|
var CONNECTION_TIMEOUT_MS = 8e3;
|
|
184
239
|
var NEGOTIATION_TIMEOUT_MS = 35e3;
|
|
185
240
|
var PEER_OPERATION_TIMEOUT_MS = 8e3;
|
|
241
|
+
var CAPTURE_REUSE_WINDOW_MS = 2e4;
|
|
186
242
|
var CloudflareSFUClient = class {
|
|
187
243
|
#localListeners = /* @__PURE__ */ new Set();
|
|
188
244
|
#onError;
|
|
@@ -194,19 +250,21 @@ var CloudflareSFUClient = class {
|
|
|
194
250
|
#replaceMediaConnection;
|
|
195
251
|
#remoteListeners = /* @__PURE__ */ new Set();
|
|
196
252
|
#snapshotListeners = /* @__PURE__ */ new Set();
|
|
197
|
-
#
|
|
253
|
+
#failureOwners = /* @__PURE__ */ new WeakMap();
|
|
254
|
+
#locallyPausedSources = /* @__PURE__ */ new Map();
|
|
198
255
|
#localTracks = /* @__PURE__ */ new Map();
|
|
199
|
-
#
|
|
200
|
-
#reusableLocalPublicationIds = /* @__PURE__ */ new Map();
|
|
256
|
+
#ownedPublications = /* @__PURE__ */ new Set();
|
|
201
257
|
#remoteTracks = /* @__PURE__ */ new Map();
|
|
202
258
|
#bootstrap;
|
|
203
259
|
#connection;
|
|
204
260
|
#disposeConnectionObservation;
|
|
205
261
|
#connectionEpoch = 0;
|
|
206
262
|
#retiredLocalConnection = null;
|
|
263
|
+
#retiredLocalCause = "forced_pause";
|
|
264
|
+
#replacementCause;
|
|
207
265
|
#cursor = null;
|
|
208
266
|
#remotePullIncomplete = false;
|
|
209
|
-
#
|
|
267
|
+
#remotePullRetries = /* @__PURE__ */ new Map();
|
|
210
268
|
#remoteParticipantIds = null;
|
|
211
269
|
#remoteParticipantRevision = 0;
|
|
212
270
|
#subscribedPublicationIds = /* @__PURE__ */ new Set();
|
|
@@ -220,6 +278,7 @@ var CloudflareSFUClient = class {
|
|
|
220
278
|
#pollFailedPull = false;
|
|
221
279
|
#pollTimer;
|
|
222
280
|
#sdpTail = Promise.resolve();
|
|
281
|
+
#retirement = Promise.resolve();
|
|
223
282
|
#snapshot;
|
|
224
283
|
#started = false;
|
|
225
284
|
#stopped = false;
|
|
@@ -252,7 +311,7 @@ var CloudflareSFUClient = class {
|
|
|
252
311
|
return subscribeSnapshot(this.#snapshotListeners, listener);
|
|
253
312
|
}
|
|
254
313
|
remotePublicationResumed(publicationId) {
|
|
255
|
-
this.#
|
|
314
|
+
this.#remotePullRetries.delete(publicationId);
|
|
256
315
|
this.remotePublicationsChanged();
|
|
257
316
|
}
|
|
258
317
|
remoteParticipantsChanged(participantIds) {
|
|
@@ -273,20 +332,17 @@ var CloudflareSFUClient = class {
|
|
|
273
332
|
this.#requireActive();
|
|
274
333
|
validateTrackSource(source, track);
|
|
275
334
|
if (this.#localTracks.has(source)) throw new CloudflareSFUError(`A ${source} track is already prepared`, "media_failed");
|
|
335
|
+
const reusable = [...this.#ownedPublications].find((item) => item.state.source === source && item.connection === this.#connection && item.lifecycle.kind === "confirmed");
|
|
276
336
|
const state = {
|
|
277
337
|
source,
|
|
278
338
|
track,
|
|
279
|
-
transceiver:
|
|
280
|
-
providerPublicationId:
|
|
281
|
-
pendingOperationId: null,
|
|
282
|
-
pendingTrackName: null,
|
|
339
|
+
transceiver: reusable?.transceiver ?? null,
|
|
340
|
+
providerPublicationId: reusable?.lifecycle.kind === "confirmed" ? reusable.lifecycle.handle.publicationId : null,
|
|
283
341
|
desiredEnabled: false,
|
|
284
342
|
enabled: false,
|
|
285
343
|
endedListener: null,
|
|
286
344
|
clearing: null
|
|
287
345
|
};
|
|
288
|
-
this.#reusableLocalTransceivers.delete(source);
|
|
289
|
-
this.#reusableLocalPublicationIds.delete(source);
|
|
290
346
|
if (source === "screen") {
|
|
291
347
|
state.endedListener = () => {
|
|
292
348
|
if (this.#localTracks.get("screen") === state) this.#invokeListener(() => this.#onScreenEnded?.());
|
|
@@ -310,9 +366,10 @@ var CloudflareSFUClient = class {
|
|
|
310
366
|
}
|
|
311
367
|
async #clearPreparedTrack(state, stopTrack = true) {
|
|
312
368
|
const source = state.source;
|
|
313
|
-
if (source === "screen_audio") {
|
|
369
|
+
if (source === "screen" || source === "screen_audio") {
|
|
314
370
|
try {
|
|
315
|
-
|
|
371
|
+
this.#pauseLocalSource(source, "capture_cleanup");
|
|
372
|
+
await this.#retireOwnedPublications();
|
|
316
373
|
} finally {
|
|
317
374
|
this.#removeOwnedLocalTrack(state, stopTrack);
|
|
318
375
|
if (this.#localTracks.get(source) === state) this.#localTracks.delete(source);
|
|
@@ -321,10 +378,8 @@ var CloudflareSFUClient = class {
|
|
|
321
378
|
}
|
|
322
379
|
return;
|
|
323
380
|
}
|
|
324
|
-
if (state.enabled) await this.#
|
|
381
|
+
if (state.enabled) await this.#disablePreparedTrack(state);
|
|
325
382
|
if (this.#localTracks.get(source) !== state) return;
|
|
326
|
-
if (state.transceiver) this.#reusableLocalTransceivers.set(source, state.transceiver);
|
|
327
|
-
if (state.providerPublicationId) this.#reusableLocalPublicationIds.set(source, state.providerPublicationId);
|
|
328
383
|
this.#removeOwnedLocalTrack(state, stopTrack);
|
|
329
384
|
this.#localTracks.delete(source);
|
|
330
385
|
this.#publishSnapshot();
|
|
@@ -349,7 +404,7 @@ var CloudflareSFUClient = class {
|
|
|
349
404
|
this.#pollFailedPull = false;
|
|
350
405
|
try {
|
|
351
406
|
const transport = this.#requireTransport();
|
|
352
|
-
const authoritative = await (() => transport.listPublications())
|
|
407
|
+
const authoritative = await this.#signal(() => transport.listPublications());
|
|
353
408
|
this.#requireGeneration(generation);
|
|
354
409
|
await this.#reconcileRemotePublications(authoritative, generation);
|
|
355
410
|
this.#requireGeneration(generation);
|
|
@@ -370,46 +425,63 @@ var CloudflareSFUClient = class {
|
|
|
370
425
|
this.#schedulePoll(0);
|
|
371
426
|
}
|
|
372
427
|
async setLocalPublicationTarget(target) {
|
|
373
|
-
|
|
428
|
+
if (!target.enabled) return this.#pausePublicationTarget(target);
|
|
429
|
+
const result = await this.#enablePublicationTarget(target);
|
|
374
430
|
if (target.source !== "screen" || !this.#localTracks.has("screen_audio") || result.outcome !== "confirmed" && result.outcome !== "satisfied") return result;
|
|
375
|
-
return this.#
|
|
431
|
+
return this.#enablePublicationTarget({ ...target, source: "screen_audio" });
|
|
376
432
|
}
|
|
377
|
-
async #
|
|
433
|
+
async #pausePublicationTarget(target) {
|
|
434
|
+
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
435
|
+
if (resolved.kind === "result" && resolved.result.outcome !== "satisfied") return resolved.result;
|
|
436
|
+
const outcome = resolved.kind === "state" && resolved.value.enabled ? "confirmed" : "satisfied";
|
|
437
|
+
if (target.source !== "screen" && this.#locallyPausedSources.get(target.source) === "capture_cleanup") {
|
|
438
|
+
await this.pauseLocalCapture(target.source);
|
|
439
|
+
return { outcome, errorCode: null };
|
|
440
|
+
}
|
|
441
|
+
this.#pauseLocalSource(target.source);
|
|
442
|
+
try {
|
|
443
|
+
await this.#retireOwnedPublications();
|
|
444
|
+
return { outcome, errorCode: null };
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return mediaTargetFailure(error);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async #enablePublicationTarget(target) {
|
|
450
|
+
const generation = this.#generation;
|
|
378
451
|
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
379
452
|
if (resolved.kind === "result") return resolved.result;
|
|
380
453
|
const state = resolved.value;
|
|
381
|
-
if (this.#
|
|
454
|
+
if (this.#locallyPausedSources.has(target.source)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
382
455
|
try {
|
|
383
|
-
|
|
384
|
-
if (this.#
|
|
385
|
-
if (state.enabled
|
|
386
|
-
await this.#
|
|
456
|
+
await this.#retireUnavailableLocalPublication(state);
|
|
457
|
+
if (this.#locallyPausedSources.has(target.source)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
458
|
+
if (state.enabled) return { outcome: "satisfied", errorCode: null };
|
|
459
|
+
if (!await this.#enablePreparedTrack(state)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
387
460
|
return { outcome: "confirmed", errorCode: null };
|
|
388
461
|
} catch (error) {
|
|
389
|
-
if (!this.#stopped) this.#reportError(error);
|
|
462
|
+
if (generation === this.#generation && !this.#stopped) this.#reportError(error);
|
|
390
463
|
return mediaTargetFailure(error);
|
|
391
464
|
}
|
|
392
465
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
setLocalSourceIntent(source, enabled) {
|
|
397
|
-
if (source === "screen") this.setLocalSourceIntent("screen_audio", enabled);
|
|
398
|
-
if (enabled) {
|
|
399
|
-
this.#locallyPausedSources.delete(source);
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
this.#locallyPausedSources.add(source);
|
|
466
|
+
async pauseLocalCapture(source) {
|
|
467
|
+
this.#locallyPausedSources.set(source, "capture_cleanup");
|
|
403
468
|
const state = this.#localTracks.get(source);
|
|
404
|
-
if (
|
|
405
|
-
state.desiredEnabled = false;
|
|
406
|
-
state.track.enabled = false;
|
|
469
|
+
if (state) await this.#disablePreparedTrack(state);
|
|
407
470
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
471
|
+
setLocalSourceIntent(source, enabled, cause = "forced_pause") {
|
|
472
|
+
const sources = localSourceGroup(source);
|
|
473
|
+
for (const item of sources) {
|
|
474
|
+
if (enabled) this.#locallyPausedSources.delete(item);
|
|
475
|
+
else this.#locallyPausedSources.set(item, cause);
|
|
476
|
+
}
|
|
477
|
+
if (!enabled) this.#pauseLocalSource(source, cause);
|
|
478
|
+
return enabled ? Promise.resolve() : this.#retireOwnedPublications();
|
|
479
|
+
}
|
|
480
|
+
async closeForcedLocalPublication(source, cause = "forced_pause") {
|
|
481
|
+
const sources = localSourceGroup(source);
|
|
482
|
+
const ownsPublication = [...this.#ownedPublications].some((item) => sources.includes(item.state.source));
|
|
483
|
+
await this.setLocalSourceIntent(source, false, cause);
|
|
484
|
+
if (!ownsPublication || !this.#replaceMediaConnection) return;
|
|
413
485
|
const generation = this.#generation;
|
|
414
486
|
await this.#serializeSDP(async () => {
|
|
415
487
|
const existing = new Set([...this.#localTracks.values()].filter((state) => state.source !== source && state.providerPublicationId !== null));
|
|
@@ -423,54 +495,90 @@ var CloudflareSFUClient = class {
|
|
|
423
495
|
this.#requireGeneration(generation);
|
|
424
496
|
this.#setPhase("live", null);
|
|
425
497
|
});
|
|
498
|
+
await this.#retirement;
|
|
426
499
|
}
|
|
427
500
|
async #retireUnavailableLocalPublication(state) {
|
|
428
501
|
const publicationId = state.providerPublicationId;
|
|
429
502
|
if (!publicationId) return;
|
|
430
|
-
const authoritative = await (() => this.#requireTransport().listPublications())
|
|
503
|
+
const authoritative = await this.#signal(() => this.#requireTransport().listPublications());
|
|
431
504
|
if (authoritative.publications.some((publication) => publication.publicationId === publicationId)) return;
|
|
432
|
-
if (state.providerPublicationId === publicationId)
|
|
505
|
+
if (state.providerPublicationId === publicationId) {
|
|
506
|
+
this.#pauseLocalSource(state.source, "discovery_absent");
|
|
507
|
+
await this.#retireOwnedPublications();
|
|
508
|
+
}
|
|
433
509
|
}
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
state.
|
|
510
|
+
#pauseLocalSource(source, cause = "forced_pause") {
|
|
511
|
+
const sources = localSourceGroup(source);
|
|
512
|
+
for (const publication of this.#ownedPublications) {
|
|
513
|
+
if (!sources.includes(publication.state.source)) continue;
|
|
514
|
+
this.#markPublicationRetiring(publication, cause);
|
|
515
|
+
}
|
|
516
|
+
for (const state of this.#localTracks.values()) {
|
|
517
|
+
if (!sources.includes(state.source)) continue;
|
|
442
518
|
state.desiredEnabled = false;
|
|
443
|
-
state
|
|
444
|
-
state.providerPublicationId = null;
|
|
445
|
-
state.transceiver = null;
|
|
519
|
+
this.#clearLocalPublication(state);
|
|
446
520
|
}
|
|
447
521
|
this.#publishSnapshot();
|
|
448
522
|
this.#emitLocal();
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const response = await this.#requireTransport().closeTracks({
|
|
462
|
-
connectionId,
|
|
463
|
-
...providerDescription(requireDescription(offer)),
|
|
464
|
-
tracks: [{ mid, source, publicationId }],
|
|
465
|
-
force: false
|
|
466
|
-
});
|
|
467
|
-
await this.#applyProviderDescription(response, connection);
|
|
468
|
-
} catch (error) {
|
|
469
|
-
await this.#rollbackLocalOffer(connection);
|
|
470
|
-
if (!this.#stopped) this.#reportError(error);
|
|
471
|
-
throw error;
|
|
523
|
+
}
|
|
524
|
+
#retireOwnedPublications(reportFailure = this.#reportError.bind(this)) {
|
|
525
|
+
const publications = [...this.#ownedPublications].filter((item) => item.lifecycle.kind === "retiring" && item.lifecycle.handle);
|
|
526
|
+
const retirement = (async () => {
|
|
527
|
+
const errors = [];
|
|
528
|
+
for (const publication of publications) {
|
|
529
|
+
try {
|
|
530
|
+
await this.#serializeSDP(() => this.#closeOwnedPublication(publication), true);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
errors.push(error);
|
|
533
|
+
if (!this.#stopped) reportFailure(error);
|
|
534
|
+
}
|
|
472
535
|
}
|
|
536
|
+
if (this.#stopped && this.#ownedPublications.size === 0) this.#transport = void 0;
|
|
537
|
+
if (errors.length) throw new AggregateError(errors, "Media retirement failed");
|
|
538
|
+
})();
|
|
539
|
+
void retirement.catch(() => {
|
|
473
540
|
});
|
|
541
|
+
return this.#retirement = retirement;
|
|
542
|
+
}
|
|
543
|
+
#markPublicationRetiring(publication, cause = "publish_failure") {
|
|
544
|
+
const phase = publication.lifecycle;
|
|
545
|
+
if (phase.kind === "pending" || phase.kind === "confirmed") publication.lifecycle = { kind: "retiring", handle: phase.kind === "confirmed" ? phase.handle : null, detached: false, cause };
|
|
546
|
+
}
|
|
547
|
+
#markBundledTransportRetiring(publication) {
|
|
548
|
+
const bundleTag = /(?:^|\r?\n)a=group:BUNDLE[ \t]+(\S+)/.exec(publication.connection.localDescription?.sdp ?? "")?.[1];
|
|
549
|
+
if (!bundleTag || publication.transceiver.mid !== null && publication.transceiver.mid === bundleTag) {
|
|
550
|
+
if (this.#retiredLocalConnection === publication.connection) return;
|
|
551
|
+
this.#retiredLocalConnection = publication.connection;
|
|
552
|
+
if (publication.lifecycle.kind === "retiring") this.#retiredLocalCause = publication.lifecycle.cause;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async #closeOwnedPublication(publication) {
|
|
556
|
+
const phase = publication.lifecycle;
|
|
557
|
+
if (phase.kind !== "retiring" || !phase.handle) return;
|
|
558
|
+
const handle = phase.handle;
|
|
559
|
+
const { connection, connectionId, transceiver, state } = publication;
|
|
560
|
+
this.#markBundledTransportRetiring(publication);
|
|
561
|
+
let closed = connection.connectionState === "closed";
|
|
562
|
+
try {
|
|
563
|
+
if (!phase.detached && !closed) {
|
|
564
|
+
await this.#boundPeerOperation(transceiver.sender.replaceTrack(null));
|
|
565
|
+
closed = connection.connectionState === "closed";
|
|
566
|
+
if (!closed) transceiver.stop();
|
|
567
|
+
}
|
|
568
|
+
phase.detached = true;
|
|
569
|
+
const sessionDescription = closed ? void 0 : await createLocalOffer(connection);
|
|
570
|
+
const response = await this.#signal(() => this.#requireTransport().closeTracks({ connectionId, sessionDescription, tracks: [{ ...handle, source: state.source }], force: closed }), { generation: this.#generation, connectionId });
|
|
571
|
+
publication.lifecycle = { kind: "retired" };
|
|
572
|
+
this.#ownedPublications.delete(publication);
|
|
573
|
+
if (connection.connectionState !== "closed") await this.#applyProviderDescription(response, connection);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
await this.#rollbackLocalOffer(connection);
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
#fenceLocalPublications() {
|
|
580
|
+
for (const publication of this.#ownedPublications) publication.lifecycle = { kind: "retired" };
|
|
581
|
+
this.#ownedPublications.clear();
|
|
474
582
|
}
|
|
475
583
|
observeLocalPublications(listener) {
|
|
476
584
|
this.#localListeners.add(listener);
|
|
@@ -486,19 +594,23 @@ var CloudflareSFUClient = class {
|
|
|
486
594
|
this.#requireActive();
|
|
487
595
|
const options = "connectionId" in input ? { bootstrap: input } : input;
|
|
488
596
|
validateBootstrap(options.bootstrap);
|
|
597
|
+
if (options.bootstrap.connectionId !== this.#bootstrap.connectionId) {
|
|
598
|
+
this.#fenceLocalPublications();
|
|
599
|
+
this.#replacementCause = "access_refresh";
|
|
600
|
+
}
|
|
489
601
|
const generation = ++this.#generation;
|
|
490
602
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
491
603
|
this.#polling = false;
|
|
492
604
|
this.#pollAfterCurrent = false;
|
|
493
605
|
this.#clearPoll();
|
|
494
|
-
this.#
|
|
606
|
+
this.#remotePullRetries.clear();
|
|
495
607
|
this.#disposeConnection(false);
|
|
496
|
-
this.#
|
|
497
|
-
this.#
|
|
608
|
+
for (const publication of this.#ownedPublications) this.#markPublicationRetiring(publication);
|
|
609
|
+
void this.#retireOwnedPublications();
|
|
498
610
|
this.#clearRemoteTracks();
|
|
499
611
|
this.#cursor = null;
|
|
500
612
|
this.#negotiatedGeneration = null;
|
|
501
|
-
this.#replacementAttemptedGeneration = null;
|
|
613
|
+
this.#replacementAttemptedGeneration = options.bootstrap.connectionId !== this.#bootstrap.connectionId ? generation : null;
|
|
502
614
|
if (options.bootstrap.connectionId !== this.#bootstrap.connectionId) this.#started = false;
|
|
503
615
|
this.#bootstrap = options.bootstrap;
|
|
504
616
|
if (options.transport) this.#transport = options.transport;
|
|
@@ -524,22 +636,27 @@ var CloudflareSFUClient = class {
|
|
|
524
636
|
}
|
|
525
637
|
}
|
|
526
638
|
stop() {
|
|
527
|
-
if (this.#stopped)
|
|
639
|
+
if (this.#stopped) {
|
|
640
|
+
void this.#retireOwnedPublications();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
528
643
|
this.#stopped = true;
|
|
529
644
|
this.#subscribedPublicationIds.clear();
|
|
530
645
|
this.#generation++;
|
|
531
646
|
this.#connectionEpoch++;
|
|
532
647
|
this.#clearPoll();
|
|
533
|
-
this.#
|
|
648
|
+
this.#remotePullRetries.clear();
|
|
534
649
|
this.#polling = false;
|
|
535
650
|
this.#pollAfterCurrent = false;
|
|
536
651
|
this.#disposeConnection(true);
|
|
537
|
-
this.#
|
|
538
|
-
this.#reusableLocalPublicationIds.clear();
|
|
652
|
+
for (const publication of this.#ownedPublications) this.#markPublicationRetiring(publication);
|
|
539
653
|
this.#clearRemoteTracks();
|
|
540
|
-
for (const state of this.#localTracks.values())
|
|
654
|
+
for (const state of this.#localTracks.values()) {
|
|
655
|
+
state.track.enabled = false;
|
|
656
|
+
this.#removeOwnedLocalTrack(state);
|
|
657
|
+
}
|
|
541
658
|
this.#localTracks.clear();
|
|
542
|
-
this.#
|
|
659
|
+
void this.#retireOwnedPublications();
|
|
543
660
|
this.#started = false;
|
|
544
661
|
this.#cursor = null;
|
|
545
662
|
this.#publishSnapshot("stopped", null);
|
|
@@ -549,94 +666,115 @@ var CloudflareSFUClient = class {
|
|
|
549
666
|
this.#remoteListeners.clear();
|
|
550
667
|
this.#snapshotListeners.clear();
|
|
551
668
|
}
|
|
552
|
-
async #
|
|
553
|
-
if (enabled) return this.#enablePreparedTrack(state, operationId);
|
|
554
|
-
return this.#disablePreparedTrack(state);
|
|
555
|
-
}
|
|
556
|
-
async #enablePreparedTrack(state, operationId) {
|
|
557
|
-
if (state.providerPublicationId && state.transceiver) {
|
|
558
|
-
state.desiredEnabled = true;
|
|
559
|
-
state.track.enabled = true;
|
|
560
|
-
try {
|
|
561
|
-
await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(state.track));
|
|
562
|
-
} catch (error) {
|
|
563
|
-
state.desiredEnabled = false;
|
|
564
|
-
state.track.enabled = false;
|
|
565
|
-
throw error;
|
|
566
|
-
}
|
|
567
|
-
state.enabled = !this.#locallyPausedSources.has(state.source);
|
|
568
|
-
state.desiredEnabled = state.enabled;
|
|
569
|
-
state.track.enabled = state.enabled;
|
|
570
|
-
if (!state.enabled) await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(null));
|
|
571
|
-
this.#publishSnapshot();
|
|
572
|
-
this.#emitLocal();
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
if (operationId && state.pendingOperationId !== operationId) {
|
|
576
|
-
state.pendingOperationId = operationId;
|
|
577
|
-
state.pendingTrackName = `${state.source}-${operationId}`;
|
|
578
|
-
}
|
|
579
|
-
state.desiredEnabled = true;
|
|
580
|
-
state.track.enabled = true;
|
|
669
|
+
async #enablePreparedTrack(state) {
|
|
581
670
|
const generation = this.#generation;
|
|
582
671
|
try {
|
|
672
|
+
await this.#retireExpiredCapture(state);
|
|
673
|
+
if (this.#locallyPausedSources.has(state.source)) return false;
|
|
674
|
+
state.desiredEnabled = true;
|
|
675
|
+
state.track.enabled = true;
|
|
676
|
+
if (state.providerPublicationId && state.transceiver) return await this.#reattachLocalTrack(state, state.transceiver);
|
|
583
677
|
await this.#publishPreparedTracks([state], generation);
|
|
584
678
|
if (this.#remoteRecoveryGeneration !== generation) this.#replacementAttemptedGeneration = null;
|
|
679
|
+
return state.enabled;
|
|
585
680
|
} catch (error) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
681
|
+
if (generation === this.#generation) {
|
|
682
|
+
this.#pauseLocalSource(state.source, "enable_failure");
|
|
683
|
+
await this.#retireOwnedPublications().catch(() => {
|
|
684
|
+
});
|
|
685
|
+
}
|
|
589
686
|
throw error;
|
|
590
687
|
}
|
|
591
688
|
}
|
|
689
|
+
async #retireExpiredCapture(state) {
|
|
690
|
+
const publication = [...this.#ownedPublications].find((item) => item.transceiver === state.transceiver && item.lifecycle.kind === "confirmed");
|
|
691
|
+
if (!publication || publication.detachedAt === null || Date.now() - publication.detachedAt < CAPTURE_REUSE_WINDOW_MS) return;
|
|
692
|
+
this.#pauseLocalSource(state.source, "capture_cleanup");
|
|
693
|
+
try {
|
|
694
|
+
await this.#retireOwnedPublications((error) => {
|
|
695
|
+
if (!this.#isExpiredRetiredConnection(error)) this.#reportError(error);
|
|
696
|
+
});
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (!this.#isExpiredRetiredConnection(error)) throw error;
|
|
699
|
+
this.#fenceLocalPublications();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
#isExpiredRetiredConnection(error) {
|
|
703
|
+
if (!this.#replaceMediaConnection || this.#retiredLocalConnection !== this.#connection) return false;
|
|
704
|
+
if (error instanceof AggregateError) return error.errors.every((cause) => this.#isRetryableConnectionFailure(cause));
|
|
705
|
+
return this.#isRetryableConnectionFailure(error);
|
|
706
|
+
}
|
|
707
|
+
async #reattachLocalTrack(state, transceiver) {
|
|
708
|
+
await this.#boundPeerOperation(transceiver.sender.replaceTrack(state.track));
|
|
709
|
+
const cause = this.#locallyPausedSources.get(state.source);
|
|
710
|
+
state.enabled = cause === void 0;
|
|
711
|
+
state.desiredEnabled = state.enabled;
|
|
712
|
+
state.track.enabled = state.enabled;
|
|
713
|
+
if (cause === "capture_cleanup") await this.#disablePreparedTrack(state);
|
|
714
|
+
else if (cause) {
|
|
715
|
+
this.#pauseLocalSource(state.source, cause);
|
|
716
|
+
await this.#retireOwnedPublications();
|
|
717
|
+
} else {
|
|
718
|
+
for (const publication of this.#ownedPublications) if (publication.transceiver === transceiver) publication.detachedAt = null;
|
|
719
|
+
}
|
|
720
|
+
this.#publishSnapshot();
|
|
721
|
+
this.#emitLocal();
|
|
722
|
+
return state.enabled;
|
|
723
|
+
}
|
|
592
724
|
async #disablePreparedTrack(state) {
|
|
593
725
|
const transceiver = state.transceiver;
|
|
726
|
+
for (const publication of this.#ownedPublications) {
|
|
727
|
+
if (publication.transceiver === transceiver && publication.lifecycle.kind === "confirmed") publication.detachedAt ??= Date.now();
|
|
728
|
+
}
|
|
594
729
|
state.desiredEnabled = false;
|
|
595
730
|
state.track.enabled = false;
|
|
596
731
|
const detach = transceiver ? this.#boundPeerOperation(transceiver.sender.replaceTrack(null)) : Promise.resolve();
|
|
597
732
|
state.enabled = false;
|
|
598
|
-
state.pendingOperationId = null;
|
|
599
|
-
state.pendingTrackName = null;
|
|
600
733
|
this.#publishSnapshot();
|
|
601
734
|
this.#emitLocal();
|
|
602
735
|
await detach;
|
|
603
736
|
}
|
|
604
737
|
async #publishPreparedTracks(states, generation) {
|
|
605
738
|
if (states.length === 0) return;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
739
|
+
try {
|
|
740
|
+
await this.#serializeSDP(async () => {
|
|
741
|
+
const pending = await this.#prepareConnectionForPublication(states, generation);
|
|
742
|
+
try {
|
|
743
|
+
await this.#publishPreparedTracksSerialized(pending, generation, this.#connection, this.#bootstrap.connectionId);
|
|
744
|
+
} catch (error) {
|
|
745
|
+
if (!this.#replaceMediaConnection || !this.#isRetryableConnectionFailure(error) || this.#replacementAttemptedGeneration === generation) throw error;
|
|
746
|
+
await this.#replaceMediaConnectionForRecovery(generation, "local_signaling_failure");
|
|
747
|
+
await this.#publishPreparedTracksSerialized(
|
|
748
|
+
[...this.#localTracks.values()].filter((state) => (state.desiredEnabled || states.includes(state)) && state.track.readyState !== "ended"),
|
|
749
|
+
generation,
|
|
750
|
+
this.#connection,
|
|
751
|
+
this.#bootstrap.connectionId
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
} finally {
|
|
756
|
+
await this.#retirement.catch(() => {
|
|
757
|
+
});
|
|
758
|
+
}
|
|
621
759
|
}
|
|
622
760
|
async #prepareConnectionForPublication(states, generation) {
|
|
623
761
|
if (this.#retiredLocalConnection !== this.#connection || !this.#replaceMediaConnection) {
|
|
624
762
|
await this.#replaceDormantConnectionBeforeNegotiation(generation);
|
|
625
763
|
return states;
|
|
626
764
|
}
|
|
627
|
-
const authoritative = await (() => this.#requireTransport().listPublications())
|
|
765
|
+
const authoritative = await this.#signal(() => this.#requireTransport().listPublications());
|
|
628
766
|
this.#pauseUnavailableOtherSources(authoritative.publications, states);
|
|
629
767
|
this.#replacementAttemptedGeneration = null;
|
|
630
|
-
await this.#replaceMediaConnectionForRecovery(generation);
|
|
768
|
+
await this.#replaceMediaConnectionForRecovery(generation, this.#retiredLocalCause);
|
|
631
769
|
return [...this.#localTracks.values()].filter((state) => (state.desiredEnabled || states.includes(state)) && state.track.readyState !== "ended");
|
|
632
770
|
}
|
|
633
771
|
#pauseUnavailableOtherSources(publications, states) {
|
|
634
772
|
for (const state of this.#localTracks.values()) {
|
|
635
773
|
if (states.includes(state) || !state.providerPublicationId) continue;
|
|
636
774
|
if (publications.some((publication) => publication.publicationId === state.providerPublicationId)) continue;
|
|
637
|
-
state.
|
|
638
|
-
state.track.enabled = false;
|
|
775
|
+
this.#pauseLocalSource(state.source, "discovery_absent");
|
|
639
776
|
}
|
|
777
|
+
void this.#retireOwnedPublications();
|
|
640
778
|
}
|
|
641
779
|
async #publishPreparedTracksSerialized(states, generation, connection, connectionId) {
|
|
642
780
|
this.#requireGeneration(generation);
|
|
@@ -646,28 +784,30 @@ var CloudflareSFUClient = class {
|
|
|
646
784
|
const publications = [];
|
|
647
785
|
try {
|
|
648
786
|
for (const state of pendingStates) publications.push(await this.#prepareLocalPublication(connection, state, generation));
|
|
649
|
-
|
|
787
|
+
await this.#negotiateLocalPublications(connection, connectionId, publications, generation);
|
|
650
788
|
if (!wasLive) await this.#waitForConnection(connection, generation);
|
|
651
789
|
this.#requireGeneration(generation);
|
|
652
790
|
this.#negotiatedGeneration = generation;
|
|
653
|
-
this.#
|
|
791
|
+
this.#settleLocalPublications(publications);
|
|
654
792
|
this.#publishSnapshot();
|
|
655
793
|
this.#emitLocal();
|
|
656
794
|
} catch (error) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
795
|
+
await this.#rollbackLocalOffer(connection);
|
|
796
|
+
for (const publication of publications) this.#markPublicationRetiring(publication);
|
|
797
|
+
this.#settleLocalPublications(publications);
|
|
661
798
|
throw error;
|
|
799
|
+
} finally {
|
|
800
|
+
void this.#retireOwnedPublications();
|
|
662
801
|
}
|
|
663
802
|
}
|
|
664
803
|
async #prepareLocalPublication(connection, state, generation) {
|
|
665
|
-
const reusedTransceiver = state.transceiver !== null;
|
|
666
804
|
const transceiver = state.transceiver ?? this.#addLocalTransceiver(connection, state);
|
|
667
|
-
|
|
805
|
+
const publication = { state, transceiver, trackName: `${state.source}-${globalThis.crypto.randomUUID()}`, connection, connectionId: this.#bootstrap.connectionId, detachedAt: null, lifecycle: { kind: "pending" } };
|
|
806
|
+
this.#ownedPublications.add(publication);
|
|
807
|
+
if (state.transceiver) await this.#boundPeerOperation(transceiver.sender.replaceTrack(state.track));
|
|
668
808
|
this.#requireGeneration(generation);
|
|
669
809
|
state.transceiver = transceiver;
|
|
670
|
-
return
|
|
810
|
+
return publication;
|
|
671
811
|
}
|
|
672
812
|
#addLocalTransceiver(connection, state) {
|
|
673
813
|
if (state.source !== "camera" || firefoxCameraSimulcastUnsupported()) return connection.addTransceiver(state.track, { direction: "sendonly" });
|
|
@@ -686,10 +826,9 @@ var CloudflareSFUClient = class {
|
|
|
686
826
|
}
|
|
687
827
|
}
|
|
688
828
|
async #negotiateLocalPublications(connection, connectionId, publications, generation) {
|
|
689
|
-
let
|
|
829
|
+
let sessionDescription;
|
|
690
830
|
try {
|
|
691
|
-
|
|
692
|
-
await (() => connection.setLocalDescription(offer))();
|
|
831
|
+
sessionDescription = await createLocalOffer(connection);
|
|
693
832
|
} catch (error) {
|
|
694
833
|
const cameras = publications.filter(({ state, transceiver }) => state.source === "camera" && transceiver.sender.getParameters().encodings.length > 1);
|
|
695
834
|
if (!simulcastUnsupported(error) || cameras.length === 0) throw error;
|
|
@@ -701,8 +840,7 @@ var CloudflareSFUClient = class {
|
|
|
701
840
|
publication.transceiver = connection.addTransceiver(publication.state.track, { direction: "sendonly" });
|
|
702
841
|
publication.state.transceiver = publication.transceiver;
|
|
703
842
|
}
|
|
704
|
-
|
|
705
|
-
await (() => connection.setLocalDescription(offer))();
|
|
843
|
+
sessionDescription = await createLocalOffer(connection);
|
|
706
844
|
}
|
|
707
845
|
const tracks = publications.map(
|
|
708
846
|
({ state, transceiver, trackName }) => ({
|
|
@@ -712,43 +850,44 @@ var CloudflareSFUClient = class {
|
|
|
712
850
|
source: state.source
|
|
713
851
|
})
|
|
714
852
|
);
|
|
715
|
-
const response = await (() => this.#
|
|
853
|
+
const response = await this.#signal(() => this.#addTracks({ connectionId, sessionDescription, tracks }), { generation, connectionId });
|
|
854
|
+
this.#recordLocalPublicationHandles(publications, response.tracks);
|
|
716
855
|
this.#requireGeneration(generation);
|
|
717
856
|
await this.#applyProviderDescription(response, connection);
|
|
718
|
-
return response;
|
|
719
857
|
}
|
|
720
|
-
#
|
|
721
|
-
for (const
|
|
858
|
+
#recordLocalPublicationHandles(publications, tracks) {
|
|
859
|
+
for (const publication of publications) {
|
|
860
|
+
const { state, transceiver, lifecycle } = publication;
|
|
861
|
+
if (lifecycle.kind === "retired") continue;
|
|
722
862
|
const authoritative = tracks?.find((track) => track.location === "local" && track.mid === transceiver.mid && track.source === state.source);
|
|
723
|
-
if (!authoritative?.publicationId) throw new CloudflareSFUError("
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
state.desiredEnabled = state.enabled;
|
|
727
|
-
state.track.enabled = state.enabled;
|
|
728
|
-
if (!state.enabled) void transceiver.sender.replaceTrack(null).catch((error) => this.#reportError(error));
|
|
729
|
-
state.providerPublicationId = authoritative.publicationId;
|
|
730
|
-
state.pendingOperationId = null;
|
|
731
|
-
state.pendingTrackName = null;
|
|
863
|
+
if (!authoritative?.publicationId) throw new CloudflareSFUError("Missing publication ID", "invalid_publication");
|
|
864
|
+
const handle = { publicationId: authoritative.publicationId, mid: requireTransceiverMid(transceiver) };
|
|
865
|
+
publication.lifecycle = lifecycle.kind === "retiring" ? { ...lifecycle, handle } : { kind: "confirmed", handle };
|
|
732
866
|
}
|
|
733
867
|
}
|
|
734
|
-
|
|
735
|
-
for (const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
868
|
+
#settleLocalPublications(publications) {
|
|
869
|
+
for (const publication of publications) {
|
|
870
|
+
const { state, transceiver, lifecycle } = publication;
|
|
871
|
+
if (lifecycle.kind === "retired") continue;
|
|
872
|
+
if (lifecycle.kind === "confirmed" && state.transceiver === transceiver && !this.#locallyPausedSources.has(state.source)) {
|
|
873
|
+
state.enabled = true;
|
|
874
|
+
state.desiredEnabled = true;
|
|
875
|
+
state.track.enabled = true;
|
|
876
|
+
state.providerPublicationId = lifecycle.handle.publicationId;
|
|
742
877
|
continue;
|
|
743
878
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
this.#reportError(error);
|
|
879
|
+
this.#markPublicationRetiring(publication, this.#locallyPausedSources.get(state.source) ?? "publish_failure");
|
|
880
|
+
if (state.transceiver === transceiver) {
|
|
881
|
+
this.#clearLocalPublication(state);
|
|
748
882
|
}
|
|
749
|
-
if (state.transceiver === transceiver) state.transceiver = null;
|
|
750
883
|
}
|
|
751
884
|
}
|
|
885
|
+
#clearLocalPublication(state) {
|
|
886
|
+
state.transceiver = null;
|
|
887
|
+
state.providerPublicationId = null;
|
|
888
|
+
state.enabled = false;
|
|
889
|
+
state.track.enabled = false;
|
|
890
|
+
}
|
|
752
891
|
async #rollbackLocalOffer(connection) {
|
|
753
892
|
if (connection.signalingState !== "have-local-offer") return;
|
|
754
893
|
try {
|
|
@@ -784,8 +923,8 @@ var CloudflareSFUClient = class {
|
|
|
784
923
|
}
|
|
785
924
|
#pendingRemotePulls(desired) {
|
|
786
925
|
const desiredIds = new Set([...desired.values()].map((publication) => publication.publicationId));
|
|
787
|
-
for (const publicationId of this.#
|
|
788
|
-
if (!desiredIds.has(publicationId)) this.#
|
|
926
|
+
for (const publicationId of this.#remotePullRetries.keys()) {
|
|
927
|
+
if (!desiredIds.has(publicationId)) this.#remotePullRetries.delete(publicationId);
|
|
789
928
|
}
|
|
790
929
|
for (const publicationId of this.#subscribedPublicationIds) {
|
|
791
930
|
if (!desiredIds.has(publicationId)) this.#subscribedPublicationIds.delete(publicationId);
|
|
@@ -793,7 +932,7 @@ var CloudflareSFUClient = class {
|
|
|
793
932
|
const now = Date.now();
|
|
794
933
|
return [...desired].filter(([key, publication]) => {
|
|
795
934
|
if (this.#remoteTracks.get(key)?.publicationId === publication.publicationId) return false;
|
|
796
|
-
const remaining = (this.#
|
|
935
|
+
const remaining = (this.#remotePullRetries.get(publication.publicationId)?.retryAfter ?? 0) - now;
|
|
797
936
|
return remaining <= 0;
|
|
798
937
|
}).map(([, publication]) => publication);
|
|
799
938
|
}
|
|
@@ -834,7 +973,8 @@ var CloudflareSFUClient = class {
|
|
|
834
973
|
}
|
|
835
974
|
}
|
|
836
975
|
#canReplaceConnectionAfterRemotePull(error, generation) {
|
|
837
|
-
|
|
976
|
+
const gone = error instanceof CloudflareSFUError && (error.options.status === 404 || error.options.status === 410);
|
|
977
|
+
return this.#isRetryableConnectionFailure(error) && (gone || !this.#hasLiveRemoteTracks()) && this.#replaceMediaConnection !== void 0 && this.#replacementAttemptedGeneration !== generation;
|
|
838
978
|
}
|
|
839
979
|
#hasLiveRemoteTracks() {
|
|
840
980
|
return [...this.#remoteTracks.values()].some((publication) => publication.track.readyState === "live");
|
|
@@ -842,7 +982,7 @@ var CloudflareSFUClient = class {
|
|
|
842
982
|
async #retryRemotePullOnReplacement(publications, cursor, generation) {
|
|
843
983
|
this.#remoteRecoveryGeneration = generation;
|
|
844
984
|
try {
|
|
845
|
-
await this.#replaceMediaConnectionForRecovery(generation);
|
|
985
|
+
await this.#replaceMediaConnectionForRecovery(generation, "remote_signaling_failure");
|
|
846
986
|
await this.#republishPreparedTracksAfterRecovery(generation);
|
|
847
987
|
return await this.#pull(publications, generation);
|
|
848
988
|
} catch (error) {
|
|
@@ -887,7 +1027,10 @@ var CloudflareSFUClient = class {
|
|
|
887
1027
|
}
|
|
888
1028
|
#deferRemotePulls(publications, error) {
|
|
889
1029
|
const retryAfter = retryableRemotePull(error) ? Date.now() + 15e3 : Number.POSITIVE_INFINITY;
|
|
890
|
-
for (const publication of publications)
|
|
1030
|
+
for (const publication of publications) {
|
|
1031
|
+
const failures = this.#remotePullRetries.get(publication.publicationId)?.failures ?? 0;
|
|
1032
|
+
this.#remotePullRetries.set(publication.publicationId, { retryAfter, failures });
|
|
1033
|
+
}
|
|
891
1034
|
}
|
|
892
1035
|
async #pullBatch(publications, generation) {
|
|
893
1036
|
if (publications.length === 0) return [];
|
|
@@ -913,12 +1056,12 @@ var CloudflareSFUClient = class {
|
|
|
913
1056
|
connection.addEventListener("track", onTrack);
|
|
914
1057
|
try {
|
|
915
1058
|
this.#requireGeneration(generation);
|
|
916
|
-
const response = await (() => this.#
|
|
1059
|
+
const response = await this.#signal(() => this.#addTracks({ connectionId, tracks: requested, allowPartialRemoteTracks: true }), { generation, connectionId });
|
|
917
1060
|
this.#requireGeneration(generation);
|
|
918
1061
|
this.#recordRemotePullErrors(publications, response.trackErrors ?? []);
|
|
919
1062
|
const responseTracks = response.tracks ?? [];
|
|
920
1063
|
const matched = matchRemotePullTracks(publications, responseTracks);
|
|
921
|
-
for (const { publication } of matched) this.#
|
|
1064
|
+
for (const { publication } of matched) this.#remotePullRetries.delete(publication.publicationId);
|
|
922
1065
|
await this.#completeRenegotiation(response, connection, connectionId, generation);
|
|
923
1066
|
if (matched.length === 0) return [];
|
|
924
1067
|
await this.#waitForConnection(connection, generation);
|
|
@@ -948,8 +1091,10 @@ var CloudflareSFUClient = class {
|
|
|
948
1091
|
for (const failure of failures) {
|
|
949
1092
|
const publication = requested.get(`${failure.connectionId}\0${failure.trackName}`);
|
|
950
1093
|
if (!publication) throw new CloudflareSFUError("Cloudflare SFU returned an unrequested remote track error", "invalid_publication");
|
|
951
|
-
const
|
|
952
|
-
|
|
1094
|
+
const previous = this.#remotePullRetries.get(publication.publicationId) ?? { retryAfter: 0, failures: 0 };
|
|
1095
|
+
const failureCount = Math.min(previous.failures + 1, 6);
|
|
1096
|
+
const delay = Math.min(15e3, 750 * 2 ** (failureCount - 1));
|
|
1097
|
+
this.#remotePullRetries.set(publication.publicationId, { retryAfter: Date.now() + delay, failures: failureCount });
|
|
953
1098
|
this.#reportError(new CloudflareSFUError(`Cloudflare SFU remote track failed: ${failure.code}`, "media_failed", { providerCode: failure.code }));
|
|
954
1099
|
}
|
|
955
1100
|
}
|
|
@@ -958,13 +1103,13 @@ var CloudflareSFUClient = class {
|
|
|
958
1103
|
await this.#applyProviderDescription(response, connection);
|
|
959
1104
|
const answer = await (() => connection.createAnswer())();
|
|
960
1105
|
await (() => connection.setLocalDescription(answer))();
|
|
961
|
-
await (() => this.#requireTransport().renegotiate({ connectionId,
|
|
1106
|
+
await this.#signal(() => this.#requireTransport().renegotiate({ connectionId, sessionDescription: requireDescription(answer) }), { generation, connectionId });
|
|
962
1107
|
this.#requireGeneration(generation);
|
|
963
1108
|
}
|
|
964
1109
|
async #applyProviderDescription(response, connection = this.#connection) {
|
|
965
1110
|
await (() => connection.setRemoteDescription(requireSFUDescription(response.sessionDescription)))();
|
|
966
1111
|
}
|
|
967
|
-
#serializeSDP(operation) {
|
|
1112
|
+
#serializeSDP(operation, retirement = false) {
|
|
968
1113
|
const generation = this.#generation;
|
|
969
1114
|
let expired = false;
|
|
970
1115
|
let timer;
|
|
@@ -977,7 +1122,7 @@ var CloudflareSFUClient = class {
|
|
|
977
1122
|
}, NEGOTIATION_TIMEOUT_MS);
|
|
978
1123
|
});
|
|
979
1124
|
const run = () => {
|
|
980
|
-
if (expired || generation !== this.#generation) throw timeout;
|
|
1125
|
+
if (expired || !retirement && generation !== this.#generation) throw timeout;
|
|
981
1126
|
return operation();
|
|
982
1127
|
};
|
|
983
1128
|
const pending = this.#sdpTail.then(run, run);
|
|
@@ -1100,9 +1245,14 @@ var CloudflareSFUClient = class {
|
|
|
1100
1245
|
}
|
|
1101
1246
|
async #replaceDormantConnectionBeforeNegotiation(generation) {
|
|
1102
1247
|
if (!this.#started || this.#negotiatedGeneration === generation || this.#replacementAttemptedGeneration === generation || !this.#replaceMediaConnection) return;
|
|
1103
|
-
await this.#replaceMediaConnectionForRecovery(generation);
|
|
1248
|
+
await this.#replaceMediaConnectionForRecovery(generation, "dormant_connection");
|
|
1249
|
+
}
|
|
1250
|
+
#addTracks(input) {
|
|
1251
|
+
const replacementCause = this.#replacementCause;
|
|
1252
|
+
this.#replacementCause = void 0;
|
|
1253
|
+
return this.#requireTransport().addTracks({ ...input, replacementCause });
|
|
1104
1254
|
}
|
|
1105
|
-
async #replaceMediaConnectionForRecovery(generation) {
|
|
1255
|
+
async #replaceMediaConnectionForRecovery(generation, cause) {
|
|
1106
1256
|
this.#requireGeneration(generation);
|
|
1107
1257
|
if (!this.#replaceMediaConnection || this.#replacementAttemptedGeneration === generation) {
|
|
1108
1258
|
throw new CloudflareSFUError("A fresh Cloudflare SFU connection is unavailable", "signaling_failed");
|
|
@@ -1115,14 +1265,14 @@ var CloudflareSFUClient = class {
|
|
|
1115
1265
|
if (bootstrap.connectionId === this.#bootstrap.connectionId) {
|
|
1116
1266
|
throw new CloudflareSFUError("Participant access did not replace the Cloudflare SFU connection", "signaling_failed");
|
|
1117
1267
|
}
|
|
1268
|
+
this.#fenceLocalPublications();
|
|
1118
1269
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
1119
1270
|
this.#disposeConnection(false);
|
|
1120
|
-
this.#reusableLocalTransceivers.clear();
|
|
1121
|
-
this.#reusableLocalPublicationIds.clear();
|
|
1122
1271
|
this.#clearRemoteTracks();
|
|
1123
1272
|
this.#cursor = null;
|
|
1124
1273
|
this.#bootstrap = bootstrap;
|
|
1125
1274
|
this.#rebuildPeerConnection(bootstrap, generation, connectionEpoch);
|
|
1275
|
+
this.#replacementCause = cause;
|
|
1126
1276
|
this.#retiredLocalConnection = null;
|
|
1127
1277
|
this.#negotiatedGeneration = null;
|
|
1128
1278
|
for (const state of this.#localTracks.values()) {
|
|
@@ -1190,8 +1340,6 @@ var CloudflareSFUClient = class {
|
|
|
1190
1340
|
state.desiredEnabled = false;
|
|
1191
1341
|
state.transceiver = null;
|
|
1192
1342
|
state.providerPublicationId = null;
|
|
1193
|
-
state.pendingOperationId = null;
|
|
1194
|
-
state.pendingTrackName = null;
|
|
1195
1343
|
state.endedListener = null;
|
|
1196
1344
|
}
|
|
1197
1345
|
#clearRemoteTracks() {
|
|
@@ -1271,7 +1419,7 @@ var CloudflareSFUClient = class {
|
|
|
1271
1419
|
}
|
|
1272
1420
|
#setFailure(error, fallback) {
|
|
1273
1421
|
const code = error instanceof CloudflareSFUError ? error.code : fallback;
|
|
1274
|
-
this.#publishSnapshot("failed", { code, recoverable: code !== "invalid_bootstrap" && code !== "invalid_target" && code !== "invalid_publication" });
|
|
1422
|
+
this.#publishSnapshot("failed", { code, recoverable: code !== "invalid_bootstrap" && code !== "invalid_target" && code !== "invalid_publication", diagnostic: classifyMediaFailure(error, "peer_connection") });
|
|
1275
1423
|
this.#reportError(error);
|
|
1276
1424
|
}
|
|
1277
1425
|
#emitLocal() {
|
|
@@ -1290,19 +1438,29 @@ var CloudflareSFUClient = class {
|
|
|
1290
1438
|
}
|
|
1291
1439
|
}
|
|
1292
1440
|
#reportError(error) {
|
|
1441
|
+
const owner = error instanceof Error ? this.#failureOwners.get(error) : void 0;
|
|
1442
|
+
if (owner && (owner.generation !== this.#generation || owner.connectionId !== this.#bootstrap.connectionId)) return;
|
|
1293
1443
|
try {
|
|
1294
|
-
this.#onError?.(error);
|
|
1444
|
+
this.#onError?.(error, owner?.connectionId ?? this.#bootstrap.connectionId);
|
|
1295
1445
|
} catch {
|
|
1296
1446
|
}
|
|
1297
1447
|
}
|
|
1448
|
+
async #signal(operation, owner = { generation: this.#generation, connectionId: this.#bootstrap.connectionId }) {
|
|
1449
|
+
try {
|
|
1450
|
+
return await operation();
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (error instanceof Error) this.#failureOwners.set(error, owner);
|
|
1453
|
+
throw error;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1298
1456
|
#requireGeneration(generation) {
|
|
1299
|
-
if (generation !== this.#generation || this.#stopped) throw new CloudflareSFUError("
|
|
1457
|
+
if (generation !== this.#generation || this.#stopped) throw new CloudflareSFUError("Stale media connection", "stale_generation");
|
|
1300
1458
|
}
|
|
1301
1459
|
#requireActive() {
|
|
1302
|
-
if (this.#stopped) throw new CloudflareSFUError("
|
|
1460
|
+
if (this.#stopped) throw new CloudflareSFUError("SFU client stopped", "media_failed");
|
|
1303
1461
|
}
|
|
1304
1462
|
#requireTransport() {
|
|
1305
|
-
if (!this.#transport) throw new CloudflareSFUError("
|
|
1463
|
+
if (!this.#transport) throw new CloudflareSFUError("SFU signaling unavailable", "signaling_failed");
|
|
1306
1464
|
return this.#transport;
|
|
1307
1465
|
}
|
|
1308
1466
|
};
|
|
@@ -1391,8 +1549,13 @@ function publicationEqual(left, right) {
|
|
|
1391
1549
|
function remotePublicationEqual(left, right) {
|
|
1392
1550
|
return right !== void 0 && left.participantId === right.participantId && left.source === right.source && left.publicationId === right.publicationId && left.track === right.track;
|
|
1393
1551
|
}
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1552
|
+
async function createLocalOffer(connection) {
|
|
1553
|
+
const offer = await connection.createOffer();
|
|
1554
|
+
await connection.setLocalDescription(offer);
|
|
1555
|
+
return requireDescription(offer);
|
|
1556
|
+
}
|
|
1557
|
+
function localSourceGroup(source) {
|
|
1558
|
+
return source === "screen" ? [source, "screen_audio"] : [source];
|
|
1396
1559
|
}
|
|
1397
1560
|
function isRateLimited(error) {
|
|
1398
1561
|
return error instanceof CloudflareSFUError && error.options.status === 429;
|
|
@@ -1425,31 +1588,43 @@ function createCloudflareSFUHTTPTransport(options) {
|
|
|
1425
1588
|
const timeoutMs = options.requestTimeoutMs ?? SIGNALING_TIMEOUT_MS;
|
|
1426
1589
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new CloudflareSFUError("The signaling deadline is invalid", "signaling_failed");
|
|
1427
1590
|
const request = async (path, init) => {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
} catch {
|
|
1441
|
-
}
|
|
1442
|
-
throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
|
|
1443
|
-
status: response.status,
|
|
1444
|
-
retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
|
|
1591
|
+
try {
|
|
1592
|
+
const token = await withDeadline(CREDENTIAL_TIMEOUT_MS, (signal) => credential(signal));
|
|
1593
|
+
if (!token.trim()) throw new CloudflareSFUError("The media credential provider returned an empty token", "signaling_failed");
|
|
1594
|
+
return await withDeadline(timeoutMs, async (signal) => {
|
|
1595
|
+
const startedAt = performance.now();
|
|
1596
|
+
const response = await fetch(`${mediaPath}/${path}`, {
|
|
1597
|
+
...init,
|
|
1598
|
+
signal,
|
|
1599
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...options.traceparent ? { traceparent: options.traceparent } : {}, ...init?.headers }
|
|
1600
|
+
}).catch((error) => {
|
|
1601
|
+
markMediaFetchFailure(error);
|
|
1602
|
+
throw error;
|
|
1445
1603
|
});
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1604
|
+
if (!response.ok) {
|
|
1605
|
+
try {
|
|
1606
|
+
options.onDiagnostic?.({ event: "media_http_error", http_status: response.status, duration_ms: performance.now() - startedAt });
|
|
1607
|
+
} catch {
|
|
1608
|
+
}
|
|
1609
|
+
throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
|
|
1610
|
+
status: response.status,
|
|
1611
|
+
retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
return await response.json();
|
|
1615
|
+
});
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
const step = path === "publications" ? "sfu_publications" : path === "tracks/close" ? "sfu_close_tracks" : path === "renegotiate" ? "sfu_renegotiate" : "sfu_tracks";
|
|
1618
|
+
markMediaFailure(error, step);
|
|
1619
|
+
throw error;
|
|
1620
|
+
}
|
|
1449
1621
|
};
|
|
1450
1622
|
return {
|
|
1451
1623
|
addTracks: async (input) => {
|
|
1452
|
-
const
|
|
1624
|
+
const query = new URLSearchParams();
|
|
1625
|
+
if (input.allowPartialRemoteTracks) query.set("allow_partial_remote_tracks", "true");
|
|
1626
|
+
if (input.replacementCause) query.set("replacement_cause", input.replacementCause);
|
|
1627
|
+
const path = query.size ? `tracks?${query}` : "tracks";
|
|
1453
1628
|
const response = await request(path, {
|
|
1454
1629
|
method: "POST",
|
|
1455
1630
|
body: JSON.stringify({ connection_id: input.connectionId, session_description: input.sessionDescription, tracks: input.tracks })
|
|
@@ -1538,7 +1713,6 @@ function describeMediaCaptureError(cause) {
|
|
|
1538
1713
|
}
|
|
1539
1714
|
|
|
1540
1715
|
export {
|
|
1541
|
-
describeMediaCaptureError,
|
|
1542
1716
|
CloudflareSFUError,
|
|
1543
1717
|
parseCloudflareSFUPublicationID,
|
|
1544
1718
|
requireDescription,
|
|
@@ -1547,6 +1721,10 @@ export {
|
|
|
1547
1721
|
comparePublicationCursor,
|
|
1548
1722
|
publicationKey,
|
|
1549
1723
|
waitFor,
|
|
1724
|
+
grantedMediaFromStream,
|
|
1725
|
+
markMediaFailure,
|
|
1726
|
+
classifyMediaFailure,
|
|
1550
1727
|
CloudflareSFUClient,
|
|
1551
|
-
createCloudflareSFUHTTPTransport
|
|
1728
|
+
createCloudflareSFUHTTPTransport,
|
|
1729
|
+
describeMediaCaptureError
|
|
1552
1730
|
};
|