@q9labsai/chalk-client 5.2.4 → 5.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/access/grant.d.ts +2 -1
- package/dist/access/manager.d.ts +1 -0
- package/dist/{chunk-AZONR3Q3.js → chunk-CJVLTSIO.js} +120 -8
- package/dist/{chunk-LDPC2JZB.js → chunk-I7Y4HR2P.js} +711 -194
- package/dist/{chunk-SYUGRHZ5.js → chunk-WFRTUBN6.js} +1 -1
- package/dist/{chunk-3ASKGMD3.js → chunk-YIDKETOX.js} +410 -232
- package/dist/client.d.ts +74 -2
- package/dist/connection/dependencies.d.ts +4 -1
- package/dist/connection/diagnostics.d.ts +4 -0
- package/dist/connection/episode-status.d.ts +7 -0
- package/dist/connection/index.d.ts +2 -0
- package/dist/connection/lifecycle.d.ts +2 -0
- package/dist/connection/production.d.ts +1 -1
- package/dist/effect.js +19 -3
- package/dist/generated/http-api.d.ts +73 -2
- package/dist/generated/schemas.d.ts +126 -4
- package/dist/index.js +4 -4
- package/dist/media/client.d.ts +4 -3
- package/dist/media/failure-diagnostics.d.ts +10 -0
- package/dist/media/index.js +1 -1
- package/dist/media/plane.d.ts +7 -2
- package/dist/media/types.d.ts +4 -2
- package/dist/public-invites/index.js +2 -2
- package/dist/server/access-refresh.d.cts +1 -1
- package/dist/server/access-refresh.d.ts +1 -1
- package/dist/server/index.cjs +15 -5
- package/dist/server/index.js +15 -5
- package/dist/server/types.d.cts +2 -1
- package/dist/server/types.d.ts +2 -1
- package/dist/space-client/media-projection-monitor.d.ts +26 -0
- package/dist/space-client/promise-facade.d.ts +5 -5
- package/dist/space-client/types.d.ts +4 -0
- package/dist/transport-diagnostics.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CloudflareSFUClient,
|
|
3
|
+
CloudflareSFUError,
|
|
4
|
+
classifyMediaFailure,
|
|
3
5
|
createCloudflareSFUHTTPTransport,
|
|
4
|
-
describeMediaCaptureError
|
|
5
|
-
|
|
6
|
+
describeMediaCaptureError,
|
|
7
|
+
grantedMediaFromStream,
|
|
8
|
+
markMediaFailure
|
|
9
|
+
} from "./chunk-YIDKETOX.js";
|
|
6
10
|
import {
|
|
7
11
|
AccessGrantError,
|
|
8
12
|
requireParsedAccessGrant
|
|
9
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-CJVLTSIO.js";
|
|
10
14
|
import {
|
|
11
15
|
ChatAttachmentMimeTypeSchema,
|
|
12
16
|
ChatReactionSetFrameSchema,
|
|
@@ -23,19 +27,43 @@ import {
|
|
|
23
27
|
} from "./chunk-5YPQNMJE.js";
|
|
24
28
|
|
|
25
29
|
// src/access/manager.ts
|
|
26
|
-
import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
|
|
30
|
+
import { Clock, Context, Data, Deferred, Effect, Layer, SynchronizedRef } from "effect";
|
|
27
31
|
var DEFAULT_REFRESH_WINDOW_MS = 6e4;
|
|
28
32
|
var ACCESS_PROVIDER_TIMEOUT_MS = 7e3;
|
|
29
33
|
var ConnectionAccessFailure = class extends Data.TaggedError("ConnectionAccessFailure") {
|
|
30
34
|
};
|
|
35
|
+
var MediaReplacementPending = class extends Data.TaggedError("MediaReplacementPending") {
|
|
36
|
+
};
|
|
31
37
|
var ConnectionAccessService = class extends Context.Service()("@chalk/client/ConnectionAccess") {
|
|
32
38
|
};
|
|
33
39
|
var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WINDOW_MS) => Layer.effect(
|
|
34
40
|
ConnectionAccessService,
|
|
35
41
|
Effect.gen(function* () {
|
|
36
42
|
const state = yield* SynchronizedRef.make({ access: null });
|
|
43
|
+
const holds = /* @__PURE__ */ new Map();
|
|
44
|
+
const replacementGate = () => {
|
|
45
|
+
const binding = SynchronizedRef.getUnsafe(state).access?.media.clientPayload.connectionId;
|
|
46
|
+
return binding ? holds.get(binding)?.done : void 0;
|
|
47
|
+
};
|
|
48
|
+
const holdMediaBinding = (binding) => Effect.gen(function* () {
|
|
49
|
+
const hold = holds.get(binding) ?? { count: 0, done: yield* Deferred.make() };
|
|
50
|
+
hold.count++;
|
|
51
|
+
holds.set(binding, hold);
|
|
52
|
+
return Effect.gen(function* () {
|
|
53
|
+
if (--hold.count !== 0) return;
|
|
54
|
+
holds.delete(binding);
|
|
55
|
+
yield* Deferred.succeed(hold.done, void 0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
37
58
|
const automaticMediaReplacementListeners = /* @__PURE__ */ new Set();
|
|
38
|
-
const fetch2 = (reason, replaceMediaConnection, force) => SynchronizedRef.modifyEffect(state, (current) => fetchAccessGrant(provider, { current, reason, replaceMediaConnection, force, refreshWindowMs })).pipe(
|
|
59
|
+
const fetch2 = (reason, replaceMediaConnection, force) => SynchronizedRef.modifyEffect(state, (current) => fetchAccessGrant(provider, { current, reason, replaceMediaConnection, force, refreshWindowMs, replacementGate })).pipe(
|
|
60
|
+
Effect.catchTag(
|
|
61
|
+
"MediaReplacementPending",
|
|
62
|
+
({ done }) => Deferred.await(done).pipe(
|
|
63
|
+
Effect.flatMap(() => fetch2(reason, replaceMediaConnection, force)),
|
|
64
|
+
Effect.map((access) => ({ access, automaticMediaReplacement: false }))
|
|
65
|
+
)
|
|
66
|
+
),
|
|
39
67
|
Effect.tap((result) => Effect.sync(() => notifyAutomaticMediaReplacement(automaticMediaReplacementListeners, result))),
|
|
40
68
|
Effect.map((result) => result.access)
|
|
41
69
|
);
|
|
@@ -61,7 +89,12 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
|
|
|
61
89
|
}),
|
|
62
90
|
requiresRefresh: millisecondsUntilRefresh.pipe(Effect.map((milliseconds2) => milliseconds2 === null || milliseconds2 === 0)),
|
|
63
91
|
millisecondsUntilRefresh,
|
|
64
|
-
|
|
92
|
+
holdMediaBinding,
|
|
93
|
+
clear: Effect.gen(function* () {
|
|
94
|
+
yield* SynchronizedRef.set(state, { access: null });
|
|
95
|
+
for (const hold of holds.values()) yield* Deferred.fail(hold.done, new ConnectionAccessFailure({ code: "access.unavailable", cause: "Connection closed during media rejection classification" }));
|
|
96
|
+
holds.clear();
|
|
97
|
+
})
|
|
65
98
|
};
|
|
66
99
|
})
|
|
67
100
|
);
|
|
@@ -70,17 +103,21 @@ function fetchAccessGrant(provider, input) {
|
|
|
70
103
|
return Effect.gen(function* () {
|
|
71
104
|
const now = yield* Clock.currentTimeMillis;
|
|
72
105
|
const currentAccess = input.current.access;
|
|
73
|
-
if (
|
|
106
|
+
if (canReuseAccess(input, now) && currentAccess) {
|
|
74
107
|
return [{ access: currentAccess, automaticMediaReplacement: false }, input.current];
|
|
75
108
|
}
|
|
76
|
-
const
|
|
77
|
-
const shouldReplaceMediaConnection = input
|
|
109
|
+
const gate = input.replacementGate();
|
|
110
|
+
const shouldReplaceMediaConnection = mediaReplacementRequested(input, now, Boolean(gate));
|
|
111
|
+
if (shouldReplaceMediaConnection && gate) return yield* Effect.fail(new MediaReplacementPending({ done: gate }));
|
|
78
112
|
const next = yield* provider(connectionAccessRequest(input, shouldReplaceMediaConnection)).pipe(
|
|
79
113
|
Effect.timeout(ACCESS_PROVIDER_TIMEOUT_MS),
|
|
80
114
|
Effect.mapError((cause) => cause instanceof ConnectionAccessFailure ? cause : new ConnectionAccessFailure({ code: "access.unavailable", cause }))
|
|
81
115
|
);
|
|
82
116
|
yield* Effect.try({
|
|
83
|
-
try: () =>
|
|
117
|
+
try: () => {
|
|
118
|
+
validateFetchedAccess(currentAccess, next, now, shouldReplaceMediaConnection);
|
|
119
|
+
validateHeldBinding(input, next);
|
|
120
|
+
},
|
|
84
121
|
catch: (cause) => new ConnectionAccessFailure({ code: "access.invalid", cause })
|
|
85
122
|
});
|
|
86
123
|
return [{ access: next, automaticMediaReplacement: currentAccess !== null && !input.replaceMediaConnection && !hasSameMediaConnection(currentAccess, next) }, { access: next }];
|
|
@@ -92,7 +129,8 @@ function connectionAccessRequest(input, replaceMediaConnection) {
|
|
|
92
129
|
replaceMediaConnection,
|
|
93
130
|
...input.current.access ? {
|
|
94
131
|
currentMediaToken: input.current.access.media.token,
|
|
95
|
-
expectedParticipantGeneration: input.current.access.subject.participantGeneration
|
|
132
|
+
expectedParticipantGeneration: input.current.access.subject.participantGeneration,
|
|
133
|
+
...replaceMediaConnection && input.reason !== "access_retry" ? { expectedMediaConnectionId: input.current.access.media.clientPayload.connectionId } : {}
|
|
96
134
|
} : {}
|
|
97
135
|
});
|
|
98
136
|
}
|
|
@@ -138,6 +176,17 @@ function expiresAt(value) {
|
|
|
138
176
|
if (!Number.isFinite(timestamp)) throw new TypeError("Access grant expiration is invalid");
|
|
139
177
|
return timestamp;
|
|
140
178
|
}
|
|
179
|
+
function canReuseAccess(input, now) {
|
|
180
|
+
return !input.force && input.current.access !== null && millisecondsUntil(input.current.access, now, input.refreshWindowMs) > 0;
|
|
181
|
+
}
|
|
182
|
+
function mediaReplacementRequested(input, now, held) {
|
|
183
|
+
const current = input.current.access;
|
|
184
|
+
return input.replaceMediaConnection || current !== null && expiresAt(current.media.expiresAt) <= now && !held;
|
|
185
|
+
}
|
|
186
|
+
function validateHeldBinding(input, next) {
|
|
187
|
+
const previous = input.current.access;
|
|
188
|
+
if (input.replacementGate() && previous && !hasSameMediaConnection(previous, next)) throw new TypeError("Cannot replace a rejected media binding before classification");
|
|
189
|
+
}
|
|
141
190
|
|
|
142
191
|
// src/connection/dependencies.ts
|
|
143
192
|
import { Context as Context2, Layer as Layer2 } from "effect";
|
|
@@ -2288,6 +2337,28 @@ function createChalkWhiteboardV1Client(options) {
|
|
|
2288
2337
|
});
|
|
2289
2338
|
}
|
|
2290
2339
|
|
|
2340
|
+
// src/connection/episode-status.ts
|
|
2341
|
+
var EpisodeStatusError = class extends Error {
|
|
2342
|
+
constructor(status, retryAfterMs = 0) {
|
|
2343
|
+
super(`Episode status unavailable: HTTP ${status}`);
|
|
2344
|
+
this.status = status;
|
|
2345
|
+
this.retryAfterMs = retryAfterMs;
|
|
2346
|
+
}
|
|
2347
|
+
status;
|
|
2348
|
+
retryAfterMs;
|
|
2349
|
+
};
|
|
2350
|
+
function episodeStatusRetryDelay(attempt, error2) {
|
|
2351
|
+
const retryAfterMs = error2 instanceof EpisodeStatusError ? error2.retryAfterMs : 0;
|
|
2352
|
+
const ceiling = Math.min(1e3 * 2 ** Math.min(attempt, 5), 1e4);
|
|
2353
|
+
return Math.max(retryAfterMs, ceiling * (0.5 + Math.random() * 0.5));
|
|
2354
|
+
}
|
|
2355
|
+
function retryAfterMilliseconds(value, now) {
|
|
2356
|
+
if (!value) return 0;
|
|
2357
|
+
const seconds = Number(value);
|
|
2358
|
+
const delay = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(value) - now;
|
|
2359
|
+
return Number.isFinite(delay) ? Math.max(0, delay) : 0;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2291
2362
|
// src/connection/report.ts
|
|
2292
2363
|
var MAX_EVENTS = 64;
|
|
2293
2364
|
var MAX_BYTES = 32768;
|
|
@@ -2296,6 +2367,16 @@ var COOLDOWN_MS = 6e4;
|
|
|
2296
2367
|
var participantCooldowns = /* @__PURE__ */ new Map();
|
|
2297
2368
|
var milliseconds = (value) => Math.max(0, Math.min(MAX_MS, Math.round(Number.isFinite(value) ? value : 0)));
|
|
2298
2369
|
var optionalMilliseconds = (value) => value == null ? void 0 : milliseconds(value);
|
|
2370
|
+
function mediaReportFields(media) {
|
|
2371
|
+
if (!media) return {};
|
|
2372
|
+
return {
|
|
2373
|
+
media_error_kind: media.media_error_kind,
|
|
2374
|
+
media_error_step: media.media_error_step,
|
|
2375
|
+
granted_audio: media.granted_audio,
|
|
2376
|
+
granted_video: media.granted_video,
|
|
2377
|
+
http_status: media.http_status
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2299
2380
|
function reserveParticipantReport(access, now) {
|
|
2300
2381
|
const key = `${access.subject.tenantId}:${access.subject.participantId}`;
|
|
2301
2382
|
for (const [participant, until] of participantCooldowns) if (until <= now) participantCooldowns.delete(participant);
|
|
@@ -2328,7 +2409,8 @@ var ConnectionReporter = class {
|
|
|
2328
2409
|
attempt: optionalMilliseconds(diagnostic.attempt),
|
|
2329
2410
|
duration_ms: optionalMilliseconds(diagnostic.sync?.durationMs ?? diagnostic.durationMs),
|
|
2330
2411
|
close_code: diagnostic.sync?.closeCode ?? void 0,
|
|
2331
|
-
heartbeat_gap_ms: optionalMilliseconds(diagnostic.recovery?.lastHeartbeatReplyAgoMs)
|
|
2412
|
+
heartbeat_gap_ms: optionalMilliseconds(diagnostic.recovery?.lastHeartbeatReplyAgoMs),
|
|
2413
|
+
...mediaReportFields(diagnostic.media)
|
|
2332
2414
|
});
|
|
2333
2415
|
if (diagnostic.event === "recovery_exhausted") this.#report("recovery_exhausted");
|
|
2334
2416
|
else if (diagnostic.event === "connection_failed") this.#report(diagnostic.state === "joining" ? "join_failed" : "connection_lost");
|
|
@@ -2342,8 +2424,13 @@ var ConnectionReporter = class {
|
|
|
2342
2424
|
...event.duration_ms == null ? {} : { duration_ms: milliseconds(event.duration_ms) },
|
|
2343
2425
|
...event.heartbeat_gap_ms == null ? {} : { heartbeat_gap_ms: milliseconds(event.heartbeat_gap_ms) },
|
|
2344
2426
|
...event.close_code == null ? {} : { close_code: event.close_code },
|
|
2345
|
-
...event.http_status == null ? {} : { http_status: event.http_status }
|
|
2427
|
+
...event.http_status == null ? {} : { http_status: event.http_status },
|
|
2428
|
+
source: event.source,
|
|
2429
|
+
publication_state: event.publication_state,
|
|
2430
|
+
projection_state: event.projection_state,
|
|
2431
|
+
outcome: event.outcome
|
|
2346
2432
|
});
|
|
2433
|
+
if (event.event === "media_projection_mismatch") this.#report("media_projection_mismatch");
|
|
2347
2434
|
}
|
|
2348
2435
|
dispose() {
|
|
2349
2436
|
this.#disposed = true;
|
|
@@ -2427,7 +2514,12 @@ function createBrowserMediaDevices(mediaDevices = globalThis.navigator?.mediaDev
|
|
|
2427
2514
|
return {
|
|
2428
2515
|
async getUserMedia(constraints) {
|
|
2429
2516
|
if (!mediaDevices?.getUserMedia || globalThis.isSecureContext === false) throw new ConnectionError({ code: "unsupported_environment", action: null, recoverable: false, message: "Browser media capture is unavailable. Use a supported browser over HTTPS." });
|
|
2430
|
-
|
|
2517
|
+
try {
|
|
2518
|
+
return await mediaDevices.getUserMedia(constraints);
|
|
2519
|
+
} catch (error2) {
|
|
2520
|
+
markMediaFailure(error2, "local_capture");
|
|
2521
|
+
throw error2;
|
|
2522
|
+
}
|
|
2431
2523
|
},
|
|
2432
2524
|
async getDisplayMedia(constraints) {
|
|
2433
2525
|
if (!mediaDevices?.getDisplayMedia) throw new DOMException("Browser display capture is unavailable", "NotSupportedError");
|
|
@@ -2506,6 +2598,20 @@ function createDefaultConnectionDependencies(options) {
|
|
|
2506
2598
|
});
|
|
2507
2599
|
},
|
|
2508
2600
|
mediaDevices: createBrowserMediaDevices(),
|
|
2601
|
+
getEpisodeStatus: async (access, signal) => {
|
|
2602
|
+
const { tenantId, spaceId, episodeId, participantId } = access.subject;
|
|
2603
|
+
const path = [tenantId, spaceId, episodeId, participantId].map(encodeURIComponent);
|
|
2604
|
+
const response = await fetch(`${options.apiBaseURL.replace(/\/$/, "")}/v1/tenants/${path[0]}/spaces/${path[1]}/episodes/${path[2]}/participants/${path[3]}/episode-status`, {
|
|
2605
|
+
headers: { Authorization: `Bearer ${access.media.token}` },
|
|
2606
|
+
signal,
|
|
2607
|
+
cache: "no-store",
|
|
2608
|
+
credentials: "omit"
|
|
2609
|
+
});
|
|
2610
|
+
if (!response.ok) throw new EpisodeStatusError(response.status, retryAfterMilliseconds(response.headers.get("Retry-After"), Date.now()));
|
|
2611
|
+
const body = await response.json();
|
|
2612
|
+
if (typeof body === "object" && body !== null && "status" in body && (body.status === "active" || body.status === "ending" || body.status === "ended")) return body.status;
|
|
2613
|
+
throw new TypeError("Invalid Episode status");
|
|
2614
|
+
},
|
|
2509
2615
|
cancelPendingAdmission: async (access, signal) => {
|
|
2510
2616
|
const { tenantId, spaceId, episodeId, participantId } = access.subject;
|
|
2511
2617
|
const path = [tenantId, spaceId, episodeId, participantId].map(encodeURIComponent);
|
|
@@ -2593,6 +2699,7 @@ function createSFUConnectionMediaClient(client) {
|
|
|
2593
2699
|
function bindConnectionMediaClient(client, restart) {
|
|
2594
2700
|
return {
|
|
2595
2701
|
setLocalPublicationTarget: client.setLocalPublicationTarget.bind(client),
|
|
2702
|
+
...client.pauseLocalCapture ? { pauseLocalCapture: client.pauseLocalCapture.bind(client) } : {},
|
|
2596
2703
|
...client.setLocalSourceIntent ? { setLocalSourceIntent: client.setLocalSourceIntent.bind(client) } : {},
|
|
2597
2704
|
...client.closeForcedLocalPublication ? { closeForcedLocalPublication: client.closeForcedLocalPublication.bind(client) } : {},
|
|
2598
2705
|
observeLocalPublications: client.observeLocalPublications.bind(client),
|
|
@@ -2665,7 +2772,7 @@ function scheduleForegroundDeadline(clock, milliseconds2, expired) {
|
|
|
2665
2772
|
}
|
|
2666
2773
|
|
|
2667
2774
|
// src/connection/lifecycle.ts
|
|
2668
|
-
import { Clock as Clock2, Context as Context3, Data as Data2, Deferred, Duration, Effect as Effect2, Exit, Fiber, Layer as Layer3, Queue, Scope, SubscriptionRef } from "effect";
|
|
2775
|
+
import { Clock as Clock2, Context as Context3, Data as Data2, Deferred as Deferred2, Duration, Effect as Effect2, Exit, Fiber, Layer as Layer3, Queue, Scope, SubscriptionRef } from "effect";
|
|
2669
2776
|
|
|
2670
2777
|
// src/connection/diagnostics.ts
|
|
2671
2778
|
var ConnectionDiagnostics = class {
|
|
@@ -2690,11 +2797,17 @@ var ConnectionDiagnostics = class {
|
|
|
2690
2797
|
}
|
|
2691
2798
|
recordRecoveryExhausted(input) {
|
|
2692
2799
|
const { state, epoch, code, attempts, transport } = input;
|
|
2800
|
+
const media = input.media ?? (code === "media_recovery_exhausted" ? {
|
|
2801
|
+
media_error_kind: "timeout",
|
|
2802
|
+
media_error_step: "peer_connection",
|
|
2803
|
+
...input.grantedMedia
|
|
2804
|
+
} : void 0);
|
|
2693
2805
|
this.record({
|
|
2694
2806
|
event: "recovery_exhausted",
|
|
2695
2807
|
state,
|
|
2696
2808
|
epoch,
|
|
2697
2809
|
code,
|
|
2810
|
+
...media ? { media } : {},
|
|
2698
2811
|
recovery: {
|
|
2699
2812
|
reference: `connection.${code}`,
|
|
2700
2813
|
attempts,
|
|
@@ -2726,7 +2839,8 @@ var ConnectionDiagnostics = class {
|
|
|
2726
2839
|
...input.parentSpanId ? { parentSpanId: input.parentSpanId } : {},
|
|
2727
2840
|
outcome: end.outcome,
|
|
2728
2841
|
durationMs: Math.max(0, this.#now() - startedAt),
|
|
2729
|
-
...end.code ? { code: end.code } : {}
|
|
2842
|
+
...end.code ? { code: end.code } : {},
|
|
2843
|
+
...end.media ? { media: end.media } : {}
|
|
2730
2844
|
});
|
|
2731
2845
|
}
|
|
2732
2846
|
};
|
|
@@ -2740,8 +2854,7 @@ var ConnectionDiagnostics = class {
|
|
|
2740
2854
|
var MEDIA_RECOVERY_TIMEOUT_MS = 6e4;
|
|
2741
2855
|
var START_TIMEOUT_MS = 3e4;
|
|
2742
2856
|
var LEAVE_TIMEOUT_MS = 5e3;
|
|
2743
|
-
var RECOVERY_BUDGET_MS =
|
|
2744
|
-
var SYNC_RECOVERY_BUDGET_MS = MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2857
|
+
var RECOVERY_BUDGET_MS = MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2745
2858
|
var MAX_RECOVERY_ATTEMPTS = 3;
|
|
2746
2859
|
var REFRESH_RETRY_MS = 5e3;
|
|
2747
2860
|
var RECONNECT_GRACE_MS = 250;
|
|
@@ -2783,6 +2896,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2783
2896
|
const context = yield* Effect2.context();
|
|
2784
2897
|
const serviceScope = yield* Effect2.scope;
|
|
2785
2898
|
const reporter = platform.createConnectionReporter?.(access.currentUnsafe);
|
|
2899
|
+
let grantedMedia = { granted_audio: "unknown", granted_video: "unknown" };
|
|
2900
|
+
let mediaFailure;
|
|
2786
2901
|
const diagnostics = new ConnectionDiagnostics({
|
|
2787
2902
|
now: () => clock.currentTimeMillisUnsafe(),
|
|
2788
2903
|
...options.diagnostics,
|
|
@@ -2802,6 +2917,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2802
2917
|
let fallbackIdentifier = 0;
|
|
2803
2918
|
let emittedPorts = null;
|
|
2804
2919
|
let foregroundRecoveryDeadline = 0;
|
|
2920
|
+
let pendingEpisodeEnd = null;
|
|
2921
|
+
let rejectionCheck = null;
|
|
2805
2922
|
const model = {
|
|
2806
2923
|
state: "idle",
|
|
2807
2924
|
sync: null,
|
|
@@ -2833,7 +2950,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2833
2950
|
epoch: 0
|
|
2834
2951
|
};
|
|
2835
2952
|
const mediaAttempt = { expiresAt: null };
|
|
2836
|
-
const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : lifecycleFailure(code, true, message, cause) });
|
|
2953
|
+
const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : sfuAccessRejected(cause) ? failureFromCause(cause, code, message) : lifecycleFailure(code, true, message, cause) });
|
|
2837
2954
|
const withinForegroundBudget = (effect, milliseconds2, failure2) => Effect2.raceFirst(
|
|
2838
2955
|
effect,
|
|
2839
2956
|
Effect2.callback((resume) => {
|
|
@@ -2882,10 +2999,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2882
2999
|
yield* publish();
|
|
2883
3000
|
});
|
|
2884
3001
|
const enqueue = (effect, join = false) => Effect2.gen(function* () {
|
|
2885
|
-
if (model.closed) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "The Connection scope is closed"));
|
|
2886
|
-
const deferred = yield*
|
|
3002
|
+
if (model.closed) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "The Connection scope is closed. Rejoin the call."));
|
|
3003
|
+
const deferred = yield* Deferred2.make();
|
|
2887
3004
|
yield* Queue.offer(queue, { effect, deferred, join });
|
|
2888
|
-
return yield*
|
|
3005
|
+
return yield* Deferred2.await(deferred);
|
|
2889
3006
|
});
|
|
2890
3007
|
const enqueueBackground = (effect) => {
|
|
2891
3008
|
void Effect2.runForkWith(context)(enqueue(effect).pipe(Effect2.ignore));
|
|
@@ -3041,7 +3158,13 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3041
3158
|
const span = diagnostics.startSpan({ step, state: model.state, epoch: model.epoch });
|
|
3042
3159
|
return effect.pipe(
|
|
3043
3160
|
Effect2.tap(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "succeeded" }))),
|
|
3044
|
-
Effect2.tapError(
|
|
3161
|
+
Effect2.tapError(
|
|
3162
|
+
(cause) => Effect2.sync(() => {
|
|
3163
|
+
const media = step === "acquire_initial_media" || step === "create_media_client" || step === "start_media" ? { ...classifyMediaFailure(cause, step === "acquire_initial_media" ? "local_capture" : "peer_connection"), ...grantedMedia } : void 0;
|
|
3164
|
+
if (media) mediaFailure = media;
|
|
3165
|
+
span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failureFromCause(cause, "internal_error", "Connection step failed").code, ...media ? { media } : {} });
|
|
3166
|
+
})
|
|
3167
|
+
)
|
|
3045
3168
|
);
|
|
3046
3169
|
};
|
|
3047
3170
|
const confirmSyncEntry = (sync) => {
|
|
@@ -3052,6 +3175,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3052
3175
|
if (model.state === "live") return;
|
|
3053
3176
|
if (model.state === "leaving" || model.closed) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Cannot join an inactive Connection"));
|
|
3054
3177
|
model.epoch += 1;
|
|
3178
|
+
pendingEpisodeEnd = null;
|
|
3179
|
+
rejectionCheck = null;
|
|
3180
|
+
grantedMedia = { granted_audio: "unknown", granted_video: "unknown" };
|
|
3181
|
+
mediaFailure = void 0;
|
|
3055
3182
|
model.failure = null;
|
|
3056
3183
|
model.episodeEndConfirmed = false;
|
|
3057
3184
|
model.episodeEndReason = void 0;
|
|
@@ -3066,6 +3193,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3066
3193
|
if (model.joinCancelled) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
3067
3194
|
const captured = yield* trace("acquire_initial_media", acquireInitialMedia(model.initialMedia, model.initialMediaIntent?.() ?? model.intent));
|
|
3068
3195
|
stream = captured.stream;
|
|
3196
|
+
grantedMedia = grantedMediaFromStream(stream);
|
|
3069
3197
|
captureWarning = captured.failure;
|
|
3070
3198
|
if (model.joinCancelled) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
3071
3199
|
const grant = yield* trace("access_initialize", reason === "access_retry" ? access.refreshAfterRejection() : access.initialize(reason));
|
|
@@ -3082,7 +3210,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3082
3210
|
}),
|
|
3083
3211
|
onTransportDiagnostic,
|
|
3084
3212
|
traceparent: reporter?.traceparent,
|
|
3085
|
-
onFailure: () => enqueueBackground(handleMediaFailure()),
|
|
3213
|
+
onFailure: (cause, binding = grant.media.clientPayload.connectionId) => enqueueBackground(handleMediaFailure(media, cause, binding)),
|
|
3086
3214
|
onScreenEnded: () => enqueueBackground(notifyScreenEnded())
|
|
3087
3215
|
}),
|
|
3088
3216
|
catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : lifecycleFailure("media_start_failed", true, "The media layer could not start", cause)
|
|
@@ -3102,7 +3230,11 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3102
3230
|
[
|
|
3103
3231
|
trace(
|
|
3104
3232
|
"start_media",
|
|
3105
|
-
model.startInitialMedia ? model.startInitialMedia(media, stream)
|
|
3233
|
+
(model.startInitialMedia ? model.startInitialMedia(media, stream) : Effect2.tryPromise({ try: () => media.start(stream), catch: (cause) => cause })).pipe(
|
|
3234
|
+
Effect2.mapError(
|
|
3235
|
+
(cause) => sfuAccessRejected(cause) ? lifecycleFailure("invalid_access", false, "Media access was rejected. Another tab or device may be using it. Rejoin explicitly to request access again.", cause) : model.startInitialMedia ? lifecycleFailure("media_start_failed", true, "The media layer could not start", cause) : failureFromCause(cause, "media_start_failed", "The media layer could not start")
|
|
3236
|
+
)
|
|
3237
|
+
)
|
|
3106
3238
|
),
|
|
3107
3239
|
trace(
|
|
3108
3240
|
"start_sync",
|
|
@@ -3120,14 +3252,20 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3120
3252
|
Effect2.matchEffect({
|
|
3121
3253
|
onFailure: (cause) => Effect2.gen(function* () {
|
|
3122
3254
|
stopStream(stream);
|
|
3123
|
-
const
|
|
3255
|
+
const originalFailure = failureFromCause(cause, "internal_error", "The Connection could not start");
|
|
3256
|
+
const failure2 = yield* resolveMediaRejection(originalFailure);
|
|
3257
|
+
if (failure2.code === "episode_ended" && needsEpisodeCheck(originalFailure)) {
|
|
3258
|
+
span.end({ state: model.state, epoch: model.epoch, outcome: "cancelled", code: "episode_ended" });
|
|
3259
|
+
yield* finishEpisodeEnded();
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3124
3262
|
model.failure = toFailure(failure2);
|
|
3125
3263
|
if (model.joinCancelled) {
|
|
3126
3264
|
span.end({ state: model.state, epoch: model.epoch, outcome: "cancelled", code: "invalid_state" });
|
|
3127
3265
|
return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
3128
3266
|
}
|
|
3129
3267
|
span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure2.code });
|
|
3130
|
-
const retry = !retried && failure2.code === "invalid_access";
|
|
3268
|
+
const retry = !retried && failure2.code === "invalid_access" && !sfuAccessRejected(failure2.cause);
|
|
3131
3269
|
if (!retry) yield* recordFailure(failure2);
|
|
3132
3270
|
model.admissionCleanupConfirmed = yield* cancelPendingAdmission();
|
|
3133
3271
|
yield* stopPorts();
|
|
@@ -3191,7 +3329,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3191
3329
|
yield* scheduleRefresh();
|
|
3192
3330
|
})
|
|
3193
3331
|
);
|
|
3194
|
-
const requireOpen = Effect2.suspend(() => model.closed ? Effect2.fail(lifecycleFailure("invalid_state", false, "The Connection scope is closed")) : Effect2.void);
|
|
3332
|
+
const requireOpen = Effect2.suspend(() => model.closed ? Effect2.fail(lifecycleFailure("invalid_state", false, "The Connection scope is closed. Rejoin the call.")) : Effect2.void);
|
|
3195
3333
|
const withFreshAccess = (operation) => Effect2.gen(function* () {
|
|
3196
3334
|
yield* requireOpen;
|
|
3197
3335
|
if (yield* access.requiresRefresh) {
|
|
@@ -3272,7 +3410,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3272
3410
|
}
|
|
3273
3411
|
const code = plan.kind === "media" ? "media_recovery_exhausted" : "sync_recovery_exhausted";
|
|
3274
3412
|
const transport = model.sync?.getSnapshot().connection.recoveryDiagnostic ?? null;
|
|
3275
|
-
diagnostics.recordRecoveryExhausted({ state: model.state, epoch, code, attempts, transport });
|
|
3413
|
+
diagnostics.recordRecoveryExhausted({ state: model.state, epoch, code, attempts, transport, media: mediaFailure, grantedMedia });
|
|
3276
3414
|
if (plan.kind === "sync") {
|
|
3277
3415
|
yield* failForSnapshot(lifecycleFailure(code, true, "Connection lost. Rejoin to continue."));
|
|
3278
3416
|
return;
|
|
@@ -3293,13 +3431,13 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3293
3431
|
const recoveryPlan = (kind) => Clock2.currentTimeMillis.pipe(
|
|
3294
3432
|
Effect2.map((now) => ({
|
|
3295
3433
|
kind,
|
|
3296
|
-
deadline: now + boundedInteger(options.recovery?.budgetMs,
|
|
3297
|
-
attempts: boundedInteger(options.recovery?.maxAttempts, MAX_RECOVERY_ATTEMPTS, 1, 10),
|
|
3298
|
-
delays: options.recovery?.backoffMs?.length ? options.recovery.backoffMs : [100, 250, 500]
|
|
3434
|
+
deadline: now + boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4),
|
|
3435
|
+
attempts: kind === "sync" ? boundedInteger(options.recovery?.maxAttempts, MAX_RECOVERY_ATTEMPTS, 1, 10) : null,
|
|
3436
|
+
delays: options.recovery?.backoffMs?.length ? options.recovery.backoffMs : kind === "media" ? [1e3, 2e3, 4e3, 6e3] : [100, 250, 500]
|
|
3299
3437
|
}))
|
|
3300
3438
|
);
|
|
3301
3439
|
const attemptRecovery = (plan, mediaAttempt2, syncAttempt, onAttempt) => Effect2.gen(function* () {
|
|
3302
|
-
for (let attempt = 1; attempt <= plan.attempts; attempt += 1) {
|
|
3440
|
+
for (let attempt = 1; plan.attempts === null || attempt <= plan.attempts; attempt += 1) {
|
|
3303
3441
|
const remaining = plan.deadline - (yield* Clock2.currentTimeMillis);
|
|
3304
3442
|
if (remaining <= 0) return null;
|
|
3305
3443
|
onAttempt();
|
|
@@ -3310,7 +3448,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3310
3448
|
);
|
|
3311
3449
|
if (failure2 === null) return attempt;
|
|
3312
3450
|
if (!failure2.recoverable) return yield* Effect2.fail(failure2);
|
|
3313
|
-
if (attempt < plan.attempts) {
|
|
3451
|
+
if (plan.attempts === null || attempt < plan.attempts) {
|
|
3314
3452
|
const remaining2 = Math.max(0, plan.deadline - (yield* Clock2.currentTimeMillis));
|
|
3315
3453
|
yield* Effect2.sleep(Math.min(recoveryDelay(plan, attempt), remaining2));
|
|
3316
3454
|
}
|
|
@@ -3327,10 +3465,13 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3327
3465
|
});
|
|
3328
3466
|
const recoveryDelay = (plan, attempt) => {
|
|
3329
3467
|
const delay = plan.delays[Math.min(attempt - 1, plan.delays.length - 1)] ?? 0;
|
|
3330
|
-
|
|
3468
|
+
const finiteDelay = Number.isFinite(delay) ? Math.max(0, delay) : 0;
|
|
3469
|
+
const mediaMinimum = [1e3, 2e3, 4e3][attempt - 1] ?? 6e3;
|
|
3470
|
+
return plan.kind === "media" ? Math.min(6e3, Math.max(mediaMinimum, finiteDelay)) : finiteDelay;
|
|
3331
3471
|
};
|
|
3332
3472
|
const finishRecovery = (attempt, epoch) => Effect2.gen(function* () {
|
|
3333
3473
|
if (model.epoch !== epoch || !active(model)) return;
|
|
3474
|
+
mediaFailure = void 0;
|
|
3334
3475
|
model.recoveryQueued = null;
|
|
3335
3476
|
model.syncSnapshot = model.sync?.getSnapshot() ?? null;
|
|
3336
3477
|
model.mediaSnapshot = model.media?.getSnapshot() ?? null;
|
|
@@ -3390,7 +3531,20 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3390
3531
|
attempt.expiresAt = (yield* Clock2.currentTimeMillis) + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
3391
3532
|
yield* foreign7(() => media.restart(restartInput), "media_start_failed", "Media transport could not restart");
|
|
3392
3533
|
yield* waitForMediaLive(media);
|
|
3393
|
-
})
|
|
3534
|
+
}).pipe(
|
|
3535
|
+
Effect2.tapError(
|
|
3536
|
+
(cause) => Effect2.sync(() => {
|
|
3537
|
+
const step = cause.code === "access_unavailable" || cause.code === "invalid_access" ? "access_refresh" : "peer_connection";
|
|
3538
|
+
const snapshotDiagnostic = cause.cause === void 0 && step === "peer_connection" ? model.media?.getSnapshot().failure?.diagnostic : void 0;
|
|
3539
|
+
mediaFailure = { ...snapshotDiagnostic ?? classifyMediaFailure(cause, step), ...grantedMedia };
|
|
3540
|
+
})
|
|
3541
|
+
),
|
|
3542
|
+
Effect2.tap(
|
|
3543
|
+
() => Effect2.sync(() => {
|
|
3544
|
+
mediaFailure = void 0;
|
|
3545
|
+
})
|
|
3546
|
+
)
|
|
3547
|
+
);
|
|
3394
3548
|
const syncAttemptExpired = (attempt, sync, now) => {
|
|
3395
3549
|
if (attempt.client !== sync) {
|
|
3396
3550
|
attempt.client = sync;
|
|
@@ -3418,7 +3572,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3418
3572
|
attempt.expiresAt = now + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
3419
3573
|
}
|
|
3420
3574
|
if ((attempt.started || snapshot.connection.phase !== "idle") && (yield* syncNeedsReplacement(snapshot))) return yield* restartSync(attempt);
|
|
3421
|
-
yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs,
|
|
3575
|
+
yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
|
|
3422
3576
|
});
|
|
3423
3577
|
const restartSync = (attempt) => Effect2.gen(function* () {
|
|
3424
3578
|
const epoch = model.epoch;
|
|
@@ -3449,7 +3603,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3449
3603
|
else if (attempt.client === sync) attempt.started = true;
|
|
3450
3604
|
})
|
|
3451
3605
|
).pipe(
|
|
3452
|
-
Effect2.andThen(waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs,
|
|
3606
|
+
Effect2.andThen(waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4))),
|
|
3453
3607
|
Effect2.andThen(Effect2.suspend(() => sync === model.sync && model.epoch === epoch && active(model) ? confirmSyncEntry(sync) : Effect2.fail(lifecycleFailure("invalid_state", false, "Sync recovery was superseded")))),
|
|
3454
3608
|
Effect2.tapError(
|
|
3455
3609
|
() => enqueue(
|
|
@@ -3502,14 +3656,111 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3502
3656
|
const syncNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "connecting" || snapshot.connection.phase === "recovering" || snapshot.connection.phase === "terminal");
|
|
3503
3657
|
const mediaNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "recovering" || snapshot.connection.phase === "failed" && snapshot.failure?.recoverable === true);
|
|
3504
3658
|
const recordFailure = (failure2) => Effect2.sync(() => {
|
|
3505
|
-
diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code, ...failure2.sync ? { sync: failure2.sync } : {} });
|
|
3659
|
+
diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure2.code, ...failure2.sync ? { sync: failure2.sync } : {}, ...mediaFailure ? { media: mediaFailure } : {} });
|
|
3660
|
+
});
|
|
3661
|
+
const finishEpisodeEnded = () => Effect2.gen(function* () {
|
|
3662
|
+
model.episodeEndConfirmed = true;
|
|
3663
|
+
model.syncSnapshot = model.sync?.getSnapshot() ?? model.syncSnapshot;
|
|
3664
|
+
model.episodeEndReason = model.syncSnapshot?.control?.endReason;
|
|
3665
|
+
yield* finishTerminalSnapshot();
|
|
3506
3666
|
});
|
|
3507
|
-
const
|
|
3667
|
+
const needsEpisodeCheck = (failure2) => failure2.code === "invalid_access" || failure2.cause instanceof CloudflareSFUError && failure2.cause.options.status === 410;
|
|
3668
|
+
const episodeHasEnded = () => {
|
|
3669
|
+
const snapshot = model.sync?.getSnapshot();
|
|
3670
|
+
return model.episodeEndConfirmed || snapshot?.control?.status === "ended" || snapshot?.connection.terminalReason === "episode_ended";
|
|
3671
|
+
};
|
|
3672
|
+
const stopRejectedMedia = () => {
|
|
3673
|
+
model.mediaBindingCleanup?.();
|
|
3674
|
+
model.mediaSnapshot = model.media?.getSnapshot() ?? null;
|
|
3675
|
+
};
|
|
3676
|
+
const waitForPendingEpisodeEnd = Effect2.suspend(() => pendingEpisodeEnd ? Deferred2.await(pendingEpisodeEnd) : Effect2.void);
|
|
3677
|
+
const resolveMediaRejection = (failure2) => {
|
|
3678
|
+
if (!needsEpisodeCheck(failure2)) return Effect2.succeed(failure2);
|
|
3679
|
+
if (failure2.code === "invalid_access") stopRejectedMedia();
|
|
3680
|
+
const epoch = model.epoch;
|
|
3681
|
+
const original = access.currentUnsafe();
|
|
3682
|
+
if (!original) return Effect2.succeed(failure2);
|
|
3683
|
+
return checkMediaRejection(failure2, original, epoch);
|
|
3684
|
+
};
|
|
3685
|
+
const checkMediaRejection = (failure2, original, epoch) => Effect2.gen(function* () {
|
|
3686
|
+
let attempt = 0;
|
|
3687
|
+
let refreshedAfterAuthRejection = false;
|
|
3688
|
+
let rejectedToken = null;
|
|
3689
|
+
while (model.epoch === epoch) {
|
|
3690
|
+
yield* waitForPendingEpisodeEnd;
|
|
3691
|
+
if (episodeHasEnded()) return lifecycleFailure("episode_ended", false, "The Episode has ended");
|
|
3692
|
+
const current = access.currentUnsafe();
|
|
3693
|
+
const grant = sameBindingCredential(current, original);
|
|
3694
|
+
const result = yield* readEpisodeStatus(platform.getEpisodeStatus, grant, rejectedToken);
|
|
3695
|
+
yield* waitForPendingEpisodeEnd;
|
|
3696
|
+
if (episodeHasEnded() || statusIsEnded(result.status)) return lifecycleFailure("episode_ended", false, "The Episode has ended");
|
|
3697
|
+
if (result.status === "active") return failure2;
|
|
3698
|
+
const error2 = result.cause;
|
|
3699
|
+
if (statusCredentialsRejected(error2)) {
|
|
3700
|
+
rejectedToken = grant.media.token;
|
|
3701
|
+
if (!refreshedAfterAuthRejection) {
|
|
3702
|
+
refreshedAfterAuthRejection = true;
|
|
3703
|
+
yield* access.refresh("scheduled_refresh", false).pipe(Effect2.catch(() => Effect2.succeed(null)));
|
|
3704
|
+
continue;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
yield* Effect2.sleep(episodeStatusRetryDelay(attempt++, error2));
|
|
3708
|
+
}
|
|
3709
|
+
return lifecycleFailure("invalid_state", false, "The connection changed");
|
|
3710
|
+
}).pipe(
|
|
3711
|
+
Effect2.timeout(6e4),
|
|
3712
|
+
Effect2.catch(() => Effect2.succeed(episodeHasEnded() ? lifecycleFailure("episode_ended", false, "The Episode has ended") : lifecycleFailure("access_unavailable", true, "Could not confirm Episode status. Rejoin to try again.")))
|
|
3713
|
+
);
|
|
3714
|
+
const commitSnapshotFailure = (failure2) => Effect2.gen(function* () {
|
|
3715
|
+
if (failure2.code === "episode_ended") return yield* finishEpisodeEnded();
|
|
3508
3716
|
model.failure = toFailure(failure2);
|
|
3509
3717
|
yield* recordFailure(failure2);
|
|
3510
3718
|
yield* stopPorts();
|
|
3511
3719
|
yield* transition("failed");
|
|
3512
3720
|
});
|
|
3721
|
+
const failForSnapshot = (failure2) => {
|
|
3722
|
+
if (!needsEpisodeCheck(failure2)) return commitSnapshotFailure(failure2);
|
|
3723
|
+
if (failure2.code === "invalid_access") stopRejectedMedia();
|
|
3724
|
+
const scope = model.activeScope;
|
|
3725
|
+
const epoch = model.epoch;
|
|
3726
|
+
const binding = access.currentUnsafe()?.media.clientPayload.connectionId;
|
|
3727
|
+
if (!scope) return Effect2.void;
|
|
3728
|
+
if (rejectionCheck?.epoch === epoch && rejectionCheck.binding === binding) {
|
|
3729
|
+
if (failure2.code === "invalid_access") rejectionCheck.failure = failure2;
|
|
3730
|
+
return publish();
|
|
3731
|
+
}
|
|
3732
|
+
const check = { epoch, binding, failure: failure2 };
|
|
3733
|
+
rejectionCheck = check;
|
|
3734
|
+
const releaseBinding = binding ? Effect2.runSync(access.holdMediaBinding(binding)) : Effect2.void;
|
|
3735
|
+
return Effect2.gen(function* () {
|
|
3736
|
+
if (model.epoch !== epoch || model.activeScope !== scope) return;
|
|
3737
|
+
yield* publish();
|
|
3738
|
+
yield* Effect2.forkIn(
|
|
3739
|
+
resolveMediaRejection(failure2).pipe(
|
|
3740
|
+
Effect2.flatMap(
|
|
3741
|
+
(resolved) => enqueue(
|
|
3742
|
+
Effect2.suspend(() => {
|
|
3743
|
+
if (model.epoch !== epoch || !active(model)) return Effect2.void;
|
|
3744
|
+
const finalFailure = resolved === failure2 ? check.failure : resolved;
|
|
3745
|
+
if (finalFailure === failure2 && failure2.cause instanceof CloudflareSFUError && failure2.cause.options.status === 410) {
|
|
3746
|
+
rejectionCheck = null;
|
|
3747
|
+
return recover("media");
|
|
3748
|
+
}
|
|
3749
|
+
return commitSnapshotFailure(finalFailure);
|
|
3750
|
+
})
|
|
3751
|
+
)
|
|
3752
|
+
),
|
|
3753
|
+
Effect2.ensuring(
|
|
3754
|
+
Effect2.gen(function* () {
|
|
3755
|
+
if (rejectionCheck === check) rejectionCheck = null;
|
|
3756
|
+
if (model.activeScope === scope) yield* releaseBinding;
|
|
3757
|
+
})
|
|
3758
|
+
)
|
|
3759
|
+
),
|
|
3760
|
+
scope
|
|
3761
|
+
);
|
|
3762
|
+
});
|
|
3763
|
+
};
|
|
3513
3764
|
const handleMediaSnapshot = (media, snapshot) => Effect2.gen(function* () {
|
|
3514
3765
|
if (media !== model.media || media.getSnapshot() !== snapshot) return;
|
|
3515
3766
|
if (snapshot === model.mediaSnapshot) return;
|
|
@@ -3519,7 +3770,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3519
3770
|
if (mediaNeedsRecovery(snapshot)) yield* recover("media");
|
|
3520
3771
|
yield* publish();
|
|
3521
3772
|
});
|
|
3522
|
-
const handleMediaFailure = (
|
|
3773
|
+
const handleMediaFailure = (media, cause, binding) => {
|
|
3774
|
+
if (media !== model.media || binding !== access.currentUnsafe()?.media.clientPayload.connectionId) return Effect2.void;
|
|
3775
|
+
if (sfuAccessRejected(cause)) stopRejectedMedia();
|
|
3776
|
+
if (sfuAccessRejected(cause) || cause instanceof CloudflareSFUError && cause.options.status === 410 || active(model) && connectionSuperseded(cause)) return failForSnapshot(failureFromCause(cause, "media_start_failed", "Media transport needs recovery"));
|
|
3777
|
+
return handleMediaSnapshot(media, media.getSnapshot());
|
|
3778
|
+
};
|
|
3523
3779
|
const applyAutomaticMediaReplacement = (grant) => Effect2.gen(function* () {
|
|
3524
3780
|
const media = model.media;
|
|
3525
3781
|
const scope = model.activeScope;
|
|
@@ -3535,7 +3791,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3535
3791
|
"media_start_failed",
|
|
3536
3792
|
"The media client could not apply a replacement access grant"
|
|
3537
3793
|
).pipe(
|
|
3538
|
-
(effect) => withinForegroundBudget(effect, MEDIA_RECOVERY_TIMEOUT_MS, lifecycleFailure("media_start_failed", true, "Automatic media replacement did not finish")),
|
|
3794
|
+
(effect) => withinForegroundBudget(effect, MEDIA_RECOVERY_TIMEOUT_MS, lifecycleFailure("media_start_failed", true, "Automatic media replacement did not finish", new CloudflareSFUError("Automatic media replacement deadline expired", "negotiation_timeout"))),
|
|
3795
|
+
Effect2.tapError(
|
|
3796
|
+
(cause) => Effect2.sync(() => {
|
|
3797
|
+
mediaFailure = { ...classifyMediaFailure(cause, "peer_connection"), ...grantedMedia };
|
|
3798
|
+
})
|
|
3799
|
+
),
|
|
3539
3800
|
Effect2.match({ onSuccess: () => true, onFailure: () => false }),
|
|
3540
3801
|
Effect2.flatMap(
|
|
3541
3802
|
(restarted) => enqueue(
|
|
@@ -3569,8 +3830,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3569
3830
|
const exit = yield* Effect2.exit(Fiber.join(fiber));
|
|
3570
3831
|
activeJoin = null;
|
|
3571
3832
|
joinDeferred = null;
|
|
3572
|
-
yield*
|
|
3573
|
-
}) :
|
|
3833
|
+
yield* Deferred2.done(work.deferred, exit);
|
|
3834
|
+
}) : Deferred2.complete(work.deferred, work.effect).pipe(Effect2.asVoid);
|
|
3574
3835
|
yield* Effect2.forkScoped(Queue.take(queue).pipe(Effect2.flatMap(process), Effect2.forever));
|
|
3575
3836
|
const accessReplacementUnsubscribe = yield* access.subscribeAutomaticMediaReplacement((grant) => enqueueBackground(applyAutomaticMediaReplacement(grant)));
|
|
3576
3837
|
const foregroundUnsubscribe = platform.subscribeForeground?.(() => {
|
|
@@ -3620,15 +3881,33 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3620
3881
|
confirmEpisodeEnded: Effect2.sync(() => {
|
|
3621
3882
|
model.episodeEndConfirmed = true;
|
|
3622
3883
|
}),
|
|
3884
|
+
runEpisodeEnd: (operation) => Effect2.gen(function* () {
|
|
3885
|
+
const completed = yield* Deferred2.make();
|
|
3886
|
+
const epoch = model.epoch;
|
|
3887
|
+
pendingEpisodeEnd = completed;
|
|
3888
|
+
return yield* operation.pipe(
|
|
3889
|
+
Effect2.tap(
|
|
3890
|
+
() => Effect2.sync(() => {
|
|
3891
|
+
if (model.epoch === epoch) model.episodeEndConfirmed = true;
|
|
3892
|
+
})
|
|
3893
|
+
),
|
|
3894
|
+
Effect2.ensuring(
|
|
3895
|
+
Effect2.gen(function* () {
|
|
3896
|
+
if (pendingEpisodeEnd === completed) pendingEpisodeEnd = null;
|
|
3897
|
+
yield* Deferred2.succeed(completed, void 0);
|
|
3898
|
+
})
|
|
3899
|
+
)
|
|
3900
|
+
);
|
|
3901
|
+
}),
|
|
3623
3902
|
join: () => Effect2.suspend(() => {
|
|
3624
3903
|
if (model.state === "live") return Effect2.void;
|
|
3625
|
-
if (joinDeferred) return
|
|
3904
|
+
if (joinDeferred) return Deferred2.await(joinDeferred);
|
|
3626
3905
|
return Effect2.gen(function* () {
|
|
3627
|
-
const deferred = yield*
|
|
3906
|
+
const deferred = yield* Deferred2.make();
|
|
3628
3907
|
model.joinCancelled = false;
|
|
3629
3908
|
joinDeferred = deferred;
|
|
3630
3909
|
yield* Queue.offer(queue, { effect: performJoin(), deferred, join: true });
|
|
3631
|
-
return yield*
|
|
3910
|
+
return yield* Deferred2.await(deferred);
|
|
3632
3911
|
});
|
|
3633
3912
|
}),
|
|
3634
3913
|
leave: () => Effect2.gen(function* () {
|
|
@@ -3656,7 +3935,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3656
3935
|
nowUnsafe: () => clock.currentTimeMillisUnsafe(),
|
|
3657
3936
|
scheduleUnsafe: (callback, milliseconds2) => platform.clock.setTimeout(callback, milliseconds2),
|
|
3658
3937
|
cancelScheduleUnsafe: (handle) => platform.clock.clearTimeout(handle),
|
|
3659
|
-
createId: () => platform.createId?.() ?? `local-${++fallbackIdentifier}
|
|
3938
|
+
createId: () => platform.createId?.() ?? `local-${++fallbackIdentifier}`,
|
|
3939
|
+
recordTransportDiagnostic: onTransportDiagnostic
|
|
3660
3940
|
};
|
|
3661
3941
|
})
|
|
3662
3942
|
);
|
|
@@ -3701,7 +3981,12 @@ function toFailure(value) {
|
|
|
3701
3981
|
return Object.freeze({ code: value.code, action: null, recoverable: value.recoverable, message: value.message, ...value.sync ? { sync: value.sync } : {} });
|
|
3702
3982
|
}
|
|
3703
3983
|
function accessFailure(value) {
|
|
3704
|
-
|
|
3984
|
+
const rejectedMessage = connectionSuperseded(value) ? "Access was rejected. Another tab or device may be using it. Rejoin explicitly to continue." : "Access was rejected";
|
|
3985
|
+
return lifecycleFailure(value.code === "access.invalid" ? "invalid_access" : "access_unavailable", value.code === "access.unavailable", value.code === "access.invalid" ? rejectedMessage : "Could not reach the Space. Check your network and try again.", value.cause);
|
|
3986
|
+
}
|
|
3987
|
+
function connectionSuperseded(failure2) {
|
|
3988
|
+
const cause = failure2 instanceof ConnectionAccessFailure ? failure2.cause : failure2;
|
|
3989
|
+
return cause instanceof AccessGrantError ? cause.reason === "connection_superseded" : typeof cause === "object" && cause !== null && "code" in cause && cause.code === "participant.connection_superseded";
|
|
3705
3990
|
}
|
|
3706
3991
|
function captureFailure(cause) {
|
|
3707
3992
|
if (cause instanceof ConnectionError && cause.cause === void 0) return lifecycleFailure(cause.code, cause.recoverable, cause.message, cause);
|
|
@@ -3757,15 +4042,19 @@ function initialCaptureWarning(failures) {
|
|
|
3757
4042
|
return lifecycleFailure(first.failure.code, true, message, first.failure.cause);
|
|
3758
4043
|
}
|
|
3759
4044
|
function failureFromCause(cause, code, message) {
|
|
4045
|
+
if (sfuAccessRejected(cause)) return lifecycleFailure("invalid_access", false, "Media access was rejected. Rejoin explicitly to continue.", cause);
|
|
3760
4046
|
if (cause instanceof ConnectionLifecycleFailure) return cause;
|
|
3761
4047
|
if (cause instanceof ConnectionAccessFailure) return accessFailure(cause);
|
|
3762
|
-
if (cause instanceof AccessGrantError) return
|
|
4048
|
+
if (cause instanceof AccessGrantError || connectionSuperseded(cause)) return accessFailure(new ConnectionAccessFailure({ code: "access.invalid", cause }));
|
|
3763
4049
|
if (cause instanceof ConnectionError) return lifecycleFailure(cause.code, cause.recoverable, cause.message, cause);
|
|
3764
4050
|
if (cause instanceof DOMException && cause.name === "NotAllowedError") return lifecycleFailure("permission_denied", true, "Media permission was denied", cause);
|
|
3765
4051
|
return lifecycleFailure(code, true, message, cause);
|
|
3766
4052
|
}
|
|
4053
|
+
function sfuAccessRejected(cause) {
|
|
4054
|
+
return cause instanceof CloudflareSFUError && (cause.options.status === 401 || cause.options.status === 403);
|
|
4055
|
+
}
|
|
3767
4056
|
function accessRejected(cause) {
|
|
3768
|
-
return cause instanceof ConnectionError ? cause.code === "invalid_access" : typeof cause === "object" && cause !== null && "code" in cause && cause.code === "access.invalid";
|
|
4057
|
+
return cause instanceof ConnectionError ? cause.code === "invalid_access" : typeof cause === "object" && cause !== null && "code" in cause && (cause.code === "access.invalid" || cause.code === "participant.connection_superseded");
|
|
3769
4058
|
}
|
|
3770
4059
|
function boundedInteger(value, fallback, minimum, maximum) {
|
|
3771
4060
|
const result = value ?? fallback;
|
|
@@ -3795,6 +4084,7 @@ function syncTerminalFailure(snapshot) {
|
|
|
3795
4084
|
const reason = snapshot.connection.terminalReason;
|
|
3796
4085
|
if (reason === "state_digest_recovery_failed") return lifecycleFailure("state_digest_recovery_failed", false, "The Episode state could not be recovered from an authoritative snapshot");
|
|
3797
4086
|
if (reason === "episode_ended") return lifecycleFailure("episode_ended", false, "The Episode has ended");
|
|
4087
|
+
if (reason === "stale_participant_generation") return lifecycleFailure("invalid_access", false, "Call access was replaced. Another tab or device may be using it. Rejoin explicitly to request access again.");
|
|
3798
4088
|
if (reason === "participant_inactive" || reason === "participant_removed" || reason === "stale_participant_generation" || reason === "access_rejected") return lifecycleFailure("invalid_access", false, "Participant access is no longer valid");
|
|
3799
4089
|
if (reason === "entry_timeout") return lifecycleFailure("sync_start_failed", true, "Call entry was not confirmed in time");
|
|
3800
4090
|
return lifecycleFailure("sync_start_failed", true, "The Sync transport needs recovery");
|
|
@@ -3809,6 +4099,22 @@ function syncStartupFailure(snapshot) {
|
|
|
3809
4099
|
function syncAutoRecoverable(snapshot, failure2) {
|
|
3810
4100
|
return failure2.recoverable && snapshot.connection.terminalReason !== "entry_timeout";
|
|
3811
4101
|
}
|
|
4102
|
+
function statusIsEnded(status) {
|
|
4103
|
+
return status === "ending" || status === "ended";
|
|
4104
|
+
}
|
|
4105
|
+
function statusCredentialsRejected(error2) {
|
|
4106
|
+
return error2 instanceof EpisodeStatusError && (error2.status === 401 || error2.status === 403);
|
|
4107
|
+
}
|
|
4108
|
+
function readEpisodeStatus(read, grant, rejectedToken) {
|
|
4109
|
+
if (grant.media.token === rejectedToken) return Effect2.succeed({ status: null, cause: null });
|
|
4110
|
+
return Effect2.tryPromise({
|
|
4111
|
+
try: (signal) => read ? read(grant, signal) : Promise.reject(new Error("Episode status lookup unavailable")),
|
|
4112
|
+
catch: (cause) => cause
|
|
4113
|
+
}).pipe(Effect2.timeout(3e3), Effect2.match({ onSuccess: (status) => ({ status, cause: null }), onFailure: (cause) => ({ status: null, cause }) }));
|
|
4114
|
+
}
|
|
4115
|
+
function sameBindingCredential(current, original) {
|
|
4116
|
+
return current?.media.clientPayload.connectionId === original.media.clientPayload.connectionId ? current : original;
|
|
4117
|
+
}
|
|
3812
4118
|
|
|
3813
4119
|
// src/space-client/errors.ts
|
|
3814
4120
|
import { Data as Data3 } from "effect";
|
|
@@ -3909,7 +4215,7 @@ function connectionErrorCode(code, action) {
|
|
|
3909
4215
|
|
|
3910
4216
|
// src/space-client/store.ts
|
|
3911
4217
|
import { Context as Context4, Effect as Effect3, Layer as Layer4, SubscriptionRef as SubscriptionRef2 } from "effect";
|
|
3912
|
-
var INTERRUPTED_CONNECTION_PHASES = /* @__PURE__ */ new Set(["unresponsive", "connecting", "recovering"]);
|
|
4218
|
+
var INTERRUPTED_CONNECTION_PHASES = /* @__PURE__ */ new Set(["unresponsive", "connecting", "recovering", "stopped"]);
|
|
3913
4219
|
var empty = (value) => Object.freeze(value);
|
|
3914
4220
|
var EMPTY = empty({
|
|
3915
4221
|
connection: empty({ status: "idle", episode: null, lastError: null, episodeEnded: false, removedByHost: false }),
|
|
@@ -4572,11 +4878,79 @@ function chatError(message) {
|
|
|
4572
4878
|
}
|
|
4573
4879
|
|
|
4574
4880
|
// src/space-client/media-controller.ts
|
|
4575
|
-
import { Clock as Clock3, Context as Context6, Deferred as
|
|
4881
|
+
import { Clock as Clock3, Context as Context6, Deferred as Deferred3, Effect as Effect5, Exit as Exit2, Layer as Layer6, Semaphore } from "effect";
|
|
4882
|
+
|
|
4883
|
+
// src/space-client/media-projection-monitor.ts
|
|
4884
|
+
var GRACE_MS = 1e4;
|
|
4885
|
+
function projectionMatches(observation) {
|
|
4886
|
+
return observation.enabled === observation.projectedEnabled && (!observation.enabled || observation.publicationId === observation.projectedPublicationId);
|
|
4887
|
+
}
|
|
4888
|
+
var MediaProjectionMonitor = class {
|
|
4889
|
+
#options;
|
|
4890
|
+
#actions = /* @__PURE__ */ new Map();
|
|
4891
|
+
constructor(options) {
|
|
4892
|
+
this.#options = options;
|
|
4893
|
+
}
|
|
4894
|
+
observe(source, observation) {
|
|
4895
|
+
let action = this.#actions.get(source);
|
|
4896
|
+
if (action?.revision !== observation.revision) {
|
|
4897
|
+
this.#clear(source);
|
|
4898
|
+
action = { revision: observation.revision, mismatch: null, reported: false, failed: false };
|
|
4899
|
+
this.#actions.set(source, action);
|
|
4900
|
+
}
|
|
4901
|
+
if (action.failed) return;
|
|
4902
|
+
if (projectionMatches(observation)) {
|
|
4903
|
+
if (action.mismatch) this.#options.cancel(action.mismatch.timer);
|
|
4904
|
+
action.mismatch = null;
|
|
4905
|
+
return;
|
|
4906
|
+
}
|
|
4907
|
+
if (action.mismatch) {
|
|
4908
|
+
action.mismatch.observation = observation;
|
|
4909
|
+
if (observation.enabled && this.#options.now() - action.mismatch.startedAt >= GRACE_MS) this.#expire(source, action);
|
|
4910
|
+
return;
|
|
4911
|
+
}
|
|
4912
|
+
const timer = this.#options.schedule(() => this.#expire(source, action), GRACE_MS);
|
|
4913
|
+
action.mismatch = { observation, startedAt: this.#options.now(), timer };
|
|
4914
|
+
}
|
|
4915
|
+
state(source) {
|
|
4916
|
+
const action = this.#actions.get(source);
|
|
4917
|
+
if (action?.failed) return "failed";
|
|
4918
|
+
return action?.mismatch?.observation.enabled ? "requesting" : null;
|
|
4919
|
+
}
|
|
4920
|
+
reset() {
|
|
4921
|
+
for (const source of this.#actions.keys()) this.#clear(source);
|
|
4922
|
+
}
|
|
4923
|
+
#clear(source) {
|
|
4924
|
+
const action = this.#actions.get(source);
|
|
4925
|
+
if (action?.mismatch) this.#options.cancel(action.mismatch.timer);
|
|
4926
|
+
this.#actions.delete(source);
|
|
4927
|
+
}
|
|
4928
|
+
#expire(source, action) {
|
|
4929
|
+
if (this.#actions.get(source) !== action || !action.mismatch) return;
|
|
4930
|
+
const { observation, startedAt } = action.mismatch;
|
|
4931
|
+
action.failed = observation.enabled;
|
|
4932
|
+
if (observation.enabled) this.#options.pause(source, observation.revision);
|
|
4933
|
+
if (!action.reported) {
|
|
4934
|
+
action.reported = true;
|
|
4935
|
+
this.#options.report({
|
|
4936
|
+
event: "media_projection_mismatch",
|
|
4937
|
+
source,
|
|
4938
|
+
publication_state: observation.enabled ? "on" : "off",
|
|
4939
|
+
projection_state: observation.projectedEnabled ? "on" : "off",
|
|
4940
|
+
duration_ms: this.#options.now() - startedAt,
|
|
4941
|
+
attempt: 0,
|
|
4942
|
+
outcome: observation.enabled ? "failed" : "started"
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
this.#options.changed();
|
|
4946
|
+
}
|
|
4947
|
+
};
|
|
4948
|
+
|
|
4949
|
+
// src/space-client/media-controller.ts
|
|
4576
4950
|
var MEDIA_SOURCES = ["microphone", "camera", "screen"];
|
|
4577
4951
|
var MEDIA_READY_TIMEOUT_MS = 1e4;
|
|
4578
4952
|
function sourceWorker(enabled) {
|
|
4579
|
-
return { desired: { enabled, revision: 0, previousEnabled: enabled }, result: { revision: 0, status: "confirmed" },
|
|
4953
|
+
return { desired: { enabled, revision: 0, controlRevision: -1, previousEnabled: enabled }, result: { revision: 0, status: "confirmed" }, initialRevision: 0, localEnabled: enabled, pause: null, serverSequence: -1, forcedRevision: null, pausedPublicationId: null, capturePause: "none" };
|
|
4580
4954
|
}
|
|
4581
4955
|
var MediaControllerService = class extends Context6.Service()("@chalk/client/MediaController") {
|
|
4582
4956
|
};
|
|
@@ -4619,6 +4993,8 @@ var MediaControllerRuntime = class {
|
|
|
4619
4993
|
#tracks = /* @__PURE__ */ new Map();
|
|
4620
4994
|
#sources = { microphone: sourceWorker(true), camera: sourceWorker(true), screen: sourceWorker(false) };
|
|
4621
4995
|
#syncLive = false;
|
|
4996
|
+
#disposed = false;
|
|
4997
|
+
#recovering = null;
|
|
4622
4998
|
#pendingSources = /* @__PURE__ */ new Map();
|
|
4623
4999
|
#pendingCaptures = /* @__PURE__ */ new Set();
|
|
4624
5000
|
#readinessChecks = /* @__PURE__ */ new Set();
|
|
@@ -4634,6 +5010,8 @@ var MediaControllerRuntime = class {
|
|
|
4634
5010
|
#unsubscribeConnection = null;
|
|
4635
5011
|
#unsubscribeScreenEnded = null;
|
|
4636
5012
|
#unsubscribePorts = null;
|
|
5013
|
+
#microphoneWatch = null;
|
|
5014
|
+
#projection;
|
|
4637
5015
|
constructor(connection, store, selection, gates, now, fork) {
|
|
4638
5016
|
this.#connection = connection;
|
|
4639
5017
|
this.#store = store;
|
|
@@ -4641,6 +5019,16 @@ var MediaControllerRuntime = class {
|
|
|
4641
5019
|
this.#gates = gates;
|
|
4642
5020
|
this.#now = now;
|
|
4643
5021
|
this.#fork = fork;
|
|
5022
|
+
this.#projection = new MediaProjectionMonitor({
|
|
5023
|
+
now,
|
|
5024
|
+
schedule: connection.scheduleUnsafe,
|
|
5025
|
+
cancel: connection.cancelScheduleUnsafe,
|
|
5026
|
+
changed: () => this.#publish(),
|
|
5027
|
+
report: (event) => connection.recordTransportDiagnostic(event),
|
|
5028
|
+
pause: (source, revision) => {
|
|
5029
|
+
if (this.#ports) this.#pausePublication(this.#ports, source, revision, "projection_mismatch");
|
|
5030
|
+
}
|
|
5031
|
+
});
|
|
4644
5032
|
this.#unsubscribeConnection = connection.subscribe(() => this.#handleConnectionChange());
|
|
4645
5033
|
this.#unsubscribeScreenEnded = connection.subscribeScreenEnded(() => this.#handleScreenEnded());
|
|
4646
5034
|
this.#unsubscribePorts = connection.subscribePorts((ports) => this.#bind(ports));
|
|
@@ -4688,7 +5076,7 @@ var MediaControllerRuntime = class {
|
|
|
4688
5076
|
}).pipe(Effect5.mapError(normalizeMediaError));
|
|
4689
5077
|
};
|
|
4690
5078
|
initialIntent() {
|
|
4691
|
-
return { microphone: this.#
|
|
5079
|
+
return { microphone: this.#hasConsent("microphone"), camera: this.#hasConsent("camera") };
|
|
4692
5080
|
}
|
|
4693
5081
|
captureInitial(intent) {
|
|
4694
5082
|
for (const source of ["microphone", "camera"]) this.#sources[source].initialRevision = this.#sources[source].desired.revision;
|
|
@@ -4744,7 +5132,9 @@ var MediaControllerRuntime = class {
|
|
|
4744
5132
|
this.#fork(this.#refreshDevices().pipe(Effect5.ignore));
|
|
4745
5133
|
}
|
|
4746
5134
|
dispose() {
|
|
4747
|
-
|
|
5135
|
+
this.#disposed = true;
|
|
5136
|
+
this.#projection.reset();
|
|
5137
|
+
for (const check of this.#readinessChecks) check(normalizeMediaError(this.#cancelledCaptureError()));
|
|
4748
5138
|
this.#cancelCaptures();
|
|
4749
5139
|
for (const unsubscribe of [this.#unsubscribeMedia, this.#unsubscribeRequests, this.#unsubscribePause, this.#unsubscribeSync]) unsubscribe?.();
|
|
4750
5140
|
this.#unsubscribeConnection?.();
|
|
@@ -4759,9 +5149,6 @@ var MediaControllerRuntime = class {
|
|
|
4759
5149
|
this.#unsubscribePorts = null;
|
|
4760
5150
|
this.#ports = null;
|
|
4761
5151
|
this.#resetObservations();
|
|
4762
|
-
this.#screenAudioTrack?.stop();
|
|
4763
|
-
this.#screenAudioTrack = null;
|
|
4764
|
-
for (const track of this.#tracks.values()) track.stop();
|
|
4765
5152
|
this.#clearTracks();
|
|
4766
5153
|
this.#clearRequests();
|
|
4767
5154
|
this.#publish();
|
|
@@ -4777,10 +5164,11 @@ var MediaControllerRuntime = class {
|
|
|
4777
5164
|
const check = (failure2) => {
|
|
4778
5165
|
if (failure2) return resume(Effect5.fail(failure2));
|
|
4779
5166
|
if (this.#sources[source].desired.revision !== revision) return resume(Effect5.void);
|
|
4780
|
-
if (ports &&
|
|
5167
|
+
if (ports && !this.#canUsePorts(ports)) return resume(Effect5.fail(normalizeMediaError(this.#cancelledCaptureError())));
|
|
4781
5168
|
const state = this.#connection.getSnapshot().state;
|
|
4782
|
-
|
|
4783
|
-
if (
|
|
5169
|
+
const phases = [state, this.#ports?.sync.getSnapshot().connection.phase, this.#store.getSnapshot().connection.status];
|
|
5170
|
+
if (phases.every((phase) => phase === "live")) return resume(Effect5.void);
|
|
5171
|
+
if (!["joining", "reconnecting", "live"].includes(state)) resume(Effect5.fail(normalizeMediaError(new V1SyncError("Media enable requires a live call", "not_live"))));
|
|
4784
5172
|
};
|
|
4785
5173
|
this.#readinessChecks.add(check);
|
|
4786
5174
|
const unsubscribe = this.#connection.subscribe(() => check());
|
|
@@ -4810,12 +5198,22 @@ var MediaControllerRuntime = class {
|
|
|
4810
5198
|
#pauseTracks(source) {
|
|
4811
5199
|
const track = this.#track(source);
|
|
4812
5200
|
if (track) track.enabled = false;
|
|
5201
|
+
for (const publication of this.#ports?.media.getSnapshot().localTracks ?? []) if (publication.source === source) publication.track.enabled = false;
|
|
4813
5202
|
if (source === "screen" && this.#screenAudioTrack) this.#screenAudioTrack.enabled = false;
|
|
4814
5203
|
}
|
|
5204
|
+
#retainPause(state, next) {
|
|
5205
|
+
const previous = state.pause;
|
|
5206
|
+
state.pause = previous ? Promise.allSettled([previous, next]).then((results) => {
|
|
5207
|
+
for (const result of results) if (result.status === "rejected") throw result.reason;
|
|
5208
|
+
}) : next;
|
|
5209
|
+
void state.pause.catch(() => void 0);
|
|
5210
|
+
}
|
|
4815
5211
|
#recordIntent(source, enabled, options) {
|
|
4816
5212
|
const state = this.#sources[source];
|
|
4817
|
-
const
|
|
5213
|
+
const controlRevision = Math.max(state.serverSequence, this.#ports?.sync.getSnapshot().control?.revision ?? -1);
|
|
5214
|
+
const desired = { enabled, revision: state.desired.revision + 1, controlRevision, options, previousEnabled: state.localEnabled };
|
|
4818
5215
|
state.desired = desired;
|
|
5216
|
+
state.capturePause = "none";
|
|
4819
5217
|
if (!enabled) {
|
|
4820
5218
|
state.localEnabled = false;
|
|
4821
5219
|
this.#pauseTracks(source);
|
|
@@ -4825,13 +5223,8 @@ var MediaControllerRuntime = class {
|
|
|
4825
5223
|
}
|
|
4826
5224
|
const ports = this.#ports;
|
|
4827
5225
|
if (ports) {
|
|
4828
|
-
ports.media.setLocalSourceIntent?.(source, false);
|
|
4829
|
-
|
|
4830
|
-
const next = this.#beginLocalPause(ports, source);
|
|
4831
|
-
state.pause = previous ? Promise.allSettled([previous, next]).then((results) => {
|
|
4832
|
-
for (const result of results) if (result.status === "rejected") throw result.reason;
|
|
4833
|
-
}) : next;
|
|
4834
|
-
void state.pause.catch(() => void 0);
|
|
5226
|
+
ports.media.setLocalSourceIntent?.(source, false, "user_pause");
|
|
5227
|
+
this.#retainPause(state, this.#beginLocalPause(ports, source, "user_pause"));
|
|
4835
5228
|
}
|
|
4836
5229
|
}
|
|
4837
5230
|
for (const check of this.#readinessChecks) check();
|
|
@@ -4839,6 +5232,7 @@ var MediaControllerRuntime = class {
|
|
|
4839
5232
|
}
|
|
4840
5233
|
#act(source, enabled, options) {
|
|
4841
5234
|
return Effect5.suspend(() => {
|
|
5235
|
+
if (this.#ports?.media.getSnapshot().connection.phase === "stopped") return Effect5.void;
|
|
4842
5236
|
const desired = this.#recordIntent(source, enabled, options);
|
|
4843
5237
|
const ready = enabled && source !== "screen" && this.#store.getSnapshot().connection.status !== "live" ? this.#waitForLive(source, desired.revision).pipe(
|
|
4844
5238
|
Effect5.tapError(
|
|
@@ -4923,7 +5317,16 @@ var MediaControllerRuntime = class {
|
|
|
4923
5317
|
const state = this.#sources[source];
|
|
4924
5318
|
if (!intent.enabled && ports.sync.getSnapshot().connection.phase !== "live") return;
|
|
4925
5319
|
const previous = intent.previousEnabled;
|
|
4926
|
-
if (state.desired.revision
|
|
5320
|
+
if (state.desired.revision !== intent.revision) return;
|
|
5321
|
+
state.result = { revision: intent.revision, status: "failed", cause, cleanup: !intent.enabled || !previous };
|
|
5322
|
+
if (state.result.cleanup) {
|
|
5323
|
+
if (intent.enabled && source !== "screen") this.#pausePublication(ports, source, intent.revision, "enable_failure");
|
|
5324
|
+
else {
|
|
5325
|
+
state.localEnabled = false;
|
|
5326
|
+
this.#pauseTracks(source);
|
|
5327
|
+
ports.media.setLocalSourceIntent?.(source, false, "enable_failure");
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
4927
5330
|
});
|
|
4928
5331
|
}
|
|
4929
5332
|
#prepareDevice(ports, source, intent) {
|
|
@@ -5033,21 +5436,26 @@ var MediaControllerRuntime = class {
|
|
|
5033
5436
|
})
|
|
5034
5437
|
);
|
|
5035
5438
|
}
|
|
5036
|
-
#assertCurrentIntent(source, revision) {
|
|
5037
|
-
|
|
5439
|
+
#assertCurrentIntent(source, revision, enabling = true) {
|
|
5440
|
+
const state = this.#sources[source];
|
|
5441
|
+
if (state.desired.revision !== revision || enabling && state.forcedRevision === revision) throw this.#cancelledCaptureError();
|
|
5442
|
+
}
|
|
5443
|
+
#hasConsent(source) {
|
|
5444
|
+
const state = this.#sources[source];
|
|
5445
|
+
return state.desired.enabled && state.forcedRevision !== state.desired.revision;
|
|
5038
5446
|
}
|
|
5039
5447
|
#resetObservations() {
|
|
5448
|
+
if (this.#connection.getSnapshot().state !== "reconnecting") this.#recovering = null;
|
|
5040
5449
|
for (const source of MEDIA_SOURCES) {
|
|
5041
5450
|
const state = this.#sources[source];
|
|
5042
5451
|
state.result = { revision: state.desired.revision, status: "confirmed" };
|
|
5043
5452
|
state.pause = null;
|
|
5044
5453
|
state.serverSequence = -1;
|
|
5045
|
-
state.forcedRevision = null;
|
|
5046
5454
|
}
|
|
5047
5455
|
}
|
|
5048
5456
|
#pauseInitialTracks() {
|
|
5049
5457
|
for (const source of ["microphone", "camera"]) {
|
|
5050
|
-
const enabled = this.#
|
|
5458
|
+
const enabled = this.#hasConsent(source);
|
|
5051
5459
|
const track = this.#track(source);
|
|
5052
5460
|
if (track && !enabled) track.enabled = false;
|
|
5053
5461
|
this.#sources[source].localEnabled = !!track && enabled;
|
|
@@ -5058,15 +5466,14 @@ var MediaControllerRuntime = class {
|
|
|
5058
5466
|
const revisions = { microphone: this.#sources.microphone.initialRevision, camera: this.#sources.camera.initialRevision };
|
|
5059
5467
|
const tracks = stream.getTracks().filter((track) => {
|
|
5060
5468
|
const source = track.kind === "audio" ? "microphone" : "camera";
|
|
5061
|
-
const
|
|
5062
|
-
track.enabled =
|
|
5063
|
-
if (!
|
|
5469
|
+
const enabled = this.#hasConsent(source);
|
|
5470
|
+
track.enabled = enabled;
|
|
5471
|
+
if (!enabled) {
|
|
5064
5472
|
this.#deleteTrack(source);
|
|
5065
5473
|
if (source === "microphone") track.stop();
|
|
5066
5474
|
}
|
|
5067
|
-
return
|
|
5475
|
+
return enabled;
|
|
5068
5476
|
});
|
|
5069
|
-
for (const source of ["microphone", "camera"]) this.#sources[source].sentRevision = revisions[source];
|
|
5070
5477
|
await media.start(streamFromTracks(tracks));
|
|
5071
5478
|
for (const source of ["microphone", "camera"]) {
|
|
5072
5479
|
const state = this.#sources[source];
|
|
@@ -5079,11 +5486,10 @@ var MediaControllerRuntime = class {
|
|
|
5079
5486
|
const command = this.#mediaAction(
|
|
5080
5487
|
enabled,
|
|
5081
5488
|
foreign2(async () => {
|
|
5082
|
-
this.#assertCurrentIntent(source, intent.revision);
|
|
5489
|
+
this.#assertCurrentIntent(source, intent.revision, enabled);
|
|
5083
5490
|
this.#assertActivePorts(ports, source === "screen" ? "startScreenShare" : source === "camera" ? "setCameraEnabled" : "setMicrophoneEnabled");
|
|
5084
|
-
const
|
|
5085
|
-
|
|
5086
|
-
await (source === "microphone" ? ports.sync.setMicrophoneEnabled(enabled) : source === "camera" ? ports.sync.setCameraEnabled(enabled) : ports.sync.setScreenShareEnabled(enabled));
|
|
5491
|
+
const send = source === "microphone" ? ports.sync.setMicrophoneEnabled : source === "camera" ? ports.sync.setCameraEnabled : ports.sync.setScreenShareEnabled;
|
|
5492
|
+
await send.call(ports.sync, enabled);
|
|
5087
5493
|
}).pipe(
|
|
5088
5494
|
Effect5.andThen(enabled && source !== "screen" ? this.#waitForLive(source, intent.revision, ports) : Effect5.void),
|
|
5089
5495
|
Effect5.tap(() => Effect5.try({ try: () => this.#assertActivePorts(ports, source === "screen" ? "startScreenShare" : source === "camera" ? "setCameraEnabled" : "setMicrophoneEnabled"), catch: (cause) => cause }))
|
|
@@ -5093,10 +5499,19 @@ var MediaControllerRuntime = class {
|
|
|
5093
5499
|
}
|
|
5094
5500
|
#reconcileIntents() {
|
|
5095
5501
|
const live = this.#connection.getSnapshot().state === "live" && this.#ports?.sync.getSnapshot().connection.phase === "live";
|
|
5502
|
+
if (this.#syncLive && !live) this.#rememberRecoveryIntents();
|
|
5096
5503
|
const resumed = live && !this.#syncLive;
|
|
5097
5504
|
this.#syncLive = live;
|
|
5098
5505
|
if (!resumed) return;
|
|
5099
|
-
|
|
5506
|
+
if (this.#ports) this.#pauseRecoveredIntents(this.#ports, this.#ports.sync.getSnapshot());
|
|
5507
|
+
this.#resumePendingActions();
|
|
5508
|
+
}
|
|
5509
|
+
#resumePendingActions() {
|
|
5510
|
+
for (const source of MEDIA_SOURCES) {
|
|
5511
|
+
const state = this.#sources[source];
|
|
5512
|
+
if (source !== "screen" && (state.desired.enabled || state.result.revision === state.desired.revision)) continue;
|
|
5513
|
+
this.#fork(this.#serialize(source, this.#work(source)).pipe(Effect5.ignore));
|
|
5514
|
+
}
|
|
5100
5515
|
}
|
|
5101
5516
|
#clearScreenAudio(media) {
|
|
5102
5517
|
return foreign2(() => media.clearPreparedLocalTrack("screen_audio")).pipe(
|
|
@@ -5108,8 +5523,9 @@ var MediaControllerRuntime = class {
|
|
|
5108
5523
|
)
|
|
5109
5524
|
);
|
|
5110
5525
|
}
|
|
5111
|
-
#beginLocalPause(ports, source) {
|
|
5112
|
-
|
|
5526
|
+
#beginLocalPause(ports, source, cause) {
|
|
5527
|
+
this.#sources[source].pausedPublicationId = ports.media.getSnapshot().localTracks.find((item) => item.source === source)?.publicationId ?? null;
|
|
5528
|
+
const pause = ports.media.closeForcedLocalPublication ? Promise.resolve().then(() => ports.media.closeForcedLocalPublication(source, cause)) : Effect5.runPromise(this.#localTarget(ports, source, false));
|
|
5113
5529
|
void pause.catch(() => void 0);
|
|
5114
5530
|
return pause;
|
|
5115
5531
|
}
|
|
@@ -5178,18 +5594,53 @@ var MediaControllerRuntime = class {
|
|
|
5178
5594
|
#setTrack(source, track) {
|
|
5179
5595
|
if (source !== "camera") {
|
|
5180
5596
|
this.#tracks.set(source, track);
|
|
5597
|
+
if (source === "microphone") this.#watchMicrophone(track);
|
|
5181
5598
|
return;
|
|
5182
5599
|
}
|
|
5183
5600
|
const state = this.#camera;
|
|
5184
|
-
if (!("owner" in state)) throw cancelledCaptureError();
|
|
5185
|
-
if (state.status === "live" && state.track !== track) throw cancelledCaptureError();
|
|
5186
|
-
const onEnded = () =>
|
|
5187
|
-
if (this.#camera.status === "live" && this.#camera.track === track) this.#transitionCamera({ status: "ended" });
|
|
5188
|
-
};
|
|
5601
|
+
if (!("owner" in state)) throw this.#cancelledCaptureError();
|
|
5602
|
+
if (state.status === "live" && state.track !== track) throw this.#cancelledCaptureError();
|
|
5603
|
+
const onEnded = () => this.#handleCameraEnded(track);
|
|
5189
5604
|
this.#transitionCamera({ status: "live", owner: state.owner, track, publication: this.#ports?.media ?? "initial", onEnded });
|
|
5190
5605
|
}
|
|
5606
|
+
#handleCameraEnded(track) {
|
|
5607
|
+
const state = this.#camera;
|
|
5608
|
+
if (state.status !== "live" || state.track !== track) return;
|
|
5609
|
+
if (this.#ports && state.publication === this.#ports.media) this.#pausePublication(this.#ports, "camera", this.#sources.camera.desired.revision, "capture_cleanup");
|
|
5610
|
+
this.#transitionCamera({ status: "ended" });
|
|
5611
|
+
}
|
|
5612
|
+
#watchMicrophone(track) {
|
|
5613
|
+
if (this.#microphoneWatch?.track === track) return;
|
|
5614
|
+
this.#microphoneWatch?.unsubscribe();
|
|
5615
|
+
this.#microphoneWatch = null;
|
|
5616
|
+
if (!track) return;
|
|
5617
|
+
const changed = () => {
|
|
5618
|
+
if (this.#microphoneWatch?.track !== track) return;
|
|
5619
|
+
if (track.muted || track.readyState === "ended") {
|
|
5620
|
+
track.enabled = false;
|
|
5621
|
+
const state = this.#sources.microphone;
|
|
5622
|
+
if (this.#ports) this.#pausePublication(this.#ports, "microphone", state.desired.revision, "capture_cleanup");
|
|
5623
|
+
else {
|
|
5624
|
+
state.forcedRevision = state.desired.revision;
|
|
5625
|
+
state.localEnabled = false;
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
this.#publish();
|
|
5629
|
+
};
|
|
5630
|
+
for (const event of ["mute", "unmute", "ended"]) track.addEventListener(event, changed);
|
|
5631
|
+
this.#microphoneWatch = {
|
|
5632
|
+
track,
|
|
5633
|
+
unsubscribe: () => {
|
|
5634
|
+
for (const event of ["mute", "unmute", "ended"]) track.removeEventListener(event, changed);
|
|
5635
|
+
}
|
|
5636
|
+
};
|
|
5637
|
+
if (track.muted || track.readyState === "ended") changed();
|
|
5638
|
+
}
|
|
5191
5639
|
#deleteTrack(source) {
|
|
5192
5640
|
if (source !== "camera") {
|
|
5641
|
+
if (source === "microphone") {
|
|
5642
|
+
this.#watchMicrophone(null);
|
|
5643
|
+
}
|
|
5193
5644
|
this.#tracks.delete(source);
|
|
5194
5645
|
return;
|
|
5195
5646
|
}
|
|
@@ -5200,6 +5651,7 @@ var MediaControllerRuntime = class {
|
|
|
5200
5651
|
} else this.#transitionCamera({ status: "ended" });
|
|
5201
5652
|
}
|
|
5202
5653
|
#clearTracks() {
|
|
5654
|
+
this.#watchMicrophone(null);
|
|
5203
5655
|
this.#tracks.clear();
|
|
5204
5656
|
this.#transitionCamera({ status: "idle" });
|
|
5205
5657
|
}
|
|
@@ -5242,11 +5694,11 @@ var MediaControllerRuntime = class {
|
|
|
5242
5694
|
#beginCameraCapture(acquire) {
|
|
5243
5695
|
const abort = new AbortController();
|
|
5244
5696
|
const owner = {
|
|
5245
|
-
result:
|
|
5697
|
+
result: Deferred3.makeUnsafe(),
|
|
5246
5698
|
references: 0,
|
|
5247
5699
|
cancel: () => {
|
|
5248
5700
|
abort.abort();
|
|
5249
|
-
|
|
5701
|
+
Deferred3.doneUnsafe(owner.result, Effect5.fail(this.#cancelledCaptureError()));
|
|
5250
5702
|
}
|
|
5251
5703
|
};
|
|
5252
5704
|
this.#transitionCamera({ status: "capturing", owner });
|
|
@@ -5258,21 +5710,19 @@ var MediaControllerRuntime = class {
|
|
|
5258
5710
|
const state = this.#camera;
|
|
5259
5711
|
if (abort.signal.aborted || !("owner" in state) || state.owner !== owner) {
|
|
5260
5712
|
track.stop();
|
|
5261
|
-
throw cancelledCaptureError();
|
|
5713
|
+
throw this.#cancelledCaptureError();
|
|
5262
5714
|
}
|
|
5263
|
-
const onEnded = () =>
|
|
5264
|
-
if (this.#camera.status === "live" && this.#camera.owner === owner) this.#transitionCamera({ status: "ended" });
|
|
5265
|
-
};
|
|
5715
|
+
const onEnded = () => this.#handleCameraEnded(track);
|
|
5266
5716
|
this.#transitionCamera({ status: "live", owner, track, publication: state.status === "live" ? state.publication : null, onEnded });
|
|
5267
5717
|
if (track.readyState !== "live") {
|
|
5268
5718
|
this.#transitionCamera({ status: "ended" });
|
|
5269
|
-
throw cancelledCaptureError();
|
|
5719
|
+
throw this.#cancelledCaptureError();
|
|
5270
5720
|
}
|
|
5271
5721
|
},
|
|
5272
5722
|
catch: (cause) => cause
|
|
5273
5723
|
})
|
|
5274
5724
|
),
|
|
5275
|
-
Effect5.onExit((exit) =>
|
|
5725
|
+
Effect5.onExit((exit) => Deferred3.done(owner.result, exit)),
|
|
5276
5726
|
Effect5.ignore
|
|
5277
5727
|
)
|
|
5278
5728
|
);
|
|
@@ -5288,8 +5738,8 @@ var MediaControllerRuntime = class {
|
|
|
5288
5738
|
const current = this.#camera;
|
|
5289
5739
|
if (owner.references === 0 && "owner" in current && current.owner === owner && (current.status !== "live" || current.publication === null)) this.#transitionCamera({ status: "ended" });
|
|
5290
5740
|
};
|
|
5291
|
-
return
|
|
5292
|
-
Effect5.flatMap((track) => track.readyState === "live" ? Effect5.succeed({ track, release }) : Effect5.fail(cancelledCaptureError())),
|
|
5741
|
+
return Deferred3.await(owner.result).pipe(
|
|
5742
|
+
Effect5.flatMap((track) => track.readyState === "live" ? Effect5.succeed({ track, release }) : Effect5.fail(this.#cancelledCaptureError())),
|
|
5293
5743
|
Effect5.onExit(
|
|
5294
5744
|
(exit) => Effect5.sync(() => {
|
|
5295
5745
|
if (Exit2.isFailure(exit)) release();
|
|
@@ -5318,26 +5768,29 @@ var MediaControllerRuntime = class {
|
|
|
5318
5768
|
return Effect5.suspend(() => {
|
|
5319
5769
|
let cancelled = false;
|
|
5320
5770
|
let captured = null;
|
|
5771
|
+
const cancellation = Deferred3.makeUnsafe();
|
|
5321
5772
|
const cancel = () => {
|
|
5322
5773
|
cancelled = true;
|
|
5323
5774
|
stopStream(captured);
|
|
5775
|
+
Deferred3.doneUnsafe(cancellation, Effect5.fail(this.#cancelledCaptureError()));
|
|
5324
5776
|
};
|
|
5325
5777
|
this.#pendingCaptures.add(cancel);
|
|
5326
5778
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
5327
5779
|
if (signal?.aborted) cancel();
|
|
5328
5780
|
return foreign2(async () => {
|
|
5329
|
-
if (cancelled) throw cancelledCaptureError();
|
|
5781
|
+
if (cancelled) throw this.#cancelledCaptureError();
|
|
5330
5782
|
const stream = await operation();
|
|
5331
5783
|
captured = stream;
|
|
5332
|
-
if (cancelled) {
|
|
5784
|
+
if (cancelled || this.#ports?.media.getSnapshot().connection.phase === "stopped") {
|
|
5333
5785
|
stopStream(stream);
|
|
5334
|
-
throw cancelledCaptureError();
|
|
5786
|
+
throw this.#cancelledCaptureError();
|
|
5335
5787
|
}
|
|
5336
5788
|
return stream;
|
|
5337
5789
|
}).pipe(
|
|
5338
5790
|
Effect5.mapError(onCaptureError),
|
|
5339
5791
|
Effect5.flatMap(use),
|
|
5340
|
-
Effect5.
|
|
5792
|
+
Effect5.raceFirst(Deferred3.await(cancellation)),
|
|
5793
|
+
Effect5.tap(() => cancelled ? Effect5.fail(this.#cancelledCaptureError()) : Effect5.void),
|
|
5341
5794
|
Effect5.onExit(
|
|
5342
5795
|
(exit) => Effect5.sync(() => {
|
|
5343
5796
|
this.#pendingCaptures.delete(cancel);
|
|
@@ -5352,6 +5805,9 @@ var MediaControllerRuntime = class {
|
|
|
5352
5805
|
this.#transitionCamera({ status: "ended" });
|
|
5353
5806
|
for (const cancel of this.#pendingCaptures) cancel();
|
|
5354
5807
|
this.#pendingCaptures.clear();
|
|
5808
|
+
this.#screenAudioTrack?.stop();
|
|
5809
|
+
this.#screenAudioTrack = null;
|
|
5810
|
+
for (const track of this.#tracks.values()) track.stop();
|
|
5355
5811
|
}
|
|
5356
5812
|
#captureSource(source, signal) {
|
|
5357
5813
|
return this.#captureStream(
|
|
@@ -5408,42 +5864,71 @@ var MediaControllerRuntime = class {
|
|
|
5408
5864
|
const failure2 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
|
|
5409
5865
|
return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(failure2)));
|
|
5410
5866
|
}
|
|
5867
|
+
#cancelledCaptureError() {
|
|
5868
|
+
return new ConnectionError({ code: "invalid_state", action: null, recoverable: !this.#disposed, message: this.#disposed ? "This media client has been disposed. Rejoin the call." : "Media capture was cancelled before its result could be used" });
|
|
5869
|
+
}
|
|
5870
|
+
#canUsePorts(ports) {
|
|
5871
|
+
return this.#ports?.sync === ports.sync && this.#ports.media === ports.media && ports.media.getSnapshot().connection.phase !== "stopped";
|
|
5872
|
+
}
|
|
5411
5873
|
#assertActivePorts(ports, action) {
|
|
5412
|
-
if (this.#connection.getSnapshot().state === "live" && this.#ports
|
|
5413
|
-
throw new ConnectionError({ code: "invalid_state", action, recoverable:
|
|
5874
|
+
if (this.#connection.getSnapshot().state === "live" && this.#canUsePorts(ports)) return;
|
|
5875
|
+
throw new ConnectionError({ code: "invalid_state", action, recoverable: !this.#disposed, message: this.#disposed ? "This media client has been disposed. Rejoin the call." : `${action} belongs to an inactive connection` });
|
|
5414
5876
|
}
|
|
5415
|
-
#pausePublication(ports, source, revision) {
|
|
5877
|
+
#pausePublication(ports, source, revision, cause) {
|
|
5416
5878
|
const state = this.#sources[source];
|
|
5417
|
-
if (state.desired.revision !== revision || ports.sync.getSnapshot().control?.status === "ended") return;
|
|
5879
|
+
if (state.desired.revision !== revision || state.forcedRevision === revision || ports.sync.getSnapshot().control?.status === "ended") return;
|
|
5418
5880
|
state.forcedRevision = revision;
|
|
5419
5881
|
state.localEnabled = false;
|
|
5420
5882
|
this.#pauseTracks(source);
|
|
5421
|
-
ports.media.
|
|
5422
|
-
|
|
5883
|
+
if (cause === "capture_cleanup" && source !== "screen" && ports.media.pauseLocalCapture) {
|
|
5884
|
+
state.capturePause = "pending";
|
|
5885
|
+
this.#retainPause(state, ports.media.pauseLocalCapture(source));
|
|
5886
|
+
this.#notifyCapturePauses(ports);
|
|
5887
|
+
} else {
|
|
5888
|
+
ports.media.setLocalSourceIntent?.(source, false, cause);
|
|
5889
|
+
this.#retainPause(state, this.#beginLocalPause(ports, source, cause));
|
|
5890
|
+
}
|
|
5423
5891
|
this.#publish();
|
|
5424
5892
|
}
|
|
5893
|
+
#notifyCapturePauses(ports) {
|
|
5894
|
+
for (const source of ["microphone", "camera"]) {
|
|
5895
|
+
const state = this.#sources[source];
|
|
5896
|
+
if (state.capturePause === "pending") this.#retainPause(state, this.#announceCapturePause(ports, source));
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
async #announceCapturePause(ports, source) {
|
|
5900
|
+
const snapshot = ports.sync.getSnapshot();
|
|
5901
|
+
if (snapshot.connection.phase !== "live") return;
|
|
5902
|
+
const owned = ports.media.getSnapshot().localTracks.find((item) => item.source === source);
|
|
5903
|
+
const projected = snapshot.media?.items.find((item) => item.participantId === snapshot.participantId && item.source === source);
|
|
5904
|
+
if (!owned?.publicationId || !projected?.enabled || projected.publicationId !== owned.publicationId) return;
|
|
5905
|
+
this.#sources[source].capturePause = "announced";
|
|
5906
|
+
const send = source === "microphone" ? ports.sync.setMicrophoneEnabled : ports.sync.setCameraEnabled;
|
|
5907
|
+
await send.call(ports.sync, false);
|
|
5908
|
+
}
|
|
5425
5909
|
#bind(ports) {
|
|
5910
|
+
if (ports?.media.getSnapshot().connection.phase === "stopped") this.#cancelCaptures();
|
|
5426
5911
|
if (ports && this.#ports?.sync === ports.sync && this.#ports.media === ports.media) return;
|
|
5912
|
+
this.#rememberRecoveryIntents();
|
|
5427
5913
|
for (const unsubscribe of [this.#unsubscribeMedia, this.#unsubscribeRequests, this.#unsubscribePause, this.#unsubscribeSync]) unsubscribe?.();
|
|
5428
5914
|
this.#unsubscribeMedia = null;
|
|
5429
5915
|
this.#unsubscribeRequests = null;
|
|
5430
5916
|
this.#unsubscribePause = null;
|
|
5431
5917
|
this.#unsubscribeSync = null;
|
|
5432
5918
|
this.#ports = ports;
|
|
5919
|
+
this.#projection.reset();
|
|
5433
5920
|
this.#syncLive = false;
|
|
5434
5921
|
this.#bindCamera(ports);
|
|
5435
5922
|
if (!ports) {
|
|
5436
5923
|
this.#resetObservations();
|
|
5437
5924
|
this.#cancelCaptures();
|
|
5438
|
-
this.#screenAudioTrack?.stop();
|
|
5439
|
-
this.#screenAudioTrack = null;
|
|
5440
|
-
for (const track of this.#tracks.values()) track.stop();
|
|
5441
5925
|
this.#clearTracks();
|
|
5442
5926
|
this.#clearRequests();
|
|
5443
5927
|
this.#publish();
|
|
5444
5928
|
return;
|
|
5445
5929
|
}
|
|
5446
5930
|
this.#unsubscribeMedia = ports.media.subscribe(() => {
|
|
5931
|
+
if (ports.media.getSnapshot().connection.phase === "stopped") this.#cancelCaptures();
|
|
5447
5932
|
this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
|
|
5448
5933
|
this.#publish();
|
|
5449
5934
|
});
|
|
@@ -5458,41 +5943,29 @@ var MediaControllerRuntime = class {
|
|
|
5458
5943
|
const state = this.#sources[pause.source];
|
|
5459
5944
|
if (pause.revision <= state.serverSequence) return;
|
|
5460
5945
|
state.serverSequence = pause.revision;
|
|
5461
|
-
this.#pausePublication(ports, pause.source, state.
|
|
5946
|
+
if (pause.revision > state.desired.controlRevision) this.#pausePublication(ports, pause.source, state.desired.revision, "moderation_pause");
|
|
5462
5947
|
}) ?? null;
|
|
5463
5948
|
}
|
|
5464
|
-
#
|
|
5465
|
-
const
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
if (item.source === "screen_audio" || item.publicationId === null) continue;
|
|
5469
|
-
const state = this.#sources[item.source];
|
|
5470
|
-
if (state.localEnabled && state.sentRevision === state.desired.revision) publications.set(item.source, { revision: state.sentRevision, publicationId: item.publicationId });
|
|
5471
|
-
}
|
|
5472
|
-
return publications;
|
|
5949
|
+
#rememberRecoveryIntents() {
|
|
5950
|
+
for (const state of Object.values(this.#sources)) if (state.capturePause === "announced") state.capturePause = "pending";
|
|
5951
|
+
if (!this.#ports) return;
|
|
5952
|
+
this.#recovering ??= new Map(MEDIA_SOURCES.filter((source) => this.#sources[source].desired.enabled).map((source) => [source, this.#sources[source].desired.revision]));
|
|
5473
5953
|
}
|
|
5474
|
-
#
|
|
5475
|
-
|
|
5954
|
+
#pauseRecoveredIntents(ports, snapshot) {
|
|
5955
|
+
const recovering = this.#recovering;
|
|
5956
|
+
this.#recovering = null;
|
|
5957
|
+
for (const [source, revision] of recovering ?? []) {
|
|
5476
5958
|
const current = snapshot.media?.items.find((item) => item.participantId === snapshot.participantId && item.source === source);
|
|
5477
|
-
if (!current
|
|
5959
|
+
if (!current?.enabled) this.#pausePublication(ports, source, revision, "sync_recovery_loss");
|
|
5478
5960
|
}
|
|
5479
5961
|
}
|
|
5480
5962
|
#subscribeSync(ports) {
|
|
5481
|
-
let recovering = /* @__PURE__ */ new Map();
|
|
5482
|
-
let lastLive = ports.sync.getSnapshot();
|
|
5483
|
-
let wasLive = lastLive.connection.phase === "live";
|
|
5484
5963
|
return ports.sync.subscribe((snapshot) => {
|
|
5485
|
-
const live = snapshot.connection.phase === "live";
|
|
5486
|
-
if (wasLive && !live) recovering = this.#rememberPublications(lastLive);
|
|
5487
|
-
if (live && !wasLive) {
|
|
5488
|
-
this.#pauseLostPublications(ports, snapshot, recovering);
|
|
5489
|
-
recovering.clear();
|
|
5490
|
-
}
|
|
5491
|
-
if (live) lastLive = snapshot;
|
|
5492
|
-
wasLive = live;
|
|
5493
5964
|
this.#removeRequestsFromMissingParticipants(snapshot);
|
|
5494
5965
|
this.#applyRemoteMediaState(ports, snapshot);
|
|
5495
5966
|
this.#reconcileIntents();
|
|
5967
|
+
this.#notifyCapturePauses(ports);
|
|
5968
|
+
this.#publish();
|
|
5496
5969
|
});
|
|
5497
5970
|
}
|
|
5498
5971
|
#bindCamera(ports) {
|
|
@@ -5566,14 +6039,64 @@ var MediaControllerRuntime = class {
|
|
|
5566
6039
|
if (!track || this.#track("screen") !== track) return;
|
|
5567
6040
|
this.#fork(this.#stopScreen().pipe(Effect5.ignore));
|
|
5568
6041
|
}
|
|
6042
|
+
#observeProjections() {
|
|
6043
|
+
const ports = this.#ports;
|
|
6044
|
+
this.#watchMicrophone(ports?.media.getSnapshot().localTracks.find((item) => item.source === "microphone")?.track ?? this.#track("microphone") ?? null);
|
|
6045
|
+
if (!ports) {
|
|
6046
|
+
this.#projection.reset();
|
|
6047
|
+
return;
|
|
6048
|
+
}
|
|
6049
|
+
this.#enforcePause(ports, "microphone");
|
|
6050
|
+
this.#enforcePause(ports, "camera");
|
|
6051
|
+
if (!this.#syncLive) {
|
|
6052
|
+
this.#projection.reset();
|
|
6053
|
+
return;
|
|
6054
|
+
}
|
|
6055
|
+
this.#observeProjection(ports, "microphone");
|
|
6056
|
+
this.#observeProjection(ports, "camera");
|
|
6057
|
+
}
|
|
6058
|
+
#enforcePause(ports, source) {
|
|
6059
|
+
const publication = ports.media.getSnapshot().localTracks.find((item) => item.source === source);
|
|
6060
|
+
const state = this.#sources[source];
|
|
6061
|
+
if (!this.#hasConsent(source) && publication?.enabled && state.pausedPublicationId !== publication.publicationId) {
|
|
6062
|
+
this.#pauseTracks(source);
|
|
6063
|
+
ports.media.setLocalSourceIntent?.(source, false, "consent_revoked");
|
|
6064
|
+
this.#retainPause(state, this.#beginLocalPause(ports, source, "consent_revoked"));
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
#observeProjection(ports, source) {
|
|
6068
|
+
const sync = ports.sync.getSnapshot();
|
|
6069
|
+
const state = this.#sources[source];
|
|
6070
|
+
const publication = ports.media.getSnapshot().localTracks.find((item) => item.source === source);
|
|
6071
|
+
const projection = sync.media?.items.find((item) => item.participantId === sync.participantId && item.source === source) ?? { enabled: false, publicationId: null };
|
|
6072
|
+
this.#projection.observe(source, {
|
|
6073
|
+
revision: state.desired.revision,
|
|
6074
|
+
publicationId: publication?.publicationId ?? null,
|
|
6075
|
+
enabled: livePublication(publication),
|
|
6076
|
+
projectedEnabled: projection.enabled,
|
|
6077
|
+
projectedPublicationId: projection.publicationId
|
|
6078
|
+
});
|
|
6079
|
+
}
|
|
6080
|
+
#confirmedLocal(source) {
|
|
6081
|
+
const snapshot = this.#ports?.media.getSnapshot();
|
|
6082
|
+
if (snapshot?.connection.phase === "stopped") return { source, state: "unavailable", track: null };
|
|
6083
|
+
const publication = snapshot?.localTracks.find((item) => item.source === source);
|
|
6084
|
+
const sync = this.#ports?.sync.getSnapshot();
|
|
6085
|
+
const projected = sync?.media?.items.some((item) => item.participantId === sync.participantId && item.source === source && item.enabled && item.publicationId === publication?.publicationId) ?? false;
|
|
6086
|
+
const confirmed = this.#syncLive && livePublication(publication) && projected;
|
|
6087
|
+
const pending = this.#pendingSources.has(source) || this.#connection.getSnapshot().state === "joining";
|
|
6088
|
+
const track = publication?.track ?? this.#track(source) ?? null;
|
|
6089
|
+
return { source, track, state: confirmedLocalMediaState(track, this.#hasConsent(source), pending, confirmed, this.#syncLive, this.#projection.state(source)) };
|
|
6090
|
+
}
|
|
5569
6091
|
#publish() {
|
|
5570
6092
|
const current = this.#store.getSnapshot();
|
|
5571
6093
|
const snapshot = this.#ports?.media.getSnapshot();
|
|
5572
6094
|
const state = this.#connection.getSnapshot().state;
|
|
6095
|
+
this.#observeProjections();
|
|
5573
6096
|
const local = Object.freeze({
|
|
5574
|
-
microphone:
|
|
5575
|
-
camera:
|
|
5576
|
-
screen:
|
|
6097
|
+
microphone: this.#confirmedLocal("microphone"),
|
|
6098
|
+
camera: this.#confirmedLocal("camera"),
|
|
6099
|
+
screen: screenLocalMedia(this.#track("screen") ?? null, snapshot, state, this.#pendingSources.has("screen") && this.#sources.screen.desired.enabled)
|
|
5577
6100
|
});
|
|
5578
6101
|
const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
|
|
5579
6102
|
const media = Object.freeze({
|
|
@@ -5590,65 +6113,52 @@ var MediaControllerRuntime = class {
|
|
|
5590
6113
|
if (!sameMediaSlice(current.media, media)) this.#store.updateMedia(media);
|
|
5591
6114
|
}
|
|
5592
6115
|
};
|
|
5593
|
-
function
|
|
5594
|
-
return
|
|
6116
|
+
function liveTrack(track) {
|
|
6117
|
+
return track.enabled && track.readyState === "live" && !track.muted;
|
|
6118
|
+
}
|
|
6119
|
+
function livePublication(publication) {
|
|
6120
|
+
return !!publication?.enabled && liveTrack(publication.track);
|
|
6121
|
+
}
|
|
6122
|
+
function confirmedLocalMediaState(track, desired, pending, confirmed, live, convergence) {
|
|
6123
|
+
if (!desired) return convergence === "failed" ? "failed" : "disabled";
|
|
6124
|
+
if (convergence) return convergence;
|
|
6125
|
+
if (!live && track && liveTrack(track)) return "requesting";
|
|
6126
|
+
return confirmed ? "enabled" : pending ? "requesting" : "disabled";
|
|
5595
6127
|
}
|
|
5596
6128
|
function foreign2(operation) {
|
|
5597
6129
|
return Effect5.tryPromise({ try: operation, catch: (cause) => cause });
|
|
5598
6130
|
}
|
|
5599
6131
|
function selectInitialTracks(stream, intent) {
|
|
5600
6132
|
const tracks = stream.getTracks();
|
|
5601
|
-
const
|
|
5602
|
-
const
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
6133
|
+
const selected = /* @__PURE__ */ new Map();
|
|
6134
|
+
for (const [source, kind] of [
|
|
6135
|
+
["microphone", "audio"],
|
|
6136
|
+
["camera", "video"]
|
|
6137
|
+
]) {
|
|
6138
|
+
if (!intent[source]) continue;
|
|
6139
|
+
const track = tracks.find((track2) => track2.kind === kind);
|
|
6140
|
+
if (track) selected.set(source, track);
|
|
6141
|
+
}
|
|
6142
|
+
const retained = new Set(selected.values());
|
|
6143
|
+
for (const track of tracks) if (!retained.has(track)) track.stop();
|
|
6144
|
+
if (selected.size !== Number(intent.microphone) + Number(intent.camera)) {
|
|
6145
|
+
for (const track of retained) track.stop();
|
|
5607
6146
|
throw new TypeError("Media capture did not return every requested track");
|
|
5608
6147
|
}
|
|
5609
|
-
return
|
|
5610
|
-
}
|
|
5611
|
-
function initialTrackFor(tracks, enabled, kind) {
|
|
5612
|
-
if (!enabled) return void 0;
|
|
5613
|
-
return tracks.find((track) => track.kind === kind);
|
|
5614
|
-
}
|
|
5615
|
-
function initialTracks(microphone, camera) {
|
|
5616
|
-
return [microphone, camera].filter((track) => track !== void 0);
|
|
5617
|
-
}
|
|
5618
|
-
function missingInitialTrack(intent, microphone, camera) {
|
|
5619
|
-
return intent.microphone && !microphone || intent.camera && !camera;
|
|
5620
|
-
}
|
|
5621
|
-
function initialTrackEntries(microphone, camera) {
|
|
5622
|
-
const entries = [];
|
|
5623
|
-
if (microphone) entries.push(["microphone", microphone]);
|
|
5624
|
-
if (camera) entries.push(["camera", camera]);
|
|
5625
|
-
return entries;
|
|
5626
|
-
}
|
|
5627
|
-
function localMedia(source, tracks, snapshot, intended, connectionState, pending) {
|
|
5628
|
-
const track = tracks.get(source) ?? null;
|
|
5629
|
-
const publication = snapshot?.localTracks.find((candidate) => candidate.source === source);
|
|
5630
|
-
if (pending) return Object.freeze({ source, state: "requesting", track });
|
|
5631
|
-
if (locallyMuted(intended, track, publication)) return Object.freeze({ source, state: "disabled", track });
|
|
5632
|
-
if (publication?.enabled) return Object.freeze({ source, state: "enabled", track });
|
|
5633
|
-
if (publication && connectionState === "live") return Object.freeze({ source, state: "disabled", track });
|
|
5634
|
-
return Object.freeze({ source, state: localMediaState(source, track, intended, connectionState), track });
|
|
6148
|
+
return selected;
|
|
5635
6149
|
}
|
|
5636
|
-
function
|
|
5637
|
-
return
|
|
6150
|
+
function screenLocalMedia(track, snapshot, connectionState, pending) {
|
|
6151
|
+
if (snapshot?.connection.phase === "stopped") return Object.freeze({ source: "screen", state: "unavailable", track: null });
|
|
6152
|
+
const publication = snapshot?.localTracks.find((candidate) => candidate.source === "screen");
|
|
6153
|
+
return Object.freeze({ source: "screen", state: pending ? "requesting" : screenLocalMediaState(track, publication?.enabled, connectionState), track });
|
|
5638
6154
|
}
|
|
5639
|
-
function
|
|
5640
|
-
if (
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
return
|
|
5646
|
-
}
|
|
5647
|
-
function inactiveLocalMediaState(connectionState, track) {
|
|
5648
|
-
return connectionState === "live" || connectionState === "reconnecting" || track ? "disabled" : "unavailable";
|
|
5649
|
-
}
|
|
5650
|
-
function activeLocalMediaState(connectionState) {
|
|
5651
|
-
return connectionState === "failed" || connectionState === "live" || connectionState === "reconnecting" ? "failed" : "unavailable";
|
|
6155
|
+
function screenLocalMediaState(track, enabled, connectionState) {
|
|
6156
|
+
if (enabled === false) {
|
|
6157
|
+
if (track?.enabled === false || connectionState === "live") return "disabled";
|
|
6158
|
+
}
|
|
6159
|
+
if (enabled) return "enabled";
|
|
6160
|
+
if (track) return "requesting";
|
|
6161
|
+
return connectionState === "live" || connectionState === "reconnecting" ? "disabled" : "unavailable";
|
|
5652
6162
|
}
|
|
5653
6163
|
function sameMediaSlice(left, right) {
|
|
5654
6164
|
if (left.selection !== right.selection || !sameDevices(left.devices, right.devices)) return false;
|
|
@@ -6655,7 +7165,7 @@ var SpaceClientCore = class {
|
|
|
6655
7165
|
return this.#connection.leave().pipe(Effect12.mapError(normalizeClientError));
|
|
6656
7166
|
}
|
|
6657
7167
|
endEpisode() {
|
|
6658
|
-
return this.#connection.runCommand(({ sync }) => foreign6(() => sync.endEpisode())
|
|
7168
|
+
return this.#connection.runCommand(({ sync }) => this.#connection.runEpisodeEnd(foreign6(() => sync.endEpisode()))).pipe(Effect12.tap(() => this.#connection.leave())).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
|
|
6659
7169
|
}
|
|
6660
7170
|
extendEpisode(minutes) {
|
|
6661
7171
|
return this.#connection.runCommand(({ sync }) => foreign6(() => sync.extendEpisode(minutes))).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
|
|
@@ -6711,7 +7221,7 @@ var SpaceClientCore = class {
|
|
|
6711
7221
|
}
|
|
6712
7222
|
}
|
|
6713
7223
|
#requireActive() {
|
|
6714
|
-
if (this.#disposed) throw new SpaceClientError({ code: "connection.invalid_state", recoverable: false, message: "The SpaceClient has been disposed" });
|
|
7224
|
+
if (this.#disposed) throw new SpaceClientError({ code: "connection.invalid_state", recoverable: false, message: "The SpaceClient has been disposed. Rejoin the call." });
|
|
6715
7225
|
}
|
|
6716
7226
|
};
|
|
6717
7227
|
function liveClock(dependencies) {
|
|
@@ -6731,7 +7241,14 @@ function foreign6(operation) {
|
|
|
6731
7241
|
}
|
|
6732
7242
|
async function fetchAccessGrant2(input) {
|
|
6733
7243
|
if (input.platform.connectionAccess) return input.platform.connectionAccess(input.request);
|
|
6734
|
-
return input.options.getAccess({
|
|
7244
|
+
return input.options.getAccess({
|
|
7245
|
+
space: input.options.space.trim(),
|
|
7246
|
+
reason: accessReason(input.request?.reason),
|
|
7247
|
+
replaceMediaConnection: input.request?.replaceMediaConnection ?? false,
|
|
7248
|
+
expectedMediaConnectionId: input.request?.expectedMediaConnectionId,
|
|
7249
|
+
currentMediaToken: input.request?.replaceMediaConnection ? input.request.currentMediaToken : void 0,
|
|
7250
|
+
signal: input.request?.signal
|
|
7251
|
+
});
|
|
6735
7252
|
}
|
|
6736
7253
|
function accessReason(reason) {
|
|
6737
7254
|
if (reason === void 0 || reason === "join") return "join";
|