@q9labsai/chalk-client 4.1.15 → 4.1.16
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 +56 -2
- package/dist/access/manager.d.ts.map +1 -1
- package/dist/{chunk-LQSXEWLL.js → chunk-BD7FRQ67.js} +2061 -326
- package/dist/{chunk-6ONSFCIY.js → chunk-E4OK5K6N.js} +7 -3
- package/dist/{chunk-NKOKY2AE.js → chunk-EWTITNJ3.js} +15 -101
- package/dist/chunk-HW5TTZL2.js +99 -0
- package/dist/{chunk-LSDDADCN.js → chunk-M3S7HNQF.js} +1656 -517
- package/dist/{chunk-PQNSA7ZN.js → chunk-O7WTAXAY.js} +610 -73
- package/dist/{chunk-PWUU6T3R.js → chunk-OKIL2ELF.js} +1185 -874
- package/dist/chunk-RCDXIDCN.js +33 -0
- package/dist/client.d.ts +1412 -463
- package/dist/client.d.ts.map +1 -1
- package/dist/connection/dependencies.d.ts +10 -0
- package/dist/connection/dependencies.d.ts.map +1 -1
- package/dist/connection/index.d.ts +2 -0
- package/dist/connection/index.d.ts.map +1 -1
- package/dist/connection/lifecycle.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 +343 -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/generated/http-api.d.ts +1401 -462
- package/dist/generated/http-api.d.ts.map +1 -1
- package/dist/generated/schemas.d.ts +2601 -858
- 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 +150 -35
- 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/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 +8 -0
- package/dist/server/access-refresh.d.ts +9 -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 +181 -20
- 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 +179 -19
- package/dist/server/types.d.cts +74 -0
- package/dist/server/types.d.ts +74 -0
- 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 +1 -0
- 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/types.d.ts +40 -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 +1 -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/whiteboard/v1-client.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
journeyHeaders
|
|
3
|
+
} from "./chunk-HW5TTZL2.js";
|
|
4
|
+
import {
|
|
5
|
+
recordReconnect,
|
|
6
|
+
traceReconnect
|
|
7
|
+
} from "./chunk-RCDXIDCN.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,24 @@ 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;
|
|
163
233
|
#pollTimer;
|
|
164
234
|
#sdpTail = Promise.resolve();
|
|
165
235
|
#snapshot;
|
|
@@ -171,11 +241,12 @@ var CloudflareSFUClient = class {
|
|
|
171
241
|
this.#participantId = options.participantId;
|
|
172
242
|
this.#bootstrap = options.bootstrap;
|
|
173
243
|
this.#transport = options.transport;
|
|
174
|
-
this.#pollIntervalMs = options.pollIntervalMs ??
|
|
244
|
+
this.#pollIntervalMs = options.pollIntervalMs ?? 15e3;
|
|
175
245
|
this.#replaceMediaConnection = options.replaceMediaConnection;
|
|
176
246
|
this.#onError = options.onError;
|
|
177
247
|
this.#onRemoteTrack = options.onRemoteTrack;
|
|
178
248
|
this.#onRtcSummary = options.onRtcSummary;
|
|
249
|
+
this.#recordReconnect = options.recordReconnect;
|
|
179
250
|
this.#onScreenEnded = options.onScreenEnded;
|
|
180
251
|
this.#peerConnectionFactory = options.peerConnectionFactory;
|
|
181
252
|
this.#connection = this.#createPeerConnection(options.bootstrap);
|
|
@@ -194,6 +265,19 @@ var CloudflareSFUClient = class {
|
|
|
194
265
|
subscribe(listener) {
|
|
195
266
|
return subscribeSnapshot(this.#snapshotListeners, listener);
|
|
196
267
|
}
|
|
268
|
+
remotePublicationResumed(publicationId) {
|
|
269
|
+
this.#remotePullRetryAfter.delete(publicationId);
|
|
270
|
+
this.remotePublicationsChanged();
|
|
271
|
+
}
|
|
272
|
+
remotePublicationsChanged() {
|
|
273
|
+
recordReconnect(this.#recordReconnect, "remote_publications_changed");
|
|
274
|
+
if (this.#polling) {
|
|
275
|
+
this.#pollAfterCurrent = true;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
this.#clearPoll();
|
|
279
|
+
this.#schedulePoll(0);
|
|
280
|
+
}
|
|
197
281
|
prepareLocalTrack(source, track) {
|
|
198
282
|
this.#requireActive();
|
|
199
283
|
validateTrackSource(source, track);
|
|
@@ -202,7 +286,7 @@ var CloudflareSFUClient = class {
|
|
|
202
286
|
source,
|
|
203
287
|
track,
|
|
204
288
|
transceiver: this.#reusableLocalTransceivers.get(source) ?? null,
|
|
205
|
-
providerPublicationId: null,
|
|
289
|
+
providerPublicationId: this.#reusableLocalPublicationIds.get(source) ?? null,
|
|
206
290
|
pendingOperationId: null,
|
|
207
291
|
pendingTrackName: null,
|
|
208
292
|
desiredEnabled: false,
|
|
@@ -210,6 +294,7 @@ var CloudflareSFUClient = class {
|
|
|
210
294
|
endedListener: null
|
|
211
295
|
};
|
|
212
296
|
this.#reusableLocalTransceivers.delete(source);
|
|
297
|
+
this.#reusableLocalPublicationIds.delete(source);
|
|
213
298
|
if (source === "screen") {
|
|
214
299
|
state.endedListener = () => {
|
|
215
300
|
if (this.#localTracks.get("screen") === state) this.#invokeListener(() => this.#onScreenEnded?.());
|
|
@@ -225,6 +310,7 @@ var CloudflareSFUClient = class {
|
|
|
225
310
|
if (!state) return;
|
|
226
311
|
if (state.enabled) await this.#setPreparedTrackEnabled(state, false);
|
|
227
312
|
if (state.transceiver) this.#reusableLocalTransceivers.set(source, state.transceiver);
|
|
313
|
+
if (state.providerPublicationId) this.#reusableLocalPublicationIds.set(source, state.providerPublicationId);
|
|
228
314
|
this.#removeOwnedLocalTrack(state);
|
|
229
315
|
this.#localTracks.delete(source);
|
|
230
316
|
this.#publishSnapshot();
|
|
@@ -248,28 +334,118 @@ var CloudflareSFUClient = class {
|
|
|
248
334
|
const generation = this.#generation;
|
|
249
335
|
try {
|
|
250
336
|
const transport = this.#requireTransport();
|
|
251
|
-
const authoritative = await transport.listPublications();
|
|
337
|
+
const authoritative = await traceReconnect(this.#recordReconnect, "list_publications", () => transport.listPublications());
|
|
252
338
|
this.#requireGeneration(generation);
|
|
253
339
|
await this.#reconcileRemotePublications(authoritative, generation);
|
|
254
340
|
} catch (error) {
|
|
255
341
|
if (generation === this.#generation && !this.#stopped) this.#reportError(error);
|
|
256
342
|
throw error;
|
|
257
343
|
} finally {
|
|
258
|
-
if (generation === this.#generation)
|
|
344
|
+
if (generation === this.#generation) {
|
|
345
|
+
this.#polling = false;
|
|
346
|
+
if (this.#pollAfterCurrent) {
|
|
347
|
+
this.#pollAfterCurrent = false;
|
|
348
|
+
this.#clearPoll();
|
|
349
|
+
this.#schedulePoll(0);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
259
352
|
}
|
|
260
353
|
}
|
|
261
354
|
async setLocalPublicationTarget(target) {
|
|
262
355
|
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
263
356
|
if (resolved.kind === "result") return resolved.result;
|
|
264
357
|
const state = resolved.value;
|
|
265
|
-
if (
|
|
358
|
+
if (this.#pausedTarget(target)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
266
359
|
try {
|
|
360
|
+
if (target.enabled) await this.#retireUnavailableLocalPublication(state);
|
|
361
|
+
if (this.#pausedTarget(target)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
362
|
+
if (state.enabled === target.enabled) return { outcome: "satisfied", errorCode: null };
|
|
267
363
|
await this.#setPreparedTrackEnabled(state, target.enabled, target.operationId);
|
|
268
364
|
return { outcome: "confirmed", errorCode: null };
|
|
269
365
|
} catch (error) {
|
|
270
366
|
if (!this.#stopped) this.#reportError(error);
|
|
271
|
-
return
|
|
367
|
+
return mediaTargetFailure(error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
#pausedTarget(target) {
|
|
371
|
+
return target.enabled && this.#locallyPausedSources.has(target.source);
|
|
372
|
+
}
|
|
373
|
+
setLocalSourceIntent(source, enabled) {
|
|
374
|
+
if (enabled) {
|
|
375
|
+
this.#locallyPausedSources.delete(source);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this.#locallyPausedSources.add(source);
|
|
379
|
+
const state = this.#localTracks.get(source);
|
|
380
|
+
if (!state) return;
|
|
381
|
+
state.desiredEnabled = false;
|
|
382
|
+
state.track.enabled = false;
|
|
383
|
+
}
|
|
384
|
+
async closeForcedLocalPublication(source) {
|
|
385
|
+
this.setLocalSourceIntent(source, false);
|
|
386
|
+
await this.#retireLocalPublication(source);
|
|
387
|
+
if (!this.#replaceMediaConnection) return;
|
|
388
|
+
const generation = this.#generation;
|
|
389
|
+
await this.#serializeSDP(async () => {
|
|
390
|
+
const existing = new Set([...this.#localTracks.values()].filter((state) => state.source !== source && state.providerPublicationId !== null));
|
|
391
|
+
const states = await this.#prepareConnectionForPublication([], generation);
|
|
392
|
+
await this.#publishPreparedTracksSerialized(
|
|
393
|
+
states.filter((state) => existing.has(state)),
|
|
394
|
+
generation,
|
|
395
|
+
this.#connection,
|
|
396
|
+
this.#bootstrap.connectionId
|
|
397
|
+
);
|
|
398
|
+
this.#requireGeneration(generation);
|
|
399
|
+
this.#setPhase("live", null);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async #retireUnavailableLocalPublication(state) {
|
|
403
|
+
const publicationId = state.providerPublicationId;
|
|
404
|
+
if (!publicationId) return;
|
|
405
|
+
const authoritative = await traceReconnect(this.#recordReconnect, "list_publications", () => this.#requireTransport().listPublications());
|
|
406
|
+
if (authoritative.publications.some((publication) => publication.publicationId === publicationId)) return;
|
|
407
|
+
if (state.providerPublicationId === publicationId) await this.#retireLocalPublication(state.source);
|
|
408
|
+
}
|
|
409
|
+
async #retireLocalPublication(source) {
|
|
410
|
+
const state = this.#localTracks.get(source);
|
|
411
|
+
const transceiver = state?.transceiver ?? this.#reusableLocalTransceivers.get(source);
|
|
412
|
+
const publicationId = state?.providerPublicationId ?? this.#reusableLocalPublicationIds.get(source);
|
|
413
|
+
this.#reusableLocalTransceivers.delete(source);
|
|
414
|
+
this.#reusableLocalPublicationIds.delete(source);
|
|
415
|
+
if (state) {
|
|
416
|
+
state.track.enabled = false;
|
|
417
|
+
state.desiredEnabled = false;
|
|
418
|
+
state.enabled = false;
|
|
419
|
+
state.providerPublicationId = null;
|
|
420
|
+
state.transceiver = null;
|
|
272
421
|
}
|
|
422
|
+
this.#publishSnapshot();
|
|
423
|
+
this.#emitLocal();
|
|
424
|
+
if (!transceiver || !publicationId) return;
|
|
425
|
+
const connection = this.#connection;
|
|
426
|
+
const connectionId = this.#bootstrap.connectionId;
|
|
427
|
+
this.#retiredLocalConnection = connection;
|
|
428
|
+
await this.#serializeSDP(async () => {
|
|
429
|
+
if (connection !== this.#connection) return;
|
|
430
|
+
const mid = requireTransceiverMid(transceiver);
|
|
431
|
+
await this.#boundPeerOperation(transceiver.sender.replaceTrack(null));
|
|
432
|
+
transceiver.stop();
|
|
433
|
+
const offer = await traceReconnect(this.#recordReconnect, "create_offer", () => connection.createOffer());
|
|
434
|
+
await traceReconnect(this.#recordReconnect, "set_local_description", () => connection.setLocalDescription(offer));
|
|
435
|
+
try {
|
|
436
|
+
const response = await this.#requireTransport().closeTracks({
|
|
437
|
+
connectionId,
|
|
438
|
+
...providerDescription(requireDescription(offer)),
|
|
439
|
+
tracks: [{ mid, source, publicationId }],
|
|
440
|
+
force: false
|
|
441
|
+
});
|
|
442
|
+
await this.#applyProviderDescription(response, connection);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
await this.#rollbackLocalOffer(connection);
|
|
445
|
+
if (!this.#stopped) this.#reportError(error);
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
273
449
|
}
|
|
274
450
|
observeLocalPublications(listener) {
|
|
275
451
|
this.#localListeners.add(listener);
|
|
@@ -285,16 +461,21 @@ var CloudflareSFUClient = class {
|
|
|
285
461
|
this.#requireActive();
|
|
286
462
|
const options = "connectionId" in input ? { bootstrap: input } : input;
|
|
287
463
|
validateBootstrap(options.bootstrap);
|
|
464
|
+
recordReconnect(this.#recordReconnect, "media_rebuild_start");
|
|
288
465
|
const generation = ++this.#generation;
|
|
289
466
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
290
467
|
this.#polling = false;
|
|
468
|
+
this.#pollAfterCurrent = false;
|
|
291
469
|
this.#clearPoll();
|
|
470
|
+
this.#remotePullRetryAfter.clear();
|
|
292
471
|
this.#disposeConnection(false);
|
|
293
472
|
this.#reusableLocalTransceivers.clear();
|
|
473
|
+
this.#reusableLocalPublicationIds.clear();
|
|
294
474
|
this.#clearRemoteTracks();
|
|
295
475
|
this.#cursor = null;
|
|
296
476
|
this.#negotiatedGeneration = null;
|
|
297
477
|
this.#replacementAttemptedGeneration = null;
|
|
478
|
+
if (options.bootstrap.connectionId !== this.#bootstrap.connectionId) this.#started = false;
|
|
298
479
|
this.#bootstrap = options.bootstrap;
|
|
299
480
|
if (options.transport) this.#transport = options.transport;
|
|
300
481
|
this.#connection = this.#createPeerConnection(options.bootstrap);
|
|
@@ -325,9 +506,12 @@ var CloudflareSFUClient = class {
|
|
|
325
506
|
this.#generation++;
|
|
326
507
|
this.#connectionEpoch++;
|
|
327
508
|
this.#clearPoll();
|
|
509
|
+
this.#remotePullRetryAfter.clear();
|
|
328
510
|
this.#polling = false;
|
|
511
|
+
this.#pollAfterCurrent = false;
|
|
329
512
|
this.#disposeConnection(true);
|
|
330
513
|
this.#reusableLocalTransceivers.clear();
|
|
514
|
+
this.#reusableLocalPublicationIds.clear();
|
|
331
515
|
this.#clearRemoteTracks();
|
|
332
516
|
for (const state of this.#localTracks.values()) this.#removeOwnedLocalTrack(state);
|
|
333
517
|
this.#localTracks.clear();
|
|
@@ -346,14 +530,33 @@ var CloudflareSFUClient = class {
|
|
|
346
530
|
return this.#disablePreparedTrack(state);
|
|
347
531
|
}
|
|
348
532
|
async #enablePreparedTrack(state, operationId) {
|
|
533
|
+
if (state.providerPublicationId && state.transceiver) {
|
|
534
|
+
state.desiredEnabled = true;
|
|
535
|
+
state.track.enabled = true;
|
|
536
|
+
try {
|
|
537
|
+
await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(state.track));
|
|
538
|
+
} catch (error) {
|
|
539
|
+
state.desiredEnabled = false;
|
|
540
|
+
state.track.enabled = false;
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
state.enabled = !this.#locallyPausedSources.has(state.source);
|
|
544
|
+
state.desiredEnabled = state.enabled;
|
|
545
|
+
state.track.enabled = state.enabled;
|
|
546
|
+
if (!state.enabled) await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(null));
|
|
547
|
+
this.#publishSnapshot();
|
|
548
|
+
this.#emitLocal();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
349
551
|
if (operationId && state.pendingOperationId !== operationId) {
|
|
350
552
|
state.pendingOperationId = operationId;
|
|
351
553
|
state.pendingTrackName = `${state.source}-${operationId}`;
|
|
352
554
|
}
|
|
353
555
|
state.desiredEnabled = true;
|
|
354
556
|
state.track.enabled = true;
|
|
557
|
+
const generation = this.#generation;
|
|
355
558
|
try {
|
|
356
|
-
await this.#publishPreparedTracks([state],
|
|
559
|
+
await this.#publishPreparedTracks([state], generation);
|
|
357
560
|
} catch (error) {
|
|
358
561
|
state.desiredEnabled = false;
|
|
359
562
|
state.enabled = false;
|
|
@@ -365,26 +568,21 @@ var CloudflareSFUClient = class {
|
|
|
365
568
|
const transceiver = state.transceiver;
|
|
366
569
|
state.desiredEnabled = false;
|
|
367
570
|
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
|
-
}
|
|
571
|
+
const detach = transceiver ? this.#boundPeerOperation(transceiver.sender.replaceTrack(null)) : Promise.resolve();
|
|
375
572
|
state.enabled = false;
|
|
376
|
-
state.providerPublicationId = null;
|
|
377
573
|
state.pendingOperationId = null;
|
|
378
574
|
state.pendingTrackName = null;
|
|
379
575
|
this.#publishSnapshot();
|
|
380
576
|
this.#emitLocal();
|
|
577
|
+
await detach;
|
|
381
578
|
}
|
|
382
579
|
async #publishPreparedTracks(states, generation) {
|
|
383
580
|
if (states.length === 0) return;
|
|
384
581
|
await this.#serializeSDP(async () => {
|
|
385
|
-
await this.#
|
|
582
|
+
const pending = await this.#prepareConnectionForPublication(states, generation);
|
|
583
|
+
this.#replacementAttemptedGeneration = null;
|
|
386
584
|
try {
|
|
387
|
-
await this.#publishPreparedTracksSerialized(
|
|
585
|
+
await this.#publishPreparedTracksSerialized(pending, generation, this.#connection, this.#bootstrap.connectionId);
|
|
388
586
|
} catch (error) {
|
|
389
587
|
if (!this.#replaceMediaConnection || !this.#isRetryableConnectionFailure(error) || this.#replacementAttemptedGeneration === generation) throw error;
|
|
390
588
|
await this.#replaceMediaConnectionForRecovery(generation);
|
|
@@ -397,6 +595,25 @@ var CloudflareSFUClient = class {
|
|
|
397
595
|
}
|
|
398
596
|
});
|
|
399
597
|
}
|
|
598
|
+
async #prepareConnectionForPublication(states, generation) {
|
|
599
|
+
if (this.#retiredLocalConnection !== this.#connection || !this.#replaceMediaConnection) {
|
|
600
|
+
await this.#replaceDormantConnectionBeforeNegotiation(generation);
|
|
601
|
+
return states;
|
|
602
|
+
}
|
|
603
|
+
const authoritative = await traceReconnect(this.#recordReconnect, "list_publications", () => this.#requireTransport().listPublications());
|
|
604
|
+
this.#pauseUnavailableOtherSources(authoritative.publications, states);
|
|
605
|
+
this.#replacementAttemptedGeneration = null;
|
|
606
|
+
await this.#replaceMediaConnectionForRecovery(generation);
|
|
607
|
+
return [...this.#localTracks.values()].filter((state) => (state.desiredEnabled || states.includes(state)) && state.track.readyState !== "ended");
|
|
608
|
+
}
|
|
609
|
+
#pauseUnavailableOtherSources(publications, states) {
|
|
610
|
+
for (const state of this.#localTracks.values()) {
|
|
611
|
+
if (states.includes(state) || !state.providerPublicationId) continue;
|
|
612
|
+
if (publications.some((publication) => publication.publicationId === state.providerPublicationId)) continue;
|
|
613
|
+
state.desiredEnabled = false;
|
|
614
|
+
state.track.enabled = false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
400
617
|
async #publishPreparedTracksSerialized(states, generation, connection, connectionId) {
|
|
401
618
|
this.#requireGeneration(generation);
|
|
402
619
|
const pendingStates = states.filter((state) => !state.enabled);
|
|
@@ -404,7 +621,7 @@ var CloudflareSFUClient = class {
|
|
|
404
621
|
const wasLive = connectionIsLive(connection);
|
|
405
622
|
const publications = [];
|
|
406
623
|
try {
|
|
407
|
-
for (const state of pendingStates) publications.push(await this.#prepareLocalPublication(connection, state));
|
|
624
|
+
for (const state of pendingStates) publications.push(await this.#prepareLocalPublication(connection, state, generation));
|
|
408
625
|
const response = await this.#negotiateLocalPublications(connection, connectionId, publications, generation);
|
|
409
626
|
if (!wasLive) await this.#waitForConnection(connection, generation);
|
|
410
627
|
this.#requireGeneration(generation);
|
|
@@ -413,21 +630,56 @@ var CloudflareSFUClient = class {
|
|
|
413
630
|
this.#publishSnapshot();
|
|
414
631
|
this.#emitLocal();
|
|
415
632
|
} catch (error) {
|
|
416
|
-
|
|
417
|
-
|
|
633
|
+
if (generation === this.#generation) {
|
|
634
|
+
await this.#rollbackLocalOffer(connection);
|
|
635
|
+
await this.#discardLocalPublications(publications);
|
|
636
|
+
}
|
|
418
637
|
throw error;
|
|
419
638
|
}
|
|
420
639
|
}
|
|
421
|
-
async #prepareLocalPublication(connection, state) {
|
|
640
|
+
async #prepareLocalPublication(connection, state, generation) {
|
|
422
641
|
const reusedTransceiver = state.transceiver !== null;
|
|
423
|
-
const transceiver = state.transceiver ?? connection
|
|
424
|
-
if (reusedTransceiver) await transceiver.sender.replaceTrack(state.track);
|
|
642
|
+
const transceiver = state.transceiver ?? this.#addLocalTransceiver(connection, state);
|
|
643
|
+
if (reusedTransceiver) await this.#boundPeerOperation(transceiver.sender.replaceTrack(state.track));
|
|
644
|
+
this.#requireGeneration(generation);
|
|
425
645
|
state.transceiver = transceiver;
|
|
426
646
|
return { state, transceiver, trackName: state.pendingTrackName ?? `${state.source}-${globalThis.crypto.randomUUID()}`, reusedTransceiver };
|
|
427
647
|
}
|
|
648
|
+
#addLocalTransceiver(connection, state) {
|
|
649
|
+
if (state.source !== "camera" || firefoxCameraSimulcastUnsupported()) return connection.addTransceiver(state.track, { direction: "sendonly" });
|
|
650
|
+
const sendEncodings = [
|
|
651
|
+
{ rid: "h", scaleResolutionDownBy: 1, maxBitrate: 25e5, scalabilityMode: "L1T1" },
|
|
652
|
+
{ rid: "l", scaleResolutionDownBy: 2, maxBitrate: 65e4, scalabilityMode: "L1T1" }
|
|
653
|
+
];
|
|
654
|
+
try {
|
|
655
|
+
return connection.addTransceiver(state.track, {
|
|
656
|
+
direction: "sendonly",
|
|
657
|
+
sendEncodings
|
|
658
|
+
});
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (!simulcastUnsupported(error)) throw error;
|
|
661
|
+
return connection.addTransceiver(state.track, { direction: "sendonly" });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
428
664
|
async #negotiateLocalPublications(connection, connectionId, publications, generation) {
|
|
429
|
-
|
|
430
|
-
|
|
665
|
+
let offer;
|
|
666
|
+
try {
|
|
667
|
+
offer = await traceReconnect(this.#recordReconnect, "create_offer", () => connection.createOffer());
|
|
668
|
+
await traceReconnect(this.#recordReconnect, "set_local_description", () => connection.setLocalDescription(offer));
|
|
669
|
+
} catch (error) {
|
|
670
|
+
const cameras = publications.filter(({ state, transceiver }) => state.source === "camera" && transceiver.sender.getParameters().encodings.length > 1);
|
|
671
|
+
if (!simulcastUnsupported(error) || cameras.length === 0) throw error;
|
|
672
|
+
await this.#rollbackLocalOffer(connection);
|
|
673
|
+
this.#requireGeneration(generation);
|
|
674
|
+
for (const publication of cameras) {
|
|
675
|
+
await this.#boundPeerOperation(publication.transceiver.sender.replaceTrack(null));
|
|
676
|
+
publication.transceiver.stop();
|
|
677
|
+
publication.transceiver = connection.addTransceiver(publication.state.track, { direction: "sendonly" });
|
|
678
|
+
publication.state.transceiver = publication.transceiver;
|
|
679
|
+
}
|
|
680
|
+
offer = await traceReconnect(this.#recordReconnect, "create_offer", () => connection.createOffer());
|
|
681
|
+
await traceReconnect(this.#recordReconnect, "set_local_description", () => connection.setLocalDescription(offer));
|
|
682
|
+
}
|
|
431
683
|
const tracks = publications.map(
|
|
432
684
|
({ state, transceiver, trackName }) => ({
|
|
433
685
|
location: "local",
|
|
@@ -436,17 +688,20 @@ var CloudflareSFUClient = class {
|
|
|
436
688
|
source: state.source
|
|
437
689
|
})
|
|
438
690
|
);
|
|
439
|
-
const response = await this.#requireTransport().addTracks({ connectionId,
|
|
691
|
+
const response = await traceReconnect(this.#recordReconnect, "publish_tracks", () => this.#requireTransport().addTracks({ connectionId, ...providerDescription(requireDescription(offer)), tracks }));
|
|
440
692
|
this.#requireGeneration(generation);
|
|
441
|
-
await
|
|
693
|
+
await this.#applyProviderDescription(response, connection);
|
|
442
694
|
return response;
|
|
443
695
|
}
|
|
444
696
|
#confirmLocalPublications(publications, tracks) {
|
|
445
697
|
for (const { state, transceiver } of publications) {
|
|
446
698
|
const authoritative = tracks?.find((track) => track.location === "local" && track.mid === transceiver.mid && track.source === state.source);
|
|
447
699
|
if (!authoritative?.publicationId) throw new CloudflareSFUError("Chalk did not return an authoritative local publication ID", "invalid_publication");
|
|
448
|
-
state.
|
|
449
|
-
state.enabled =
|
|
700
|
+
if (state.transceiver !== transceiver) continue;
|
|
701
|
+
state.enabled = !this.#locallyPausedSources.has(state.source);
|
|
702
|
+
state.desiredEnabled = state.enabled;
|
|
703
|
+
state.track.enabled = state.enabled;
|
|
704
|
+
if (!state.enabled) void transceiver.sender.replaceTrack(null).catch((error) => this.#reportError(error));
|
|
450
705
|
state.providerPublicationId = authoritative.publicationId;
|
|
451
706
|
state.pendingOperationId = null;
|
|
452
707
|
state.pendingTrackName = null;
|
|
@@ -482,9 +737,9 @@ var CloudflareSFUClient = class {
|
|
|
482
737
|
const cursor = this.#validatedRemotePublicationCursor(authoritative);
|
|
483
738
|
if (cursor === null) return;
|
|
484
739
|
const ordering = comparePublicationCursor(this.#cursor, cursor);
|
|
485
|
-
if (ordering
|
|
740
|
+
if (ordering === "stale" || ordering === "same" && !this.#remotePullIncomplete) return;
|
|
486
741
|
const desired = this.#desiredRemotePublications(authoritative, cursor);
|
|
487
|
-
const toPull =
|
|
742
|
+
const toPull = this.#pendingRemotePulls(desired);
|
|
488
743
|
const pulled = await this.#pullWithRecovery(toPull, cursor, generation);
|
|
489
744
|
if (pulled === null) return;
|
|
490
745
|
this.#requireGeneration(generation);
|
|
@@ -493,10 +748,24 @@ var CloudflareSFUClient = class {
|
|
|
493
748
|
this.#remoteTracks.clear();
|
|
494
749
|
for (const [key, publication] of next) this.#remoteTracks.set(key, publication);
|
|
495
750
|
this.#cursor = cursor;
|
|
751
|
+
this.#remotePullIncomplete = [...desired].some(([key, publication]) => next.get(key)?.publicationId !== publication.publicationId);
|
|
496
752
|
for (const publication of pulled) this.#invokeListener(() => this.#onRemoteTrack?.(publication));
|
|
497
753
|
this.#publishSnapshot();
|
|
498
754
|
this.#emitRemote();
|
|
499
755
|
}
|
|
756
|
+
#pendingRemotePulls(desired) {
|
|
757
|
+
const desiredIds = new Set([...desired.values()].map((publication) => publication.publicationId));
|
|
758
|
+
for (const publicationId of this.#remotePullRetryAfter.keys()) {
|
|
759
|
+
if (!desiredIds.has(publicationId)) this.#remotePullRetryAfter.delete(publicationId);
|
|
760
|
+
}
|
|
761
|
+
const now = Date.now();
|
|
762
|
+
return [...desired].filter(([key, publication]) => {
|
|
763
|
+
if (this.#remoteTracks.get(key)?.publicationId === publication.publicationId) return false;
|
|
764
|
+
const remaining = (this.#remotePullRetryAfter.get(publication.publicationId) ?? 0) - now;
|
|
765
|
+
if (remaining > 0) recordReconnect(this.#recordReconnect, "remote_pull_deferred", { remaining_ms: remaining, source: publication.source });
|
|
766
|
+
return remaining <= 0;
|
|
767
|
+
}).map(([, publication]) => publication);
|
|
768
|
+
}
|
|
500
769
|
#validatedRemotePublicationCursor(authoritative) {
|
|
501
770
|
if (this.#cursor?.signature === INVALID_PUBLICATION_SIGNATURE && this.#cursor.incarnation === authoritative.incarnation && this.#cursor.sequence === authoritative.sequence) return null;
|
|
502
771
|
try {
|
|
@@ -520,7 +789,7 @@ var CloudflareSFUClient = class {
|
|
|
520
789
|
} catch (error) {
|
|
521
790
|
if (generation !== this.#generation || this.#stopped) throw error;
|
|
522
791
|
if (!this.#canReplaceConnectionAfterRemotePull(error, generation)) {
|
|
523
|
-
this.#observeRemotePublicationCursor(cursor);
|
|
792
|
+
this.#observeRemotePublicationCursor(cursor, retryableRemotePull(error));
|
|
524
793
|
throw error;
|
|
525
794
|
}
|
|
526
795
|
return this.#retryRemotePullOnReplacement(publications, cursor, generation);
|
|
@@ -539,7 +808,7 @@ var CloudflareSFUClient = class {
|
|
|
539
808
|
return await this.#pull(publications, generation);
|
|
540
809
|
} catch (error) {
|
|
541
810
|
if (generation !== this.#generation || this.#stopped) throw error;
|
|
542
|
-
this.#observeRemotePublicationCursor(cursor);
|
|
811
|
+
this.#observeRemotePublicationCursor(cursor, retryableRemotePull(error));
|
|
543
812
|
throw error;
|
|
544
813
|
}
|
|
545
814
|
}
|
|
@@ -550,7 +819,15 @@ var CloudflareSFUClient = class {
|
|
|
550
819
|
const connectionId = this.#bootstrap.connectionId;
|
|
551
820
|
const requested = publications.map((publication) => {
|
|
552
821
|
const reference = parseCloudflareSFUPublicationID(publication.publicationId);
|
|
553
|
-
return {
|
|
822
|
+
return {
|
|
823
|
+
location: "remote",
|
|
824
|
+
sessionId: reference.connectionId,
|
|
825
|
+
trackName: reference.trackName,
|
|
826
|
+
// h sorts before l: prefer full camera quality, but keep video flowing
|
|
827
|
+
// when the publisher pauses h. Do not silently downgrade a live h layer.
|
|
828
|
+
// https://developers.cloudflare.com/realtime/sfu/features/simulcast/#quality-control
|
|
829
|
+
...publication.source === "camera" ? { simulcast: { preferredRid: "h", priorityOrdering: "none", ridNotAvailable: "asciibetical" } } : {}
|
|
830
|
+
};
|
|
554
831
|
});
|
|
555
832
|
const received = /* @__PURE__ */ new Map();
|
|
556
833
|
const onTrack = (event) => {
|
|
@@ -559,17 +836,20 @@ var CloudflareSFUClient = class {
|
|
|
559
836
|
connection.addEventListener("track", onTrack);
|
|
560
837
|
try {
|
|
561
838
|
this.#requireGeneration(generation);
|
|
562
|
-
const response = await this.#requireTransport().addTracks({ connectionId, tracks: requested });
|
|
839
|
+
const response = await traceReconnect(this.#recordReconnect, "pull_tracks", () => this.#requireTransport().addTracks({ connectionId, tracks: requested, allowPartialRemoteTracks: true }));
|
|
563
840
|
this.#requireGeneration(generation);
|
|
841
|
+
this.#recordRemotePullErrors(publications, response.trackErrors ?? []);
|
|
564
842
|
const responseTracks = response.tracks ?? [];
|
|
843
|
+
const matched = matchRemotePullTracks(publications, responseTracks);
|
|
844
|
+
for (const { publication } of matched) this.#remotePullRetryAfter.delete(publication.publicationId);
|
|
565
845
|
await this.#completeRenegotiation(response, connection, connectionId, generation);
|
|
846
|
+
if (matched.length === 0) return [];
|
|
566
847
|
await this.#waitForConnection(connection, generation);
|
|
567
848
|
this.#negotiatedGeneration = generation;
|
|
568
|
-
await waitFor(() => responseTracks.every((track) => track.mid !== void 0 && received.has(track.mid)), 5e3);
|
|
849
|
+
await traceReconnect(this.#recordReconnect, "remote_track_arrival", () => waitFor(() => responseTracks.every((track) => track.mid !== void 0 && received.has(track.mid)), 5e3));
|
|
569
850
|
this.#requireGeneration(generation);
|
|
570
|
-
return
|
|
571
|
-
const
|
|
572
|
-
const track = responseTrack?.mid === void 0 ? void 0 : received.get(responseTrack.mid);
|
|
851
|
+
return matched.map(({ publication, mid }) => {
|
|
852
|
+
const track = received.get(mid);
|
|
573
853
|
if (!track) throw new CloudflareSFUError("A negotiated remote track did not arrive", "media_failed");
|
|
574
854
|
return Object.freeze({ ...publication, track });
|
|
575
855
|
});
|
|
@@ -581,38 +861,151 @@ var CloudflareSFUClient = class {
|
|
|
581
861
|
}
|
|
582
862
|
});
|
|
583
863
|
}
|
|
864
|
+
#recordRemotePullErrors(publications, failures) {
|
|
865
|
+
const requested = new Map(
|
|
866
|
+
publications.map((publication) => {
|
|
867
|
+
const reference = parseCloudflareSFUPublicationID(publication.publicationId);
|
|
868
|
+
return [`${reference.connectionId}\0${reference.trackName}`, publication];
|
|
869
|
+
})
|
|
870
|
+
);
|
|
871
|
+
for (const failure of failures) {
|
|
872
|
+
const publication = requested.get(`${failure.connectionId}\0${failure.trackName}`);
|
|
873
|
+
if (!publication) throw new CloudflareSFUError("Cloudflare SFU returned an unrequested remote track error", "invalid_publication");
|
|
874
|
+
const delay = 750;
|
|
875
|
+
this.#remotePullRetryAfter.set(publication.publicationId, Date.now() + delay);
|
|
876
|
+
recordReconnect(this.#recordReconnect, "remote_pull_backoff", { delay_ms: delay, retry_kind: failure.code === "empty_track_error" ? "empty_track" : "provider_error", source: publication.source });
|
|
877
|
+
this.#reportError(new CloudflareSFUError(`Cloudflare SFU remote track failed: ${failure.code}`, "media_failed", { providerCode: failure.code }));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
584
880
|
async #completeRenegotiation(response, connection, connectionId, generation) {
|
|
585
881
|
if (!response.requiresImmediateRenegotiation) return;
|
|
586
|
-
|
|
587
|
-
await connection.
|
|
588
|
-
|
|
589
|
-
await
|
|
590
|
-
await this.#requireTransport().renegotiate({ connectionId, sessionDescription: requireDescription(answer) });
|
|
882
|
+
await this.#applyProviderDescription(response, connection);
|
|
883
|
+
const answer = await traceReconnect(this.#recordReconnect, "create_answer", () => connection.createAnswer());
|
|
884
|
+
await traceReconnect(this.#recordReconnect, "set_local_answer", () => connection.setLocalDescription(answer));
|
|
885
|
+
await traceReconnect(this.#recordReconnect, "renegotiate", () => this.#requireTransport().renegotiate({ connectionId, ...providerDescription(requireDescription(answer)) }));
|
|
591
886
|
this.#requireGeneration(generation);
|
|
592
887
|
}
|
|
888
|
+
async #applyProviderDescription(response, connection = this.#connection) {
|
|
889
|
+
await traceReconnect(this.#recordReconnect, "set_remote_description", () => connection.setRemoteDescription(requireSFUDescription(response.sessionDescription)));
|
|
890
|
+
}
|
|
593
891
|
#serializeSDP(operation) {
|
|
594
|
-
const
|
|
892
|
+
const generation = this.#generation;
|
|
893
|
+
const queuedAt = performance.now();
|
|
894
|
+
let expired = false;
|
|
895
|
+
let timer;
|
|
896
|
+
const timeout = new CloudflareSFUError("Cloudflare SFU negotiation timed out. Try again.", "negotiation_timeout");
|
|
897
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
898
|
+
timer = globalThis.setTimeout(() => {
|
|
899
|
+
expired = true;
|
|
900
|
+
if (generation === this.#generation && !this.#stopped) this.#expireNegotiation(timeout);
|
|
901
|
+
reject(timeout);
|
|
902
|
+
}, NEGOTIATION_TIMEOUT_MS);
|
|
903
|
+
});
|
|
904
|
+
const run = () => {
|
|
905
|
+
if (expired || generation !== this.#generation) throw timeout;
|
|
906
|
+
recordReconnect(this.#recordReconnect, "sdp_queue", { duration_ms: performance.now() - queuedAt });
|
|
907
|
+
return operation();
|
|
908
|
+
};
|
|
909
|
+
const pending = this.#sdpTail.then(run, run);
|
|
910
|
+
const result = Promise.race([pending, deadline]).catch((error) => {
|
|
911
|
+
if (error instanceof CloudflareSFUError && error.code === "signaling_timeout" && generation === this.#generation && !this.#stopped) this.#expireNegotiation(error);
|
|
912
|
+
throw error;
|
|
913
|
+
}).finally(() => {
|
|
914
|
+
if (timer !== void 0) globalThis.clearTimeout(timer);
|
|
915
|
+
});
|
|
595
916
|
this.#sdpTail = result.then(
|
|
596
917
|
() => void 0,
|
|
597
918
|
() => void 0
|
|
598
919
|
);
|
|
599
920
|
return result;
|
|
600
921
|
}
|
|
922
|
+
async #boundPeerOperation(operation) {
|
|
923
|
+
const generation = this.#generation;
|
|
924
|
+
let timer;
|
|
925
|
+
const timeout = new CloudflareSFUError("The browser media operation timed out. Try again.", "negotiation_timeout");
|
|
926
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
927
|
+
timer = globalThis.setTimeout(() => {
|
|
928
|
+
if (generation === this.#generation && !this.#stopped) this.#expireNegotiation(timeout);
|
|
929
|
+
reject(timeout);
|
|
930
|
+
}, PEER_OPERATION_TIMEOUT_MS);
|
|
931
|
+
});
|
|
932
|
+
try {
|
|
933
|
+
await Promise.race([operation, deadline]);
|
|
934
|
+
} finally {
|
|
935
|
+
if (timer !== void 0) globalThis.clearTimeout(timer);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
#expireNegotiation(error) {
|
|
939
|
+
this.#generation++;
|
|
940
|
+
this.#connectionEpoch++;
|
|
941
|
+
this.#clearPoll();
|
|
942
|
+
this.#polling = false;
|
|
943
|
+
this.#disposeConnection(false);
|
|
944
|
+
this.#setFailure(error, "media_failed");
|
|
945
|
+
}
|
|
601
946
|
#waitForConnection(connection, generation) {
|
|
602
|
-
return
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
947
|
+
return traceReconnect(
|
|
948
|
+
this.#recordReconnect,
|
|
949
|
+
"connection_ready",
|
|
950
|
+
() => waitFor(
|
|
951
|
+
() => {
|
|
952
|
+
this.#requireGeneration(generation);
|
|
953
|
+
return connectionIsLive(connection);
|
|
954
|
+
},
|
|
955
|
+
CONNECTION_TIMEOUT_MS,
|
|
956
|
+
"Timed out waiting for the Cloudflare SFU peer connection"
|
|
957
|
+
)
|
|
609
958
|
);
|
|
610
959
|
}
|
|
960
|
+
#cameraSimulcastSender(state) {
|
|
961
|
+
if (!state?.enabled) return void 0;
|
|
962
|
+
const sender = state.transceiver?.sender;
|
|
963
|
+
if (!sender?.track || sender.getParameters().encodings.length < 2) return void 0;
|
|
964
|
+
return sender;
|
|
965
|
+
}
|
|
966
|
+
async #applyCameraUplink(sender, report, policy) {
|
|
967
|
+
const parameters = sender.getParameters();
|
|
968
|
+
const low = parameters.encodings.find((encoding) => encoding.rid === "l");
|
|
969
|
+
if (!low) return;
|
|
970
|
+
const active = policy.sample(cameraUplinkBitrate(report.values()), low.active !== false);
|
|
971
|
+
if (active === (low.active !== false)) return;
|
|
972
|
+
low.active = active;
|
|
973
|
+
parameters.degradationPreference = "maintain-framerate";
|
|
974
|
+
await sender.setParameters(parameters);
|
|
975
|
+
}
|
|
611
976
|
#observeConnection(connection, generation, connectionEpoch) {
|
|
977
|
+
this.#disposeConnectionObservation?.();
|
|
612
978
|
let disposed = false;
|
|
979
|
+
let cameraTimer;
|
|
980
|
+
const cameraPolicy = new CameraUplinkPolicy();
|
|
981
|
+
let observedCamera;
|
|
982
|
+
const current = () => !disposed && generation === this.#generation && connectionEpoch === this.#connectionEpoch && !this.#stopped && connection.connectionState !== "failed" && connection.connectionState !== "closed";
|
|
983
|
+
const adaptCamera = async () => {
|
|
984
|
+
if (!current()) return;
|
|
985
|
+
try {
|
|
986
|
+
const state = this.#localTracks.get("camera");
|
|
987
|
+
if (state !== observedCamera) {
|
|
988
|
+
cameraPolicy.reset();
|
|
989
|
+
observedCamera = state;
|
|
990
|
+
}
|
|
991
|
+
const sender = this.#cameraSimulcastSender(state);
|
|
992
|
+
if (!sender) {
|
|
993
|
+
cameraPolicy.reset();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const report = await connection.getStats();
|
|
997
|
+
if (!current() || this.#cameraSimulcastSender(state) !== sender || connection.signalingState !== "stable") return;
|
|
998
|
+
await this.#applyCameraUplink(sender, report, cameraPolicy);
|
|
999
|
+
} catch {
|
|
1000
|
+
cameraPolicy.reset();
|
|
1001
|
+
} finally {
|
|
1002
|
+
if (current()) cameraTimer = globalThis.setTimeout(adaptCamera, 1e3);
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
613
1005
|
const dispose = () => {
|
|
614
1006
|
if (disposed) return;
|
|
615
1007
|
disposed = true;
|
|
1008
|
+
if (cameraTimer !== void 0) globalThis.clearTimeout(cameraTimer);
|
|
616
1009
|
connection.removeEventListener("connectionstatechange", capture);
|
|
617
1010
|
connection.removeEventListener("iceconnectionstatechange", capture);
|
|
618
1011
|
connection.removeEventListener("signalingstatechange", capture);
|
|
@@ -625,6 +1018,7 @@ var CloudflareSFUClient = class {
|
|
|
625
1018
|
};
|
|
626
1019
|
const capture = () => {
|
|
627
1020
|
if (disposed || generation !== this.#generation || connectionEpoch !== this.#connectionEpoch || this.#stopped) return;
|
|
1021
|
+
recordReconnect(this.#recordReconnect, "rtc_state", { connection_state: connection.connectionState, ice_connection_state: connection.iceConnectionState, signaling_state: connection.signalingState });
|
|
628
1022
|
observe();
|
|
629
1023
|
const recorder = this.#onRtcSummary;
|
|
630
1024
|
if (recorder) {
|
|
@@ -641,11 +1035,67 @@ var CloudflareSFUClient = class {
|
|
|
641
1035
|
}
|
|
642
1036
|
if (connection.connectionState === "closed") dispose();
|
|
643
1037
|
};
|
|
1038
|
+
this.#disposeConnectionObservation = dispose;
|
|
1039
|
+
cameraTimer = globalThis.setTimeout(adaptCamera, 1e3);
|
|
1040
|
+
if (this.#recordReconnect) this.#sampleRecoveryTraffic(connection, generation, connectionEpoch);
|
|
644
1041
|
connection.addEventListener("connectionstatechange", capture);
|
|
645
1042
|
connection.addEventListener("iceconnectionstatechange", capture);
|
|
646
1043
|
connection.addEventListener("signalingstatechange", capture);
|
|
647
1044
|
capture();
|
|
648
1045
|
}
|
|
1046
|
+
#sampleRecoveryTraffic(connection, generation, connectionEpoch) {
|
|
1047
|
+
const traffic = /* @__PURE__ */ new Map();
|
|
1048
|
+
let dtlsState;
|
|
1049
|
+
const current = () => generation === this.#generation && connectionEpoch === this.#connectionEpoch && !this.#stopped && connection.connectionState !== "closed" && connection.connectionState !== "failed";
|
|
1050
|
+
const sample = async () => {
|
|
1051
|
+
if (!current()) return;
|
|
1052
|
+
try {
|
|
1053
|
+
const report = await connection.getStats();
|
|
1054
|
+
if (!current()) return;
|
|
1055
|
+
const now = performance.now();
|
|
1056
|
+
report.forEach((entry) => {
|
|
1057
|
+
if (entry.type === "transport") {
|
|
1058
|
+
const nextState = rtcString(entry, "dtlsState");
|
|
1059
|
+
if (nextState !== dtlsState) recordReconnect(this.#recordReconnect, "dtls_state", { dtls_state: nextState ?? "unknown" });
|
|
1060
|
+
dtlsState = nextState;
|
|
1061
|
+
}
|
|
1062
|
+
this.#observeRecoveryTraffic(entry, traffic, now);
|
|
1063
|
+
});
|
|
1064
|
+
} catch {
|
|
1065
|
+
if (current()) recordReconnect(this.#recordReconnect, "rtc_stats_unavailable");
|
|
1066
|
+
}
|
|
1067
|
+
if (current()) globalThis.setTimeout(() => void sample(), 250);
|
|
1068
|
+
};
|
|
1069
|
+
void sample();
|
|
1070
|
+
}
|
|
1071
|
+
#observeRecoveryTraffic(entry, traffic, now) {
|
|
1072
|
+
if (entry.type !== "inbound-rtp" && entry.type !== "outbound-rtp") return;
|
|
1073
|
+
const direction = entry.type === "inbound-rtp" ? "inbound" : "outbound";
|
|
1074
|
+
const kind = rtcString(entry, "kind") ?? "unknown";
|
|
1075
|
+
const previous = traffic.get(entry.id);
|
|
1076
|
+
const next = previous ?? {
|
|
1077
|
+
packets: { count: 0, lastProgress: now, stalled: false },
|
|
1078
|
+
frames: { count: 0, lastProgress: now, stalled: false }
|
|
1079
|
+
};
|
|
1080
|
+
const packets = recoveryTrafficCount(entry, direction, "packet");
|
|
1081
|
+
const frames = recoveryTrafficCount(entry, direction, "frame");
|
|
1082
|
+
this.#observeTrafficCounter(next.packets, { count: packets, now, direction, kind, unit: "packet", existed: previous !== void 0 });
|
|
1083
|
+
this.#observeTrafficCounter(next.frames, { count: frames, now, direction, kind, unit: "frame", existed: previous !== void 0 });
|
|
1084
|
+
traffic.set(entry.id, next);
|
|
1085
|
+
}
|
|
1086
|
+
#observeTrafficCounter(progress, sample) {
|
|
1087
|
+
const attributes = { direction: sample.direction, kind: sample.kind };
|
|
1088
|
+
if (sample.count > progress.count) {
|
|
1089
|
+
if (progress.count === 0) recordReconnect(this.#recordReconnect, `first_${sample.unit}`, attributes);
|
|
1090
|
+
else if (progress.stalled) recordReconnect(this.#recordReconnect, sample.unit === "packet" ? "rtp_resumed" : "frame_resumed", attributes);
|
|
1091
|
+
progress.lastProgress = sample.now;
|
|
1092
|
+
progress.stalled = false;
|
|
1093
|
+
} else if (trafficStalled(progress, sample)) {
|
|
1094
|
+
progress.stalled = true;
|
|
1095
|
+
recordReconnect(this.#recordReconnect, sample.unit === "packet" ? "rtp_stall" : "frame_stall", attributes);
|
|
1096
|
+
}
|
|
1097
|
+
progress.count = sample.count;
|
|
1098
|
+
}
|
|
649
1099
|
async #replaceDormantConnectionBeforeNegotiation(generation) {
|
|
650
1100
|
if (!this.#started || this.#negotiatedGeneration === generation || this.#replacementAttemptedGeneration === generation || !this.#replaceMediaConnection) return;
|
|
651
1101
|
await this.#replaceMediaConnectionForRecovery(generation);
|
|
@@ -656,7 +1106,8 @@ var CloudflareSFUClient = class {
|
|
|
656
1106
|
throw new CloudflareSFUError("A fresh Cloudflare SFU connection is unavailable", "signaling_failed");
|
|
657
1107
|
}
|
|
658
1108
|
this.#replacementAttemptedGeneration = generation;
|
|
659
|
-
const
|
|
1109
|
+
const replaceMediaConnection = this.#replaceMediaConnection;
|
|
1110
|
+
const bootstrap = await traceReconnect(this.#recordReconnect, "replace_access", replaceMediaConnection);
|
|
660
1111
|
this.#requireGeneration(generation);
|
|
661
1112
|
validateBootstrap(bootstrap);
|
|
662
1113
|
if (bootstrap.connectionId === this.#bootstrap.connectionId) {
|
|
@@ -665,10 +1116,12 @@ var CloudflareSFUClient = class {
|
|
|
665
1116
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
666
1117
|
this.#disposeConnection(false);
|
|
667
1118
|
this.#reusableLocalTransceivers.clear();
|
|
1119
|
+
this.#reusableLocalPublicationIds.clear();
|
|
668
1120
|
this.#clearRemoteTracks();
|
|
669
1121
|
this.#cursor = null;
|
|
670
1122
|
this.#bootstrap = bootstrap;
|
|
671
1123
|
this.#connection = this.#createPeerConnection(bootstrap);
|
|
1124
|
+
this.#retiredLocalConnection = null;
|
|
672
1125
|
this.#negotiatedGeneration = null;
|
|
673
1126
|
this.#observeConnection(this.#connection, generation, connectionEpoch);
|
|
674
1127
|
for (const state of this.#localTracks.values()) {
|
|
@@ -677,13 +1130,15 @@ var CloudflareSFUClient = class {
|
|
|
677
1130
|
state.providerPublicationId = null;
|
|
678
1131
|
}
|
|
679
1132
|
this.#setPhase("recovering", null);
|
|
1133
|
+
this.remotePublicationsChanged();
|
|
680
1134
|
}
|
|
681
1135
|
async #republishPreparedTracksAfterRecovery(generation) {
|
|
682
1136
|
const enabled = [...this.#localTracks.values()].filter((state) => state.desiredEnabled && state.track.readyState !== "ended");
|
|
683
1137
|
await this.#publishPreparedTracks(enabled, generation);
|
|
684
1138
|
}
|
|
685
|
-
#observeRemotePublicationCursor(cursor) {
|
|
1139
|
+
#observeRemotePublicationCursor(cursor, pullIncomplete = false) {
|
|
686
1140
|
this.#cursor = cursor;
|
|
1141
|
+
this.#remotePullIncomplete = pullIncomplete;
|
|
687
1142
|
this.#publishSnapshot();
|
|
688
1143
|
}
|
|
689
1144
|
#quarantineInvalidRemotePublicationCursor(authoritative) {
|
|
@@ -698,6 +1153,8 @@ var CloudflareSFUClient = class {
|
|
|
698
1153
|
return create({ iceServers: [{ urls: bootstrap.stunServer }], bundlePolicy: "max-bundle" });
|
|
699
1154
|
}
|
|
700
1155
|
#disposeConnection(stopSenders) {
|
|
1156
|
+
this.#disposeConnectionObservation?.();
|
|
1157
|
+
this.#disposeConnectionObservation = void 0;
|
|
701
1158
|
if (stopSenders) {
|
|
702
1159
|
for (const sender of this.#connection.getSenders()) {
|
|
703
1160
|
if (sender.track) safeStopTrack(sender.track, this.#reportError.bind(this));
|
|
@@ -737,11 +1194,15 @@ var CloudflareSFUClient = class {
|
|
|
737
1194
|
if (this.#stopped || !this.#started || this.#pollTimer !== void 0) return;
|
|
738
1195
|
this.#pollTimer = globalThis.setTimeout(async () => {
|
|
739
1196
|
this.#pollTimer = void 0;
|
|
1197
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1198
|
+
this.#schedulePoll();
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
740
1201
|
try {
|
|
741
1202
|
await this.refreshRemotePublications();
|
|
742
1203
|
} catch {
|
|
743
1204
|
} finally {
|
|
744
|
-
this.#schedulePoll();
|
|
1205
|
+
this.#schedulePoll(this.#remotePullIncomplete ? 750 : this.#pollIntervalMs);
|
|
745
1206
|
}
|
|
746
1207
|
}, delayMs);
|
|
747
1208
|
}
|
|
@@ -754,7 +1215,7 @@ var CloudflareSFUClient = class {
|
|
|
754
1215
|
participantId: this.#participantId,
|
|
755
1216
|
source: state.source,
|
|
756
1217
|
enabled: state.enabled,
|
|
757
|
-
publicationId: state.
|
|
1218
|
+
publicationId: state.providerPublicationId
|
|
758
1219
|
}));
|
|
759
1220
|
}
|
|
760
1221
|
#projectRemotePublications() {
|
|
@@ -772,7 +1233,7 @@ var CloudflareSFUClient = class {
|
|
|
772
1233
|
(state) => Object.freeze({
|
|
773
1234
|
source: state.source,
|
|
774
1235
|
enabled: state.enabled,
|
|
775
|
-
publicationId: state.
|
|
1236
|
+
publicationId: state.providerPublicationId,
|
|
776
1237
|
track: state.track
|
|
777
1238
|
})
|
|
778
1239
|
) : EMPTY_LOCAL,
|
|
@@ -908,10 +1369,10 @@ function desiredRemotePublications(publications, participantId) {
|
|
|
908
1369
|
function reconcileRemoteTracks(desired, pulled, current) {
|
|
909
1370
|
const pulledByKey = new Map(pulled.map((publication) => [publicationKey(publication), publication]));
|
|
910
1371
|
return new Map(
|
|
911
|
-
[...desired].
|
|
1372
|
+
[...desired].flatMap(([key, publication]) => {
|
|
912
1373
|
const track = pulledByKey.get(key) ?? current.get(key);
|
|
913
|
-
if (!track || track.publicationId !== publication.publicationId)
|
|
914
|
-
return [key, track];
|
|
1374
|
+
if (!track || track.publicationId !== publication.publicationId) return [];
|
|
1375
|
+
return [[key, track]];
|
|
915
1376
|
})
|
|
916
1377
|
);
|
|
917
1378
|
}
|
|
@@ -973,31 +1434,63 @@ function publicationEqual(left, right) {
|
|
|
973
1434
|
function remotePublicationEqual(left, right) {
|
|
974
1435
|
return right !== void 0 && left.participantId === right.participantId && left.source === right.source && left.publicationId === right.publicationId && left.track === right.track;
|
|
975
1436
|
}
|
|
1437
|
+
function providerDescription(description) {
|
|
1438
|
+
return { sessionDescription: description };
|
|
1439
|
+
}
|
|
1440
|
+
function retryableRemotePull(error) {
|
|
1441
|
+
return error instanceof CloudflareSFUError && ["signaling_failed", "signaling_timeout", "media_failed", "negotiation_timeout"].includes(error.code);
|
|
1442
|
+
}
|
|
1443
|
+
function firefoxCameraSimulcastUnsupported() {
|
|
1444
|
+
return typeof navigator !== "undefined" && /\bFirefox\/\d+/i.test(navigator.userAgent);
|
|
1445
|
+
}
|
|
1446
|
+
function simulcastUnsupported(error) {
|
|
1447
|
+
return error instanceof Error && ["NotSupportedError", "OperationError", "TypeError"].includes(error.name);
|
|
1448
|
+
}
|
|
1449
|
+
function mediaTargetFailure(error) {
|
|
1450
|
+
const code = error instanceof CloudflareSFUError ? error.code : "media_failed";
|
|
1451
|
+
const ambiguous = code === "signaling_timeout" || code === "negotiation_timeout";
|
|
1452
|
+
return { outcome: ambiguous ? "ambiguous" : "retryable_failure", errorCode: code };
|
|
1453
|
+
}
|
|
1454
|
+
function recoveryTrafficCount(entry, direction, unit) {
|
|
1455
|
+
const property = unit === "packet" ? direction === "inbound" ? "packetsReceived" : "packetsSent" : direction === "inbound" ? "framesDecoded" : "framesEncoded";
|
|
1456
|
+
return rtcNumber(entry, property) ?? 0;
|
|
1457
|
+
}
|
|
1458
|
+
function trafficStalled(progress, sample) {
|
|
1459
|
+
return sample.existed && !progress.stalled && sample.now - progress.lastProgress >= 1e3 && (sample.unit === "packet" || progress.count > 0);
|
|
1460
|
+
}
|
|
976
1461
|
|
|
977
1462
|
// src/media/transport.ts
|
|
1463
|
+
var SIGNALING_TIMEOUT_MS = 8e3;
|
|
1464
|
+
var CREDENTIAL_TIMEOUT_MS = 7e3;
|
|
978
1465
|
function createCloudflareSFUHTTPTransport(options) {
|
|
979
1466
|
const fetch = options.fetch ?? globalThis.fetch;
|
|
980
1467
|
const credential = requireCredential(options);
|
|
981
1468
|
const base = options.apiBaseURL.replace(/\/$/, "");
|
|
982
1469
|
const mediaPath = `${base}/v1/tenants/${encodeURIComponent(options.tenantId)}/spaces/${encodeURIComponent(options.spaceId)}/episodes/${encodeURIComponent(options.episodeId)}/participants/${encodeURIComponent(options.participantId)}/media/sfu`;
|
|
1470
|
+
const timeoutMs = options.requestTimeoutMs ?? SIGNALING_TIMEOUT_MS;
|
|
1471
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new CloudflareSFUError("The signaling deadline is invalid", "signaling_failed");
|
|
983
1472
|
const request = async (path, init) => {
|
|
984
|
-
const token = await credential();
|
|
1473
|
+
const token = await withDeadline(CREDENTIAL_TIMEOUT_MS, (signal) => credential(signal));
|
|
985
1474
|
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
|
|
1475
|
+
return withDeadline(timeoutMs, async (signal) => {
|
|
1476
|
+
const response = await fetch(`${mediaPath}/${path}`, {
|
|
1477
|
+
...init,
|
|
1478
|
+
signal,
|
|
1479
|
+
headers: { ...options.telemetry ? journeyHeaders(options.telemetry) : {}, Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init?.headers }
|
|
994
1480
|
});
|
|
995
|
-
|
|
996
|
-
|
|
1481
|
+
if (!response.ok) {
|
|
1482
|
+
throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
|
|
1483
|
+
status: response.status,
|
|
1484
|
+
retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
return await response.json();
|
|
1488
|
+
});
|
|
997
1489
|
};
|
|
998
1490
|
return {
|
|
999
1491
|
addTracks: async (input) => {
|
|
1000
|
-
const
|
|
1492
|
+
const path = input.allowPartialRemoteTracks ? "tracks?allow_partial_remote_tracks=true" : "tracks";
|
|
1493
|
+
const response = await request(path, {
|
|
1001
1494
|
method: "POST",
|
|
1002
1495
|
body: JSON.stringify({ connection_id: input.connectionId, session_description: input.sessionDescription, tracks: input.tracks })
|
|
1003
1496
|
});
|
|
@@ -1035,6 +1528,25 @@ function createCloudflareSFUHTTPTransport(options) {
|
|
|
1035
1528
|
}
|
|
1036
1529
|
};
|
|
1037
1530
|
}
|
|
1531
|
+
async function withDeadline(timeoutMs, operation) {
|
|
1532
|
+
const controller = new AbortController();
|
|
1533
|
+
const timeout = new CloudflareSFUError("Cloudflare SFU signaling timed out. Try again.", "signaling_timeout");
|
|
1534
|
+
let timer;
|
|
1535
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
1536
|
+
timer = globalThis.setTimeout(() => {
|
|
1537
|
+
controller.abort();
|
|
1538
|
+
reject(timeout);
|
|
1539
|
+
}, timeoutMs);
|
|
1540
|
+
});
|
|
1541
|
+
try {
|
|
1542
|
+
return await Promise.race([Promise.resolve().then(() => operation(controller.signal)), deadline]);
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
if (controller.signal.aborted) throw timeout;
|
|
1545
|
+
throw error;
|
|
1546
|
+
} finally {
|
|
1547
|
+
if (timer !== void 0) globalThis.clearTimeout(timer);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1038
1550
|
function requireCredential(options) {
|
|
1039
1551
|
if (options.credential) return options.credential;
|
|
1040
1552
|
if (options.bearerToken !== void 0) return () => options.bearerToken ?? "";
|
|
@@ -1451,6 +1963,30 @@ function toParticipant(participant) {
|
|
|
1451
1963
|
};
|
|
1452
1964
|
}
|
|
1453
1965
|
|
|
1966
|
+
// src/media/capture-error.ts
|
|
1967
|
+
function describeMediaCaptureError(cause) {
|
|
1968
|
+
const name = typeof cause === "object" && cause !== null && "name" in cause ? cause.name : void 0;
|
|
1969
|
+
switch (name) {
|
|
1970
|
+
case "NotAllowedError":
|
|
1971
|
+
case "PermissionDeniedError":
|
|
1972
|
+
return { code: "permission_denied", recoverable: true, message: "Camera or microphone permission was denied. Allow access in your device settings and try again." };
|
|
1973
|
+
case "NotFoundError":
|
|
1974
|
+
case "DevicesNotFoundError":
|
|
1975
|
+
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." };
|
|
1976
|
+
case "NotReadableError":
|
|
1977
|
+
case "TrackStartError":
|
|
1978
|
+
return { code: "device_busy", recoverable: true, message: "The camera or microphone could not start. Close other apps using it and try again." };
|
|
1979
|
+
case "OverconstrainedError":
|
|
1980
|
+
case "ConstraintNotSatisfiedError":
|
|
1981
|
+
return { code: "device_constraint_invalid", recoverable: true, message: "The selected camera or microphone is unavailable. Select another input device and try again." };
|
|
1982
|
+
case "SecurityError":
|
|
1983
|
+
case "NotSupportedError":
|
|
1984
|
+
return { code: "unsupported_environment", recoverable: false, message: "Media capture is unavailable in this environment. Use a supported browser over HTTPS or the native app." };
|
|
1985
|
+
default:
|
|
1986
|
+
return { code: "media_capture_failed", recoverable: true, message: "Media capture failed. Try again or enter with devices off." };
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1454
1990
|
export {
|
|
1455
1991
|
CloudflareSFUError,
|
|
1456
1992
|
parseCloudflareSFUPublicationID,
|
|
@@ -1463,5 +1999,6 @@ export {
|
|
|
1463
1999
|
CloudflareSFUClient,
|
|
1464
2000
|
createCloudflareSFUHTTPTransport,
|
|
1465
2001
|
CloudflareRTKError,
|
|
1466
|
-
CloudflareRTKClient
|
|
2002
|
+
CloudflareRTKClient,
|
|
2003
|
+
describeMediaCaptureError
|
|
1467
2004
|
};
|