@q9labsai/chalk-client 5.2.4 → 5.2.5
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/dist/access/grant.d.ts +2 -1
- package/dist/access/manager.d.ts +1 -0
- package/dist/{chunk-LDPC2JZB.js → chunk-BUNSEDXL.js} +664 -175
- package/dist/{chunk-3ASKGMD3.js → chunk-FD25AMLG.js} +322 -211
- package/dist/{chunk-SYUGRHZ5.js → chunk-LKNT6XFD.js} +1 -1
- package/dist/{chunk-AZONR3Q3.js → chunk-UMAE5GCV.js} +101 -7
- package/dist/client.d.ts +73 -2
- package/dist/connection/dependencies.d.ts +4 -1
- package/dist/connection/diagnostics.d.ts +4 -0
- package/dist/connection/episode-status.d.ts +7 -0
- package/dist/connection/index.d.ts +2 -0
- package/dist/connection/lifecycle.d.ts +2 -0
- package/dist/effect.js +19 -3
- package/dist/generated/http-api.d.ts +72 -2
- package/dist/generated/schemas.d.ts +125 -4
- package/dist/index.js +4 -4
- package/dist/media/client.d.ts +1 -1
- package/dist/media/failure-diagnostics.d.ts +10 -0
- package/dist/media/index.js +1 -1
- package/dist/media/plane.d.ts +1 -0
- package/dist/media/types.d.ts +2 -1
- package/dist/public-invites/index.js +2 -2
- package/dist/server/access-refresh.d.cts +1 -1
- package/dist/server/access-refresh.d.ts +1 -1
- package/dist/server/index.cjs +15 -5
- package/dist/server/index.js +15 -5
- package/dist/server/types.d.cts +2 -1
- package/dist/server/types.d.ts +2 -1
- package/dist/space-client/media-projection-monitor.d.ts +26 -0
- package/dist/space-client/types.d.ts +4 -0
- package/dist/transport-diagnostics.d.ts +1 -1
- package/package.json +1 -1
|
@@ -176,6 +176,61 @@ function matchRemotePullTracks(publications, tracks) {
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// src/media/failure-diagnostics.ts
|
|
180
|
+
var steps = /* @__PURE__ */ new WeakMap();
|
|
181
|
+
var fetchFailures = /* @__PURE__ */ new WeakSet();
|
|
182
|
+
var domErrorKinds = /* @__PURE__ */ new Map([
|
|
183
|
+
["NotAllowedError", "NotAllowedError"],
|
|
184
|
+
["NotReadableError", "NotReadableError"],
|
|
185
|
+
["NotFoundError", "NotFoundError"],
|
|
186
|
+
["OverconstrainedError", "OverconstrainedError"],
|
|
187
|
+
["AbortError", "AbortError"],
|
|
188
|
+
["SecurityError", "SecurityError"],
|
|
189
|
+
["InvalidStateError", "InvalidStateError"],
|
|
190
|
+
["OperationError", "OperationError"],
|
|
191
|
+
["NotSupportedError", "NotSupportedError"],
|
|
192
|
+
["TimeoutError", "timeout"],
|
|
193
|
+
["NetworkError", "network"]
|
|
194
|
+
]);
|
|
195
|
+
function grantedMediaFromStream(stream) {
|
|
196
|
+
const liveTracks = stream.getTracks().filter((track) => track.readyState === "live");
|
|
197
|
+
return {
|
|
198
|
+
granted_audio: liveTracks.some((track) => track.kind === "audio") ? "granted" : "not_granted",
|
|
199
|
+
granted_video: liveTracks.some((track) => track.kind === "video") ? "granted" : "not_granted"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function markMediaFetchFailure(error) {
|
|
203
|
+
if (typeof error === "object" && error !== null) fetchFailures.add(error);
|
|
204
|
+
}
|
|
205
|
+
function markMediaFailure(error, step) {
|
|
206
|
+
if (typeof error === "object" && error !== null && !steps.has(error)) steps.set(error, step);
|
|
207
|
+
}
|
|
208
|
+
function unwrapMediaFailure(error, step) {
|
|
209
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
210
|
+
if (typeof error !== "object" || error === null) break;
|
|
211
|
+
step = steps.get(error) ?? step;
|
|
212
|
+
if (fetchFailures.has(error)) break;
|
|
213
|
+
if (!("cause" in error) || error.cause === void 0) break;
|
|
214
|
+
error = error.cause;
|
|
215
|
+
}
|
|
216
|
+
return { error, step };
|
|
217
|
+
}
|
|
218
|
+
function classifyMediaFailure(cause, fallbackStep) {
|
|
219
|
+
const { error, step } = unwrapMediaFailure(cause, fallbackStep);
|
|
220
|
+
if (error instanceof CloudflareSFUError) {
|
|
221
|
+
const status = error.options.status;
|
|
222
|
+
return {
|
|
223
|
+
media_error_kind: error.code === "signaling_timeout" || error.code === "negotiation_timeout" ? "timeout" : "CloudflareSFUError",
|
|
224
|
+
media_error_step: step,
|
|
225
|
+
...status !== void 0 && Number.isInteger(status) && status >= 400 && status <= 599 ? { http_status: status } : {}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (typeof DOMException !== "undefined" && error instanceof DOMException) {
|
|
229
|
+
return { media_error_kind: domErrorKinds.get(error.name) ?? "unknown", media_error_step: step };
|
|
230
|
+
}
|
|
231
|
+
return { media_error_kind: error instanceof TypeError && fetchFailures.has(error) ? "network" : "unknown", media_error_step: step };
|
|
232
|
+
}
|
|
233
|
+
|
|
179
234
|
// src/media/client.ts
|
|
180
235
|
var EMPTY_LOCAL = Object.freeze([]);
|
|
181
236
|
var EMPTY_REMOTE = Object.freeze([]);
|
|
@@ -194,10 +249,10 @@ var CloudflareSFUClient = class {
|
|
|
194
249
|
#replaceMediaConnection;
|
|
195
250
|
#remoteListeners = /* @__PURE__ */ new Set();
|
|
196
251
|
#snapshotListeners = /* @__PURE__ */ new Set();
|
|
252
|
+
#failureOwners = /* @__PURE__ */ new WeakMap();
|
|
197
253
|
#locallyPausedSources = /* @__PURE__ */ new Set();
|
|
198
254
|
#localTracks = /* @__PURE__ */ new Map();
|
|
199
|
-
#
|
|
200
|
-
#reusableLocalPublicationIds = /* @__PURE__ */ new Map();
|
|
255
|
+
#ownedPublications = /* @__PURE__ */ new Set();
|
|
201
256
|
#remoteTracks = /* @__PURE__ */ new Map();
|
|
202
257
|
#bootstrap;
|
|
203
258
|
#connection;
|
|
@@ -220,6 +275,7 @@ var CloudflareSFUClient = class {
|
|
|
220
275
|
#pollFailedPull = false;
|
|
221
276
|
#pollTimer;
|
|
222
277
|
#sdpTail = Promise.resolve();
|
|
278
|
+
#retirement = Promise.resolve();
|
|
223
279
|
#snapshot;
|
|
224
280
|
#started = false;
|
|
225
281
|
#stopped = false;
|
|
@@ -273,20 +329,17 @@ var CloudflareSFUClient = class {
|
|
|
273
329
|
this.#requireActive();
|
|
274
330
|
validateTrackSource(source, track);
|
|
275
331
|
if (this.#localTracks.has(source)) throw new CloudflareSFUError(`A ${source} track is already prepared`, "media_failed");
|
|
332
|
+
const reusable = [...this.#ownedPublications].find((item) => item.state.source === source && item.connection === this.#connection && item.lifecycle.kind === "confirmed");
|
|
276
333
|
const state = {
|
|
277
334
|
source,
|
|
278
335
|
track,
|
|
279
|
-
transceiver:
|
|
280
|
-
providerPublicationId:
|
|
281
|
-
pendingOperationId: null,
|
|
282
|
-
pendingTrackName: null,
|
|
336
|
+
transceiver: reusable?.transceiver ?? null,
|
|
337
|
+
providerPublicationId: reusable?.lifecycle.kind === "confirmed" ? reusable.lifecycle.handle.publicationId : null,
|
|
283
338
|
desiredEnabled: false,
|
|
284
339
|
enabled: false,
|
|
285
340
|
endedListener: null,
|
|
286
341
|
clearing: null
|
|
287
342
|
};
|
|
288
|
-
this.#reusableLocalTransceivers.delete(source);
|
|
289
|
-
this.#reusableLocalPublicationIds.delete(source);
|
|
290
343
|
if (source === "screen") {
|
|
291
344
|
state.endedListener = () => {
|
|
292
345
|
if (this.#localTracks.get("screen") === state) this.#invokeListener(() => this.#onScreenEnded?.());
|
|
@@ -310,9 +363,10 @@ var CloudflareSFUClient = class {
|
|
|
310
363
|
}
|
|
311
364
|
async #clearPreparedTrack(state, stopTrack = true) {
|
|
312
365
|
const source = state.source;
|
|
313
|
-
if (source === "screen_audio") {
|
|
366
|
+
if (source === "screen" || source === "screen_audio" || state.track.readyState === "ended") {
|
|
314
367
|
try {
|
|
315
|
-
|
|
368
|
+
this.#pauseLocalSource(source);
|
|
369
|
+
await this.#retireOwnedPublications();
|
|
316
370
|
} finally {
|
|
317
371
|
this.#removeOwnedLocalTrack(state, stopTrack);
|
|
318
372
|
if (this.#localTracks.get(source) === state) this.#localTracks.delete(source);
|
|
@@ -321,10 +375,8 @@ var CloudflareSFUClient = class {
|
|
|
321
375
|
}
|
|
322
376
|
return;
|
|
323
377
|
}
|
|
324
|
-
if (state.enabled) await this.#
|
|
378
|
+
if (state.enabled) await this.#disablePreparedTrack(state);
|
|
325
379
|
if (this.#localTracks.get(source) !== state) return;
|
|
326
|
-
if (state.transceiver) this.#reusableLocalTransceivers.set(source, state.transceiver);
|
|
327
|
-
if (state.providerPublicationId) this.#reusableLocalPublicationIds.set(source, state.providerPublicationId);
|
|
328
380
|
this.#removeOwnedLocalTrack(state, stopTrack);
|
|
329
381
|
this.#localTracks.delete(source);
|
|
330
382
|
this.#publishSnapshot();
|
|
@@ -349,7 +401,7 @@ var CloudflareSFUClient = class {
|
|
|
349
401
|
this.#pollFailedPull = false;
|
|
350
402
|
try {
|
|
351
403
|
const transport = this.#requireTransport();
|
|
352
|
-
const authoritative = await (() => transport.listPublications())
|
|
404
|
+
const authoritative = await this.#signal(() => transport.listPublications());
|
|
353
405
|
this.#requireGeneration(generation);
|
|
354
406
|
await this.#reconcileRemotePublications(authoritative, generation);
|
|
355
407
|
this.#requireGeneration(generation);
|
|
@@ -370,46 +422,54 @@ var CloudflareSFUClient = class {
|
|
|
370
422
|
this.#schedulePoll(0);
|
|
371
423
|
}
|
|
372
424
|
async setLocalPublicationTarget(target) {
|
|
373
|
-
|
|
425
|
+
if (!target.enabled) return this.#pausePublicationTarget(target);
|
|
426
|
+
const result = await this.#enablePublicationTarget(target);
|
|
374
427
|
if (target.source !== "screen" || !this.#localTracks.has("screen_audio") || result.outcome !== "confirmed" && result.outcome !== "satisfied") return result;
|
|
375
|
-
return this.#
|
|
428
|
+
return this.#enablePublicationTarget({ ...target, source: "screen_audio" });
|
|
429
|
+
}
|
|
430
|
+
async #pausePublicationTarget(target) {
|
|
431
|
+
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
432
|
+
if (resolved.kind === "result" && resolved.result.outcome !== "satisfied") return resolved.result;
|
|
433
|
+
const outcome = resolved.kind === "state" && resolved.value.enabled ? "confirmed" : "satisfied";
|
|
434
|
+
this.#pauseLocalSource(target.source);
|
|
435
|
+
try {
|
|
436
|
+
await this.#retireOwnedPublications();
|
|
437
|
+
return { outcome, errorCode: null };
|
|
438
|
+
} catch (error) {
|
|
439
|
+
return mediaTargetFailure(error);
|
|
440
|
+
}
|
|
376
441
|
}
|
|
377
|
-
async #
|
|
442
|
+
async #enablePublicationTarget(target) {
|
|
443
|
+
const generation = this.#generation;
|
|
378
444
|
const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
|
|
379
445
|
if (resolved.kind === "result") return resolved.result;
|
|
380
446
|
const state = resolved.value;
|
|
381
|
-
if (this.#
|
|
447
|
+
if (this.#locallyPausedSources.has(target.source)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
382
448
|
try {
|
|
383
|
-
|
|
384
|
-
if (this.#
|
|
385
|
-
if (state.enabled
|
|
386
|
-
await this.#
|
|
449
|
+
await this.#retireUnavailableLocalPublication(state);
|
|
450
|
+
if (this.#locallyPausedSources.has(target.source)) return { outcome: "terminal_failure", errorCode: "local_source_paused" };
|
|
451
|
+
if (state.enabled) return { outcome: "satisfied", errorCode: null };
|
|
452
|
+
await this.#enablePreparedTrack(state);
|
|
387
453
|
return { outcome: "confirmed", errorCode: null };
|
|
388
454
|
} catch (error) {
|
|
389
|
-
if (!this.#stopped) this.#reportError(error);
|
|
455
|
+
if (generation === this.#generation && !this.#stopped) this.#reportError(error);
|
|
390
456
|
return mediaTargetFailure(error);
|
|
391
457
|
}
|
|
392
458
|
}
|
|
393
|
-
#pausedTarget(target) {
|
|
394
|
-
return target.enabled && this.#locallyPausedSources.has(target.source);
|
|
395
|
-
}
|
|
396
459
|
setLocalSourceIntent(source, enabled) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this.#locallyPausedSources.delete(
|
|
400
|
-
|
|
460
|
+
const sources = localSourceGroup(source);
|
|
461
|
+
for (const item of sources) {
|
|
462
|
+
if (enabled) this.#locallyPausedSources.delete(item);
|
|
463
|
+
else this.#locallyPausedSources.add(item);
|
|
401
464
|
}
|
|
402
|
-
this.#
|
|
403
|
-
|
|
404
|
-
if (!state) return;
|
|
405
|
-
state.desiredEnabled = false;
|
|
406
|
-
state.track.enabled = false;
|
|
465
|
+
if (!enabled) this.#pauseLocalSource(source);
|
|
466
|
+
return enabled ? Promise.resolve() : this.#retireOwnedPublications();
|
|
407
467
|
}
|
|
408
468
|
async closeForcedLocalPublication(source) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (!this.#replaceMediaConnection) return;
|
|
469
|
+
const sources = localSourceGroup(source);
|
|
470
|
+
const ownsPublication = [...this.#ownedPublications].some((item) => sources.includes(item.state.source));
|
|
471
|
+
await this.setLocalSourceIntent(source, false);
|
|
472
|
+
if (!ownsPublication || !this.#replaceMediaConnection) return;
|
|
413
473
|
const generation = this.#generation;
|
|
414
474
|
await this.#serializeSDP(async () => {
|
|
415
475
|
const existing = new Set([...this.#localTracks.values()].filter((state) => state.source !== source && state.providerPublicationId !== null));
|
|
@@ -423,54 +483,82 @@ var CloudflareSFUClient = class {
|
|
|
423
483
|
this.#requireGeneration(generation);
|
|
424
484
|
this.#setPhase("live", null);
|
|
425
485
|
});
|
|
486
|
+
await this.#retirement;
|
|
426
487
|
}
|
|
427
488
|
async #retireUnavailableLocalPublication(state) {
|
|
428
489
|
const publicationId = state.providerPublicationId;
|
|
429
490
|
if (!publicationId) return;
|
|
430
|
-
const authoritative = await (() => this.#requireTransport().listPublications())
|
|
491
|
+
const authoritative = await this.#signal(() => this.#requireTransport().listPublications());
|
|
431
492
|
if (authoritative.publications.some((publication) => publication.publicationId === publicationId)) return;
|
|
432
|
-
if (state.providerPublicationId === publicationId)
|
|
493
|
+
if (state.providerPublicationId === publicationId) {
|
|
494
|
+
this.#pauseLocalSource(state.source);
|
|
495
|
+
await this.#retireOwnedPublications();
|
|
496
|
+
}
|
|
433
497
|
}
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
498
|
+
#pauseLocalSource(source) {
|
|
499
|
+
const sources = localSourceGroup(source);
|
|
500
|
+
for (const publication of this.#ownedPublications) {
|
|
501
|
+
if (!sources.includes(publication.state.source)) continue;
|
|
502
|
+
this.#markPublicationRetiring(publication);
|
|
503
|
+
this.#retiredLocalConnection = this.#connection;
|
|
504
|
+
}
|
|
505
|
+
for (const state of this.#localTracks.values()) {
|
|
506
|
+
if (!sources.includes(state.source)) continue;
|
|
442
507
|
state.desiredEnabled = false;
|
|
443
|
-
state
|
|
444
|
-
state.providerPublicationId = null;
|
|
445
|
-
state.transceiver = null;
|
|
508
|
+
this.#clearLocalPublication(state);
|
|
446
509
|
}
|
|
447
510
|
this.#publishSnapshot();
|
|
448
511
|
this.#emitLocal();
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const response = await this.#requireTransport().closeTracks({
|
|
462
|
-
connectionId,
|
|
463
|
-
...providerDescription(requireDescription(offer)),
|
|
464
|
-
tracks: [{ mid, source, publicationId }],
|
|
465
|
-
force: false
|
|
466
|
-
});
|
|
467
|
-
await this.#applyProviderDescription(response, connection);
|
|
468
|
-
} catch (error) {
|
|
469
|
-
await this.#rollbackLocalOffer(connection);
|
|
470
|
-
if (!this.#stopped) this.#reportError(error);
|
|
471
|
-
throw error;
|
|
512
|
+
}
|
|
513
|
+
#retireOwnedPublications() {
|
|
514
|
+
const publications = [...this.#ownedPublications].filter((item) => item.lifecycle.kind === "retiring" && item.lifecycle.handle);
|
|
515
|
+
const retirement = (async () => {
|
|
516
|
+
const errors = [];
|
|
517
|
+
for (const publication of publications) {
|
|
518
|
+
try {
|
|
519
|
+
await this.#serializeSDP(() => this.#closeOwnedPublication(publication), true);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
errors.push(error);
|
|
522
|
+
if (!this.#stopped) this.#reportError(error);
|
|
523
|
+
}
|
|
472
524
|
}
|
|
525
|
+
if (this.#stopped && this.#ownedPublications.size === 0) this.#transport = void 0;
|
|
526
|
+
if (errors.length) throw new AggregateError(errors, "Media retirement failed");
|
|
527
|
+
})();
|
|
528
|
+
void retirement.catch(() => {
|
|
473
529
|
});
|
|
530
|
+
return this.#retirement = retirement;
|
|
531
|
+
}
|
|
532
|
+
#markPublicationRetiring(publication) {
|
|
533
|
+
const phase = publication.lifecycle;
|
|
534
|
+
if (phase.kind === "pending" || phase.kind === "confirmed") publication.lifecycle = { kind: "retiring", handle: phase.kind === "confirmed" ? phase.handle : null, detached: false };
|
|
535
|
+
}
|
|
536
|
+
async #closeOwnedPublication(publication) {
|
|
537
|
+
const phase = publication.lifecycle;
|
|
538
|
+
if (phase.kind !== "retiring" || !phase.handle) return;
|
|
539
|
+
const handle = phase.handle;
|
|
540
|
+
const { connection, connectionId, transceiver, state } = publication;
|
|
541
|
+
let closed = connection.connectionState === "closed";
|
|
542
|
+
try {
|
|
543
|
+
if (!phase.detached && !closed) {
|
|
544
|
+
await this.#boundPeerOperation(transceiver.sender.replaceTrack(null));
|
|
545
|
+
closed = connection.connectionState === "closed";
|
|
546
|
+
if (!closed) transceiver.stop();
|
|
547
|
+
}
|
|
548
|
+
phase.detached = true;
|
|
549
|
+
const sessionDescription = closed ? void 0 : await createLocalOffer(connection);
|
|
550
|
+
const response = await this.#signal(() => this.#requireTransport().closeTracks({ connectionId, sessionDescription, tracks: [{ ...handle, source: state.source }], force: closed }), { generation: this.#generation, connectionId });
|
|
551
|
+
publication.lifecycle = { kind: "retired" };
|
|
552
|
+
this.#ownedPublications.delete(publication);
|
|
553
|
+
if (connection.connectionState !== "closed") await this.#applyProviderDescription(response, connection);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
await this.#rollbackLocalOffer(connection);
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
#fenceLocalPublications() {
|
|
560
|
+
for (const publication of this.#ownedPublications) publication.lifecycle = { kind: "retired" };
|
|
561
|
+
this.#ownedPublications.clear();
|
|
474
562
|
}
|
|
475
563
|
observeLocalPublications(listener) {
|
|
476
564
|
this.#localListeners.add(listener);
|
|
@@ -486,6 +574,7 @@ var CloudflareSFUClient = class {
|
|
|
486
574
|
this.#requireActive();
|
|
487
575
|
const options = "connectionId" in input ? { bootstrap: input } : input;
|
|
488
576
|
validateBootstrap(options.bootstrap);
|
|
577
|
+
if (options.bootstrap.connectionId !== this.#bootstrap.connectionId) this.#fenceLocalPublications();
|
|
489
578
|
const generation = ++this.#generation;
|
|
490
579
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
491
580
|
this.#polling = false;
|
|
@@ -493,12 +582,12 @@ var CloudflareSFUClient = class {
|
|
|
493
582
|
this.#clearPoll();
|
|
494
583
|
this.#remotePullRetryAfter.clear();
|
|
495
584
|
this.#disposeConnection(false);
|
|
496
|
-
this.#
|
|
497
|
-
this.#
|
|
585
|
+
for (const publication of this.#ownedPublications) this.#markPublicationRetiring(publication);
|
|
586
|
+
void this.#retireOwnedPublications();
|
|
498
587
|
this.#clearRemoteTracks();
|
|
499
588
|
this.#cursor = null;
|
|
500
589
|
this.#negotiatedGeneration = null;
|
|
501
|
-
this.#replacementAttemptedGeneration = null;
|
|
590
|
+
this.#replacementAttemptedGeneration = options.bootstrap.connectionId !== this.#bootstrap.connectionId ? generation : null;
|
|
502
591
|
if (options.bootstrap.connectionId !== this.#bootstrap.connectionId) this.#started = false;
|
|
503
592
|
this.#bootstrap = options.bootstrap;
|
|
504
593
|
if (options.transport) this.#transport = options.transport;
|
|
@@ -524,7 +613,10 @@ var CloudflareSFUClient = class {
|
|
|
524
613
|
}
|
|
525
614
|
}
|
|
526
615
|
stop() {
|
|
527
|
-
if (this.#stopped)
|
|
616
|
+
if (this.#stopped) {
|
|
617
|
+
void this.#retireOwnedPublications();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
528
620
|
this.#stopped = true;
|
|
529
621
|
this.#subscribedPublicationIds.clear();
|
|
530
622
|
this.#generation++;
|
|
@@ -534,12 +626,14 @@ var CloudflareSFUClient = class {
|
|
|
534
626
|
this.#polling = false;
|
|
535
627
|
this.#pollAfterCurrent = false;
|
|
536
628
|
this.#disposeConnection(true);
|
|
537
|
-
this.#
|
|
538
|
-
this.#reusableLocalPublicationIds.clear();
|
|
629
|
+
for (const publication of this.#ownedPublications) this.#markPublicationRetiring(publication);
|
|
539
630
|
this.#clearRemoteTracks();
|
|
540
|
-
for (const state of this.#localTracks.values())
|
|
631
|
+
for (const state of this.#localTracks.values()) {
|
|
632
|
+
state.track.enabled = false;
|
|
633
|
+
this.#removeOwnedLocalTrack(state);
|
|
634
|
+
}
|
|
541
635
|
this.#localTracks.clear();
|
|
542
|
-
this.#
|
|
636
|
+
void this.#retireOwnedPublications();
|
|
543
637
|
this.#started = false;
|
|
544
638
|
this.#cursor = null;
|
|
545
639
|
this.#publishSnapshot("stopped", null);
|
|
@@ -549,43 +643,32 @@ var CloudflareSFUClient = class {
|
|
|
549
643
|
this.#remoteListeners.clear();
|
|
550
644
|
this.#snapshotListeners.clear();
|
|
551
645
|
}
|
|
552
|
-
async #
|
|
553
|
-
|
|
554
|
-
return this.#disablePreparedTrack(state);
|
|
555
|
-
}
|
|
556
|
-
async #enablePreparedTrack(state, operationId) {
|
|
557
|
-
if (state.providerPublicationId && state.transceiver) {
|
|
558
|
-
state.desiredEnabled = true;
|
|
559
|
-
state.track.enabled = true;
|
|
560
|
-
try {
|
|
561
|
-
await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(state.track));
|
|
562
|
-
} catch (error) {
|
|
563
|
-
state.desiredEnabled = false;
|
|
564
|
-
state.track.enabled = false;
|
|
565
|
-
throw error;
|
|
566
|
-
}
|
|
567
|
-
state.enabled = !this.#locallyPausedSources.has(state.source);
|
|
568
|
-
state.desiredEnabled = state.enabled;
|
|
569
|
-
state.track.enabled = state.enabled;
|
|
570
|
-
if (!state.enabled) await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(null));
|
|
571
|
-
this.#publishSnapshot();
|
|
572
|
-
this.#emitLocal();
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
if (operationId && state.pendingOperationId !== operationId) {
|
|
576
|
-
state.pendingOperationId = operationId;
|
|
577
|
-
state.pendingTrackName = `${state.source}-${operationId}`;
|
|
578
|
-
}
|
|
646
|
+
async #enablePreparedTrack(state) {
|
|
647
|
+
const generation = this.#generation;
|
|
579
648
|
state.desiredEnabled = true;
|
|
580
649
|
state.track.enabled = true;
|
|
581
|
-
const generation = this.#generation;
|
|
582
650
|
try {
|
|
651
|
+
if (state.providerPublicationId && state.transceiver) {
|
|
652
|
+
await this.#boundPeerOperation(state.transceiver.sender.replaceTrack(state.track));
|
|
653
|
+
state.enabled = !this.#locallyPausedSources.has(state.source);
|
|
654
|
+
state.desiredEnabled = state.enabled;
|
|
655
|
+
state.track.enabled = state.enabled;
|
|
656
|
+
if (!state.enabled) {
|
|
657
|
+
this.#pauseLocalSource(state.source);
|
|
658
|
+
await this.#retireOwnedPublications();
|
|
659
|
+
}
|
|
660
|
+
this.#publishSnapshot();
|
|
661
|
+
this.#emitLocal();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
583
664
|
await this.#publishPreparedTracks([state], generation);
|
|
584
665
|
if (this.#remoteRecoveryGeneration !== generation) this.#replacementAttemptedGeneration = null;
|
|
585
666
|
} catch (error) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
667
|
+
if (generation === this.#generation) {
|
|
668
|
+
this.#pauseLocalSource(state.source);
|
|
669
|
+
await this.#retireOwnedPublications().catch(() => {
|
|
670
|
+
});
|
|
671
|
+
}
|
|
589
672
|
throw error;
|
|
590
673
|
}
|
|
591
674
|
}
|
|
@@ -595,36 +678,39 @@ var CloudflareSFUClient = class {
|
|
|
595
678
|
state.track.enabled = false;
|
|
596
679
|
const detach = transceiver ? this.#boundPeerOperation(transceiver.sender.replaceTrack(null)) : Promise.resolve();
|
|
597
680
|
state.enabled = false;
|
|
598
|
-
state.pendingOperationId = null;
|
|
599
|
-
state.pendingTrackName = null;
|
|
600
681
|
this.#publishSnapshot();
|
|
601
682
|
this.#emitLocal();
|
|
602
683
|
await detach;
|
|
603
684
|
}
|
|
604
685
|
async #publishPreparedTracks(states, generation) {
|
|
605
686
|
if (states.length === 0) return;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
687
|
+
try {
|
|
688
|
+
await this.#serializeSDP(async () => {
|
|
689
|
+
const pending = await this.#prepareConnectionForPublication(states, generation);
|
|
690
|
+
try {
|
|
691
|
+
await this.#publishPreparedTracksSerialized(pending, generation, this.#connection, this.#bootstrap.connectionId);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (!this.#replaceMediaConnection || !this.#isRetryableConnectionFailure(error) || this.#replacementAttemptedGeneration === generation) throw error;
|
|
694
|
+
await this.#replaceMediaConnectionForRecovery(generation);
|
|
695
|
+
await this.#publishPreparedTracksSerialized(
|
|
696
|
+
[...this.#localTracks.values()].filter((state) => (state.desiredEnabled || states.includes(state)) && state.track.readyState !== "ended"),
|
|
697
|
+
generation,
|
|
698
|
+
this.#connection,
|
|
699
|
+
this.#bootstrap.connectionId
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
} finally {
|
|
704
|
+
await this.#retirement.catch(() => {
|
|
705
|
+
});
|
|
706
|
+
}
|
|
621
707
|
}
|
|
622
708
|
async #prepareConnectionForPublication(states, generation) {
|
|
623
709
|
if (this.#retiredLocalConnection !== this.#connection || !this.#replaceMediaConnection) {
|
|
624
710
|
await this.#replaceDormantConnectionBeforeNegotiation(generation);
|
|
625
711
|
return states;
|
|
626
712
|
}
|
|
627
|
-
const authoritative = await (() => this.#requireTransport().listPublications())
|
|
713
|
+
const authoritative = await this.#signal(() => this.#requireTransport().listPublications());
|
|
628
714
|
this.#pauseUnavailableOtherSources(authoritative.publications, states);
|
|
629
715
|
this.#replacementAttemptedGeneration = null;
|
|
630
716
|
await this.#replaceMediaConnectionForRecovery(generation);
|
|
@@ -634,9 +720,9 @@ var CloudflareSFUClient = class {
|
|
|
634
720
|
for (const state of this.#localTracks.values()) {
|
|
635
721
|
if (states.includes(state) || !state.providerPublicationId) continue;
|
|
636
722
|
if (publications.some((publication) => publication.publicationId === state.providerPublicationId)) continue;
|
|
637
|
-
state.
|
|
638
|
-
state.track.enabled = false;
|
|
723
|
+
this.#pauseLocalSource(state.source);
|
|
639
724
|
}
|
|
725
|
+
void this.#retireOwnedPublications();
|
|
640
726
|
}
|
|
641
727
|
async #publishPreparedTracksSerialized(states, generation, connection, connectionId) {
|
|
642
728
|
this.#requireGeneration(generation);
|
|
@@ -646,28 +732,30 @@ var CloudflareSFUClient = class {
|
|
|
646
732
|
const publications = [];
|
|
647
733
|
try {
|
|
648
734
|
for (const state of pendingStates) publications.push(await this.#prepareLocalPublication(connection, state, generation));
|
|
649
|
-
|
|
735
|
+
await this.#negotiateLocalPublications(connection, connectionId, publications, generation);
|
|
650
736
|
if (!wasLive) await this.#waitForConnection(connection, generation);
|
|
651
737
|
this.#requireGeneration(generation);
|
|
652
738
|
this.#negotiatedGeneration = generation;
|
|
653
|
-
this.#
|
|
739
|
+
this.#settleLocalPublications(publications);
|
|
654
740
|
this.#publishSnapshot();
|
|
655
741
|
this.#emitLocal();
|
|
656
742
|
} catch (error) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
743
|
+
await this.#rollbackLocalOffer(connection);
|
|
744
|
+
for (const publication of publications) this.#markPublicationRetiring(publication);
|
|
745
|
+
this.#settleLocalPublications(publications);
|
|
661
746
|
throw error;
|
|
747
|
+
} finally {
|
|
748
|
+
void this.#retireOwnedPublications();
|
|
662
749
|
}
|
|
663
750
|
}
|
|
664
751
|
async #prepareLocalPublication(connection, state, generation) {
|
|
665
|
-
const reusedTransceiver = state.transceiver !== null;
|
|
666
752
|
const transceiver = state.transceiver ?? this.#addLocalTransceiver(connection, state);
|
|
667
|
-
|
|
753
|
+
const publication = { state, transceiver, trackName: `${state.source}-${globalThis.crypto.randomUUID()}`, connection, connectionId: this.#bootstrap.connectionId, lifecycle: { kind: "pending" } };
|
|
754
|
+
this.#ownedPublications.add(publication);
|
|
755
|
+
if (state.transceiver) await this.#boundPeerOperation(transceiver.sender.replaceTrack(state.track));
|
|
668
756
|
this.#requireGeneration(generation);
|
|
669
757
|
state.transceiver = transceiver;
|
|
670
|
-
return
|
|
758
|
+
return publication;
|
|
671
759
|
}
|
|
672
760
|
#addLocalTransceiver(connection, state) {
|
|
673
761
|
if (state.source !== "camera" || firefoxCameraSimulcastUnsupported()) return connection.addTransceiver(state.track, { direction: "sendonly" });
|
|
@@ -686,10 +774,9 @@ var CloudflareSFUClient = class {
|
|
|
686
774
|
}
|
|
687
775
|
}
|
|
688
776
|
async #negotiateLocalPublications(connection, connectionId, publications, generation) {
|
|
689
|
-
let
|
|
777
|
+
let sessionDescription;
|
|
690
778
|
try {
|
|
691
|
-
|
|
692
|
-
await (() => connection.setLocalDescription(offer))();
|
|
779
|
+
sessionDescription = await createLocalOffer(connection);
|
|
693
780
|
} catch (error) {
|
|
694
781
|
const cameras = publications.filter(({ state, transceiver }) => state.source === "camera" && transceiver.sender.getParameters().encodings.length > 1);
|
|
695
782
|
if (!simulcastUnsupported(error) || cameras.length === 0) throw error;
|
|
@@ -701,8 +788,7 @@ var CloudflareSFUClient = class {
|
|
|
701
788
|
publication.transceiver = connection.addTransceiver(publication.state.track, { direction: "sendonly" });
|
|
702
789
|
publication.state.transceiver = publication.transceiver;
|
|
703
790
|
}
|
|
704
|
-
|
|
705
|
-
await (() => connection.setLocalDescription(offer))();
|
|
791
|
+
sessionDescription = await createLocalOffer(connection);
|
|
706
792
|
}
|
|
707
793
|
const tracks = publications.map(
|
|
708
794
|
({ state, transceiver, trackName }) => ({
|
|
@@ -712,43 +798,44 @@ var CloudflareSFUClient = class {
|
|
|
712
798
|
source: state.source
|
|
713
799
|
})
|
|
714
800
|
);
|
|
715
|
-
const response = await (() => this.#requireTransport().addTracks({ connectionId,
|
|
801
|
+
const response = await this.#signal(() => this.#requireTransport().addTracks({ connectionId, sessionDescription, tracks }), { generation, connectionId });
|
|
802
|
+
this.#recordLocalPublicationHandles(publications, response.tracks);
|
|
716
803
|
this.#requireGeneration(generation);
|
|
717
804
|
await this.#applyProviderDescription(response, connection);
|
|
718
|
-
return response;
|
|
719
805
|
}
|
|
720
|
-
#
|
|
721
|
-
for (const
|
|
806
|
+
#recordLocalPublicationHandles(publications, tracks) {
|
|
807
|
+
for (const publication of publications) {
|
|
808
|
+
const { state, transceiver, lifecycle } = publication;
|
|
809
|
+
if (lifecycle.kind === "retired") continue;
|
|
722
810
|
const authoritative = tracks?.find((track) => track.location === "local" && track.mid === transceiver.mid && track.source === state.source);
|
|
723
|
-
if (!authoritative?.publicationId) throw new CloudflareSFUError("
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
state.desiredEnabled = state.enabled;
|
|
727
|
-
state.track.enabled = state.enabled;
|
|
728
|
-
if (!state.enabled) void transceiver.sender.replaceTrack(null).catch((error) => this.#reportError(error));
|
|
729
|
-
state.providerPublicationId = authoritative.publicationId;
|
|
730
|
-
state.pendingOperationId = null;
|
|
731
|
-
state.pendingTrackName = null;
|
|
811
|
+
if (!authoritative?.publicationId) throw new CloudflareSFUError("Missing publication ID", "invalid_publication");
|
|
812
|
+
const handle = { publicationId: authoritative.publicationId, mid: requireTransceiverMid(transceiver) };
|
|
813
|
+
publication.lifecycle = lifecycle.kind === "retiring" ? { ...lifecycle, handle } : { kind: "confirmed", handle };
|
|
732
814
|
}
|
|
733
815
|
}
|
|
734
|
-
|
|
735
|
-
for (const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
816
|
+
#settleLocalPublications(publications) {
|
|
817
|
+
for (const publication of publications) {
|
|
818
|
+
const { state, transceiver, lifecycle } = publication;
|
|
819
|
+
if (lifecycle.kind === "retired") continue;
|
|
820
|
+
if (lifecycle.kind === "confirmed" && state.transceiver === transceiver && !this.#locallyPausedSources.has(state.source)) {
|
|
821
|
+
state.enabled = true;
|
|
822
|
+
state.desiredEnabled = true;
|
|
823
|
+
state.track.enabled = true;
|
|
824
|
+
state.providerPublicationId = lifecycle.handle.publicationId;
|
|
742
825
|
continue;
|
|
743
826
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
this.#reportError(error);
|
|
827
|
+
this.#markPublicationRetiring(publication);
|
|
828
|
+
if (state.transceiver === transceiver) {
|
|
829
|
+
this.#clearLocalPublication(state);
|
|
748
830
|
}
|
|
749
|
-
if (state.transceiver === transceiver) state.transceiver = null;
|
|
750
831
|
}
|
|
751
832
|
}
|
|
833
|
+
#clearLocalPublication(state) {
|
|
834
|
+
state.transceiver = null;
|
|
835
|
+
state.providerPublicationId = null;
|
|
836
|
+
state.enabled = false;
|
|
837
|
+
state.track.enabled = false;
|
|
838
|
+
}
|
|
752
839
|
async #rollbackLocalOffer(connection) {
|
|
753
840
|
if (connection.signalingState !== "have-local-offer") return;
|
|
754
841
|
try {
|
|
@@ -913,7 +1000,7 @@ var CloudflareSFUClient = class {
|
|
|
913
1000
|
connection.addEventListener("track", onTrack);
|
|
914
1001
|
try {
|
|
915
1002
|
this.#requireGeneration(generation);
|
|
916
|
-
const response = await (() => this.#requireTransport().addTracks({ connectionId, tracks: requested, allowPartialRemoteTracks: true }))
|
|
1003
|
+
const response = await this.#signal(() => this.#requireTransport().addTracks({ connectionId, tracks: requested, allowPartialRemoteTracks: true }), { generation, connectionId });
|
|
917
1004
|
this.#requireGeneration(generation);
|
|
918
1005
|
this.#recordRemotePullErrors(publications, response.trackErrors ?? []);
|
|
919
1006
|
const responseTracks = response.tracks ?? [];
|
|
@@ -958,13 +1045,13 @@ var CloudflareSFUClient = class {
|
|
|
958
1045
|
await this.#applyProviderDescription(response, connection);
|
|
959
1046
|
const answer = await (() => connection.createAnswer())();
|
|
960
1047
|
await (() => connection.setLocalDescription(answer))();
|
|
961
|
-
await (() => this.#requireTransport().renegotiate({ connectionId,
|
|
1048
|
+
await this.#signal(() => this.#requireTransport().renegotiate({ connectionId, sessionDescription: requireDescription(answer) }), { generation, connectionId });
|
|
962
1049
|
this.#requireGeneration(generation);
|
|
963
1050
|
}
|
|
964
1051
|
async #applyProviderDescription(response, connection = this.#connection) {
|
|
965
1052
|
await (() => connection.setRemoteDescription(requireSFUDescription(response.sessionDescription)))();
|
|
966
1053
|
}
|
|
967
|
-
#serializeSDP(operation) {
|
|
1054
|
+
#serializeSDP(operation, retirement = false) {
|
|
968
1055
|
const generation = this.#generation;
|
|
969
1056
|
let expired = false;
|
|
970
1057
|
let timer;
|
|
@@ -977,7 +1064,7 @@ var CloudflareSFUClient = class {
|
|
|
977
1064
|
}, NEGOTIATION_TIMEOUT_MS);
|
|
978
1065
|
});
|
|
979
1066
|
const run = () => {
|
|
980
|
-
if (expired || generation !== this.#generation) throw timeout;
|
|
1067
|
+
if (expired || !retirement && generation !== this.#generation) throw timeout;
|
|
981
1068
|
return operation();
|
|
982
1069
|
};
|
|
983
1070
|
const pending = this.#sdpTail.then(run, run);
|
|
@@ -1115,10 +1202,9 @@ var CloudflareSFUClient = class {
|
|
|
1115
1202
|
if (bootstrap.connectionId === this.#bootstrap.connectionId) {
|
|
1116
1203
|
throw new CloudflareSFUError("Participant access did not replace the Cloudflare SFU connection", "signaling_failed");
|
|
1117
1204
|
}
|
|
1205
|
+
this.#fenceLocalPublications();
|
|
1118
1206
|
const connectionEpoch = ++this.#connectionEpoch;
|
|
1119
1207
|
this.#disposeConnection(false);
|
|
1120
|
-
this.#reusableLocalTransceivers.clear();
|
|
1121
|
-
this.#reusableLocalPublicationIds.clear();
|
|
1122
1208
|
this.#clearRemoteTracks();
|
|
1123
1209
|
this.#cursor = null;
|
|
1124
1210
|
this.#bootstrap = bootstrap;
|
|
@@ -1190,8 +1276,6 @@ var CloudflareSFUClient = class {
|
|
|
1190
1276
|
state.desiredEnabled = false;
|
|
1191
1277
|
state.transceiver = null;
|
|
1192
1278
|
state.providerPublicationId = null;
|
|
1193
|
-
state.pendingOperationId = null;
|
|
1194
|
-
state.pendingTrackName = null;
|
|
1195
1279
|
state.endedListener = null;
|
|
1196
1280
|
}
|
|
1197
1281
|
#clearRemoteTracks() {
|
|
@@ -1271,7 +1355,7 @@ var CloudflareSFUClient = class {
|
|
|
1271
1355
|
}
|
|
1272
1356
|
#setFailure(error, fallback) {
|
|
1273
1357
|
const code = error instanceof CloudflareSFUError ? error.code : fallback;
|
|
1274
|
-
this.#publishSnapshot("failed", { code, recoverable: code !== "invalid_bootstrap" && code !== "invalid_target" && code !== "invalid_publication" });
|
|
1358
|
+
this.#publishSnapshot("failed", { code, recoverable: code !== "invalid_bootstrap" && code !== "invalid_target" && code !== "invalid_publication", diagnostic: classifyMediaFailure(error, "peer_connection") });
|
|
1275
1359
|
this.#reportError(error);
|
|
1276
1360
|
}
|
|
1277
1361
|
#emitLocal() {
|
|
@@ -1290,19 +1374,29 @@ var CloudflareSFUClient = class {
|
|
|
1290
1374
|
}
|
|
1291
1375
|
}
|
|
1292
1376
|
#reportError(error) {
|
|
1377
|
+
const owner = error instanceof Error ? this.#failureOwners.get(error) : void 0;
|
|
1378
|
+
if (owner && (owner.generation !== this.#generation || owner.connectionId !== this.#bootstrap.connectionId)) return;
|
|
1293
1379
|
try {
|
|
1294
|
-
this.#onError?.(error);
|
|
1380
|
+
this.#onError?.(error, owner?.connectionId ?? this.#bootstrap.connectionId);
|
|
1295
1381
|
} catch {
|
|
1296
1382
|
}
|
|
1297
1383
|
}
|
|
1384
|
+
async #signal(operation, owner = { generation: this.#generation, connectionId: this.#bootstrap.connectionId }) {
|
|
1385
|
+
try {
|
|
1386
|
+
return await operation();
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
if (error instanceof Error) this.#failureOwners.set(error, owner);
|
|
1389
|
+
throw error;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1298
1392
|
#requireGeneration(generation) {
|
|
1299
|
-
if (generation !== this.#generation || this.#stopped) throw new CloudflareSFUError("
|
|
1393
|
+
if (generation !== this.#generation || this.#stopped) throw new CloudflareSFUError("Stale media connection", "stale_generation");
|
|
1300
1394
|
}
|
|
1301
1395
|
#requireActive() {
|
|
1302
|
-
if (this.#stopped) throw new CloudflareSFUError("
|
|
1396
|
+
if (this.#stopped) throw new CloudflareSFUError("SFU client stopped", "media_failed");
|
|
1303
1397
|
}
|
|
1304
1398
|
#requireTransport() {
|
|
1305
|
-
if (!this.#transport) throw new CloudflareSFUError("
|
|
1399
|
+
if (!this.#transport) throw new CloudflareSFUError("SFU signaling unavailable", "signaling_failed");
|
|
1306
1400
|
return this.#transport;
|
|
1307
1401
|
}
|
|
1308
1402
|
};
|
|
@@ -1391,8 +1485,13 @@ function publicationEqual(left, right) {
|
|
|
1391
1485
|
function remotePublicationEqual(left, right) {
|
|
1392
1486
|
return right !== void 0 && left.participantId === right.participantId && left.source === right.source && left.publicationId === right.publicationId && left.track === right.track;
|
|
1393
1487
|
}
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1488
|
+
async function createLocalOffer(connection) {
|
|
1489
|
+
const offer = await connection.createOffer();
|
|
1490
|
+
await connection.setLocalDescription(offer);
|
|
1491
|
+
return requireDescription(offer);
|
|
1492
|
+
}
|
|
1493
|
+
function localSourceGroup(source) {
|
|
1494
|
+
return source === "screen" ? [source, "screen_audio"] : [source];
|
|
1396
1495
|
}
|
|
1397
1496
|
function isRateLimited(error) {
|
|
1398
1497
|
return error instanceof CloudflareSFUError && error.options.status === 429;
|
|
@@ -1425,27 +1524,36 @@ function createCloudflareSFUHTTPTransport(options) {
|
|
|
1425
1524
|
const timeoutMs = options.requestTimeoutMs ?? SIGNALING_TIMEOUT_MS;
|
|
1426
1525
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new CloudflareSFUError("The signaling deadline is invalid", "signaling_failed");
|
|
1427
1526
|
const request = async (path, init) => {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
} catch {
|
|
1441
|
-
}
|
|
1442
|
-
throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
|
|
1443
|
-
status: response.status,
|
|
1444
|
-
retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
|
|
1527
|
+
try {
|
|
1528
|
+
const token = await withDeadline(CREDENTIAL_TIMEOUT_MS, (signal) => credential(signal));
|
|
1529
|
+
if (!token.trim()) throw new CloudflareSFUError("The media credential provider returned an empty token", "signaling_failed");
|
|
1530
|
+
return await withDeadline(timeoutMs, async (signal) => {
|
|
1531
|
+
const startedAt = performance.now();
|
|
1532
|
+
const response = await fetch(`${mediaPath}/${path}`, {
|
|
1533
|
+
...init,
|
|
1534
|
+
signal,
|
|
1535
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...options.traceparent ? { traceparent: options.traceparent } : {}, ...init?.headers }
|
|
1536
|
+
}).catch((error) => {
|
|
1537
|
+
markMediaFetchFailure(error);
|
|
1538
|
+
throw error;
|
|
1445
1539
|
});
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1540
|
+
if (!response.ok) {
|
|
1541
|
+
try {
|
|
1542
|
+
options.onDiagnostic?.({ event: "media_http_error", http_status: response.status, duration_ms: performance.now() - startedAt });
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1545
|
+
throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
|
|
1546
|
+
status: response.status,
|
|
1547
|
+
retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
return await response.json();
|
|
1551
|
+
});
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
const step = path === "publications" ? "sfu_publications" : path === "tracks/close" ? "sfu_close_tracks" : path === "renegotiate" ? "sfu_renegotiate" : "sfu_tracks";
|
|
1554
|
+
markMediaFailure(error, step);
|
|
1555
|
+
throw error;
|
|
1556
|
+
}
|
|
1449
1557
|
};
|
|
1450
1558
|
return {
|
|
1451
1559
|
addTracks: async (input) => {
|
|
@@ -1538,7 +1646,6 @@ function describeMediaCaptureError(cause) {
|
|
|
1538
1646
|
}
|
|
1539
1647
|
|
|
1540
1648
|
export {
|
|
1541
|
-
describeMediaCaptureError,
|
|
1542
1649
|
CloudflareSFUError,
|
|
1543
1650
|
parseCloudflareSFUPublicationID,
|
|
1544
1651
|
requireDescription,
|
|
@@ -1547,6 +1654,10 @@ export {
|
|
|
1547
1654
|
comparePublicationCursor,
|
|
1548
1655
|
publicationKey,
|
|
1549
1656
|
waitFor,
|
|
1657
|
+
grantedMediaFromStream,
|
|
1658
|
+
markMediaFailure,
|
|
1659
|
+
classifyMediaFailure,
|
|
1550
1660
|
CloudflareSFUClient,
|
|
1551
|
-
createCloudflareSFUHTTPTransport
|
|
1661
|
+
createCloudflareSFUHTTPTransport,
|
|
1662
|
+
describeMediaCaptureError
|
|
1552
1663
|
};
|