@q9labsai/chalk-client 5.2.5 → 5.3.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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/dist/{chunk-FD25AMLG.js → chunk-3TGVTP6Z.js} +364 -79
- package/dist/{chunk-BUNSEDXL.js → chunk-DHUHTIU7.js} +213 -64
- package/dist/{chunk-UMAE5GCV.js → chunk-GOSWSXTH.js} +21 -1
- package/dist/{chunk-LKNT6XFD.js → chunk-O26BHHMI.js} +1 -1
- package/dist/{chunk-5YPQNMJE.js → chunk-XSB2GDRW.js} +32 -8
- package/dist/client.d.ts +2 -0
- package/dist/connection/dependencies.d.ts +2 -1
- package/dist/connection/production.d.ts +1 -1
- package/dist/effect.js +4 -4
- package/dist/generated/http-api.d.ts +2 -0
- package/dist/generated/schemas.d.ts +3 -0
- package/dist/generated/sync.d.ts +45 -0
- package/dist/index.js +5 -5
- package/dist/media/client.d.ts +6 -5
- package/dist/media/index.js +1 -1
- package/dist/media/plane.d.ts +9 -3
- package/dist/media/receive-stall.d.ts +43 -0
- package/dist/media/types.d.ts +4 -1
- package/dist/public-invites/index.js +2 -2
- package/dist/server/index.cjs +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/types.d.cts +2 -0
- package/dist/server/types.d.ts +2 -0
- package/dist/space-client/promise-facade.d.ts +5 -5
- package/dist/space-client/same-identity.d.ts +6 -0
- package/dist/space-client/types.d.ts +6 -0
- package/dist/sync/index.js +1 -1
- package/dist/sync/v1-types.d.ts +6 -0
- package/package.json +2 -1
|
@@ -1,3 +1,145 @@
|
|
|
1
|
+
// src/media/receive-stall.ts
|
|
2
|
+
var RECEIVE_STALL_WINDOW_MS = 7e3;
|
|
3
|
+
var RECEIVE_STARTUP_GRACE_MS = 2e4;
|
|
4
|
+
var MAX_SAMPLE_GAP_MS = 3e3;
|
|
5
|
+
var ReceiveStallDetector = class {
|
|
6
|
+
#progress = /* @__PURE__ */ new Map();
|
|
7
|
+
#sampledAt;
|
|
8
|
+
forget(publicationId) {
|
|
9
|
+
this.#progress.delete(publicationId);
|
|
10
|
+
}
|
|
11
|
+
clear() {
|
|
12
|
+
this.#progress.clear();
|
|
13
|
+
this.#sampledAt = void 0;
|
|
14
|
+
}
|
|
15
|
+
sample(now, tracks) {
|
|
16
|
+
if (this.#sampledAt !== void 0 && (now - this.#sampledAt > MAX_SAMPLE_GAP_MS || now < this.#sampledAt)) this.clear();
|
|
17
|
+
this.#sampledAt = now;
|
|
18
|
+
const observed = new Set(tracks.map((track) => track.publicationId));
|
|
19
|
+
for (const id of this.#progress.keys()) if (!observed.has(id)) this.#progress.delete(id);
|
|
20
|
+
const observations = tracks.map((track) => ({ id: track.publicationId, outcome: this.#observeTrack(now, track) }));
|
|
21
|
+
return {
|
|
22
|
+
stalled: observations.filter((track) => track.outcome === "stalled").map((track) => track.id),
|
|
23
|
+
resumed: observations.filter((track) => track.outcome === "resumed").map((track) => track.id),
|
|
24
|
+
advanced: observations.filter((track) => track.outcome === "advanced" || track.outcome === "resumed").map((track) => track.id)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
#observeTrack(now, { publicationId, trackId, packets }) {
|
|
28
|
+
const previous = this.#progress.get(publicationId);
|
|
29
|
+
const sameTrack = previous?.trackId === trackId;
|
|
30
|
+
if (sameTrack && previous.packets === packets) {
|
|
31
|
+
const window = previous.flowing ? RECEIVE_STALL_WINDOW_MS : RECEIVE_STARTUP_GRACE_MS;
|
|
32
|
+
return now - previous.advancedAt >= window ? "stalled" : null;
|
|
33
|
+
}
|
|
34
|
+
return this.#advanceTrack(now, { publicationId, trackId, packets }, previous);
|
|
35
|
+
}
|
|
36
|
+
#advanceTrack(now, { publicationId, trackId, packets }, previous) {
|
|
37
|
+
const advanced = previous?.trackId === trackId && packets > previous.packets;
|
|
38
|
+
const flowingSince = advanced && now - previous.advancedAt < RECEIVE_STALL_WINDOW_MS ? previous.flowingSince : now;
|
|
39
|
+
this.#progress.set(publicationId, { trackId, packets, advancedAt: now, flowing: advanced, flowingSince });
|
|
40
|
+
if (!advanced) return null;
|
|
41
|
+
return now - flowingSince >= RECEIVE_STALL_WINDOW_MS ? "resumed" : "advanced";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var MAX_RECEIVE_REBUILDS = 2;
|
|
45
|
+
var MAX_RECEIVE_OBSERVATIONS = 32;
|
|
46
|
+
var ReceiveStallRecovery = class {
|
|
47
|
+
#detector = new ReceiveStallDetector();
|
|
48
|
+
#attempts = /* @__PURE__ */ new Map();
|
|
49
|
+
#identities = /* @__PURE__ */ new Map();
|
|
50
|
+
#expected = /* @__PURE__ */ new Set();
|
|
51
|
+
#screens = /* @__PURE__ */ new Set();
|
|
52
|
+
#stalled = /* @__PURE__ */ new Set();
|
|
53
|
+
#active = [];
|
|
54
|
+
#observations = Object.freeze([]);
|
|
55
|
+
get observations() {
|
|
56
|
+
return this.#observations;
|
|
57
|
+
}
|
|
58
|
+
expect(publications, participantId) {
|
|
59
|
+
const enabled = publications.filter((publication) => publication.participantId !== participantId && publication.enabled);
|
|
60
|
+
this.#expected = new Set(enabled.flatMap((publication) => publication.publicationId ? [publication.publicationId] : []));
|
|
61
|
+
this.#screens = new Set(enabled.filter((publication) => publication.source === "screen").map((publication) => publication.participantId));
|
|
62
|
+
this.#pruneWithdrawn(publications);
|
|
63
|
+
for (const publication of this.#identities.values()) {
|
|
64
|
+
if (this.#isExpected(publication)) continue;
|
|
65
|
+
this.#detector.forget(publication.publicationId);
|
|
66
|
+
this.#stalled.delete(publication.publicationId);
|
|
67
|
+
}
|
|
68
|
+
this.#active = [...this.#identities.values()].filter((publication) => this.#isExpected(publication)).map((publication) => publication.publicationId);
|
|
69
|
+
}
|
|
70
|
+
suspend() {
|
|
71
|
+
this.#expected.clear();
|
|
72
|
+
this.#screens.clear();
|
|
73
|
+
this.clear();
|
|
74
|
+
}
|
|
75
|
+
#pruneWithdrawn(publications) {
|
|
76
|
+
const advertised = new Set(publications.map((publication) => publication.publicationId));
|
|
77
|
+
const screens = new Set(publications.filter((publication) => publication.source === "screen").map((publication) => publication.participantId));
|
|
78
|
+
for (const [key, publication] of this.#identities) {
|
|
79
|
+
if (advertised.has(publication.publicationId) || publication.source === "screen_audio" && screens.has(publication.participantId)) continue;
|
|
80
|
+
this.#forget(publication.publicationId);
|
|
81
|
+
this.#identities.delete(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
isStalled(publicationId) {
|
|
85
|
+
return this.#stalled.has(publicationId);
|
|
86
|
+
}
|
|
87
|
+
canRebuild() {
|
|
88
|
+
return this.#active.length > 0 && this.#active.every((id) => this.#stalled.has(id) && (this.#attempts.get(id) ?? 0) < MAX_RECEIVE_REBUILDS);
|
|
89
|
+
}
|
|
90
|
+
reserveRebuild() {
|
|
91
|
+
if (!this.canRebuild()) return false;
|
|
92
|
+
for (const id of this.#active) this.#attempts.set(id, (this.#attempts.get(id) ?? 0) + 1);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
clear() {
|
|
96
|
+
this.#detector.clear();
|
|
97
|
+
this.#stalled.clear();
|
|
98
|
+
this.#active = [];
|
|
99
|
+
}
|
|
100
|
+
sample(now, connection, report, tracks) {
|
|
101
|
+
const active = [...tracks].filter((publication) => this.#isExpected(publication));
|
|
102
|
+
for (const publication of active) this.#rememberIdentity(publication);
|
|
103
|
+
this.#active = active.map((publication) => publication.publicationId);
|
|
104
|
+
const samples = active.flatMap((publication) => receivePacketSample(connection, report, publication) ?? []);
|
|
105
|
+
const { stalled, advanced, resumed } = this.#detector.sample(now, samples);
|
|
106
|
+
for (const id of resumed) this.#attempts.delete(id);
|
|
107
|
+
const newlyStalled = stalled.filter((id) => !this.#stalled.has(id));
|
|
108
|
+
this.#stalled = new Set(stalled);
|
|
109
|
+
if (newlyStalled.length) this.#record(now);
|
|
110
|
+
return { stalled, advanced, newlyStalled };
|
|
111
|
+
}
|
|
112
|
+
#isExpected(publication) {
|
|
113
|
+
return publication.source === "screen_audio" ? this.#screens.has(publication.participantId) : this.#expected.has(publication.publicationId);
|
|
114
|
+
}
|
|
115
|
+
#rememberIdentity(publication) {
|
|
116
|
+
const key = `${publication.participantId}\0${publication.source}`;
|
|
117
|
+
const previous = this.#identities.get(key);
|
|
118
|
+
if (previous && previous.publicationId !== publication.publicationId) this.#forget(previous.publicationId);
|
|
119
|
+
this.#identities.set(key, publication);
|
|
120
|
+
}
|
|
121
|
+
#forget(id) {
|
|
122
|
+
this.#detector.forget(id);
|
|
123
|
+
this.#stalled.delete(id);
|
|
124
|
+
this.#attempts.delete(id);
|
|
125
|
+
}
|
|
126
|
+
#record(now) {
|
|
127
|
+
const observation = Object.freeze({ event: "receive_stalled", scope: this.#active.every((id) => this.#stalled.has(id)) ? "all" : "partial", observedAtMs: now, expectedTracks: this.#active.length, stalledTracks: this.#stalled.size });
|
|
128
|
+
this.#observations = Object.freeze([...this.#observations.slice(-(MAX_RECEIVE_OBSERVATIONS - 1)), observation]);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
function receivePacketSample(connection, report, publication) {
|
|
132
|
+
const rows = [...report.values()].filter((row) => row.type === "inbound-rtp" && row.trackIdentifier === publication.track.id);
|
|
133
|
+
if (rows.some((row) => !Number.isSafeInteger(row.packetsReceived) || row.packetsReceived < 0)) return;
|
|
134
|
+
if (publication.track.kind === "audio" && !audioPacketCadenceKnown(connection, report, publication.track, rows)) return;
|
|
135
|
+
return { publicationId: publication.publicationId, trackId: publication.track.id, packets: rows.reduce((sum, row) => sum + row.packetsReceived, 0) };
|
|
136
|
+
}
|
|
137
|
+
function audioPacketCadenceKnown(connection, report, track, rows) {
|
|
138
|
+
const negotiated = rows.length ? rows.map((row) => row.codecId ? report.get(row.codecId) : void 0) : connection.getReceivers().find((receiver) => receiver.track.id === track.id)?.getParameters().codecs ?? [];
|
|
139
|
+
const opus = negotiated.filter((codec) => codec?.mimeType?.toLowerCase() === "audio/opus");
|
|
140
|
+
return opus.length > 0 && opus.every((codec) => typeof codec.sdpFmtpLine === "string");
|
|
141
|
+
}
|
|
142
|
+
|
|
1
143
|
// src/media/camera-uplink.ts
|
|
2
144
|
var CameraUplinkPolicy = class {
|
|
3
145
|
#healthySamples = 0;
|
|
@@ -234,10 +376,12 @@ function classifyMediaFailure(cause, fallbackStep) {
|
|
|
234
376
|
// src/media/client.ts
|
|
235
377
|
var EMPTY_LOCAL = Object.freeze([]);
|
|
236
378
|
var EMPTY_REMOTE = Object.freeze([]);
|
|
379
|
+
var EMPTY_RECEIVE_STALLS = Object.freeze([]);
|
|
237
380
|
var INVALID_PUBLICATION_SIGNATURE = "\0invalid";
|
|
238
381
|
var CONNECTION_TIMEOUT_MS = 8e3;
|
|
239
382
|
var NEGOTIATION_TIMEOUT_MS = 35e3;
|
|
240
383
|
var PEER_OPERATION_TIMEOUT_MS = 8e3;
|
|
384
|
+
var CAPTURE_REUSE_WINDOW_MS = 2e4;
|
|
241
385
|
var CloudflareSFUClient = class {
|
|
242
386
|
#localListeners = /* @__PURE__ */ new Set();
|
|
243
387
|
#onError;
|
|
@@ -250,7 +394,7 @@ var CloudflareSFUClient = class {
|
|
|
250
394
|
#remoteListeners = /* @__PURE__ */ new Set();
|
|
251
395
|
#snapshotListeners = /* @__PURE__ */ new Set();
|
|
252
396
|
#failureOwners = /* @__PURE__ */ new WeakMap();
|
|
253
|
-
#locallyPausedSources = /* @__PURE__ */ new
|
|
397
|
+
#locallyPausedSources = /* @__PURE__ */ new Map();
|
|
254
398
|
#localTracks = /* @__PURE__ */ new Map();
|
|
255
399
|
#ownedPublications = /* @__PURE__ */ new Set();
|
|
256
400
|
#remoteTracks = /* @__PURE__ */ new Map();
|
|
@@ -259,9 +403,12 @@ var CloudflareSFUClient = class {
|
|
|
259
403
|
#disposeConnectionObservation;
|
|
260
404
|
#connectionEpoch = 0;
|
|
261
405
|
#retiredLocalConnection = null;
|
|
406
|
+
#retiredLocalCause = "forced_pause";
|
|
407
|
+
#replacementCause;
|
|
262
408
|
#cursor = null;
|
|
263
409
|
#remotePullIncomplete = false;
|
|
264
|
-
#
|
|
410
|
+
#remotePullRetries = /* @__PURE__ */ new Map();
|
|
411
|
+
#receiveProgress = new ReceiveStallRecovery();
|
|
265
412
|
#remoteParticipantIds = null;
|
|
266
413
|
#remoteParticipantRevision = 0;
|
|
267
414
|
#subscribedPublicationIds = /* @__PURE__ */ new Set();
|
|
@@ -278,6 +425,7 @@ var CloudflareSFUClient = class {
|
|
|
278
425
|
#retirement = Promise.resolve();
|
|
279
426
|
#snapshot;
|
|
280
427
|
#started = false;
|
|
428
|
+
#receivingInitial = false;
|
|
281
429
|
#stopped = false;
|
|
282
430
|
#transport;
|
|
283
431
|
constructor(options) {
|
|
@@ -297,6 +445,7 @@ var CloudflareSFUClient = class {
|
|
|
297
445
|
cursor: null,
|
|
298
446
|
localTracks: EMPTY_LOCAL,
|
|
299
447
|
remoteTracks: EMPTY_REMOTE,
|
|
448
|
+
receiveStalls: this.#receiveProgress.observations,
|
|
300
449
|
failure: null
|
|
301
450
|
});
|
|
302
451
|
this.#observeConnection(this.#connection, this.#generation, this.#connectionEpoch);
|
|
@@ -308,7 +457,7 @@ var CloudflareSFUClient = class {
|
|
|
308
457
|
return subscribeSnapshot(this.#snapshotListeners, listener);
|
|
309
458
|
}
|
|
310
459
|
remotePublicationResumed(publicationId) {
|
|
311
|
-
this.#
|
|
460
|
+
this.#remotePullRetries.delete(publicationId);
|
|
312
461
|
this.remotePublicationsChanged();
|
|
313
462
|
}
|
|
314
463
|
remoteParticipantsChanged(participantIds) {
|
|
@@ -317,7 +466,9 @@ var CloudflareSFUClient = class {
|
|
|
317
466
|
this.#remotePullIncomplete = true;
|
|
318
467
|
this.remotePublicationsChanged();
|
|
319
468
|
}
|
|
320
|
-
remotePublicationsChanged() {
|
|
469
|
+
remotePublicationsChanged(publications, projectionState = "available") {
|
|
470
|
+
if (projectionState === "unavailable") this.#receiveProgress.suspend();
|
|
471
|
+
else if (publications) this.#receiveProgress.expect(publications, this.#participantId);
|
|
321
472
|
if (this.#polling) {
|
|
322
473
|
this.#pollAfterCurrent = true;
|
|
323
474
|
return;
|
|
@@ -363,9 +514,9 @@ var CloudflareSFUClient = class {
|
|
|
363
514
|
}
|
|
364
515
|
async #clearPreparedTrack(state, stopTrack = true) {
|
|
365
516
|
const source = state.source;
|
|
366
|
-
if (source === "screen" || source === "screen_audio"
|
|
517
|
+
if (source === "screen" || source === "screen_audio") {
|
|
367
518
|
try {
|
|
368
|
-
this.#pauseLocalSource(source);
|
|
519
|
+
this.#pauseLocalSource(source, "capture_cleanup");
|
|
369
520
|
await this.#retireOwnedPublications();
|
|
370
521
|
} finally {
|
|
371
522
|
this.#removeOwnedLocalTrack(state, stopTrack);
|
|
@@ -382,20 +533,30 @@ var CloudflareSFUClient = class {
|
|
|
382
533
|
this.#publishSnapshot();
|
|
383
534
|
this.#emitLocal();
|
|
384
535
|
}
|
|
385
|
-
async start(localMedia) {
|
|
536
|
+
async start(localMedia, beforePublish) {
|
|
386
537
|
if (this.#started) return;
|
|
387
538
|
this.#requireActive();
|
|
388
|
-
const tracks = localMedia.getTracks().filter((track) => track.kind === "audio" || track.kind === "video");
|
|
389
|
-
for (const track of tracks) this.prepareLocalTrack(track.kind === "audio" ? "microphone" : "camera", track);
|
|
390
539
|
const generation = this.#generation;
|
|
391
540
|
this.#setPhase("connecting", null);
|
|
541
|
+
if (beforePublish) {
|
|
542
|
+
this.#receivingInitial = true;
|
|
543
|
+
this.#schedulePoll(0);
|
|
544
|
+
try {
|
|
545
|
+
localMedia = await beforePublish();
|
|
546
|
+
} finally {
|
|
547
|
+
this.#receivingInitial = false;
|
|
548
|
+
}
|
|
549
|
+
if (generation !== this.#generation || this.#stopped) return;
|
|
550
|
+
}
|
|
551
|
+
const tracks = localMedia.getTracks().filter((track) => track.kind === "audio" || track.kind === "video");
|
|
552
|
+
for (const track of tracks) this.prepareLocalTrack(track.kind === "audio" ? "microphone" : "camera", track);
|
|
392
553
|
await this.#activatePreparedTracks(
|
|
393
554
|
[...this.#localTracks.values()].filter((state) => state.source === "microphone" || state.source === "camera"),
|
|
394
555
|
generation
|
|
395
556
|
);
|
|
396
557
|
}
|
|
397
558
|
async refreshRemotePublications() {
|
|
398
|
-
if (!this.#started || this.#stopped || this.#polling) return;
|
|
559
|
+
if (!this.#started && !this.#receivingInitial || this.#stopped || this.#polling) return;
|
|
399
560
|
this.#polling = true;
|
|
400
561
|
const generation = this.#generation;
|
|
401
562
|
this.#pollFailedPull = false;
|
|
@@ -431,6 +592,10 @@ var CloudflareSFUClient = class {
|
|
|
431
592
|
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
432
593
|
if (resolved.kind === "result" && resolved.result.outcome !== "satisfied") return resolved.result;
|
|
433
594
|
const outcome = resolved.kind === "state" && resolved.value.enabled ? "confirmed" : "satisfied";
|
|
595
|
+
if (target.source !== "screen" && this.#locallyPausedSources.get(target.source) === "capture_cleanup") {
|
|
596
|
+
await this.pauseLocalCapture(target.source);
|
|
597
|
+
return { outcome, errorCode: null };
|
|
598
|
+
}
|
|
434
599
|
this.#pauseLocalSource(target.source);
|
|
435
600
|
try {
|
|
436
601
|
await this.#retireOwnedPublications();
|
|
@@ -449,29 +614,56 @@ var CloudflareSFUClient = class {
|
|
|
449
614
|
await this.#retireUnavailableLocalPublication(state);
|
|
450
615
|
if (this.#locallyPausedSources.has(target.source)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
451
616
|
if (state.enabled) return { outcome: "satisfied", errorCode: null };
|
|
452
|
-
await this.#enablePreparedTrack(state);
|
|
617
|
+
if (!await this.#enablePreparedTrack(state)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
453
618
|
return { outcome: "confirmed", errorCode: null };
|
|
454
619
|
} catch (error) {
|
|
455
620
|
if (generation === this.#generation && !this.#stopped) this.#reportError(error);
|
|
456
621
|
return mediaTargetFailure(error);
|
|
457
622
|
}
|
|
458
623
|
}
|
|
459
|
-
|
|
624
|
+
async pauseLocalCapture(source) {
|
|
625
|
+
this.#locallyPausedSources.set(source, "capture_cleanup");
|
|
626
|
+
const state = this.#localTracks.get(source);
|
|
627
|
+
if (state) await this.#disablePreparedTrack(state);
|
|
628
|
+
}
|
|
629
|
+
setLocalSourceIntent(source, enabled, cause = "forced_pause") {
|
|
460
630
|
const sources = localSourceGroup(source);
|
|
461
631
|
for (const item of sources) {
|
|
462
632
|
if (enabled) this.#locallyPausedSources.delete(item);
|
|
463
|
-
else this.#locallyPausedSources.
|
|
633
|
+
else this.#locallyPausedSources.set(item, cause);
|
|
464
634
|
}
|
|
465
|
-
if (!enabled) this.#pauseLocalSource(source);
|
|
635
|
+
if (!enabled) this.#pauseLocalSource(source, cause);
|
|
466
636
|
return enabled ? Promise.resolve() : this.#retireOwnedPublications();
|
|
467
637
|
}
|
|
468
|
-
async closeForcedLocalPublication(source) {
|
|
638
|
+
async closeForcedLocalPublication(source, cause = "forced_pause") {
|
|
639
|
+
const connection = this.#connection;
|
|
640
|
+
const generationAtClose = this.#generation;
|
|
469
641
|
const sources = localSourceGroup(source);
|
|
470
642
|
const ownsPublication = [...this.#ownedPublications].some((item) => sources.includes(item.state.source));
|
|
471
|
-
|
|
472
|
-
|
|
643
|
+
const canRecover = ownsPublication && this.#replaceMediaConnection !== void 0;
|
|
644
|
+
let failedRetirement;
|
|
645
|
+
try {
|
|
646
|
+
await this.setLocalSourceIntent(source, false, cause);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (!canRecover) throw error;
|
|
649
|
+
failedRetirement = this.#fenceFailedClose(error, connection, generationAtClose, cause);
|
|
650
|
+
if (!failedRetirement) return;
|
|
651
|
+
}
|
|
652
|
+
if (!canRecover) return;
|
|
653
|
+
await this.#restoreEnabledSourcesAfterClose(source);
|
|
654
|
+
if (this.#retirement !== failedRetirement) await this.#retirement;
|
|
655
|
+
}
|
|
656
|
+
#fenceFailedClose(error, connection, generation, cause) {
|
|
657
|
+
const failures = error instanceof AggregateError ? error.errors : [error];
|
|
658
|
+
if (!failures.every((failure) => this.#isRetryableConnectionFailure(failure))) throw error;
|
|
659
|
+
if (connection !== this.#connection || generation !== this.#generation) return;
|
|
660
|
+
this.#retiredLocalConnection = connection;
|
|
661
|
+
this.#retiredLocalCause = cause;
|
|
662
|
+
return this.#retirement;
|
|
663
|
+
}
|
|
664
|
+
#restoreEnabledSourcesAfterClose(source) {
|
|
473
665
|
const generation = this.#generation;
|
|
474
|
-
|
|
666
|
+
return this.#serializeSDP(async () => {
|
|
475
667
|
const existing = new Set([...this.#localTracks.values()].filter((state) => state.source !== source && state.providerPublicationId !== null));
|
|
476
668
|
const states = await this.#prepareConnectionForPublication([], generation);
|
|
477
669
|
await this.#publishPreparedTracksSerialized(
|
|
@@ -483,7 +675,6 @@ var CloudflareSFUClient = class {
|
|
|
483
675
|
this.#requireGeneration(generation);
|
|
484
676
|
this.#setPhase("live", null);
|
|
485
677
|
});
|
|
486
|
-
await this.#retirement;
|
|
487
678
|
}
|
|
488
679
|
async #retireUnavailableLocalPublication(state) {
|
|
489
680
|
const publicationId = state.providerPublicationId;
|
|
@@ -491,16 +682,15 @@ var CloudflareSFUClient = class {
|
|
|
491
682
|
const authoritative = await this.#signal(() => this.#requireTransport().listPublications());
|
|
492
683
|
if (authoritative.publications.some((publication) => publication.publicationId === publicationId)) return;
|
|
493
684
|
if (state.providerPublicationId === publicationId) {
|
|
494
|
-
this.#pauseLocalSource(state.source);
|
|
685
|
+
this.#pauseLocalSource(state.source, "discovery_absent");
|
|
495
686
|
await this.#retireOwnedPublications();
|
|
496
687
|
}
|
|
497
688
|
}
|
|
498
|
-
#pauseLocalSource(source) {
|
|
689
|
+
#pauseLocalSource(source, cause = "forced_pause") {
|
|
499
690
|
const sources = localSourceGroup(source);
|
|
500
691
|
for (const publication of this.#ownedPublications) {
|
|
501
692
|
if (!sources.includes(publication.state.source)) continue;
|
|
502
|
-
this.#markPublicationRetiring(publication);
|
|
503
|
-
this.#retiredLocalConnection = this.#connection;
|
|
693
|
+
this.#markPublicationRetiring(publication, cause);
|
|
504
694
|
}
|
|
505
695
|
for (const state of this.#localTracks.values()) {
|
|
506
696
|
if (!sources.includes(state.source)) continue;
|
|
@@ -510,7 +700,7 @@ var CloudflareSFUClient = class {
|
|
|
510
700
|
this.#publishSnapshot();
|
|
511
701
|
this.#emitLocal();
|
|
512
702
|
}
|
|
513
|
-
#retireOwnedPublications() {
|
|
703
|
+
#retireOwnedPublications(reportFailure = this.#reportError.bind(this)) {
|
|
514
704
|
const publications = [...this.#ownedPublications].filter((item) => item.lifecycle.kind === "retiring" && item.lifecycle.handle);
|
|
515
705
|
const retirement = (async () => {
|
|
516
706
|
const errors = [];
|
|
@@ -519,7 +709,7 @@ var CloudflareSFUClient = class {
|
|
|
519
709
|
await this.#serializeSDP(() => this.#closeOwnedPublication(publication), true);
|
|
520
710
|
} catch (error) {
|
|
521
711
|
errors.push(error);
|
|
522
|
-
if (!this.#stopped)
|
|
712
|
+
if (!this.#stopped) reportFailure(error);
|
|
523
713
|
}
|
|
524
714
|
}
|
|
525
715
|
if (this.#stopped && this.#ownedPublications.size === 0) this.#transport = void 0;
|
|
@@ -529,15 +719,24 @@ var CloudflareSFUClient = class {
|
|
|
529
719
|
});
|
|
530
720
|
return this.#retirement = retirement;
|
|
531
721
|
}
|
|
532
|
-
#markPublicationRetiring(publication) {
|
|
722
|
+
#markPublicationRetiring(publication, cause = "publish_failure") {
|
|
533
723
|
const phase = publication.lifecycle;
|
|
534
|
-
if (phase.kind === "pending" || phase.kind === "confirmed") publication.lifecycle = { kind: "retiring", handle: phase.kind === "confirmed" ? phase.handle : null, detached: false };
|
|
724
|
+
if (phase.kind === "pending" || phase.kind === "confirmed") publication.lifecycle = { kind: "retiring", handle: phase.kind === "confirmed" ? phase.handle : null, detached: false, cause };
|
|
725
|
+
}
|
|
726
|
+
#markBundledTransportRetiring(publication) {
|
|
727
|
+
const bundleTag = /(?:^|\r?\n)a=group:BUNDLE[ \t]+(\S+)/.exec(publication.connection.localDescription?.sdp ?? "")?.[1];
|
|
728
|
+
if (!bundleTag || publication.transceiver.mid !== null && publication.transceiver.mid === bundleTag) {
|
|
729
|
+
if (this.#retiredLocalConnection === publication.connection) return;
|
|
730
|
+
this.#retiredLocalConnection = publication.connection;
|
|
731
|
+
if (publication.lifecycle.kind === "retiring") this.#retiredLocalCause = publication.lifecycle.cause;
|
|
732
|
+
}
|
|
535
733
|
}
|
|
536
734
|
async #closeOwnedPublication(publication) {
|
|
537
735
|
const phase = publication.lifecycle;
|
|
538
736
|
if (phase.kind !== "retiring" || !phase.handle) return;
|
|
539
737
|
const handle = phase.handle;
|
|
540
738
|
const { connection, connectionId, transceiver, state } = publication;
|
|
739
|
+
this.#markBundledTransportRetiring(publication);
|
|
541
740
|
let closed = connection.connectionState === "closed";
|
|
542
741
|
try {
|
|
543
742
|
if (!phase.detached && !closed) {
|
|
@@ -574,13 +773,16 @@ var CloudflareSFUClient = class {
|
|
|
574
773
|
this.#requireActive();
|
|
575
774
|
const options = "connectionId" in input ? { bootstrap: input } : input;
|
|
576
775
|
validateBootstrap(options.bootstrap);
|
|
577
|
-
if (options.bootstrap.connectionId !== this.#bootstrap.connectionId)
|
|
776
|
+
if (options.bootstrap.connectionId !== this.#bootstrap.connectionId) {
|
|
777
|
+
this.#fenceLocalPublications();
|
|
778
|
+
this.#replacementCause = "access_refresh";
|
|
779
|
+
}
|
|
578
780
|
const generation = ++this.#generation;
|
|
579
781
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
580
782
|
this.#polling = false;
|
|
581
783
|
this.#pollAfterCurrent = false;
|
|
582
784
|
this.#clearPoll();
|
|
583
|
-
this.#
|
|
785
|
+
this.#remotePullRetries.clear();
|
|
584
786
|
this.#disposeConnection(false);
|
|
585
787
|
for (const publication of this.#ownedPublications) this.#markPublicationRetiring(publication);
|
|
586
788
|
void this.#retireOwnedPublications();
|
|
@@ -618,11 +820,12 @@ var CloudflareSFUClient = class {
|
|
|
618
820
|
return;
|
|
619
821
|
}
|
|
620
822
|
this.#stopped = true;
|
|
823
|
+
this.#receiveProgress.expect([], this.#participantId);
|
|
621
824
|
this.#subscribedPublicationIds.clear();
|
|
622
825
|
this.#generation++;
|
|
623
826
|
this.#connectionEpoch++;
|
|
624
827
|
this.#clearPoll();
|
|
625
|
-
this.#
|
|
828
|
+
this.#remotePullRetries.clear();
|
|
626
829
|
this.#polling = false;
|
|
627
830
|
this.#pollAfterCurrent = false;
|
|
628
831
|
this.#disposeConnection(true);
|
|
@@ -645,35 +848,64 @@ var CloudflareSFUClient = class {
|
|
|
645
848
|
}
|
|
646
849
|
async #enablePreparedTrack(state) {
|
|
647
850
|
const generation = this.#generation;
|
|
648
|
-
state.desiredEnabled = true;
|
|
649
|
-
state.track.enabled = true;
|
|
650
851
|
try {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (!state.enabled) {
|
|
657
|
-
this.#pauseLocalSource(state.source);
|
|
658
|
-
await this.#retireOwnedPublications();
|
|
659
|
-
}
|
|
660
|
-
this.#publishSnapshot();
|
|
661
|
-
this.#emitLocal();
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
852
|
+
await this.#retireExpiredCapture(state);
|
|
853
|
+
if (this.#locallyPausedSources.has(state.source)) return false;
|
|
854
|
+
state.desiredEnabled = true;
|
|
855
|
+
state.track.enabled = true;
|
|
856
|
+
if (state.providerPublicationId && state.transceiver) return await this.#reattachLocalTrack(state, state.transceiver);
|
|
664
857
|
await this.#publishPreparedTracks([state], generation);
|
|
665
858
|
if (this.#remoteRecoveryGeneration !== generation) this.#replacementAttemptedGeneration = null;
|
|
859
|
+
return state.enabled;
|
|
666
860
|
} catch (error) {
|
|
667
861
|
if (generation === this.#generation) {
|
|
668
|
-
this.#pauseLocalSource(state.source);
|
|
862
|
+
this.#pauseLocalSource(state.source, "enable_failure");
|
|
669
863
|
await this.#retireOwnedPublications().catch(() => {
|
|
670
864
|
});
|
|
671
865
|
}
|
|
672
866
|
throw error;
|
|
673
867
|
}
|
|
674
868
|
}
|
|
869
|
+
async #retireExpiredCapture(state) {
|
|
870
|
+
const publication = [...this.#ownedPublications].find((item) => item.transceiver === state.transceiver && item.lifecycle.kind === "confirmed");
|
|
871
|
+
if (!publication || publication.detachedAt === null || Date.now() - publication.detachedAt < CAPTURE_REUSE_WINDOW_MS) return;
|
|
872
|
+
this.#pauseLocalSource(state.source, "capture_cleanup");
|
|
873
|
+
try {
|
|
874
|
+
await this.#retireOwnedPublications((error) => {
|
|
875
|
+
if (!this.#isExpiredRetiredConnection(error)) this.#reportError(error);
|
|
876
|
+
});
|
|
877
|
+
} catch (error) {
|
|
878
|
+
if (!this.#isExpiredRetiredConnection(error)) throw error;
|
|
879
|
+
this.#fenceLocalPublications();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
#isExpiredRetiredConnection(error) {
|
|
883
|
+
if (!this.#replaceMediaConnection || this.#retiredLocalConnection !== this.#connection) return false;
|
|
884
|
+
if (error instanceof AggregateError) return error.errors.every((cause) => this.#isRetryableConnectionFailure(cause));
|
|
885
|
+
return this.#isRetryableConnectionFailure(error);
|
|
886
|
+
}
|
|
887
|
+
async #reattachLocalTrack(state, transceiver) {
|
|
888
|
+
await this.#boundPeerOperation(transceiver.sender.replaceTrack(state.track));
|
|
889
|
+
const cause = this.#locallyPausedSources.get(state.source);
|
|
890
|
+
state.enabled = cause === void 0;
|
|
891
|
+
state.desiredEnabled = state.enabled;
|
|
892
|
+
state.track.enabled = state.enabled;
|
|
893
|
+
if (cause === "capture_cleanup") await this.#disablePreparedTrack(state);
|
|
894
|
+
else if (cause) {
|
|
895
|
+
this.#pauseLocalSource(state.source, cause);
|
|
896
|
+
await this.#retireOwnedPublications();
|
|
897
|
+
} else {
|
|
898
|
+
for (const publication of this.#ownedPublications) if (publication.transceiver === transceiver) publication.detachedAt = null;
|
|
899
|
+
}
|
|
900
|
+
this.#publishSnapshot();
|
|
901
|
+
this.#emitLocal();
|
|
902
|
+
return state.enabled;
|
|
903
|
+
}
|
|
675
904
|
async #disablePreparedTrack(state) {
|
|
676
905
|
const transceiver = state.transceiver;
|
|
906
|
+
for (const publication of this.#ownedPublications) {
|
|
907
|
+
if (publication.transceiver === transceiver && publication.lifecycle.kind === "confirmed") publication.detachedAt ??= Date.now();
|
|
908
|
+
}
|
|
677
909
|
state.desiredEnabled = false;
|
|
678
910
|
state.track.enabled = false;
|
|
679
911
|
const detach = transceiver ? this.#boundPeerOperation(transceiver.sender.replaceTrack(null)) : Promise.resolve();
|
|
@@ -691,7 +923,7 @@ var CloudflareSFUClient = class {
|
|
|
691
923
|
await this.#publishPreparedTracksSerialized(pending, generation, this.#connection, this.#bootstrap.connectionId);
|
|
692
924
|
} catch (error) {
|
|
693
925
|
if (!this.#replaceMediaConnection || !this.#isRetryableConnectionFailure(error) || this.#replacementAttemptedGeneration === generation) throw error;
|
|
694
|
-
await this.#replaceMediaConnectionForRecovery(generation);
|
|
926
|
+
await this.#replaceMediaConnectionForRecovery(generation, "local_signaling_failure");
|
|
695
927
|
await this.#publishPreparedTracksSerialized(
|
|
696
928
|
[...this.#localTracks.values()].filter((state) => (state.desiredEnabled || states.includes(state)) && state.track.readyState !== "ended"),
|
|
697
929
|
generation,
|
|
@@ -713,14 +945,14 @@ var CloudflareSFUClient = class {
|
|
|
713
945
|
const authoritative = await this.#signal(() => this.#requireTransport().listPublications());
|
|
714
946
|
this.#pauseUnavailableOtherSources(authoritative.publications, states);
|
|
715
947
|
this.#replacementAttemptedGeneration = null;
|
|
716
|
-
await this.#replaceMediaConnectionForRecovery(generation);
|
|
948
|
+
await this.#replaceMediaConnectionForRecovery(generation, this.#retiredLocalCause);
|
|
717
949
|
return [...this.#localTracks.values()].filter((state) => (state.desiredEnabled || states.includes(state)) && state.track.readyState !== "ended");
|
|
718
950
|
}
|
|
719
951
|
#pauseUnavailableOtherSources(publications, states) {
|
|
720
952
|
for (const state of this.#localTracks.values()) {
|
|
721
953
|
if (states.includes(state) || !state.providerPublicationId) continue;
|
|
722
954
|
if (publications.some((publication) => publication.publicationId === state.providerPublicationId)) continue;
|
|
723
|
-
this.#pauseLocalSource(state.source);
|
|
955
|
+
this.#pauseLocalSource(state.source, "discovery_absent");
|
|
724
956
|
}
|
|
725
957
|
void this.#retireOwnedPublications();
|
|
726
958
|
}
|
|
@@ -750,7 +982,7 @@ var CloudflareSFUClient = class {
|
|
|
750
982
|
}
|
|
751
983
|
async #prepareLocalPublication(connection, state, generation) {
|
|
752
984
|
const transceiver = state.transceiver ?? this.#addLocalTransceiver(connection, state);
|
|
753
|
-
const publication = { state, transceiver, trackName: `${state.source}-${globalThis.crypto.randomUUID()}`, connection, connectionId: this.#bootstrap.connectionId, lifecycle: { kind: "pending" } };
|
|
985
|
+
const publication = { state, transceiver, trackName: `${state.source}-${globalThis.crypto.randomUUID()}`, connection, connectionId: this.#bootstrap.connectionId, detachedAt: null, lifecycle: { kind: "pending" } };
|
|
754
986
|
this.#ownedPublications.add(publication);
|
|
755
987
|
if (state.transceiver) await this.#boundPeerOperation(transceiver.sender.replaceTrack(state.track));
|
|
756
988
|
this.#requireGeneration(generation);
|
|
@@ -798,7 +1030,7 @@ var CloudflareSFUClient = class {
|
|
|
798
1030
|
source: state.source
|
|
799
1031
|
})
|
|
800
1032
|
);
|
|
801
|
-
const response = await this.#signal(() => this.#
|
|
1033
|
+
const response = await this.#signal(() => this.#addTracks({ connectionId, sessionDescription, tracks }), { generation, connectionId });
|
|
802
1034
|
this.#recordLocalPublicationHandles(publications, response.tracks);
|
|
803
1035
|
this.#requireGeneration(generation);
|
|
804
1036
|
await this.#applyProviderDescription(response, connection);
|
|
@@ -824,7 +1056,7 @@ var CloudflareSFUClient = class {
|
|
|
824
1056
|
state.providerPublicationId = lifecycle.handle.publicationId;
|
|
825
1057
|
continue;
|
|
826
1058
|
}
|
|
827
|
-
this.#markPublicationRetiring(publication);
|
|
1059
|
+
this.#markPublicationRetiring(publication, this.#locallyPausedSources.get(state.source) ?? "publish_failure");
|
|
828
1060
|
if (state.transceiver === transceiver) {
|
|
829
1061
|
this.#clearLocalPublication(state);
|
|
830
1062
|
}
|
|
@@ -849,9 +1081,8 @@ var CloudflareSFUClient = class {
|
|
|
849
1081
|
if (cursor === null) return;
|
|
850
1082
|
const participantRevision = this.#remoteParticipantRevision;
|
|
851
1083
|
const desired = this.#desiredRemotePublications(authoritative, cursor);
|
|
852
|
-
const toPull = this.#pendingRemotePulls(desired);
|
|
853
1084
|
this.#pollFailedPull = true;
|
|
854
|
-
const pulled = await this.#pullWithRecovery(
|
|
1085
|
+
const pulled = await this.#pullWithRecovery(desired, cursor, generation);
|
|
855
1086
|
if (pulled === null) return;
|
|
856
1087
|
this.#requireGeneration(generation);
|
|
857
1088
|
const next = reconcileRemoteTracks(desired, pulled, this.#remoteTracks);
|
|
@@ -871,16 +1102,18 @@ var CloudflareSFUClient = class {
|
|
|
871
1102
|
}
|
|
872
1103
|
#pendingRemotePulls(desired) {
|
|
873
1104
|
const desiredIds = new Set([...desired.values()].map((publication) => publication.publicationId));
|
|
874
|
-
for (const publicationId of this.#
|
|
875
|
-
if (!desiredIds.has(publicationId)) this.#
|
|
1105
|
+
for (const publicationId of this.#remotePullRetries.keys()) {
|
|
1106
|
+
if (!desiredIds.has(publicationId)) this.#remotePullRetries.delete(publicationId);
|
|
876
1107
|
}
|
|
877
1108
|
for (const publicationId of this.#subscribedPublicationIds) {
|
|
878
1109
|
if (!desiredIds.has(publicationId)) this.#subscribedPublicationIds.delete(publicationId);
|
|
879
1110
|
}
|
|
880
1111
|
const now = Date.now();
|
|
881
1112
|
return [...desired].filter(([key, publication]) => {
|
|
882
|
-
if (this.#remoteTracks.get(key)?.publicationId === publication.publicationId)
|
|
883
|
-
|
|
1113
|
+
if (this.#remoteTracks.get(key)?.publicationId === publication.publicationId) {
|
|
1114
|
+
if (!this.#receiveProgress.canRebuild() || !this.#replaceMediaConnection) return false;
|
|
1115
|
+
}
|
|
1116
|
+
const remaining = (this.#remotePullRetries.get(publication.publicationId)?.retryAfter ?? 0) - now;
|
|
884
1117
|
return remaining <= 0;
|
|
885
1118
|
}).map(([, publication]) => publication);
|
|
886
1119
|
}
|
|
@@ -908,7 +1141,14 @@ var CloudflareSFUClient = class {
|
|
|
908
1141
|
#canSubscribeToRemotePublication(publication) {
|
|
909
1142
|
return this.#remoteParticipantIds === null || this.#remoteParticipantIds.has(publication.participantId) || this.#subscribedPublicationIds.has(publication.publicationId);
|
|
910
1143
|
}
|
|
911
|
-
async #pullWithRecovery(
|
|
1144
|
+
async #pullWithRecovery(desired, cursor, generation) {
|
|
1145
|
+
const pending = this.#pendingRemotePulls(desired);
|
|
1146
|
+
const receiveStalled = pending.some((publication) => this.#receiveProgress.isStalled(publication.publicationId));
|
|
1147
|
+
const publications = receiveStalled ? [...desired.values()] : pending;
|
|
1148
|
+
if (receiveStalled) {
|
|
1149
|
+
if (!this.#replaceMediaConnection || !this.#receiveProgress.reserveRebuild()) return [];
|
|
1150
|
+
return this.#retryRemotePullOnReplacement(publications, cursor, generation, "receive_stalled");
|
|
1151
|
+
}
|
|
912
1152
|
try {
|
|
913
1153
|
return await this.#pull(publications, generation);
|
|
914
1154
|
} catch (error) {
|
|
@@ -921,19 +1161,25 @@ var CloudflareSFUClient = class {
|
|
|
921
1161
|
}
|
|
922
1162
|
}
|
|
923
1163
|
#canReplaceConnectionAfterRemotePull(error, generation) {
|
|
924
|
-
|
|
1164
|
+
const gone = error instanceof CloudflareSFUError && (error.options.status === 404 || error.options.status === 410);
|
|
1165
|
+
return this.#isRetryableConnectionFailure(error) && (gone || !this.#hasLiveRemoteTracks()) && this.#replaceMediaConnection !== void 0 && this.#replacementAttemptedGeneration !== generation;
|
|
925
1166
|
}
|
|
926
1167
|
#hasLiveRemoteTracks() {
|
|
927
1168
|
return [...this.#remoteTracks.values()].some((publication) => publication.track.readyState === "live");
|
|
928
1169
|
}
|
|
929
|
-
async #retryRemotePullOnReplacement(publications, cursor, generation) {
|
|
1170
|
+
async #retryRemotePullOnReplacement(publications, cursor, generation, cause = "remote_signaling_failure") {
|
|
930
1171
|
this.#remoteRecoveryGeneration = generation;
|
|
1172
|
+
const connection = this.#connection;
|
|
931
1173
|
try {
|
|
932
|
-
await this.#replaceMediaConnectionForRecovery(generation);
|
|
1174
|
+
await this.#replaceMediaConnectionForRecovery(generation, cause);
|
|
933
1175
|
await this.#republishPreparedTracksAfterRecovery(generation);
|
|
934
1176
|
return await this.#pull(publications, generation);
|
|
935
1177
|
} catch (error) {
|
|
936
1178
|
if (generation !== this.#generation || this.#stopped) throw error;
|
|
1179
|
+
if (cause === "receive_stalled" && connection === this.#connection) {
|
|
1180
|
+
this.#replacementAttemptedGeneration = null;
|
|
1181
|
+
for (const publication of publications) this.#backoffRemotePull(publication.publicationId, true);
|
|
1182
|
+
}
|
|
937
1183
|
this.#observeRemotePublicationCursor(cursor, retryableRemotePull(error));
|
|
938
1184
|
throw error;
|
|
939
1185
|
}
|
|
@@ -974,7 +1220,10 @@ var CloudflareSFUClient = class {
|
|
|
974
1220
|
}
|
|
975
1221
|
#deferRemotePulls(publications, error) {
|
|
976
1222
|
const retryAfter = retryableRemotePull(error) ? Date.now() + 15e3 : Number.POSITIVE_INFINITY;
|
|
977
|
-
for (const publication of publications)
|
|
1223
|
+
for (const publication of publications) {
|
|
1224
|
+
const failures = this.#remotePullRetries.get(publication.publicationId)?.failures ?? 0;
|
|
1225
|
+
this.#remotePullRetries.set(publication.publicationId, { retryAfter, failures, receiveStalled: this.#remotePullRetries.get(publication.publicationId)?.receiveStalled });
|
|
1226
|
+
}
|
|
978
1227
|
}
|
|
979
1228
|
async #pullBatch(publications, generation) {
|
|
980
1229
|
if (publications.length === 0) return [];
|
|
@@ -1000,12 +1249,17 @@ var CloudflareSFUClient = class {
|
|
|
1000
1249
|
connection.addEventListener("track", onTrack);
|
|
1001
1250
|
try {
|
|
1002
1251
|
this.#requireGeneration(generation);
|
|
1003
|
-
const response = await this.#signal(() => this.#
|
|
1252
|
+
const response = await this.#signal(() => this.#addTracks({ connectionId, tracks: requested, allowPartialRemoteTracks: true }), {
|
|
1253
|
+
generation,
|
|
1254
|
+
connectionId
|
|
1255
|
+
});
|
|
1004
1256
|
this.#requireGeneration(generation);
|
|
1005
1257
|
this.#recordRemotePullErrors(publications, response.trackErrors ?? []);
|
|
1006
1258
|
const responseTracks = response.tracks ?? [];
|
|
1007
1259
|
const matched = matchRemotePullTracks(publications, responseTracks);
|
|
1008
|
-
for (const { publication } of matched)
|
|
1260
|
+
for (const { publication } of matched) {
|
|
1261
|
+
if (!this.#remotePullRetries.get(publication.publicationId)?.receiveStalled) this.#remotePullRetries.delete(publication.publicationId);
|
|
1262
|
+
}
|
|
1009
1263
|
await this.#completeRenegotiation(response, connection, connectionId, generation);
|
|
1010
1264
|
if (matched.length === 0) return [];
|
|
1011
1265
|
await this.#waitForConnection(connection, generation);
|
|
@@ -1035,11 +1289,33 @@ var CloudflareSFUClient = class {
|
|
|
1035
1289
|
for (const failure of failures) {
|
|
1036
1290
|
const publication = requested.get(`${failure.connectionId}\0${failure.trackName}`);
|
|
1037
1291
|
if (!publication) throw new CloudflareSFUError("Cloudflare SFU returned an unrequested remote track error", "invalid_publication");
|
|
1038
|
-
|
|
1039
|
-
this.#remotePullRetryAfter.set(publication.publicationId, Date.now() + delay);
|
|
1292
|
+
this.#backoffRemotePull(publication.publicationId);
|
|
1040
1293
|
this.#reportError(new CloudflareSFUError(`Cloudflare SFU remote track failed: ${failure.code}`, "media_failed", { providerCode: failure.code }));
|
|
1041
1294
|
}
|
|
1042
1295
|
}
|
|
1296
|
+
#backoffRemotePull(publicationId, receiveStalled = false) {
|
|
1297
|
+
const previous = this.#remotePullRetries.get(publicationId) ?? { retryAfter: 0, failures: 0 };
|
|
1298
|
+
const failures = Math.min(previous.failures + 1, 6);
|
|
1299
|
+
this.#remotePullRetries.set(publicationId, { retryAfter: Date.now() + Math.min(15e3, 750 * 2 ** (failures - 1)), failures, receiveStalled: receiveStalled || previous.receiveStalled });
|
|
1300
|
+
}
|
|
1301
|
+
#canObserveReceiveProgress(connection) {
|
|
1302
|
+
return this.#started && connectionIsLive(connection) && (typeof document === "undefined" || document.visibilityState !== "hidden");
|
|
1303
|
+
}
|
|
1304
|
+
#observeReceiveProgress(connection, report) {
|
|
1305
|
+
if (!this.#canObserveReceiveProgress(connection)) {
|
|
1306
|
+
this.#receiveProgress.clear();
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const { stalled, advanced } = this.#receiveProgress.sample(performance.now(), connection, report, this.#remoteTracks.values());
|
|
1310
|
+
for (const publicationId of advanced) this.#remotePullRetries.delete(publicationId);
|
|
1311
|
+
this.#publishSnapshot();
|
|
1312
|
+
if (!this.#receiveProgress.canRebuild()) return;
|
|
1313
|
+
for (const publicationId of stalled) {
|
|
1314
|
+
if (!this.#remotePullRetries.get(publicationId)?.receiveStalled) this.#backoffRemotePull(publicationId, true);
|
|
1315
|
+
}
|
|
1316
|
+
this.#remotePullIncomplete = true;
|
|
1317
|
+
this.remotePublicationsChanged();
|
|
1318
|
+
}
|
|
1043
1319
|
async #completeRenegotiation(response, connection, connectionId, generation) {
|
|
1044
1320
|
if (!response.requiresImmediateRenegotiation) return;
|
|
1045
1321
|
await this.#applyProviderDescription(response, connection);
|
|
@@ -1145,14 +1421,12 @@ var CloudflareSFUClient = class {
|
|
|
1145
1421
|
cameraPolicy.reset();
|
|
1146
1422
|
observedCamera = state;
|
|
1147
1423
|
}
|
|
1148
|
-
const sender = this.#cameraSimulcastSender(state);
|
|
1149
|
-
if (!sender) {
|
|
1150
|
-
cameraPolicy.reset();
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
1424
|
const report = await connection.getStats();
|
|
1154
|
-
if (!current() ||
|
|
1155
|
-
|
|
1425
|
+
if (!current() || connection.signalingState !== "stable") return;
|
|
1426
|
+
this.#observeReceiveProgress(connection, report);
|
|
1427
|
+
const sender = this.#cameraSimulcastSender(state);
|
|
1428
|
+
if (!sender) cameraPolicy.reset();
|
|
1429
|
+
else await this.#applyCameraUplink(sender, report, cameraPolicy);
|
|
1156
1430
|
} catch {
|
|
1157
1431
|
cameraPolicy.reset();
|
|
1158
1432
|
} finally {
|
|
@@ -1187,9 +1461,14 @@ var CloudflareSFUClient = class {
|
|
|
1187
1461
|
}
|
|
1188
1462
|
async #replaceDormantConnectionBeforeNegotiation(generation) {
|
|
1189
1463
|
if (!this.#started || this.#negotiatedGeneration === generation || this.#replacementAttemptedGeneration === generation || !this.#replaceMediaConnection) return;
|
|
1190
|
-
await this.#replaceMediaConnectionForRecovery(generation);
|
|
1464
|
+
await this.#replaceMediaConnectionForRecovery(generation, "dormant_connection");
|
|
1465
|
+
}
|
|
1466
|
+
#addTracks(input) {
|
|
1467
|
+
const replacementCause = this.#replacementCause;
|
|
1468
|
+
this.#replacementCause = void 0;
|
|
1469
|
+
return this.#requireTransport().addTracks({ ...input, replacementCause });
|
|
1191
1470
|
}
|
|
1192
|
-
async #replaceMediaConnectionForRecovery(generation) {
|
|
1471
|
+
async #replaceMediaConnectionForRecovery(generation, cause) {
|
|
1193
1472
|
this.#requireGeneration(generation);
|
|
1194
1473
|
if (!this.#replaceMediaConnection || this.#replacementAttemptedGeneration === generation) {
|
|
1195
1474
|
throw new CloudflareSFUError("A fresh Cloudflare SFU connection is unavailable", "signaling_failed");
|
|
@@ -1209,6 +1488,7 @@ var CloudflareSFUClient = class {
|
|
|
1209
1488
|
this.#cursor = null;
|
|
1210
1489
|
this.#bootstrap = bootstrap;
|
|
1211
1490
|
this.#rebuildPeerConnection(bootstrap, generation, connectionEpoch);
|
|
1491
|
+
this.#replacementCause = cause;
|
|
1212
1492
|
this.#retiredLocalConnection = null;
|
|
1213
1493
|
this.#negotiatedGeneration = null;
|
|
1214
1494
|
for (const state of this.#localTracks.values()) {
|
|
@@ -1248,6 +1528,7 @@ var CloudflareSFUClient = class {
|
|
|
1248
1528
|
}
|
|
1249
1529
|
}
|
|
1250
1530
|
#disposeConnection(stopSenders) {
|
|
1531
|
+
this.#receiveProgress.clear();
|
|
1251
1532
|
if (!stopSenders) this.#setPhase("recovering", null);
|
|
1252
1533
|
this.#disposeConnectionObservation?.();
|
|
1253
1534
|
this.#disposeConnectionObservation = void 0;
|
|
@@ -1285,7 +1566,7 @@ var CloudflareSFUClient = class {
|
|
|
1285
1566
|
this.#emitRemote();
|
|
1286
1567
|
}
|
|
1287
1568
|
#schedulePoll(delayMs = this.#pollIntervalMs) {
|
|
1288
|
-
if (this.#stopped || !this.#started || this.#pollTimer !== void 0) return;
|
|
1569
|
+
if (this.#stopped || !this.#started && !this.#receivingInitial || this.#pollTimer !== void 0) return;
|
|
1289
1570
|
this.#pollTimer = globalThis.setTimeout(async () => {
|
|
1290
1571
|
this.#pollTimer = void 0;
|
|
1291
1572
|
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
@@ -1344,6 +1625,7 @@ var CloudflareSFUClient = class {
|
|
|
1344
1625
|
})
|
|
1345
1626
|
) : EMPTY_LOCAL,
|
|
1346
1627
|
remoteTracks: this.#remoteTracks.size ? [...this.#remoteTracks.values()] : EMPTY_REMOTE,
|
|
1628
|
+
receiveStalls: this.#receiveProgress.observations,
|
|
1347
1629
|
failure
|
|
1348
1630
|
});
|
|
1349
1631
|
if (snapshotEqual(this.#snapshot, next)) return;
|
|
@@ -1468,7 +1750,7 @@ function snapshotEqual(left, right) {
|
|
|
1468
1750
|
return snapshotMetadataEqual(left, right) && left.localTracks.every((publication, index) => publicationEqual(publication, right.localTracks[index])) && left.remoteTracks.every((publication, index) => remotePublicationEqual(publication, right.remoteTracks[index]));
|
|
1469
1751
|
}
|
|
1470
1752
|
function snapshotMetadataEqual(left, right) {
|
|
1471
|
-
return connectionEqual(left.connection, right.connection) && cursorEqual(left.cursor, right.cursor) && failureEqual(left.failure, right.failure) && left.localTracks.length === right.localTracks.length && left.remoteTracks.length === right.remoteTracks.length;
|
|
1753
|
+
return (left.receiveStalls ?? EMPTY_RECEIVE_STALLS) === (right.receiveStalls ?? EMPTY_RECEIVE_STALLS) && connectionEqual(left.connection, right.connection) && cursorEqual(left.cursor, right.cursor) && failureEqual(left.failure, right.failure) && left.localTracks.length === right.localTracks.length && left.remoteTracks.length === right.remoteTracks.length;
|
|
1472
1754
|
}
|
|
1473
1755
|
function connectionEqual(left, right) {
|
|
1474
1756
|
return left.phase === right.phase && left.peerConnectionState === right.peerConnectionState && left.iceConnectionState === right.iceConnectionState;
|
|
@@ -1557,7 +1839,10 @@ function createCloudflareSFUHTTPTransport(options) {
|
|
|
1557
1839
|
};
|
|
1558
1840
|
return {
|
|
1559
1841
|
addTracks: async (input) => {
|
|
1560
|
-
const
|
|
1842
|
+
const query = new URLSearchParams();
|
|
1843
|
+
if (input.allowPartialRemoteTracks) query.set("allow_partial_remote_tracks", "true");
|
|
1844
|
+
if (input.replacementCause) query.set("replacement_cause", input.replacementCause);
|
|
1845
|
+
const path = query.size ? `tracks?${query}` : "tracks";
|
|
1561
1846
|
const response = await request(path, {
|
|
1562
1847
|
method: "POST",
|
|
1563
1848
|
body: JSON.stringify({ connection_id: input.connectionId, session_description: input.sessionDescription, tracks: input.tracks })
|