@q9labsai/chalk-client 4.1.15 → 4.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -5
- package/dist/access/grant.d.ts.map +1 -1
- package/dist/access/manager.d.ts.map +1 -1
- package/dist/{chunk-PQNSA7ZN.js → chunk-2DZUHV6I.js} +782 -122
- package/dist/{chunk-6ONSFCIY.js → chunk-A7BVZEFM.js} +7 -3
- package/dist/{chunk-LQSXEWLL.js → chunk-AUQTQOLR.js} +2685 -511
- package/dist/{chunk-PWUU6T3R.js → chunk-CZUJNAUI.js} +1196 -874
- package/dist/{chunk-NKOKY2AE.js → chunk-EWTITNJ3.js} +15 -101
- package/dist/chunk-HW5TTZL2.js +99 -0
- package/dist/chunk-RCDXIDCN.js +33 -0
- package/dist/{chunk-LSDDADCN.js → chunk-ZRMGKMX4.js} +1728 -517
- package/dist/client.d.ts +1452 -465
- package/dist/client.d.ts.map +1 -1
- package/dist/connection/dependencies.d.ts +11 -0
- package/dist/connection/dependencies.d.ts.map +1 -1
- package/dist/connection/diagnostics.d.ts +1 -1
- package/dist/connection/diagnostics.d.ts.map +1 -1
- package/dist/connection/index.d.ts +3 -0
- package/dist/connection/index.d.ts.map +1 -1
- package/dist/connection/lifecycle.d.ts +4 -1
- package/dist/connection/lifecycle.d.ts.map +1 -1
- package/dist/connection/media-devices.d.ts.map +1 -1
- package/dist/connection/production.d.ts +4 -1
- package/dist/connection/production.d.ts.map +1 -1
- package/dist/connection/types.d.ts +2 -2
- package/dist/connection/types.d.ts.map +1 -1
- package/dist/effect.js +355 -130
- package/dist/feedback/collector.d.ts +14 -0
- package/dist/feedback/collector.d.ts.map +1 -0
- package/dist/feedback/context.d.ts +38 -0
- package/dist/feedback/context.d.ts.map +1 -0
- package/dist/feedback/controller.d.ts +4 -0
- package/dist/feedback/controller.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/transport.d.ts +29 -0
- package/dist/feedback/transport.d.ts.map +1 -0
- package/dist/feedback/types.d.ts +202 -0
- package/dist/feedback/types.d.ts.map +1 -0
- package/dist/feedback/validation.d.ts +30 -0
- package/dist/feedback/validation.d.ts.map +1 -0
- package/dist/foreground-deadline.d.ts +9 -0
- package/dist/foreground-deadline.d.ts.map +1 -0
- package/dist/generated/http-api.d.ts +1438 -461
- package/dist/generated/http-api.d.ts.map +1 -1
- package/dist/generated/schemas.d.ts +2667 -842
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +136 -21
- package/dist/media/camera-uplink.d.ts +13 -0
- package/dist/media/camera-uplink.d.ts.map +1 -0
- package/dist/media/capture-error.d.ts +27 -0
- package/dist/media/capture-error.d.ts.map +1 -0
- package/dist/media/client.d.ts +4 -0
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +5 -1
- package/dist/media/plane.d.ts +4 -0
- package/dist/media/plane.d.ts.map +1 -1
- package/dist/media/remote-track-response.d.ts +6 -0
- package/dist/media/remote-track-response.d.ts.map +1 -0
- package/dist/media/rtk.d.ts.map +1 -1
- package/dist/media/transport.d.ts.map +1 -1
- package/dist/media/types.d.ts +20 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/public-invites/client.d.ts.map +1 -1
- package/dist/public-invites/index.js +4 -3
- package/dist/public-invites/types.d.ts +4 -2
- package/dist/public-invites/types.d.ts.map +1 -1
- package/dist/server/access-refresh.d.cts +13 -0
- package/dist/server/access-refresh.d.ts +14 -0
- package/dist/server/access-refresh.d.ts.map +1 -0
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/index.cjs +189 -23
- package/dist/server/index.d.cts +2 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +187 -22
- package/dist/server/types.d.cts +76 -1
- package/dist/server/types.d.ts +76 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/space-client/chat-controller.d.ts +3 -0
- package/dist/space-client/chat-controller.d.ts.map +1 -1
- package/dist/space-client/controller-effects.d.ts +6 -0
- package/dist/space-client/controller-effects.d.ts.map +1 -1
- package/dist/space-client/core.d.ts +12 -1
- package/dist/space-client/core.d.ts.map +1 -1
- package/dist/space-client/core.test.helpers.d.ts +2 -1
- package/dist/space-client/core.test.helpers.d.ts.map +1 -1
- package/dist/space-client/effect.d.ts +5 -0
- package/dist/space-client/effect.d.ts.map +1 -1
- package/dist/space-client/episode-diagnostic-runtime.d.ts +3 -0
- package/dist/space-client/episode-diagnostic-runtime.d.ts.map +1 -1
- package/dist/space-client/errors.d.ts.map +1 -1
- package/dist/space-client/index.d.ts +4 -1
- package/dist/space-client/index.d.ts.map +1 -1
- package/dist/space-client/media-controller.d.ts.map +1 -1
- package/dist/space-client/offline-actions-storage.d.ts +66 -0
- package/dist/space-client/offline-actions-storage.d.ts.map +1 -0
- package/dist/space-client/offline-actions.d.ts +36 -0
- package/dist/space-client/offline-actions.d.ts.map +1 -0
- package/dist/space-client/participants-controller.d.ts +2 -1
- package/dist/space-client/participants-controller.d.ts.map +1 -1
- package/dist/space-client/recording-controller.d.ts +20 -0
- package/dist/space-client/recording-controller.d.ts.map +1 -0
- package/dist/space-client/reload-rejoin.d.ts +24 -0
- package/dist/space-client/reload-rejoin.d.ts.map +1 -0
- package/dist/space-client/space-client.d.ts.map +1 -1
- package/dist/space-client/store.d.ts +3 -1
- package/dist/space-client/store.d.ts.map +1 -1
- package/dist/space-client/terminal.test.helpers.d.ts +115 -0
- package/dist/space-client/terminal.test.helpers.d.ts.map +1 -0
- package/dist/space-client/types.d.ts +42 -4
- package/dist/space-client/types.d.ts.map +1 -1
- package/dist/sync/index.js +2 -1
- package/dist/sync/v1-client.d.ts +2 -5
- package/dist/sync/v1-client.d.ts.map +1 -1
- package/dist/sync/v1-collaboration-state.d.ts +65 -0
- package/dist/sync/v1-collaboration-state.d.ts.map +1 -0
- package/dist/sync/v1-command-scheduler.d.ts +54 -0
- package/dist/sync/v1-command-scheduler.d.ts.map +1 -0
- package/dist/sync/v1-deferred.d.ts +8 -0
- package/dist/sync/v1-deferred.d.ts.map +1 -0
- package/dist/sync/v1-error.d.ts +5 -0
- package/dist/sync/v1-error.d.ts.map +1 -0
- package/dist/sync/v1-frame-signature.d.ts +2 -0
- package/dist/sync/v1-frame-signature.d.ts.map +1 -0
- package/dist/sync/v1-live-target-coordinator.d.ts +32 -0
- package/dist/sync/v1-live-target-coordinator.d.ts.map +1 -0
- package/dist/sync/v1-types.d.ts +3 -0
- package/dist/sync/v1-types.d.ts.map +1 -1
- package/dist/telemetry/index.js +9 -7
- package/dist/telemetry/reconnect.d.ts +7 -0
- package/dist/telemetry/reconnect.d.ts.map +1 -0
- package/dist/telemetry/trace.d.ts +1 -1
- package/dist/telemetry/trace.d.ts.map +1 -1
- package/dist/webhooks/generated/event-v1.d.cts +10 -2
- package/dist/webhooks/generated/event-v1.d.ts +10 -2
- package/dist/webhooks/generated/event-v1.d.ts.map +1 -1
- package/dist/webhooks/generated/fixtures-v1.d.cts +2 -2
- package/dist/webhooks/generated/fixtures-v1.d.ts +2 -2
- package/dist/webhooks/index.cjs +10 -2
- package/dist/webhooks/index.js +10 -2
- package/dist/webhooks/test.cjs +2 -2
- package/dist/webhooks/test.js +2 -2
- package/dist/whiteboard/v1-client.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordReconnect,
|
|
3
|
+
traceReconnect
|
|
4
|
+
} from "./chunk-RCDXIDCN.js";
|
|
5
|
+
import {
|
|
6
|
+
journeyHeaders
|
|
7
|
+
} from "./chunk-HW5TTZL2.js";
|
|
8
|
+
|
|
9
|
+
// src/media/camera-uplink.ts
|
|
10
|
+
var CameraUplinkPolicy = class {
|
|
11
|
+
#healthySamples = 0;
|
|
12
|
+
sample(bitrate, lowActive) {
|
|
13
|
+
if (bitrate === void 0 || !Number.isFinite(bitrate) || bitrate <= 0) {
|
|
14
|
+
this.reset();
|
|
15
|
+
return lowActive;
|
|
16
|
+
}
|
|
17
|
+
if (bitrate < 15e5) {
|
|
18
|
+
this.reset();
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (lowActive) {
|
|
22
|
+
this.reset();
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (bitrate < 2e6) {
|
|
26
|
+
this.reset();
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return ++this.#healthySamples >= 5;
|
|
30
|
+
}
|
|
31
|
+
reset() {
|
|
32
|
+
this.#healthySamples = 0;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function cameraUplinkBitrate(stats) {
|
|
36
|
+
for (const stat of stats) {
|
|
37
|
+
if (stat.type === "candidate-pair" && stat.state === "succeeded" && (stat.nominated || stat.selected)) return stat.availableOutgoingBitrate;
|
|
38
|
+
}
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
1
42
|
// src/media/observers.ts
|
|
2
43
|
function subscribeSnapshot(listeners, listener) {
|
|
3
44
|
listeners.add(listener);
|
|
@@ -132,15 +173,37 @@ async function waitFor(predicate, timeoutMs, timeoutMessage = "Timed out waiting
|
|
|
132
173
|
}
|
|
133
174
|
}
|
|
134
175
|
|
|
176
|
+
// src/media/remote-track-response.ts
|
|
177
|
+
function matchRemotePullTracks(publications, tracks) {
|
|
178
|
+
const requested = new Map(
|
|
179
|
+
publications.map((publication) => {
|
|
180
|
+
const reference = parseCloudflareSFUPublicationID(publication.publicationId);
|
|
181
|
+
return [`${reference.connectionId}\0${reference.trackName}`, publication];
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
const mids = /* @__PURE__ */ new Set();
|
|
185
|
+
return tracks.map(({ location, sessionId: connectionId, trackName, mid }) => {
|
|
186
|
+
const key = `${connectionId}\0${trackName}`;
|
|
187
|
+
const publication = requested.get(key);
|
|
188
|
+
if (location !== "remote" || !publication || !mid || mids.has(mid)) throw new CloudflareSFUError("Cloudflare SFU returned an invalid remote track identity", "invalid_publication");
|
|
189
|
+
requested.delete(key);
|
|
190
|
+
mids.add(mid);
|
|
191
|
+
return { publication, mid };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
135
195
|
// src/media/client.ts
|
|
136
196
|
var EMPTY_LOCAL = Object.freeze([]);
|
|
137
197
|
var EMPTY_REMOTE = Object.freeze([]);
|
|
138
198
|
var INVALID_PUBLICATION_SIGNATURE = "\0invalid";
|
|
139
199
|
var CONNECTION_TIMEOUT_MS = 8e3;
|
|
200
|
+
var NEGOTIATION_TIMEOUT_MS = 35e3;
|
|
201
|
+
var PEER_OPERATION_TIMEOUT_MS = 8e3;
|
|
140
202
|
var CloudflareSFUClient = class {
|
|
141
203
|
#localListeners = /* @__PURE__ */ new Set();
|
|
142
204
|
#onError;
|
|
143
205
|
#onRemoteTrack;
|
|
206
|
+
#recordReconnect;
|
|
144
207
|
#onRtcSummary;
|
|
145
208
|
#onScreenEnded;
|
|
146
209
|
#participantId;
|
|
@@ -149,17 +212,26 @@ var CloudflareSFUClient = class {
|
|
|
149
212
|
#replaceMediaConnection;
|
|
150
213
|
#remoteListeners = /* @__PURE__ */ new Set();
|
|
151
214
|
#snapshotListeners = /* @__PURE__ */ new Set();
|
|
215
|
+
#locallyPausedSources = /* @__PURE__ */ new Set();
|
|
152
216
|
#localTracks = /* @__PURE__ */ new Map();
|
|
153
217
|
#reusableLocalTransceivers = /* @__PURE__ */ new Map();
|
|
218
|
+
#reusableLocalPublicationIds = /* @__PURE__ */ new Map();
|
|
154
219
|
#remoteTracks = /* @__PURE__ */ new Map();
|
|
155
220
|
#bootstrap;
|
|
156
221
|
#connection;
|
|
222
|
+
#disposeConnectionObservation;
|
|
157
223
|
#connectionEpoch = 0;
|
|
224
|
+
#retiredLocalConnection = null;
|
|
158
225
|
#cursor = null;
|
|
226
|
+
#remotePullIncomplete = false;
|
|
227
|
+
#remotePullRetryAfter = /* @__PURE__ */ new Map();
|
|
159
228
|
#generation = 0;
|
|
160
229
|
#negotiatedGeneration = null;
|
|
161
230
|
#replacementAttemptedGeneration = null;
|
|
162
231
|
#polling = false;
|
|
232
|
+
#pollAfterCurrent = false;
|
|
233
|
+
#pollFailures = 0;
|
|
234
|
+
#pollFailedPull = false;
|
|
163
235
|
#pollTimer;
|
|
164
236
|
#sdpTail = Promise.resolve();
|
|
165
237
|
#snapshot;
|
|
@@ -171,11 +243,12 @@ var CloudflareSFUClient = class {
|
|
|
171
243
|
this.#participantId = options.participantId;
|
|
172
244
|
this.#bootstrap = options.bootstrap;
|
|
173
245
|
this.#transport = options.transport;
|
|
174
|
-
this.#pollIntervalMs = options.pollIntervalMs ??
|
|
246
|
+
this.#pollIntervalMs = options.pollIntervalMs ?? 15e3;
|
|
175
247
|
this.#replaceMediaConnection = options.replaceMediaConnection;
|
|
176
248
|
this.#onError = options.onError;
|
|
177
249
|
this.#onRemoteTrack = options.onRemoteTrack;
|
|
178
250
|
this.#onRtcSummary = options.onRtcSummary;
|
|
251
|
+
this.#recordReconnect = options.recordReconnect;
|
|
179
252
|
this.#onScreenEnded = options.onScreenEnded;
|
|
180
253
|
this.#peerConnectionFactory = options.peerConnectionFactory;
|
|
181
254
|
this.#connection = this.#createPeerConnection(options.bootstrap);
|
|
@@ -194,6 +267,19 @@ var CloudflareSFUClient = class {
|
|
|
194
267
|
subscribe(listener) {
|
|
195
268
|
return subscribeSnapshot(this.#snapshotListeners, listener);
|
|
196
269
|
}
|
|
270
|
+
remotePublicationResumed(publicationId) {
|
|
271
|
+
this.#remotePullRetryAfter.delete(publicationId);
|
|
272
|
+
this.remotePublicationsChanged();
|
|
273
|
+
}
|
|
274
|
+
remotePublicationsChanged() {
|
|
275
|
+
recordReconnect(this.#recordReconnect, "remote_publications_changed");
|
|
276
|
+
if (this.#polling) {
|
|
277
|
+
this.#pollAfterCurrent = true;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.#clearPoll();
|
|
281
|
+
this.#schedulePoll(0);
|
|
282
|
+
}
|
|
197
283
|
prepareLocalTrack(source, track) {
|
|
198
284
|
this.#requireActive();
|
|
199
285
|
validateTrackSource(source, track);
|
|
@@ -202,7 +288,7 @@ var CloudflareSFUClient = class {
|
|
|
202
288
|
source,
|
|
203
289
|
track,
|
|
204
290
|
transceiver: this.#reusableLocalTransceivers.get(source) ?? null,
|
|
205
|
-
providerPublicationId: null,
|
|
291
|
+
providerPublicationId: this.#reusableLocalPublicationIds.get(source) ?? null,
|
|
206
292
|
pendingOperationId: null,
|
|
207
293
|
pendingTrackName: null,
|
|
208
294
|
desiredEnabled: false,
|
|
@@ -210,6 +296,7 @@ var CloudflareSFUClient = class {
|
|
|
210
296
|
endedListener: null
|
|
211
297
|
};
|
|
212
298
|
this.#reusableLocalTransceivers.delete(source);
|
|
299
|
+
this.#reusableLocalPublicationIds.delete(source);
|
|
213
300
|
if (source === "screen") {
|
|
214
301
|
state.endedListener = () => {
|
|
215
302
|
if (this.#localTracks.get("screen") === state) this.#invokeListener(() => this.#onScreenEnded?.());
|
|
@@ -225,6 +312,7 @@ var CloudflareSFUClient = class {
|
|
|
225
312
|
if (!state) return;
|
|
226
313
|
if (state.enabled) await this.#setPreparedTrackEnabled(state, false);
|
|
227
314
|
if (state.transceiver) this.#reusableLocalTransceivers.set(source, state.transceiver);
|
|
315
|
+
if (state.providerPublicationId) this.#reusableLocalPublicationIds.set(source, state.providerPublicationId);
|
|
228
316
|
this.#removeOwnedLocalTrack(state);
|
|
229
317
|
this.#localTracks.delete(source);
|
|
230
318
|
this.#publishSnapshot();
|
|
@@ -246,30 +334,124 @@ var CloudflareSFUClient = class {
|
|
|
246
334
|
if (!this.#started || this.#stopped || this.#polling) return;
|
|
247
335
|
this.#polling = true;
|
|
248
336
|
const generation = this.#generation;
|
|
337
|
+
this.#pollFailedPull = false;
|
|
249
338
|
try {
|
|
250
339
|
const transport = this.#requireTransport();
|
|
251
|
-
const authoritative = await transport.listPublications();
|
|
340
|
+
const authoritative = await traceReconnect(this.#recordReconnect, "list_publications", () => transport.listPublications());
|
|
252
341
|
this.#requireGeneration(generation);
|
|
253
342
|
await this.#reconcileRemotePublications(authoritative, generation);
|
|
343
|
+
this.#requireGeneration(generation);
|
|
344
|
+
if (!this.#remotePullIncomplete) this.#pollFailures = 0;
|
|
254
345
|
} catch (error) {
|
|
255
346
|
if (generation === this.#generation && !this.#stopped) this.#reportError(error);
|
|
256
347
|
throw error;
|
|
257
348
|
} finally {
|
|
258
|
-
|
|
349
|
+
this.#finishPublicationPoll(generation);
|
|
259
350
|
}
|
|
260
351
|
}
|
|
352
|
+
#finishPublicationPoll(generation) {
|
|
353
|
+
if (generation !== this.#generation) return;
|
|
354
|
+
this.#polling = false;
|
|
355
|
+
if (!this.#pollAfterCurrent) return;
|
|
356
|
+
this.#pollAfterCurrent = false;
|
|
357
|
+
this.#clearPoll();
|
|
358
|
+
this.#schedulePoll(0);
|
|
359
|
+
}
|
|
261
360
|
async setLocalPublicationTarget(target) {
|
|
262
361
|
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
263
362
|
if (resolved.kind === "result") return resolved.result;
|
|
264
363
|
const state = resolved.value;
|
|
265
|
-
if (
|
|
364
|
+
if (this.#pausedTarget(target)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
266
365
|
try {
|
|
366
|
+
if (target.enabled) await this.#retireUnavailableLocalPublication(state);
|
|
367
|
+
if (this.#pausedTarget(target)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
368
|
+
if (state.enabled === target.enabled) return { outcome: "satisfied", errorCode: null };
|
|
267
369
|
await this.#setPreparedTrackEnabled(state, target.enabled, target.operationId);
|
|
268
370
|
return { outcome: "confirmed", errorCode: null };
|
|
269
371
|
} catch (error) {
|
|
270
372
|
if (!this.#stopped) this.#reportError(error);
|
|
271
|
-
return
|
|
373
|
+
return mediaTargetFailure(error);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
#pausedTarget(target) {
|
|
377
|
+
return target.enabled && this.#locallyPausedSources.has(target.source);
|
|
378
|
+
}
|
|
379
|
+
setLocalSourceIntent(source, enabled) {
|
|
380
|
+
if (enabled) {
|
|
381
|
+
this.#locallyPausedSources.delete(source);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
this.#locallyPausedSources.add(source);
|
|
385
|
+
const state = this.#localTracks.get(source);
|
|
386
|
+
if (!state) return;
|
|
387
|
+
state.desiredEnabled = false;
|
|
388
|
+
state.track.enabled = false;
|
|
389
|
+
}
|
|
390
|
+
async closeForcedLocalPublication(source) {
|
|
391
|
+
this.setLocalSourceIntent(source, false);
|
|
392
|
+
await this.#retireLocalPublication(source);
|
|
393
|
+
if (!this.#replaceMediaConnection) return;
|
|
394
|
+
const generation = this.#generation;
|
|
395
|
+
await this.#serializeSDP(async () => {
|
|
396
|
+
const existing = new Set([...this.#localTracks.values()].filter((state) => state.source !== source && state.providerPublicationId !== null));
|
|
397
|
+
const states = await this.#prepareConnectionForPublication([], generation);
|
|
398
|
+
await this.#publishPreparedTracksSerialized(
|
|
399
|
+
states.filter((state) => existing.has(state)),
|
|
400
|
+
generation,
|
|
401
|
+
this.#connection,
|
|
402
|
+
this.#bootstrap.connectionId
|
|
403
|
+
);
|
|
404
|
+
this.#requireGeneration(generation);
|
|
405
|
+
this.#setPhase("live", null);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
async #retireUnavailableLocalPublication(state) {
|
|
409
|
+
const publicationId = state.providerPublicationId;
|
|
410
|
+
if (!publicationId) return;
|
|
411
|
+
const authoritative = await traceReconnect(this.#recordReconnect, "list_publications", () => this.#requireTransport().listPublications());
|
|
412
|
+
if (authoritative.publications.some((publication) => publication.publicationId === publicationId)) return;
|
|
413
|
+
if (state.providerPublicationId === publicationId) await this.#retireLocalPublication(state.source);
|
|
414
|
+
}
|
|
415
|
+
async #retireLocalPublication(source) {
|
|
416
|
+
const state = this.#localTracks.get(source);
|
|
417
|
+
const transceiver = state?.transceiver ?? this.#reusableLocalTransceivers.get(source);
|
|
418
|
+
const publicationId = state?.providerPublicationId ?? this.#reusableLocalPublicationIds.get(source);
|
|
419
|
+
this.#reusableLocalTransceivers.delete(source);
|
|
420
|
+
this.#reusableLocalPublicationIds.delete(source);
|
|
421
|
+
if (state) {
|
|
422
|
+
state.track.enabled = false;
|
|
423
|
+
state.desiredEnabled = false;
|
|
424
|
+
state.enabled = false;
|
|
425
|
+
state.providerPublicationId = null;
|
|
426
|
+
state.transceiver = null;
|
|
272
427
|
}
|
|
428
|
+
this.#publishSnapshot();
|
|
429
|
+
this.#emitLocal();
|
|
430
|
+
if (!transceiver || !publicationId) return;
|
|
431
|
+
const connection = this.#connection;
|
|
432
|
+
const connectionId = this.#bootstrap.connectionId;
|
|
433
|
+
this.#retiredLocalConnection = connection;
|
|
434
|
+
await this.#serializeSDP(async () => {
|
|
435
|
+
if (connection !== this.#connection) return;
|
|
436
|
+
const mid = requireTransceiverMid(transceiver);
|
|
437
|
+
await this.#boundPeerOperation(transceiver.sender.replaceTrack(null));
|
|
438
|
+
transceiver.stop();
|
|
439
|
+
const offer = await traceReconnect(this.#recordReconnect, "create_offer", () => connection.createOffer());
|
|
440
|
+
await traceReconnect(this.#recordReconnect, "set_local_description", () => connection.setLocalDescription(offer));
|
|
441
|
+
try {
|
|
442
|
+
const response = await this.#requireTransport().closeTracks({
|
|
443
|
+
connectionId,
|
|
444
|
+
...providerDescription(requireDescription(offer)),
|
|
445
|
+
tracks: [{ mid, source, publicationId }],
|
|
446
|
+
force: false
|
|
447
|
+
});
|
|
448
|
+
await this.#applyProviderDescription(response, connection);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
await this.#rollbackLocalOffer(connection);
|
|
451
|
+
if (!this.#stopped) this.#reportError(error);
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
273
455
|
}
|
|
274
456
|
observeLocalPublications(listener) {
|
|
275
457
|
this.#localListeners.add(listener);
|
|
@@ -285,26 +467,29 @@ var CloudflareSFUClient = class {
|
|
|
285
467
|
this.#requireActive();
|
|
286
468
|
const options = "connectionId" in input ? { bootstrap: input } : input;
|
|
287
469
|
validateBootstrap(options.bootstrap);
|
|
470
|
+
recordReconnect(this.#recordReconnect, "media_rebuild_start");
|
|
288
471
|
const generation = ++this.#generation;
|
|
289
472
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
290
473
|
this.#polling = false;
|
|
474
|
+
this.#pollAfterCurrent = false;
|
|
291
475
|
this.#clearPoll();
|
|
476
|
+
this.#remotePullRetryAfter.clear();
|
|
292
477
|
this.#disposeConnection(false);
|
|
293
478
|
this.#reusableLocalTransceivers.clear();
|
|
479
|
+
this.#reusableLocalPublicationIds.clear();
|
|
294
480
|
this.#clearRemoteTracks();
|
|
295
481
|
this.#cursor = null;
|
|
296
482
|
this.#negotiatedGeneration = null;
|
|
297
483
|
this.#replacementAttemptedGeneration = null;
|
|
484
|
+
if (options.bootstrap.connectionId !== this.#bootstrap.connectionId) this.#started = false;
|
|
298
485
|
this.#bootstrap = options.bootstrap;
|
|
299
486
|
if (options.transport) this.#transport = options.transport;
|
|
300
|
-
this.#
|
|
301
|
-
this.#observeConnection(this.#connection, generation, connectionEpoch);
|
|
487
|
+
this.#rebuildPeerConnection(options.bootstrap, generation, connectionEpoch);
|
|
302
488
|
for (const state of this.#localTracks.values()) {
|
|
303
489
|
state.transceiver = null;
|
|
304
490
|
state.enabled = false;
|
|
305
491
|
}
|
|
306
492
|
const enabled = [...this.#localTracks.values()].filter((state) => state.desiredEnabled && state.track.readyState !== "ended");
|
|
307
|
-
this.#setPhase("recovering", null);
|
|
308
493
|
await this.#activatePreparedTracks(enabled, generation);
|
|
309
494
|
}
|
|
310
495
|
async #activatePreparedTracks(states, generation) {
|
|
@@ -325,9 +510,12 @@ var CloudflareSFUClient = class {
|
|
|
325
510
|
this.#generation++;
|
|
326
511
|
this.#connectionEpoch++;
|
|
327
512
|
this.#clearPoll();
|
|
513
|
+
this.#remotePullRetryAfter.clear();
|
|
328
514
|
this.#polling = false;
|
|
515
|
+
this.#pollAfterCurrent = false;
|
|
329
516
|
this.#disposeConnection(true);
|
|
330
517
|
this.#reusableLocalTransceivers.clear();
|
|
518
|
+
this.#reusableLocalPublicationIds.clear();
|
|
331
519
|
this.#clearRemoteTracks();
|
|
332
520
|
for (const state of this.#localTracks.values()) this.#removeOwnedLocalTrack(state);
|
|
333
521
|
this.#localTracks.clear();
|
|
@@ -346,14 +534,33 @@ var CloudflareSFUClient = class {
|
|
|
346
534
|
return this.#disablePreparedTrack(state);
|
|
347
535
|
}
|
|
348
536
|
async #enablePreparedTrack(state, operationId) {
|
|
537
|
+
if (state.providerPublicationId && state.transceiver) {
|
|
538
|
+
state.desiredEnabled = true;
|
|
539
|
+
state.track.enabled = true;
|
|
540
|
+
try {
|
|
541
|
+
await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(state.track));
|
|
542
|
+
} catch (error) {
|
|
543
|
+
state.desiredEnabled = false;
|
|
544
|
+
state.track.enabled = false;
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
state.enabled = !this.#locallyPausedSources.has(state.source);
|
|
548
|
+
state.desiredEnabled = state.enabled;
|
|
549
|
+
state.track.enabled = state.enabled;
|
|
550
|
+
if (!state.enabled) await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(null));
|
|
551
|
+
this.#publishSnapshot();
|
|
552
|
+
this.#emitLocal();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
349
555
|
if (operationId && state.pendingOperationId !== operationId) {
|
|
350
556
|
state.pendingOperationId = operationId;
|
|
351
557
|
state.pendingTrackName = `${state.source}-${operationId}`;
|
|
352
558
|
}
|
|
353
559
|
state.desiredEnabled = true;
|
|
354
560
|
state.track.enabled = true;
|
|
561
|
+
const generation = this.#generation;
|
|
355
562
|
try {
|
|
356
|
-
await this.#publishPreparedTracks([state],
|
|
563
|
+
await this.#publishPreparedTracks([state], generation);
|
|
357
564
|
} catch (error) {
|
|
358
565
|
state.desiredEnabled = false;
|
|
359
566
|
state.enabled = false;
|
|
@@ -365,26 +572,21 @@ var CloudflareSFUClient = class {
|
|
|
365
572
|
const transceiver = state.transceiver;
|
|
366
573
|
state.desiredEnabled = false;
|
|
367
574
|
state.track.enabled = false;
|
|
368
|
-
|
|
369
|
-
if (transceiver) await transceiver.sender.replaceTrack(null);
|
|
370
|
-
} catch (error) {
|
|
371
|
-
state.desiredEnabled = true;
|
|
372
|
-
state.track.enabled = true;
|
|
373
|
-
throw error;
|
|
374
|
-
}
|
|
575
|
+
const detach = transceiver ? this.#boundPeerOperation(transceiver.sender.replaceTrack(null)) : Promise.resolve();
|
|
375
576
|
state.enabled = false;
|
|
376
|
-
state.providerPublicationId = null;
|
|
377
577
|
state.pendingOperationId = null;
|
|
378
578
|
state.pendingTrackName = null;
|
|
379
579
|
this.#publishSnapshot();
|
|
380
580
|
this.#emitLocal();
|
|
581
|
+
await detach;
|
|
381
582
|
}
|
|
382
583
|
async #publishPreparedTracks(states, generation) {
|
|
383
584
|
if (states.length === 0) return;
|
|
384
585
|
await this.#serializeSDP(async () => {
|
|
385
|
-
await this.#
|
|
586
|
+
const pending = await this.#prepareConnectionForPublication(states, generation);
|
|
587
|
+
this.#replacementAttemptedGeneration = null;
|
|
386
588
|
try {
|
|
387
|
-
await this.#publishPreparedTracksSerialized(
|
|
589
|
+
await this.#publishPreparedTracksSerialized(pending, generation, this.#connection, this.#bootstrap.connectionId);
|
|
388
590
|
} catch (error) {
|
|
389
591
|
if (!this.#replaceMediaConnection || !this.#isRetryableConnectionFailure(error) || this.#replacementAttemptedGeneration === generation) throw error;
|
|
390
592
|
await this.#replaceMediaConnectionForRecovery(generation);
|
|
@@ -397,6 +599,25 @@ var CloudflareSFUClient = class {
|
|
|
397
599
|
}
|
|
398
600
|
});
|
|
399
601
|
}
|
|
602
|
+
async #prepareConnectionForPublication(states, generation) {
|
|
603
|
+
if (this.#retiredLocalConnection !== this.#connection || !this.#replaceMediaConnection) {
|
|
604
|
+
await this.#replaceDormantConnectionBeforeNegotiation(generation);
|
|
605
|
+
return states;
|
|
606
|
+
}
|
|
607
|
+
const authoritative = await traceReconnect(this.#recordReconnect, "list_publications", () => this.#requireTransport().listPublications());
|
|
608
|
+
this.#pauseUnavailableOtherSources(authoritative.publications, states);
|
|
609
|
+
this.#replacementAttemptedGeneration = null;
|
|
610
|
+
await this.#replaceMediaConnectionForRecovery(generation);
|
|
611
|
+
return [...this.#localTracks.values()].filter((state) => (state.desiredEnabled || states.includes(state)) && state.track.readyState !== "ended");
|
|
612
|
+
}
|
|
613
|
+
#pauseUnavailableOtherSources(publications, states) {
|
|
614
|
+
for (const state of this.#localTracks.values()) {
|
|
615
|
+
if (states.includes(state) || !state.providerPublicationId) continue;
|
|
616
|
+
if (publications.some((publication) => publication.publicationId === state.providerPublicationId)) continue;
|
|
617
|
+
state.desiredEnabled = false;
|
|
618
|
+
state.track.enabled = false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
400
621
|
async #publishPreparedTracksSerialized(states, generation, connection, connectionId) {
|
|
401
622
|
this.#requireGeneration(generation);
|
|
402
623
|
const pendingStates = states.filter((state) => !state.enabled);
|
|
@@ -404,7 +625,7 @@ var CloudflareSFUClient = class {
|
|
|
404
625
|
const wasLive = connectionIsLive(connection);
|
|
405
626
|
const publications = [];
|
|
406
627
|
try {
|
|
407
|
-
for (const state of pendingStates) publications.push(await this.#prepareLocalPublication(connection, state));
|
|
628
|
+
for (const state of pendingStates) publications.push(await this.#prepareLocalPublication(connection, state, generation));
|
|
408
629
|
const response = await this.#negotiateLocalPublications(connection, connectionId, publications, generation);
|
|
409
630
|
if (!wasLive) await this.#waitForConnection(connection, generation);
|
|
410
631
|
this.#requireGeneration(generation);
|
|
@@ -413,21 +634,56 @@ var CloudflareSFUClient = class {
|
|
|
413
634
|
this.#publishSnapshot();
|
|
414
635
|
this.#emitLocal();
|
|
415
636
|
} catch (error) {
|
|
416
|
-
|
|
417
|
-
|
|
637
|
+
if (generation === this.#generation) {
|
|
638
|
+
await this.#rollbackLocalOffer(connection);
|
|
639
|
+
await this.#discardLocalPublications(publications);
|
|
640
|
+
}
|
|
418
641
|
throw error;
|
|
419
642
|
}
|
|
420
643
|
}
|
|
421
|
-
async #prepareLocalPublication(connection, state) {
|
|
644
|
+
async #prepareLocalPublication(connection, state, generation) {
|
|
422
645
|
const reusedTransceiver = state.transceiver !== null;
|
|
423
|
-
const transceiver = state.transceiver ?? connection
|
|
424
|
-
if (reusedTransceiver) await transceiver.sender.replaceTrack(state.track);
|
|
646
|
+
const transceiver = state.transceiver ?? this.#addLocalTransceiver(connection, state);
|
|
647
|
+
if (reusedTransceiver) await this.#boundPeerOperation(transceiver.sender.replaceTrack(state.track));
|
|
648
|
+
this.#requireGeneration(generation);
|
|
425
649
|
state.transceiver = transceiver;
|
|
426
650
|
return { state, transceiver, trackName: state.pendingTrackName ?? `${state.source}-${globalThis.crypto.randomUUID()}`, reusedTransceiver };
|
|
427
651
|
}
|
|
652
|
+
#addLocalTransceiver(connection, state) {
|
|
653
|
+
if (state.source !== "camera" || firefoxCameraSimulcastUnsupported()) return connection.addTransceiver(state.track, { direction: "sendonly" });
|
|
654
|
+
const sendEncodings = [
|
|
655
|
+
{ rid: "h", scaleResolutionDownBy: 1, maxBitrate: 25e5, scalabilityMode: "L1T1" },
|
|
656
|
+
{ rid: "l", scaleResolutionDownBy: 2, maxBitrate: 65e4, scalabilityMode: "L1T1" }
|
|
657
|
+
];
|
|
658
|
+
try {
|
|
659
|
+
return connection.addTransceiver(state.track, {
|
|
660
|
+
direction: "sendonly",
|
|
661
|
+
sendEncodings
|
|
662
|
+
});
|
|
663
|
+
} catch (error) {
|
|
664
|
+
if (!simulcastUnsupported(error)) throw error;
|
|
665
|
+
return connection.addTransceiver(state.track, { direction: "sendonly" });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
428
668
|
async #negotiateLocalPublications(connection, connectionId, publications, generation) {
|
|
429
|
-
|
|
430
|
-
|
|
669
|
+
let offer;
|
|
670
|
+
try {
|
|
671
|
+
offer = await traceReconnect(this.#recordReconnect, "create_offer", () => connection.createOffer());
|
|
672
|
+
await traceReconnect(this.#recordReconnect, "set_local_description", () => connection.setLocalDescription(offer));
|
|
673
|
+
} catch (error) {
|
|
674
|
+
const cameras = publications.filter(({ state, transceiver }) => state.source === "camera" && transceiver.sender.getParameters().encodings.length > 1);
|
|
675
|
+
if (!simulcastUnsupported(error) || cameras.length === 0) throw error;
|
|
676
|
+
await this.#rollbackLocalOffer(connection);
|
|
677
|
+
this.#requireGeneration(generation);
|
|
678
|
+
for (const publication of cameras) {
|
|
679
|
+
await this.#boundPeerOperation(publication.transceiver.sender.replaceTrack(null));
|
|
680
|
+
publication.transceiver.stop();
|
|
681
|
+
publication.transceiver = connection.addTransceiver(publication.state.track, { direction: "sendonly" });
|
|
682
|
+
publication.state.transceiver = publication.transceiver;
|
|
683
|
+
}
|
|
684
|
+
offer = await traceReconnect(this.#recordReconnect, "create_offer", () => connection.createOffer());
|
|
685
|
+
await traceReconnect(this.#recordReconnect, "set_local_description", () => connection.setLocalDescription(offer));
|
|
686
|
+
}
|
|
431
687
|
const tracks = publications.map(
|
|
432
688
|
({ state, transceiver, trackName }) => ({
|
|
433
689
|
location: "local",
|
|
@@ -436,17 +692,20 @@ var CloudflareSFUClient = class {
|
|
|
436
692
|
source: state.source
|
|
437
693
|
})
|
|
438
694
|
);
|
|
439
|
-
const response = await this.#requireTransport().addTracks({ connectionId,
|
|
695
|
+
const response = await traceReconnect(this.#recordReconnect, "publish_tracks", () => this.#requireTransport().addTracks({ connectionId, ...providerDescription(requireDescription(offer)), tracks }));
|
|
440
696
|
this.#requireGeneration(generation);
|
|
441
|
-
await
|
|
697
|
+
await this.#applyProviderDescription(response, connection);
|
|
442
698
|
return response;
|
|
443
699
|
}
|
|
444
700
|
#confirmLocalPublications(publications, tracks) {
|
|
445
701
|
for (const { state, transceiver } of publications) {
|
|
446
702
|
const authoritative = tracks?.find((track) => track.location === "local" && track.mid === transceiver.mid && track.source === state.source);
|
|
447
703
|
if (!authoritative?.publicationId) throw new CloudflareSFUError("Chalk did not return an authoritative local publication ID", "invalid_publication");
|
|
448
|
-
state.
|
|
449
|
-
state.enabled =
|
|
704
|
+
if (state.transceiver !== transceiver) continue;
|
|
705
|
+
state.enabled = !this.#locallyPausedSources.has(state.source);
|
|
706
|
+
state.desiredEnabled = state.enabled;
|
|
707
|
+
state.track.enabled = state.enabled;
|
|
708
|
+
if (!state.enabled) void transceiver.sender.replaceTrack(null).catch((error) => this.#reportError(error));
|
|
450
709
|
state.providerPublicationId = authoritative.publicationId;
|
|
451
710
|
state.pendingOperationId = null;
|
|
452
711
|
state.pendingTrackName = null;
|
|
@@ -482,9 +741,10 @@ var CloudflareSFUClient = class {
|
|
|
482
741
|
const cursor = this.#validatedRemotePublicationCursor(authoritative);
|
|
483
742
|
if (cursor === null) return;
|
|
484
743
|
const ordering = comparePublicationCursor(this.#cursor, cursor);
|
|
485
|
-
if (ordering
|
|
744
|
+
if (ordering === "stale" || ordering === "same" && !this.#remotePullIncomplete) return;
|
|
486
745
|
const desired = this.#desiredRemotePublications(authoritative, cursor);
|
|
487
|
-
const toPull =
|
|
746
|
+
const toPull = this.#pendingRemotePulls(desired);
|
|
747
|
+
this.#pollFailedPull = true;
|
|
488
748
|
const pulled = await this.#pullWithRecovery(toPull, cursor, generation);
|
|
489
749
|
if (pulled === null) return;
|
|
490
750
|
this.#requireGeneration(generation);
|
|
@@ -493,10 +753,24 @@ var CloudflareSFUClient = class {
|
|
|
493
753
|
this.#remoteTracks.clear();
|
|
494
754
|
for (const [key, publication] of next) this.#remoteTracks.set(key, publication);
|
|
495
755
|
this.#cursor = cursor;
|
|
756
|
+
this.#remotePullIncomplete = [...desired].some(([key, publication]) => next.get(key)?.publicationId !== publication.publicationId);
|
|
496
757
|
for (const publication of pulled) this.#invokeListener(() => this.#onRemoteTrack?.(publication));
|
|
497
758
|
this.#publishSnapshot();
|
|
498
759
|
this.#emitRemote();
|
|
499
760
|
}
|
|
761
|
+
#pendingRemotePulls(desired) {
|
|
762
|
+
const desiredIds = new Set([...desired.values()].map((publication) => publication.publicationId));
|
|
763
|
+
for (const publicationId of this.#remotePullRetryAfter.keys()) {
|
|
764
|
+
if (!desiredIds.has(publicationId)) this.#remotePullRetryAfter.delete(publicationId);
|
|
765
|
+
}
|
|
766
|
+
const now = Date.now();
|
|
767
|
+
return [...desired].filter(([key, publication]) => {
|
|
768
|
+
if (this.#remoteTracks.get(key)?.publicationId === publication.publicationId) return false;
|
|
769
|
+
const remaining = (this.#remotePullRetryAfter.get(publication.publicationId) ?? 0) - now;
|
|
770
|
+
if (remaining > 0) recordReconnect(this.#recordReconnect, "remote_pull_deferred", { remaining_ms: remaining, source: publication.source });
|
|
771
|
+
return remaining <= 0;
|
|
772
|
+
}).map(([, publication]) => publication);
|
|
773
|
+
}
|
|
500
774
|
#validatedRemotePublicationCursor(authoritative) {
|
|
501
775
|
if (this.#cursor?.signature === INVALID_PUBLICATION_SIGNATURE && this.#cursor.incarnation === authoritative.incarnation && this.#cursor.sequence === authoritative.sequence) return null;
|
|
502
776
|
try {
|
|
@@ -520,7 +794,7 @@ var CloudflareSFUClient = class {
|
|
|
520
794
|
} catch (error) {
|
|
521
795
|
if (generation !== this.#generation || this.#stopped) throw error;
|
|
522
796
|
if (!this.#canReplaceConnectionAfterRemotePull(error, generation)) {
|
|
523
|
-
this.#observeRemotePublicationCursor(cursor);
|
|
797
|
+
this.#observeRemotePublicationCursor(cursor, retryableRemotePull(error));
|
|
524
798
|
throw error;
|
|
525
799
|
}
|
|
526
800
|
return this.#retryRemotePullOnReplacement(publications, cursor, generation);
|
|
@@ -539,7 +813,7 @@ var CloudflareSFUClient = class {
|
|
|
539
813
|
return await this.#pull(publications, generation);
|
|
540
814
|
} catch (error) {
|
|
541
815
|
if (generation !== this.#generation || this.#stopped) throw error;
|
|
542
|
-
this.#observeRemotePublicationCursor(cursor);
|
|
816
|
+
this.#observeRemotePublicationCursor(cursor, retryableRemotePull(error));
|
|
543
817
|
throw error;
|
|
544
818
|
}
|
|
545
819
|
}
|
|
@@ -550,7 +824,15 @@ var CloudflareSFUClient = class {
|
|
|
550
824
|
const connectionId = this.#bootstrap.connectionId;
|
|
551
825
|
const requested = publications.map((publication) => {
|
|
552
826
|
const reference = parseCloudflareSFUPublicationID(publication.publicationId);
|
|
553
|
-
return {
|
|
827
|
+
return {
|
|
828
|
+
location: "remote",
|
|
829
|
+
sessionId: reference.connectionId,
|
|
830
|
+
trackName: reference.trackName,
|
|
831
|
+
// h sorts before l: prefer full camera quality, but keep video flowing
|
|
832
|
+
// when the publisher pauses h. Do not silently downgrade a live h layer.
|
|
833
|
+
// https://developers.cloudflare.com/realtime/sfu/features/simulcast/#quality-control
|
|
834
|
+
...publication.source === "camera" ? { simulcast: { preferredRid: "h", priorityOrdering: "none", ridNotAvailable: "asciibetical" } } : {}
|
|
835
|
+
};
|
|
554
836
|
});
|
|
555
837
|
const received = /* @__PURE__ */ new Map();
|
|
556
838
|
const onTrack = (event) => {
|
|
@@ -559,17 +841,20 @@ var CloudflareSFUClient = class {
|
|
|
559
841
|
connection.addEventListener("track", onTrack);
|
|
560
842
|
try {
|
|
561
843
|
this.#requireGeneration(generation);
|
|
562
|
-
const response = await this.#requireTransport().addTracks({ connectionId, tracks: requested });
|
|
844
|
+
const response = await traceReconnect(this.#recordReconnect, "pull_tracks", () => this.#requireTransport().addTracks({ connectionId, tracks: requested, allowPartialRemoteTracks: true }));
|
|
563
845
|
this.#requireGeneration(generation);
|
|
846
|
+
this.#recordRemotePullErrors(publications, response.trackErrors ?? []);
|
|
564
847
|
const responseTracks = response.tracks ?? [];
|
|
848
|
+
const matched = matchRemotePullTracks(publications, responseTracks);
|
|
849
|
+
for (const { publication } of matched) this.#remotePullRetryAfter.delete(publication.publicationId);
|
|
565
850
|
await this.#completeRenegotiation(response, connection, connectionId, generation);
|
|
851
|
+
if (matched.length === 0) return [];
|
|
566
852
|
await this.#waitForConnection(connection, generation);
|
|
567
853
|
this.#negotiatedGeneration = generation;
|
|
568
|
-
await waitFor(() => responseTracks.every((track) => track.mid !== void 0 && received.has(track.mid)), 5e3);
|
|
854
|
+
await traceReconnect(this.#recordReconnect, "remote_track_arrival", () => waitFor(() => responseTracks.every((track) => track.mid !== void 0 && received.has(track.mid)), 5e3));
|
|
569
855
|
this.#requireGeneration(generation);
|
|
570
|
-
return
|
|
571
|
-
const
|
|
572
|
-
const track = responseTrack?.mid === void 0 ? void 0 : received.get(responseTrack.mid);
|
|
856
|
+
return matched.map(({ publication, mid }) => {
|
|
857
|
+
const track = received.get(mid);
|
|
573
858
|
if (!track) throw new CloudflareSFUError("A negotiated remote track did not arrive", "media_failed");
|
|
574
859
|
return Object.freeze({ ...publication, track });
|
|
575
860
|
});
|
|
@@ -581,38 +866,151 @@ var CloudflareSFUClient = class {
|
|
|
581
866
|
}
|
|
582
867
|
});
|
|
583
868
|
}
|
|
869
|
+
#recordRemotePullErrors(publications, failures) {
|
|
870
|
+
const requested = new Map(
|
|
871
|
+
publications.map((publication) => {
|
|
872
|
+
const reference = parseCloudflareSFUPublicationID(publication.publicationId);
|
|
873
|
+
return [`${reference.connectionId}\0${reference.trackName}`, publication];
|
|
874
|
+
})
|
|
875
|
+
);
|
|
876
|
+
for (const failure of failures) {
|
|
877
|
+
const publication = requested.get(`${failure.connectionId}\0${failure.trackName}`);
|
|
878
|
+
if (!publication) throw new CloudflareSFUError("Cloudflare SFU returned an unrequested remote track error", "invalid_publication");
|
|
879
|
+
const delay = 750;
|
|
880
|
+
this.#remotePullRetryAfter.set(publication.publicationId, Date.now() + delay);
|
|
881
|
+
recordReconnect(this.#recordReconnect, "remote_pull_backoff", { delay_ms: delay, retry_kind: failure.code === "empty_track_error" ? "empty_track" : "provider_error", source: publication.source });
|
|
882
|
+
this.#reportError(new CloudflareSFUError(`Cloudflare SFU remote track failed: ${failure.code}`, "media_failed", { providerCode: failure.code }));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
584
885
|
async #completeRenegotiation(response, connection, connectionId, generation) {
|
|
585
886
|
if (!response.requiresImmediateRenegotiation) return;
|
|
586
|
-
|
|
587
|
-
await connection.
|
|
588
|
-
|
|
589
|
-
await
|
|
590
|
-
await this.#requireTransport().renegotiate({ connectionId, sessionDescription: requireDescription(answer) });
|
|
887
|
+
await this.#applyProviderDescription(response, connection);
|
|
888
|
+
const answer = await traceReconnect(this.#recordReconnect, "create_answer", () => connection.createAnswer());
|
|
889
|
+
await traceReconnect(this.#recordReconnect, "set_local_answer", () => connection.setLocalDescription(answer));
|
|
890
|
+
await traceReconnect(this.#recordReconnect, "renegotiate", () => this.#requireTransport().renegotiate({ connectionId, ...providerDescription(requireDescription(answer)) }));
|
|
591
891
|
this.#requireGeneration(generation);
|
|
592
892
|
}
|
|
893
|
+
async #applyProviderDescription(response, connection = this.#connection) {
|
|
894
|
+
await traceReconnect(this.#recordReconnect, "set_remote_description", () => connection.setRemoteDescription(requireSFUDescription(response.sessionDescription)));
|
|
895
|
+
}
|
|
593
896
|
#serializeSDP(operation) {
|
|
594
|
-
const
|
|
897
|
+
const generation = this.#generation;
|
|
898
|
+
const queuedAt = performance.now();
|
|
899
|
+
let expired = false;
|
|
900
|
+
let timer;
|
|
901
|
+
const timeout = new CloudflareSFUError("Cloudflare SFU negotiation timed out. Try again.", "negotiation_timeout");
|
|
902
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
903
|
+
timer = globalThis.setTimeout(() => {
|
|
904
|
+
expired = true;
|
|
905
|
+
if (generation === this.#generation && !this.#stopped) this.#expireNegotiation(timeout);
|
|
906
|
+
reject(timeout);
|
|
907
|
+
}, NEGOTIATION_TIMEOUT_MS);
|
|
908
|
+
});
|
|
909
|
+
const run = () => {
|
|
910
|
+
if (expired || generation !== this.#generation) throw timeout;
|
|
911
|
+
recordReconnect(this.#recordReconnect, "sdp_queue", { duration_ms: performance.now() - queuedAt });
|
|
912
|
+
return operation();
|
|
913
|
+
};
|
|
914
|
+
const pending = this.#sdpTail.then(run, run);
|
|
915
|
+
const result = Promise.race([pending, deadline]).catch((error) => {
|
|
916
|
+
if (error instanceof CloudflareSFUError && error.code === "signaling_timeout" && generation === this.#generation && !this.#stopped) this.#expireNegotiation(error);
|
|
917
|
+
throw error;
|
|
918
|
+
}).finally(() => {
|
|
919
|
+
if (timer !== void 0) globalThis.clearTimeout(timer);
|
|
920
|
+
});
|
|
595
921
|
this.#sdpTail = result.then(
|
|
596
922
|
() => void 0,
|
|
597
923
|
() => void 0
|
|
598
924
|
);
|
|
599
925
|
return result;
|
|
600
926
|
}
|
|
927
|
+
async #boundPeerOperation(operation) {
|
|
928
|
+
const generation = this.#generation;
|
|
929
|
+
let timer;
|
|
930
|
+
const timeout = new CloudflareSFUError("The browser media operation timed out. Try again.", "negotiation_timeout");
|
|
931
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
932
|
+
timer = globalThis.setTimeout(() => {
|
|
933
|
+
if (generation === this.#generation && !this.#stopped) this.#expireNegotiation(timeout);
|
|
934
|
+
reject(timeout);
|
|
935
|
+
}, PEER_OPERATION_TIMEOUT_MS);
|
|
936
|
+
});
|
|
937
|
+
try {
|
|
938
|
+
await Promise.race([operation, deadline]);
|
|
939
|
+
} finally {
|
|
940
|
+
if (timer !== void 0) globalThis.clearTimeout(timer);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
#expireNegotiation(error) {
|
|
944
|
+
this.#generation++;
|
|
945
|
+
this.#connectionEpoch++;
|
|
946
|
+
this.#clearPoll();
|
|
947
|
+
this.#polling = false;
|
|
948
|
+
this.#disposeConnection(false);
|
|
949
|
+
this.#setFailure(error, "media_failed");
|
|
950
|
+
}
|
|
601
951
|
#waitForConnection(connection, generation) {
|
|
602
|
-
return
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
952
|
+
return traceReconnect(
|
|
953
|
+
this.#recordReconnect,
|
|
954
|
+
"connection_ready",
|
|
955
|
+
() => waitFor(
|
|
956
|
+
() => {
|
|
957
|
+
this.#requireGeneration(generation);
|
|
958
|
+
return connectionIsLive(connection);
|
|
959
|
+
},
|
|
960
|
+
CONNECTION_TIMEOUT_MS,
|
|
961
|
+
"Timed out waiting for the Cloudflare SFU peer connection"
|
|
962
|
+
)
|
|
609
963
|
);
|
|
610
964
|
}
|
|
965
|
+
#cameraSimulcastSender(state) {
|
|
966
|
+
if (!state?.enabled) return void 0;
|
|
967
|
+
const sender = state.transceiver?.sender;
|
|
968
|
+
if (!sender?.track || sender.getParameters().encodings.length < 2) return void 0;
|
|
969
|
+
return sender;
|
|
970
|
+
}
|
|
971
|
+
async #applyCameraUplink(sender, report, policy) {
|
|
972
|
+
const parameters = sender.getParameters();
|
|
973
|
+
const low = parameters.encodings.find((encoding) => encoding.rid === "l");
|
|
974
|
+
if (!low) return;
|
|
975
|
+
const active = policy.sample(cameraUplinkBitrate(report.values()), low.active !== false);
|
|
976
|
+
if (active === (low.active !== false)) return;
|
|
977
|
+
low.active = active;
|
|
978
|
+
parameters.degradationPreference = "maintain-framerate";
|
|
979
|
+
await sender.setParameters(parameters);
|
|
980
|
+
}
|
|
611
981
|
#observeConnection(connection, generation, connectionEpoch) {
|
|
982
|
+
this.#disposeConnectionObservation?.();
|
|
612
983
|
let disposed = false;
|
|
984
|
+
let cameraTimer;
|
|
985
|
+
const cameraPolicy = new CameraUplinkPolicy();
|
|
986
|
+
let observedCamera;
|
|
987
|
+
const current = () => !disposed && generation === this.#generation && connectionEpoch === this.#connectionEpoch && !this.#stopped && connection.connectionState !== "failed" && connection.connectionState !== "closed";
|
|
988
|
+
const adaptCamera = async () => {
|
|
989
|
+
if (!current()) return;
|
|
990
|
+
try {
|
|
991
|
+
const state = this.#localTracks.get("camera");
|
|
992
|
+
if (state !== observedCamera) {
|
|
993
|
+
cameraPolicy.reset();
|
|
994
|
+
observedCamera = state;
|
|
995
|
+
}
|
|
996
|
+
const sender = this.#cameraSimulcastSender(state);
|
|
997
|
+
if (!sender) {
|
|
998
|
+
cameraPolicy.reset();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const report = await connection.getStats();
|
|
1002
|
+
if (!current() || this.#cameraSimulcastSender(state) !== sender || connection.signalingState !== "stable") return;
|
|
1003
|
+
await this.#applyCameraUplink(sender, report, cameraPolicy);
|
|
1004
|
+
} catch {
|
|
1005
|
+
cameraPolicy.reset();
|
|
1006
|
+
} finally {
|
|
1007
|
+
if (current()) cameraTimer = globalThis.setTimeout(adaptCamera, 1e3);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
613
1010
|
const dispose = () => {
|
|
614
1011
|
if (disposed) return;
|
|
615
1012
|
disposed = true;
|
|
1013
|
+
if (cameraTimer !== void 0) globalThis.clearTimeout(cameraTimer);
|
|
616
1014
|
connection.removeEventListener("connectionstatechange", capture);
|
|
617
1015
|
connection.removeEventListener("iceconnectionstatechange", capture);
|
|
618
1016
|
connection.removeEventListener("signalingstatechange", capture);
|
|
@@ -625,6 +1023,7 @@ var CloudflareSFUClient = class {
|
|
|
625
1023
|
};
|
|
626
1024
|
const capture = () => {
|
|
627
1025
|
if (disposed || generation !== this.#generation || connectionEpoch !== this.#connectionEpoch || this.#stopped) return;
|
|
1026
|
+
recordReconnect(this.#recordReconnect, "rtc_state", { connection_state: connection.connectionState, ice_connection_state: connection.iceConnectionState, signaling_state: connection.signalingState });
|
|
628
1027
|
observe();
|
|
629
1028
|
const recorder = this.#onRtcSummary;
|
|
630
1029
|
if (recorder) {
|
|
@@ -641,11 +1040,67 @@ var CloudflareSFUClient = class {
|
|
|
641
1040
|
}
|
|
642
1041
|
if (connection.connectionState === "closed") dispose();
|
|
643
1042
|
};
|
|
1043
|
+
this.#disposeConnectionObservation = dispose;
|
|
1044
|
+
cameraTimer = globalThis.setTimeout(adaptCamera, 1e3);
|
|
1045
|
+
if (this.#recordReconnect) this.#sampleRecoveryTraffic(connection, generation, connectionEpoch);
|
|
644
1046
|
connection.addEventListener("connectionstatechange", capture);
|
|
645
1047
|
connection.addEventListener("iceconnectionstatechange", capture);
|
|
646
1048
|
connection.addEventListener("signalingstatechange", capture);
|
|
647
1049
|
capture();
|
|
648
1050
|
}
|
|
1051
|
+
#sampleRecoveryTraffic(connection, generation, connectionEpoch) {
|
|
1052
|
+
const traffic = /* @__PURE__ */ new Map();
|
|
1053
|
+
let dtlsState;
|
|
1054
|
+
const current = () => generation === this.#generation && connectionEpoch === this.#connectionEpoch && !this.#stopped && connection.connectionState !== "closed" && connection.connectionState !== "failed";
|
|
1055
|
+
const sample = async () => {
|
|
1056
|
+
if (!current()) return;
|
|
1057
|
+
try {
|
|
1058
|
+
const report = await connection.getStats();
|
|
1059
|
+
if (!current()) return;
|
|
1060
|
+
const now = performance.now();
|
|
1061
|
+
report.forEach((entry) => {
|
|
1062
|
+
if (entry.type === "transport") {
|
|
1063
|
+
const nextState = rtcString(entry, "dtlsState");
|
|
1064
|
+
if (nextState !== dtlsState) recordReconnect(this.#recordReconnect, "dtls_state", { dtls_state: nextState ?? "unknown" });
|
|
1065
|
+
dtlsState = nextState;
|
|
1066
|
+
}
|
|
1067
|
+
this.#observeRecoveryTraffic(entry, traffic, now);
|
|
1068
|
+
});
|
|
1069
|
+
} catch {
|
|
1070
|
+
if (current()) recordReconnect(this.#recordReconnect, "rtc_stats_unavailable");
|
|
1071
|
+
}
|
|
1072
|
+
if (current()) globalThis.setTimeout(() => void sample(), 250);
|
|
1073
|
+
};
|
|
1074
|
+
void sample();
|
|
1075
|
+
}
|
|
1076
|
+
#observeRecoveryTraffic(entry, traffic, now) {
|
|
1077
|
+
if (entry.type !== "inbound-rtp" && entry.type !== "outbound-rtp") return;
|
|
1078
|
+
const direction = entry.type === "inbound-rtp" ? "inbound" : "outbound";
|
|
1079
|
+
const kind = rtcString(entry, "kind") ?? "unknown";
|
|
1080
|
+
const previous = traffic.get(entry.id);
|
|
1081
|
+
const next = previous ?? {
|
|
1082
|
+
packets: { count: 0, lastProgress: now, stalled: false },
|
|
1083
|
+
frames: { count: 0, lastProgress: now, stalled: false }
|
|
1084
|
+
};
|
|
1085
|
+
const packets = recoveryTrafficCount(entry, direction, "packet");
|
|
1086
|
+
const frames = recoveryTrafficCount(entry, direction, "frame");
|
|
1087
|
+
this.#observeTrafficCounter(next.packets, { count: packets, now, direction, kind, unit: "packet", existed: previous !== void 0 });
|
|
1088
|
+
this.#observeTrafficCounter(next.frames, { count: frames, now, direction, kind, unit: "frame", existed: previous !== void 0 });
|
|
1089
|
+
traffic.set(entry.id, next);
|
|
1090
|
+
}
|
|
1091
|
+
#observeTrafficCounter(progress, sample) {
|
|
1092
|
+
const attributes = { direction: sample.direction, kind: sample.kind };
|
|
1093
|
+
if (sample.count > progress.count) {
|
|
1094
|
+
if (progress.count === 0) recordReconnect(this.#recordReconnect, `first_${sample.unit}`, attributes);
|
|
1095
|
+
else if (progress.stalled) recordReconnect(this.#recordReconnect, sample.unit === "packet" ? "rtp_resumed" : "frame_resumed", attributes);
|
|
1096
|
+
progress.lastProgress = sample.now;
|
|
1097
|
+
progress.stalled = false;
|
|
1098
|
+
} else if (trafficStalled(progress, sample)) {
|
|
1099
|
+
progress.stalled = true;
|
|
1100
|
+
recordReconnect(this.#recordReconnect, sample.unit === "packet" ? "rtp_stall" : "frame_stall", attributes);
|
|
1101
|
+
}
|
|
1102
|
+
progress.count = sample.count;
|
|
1103
|
+
}
|
|
649
1104
|
async #replaceDormantConnectionBeforeNegotiation(generation) {
|
|
650
1105
|
if (!this.#started || this.#negotiatedGeneration === generation || this.#replacementAttemptedGeneration === generation || !this.#replaceMediaConnection) return;
|
|
651
1106
|
await this.#replaceMediaConnectionForRecovery(generation);
|
|
@@ -656,7 +1111,8 @@ var CloudflareSFUClient = class {
|
|
|
656
1111
|
throw new CloudflareSFUError("A fresh Cloudflare SFU connection is unavailable", "signaling_failed");
|
|
657
1112
|
}
|
|
658
1113
|
this.#replacementAttemptedGeneration = generation;
|
|
659
|
-
const
|
|
1114
|
+
const replaceMediaConnection = this.#replaceMediaConnection;
|
|
1115
|
+
const bootstrap = await traceReconnect(this.#recordReconnect, "replace_access", replaceMediaConnection);
|
|
660
1116
|
this.#requireGeneration(generation);
|
|
661
1117
|
validateBootstrap(bootstrap);
|
|
662
1118
|
if (bootstrap.connectionId === this.#bootstrap.connectionId) {
|
|
@@ -665,25 +1121,27 @@ var CloudflareSFUClient = class {
|
|
|
665
1121
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
666
1122
|
this.#disposeConnection(false);
|
|
667
1123
|
this.#reusableLocalTransceivers.clear();
|
|
1124
|
+
this.#reusableLocalPublicationIds.clear();
|
|
668
1125
|
this.#clearRemoteTracks();
|
|
669
1126
|
this.#cursor = null;
|
|
670
1127
|
this.#bootstrap = bootstrap;
|
|
671
|
-
this.#
|
|
1128
|
+
this.#rebuildPeerConnection(bootstrap, generation, connectionEpoch);
|
|
1129
|
+
this.#retiredLocalConnection = null;
|
|
672
1130
|
this.#negotiatedGeneration = null;
|
|
673
|
-
this.#observeConnection(this.#connection, generation, connectionEpoch);
|
|
674
1131
|
for (const state of this.#localTracks.values()) {
|
|
675
1132
|
state.transceiver = null;
|
|
676
1133
|
state.enabled = false;
|
|
677
1134
|
state.providerPublicationId = null;
|
|
678
1135
|
}
|
|
679
|
-
this
|
|
1136
|
+
this.remotePublicationsChanged();
|
|
680
1137
|
}
|
|
681
1138
|
async #republishPreparedTracksAfterRecovery(generation) {
|
|
682
1139
|
const enabled = [...this.#localTracks.values()].filter((state) => state.desiredEnabled && state.track.readyState !== "ended");
|
|
683
1140
|
await this.#publishPreparedTracks(enabled, generation);
|
|
684
1141
|
}
|
|
685
|
-
#observeRemotePublicationCursor(cursor) {
|
|
1142
|
+
#observeRemotePublicationCursor(cursor, pullIncomplete = false) {
|
|
686
1143
|
this.#cursor = cursor;
|
|
1144
|
+
this.#remotePullIncomplete = pullIncomplete;
|
|
687
1145
|
this.#publishSnapshot();
|
|
688
1146
|
}
|
|
689
1147
|
#quarantineInvalidRemotePublicationCursor(authoritative) {
|
|
@@ -697,7 +1155,19 @@ var CloudflareSFUClient = class {
|
|
|
697
1155
|
const create = this.#peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
|
|
698
1156
|
return create({ iceServers: [{ urls: bootstrap.stunServer }], bundlePolicy: "max-bundle" });
|
|
699
1157
|
}
|
|
1158
|
+
#rebuildPeerConnection(bootstrap, generation, connectionEpoch) {
|
|
1159
|
+
try {
|
|
1160
|
+
this.#connection = this.#createPeerConnection(bootstrap);
|
|
1161
|
+
this.#observeConnection(this.#connection, generation, connectionEpoch);
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
this.#setFailure(error, "media_failed");
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
700
1167
|
#disposeConnection(stopSenders) {
|
|
1168
|
+
if (!stopSenders) this.#setPhase("recovering", null);
|
|
1169
|
+
this.#disposeConnectionObservation?.();
|
|
1170
|
+
this.#disposeConnectionObservation = void 0;
|
|
701
1171
|
if (stopSenders) {
|
|
702
1172
|
for (const sender of this.#connection.getSenders()) {
|
|
703
1173
|
if (sender.track) safeStopTrack(sender.track, this.#reportError.bind(this));
|
|
@@ -737,15 +1207,31 @@ var CloudflareSFUClient = class {
|
|
|
737
1207
|
if (this.#stopped || !this.#started || this.#pollTimer !== void 0) return;
|
|
738
1208
|
this.#pollTimer = globalThis.setTimeout(async () => {
|
|
739
1209
|
this.#pollTimer = void 0;
|
|
1210
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1211
|
+
this.#schedulePoll();
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
let nextDelay = this.#pollIntervalMs;
|
|
740
1215
|
try {
|
|
741
1216
|
await this.refreshRemotePublications();
|
|
1217
|
+
if (this.#remotePullIncomplete) nextDelay = this.#incompletePullDelay();
|
|
742
1218
|
} catch {
|
|
1219
|
+
if (this.#pollFailedPull && this.#remotePullIncomplete) nextDelay = this.#incompletePullDelay();
|
|
1220
|
+
else {
|
|
1221
|
+
this.#pollFailures = Math.min(this.#pollFailures + 1, 6);
|
|
1222
|
+
nextDelay = Math.min(this.#pollIntervalMs, (500 + Math.random() * 500) * 2 ** (this.#pollFailures - 1));
|
|
1223
|
+
}
|
|
743
1224
|
} finally {
|
|
744
|
-
this.#schedulePoll();
|
|
1225
|
+
this.#schedulePoll(nextDelay);
|
|
745
1226
|
}
|
|
746
1227
|
}, delayMs);
|
|
747
1228
|
}
|
|
1229
|
+
#incompletePullDelay() {
|
|
1230
|
+
this.#pollFailures = Math.min(this.#pollFailures + 1, 6);
|
|
1231
|
+
return Math.min(this.#pollIntervalMs, 750 * 2 ** (this.#pollFailures - 1));
|
|
1232
|
+
}
|
|
748
1233
|
#clearPoll() {
|
|
1234
|
+
this.#pollFailures = 0;
|
|
749
1235
|
if (this.#pollTimer !== void 0) globalThis.clearTimeout(this.#pollTimer);
|
|
750
1236
|
this.#pollTimer = void 0;
|
|
751
1237
|
}
|
|
@@ -754,7 +1240,7 @@ var CloudflareSFUClient = class {
|
|
|
754
1240
|
participantId: this.#participantId,
|
|
755
1241
|
source: state.source,
|
|
756
1242
|
enabled: state.enabled,
|
|
757
|
-
publicationId: state.
|
|
1243
|
+
publicationId: state.providerPublicationId
|
|
758
1244
|
}));
|
|
759
1245
|
}
|
|
760
1246
|
#projectRemotePublications() {
|
|
@@ -772,7 +1258,7 @@ var CloudflareSFUClient = class {
|
|
|
772
1258
|
(state) => Object.freeze({
|
|
773
1259
|
source: state.source,
|
|
774
1260
|
enabled: state.enabled,
|
|
775
|
-
publicationId: state.
|
|
1261
|
+
publicationId: state.providerPublicationId,
|
|
776
1262
|
track: state.track
|
|
777
1263
|
})
|
|
778
1264
|
) : EMPTY_LOCAL,
|
|
@@ -908,10 +1394,10 @@ function desiredRemotePublications(publications, participantId) {
|
|
|
908
1394
|
function reconcileRemoteTracks(desired, pulled, current) {
|
|
909
1395
|
const pulledByKey = new Map(pulled.map((publication) => [publicationKey(publication), publication]));
|
|
910
1396
|
return new Map(
|
|
911
|
-
[...desired].
|
|
1397
|
+
[...desired].flatMap(([key, publication]) => {
|
|
912
1398
|
const track = pulledByKey.get(key) ?? current.get(key);
|
|
913
|
-
if (!track || track.publicationId !== publication.publicationId)
|
|
914
|
-
return [key, track];
|
|
1399
|
+
if (!track || track.publicationId !== publication.publicationId) return [];
|
|
1400
|
+
return [[key, track]];
|
|
915
1401
|
})
|
|
916
1402
|
);
|
|
917
1403
|
}
|
|
@@ -973,31 +1459,63 @@ function publicationEqual(left, right) {
|
|
|
973
1459
|
function remotePublicationEqual(left, right) {
|
|
974
1460
|
return right !== void 0 && left.participantId === right.participantId && left.source === right.source && left.publicationId === right.publicationId && left.track === right.track;
|
|
975
1461
|
}
|
|
1462
|
+
function providerDescription(description) {
|
|
1463
|
+
return { sessionDescription: description };
|
|
1464
|
+
}
|
|
1465
|
+
function retryableRemotePull(error) {
|
|
1466
|
+
return error instanceof CloudflareSFUError && ["signaling_failed", "signaling_timeout", "media_failed", "negotiation_timeout"].includes(error.code);
|
|
1467
|
+
}
|
|
1468
|
+
function firefoxCameraSimulcastUnsupported() {
|
|
1469
|
+
return typeof navigator !== "undefined" && /\bFirefox\/\d+/i.test(navigator.userAgent);
|
|
1470
|
+
}
|
|
1471
|
+
function simulcastUnsupported(error) {
|
|
1472
|
+
return error instanceof Error && ["NotSupportedError", "OperationError", "TypeError"].includes(error.name);
|
|
1473
|
+
}
|
|
1474
|
+
function mediaTargetFailure(error) {
|
|
1475
|
+
const code = error instanceof CloudflareSFUError ? error.code : "media_failed";
|
|
1476
|
+
const ambiguous = code === "signaling_timeout" || code === "negotiation_timeout";
|
|
1477
|
+
return { outcome: ambiguous ? "ambiguous" : "retryable_failure", errorCode: code };
|
|
1478
|
+
}
|
|
1479
|
+
function recoveryTrafficCount(entry, direction, unit) {
|
|
1480
|
+
const property = unit === "packet" ? direction === "inbound" ? "packetsReceived" : "packetsSent" : direction === "inbound" ? "framesDecoded" : "framesEncoded";
|
|
1481
|
+
return rtcNumber(entry, property) ?? 0;
|
|
1482
|
+
}
|
|
1483
|
+
function trafficStalled(progress, sample) {
|
|
1484
|
+
return sample.existed && !progress.stalled && sample.now - progress.lastProgress >= 1e3 && (sample.unit === "packet" || progress.count > 0);
|
|
1485
|
+
}
|
|
976
1486
|
|
|
977
1487
|
// src/media/transport.ts
|
|
1488
|
+
var SIGNALING_TIMEOUT_MS = 8e3;
|
|
1489
|
+
var CREDENTIAL_TIMEOUT_MS = 7e3;
|
|
978
1490
|
function createCloudflareSFUHTTPTransport(options) {
|
|
979
1491
|
const fetch = options.fetch ?? globalThis.fetch;
|
|
980
1492
|
const credential = requireCredential(options);
|
|
981
1493
|
const base = options.apiBaseURL.replace(/\/$/, "");
|
|
982
1494
|
const mediaPath = `${base}/v1/tenants/${encodeURIComponent(options.tenantId)}/spaces/${encodeURIComponent(options.spaceId)}/episodes/${encodeURIComponent(options.episodeId)}/participants/${encodeURIComponent(options.participantId)}/media/sfu`;
|
|
1495
|
+
const timeoutMs = options.requestTimeoutMs ?? SIGNALING_TIMEOUT_MS;
|
|
1496
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new CloudflareSFUError("The signaling deadline is invalid", "signaling_failed");
|
|
983
1497
|
const request = async (path, init) => {
|
|
984
|
-
const token = await credential();
|
|
1498
|
+
const token = await withDeadline(CREDENTIAL_TIMEOUT_MS, (signal) => credential(signal));
|
|
985
1499
|
if (!token.trim()) throw new CloudflareSFUError("The media credential provider returned an empty token", "signaling_failed");
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
|
|
992
|
-
status: response.status,
|
|
993
|
-
retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
|
|
1500
|
+
return withDeadline(timeoutMs, async (signal) => {
|
|
1501
|
+
const response = await fetch(`${mediaPath}/${path}`, {
|
|
1502
|
+
...init,
|
|
1503
|
+
signal,
|
|
1504
|
+
headers: { ...options.telemetry ? journeyHeaders(options.telemetry) : {}, Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init?.headers }
|
|
994
1505
|
});
|
|
995
|
-
|
|
996
|
-
|
|
1506
|
+
if (!response.ok) {
|
|
1507
|
+
throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
|
|
1508
|
+
status: response.status,
|
|
1509
|
+
retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
return await response.json();
|
|
1513
|
+
});
|
|
997
1514
|
};
|
|
998
1515
|
return {
|
|
999
1516
|
addTracks: async (input) => {
|
|
1000
|
-
const
|
|
1517
|
+
const path = input.allowPartialRemoteTracks ? "tracks?allow_partial_remote_tracks=true" : "tracks";
|
|
1518
|
+
const response = await request(path, {
|
|
1001
1519
|
method: "POST",
|
|
1002
1520
|
body: JSON.stringify({ connection_id: input.connectionId, session_description: input.sessionDescription, tracks: input.tracks })
|
|
1003
1521
|
});
|
|
@@ -1035,6 +1553,25 @@ function createCloudflareSFUHTTPTransport(options) {
|
|
|
1035
1553
|
}
|
|
1036
1554
|
};
|
|
1037
1555
|
}
|
|
1556
|
+
async function withDeadline(timeoutMs, operation) {
|
|
1557
|
+
const controller = new AbortController();
|
|
1558
|
+
const timeout = new CloudflareSFUError("Cloudflare SFU signaling timed out. Try again.", "signaling_timeout");
|
|
1559
|
+
let timer;
|
|
1560
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
1561
|
+
timer = globalThis.setTimeout(() => {
|
|
1562
|
+
controller.abort();
|
|
1563
|
+
reject(timeout);
|
|
1564
|
+
}, timeoutMs);
|
|
1565
|
+
});
|
|
1566
|
+
try {
|
|
1567
|
+
return await Promise.race([Promise.resolve().then(() => operation(controller.signal)), deadline]);
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
if (controller.signal.aborted) throw timeout;
|
|
1570
|
+
throw error;
|
|
1571
|
+
} finally {
|
|
1572
|
+
if (timer !== void 0) globalThis.clearTimeout(timer);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1038
1575
|
function requireCredential(options) {
|
|
1039
1576
|
if (options.credential) return options.credential;
|
|
1040
1577
|
if (options.bearerToken !== void 0) return () => options.bearerToken ?? "";
|
|
@@ -1059,12 +1596,14 @@ var CloudflareRTKClient = class {
|
|
|
1059
1596
|
#clientFactory;
|
|
1060
1597
|
#snapshotListeners = /* @__PURE__ */ new Set();
|
|
1061
1598
|
#sourceOperations = /* @__PURE__ */ new Map();
|
|
1599
|
+
#generationInvalidators = /* @__PURE__ */ new Set();
|
|
1062
1600
|
#connection = null;
|
|
1063
1601
|
#localTracks = /* @__PURE__ */ new Map();
|
|
1064
1602
|
#connectionUnsubscribers = [];
|
|
1065
1603
|
#participantUnsubscribers = /* @__PURE__ */ new Map();
|
|
1066
1604
|
#snapshot;
|
|
1067
1605
|
#stopped = false;
|
|
1606
|
+
#generation = 0;
|
|
1068
1607
|
#screenDisableRequested = false;
|
|
1069
1608
|
#initialAuthToken;
|
|
1070
1609
|
constructor(options) {
|
|
@@ -1101,16 +1640,22 @@ var CloudflareRTKClient = class {
|
|
|
1101
1640
|
async clearPreparedLocalTrack(source) {
|
|
1102
1641
|
const state = this.#localTracks.get(source);
|
|
1103
1642
|
if (!state) return;
|
|
1104
|
-
|
|
1105
|
-
await this.#setSourceEnabled(state, false);
|
|
1106
|
-
}
|
|
1643
|
+
const connection = this.#connection;
|
|
1107
1644
|
if (state.endedListener) state.track.removeEventListener("ended", state.endedListener);
|
|
1108
1645
|
this.#localTracks.delete(source);
|
|
1646
|
+
const actions = connection ? this.#sourceActions(state, connection) : null;
|
|
1647
|
+
const cleanup = actions?.enabled && state.enabled ? this.#queueSourceOperation(source, () => connection === this.#connection ? actions.disable() : Promise.resolve()) : Promise.resolve();
|
|
1109
1648
|
this.#publishSnapshot();
|
|
1649
|
+
try {
|
|
1650
|
+
await cleanup;
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
if (!isStaleGeneration(error)) throw error;
|
|
1653
|
+
}
|
|
1110
1654
|
}
|
|
1111
1655
|
async start(localMedia) {
|
|
1112
1656
|
if (this.#snapshot.connection.phase === "live") return;
|
|
1113
1657
|
this.#requireStartable();
|
|
1658
|
+
const generation = this.#advanceGeneration();
|
|
1114
1659
|
this.#setPhase("connecting", null);
|
|
1115
1660
|
try {
|
|
1116
1661
|
const tracks = localMedia.getTracks().filter((track) => track.kind === "audio" || track.kind === "video");
|
|
@@ -1118,38 +1663,60 @@ var CloudflareRTKClient = class {
|
|
|
1118
1663
|
const source = track.kind === "audio" ? "microphone" : "camera";
|
|
1119
1664
|
if (!this.#localTracks.has(source)) this.#localTracks.set(source, { source, track, enabled: false, endedListener: null });
|
|
1120
1665
|
}
|
|
1121
|
-
await this.#openConnection(this.#initialAuthToken);
|
|
1122
|
-
|
|
1123
|
-
if (state.source === "microphone" || state.source === "camera") await this.#setSourceEnabled(state, true);
|
|
1124
|
-
}
|
|
1125
|
-
this.#setPhase("live", null);
|
|
1666
|
+
if (!await this.#openConnection(this.#initialAuthToken, generation)) return;
|
|
1667
|
+
await this.#restoreLocalTracks(generation, true);
|
|
1126
1668
|
} catch (error) {
|
|
1127
|
-
this.#
|
|
1128
|
-
this.#reportError(error);
|
|
1669
|
+
this.#failCurrentGeneration(generation, error);
|
|
1129
1670
|
throw error;
|
|
1130
1671
|
}
|
|
1131
1672
|
}
|
|
1132
1673
|
async setLocalPublicationTarget(target) {
|
|
1133
1674
|
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
1134
1675
|
if (resolved.kind === "result") return resolved.result;
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1676
|
+
const generation = this.#generation;
|
|
1677
|
+
try {
|
|
1678
|
+
return await this.#queueSourceOperation(target.source, () => this.#applyLocalTarget(target, generation));
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
return { outcome: "retryable_failure", errorCode: error instanceof CloudflareRTKError ? error.code : "media_failed" };
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
async #queueSourceOperation(source, apply) {
|
|
1684
|
+
const previous = this.#sourceOperations.get(source) ?? Promise.resolve();
|
|
1685
|
+
const generation = this.#generation;
|
|
1686
|
+
let invalidate = () => {
|
|
1687
|
+
};
|
|
1688
|
+
const retired = new Promise((_resolve, reject) => {
|
|
1689
|
+
invalidate = () => reject(new CloudflareRTKError("The media operation belongs to a retired connection", "stale_generation"));
|
|
1690
|
+
this.#generationInvalidators.add(invalidate);
|
|
1146
1691
|
});
|
|
1147
|
-
|
|
1692
|
+
const operation = Promise.race([
|
|
1693
|
+
previous.catch(() => void 0).then(() => {
|
|
1694
|
+
if (!this.#isCurrentGeneration(generation)) throw new CloudflareRTKError("The queued media operation belongs to a retired connection", "stale_generation");
|
|
1695
|
+
return apply();
|
|
1696
|
+
}),
|
|
1697
|
+
retired
|
|
1698
|
+
]);
|
|
1699
|
+
this.#sourceOperations.set(source, operation);
|
|
1148
1700
|
try {
|
|
1149
|
-
|
|
1150
|
-
return { outcome, errorCode: outcome === "retryable_failure" ? "media_failed" : null };
|
|
1701
|
+
return await operation;
|
|
1151
1702
|
} finally {
|
|
1152
|
-
|
|
1703
|
+
this.#generationInvalidators.delete(invalidate);
|
|
1704
|
+
if (this.#sourceOperations.get(source) === operation) this.#sourceOperations.delete(source);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
async #applyLocalTarget(target, generation) {
|
|
1708
|
+
if (!this.#isCurrentGeneration(generation)) return { outcome: "retryable_failure", errorCode: "stale_generation" };
|
|
1709
|
+
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
1710
|
+
if (resolved.kind === "result") return resolved.result;
|
|
1711
|
+
const state = resolved.value;
|
|
1712
|
+
try {
|
|
1713
|
+
if (!this.#connection) throw new CloudflareRTKError("The RealtimeKit connection is recovering", "stale_generation");
|
|
1714
|
+
if (state.enabled === target.enabled) return { outcome: "satisfied", errorCode: null };
|
|
1715
|
+
await this.#setSourceEnabled(state, target.enabled);
|
|
1716
|
+
return { outcome: "confirmed", errorCode: null };
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
this.#reportError(error);
|
|
1719
|
+
return { outcome: "retryable_failure", errorCode: error instanceof CloudflareRTKError ? error.code : "media_failed" };
|
|
1153
1720
|
}
|
|
1154
1721
|
}
|
|
1155
1722
|
observeLocalPublications(listener) {
|
|
@@ -1161,31 +1728,74 @@ var CloudflareRTKClient = class {
|
|
|
1161
1728
|
async restart(input) {
|
|
1162
1729
|
if (!isRTKAccess(input)) throw new CloudflareRTKError("The RealtimeKit adapter requires a Cloudflare RealtimeKit access grant", "invalid_client");
|
|
1163
1730
|
if (this.#stopped) throw new CloudflareRTKError("The RealtimeKit media client has stopped", "media_stopped");
|
|
1731
|
+
const generation = this.#advanceGeneration();
|
|
1164
1732
|
this.#setPhase("recovering", null);
|
|
1165
1733
|
try {
|
|
1166
1734
|
await this.#closeConnection();
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
this.#
|
|
1735
|
+
if (!this.#isCurrentGeneration(generation)) return;
|
|
1736
|
+
if (!await this.#openConnection(input.clientPayload.token, generation)) return;
|
|
1737
|
+
await this.#restoreLocalTracks(generation, false);
|
|
1170
1738
|
} catch (error) {
|
|
1171
|
-
this.#
|
|
1172
|
-
this.#reportError(error);
|
|
1739
|
+
this.#failCurrentGeneration(generation, error);
|
|
1173
1740
|
throw error;
|
|
1174
1741
|
}
|
|
1175
1742
|
}
|
|
1176
1743
|
stop() {
|
|
1177
1744
|
if (this.#stopped) return;
|
|
1178
1745
|
this.#stopped = true;
|
|
1746
|
+
this.#advanceGeneration();
|
|
1179
1747
|
const leave = this.#closeConnection();
|
|
1180
1748
|
void leave.catch((error) => this.#reportError(error));
|
|
1181
1749
|
for (const state of this.#localTracks.values()) if (state.endedListener) state.track.removeEventListener("ended", state.endedListener);
|
|
1182
1750
|
this.#localTracks.clear();
|
|
1183
1751
|
this.#setPhase("stopped", null);
|
|
1184
1752
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1753
|
+
#isCurrentGeneration(generation) {
|
|
1754
|
+
return !this.#stopped && generation === this.#generation;
|
|
1755
|
+
}
|
|
1756
|
+
#advanceGeneration() {
|
|
1757
|
+
const generation = ++this.#generation;
|
|
1758
|
+
this.#sourceOperations.clear();
|
|
1759
|
+
for (const invalidate of this.#generationInvalidators) invalidate();
|
|
1760
|
+
this.#generationInvalidators.clear();
|
|
1761
|
+
return generation;
|
|
1762
|
+
}
|
|
1763
|
+
async #restoreLocalTracks(generation, initial) {
|
|
1764
|
+
for (const state of this.#localTracks.values()) {
|
|
1765
|
+
if (!this.#isCurrentGeneration(generation)) return;
|
|
1766
|
+
const enable = initial ? state.source !== "screen" : state.enabled;
|
|
1767
|
+
if (enable) {
|
|
1768
|
+
await this.#queueSourceOperation(state.source, async () => {
|
|
1769
|
+
if (!this.#isCurrentGeneration(generation) || this.#localTracks.get(state.source) !== state || !initial && !state.enabled) return;
|
|
1770
|
+
await this.#setSourceEnabled(state, true);
|
|
1771
|
+
}).catch((error) => {
|
|
1772
|
+
if (!this.#isCurrentGeneration(generation)) return;
|
|
1773
|
+
if (isStaleGeneration(error) && this.#localTracks.get(state.source) !== state) return;
|
|
1774
|
+
throw error;
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
if (this.#isCurrentGeneration(generation)) this.#setPhase("live", null);
|
|
1779
|
+
}
|
|
1780
|
+
#failCurrentGeneration(generation, error) {
|
|
1781
|
+
if (!this.#isCurrentGeneration(generation)) return;
|
|
1782
|
+
this.#setFailure(error);
|
|
1783
|
+
this.#reportError(error);
|
|
1784
|
+
}
|
|
1785
|
+
async #openConnection(authToken, generation) {
|
|
1786
|
+
const connection = await this.#clientFactory({ authToken, onError: (error) => this.#reportError(error) });
|
|
1787
|
+
if (!this.#isCurrentGeneration(generation)) {
|
|
1788
|
+
await connection.leave();
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
this.#connection = connection;
|
|
1792
|
+
this.#bindConnection(connection);
|
|
1793
|
+
await connection.join();
|
|
1794
|
+
if (!this.#isCurrentGeneration(generation)) {
|
|
1795
|
+
await connection.leave();
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
return true;
|
|
1189
1799
|
}
|
|
1190
1800
|
async #closeConnection() {
|
|
1191
1801
|
const connection = this.#connection;
|
|
@@ -1237,22 +1847,44 @@ var CloudflareRTKClient = class {
|
|
|
1237
1847
|
}
|
|
1238
1848
|
async #setSourceEnabled(state, enabled) {
|
|
1239
1849
|
const connection = this.#connection;
|
|
1850
|
+
const generation = this.#generation;
|
|
1240
1851
|
if (!connection) throw new CloudflareRTKError("The RealtimeKit connection is not active", "media_stopped");
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1852
|
+
const actions = this.#sourceActions(state, connection);
|
|
1853
|
+
if (state.source === "screen") this.#screenDisableRequested = !enabled;
|
|
1854
|
+
await actions[enabled ? "enable" : "disable"]();
|
|
1855
|
+
try {
|
|
1856
|
+
this.#requireCurrentSource(state, connection, generation);
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
if (enabled)
|
|
1859
|
+
await actions.disable().catch((cause) => {
|
|
1860
|
+
this.#failCurrentGeneration(generation, cause);
|
|
1861
|
+
throw cause;
|
|
1862
|
+
});
|
|
1863
|
+
throw error;
|
|
1251
1864
|
}
|
|
1252
1865
|
state.enabled = enabled;
|
|
1253
1866
|
state.track.enabled = enabled;
|
|
1254
1867
|
this.#publishSnapshot();
|
|
1255
1868
|
}
|
|
1869
|
+
#sourceActions(state, connection) {
|
|
1870
|
+
return {
|
|
1871
|
+
microphone: { enabled: connection.self.audioEnabled, enable: () => connection.self.enableAudio(state.track), disable: () => connection.self.disableAudio() },
|
|
1872
|
+
camera: { enabled: connection.self.videoEnabled, enable: () => connection.self.enableVideo(state.track), disable: () => connection.self.disableVideo() },
|
|
1873
|
+
screen: {
|
|
1874
|
+
enabled: connection.self.screenShareEnabled,
|
|
1875
|
+
enable: () => connection.self.enableScreenShare(),
|
|
1876
|
+
disable: () => {
|
|
1877
|
+
if (connection === this.#connection) this.#screenDisableRequested = true;
|
|
1878
|
+
return connection.self.disableScreenShare();
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}[state.source];
|
|
1882
|
+
}
|
|
1883
|
+
#requireCurrentSource(state, connection, generation) {
|
|
1884
|
+
if (!this.#isCurrentGeneration(generation) || connection !== this.#connection || this.#localTracks.get(state.source) !== state) {
|
|
1885
|
+
throw new CloudflareRTKError("The local media target belongs to a replaced connection or source", "stale_generation");
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1256
1888
|
#syncLocalState(source) {
|
|
1257
1889
|
const state = this.#localTracks.get(source);
|
|
1258
1890
|
const connection = this.#connection;
|
|
@@ -1450,8 +2082,36 @@ function toParticipant(participant) {
|
|
|
1450
2082
|
}
|
|
1451
2083
|
};
|
|
1452
2084
|
}
|
|
2085
|
+
function isStaleGeneration(error) {
|
|
2086
|
+
return error instanceof CloudflareRTKError && error.code === "stale_generation";
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/media/capture-error.ts
|
|
2090
|
+
function describeMediaCaptureError(cause) {
|
|
2091
|
+
const name = typeof cause === "object" && cause !== null && "name" in cause ? cause.name : void 0;
|
|
2092
|
+
switch (name) {
|
|
2093
|
+
case "NotAllowedError":
|
|
2094
|
+
case "PermissionDeniedError":
|
|
2095
|
+
return { code: "permission_denied", recoverable: true, message: "Camera or microphone permission was denied. Allow access in your device settings and try again." };
|
|
2096
|
+
case "NotFoundError":
|
|
2097
|
+
case "DevicesNotFoundError":
|
|
2098
|
+
return { code: "device_not_found", recoverable: true, message: "No matching camera or microphone was found. Connect an input device or enter with that device off." };
|
|
2099
|
+
case "NotReadableError":
|
|
2100
|
+
case "TrackStartError":
|
|
2101
|
+
return { code: "device_busy", recoverable: true, message: "The camera or microphone could not start. Close other apps using it and try again." };
|
|
2102
|
+
case "OverconstrainedError":
|
|
2103
|
+
case "ConstraintNotSatisfiedError":
|
|
2104
|
+
return { code: "device_constraint_invalid", recoverable: true, message: "The selected camera or microphone is unavailable. Select another input device and try again." };
|
|
2105
|
+
case "SecurityError":
|
|
2106
|
+
case "NotSupportedError":
|
|
2107
|
+
return { code: "unsupported_environment", recoverable: false, message: "Media capture is unavailable in this environment. Use a supported browser over HTTPS or the native app." };
|
|
2108
|
+
default:
|
|
2109
|
+
return { code: "media_capture_failed", recoverable: true, message: "Media capture failed. Try again or enter with devices off." };
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
1453
2112
|
|
|
1454
2113
|
export {
|
|
2114
|
+
describeMediaCaptureError,
|
|
1455
2115
|
CloudflareSFUError,
|
|
1456
2116
|
parseCloudflareSFUPublicationID,
|
|
1457
2117
|
requireDescription,
|