@q9labsai/chalk-client 5.2.6 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/access/grant.d.ts +2 -0
- package/dist/{chunk-I7Y4HR2P.js → chunk-2CEMOTZF.js} +333 -64
- package/dist/{chunk-5YPQNMJE.js → chunk-FD7ZSDTM.js} +103 -26
- package/dist/{chunk-CJVLTSIO.js → chunk-MHJ4UPM3.js} +165 -6
- package/dist/chunk-N4IPKRS6.js +76 -0
- package/dist/{chunk-YIDKETOX.js → chunk-O5JB5XVX.js} +840 -38
- package/dist/{chunk-WFRTUBN6.js → chunk-VAJ6XNAA.js} +1 -1
- package/dist/client.d.ts +182 -1
- package/dist/connection/dependencies.d.ts +2 -1
- package/dist/effect.js +29 -4
- package/dist/generated/http-api.d.ts +180 -1
- package/dist/generated/schemas.d.ts +290 -1
- package/dist/generated/sync.d.ts +60 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +33 -15
- package/dist/media/call-stats.d.ts +27 -0
- package/dist/media/client.d.ts +2 -2
- package/dist/media/ice-servers.d.ts +3 -0
- package/dist/media/index.d.ts +2 -0
- package/dist/media/index.js +4 -1
- package/dist/media/plane.d.ts +4 -2
- package/dist/media/receive-stall.d.ts +52 -0
- package/dist/media/types.d.ts +21 -0
- package/dist/media/video-render-size.d.ts +7 -0
- package/dist/media/viewer-layer.d.ts +48 -0
- package/dist/public-invites/index.js +3 -2
- package/dist/server/index.cjs +19 -4
- package/dist/server/index.js +19 -4
- package/dist/server/types.d.cts +2 -0
- package/dist/server/types.d.ts +2 -0
- package/dist/session/index.d.ts +18 -0
- package/dist/space-client/same-identity.d.ts +6 -0
- package/dist/space-client/types.d.ts +6 -0
- package/dist/sync/index.js +2 -1
- package/dist/sync/v1-command-scheduler.d.ts +4 -0
- package/dist/sync/v1-types.d.ts +6 -0
- package/package.json +3 -1
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccessGrantError,
|
|
3
|
+
requireParsedAccessGrant
|
|
4
|
+
} from "./chunk-MHJ4UPM3.js";
|
|
1
5
|
import {
|
|
2
6
|
CloudflareSFUClient,
|
|
3
7
|
CloudflareSFUError,
|
|
@@ -6,11 +10,7 @@ import {
|
|
|
6
10
|
describeMediaCaptureError,
|
|
7
11
|
grantedMediaFromStream,
|
|
8
12
|
markMediaFailure
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
AccessGrantError,
|
|
12
|
-
requireParsedAccessGrant
|
|
13
|
-
} from "./chunk-CJVLTSIO.js";
|
|
13
|
+
} from "./chunk-O5JB5XVX.js";
|
|
14
14
|
import {
|
|
15
15
|
ChatAttachmentMimeTypeSchema,
|
|
16
16
|
ChatReactionSetFrameSchema,
|
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
formatSyncConnectDiagnostic,
|
|
25
25
|
scheduleSyncDialDeadline,
|
|
26
26
|
syncConnectFailureCode
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-FD7ZSDTM.js";
|
|
28
|
+
import {
|
|
29
|
+
withRequestContext
|
|
30
|
+
} from "./chunk-N4IPKRS6.js";
|
|
28
31
|
|
|
29
32
|
// src/access/manager.ts
|
|
30
33
|
import { Clock, Context, Data, Deferred, Effect, Layer, SynchronizedRef } from "effect";
|
|
@@ -56,31 +59,31 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
|
|
|
56
59
|
});
|
|
57
60
|
});
|
|
58
61
|
const automaticMediaReplacementListeners = /* @__PURE__ */ new Set();
|
|
59
|
-
const
|
|
62
|
+
const fetch = (reason, replaceMediaConnection, force) => SynchronizedRef.modifyEffect(state, (current) => fetchAccessGrant(provider, { current, reason, replaceMediaConnection, force, refreshWindowMs, replacementGate })).pipe(
|
|
60
63
|
Effect.catchTag(
|
|
61
64
|
"MediaReplacementPending",
|
|
62
65
|
({ done }) => Deferred.await(done).pipe(
|
|
63
|
-
Effect.flatMap(() =>
|
|
66
|
+
Effect.flatMap(() => fetch(reason, replaceMediaConnection, force)),
|
|
64
67
|
Effect.map((access) => ({ access, automaticMediaReplacement: false }))
|
|
65
68
|
)
|
|
66
69
|
),
|
|
67
70
|
Effect.tap((result) => Effect.sync(() => notifyAutomaticMediaReplacement(automaticMediaReplacementListeners, result))),
|
|
68
71
|
Effect.map((result) => result.access)
|
|
69
72
|
);
|
|
70
|
-
const initialize = (reason = "join") =>
|
|
73
|
+
const initialize = (reason = "join") => fetch(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (failure2) => failure2.code === "access.invalid" && SynchronizedRef.getUnsafe(state).access === null ? fetch(reason, false, true) : Effect.fail(failure2)));
|
|
71
74
|
const millisecondsUntilRefresh = Effect.gen(function* () {
|
|
72
75
|
const access = yield* SynchronizedRef.get(state);
|
|
73
76
|
if (!access.access) return null;
|
|
74
77
|
return millisecondsUntil(access.access, yield* Clock.currentTimeMillis, refreshWindowMs);
|
|
75
78
|
});
|
|
76
|
-
const ensureFresh = (reason = "scheduled_refresh") =>
|
|
79
|
+
const ensureFresh = (reason = "scheduled_refresh") => fetch(reason, false, false);
|
|
77
80
|
return {
|
|
78
81
|
current: SynchronizedRef.get(state).pipe(Effect.map((value) => value.access)),
|
|
79
82
|
currentUnsafe: () => SynchronizedRef.getUnsafe(state).access,
|
|
80
83
|
initialize,
|
|
81
84
|
ensureFresh,
|
|
82
|
-
refresh: (reason, replaceMediaConnection) =>
|
|
83
|
-
refreshAfterRejection: () =>
|
|
85
|
+
refresh: (reason, replaceMediaConnection) => fetch(reason, replaceMediaConnection, true),
|
|
86
|
+
refreshAfterRejection: () => fetch("access_retry", true, true),
|
|
84
87
|
getSyncToken: (reason = "sync_recovery") => ensureFresh(reason).pipe(Effect.map((access) => access.sync.token)),
|
|
85
88
|
getMediaToken: () => ensureFresh("scheduled_refresh").pipe(Effect.map((access) => access.media.token)),
|
|
86
89
|
subscribeAutomaticMediaReplacement: (listener) => Effect.sync(() => {
|
|
@@ -291,8 +294,9 @@ function invalidUpload(message) {
|
|
|
291
294
|
throw chatFileError("initiate_upload", "invalid_payload", false, message);
|
|
292
295
|
}
|
|
293
296
|
function createChatFileRequest(options) {
|
|
294
|
-
const fetchImplementation = options.fetch
|
|
295
|
-
|
|
297
|
+
const fetchImplementation = withRequestContext(options.fetch);
|
|
298
|
+
let baseUrl = options.baseUrl;
|
|
299
|
+
while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
296
300
|
return async function request(operation, path, init, accepts) {
|
|
297
301
|
const headers = new Headers(init.headers);
|
|
298
302
|
try {
|
|
@@ -786,7 +790,7 @@ var ChalkWhiteboardViewBounds = {
|
|
|
786
790
|
|
|
787
791
|
// src/whiteboard/file-download-transport.ts
|
|
788
792
|
function createChalkWhiteboardV1FileDownloadTransport(options) {
|
|
789
|
-
const fetchImplementation = options.fetch
|
|
793
|
+
const fetchImplementation = withRequestContext(options.fetch);
|
|
790
794
|
let baseUrlEnd = options.baseUrl.length;
|
|
791
795
|
while (baseUrlEnd > 0 && options.baseUrl[baseUrlEnd - 1] === "/") baseUrlEnd -= 1;
|
|
792
796
|
const baseUrl = options.baseUrl.slice(0, baseUrlEnd);
|
|
@@ -2565,6 +2569,7 @@ function screenShareCapture(track) {
|
|
|
2565
2569
|
}
|
|
2566
2570
|
|
|
2567
2571
|
// src/connection/production.ts
|
|
2572
|
+
var apiFetch = withRequestContext();
|
|
2568
2573
|
function createDefaultConnectionDependencies(options) {
|
|
2569
2574
|
return {
|
|
2570
2575
|
clock: {
|
|
@@ -2585,7 +2590,7 @@ function createDefaultConnectionDependencies(options) {
|
|
|
2585
2590
|
return () => globalThis.window?.removeEventListener("online", listener);
|
|
2586
2591
|
},
|
|
2587
2592
|
send: async (grant, report, signal, traceparent) => {
|
|
2588
|
-
const response = await
|
|
2593
|
+
const response = await apiFetch(`${options.apiBaseURL.replace(/\/$/, "")}/v1/tenants/${encodeURIComponent(grant.subject.tenantId)}/connection-reports`, {
|
|
2589
2594
|
method: "POST",
|
|
2590
2595
|
headers: { Authorization: `Bearer ${grant.media.token}`, "Content-Type": "application/json", traceparent },
|
|
2591
2596
|
body: JSON.stringify(report),
|
|
@@ -2601,7 +2606,7 @@ function createDefaultConnectionDependencies(options) {
|
|
|
2601
2606
|
getEpisodeStatus: async (access, signal) => {
|
|
2602
2607
|
const { tenantId, spaceId, episodeId, participantId } = access.subject;
|
|
2603
2608
|
const path = [tenantId, spaceId, episodeId, participantId].map(encodeURIComponent);
|
|
2604
|
-
const response = await
|
|
2609
|
+
const response = await apiFetch(`${options.apiBaseURL.replace(/\/$/, "")}/v1/tenants/${path[0]}/spaces/${path[1]}/episodes/${path[2]}/participants/${path[3]}/episode-status`, {
|
|
2605
2610
|
headers: { Authorization: `Bearer ${access.media.token}` },
|
|
2606
2611
|
signal,
|
|
2607
2612
|
cache: "no-store",
|
|
@@ -2618,7 +2623,7 @@ function createDefaultConnectionDependencies(options) {
|
|
|
2618
2623
|
let baseEnd = options.apiBaseURL.length;
|
|
2619
2624
|
while (baseEnd > 0 && options.apiBaseURL[baseEnd - 1] === "/") baseEnd--;
|
|
2620
2625
|
const baseURL = options.apiBaseURL.slice(0, baseEnd);
|
|
2621
|
-
const response = await
|
|
2626
|
+
const response = await apiFetch(`${baseURL}/v1/tenants/${path[0]}/spaces/${path[1]}/episodes/${path[2]}/participants/${path[3]}/cancel-join`, {
|
|
2622
2627
|
method: "POST",
|
|
2623
2628
|
headers: { Authorization: `Bearer ${access.media.token}` },
|
|
2624
2629
|
signal
|
|
@@ -2853,7 +2858,7 @@ var ConnectionDiagnostics = class {
|
|
|
2853
2858
|
// src/connection/lifecycle.ts
|
|
2854
2859
|
var MEDIA_RECOVERY_TIMEOUT_MS = 6e4;
|
|
2855
2860
|
var START_TIMEOUT_MS = 3e4;
|
|
2856
|
-
var LEAVE_TIMEOUT_MS =
|
|
2861
|
+
var LEAVE_TIMEOUT_MS = 4e4;
|
|
2857
2862
|
var RECOVERY_BUDGET_MS = MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2858
2863
|
var MAX_RECOVERY_ATTEMPTS = 3;
|
|
2859
2864
|
var REFRESH_RETRY_MS = 5e3;
|
|
@@ -3291,6 +3296,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3291
3296
|
if (model.failure) return yield* Effect2.fail(lifecycleFailure("leave_unconfirmed", false, model.failure.message));
|
|
3292
3297
|
return;
|
|
3293
3298
|
}
|
|
3299
|
+
yield* Effect2.sync(() => {
|
|
3300
|
+
for (const { track } of model.media?.getSnapshot().localTracks ?? []) {
|
|
3301
|
+
track.enabled = false;
|
|
3302
|
+
track.stop();
|
|
3303
|
+
}
|
|
3304
|
+
});
|
|
3294
3305
|
yield* transition("leaving");
|
|
3295
3306
|
const confirmed = yield* leaveIsConfirmed();
|
|
3296
3307
|
yield* stopPorts();
|
|
@@ -3771,9 +3782,14 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3771
3782
|
yield* publish();
|
|
3772
3783
|
});
|
|
3773
3784
|
const handleMediaFailure = (media, cause, binding) => {
|
|
3785
|
+
if (model.state === "leaving") return Effect2.void;
|
|
3774
3786
|
if (media !== model.media || binding !== access.currentUnsafe()?.media.clientPayload.connectionId) return Effect2.void;
|
|
3775
|
-
|
|
3776
|
-
if (sfuAccessRejected(cause)
|
|
3787
|
+
const failure2 = failureFromCause(cause, "media_start_failed", "Media transport needs recovery");
|
|
3788
|
+
if (sfuAccessRejected(cause)) {
|
|
3789
|
+
stopRejectedMedia();
|
|
3790
|
+
return failForSnapshot(failure2);
|
|
3791
|
+
}
|
|
3792
|
+
if (cause instanceof CloudflareSFUError && cause.options.status === 410 || active(model) && connectionSuperseded(cause)) return failForSnapshot(failure2);
|
|
3777
3793
|
return handleMediaSnapshot(media, media.getSnapshot());
|
|
3778
3794
|
};
|
|
3779
3795
|
const applyAutomaticMediaReplacement = (grant) => Effect2.gen(function* () {
|
|
@@ -4455,13 +4471,13 @@ var ChatControllerRuntime = class {
|
|
|
4455
4471
|
#unsubscribeConnection = null;
|
|
4456
4472
|
#unsubscribeEvents = null;
|
|
4457
4473
|
#unsubscribeSnapshot = null;
|
|
4458
|
-
constructor(connection, store, transport, apiBaseUrl,
|
|
4474
|
+
constructor(connection, store, transport, apiBaseUrl, fetch, fork, offline) {
|
|
4459
4475
|
this.#offline = offline;
|
|
4460
4476
|
this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
|
|
4461
4477
|
this.#connection = connection;
|
|
4462
4478
|
this.#store = store;
|
|
4463
4479
|
this.#transport = transport;
|
|
4464
|
-
this.#fetch =
|
|
4480
|
+
this.#fetch = fetch;
|
|
4465
4481
|
this.#fork = fork;
|
|
4466
4482
|
this.#unsubscribeConnection = connection.subscribePorts((ports) => this.#bind(ports));
|
|
4467
4483
|
}
|
|
@@ -4539,7 +4555,7 @@ var ChatControllerRuntime = class {
|
|
|
4539
4555
|
});
|
|
4540
4556
|
};
|
|
4541
4557
|
upload = (file) => {
|
|
4542
|
-
const
|
|
4558
|
+
const fetch = this.#fetch;
|
|
4543
4559
|
return Effect4.suspend(() => {
|
|
4544
4560
|
if (!this.#transport) {
|
|
4545
4561
|
return Effect4.fail(new SpaceClientError({ code: "collaboration.unavailable", recoverable: false, message: "Chat file upload is unavailable" }));
|
|
@@ -4555,7 +4571,7 @@ var ChatControllerRuntime = class {
|
|
|
4555
4571
|
(upload) => Effect4.void.pipe(
|
|
4556
4572
|
Effect4.andThen(
|
|
4557
4573
|
Effect4.tryPromise({
|
|
4558
|
-
try: () =>
|
|
4574
|
+
try: () => fetch(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes }),
|
|
4559
4575
|
catch: () => new SpaceClientError({ code: "collaboration.unavailable", recoverable: true, message: CHAT_FILE_REACHABILITY_MESSAGE })
|
|
4560
4576
|
}).pipe(
|
|
4561
4577
|
Effect4.flatMap((response) => {
|
|
@@ -4877,6 +4893,20 @@ function chatError(message) {
|
|
|
4877
4893
|
return new SpaceClientError({ code: "chat.payload_invalid", recoverable: false, message });
|
|
4878
4894
|
}
|
|
4879
4895
|
|
|
4896
|
+
// src/space-client/same-identity.ts
|
|
4897
|
+
function otherDevices(participants, participantId, ownIdentityId) {
|
|
4898
|
+
const identityId = ownIdentityId ?? participants.find((item) => item.participantId === participantId)?.identityId;
|
|
4899
|
+
return identityId ? participants.filter((item) => item.participantId !== participantId && item.identityId === identityId) : [];
|
|
4900
|
+
}
|
|
4901
|
+
function otherDeviceHasMicrophone(snapshot, participants) {
|
|
4902
|
+
if (snapshot.control?.status === "ended") return false;
|
|
4903
|
+
const admitted = snapshot.control ? new Set(snapshot.control.participants.map((item) => item.participantId)) : null;
|
|
4904
|
+
const devices = new Set(
|
|
4905
|
+
otherDevices(participants, snapshot.participantId, snapshot.participantIdentityId).filter((item) => !admitted || admitted.has(item.participantId)).map((item) => item.participantId)
|
|
4906
|
+
);
|
|
4907
|
+
return snapshot.media?.items.some((item) => devices.has(item.participantId) && item.source === "microphone" && item.enabled) ?? false;
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4880
4910
|
// src/space-client/media-controller.ts
|
|
4881
4911
|
import { Clock as Clock3, Context as Context6, Deferred as Deferred3, Effect as Effect5, Exit as Exit2, Layer as Layer6, Semaphore } from "effect";
|
|
4882
4912
|
|
|
@@ -4949,8 +4979,35 @@ var MediaProjectionMonitor = class {
|
|
|
4949
4979
|
// src/space-client/media-controller.ts
|
|
4950
4980
|
var MEDIA_SOURCES = ["microphone", "camera", "screen"];
|
|
4951
4981
|
var MEDIA_READY_TIMEOUT_MS = 1e4;
|
|
4982
|
+
var OFF_RETRY_INITIAL_MS = 1e3;
|
|
4983
|
+
var OFF_RETRY_MAX_MS = 3e4;
|
|
4984
|
+
function locallyOff(state) {
|
|
4985
|
+
return !state.desired.enabled || !state.localEnabled;
|
|
4986
|
+
}
|
|
4987
|
+
function owesRemoteOff(state) {
|
|
4988
|
+
return state.offDebt === "owed" && locallyOff(state);
|
|
4989
|
+
}
|
|
4952
4990
|
function sourceWorker(enabled) {
|
|
4953
|
-
return {
|
|
4991
|
+
return {
|
|
4992
|
+
desired: { enabled, revision: 0, controlRevision: -1, previousEnabled: enabled },
|
|
4993
|
+
result: { revision: 0, status: "confirmed" },
|
|
4994
|
+
initialRevision: 0,
|
|
4995
|
+
localEnabled: enabled,
|
|
4996
|
+
pause: null,
|
|
4997
|
+
offDebt: "none",
|
|
4998
|
+
offRetry: { timer: null, delayMs: OFF_RETRY_INITIAL_MS, running: false },
|
|
4999
|
+
serverSequence: -1,
|
|
5000
|
+
forcedRevision: null,
|
|
5001
|
+
pausedPublicationId: null,
|
|
5002
|
+
capturePause: "none",
|
|
5003
|
+
awaitingLive: null
|
|
5004
|
+
};
|
|
5005
|
+
}
|
|
5006
|
+
function rejectOffline(state, intent) {
|
|
5007
|
+
if (intent.enabled) state.result = { revision: intent.revision, status: "failed", cause: new V1SyncError("Media enable requires a live call", "not_live"), cleanup: !intent.previousEnabled };
|
|
5008
|
+
}
|
|
5009
|
+
function ownProjection(snapshot, source) {
|
|
5010
|
+
return snapshot.media?.items.find((item) => item.participantId === snapshot.participantId && item.source === source);
|
|
4954
5011
|
}
|
|
4955
5012
|
var MediaControllerService = class extends Context6.Service()("@chalk/client/MediaController") {
|
|
4956
5013
|
};
|
|
@@ -5012,6 +5069,7 @@ var MediaControllerRuntime = class {
|
|
|
5012
5069
|
#unsubscribePorts = null;
|
|
5013
5070
|
#microphoneWatch = null;
|
|
5014
5071
|
#projection;
|
|
5072
|
+
#microphoneMutedByOtherDevice = false;
|
|
5015
5073
|
constructor(connection, store, selection, gates, now, fork) {
|
|
5016
5074
|
this.#connection = connection;
|
|
5017
5075
|
this.#store = store;
|
|
@@ -5037,7 +5095,10 @@ var MediaControllerRuntime = class {
|
|
|
5037
5095
|
if (intent.microphone !== void 0) this.#recordIntent("microphone", intent.microphone);
|
|
5038
5096
|
if (intent.camera !== void 0) this.#recordIntent("camera", intent.camera);
|
|
5039
5097
|
}
|
|
5040
|
-
setMicrophoneEnabled = (enabled) =>
|
|
5098
|
+
setMicrophoneEnabled = (enabled) => Effect5.sync(() => {
|
|
5099
|
+
this.#microphoneMutedByOtherDevice = false;
|
|
5100
|
+
this.#publish();
|
|
5101
|
+
}).pipe(Effect5.andThen(this.#set("microphone", enabled)));
|
|
5041
5102
|
setCameraEnabled = (enabled) => this.#set("camera", enabled);
|
|
5042
5103
|
captureCameraPreview = () => this.#cameraSource("preview").pipe(Effect5.mapError(normalizeMediaError));
|
|
5043
5104
|
setScreenShareEnabled = (enabled, options) => enabled ? this.#mediaAction(true, this.#startScreen(options)) : this.#stopScreen();
|
|
@@ -5133,6 +5194,8 @@ var MediaControllerRuntime = class {
|
|
|
5133
5194
|
}
|
|
5134
5195
|
dispose() {
|
|
5135
5196
|
this.#disposed = true;
|
|
5197
|
+
this.#cancelOffRetries();
|
|
5198
|
+
for (const source of MEDIA_SOURCES) this.#sources[source].offDebt = "none";
|
|
5136
5199
|
this.#projection.reset();
|
|
5137
5200
|
for (const check of this.#readinessChecks) check(normalizeMediaError(this.#cancelledCaptureError()));
|
|
5138
5201
|
this.#cancelCaptures();
|
|
@@ -5186,8 +5249,27 @@ var MediaControllerRuntime = class {
|
|
|
5186
5249
|
}).pipe(Effect5.ensuring(Effect5.sync(() => cleanup())));
|
|
5187
5250
|
});
|
|
5188
5251
|
}
|
|
5252
|
+
/** While a user enable waits for the call, its revision belongs to that wait; another source worker must not run it. */
|
|
5253
|
+
#awaitLive(source, revision) {
|
|
5254
|
+
const state = this.#sources[source];
|
|
5255
|
+
return Effect5.suspend(() => {
|
|
5256
|
+
state.awaitingLive = revision;
|
|
5257
|
+
return this.#waitForLive(source, revision);
|
|
5258
|
+
}).pipe(
|
|
5259
|
+
Effect5.ensuring(
|
|
5260
|
+
Effect5.sync(() => {
|
|
5261
|
+
if (state.awaitingLive === revision) state.awaitingLive = null;
|
|
5262
|
+
})
|
|
5263
|
+
)
|
|
5264
|
+
);
|
|
5265
|
+
}
|
|
5189
5266
|
#set(source, enabled) {
|
|
5190
|
-
return Effect5.suspend(() => this.#store.getSnapshot().connection.status === "joining" ? this.#act(source, enabled) : this.#mediaAction(enabled, this.#act(source, enabled)));
|
|
5267
|
+
return Effect5.suspend(() => this.#store.getSnapshot().connection.status === "joining" || this.#replacingMediaConnection() ? this.#act(source, enabled) : this.#mediaAction(enabled, this.#act(source, enabled)));
|
|
5268
|
+
}
|
|
5269
|
+
/** Sync is live and only the media connection is being replaced, e.g. while another source moves to a fresh SFU connection. */
|
|
5270
|
+
#replacingMediaConnection() {
|
|
5271
|
+
const { state, connection } = this.#connection.getSnapshot();
|
|
5272
|
+
return (state === "live" || state === "reconnecting") && connection.sync === "healthy" && (connection.media === "connecting" || connection.media === "recovering");
|
|
5191
5273
|
}
|
|
5192
5274
|
#startScreen(options) {
|
|
5193
5275
|
return this.#act("screen", true, options);
|
|
@@ -5234,7 +5316,9 @@ var MediaControllerRuntime = class {
|
|
|
5234
5316
|
return Effect5.suspend(() => {
|
|
5235
5317
|
if (this.#ports?.media.getSnapshot().connection.phase === "stopped") return Effect5.void;
|
|
5236
5318
|
const desired = this.#recordIntent(source, enabled, options);
|
|
5237
|
-
|
|
5319
|
+
if (!enabled) this.#sources[source].offDebt = "owed";
|
|
5320
|
+
else this.#cancelOffRetry(source);
|
|
5321
|
+
const ready = enabled && source !== "screen" && this.#store.getSnapshot().connection.status !== "live" ? this.#awaitLive(source, desired.revision).pipe(
|
|
5238
5322
|
Effect5.tapError(
|
|
5239
5323
|
(cause) => Effect5.sync(() => {
|
|
5240
5324
|
if (this.#sources[source].desired.revision === desired.revision) {
|
|
@@ -5269,12 +5353,12 @@ var MediaControllerRuntime = class {
|
|
|
5269
5353
|
#work(source) {
|
|
5270
5354
|
return Effect5.gen({ self: this }, function* () {
|
|
5271
5355
|
const state = this.#sources[source];
|
|
5272
|
-
while (state.result.revision !== state.desired.revision || state.result.status === "failed" && state.result.cleanup) {
|
|
5356
|
+
while (state.result.revision !== state.desired.revision || state.result.status === "failed" && state.result.cleanup || owesRemoteOff(state)) {
|
|
5273
5357
|
const intent = state.desired;
|
|
5274
5358
|
yield* this.#runRevision(source, intent);
|
|
5275
5359
|
if (state.desired.revision === intent.revision) return;
|
|
5276
5360
|
}
|
|
5277
|
-
});
|
|
5361
|
+
}).pipe(Effect5.ensuring(Effect5.sync(() => this.#scheduleOffRetry(source))));
|
|
5278
5362
|
}
|
|
5279
5363
|
#runRevision(source, intent) {
|
|
5280
5364
|
return Effect5.gen({ self: this }, function* () {
|
|
@@ -5283,11 +5367,12 @@ var MediaControllerRuntime = class {
|
|
|
5283
5367
|
state.pause = null;
|
|
5284
5368
|
const paused = yield* Effect5.exit(pause ? foreign2(() => pause) : Effect5.void);
|
|
5285
5369
|
if (state.desired.revision !== intent.revision) return;
|
|
5370
|
+
if (state.awaitingLive === intent.revision) return yield* paused;
|
|
5286
5371
|
const ports = this.#ports;
|
|
5287
5372
|
const cleanup = source === "screen" && !intent.enabled ? yield* Effect5.exit(this.#stopScreenLocally(ports)) : Exit2.succeed(void 0);
|
|
5288
5373
|
if (state.desired.revision !== intent.revision) return;
|
|
5289
5374
|
if (ports && this.#syncLive) yield* this.#executeRevision(ports, source, intent);
|
|
5290
|
-
else
|
|
5375
|
+
else rejectOffline(state, intent);
|
|
5291
5376
|
yield* paused;
|
|
5292
5377
|
yield* cleanup;
|
|
5293
5378
|
});
|
|
@@ -5295,16 +5380,13 @@ var MediaControllerRuntime = class {
|
|
|
5295
5380
|
#executeRevision(ports, source, intent) {
|
|
5296
5381
|
return Effect5.gen({ self: this }, function* () {
|
|
5297
5382
|
const state = this.#sources[source];
|
|
5298
|
-
if (state.result.revision === intent.revision && state.result.status === "failed")
|
|
5299
|
-
yield* this.#connection.runCommand((p) => this.#send(p, source, intent, false));
|
|
5300
|
-
if (state.result.revision === intent.revision && state.result.status === "failed") state.result.cleanup = false;
|
|
5301
|
-
return;
|
|
5302
|
-
}
|
|
5383
|
+
if (state.result.revision === intent.revision && (state.result.status === "failed" || owesRemoteOff(state))) return yield* this.#sendOwedOff(source, intent);
|
|
5303
5384
|
const operation = source === "screen" ? this.#prepareScreen(ports, intent) : this.#prepareDevice(ports, source, intent);
|
|
5304
5385
|
const result = yield* Effect5.exit(this.#connection.runCommand(() => operation));
|
|
5305
5386
|
if (state.desired.revision !== intent.revision) return;
|
|
5306
5387
|
if (Exit2.isSuccess(result)) {
|
|
5307
5388
|
state.result = { revision: intent.revision, status: "confirmed" };
|
|
5389
|
+
state.offDebt = intent.enabled ? "none" : "sent";
|
|
5308
5390
|
state.localEnabled = state.forcedRevision === intent.revision ? false : intent.enabled;
|
|
5309
5391
|
} else {
|
|
5310
5392
|
const cause = yield* Effect5.flip(result);
|
|
@@ -5312,6 +5394,16 @@ var MediaControllerRuntime = class {
|
|
|
5312
5394
|
}
|
|
5313
5395
|
});
|
|
5314
5396
|
}
|
|
5397
|
+
#sendOwedOff(source, intent) {
|
|
5398
|
+
return Effect5.gen({ self: this }, function* () {
|
|
5399
|
+
const state = this.#sources[source];
|
|
5400
|
+
yield* this.#connection.runCommand((p) => this.#send(p, source, intent, false));
|
|
5401
|
+
state.offDebt = "sent";
|
|
5402
|
+
if (state.result.revision !== intent.revision || state.result.status !== "failed") return;
|
|
5403
|
+
if (intent.enabled) state.result.cleanup = false;
|
|
5404
|
+
else state.result = { revision: intent.revision, status: "confirmed" };
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5315
5407
|
#failRevision(ports, source, intent, cause) {
|
|
5316
5408
|
return Effect5.sync(() => {
|
|
5317
5409
|
const state = this.#sources[source];
|
|
@@ -5462,19 +5554,36 @@ var MediaControllerRuntime = class {
|
|
|
5462
5554
|
}
|
|
5463
5555
|
}
|
|
5464
5556
|
startInitial(media, stream) {
|
|
5557
|
+
const waitForSync = Effect5.callback((resume) => {
|
|
5558
|
+
const sync = this.#ports?.sync;
|
|
5559
|
+
if (!sync) return resume(Effect5.void);
|
|
5560
|
+
const check = () => {
|
|
5561
|
+
if (sync.getSnapshot().connection.phase === "live" || media.getSnapshot().connection.phase === "stopped") resume(Effect5.void);
|
|
5562
|
+
};
|
|
5563
|
+
const unsubscribe = sync.subscribe(check);
|
|
5564
|
+
const unsubscribeMedia = media.subscribe(check);
|
|
5565
|
+
check();
|
|
5566
|
+
return Effect5.sync(() => {
|
|
5567
|
+
unsubscribe();
|
|
5568
|
+
unsubscribeMedia();
|
|
5569
|
+
});
|
|
5570
|
+
});
|
|
5465
5571
|
const publish = foreign2(async () => {
|
|
5466
5572
|
const revisions = { microphone: this.#sources.microphone.initialRevision, camera: this.#sources.camera.initialRevision };
|
|
5467
|
-
const tracks = stream.getTracks()
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5573
|
+
const tracks = this.#consentedTracks(stream.getTracks());
|
|
5574
|
+
await media.start(
|
|
5575
|
+
streamFromTracks(tracks),
|
|
5576
|
+
() => Effect5.runPromise(
|
|
5577
|
+
waitForSync.pipe(
|
|
5578
|
+
Effect5.map(() => {
|
|
5579
|
+
const snapshot = this.#ports?.sync.getSnapshot();
|
|
5580
|
+
this.#microphoneMutedByOtherDevice = Boolean(snapshot && this.#hasConsent("microphone") && otherDeviceHasMicrophone(snapshot, this.#store.getSnapshot().participants.roster));
|
|
5581
|
+
if (this.#microphoneMutedByOtherDevice) this.#recordIntent("microphone", false);
|
|
5582
|
+
return streamFromTracks(this.#consentedTracks(tracks));
|
|
5583
|
+
})
|
|
5584
|
+
)
|
|
5585
|
+
)
|
|
5586
|
+
);
|
|
5478
5587
|
for (const source of ["microphone", "camera"]) {
|
|
5479
5588
|
const state = this.#sources[source];
|
|
5480
5589
|
if (state.desired.revision === revisions[source]) state.result = { revision: revisions[source], status: "confirmed" };
|
|
@@ -5482,6 +5591,18 @@ var MediaControllerRuntime = class {
|
|
|
5482
5591
|
});
|
|
5483
5592
|
return this.#serialize("microphone", this.#serialize("camera", publish));
|
|
5484
5593
|
}
|
|
5594
|
+
#consentedTracks(tracks) {
|
|
5595
|
+
return tracks.filter((track) => {
|
|
5596
|
+
const source = track.kind === "audio" ? "microphone" : "camera";
|
|
5597
|
+
const enabled = this.#hasConsent(source);
|
|
5598
|
+
track.enabled = enabled;
|
|
5599
|
+
if (!enabled) {
|
|
5600
|
+
this.#deleteTrack(source);
|
|
5601
|
+
if (source === "microphone") track.stop();
|
|
5602
|
+
}
|
|
5603
|
+
return enabled;
|
|
5604
|
+
});
|
|
5605
|
+
}
|
|
5485
5606
|
#send(ports, source, intent, enabled) {
|
|
5486
5607
|
const command = this.#mediaAction(
|
|
5487
5608
|
enabled,
|
|
@@ -5499,19 +5620,97 @@ var MediaControllerRuntime = class {
|
|
|
5499
5620
|
}
|
|
5500
5621
|
#reconcileIntents() {
|
|
5501
5622
|
const live = this.#connection.getSnapshot().state === "live" && this.#ports?.sync.getSnapshot().connection.phase === "live";
|
|
5502
|
-
if (this.#syncLive && !live)
|
|
5623
|
+
if (this.#syncLive && !live) {
|
|
5624
|
+
this.#rememberRecoveryIntents();
|
|
5625
|
+
this.#cancelOffRetries();
|
|
5626
|
+
}
|
|
5503
5627
|
const resumed = live && !this.#syncLive;
|
|
5504
5628
|
this.#syncLive = live;
|
|
5505
5629
|
if (!resumed) return;
|
|
5506
|
-
if (this.#ports)
|
|
5630
|
+
if (this.#ports) {
|
|
5631
|
+
const snapshot = this.#ports.sync.getSnapshot();
|
|
5632
|
+
this.#pauseRecoveredIntents(this.#ports, snapshot);
|
|
5633
|
+
this.#oweOffsSyncStillShows(snapshot);
|
|
5634
|
+
}
|
|
5507
5635
|
this.#resumePendingActions();
|
|
5508
5636
|
}
|
|
5509
5637
|
#resumePendingActions() {
|
|
5638
|
+
for (const source of MEDIA_SOURCES) {
|
|
5639
|
+
this.#sources[source].offRetry.delayMs = OFF_RETRY_INITIAL_MS;
|
|
5640
|
+
if (source === "screen" && !owesRemoteOff(this.#sources[source])) this.#runWork(source);
|
|
5641
|
+
else this.#resendOwedOff(source);
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
/** After a reconnect, Sync's snapshot decides: a source the device turned off but Sync still shows on, with the publication the device paused, is an off to resend. */
|
|
5645
|
+
#oweOffsSyncStillShows(snapshot) {
|
|
5510
5646
|
for (const source of MEDIA_SOURCES) {
|
|
5511
5647
|
const state = this.#sources[source];
|
|
5512
|
-
if (
|
|
5513
|
-
|
|
5648
|
+
if (state.offDebt !== "sent" || state.pausedPublicationId === null) continue;
|
|
5649
|
+
if (ownProjection(snapshot, source)?.publicationId === state.pausedPublicationId) state.offDebt = "owed";
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
/** Sync needs no off from this device: it shows the source off, or on with a publication this device never paused (another device's). */
|
|
5653
|
+
#syncNeedsNoOff(source) {
|
|
5654
|
+
const snapshot = this.#ports?.sync.getSnapshot();
|
|
5655
|
+
if (!snapshot?.media) return false;
|
|
5656
|
+
const item = ownProjection(snapshot, source);
|
|
5657
|
+
if (!item) return false;
|
|
5658
|
+
return !item.enabled || this.#sources[source].pausedPublicationId !== null && item.publicationId !== this.#sources[source].pausedPublicationId;
|
|
5659
|
+
}
|
|
5660
|
+
/** The one place an owed off is resent after the user's own attempt. At most one is in flight per source, and none while a user action on the source is pending. */
|
|
5661
|
+
#resendOwedOff(source) {
|
|
5662
|
+
const state = this.#sources[source];
|
|
5663
|
+
if (!this.#syncLive || this.#disposed || !owesRemoteOff(state) || state.offRetry.running || this.#pendingSources.has(source)) return;
|
|
5664
|
+
this.#cancelOffRetry(source);
|
|
5665
|
+
if (this.#syncNeedsNoOff(source)) {
|
|
5666
|
+
state.offDebt = "none";
|
|
5667
|
+
state.result = { revision: state.desired.revision, status: "confirmed" };
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
this.#runWork(source);
|
|
5671
|
+
}
|
|
5672
|
+
/** A Sync snapshot repays an owed off now, unless an attempt for that source is already queued or running. */
|
|
5673
|
+
#retryOffs() {
|
|
5674
|
+
for (const source of MEDIA_SOURCES) this.#resendOwedOff(source);
|
|
5675
|
+
}
|
|
5676
|
+
#runWork(source) {
|
|
5677
|
+
const retry = this.#sources[source].offRetry;
|
|
5678
|
+
retry.running = true;
|
|
5679
|
+
this.#fork(
|
|
5680
|
+
this.#serialize(source, this.#work(source)).pipe(
|
|
5681
|
+
Effect5.ignore,
|
|
5682
|
+
Effect5.ensuring(
|
|
5683
|
+
Effect5.sync(() => {
|
|
5684
|
+
retry.running = false;
|
|
5685
|
+
})
|
|
5686
|
+
)
|
|
5687
|
+
)
|
|
5688
|
+
);
|
|
5689
|
+
}
|
|
5690
|
+
/** An off that Sync still has not shown is retried on a timer with doubling backoff, one timer per source, so a quiet healthy connection repairs itself. */
|
|
5691
|
+
#scheduleOffRetry(source) {
|
|
5692
|
+
const state = this.#sources[source];
|
|
5693
|
+
const retry = state.offRetry;
|
|
5694
|
+
if (!owesRemoteOff(state)) {
|
|
5695
|
+
this.#cancelOffRetry(source);
|
|
5696
|
+
retry.delayMs = OFF_RETRY_INITIAL_MS;
|
|
5697
|
+
return;
|
|
5514
5698
|
}
|
|
5699
|
+
if (!this.#syncLive || this.#disposed || retry.timer !== null) return;
|
|
5700
|
+
retry.timer = this.#connection.scheduleUnsafe(() => {
|
|
5701
|
+
retry.timer = null;
|
|
5702
|
+
this.#resendOwedOff(source);
|
|
5703
|
+
}, retry.delayMs);
|
|
5704
|
+
retry.delayMs = Math.min(retry.delayMs * 2, OFF_RETRY_MAX_MS);
|
|
5705
|
+
}
|
|
5706
|
+
#cancelOffRetry(source) {
|
|
5707
|
+
const retry = this.#sources[source].offRetry;
|
|
5708
|
+
if (retry.timer === null) return;
|
|
5709
|
+
this.#connection.cancelScheduleUnsafe(retry.timer);
|
|
5710
|
+
retry.timer = null;
|
|
5711
|
+
}
|
|
5712
|
+
#cancelOffRetries() {
|
|
5713
|
+
for (const source of MEDIA_SOURCES) this.#cancelOffRetry(source);
|
|
5515
5714
|
}
|
|
5516
5715
|
#clearScreenAudio(media) {
|
|
5517
5716
|
return foreign2(() => media.clearPreparedLocalTrack("screen_audio")).pipe(
|
|
@@ -5882,8 +6081,8 @@ var MediaControllerRuntime = class {
|
|
|
5882
6081
|
this.#pauseTracks(source);
|
|
5883
6082
|
if (cause === "capture_cleanup" && source !== "screen" && ports.media.pauseLocalCapture) {
|
|
5884
6083
|
state.capturePause = "pending";
|
|
5885
|
-
this.#retainPause(state, ports.media.pauseLocalCapture(source));
|
|
5886
6084
|
this.#notifyCapturePauses(ports);
|
|
6085
|
+
this.#retainPause(state, ports.media.pauseLocalCapture(source));
|
|
5887
6086
|
} else {
|
|
5888
6087
|
ports.media.setLocalSourceIntent?.(source, false, cause);
|
|
5889
6088
|
this.#retainPause(state, this.#beginLocalPause(ports, source, cause));
|
|
@@ -5918,6 +6117,7 @@ var MediaControllerRuntime = class {
|
|
|
5918
6117
|
this.#ports = ports;
|
|
5919
6118
|
this.#projection.reset();
|
|
5920
6119
|
this.#syncLive = false;
|
|
6120
|
+
this.#cancelOffRetries();
|
|
5921
6121
|
this.#bindCamera(ports);
|
|
5922
6122
|
if (!ports) {
|
|
5923
6123
|
this.#resetObservations();
|
|
@@ -5964,6 +6164,7 @@ var MediaControllerRuntime = class {
|
|
|
5964
6164
|
this.#removeRequestsFromMissingParticipants(snapshot);
|
|
5965
6165
|
this.#applyRemoteMediaState(ports, snapshot);
|
|
5966
6166
|
this.#reconcileIntents();
|
|
6167
|
+
this.#retryOffs();
|
|
5967
6168
|
this.#notifyCapturePauses(ports);
|
|
5968
6169
|
this.#publish();
|
|
5969
6170
|
});
|
|
@@ -6101,6 +6302,7 @@ var MediaControllerRuntime = class {
|
|
|
6101
6302
|
const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
|
|
6102
6303
|
const media = Object.freeze({
|
|
6103
6304
|
...current.media,
|
|
6305
|
+
microphoneMutedByOtherDevice: this.#microphoneMutedByOtherDevice,
|
|
6104
6306
|
local,
|
|
6105
6307
|
remote,
|
|
6106
6308
|
screenShare: {
|
|
@@ -6491,6 +6693,12 @@ var ParticipantsControllerRuntime = class {
|
|
|
6491
6693
|
#unsubscribePorts = null;
|
|
6492
6694
|
#unsubscribe = null;
|
|
6493
6695
|
#unsubscribeMedia = null;
|
|
6696
|
+
#deviceScope = null;
|
|
6697
|
+
// Retain verified seats while the local Sync replica is recovering, and for older servers
|
|
6698
|
+
// whose negotiated hints disappear with sockets. New servers send every admitted seat.
|
|
6699
|
+
#devices = /* @__PURE__ */ new Map();
|
|
6700
|
+
#presenceCursor = null;
|
|
6701
|
+
#invalidatedDevices = /* @__PURE__ */ new Set();
|
|
6494
6702
|
constructor(connection, store, offline) {
|
|
6495
6703
|
this.#offline = offline;
|
|
6496
6704
|
this.#connection = connection;
|
|
@@ -6544,6 +6752,8 @@ var ParticipantsControllerRuntime = class {
|
|
|
6544
6752
|
return ({ sync }) => foreign3(() => sync.setDisplayName(displayName));
|
|
6545
6753
|
});
|
|
6546
6754
|
dispose() {
|
|
6755
|
+
this.#devices.clear();
|
|
6756
|
+
this.#invalidatedDevices.clear();
|
|
6547
6757
|
this.#unsubscribePorts?.();
|
|
6548
6758
|
this.#unsubscribe?.();
|
|
6549
6759
|
this.#unsubscribeMedia?.();
|
|
@@ -6563,6 +6773,10 @@ var ParticipantsControllerRuntime = class {
|
|
|
6563
6773
|
this.#unsubscribe = null;
|
|
6564
6774
|
this.#unsubscribeMedia = null;
|
|
6565
6775
|
if (!ports) {
|
|
6776
|
+
if (["idle", "leaving", "left", "failed"].includes(this.#connection.getSnapshot().state)) {
|
|
6777
|
+
this.#devices.clear();
|
|
6778
|
+
this.#invalidatedDevices.clear();
|
|
6779
|
+
}
|
|
6566
6780
|
this.#publish(null, {});
|
|
6567
6781
|
return;
|
|
6568
6782
|
}
|
|
@@ -6571,11 +6785,57 @@ var ParticipantsControllerRuntime = class {
|
|
|
6571
6785
|
this.#unsubscribeMedia = ports.media.subscribe(publish);
|
|
6572
6786
|
publish();
|
|
6573
6787
|
}
|
|
6788
|
+
#syncDeviceScope() {
|
|
6789
|
+
const subject = this.#connection.getSnapshot().subject;
|
|
6790
|
+
const scope = subject ? JSON.stringify([subject.tenantId, subject.spaceId, subject.episodeId, subject.participantId, subject.participantGeneration]) : null;
|
|
6791
|
+
if (scope !== this.#deviceScope) {
|
|
6792
|
+
this.#devices.clear();
|
|
6793
|
+
this.#invalidatedDevices.clear();
|
|
6794
|
+
}
|
|
6795
|
+
this.#deviceScope = scope;
|
|
6796
|
+
}
|
|
6797
|
+
#rememberDevices(snapshot) {
|
|
6798
|
+
this.#syncDeviceScope();
|
|
6799
|
+
const control = snapshot?.control;
|
|
6800
|
+
if (!control) return;
|
|
6801
|
+
this.#pruneDevices(control, snapshot.presence);
|
|
6802
|
+
const retained = /* @__PURE__ */ new Map();
|
|
6803
|
+
if (control.status !== "ended") {
|
|
6804
|
+
for (const participant of control.participants) {
|
|
6805
|
+
const device = this.#deviceFor(participant, snapshot.presence?.items ?? []);
|
|
6806
|
+
if (device) retained.set(participant.participantId, device);
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
this.#devices = retained;
|
|
6810
|
+
}
|
|
6811
|
+
#pruneDevices(control, presence) {
|
|
6812
|
+
const cursor = presence ? JSON.stringify([presence.projectionId, presence.sequence]) : null;
|
|
6813
|
+
if (cursor !== this.#presenceCursor) this.#invalidatedDevices.clear();
|
|
6814
|
+
this.#presenceCursor = cursor;
|
|
6815
|
+
for (const [id, device] of this.#devices) {
|
|
6816
|
+
if (!control.participants.some((participant) => participant.participantId === id && participant.admissionRevision === device.admissionRevision)) this.#invalidatedDevices.add(id);
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
#deviceFor(participant, presence) {
|
|
6820
|
+
const previous = this.#devices.get(participant.participantId);
|
|
6821
|
+
const known = previous && previous.admissionRevision === participant.admissionRevision ? previous : {};
|
|
6822
|
+
const hint = this.#hintFor(participant, presence) ?? {};
|
|
6823
|
+
const identityId = hint.identityId ?? known.identityId;
|
|
6824
|
+
return identityId ? { admissionRevision: participant.admissionRevision, identityId, deviceType: hint.deviceType ?? known.deviceType } : void 0;
|
|
6825
|
+
}
|
|
6826
|
+
#hintFor(participant, presence) {
|
|
6827
|
+
return presence.find((item) => {
|
|
6828
|
+
if (item.participantId !== participant.participantId) return false;
|
|
6829
|
+
if (item.admissionRevision !== void 0) return item.admissionRevision === participant.admissionRevision;
|
|
6830
|
+
return !this.#invalidatedDevices.has(participant.participantId);
|
|
6831
|
+
});
|
|
6832
|
+
}
|
|
6574
6833
|
#publish(snapshot, collaborationCapabilities, media) {
|
|
6834
|
+
this.#rememberDevices(snapshot);
|
|
6575
6835
|
if (snapshot && !controlFor(snapshot)) return;
|
|
6576
6836
|
const audioParticipants = new Set(media?.remoteTracks.filter((track) => track.source === "screen_audio" && track.track.readyState === "live").map((track) => track.participantId));
|
|
6577
6837
|
if (snapshot?.participantId && media?.localTracks.some((track) => track.source === "screen_audio" && track.enabled && track.track.readyState === "live")) audioParticipants.add(snapshot.participantId);
|
|
6578
|
-
const projected = participantsFor(snapshot, collaborationCapabilities);
|
|
6838
|
+
const projected = participantsFor(snapshot, collaborationCapabilities, this.#devices);
|
|
6579
6839
|
const participants = Object.freeze({
|
|
6580
6840
|
...projected,
|
|
6581
6841
|
roster: Object.freeze(projected.roster.map((participant) => Object.freeze({ ...participant, media: Object.freeze({ ...participant.media, screenShareHasAudio: participant.media.screenShare === "active" && audioParticipants.has(participant.participantId) }) })))
|
|
@@ -6601,10 +6861,10 @@ function invalid(message) {
|
|
|
6601
6861
|
function directed(result) {
|
|
6602
6862
|
return Object.freeze({ status: result.result, requestId: result.request_id });
|
|
6603
6863
|
}
|
|
6604
|
-
function participantsFor(snapshot, collaborationCapabilities) {
|
|
6864
|
+
function participantsFor(snapshot, collaborationCapabilities, devices) {
|
|
6605
6865
|
const control = controlFor(snapshot);
|
|
6606
|
-
if (!control) return EMPTY_PARTICIPANTS;
|
|
6607
|
-
return Object.freeze({ roster: rosterFor(control.participants, collaborationCapabilities, mediaFor(snapshot), presenceFor(snapshot)), admissionQueue: admissionQueueFor(control.admissionRequests) });
|
|
6866
|
+
if (!control || control.status === "ended") return EMPTY_PARTICIPANTS;
|
|
6867
|
+
return Object.freeze({ roster: rosterFor(control.participants, collaborationCapabilities, mediaFor(snapshot), presenceFor(snapshot), devices), admissionQueue: admissionQueueFor(control.admissionRequests) });
|
|
6608
6868
|
}
|
|
6609
6869
|
function controlFor(snapshot) {
|
|
6610
6870
|
if (!snapshot) return null;
|
|
@@ -6618,11 +6878,13 @@ function mediaFor(snapshot) {
|
|
|
6618
6878
|
function presenceFor(snapshot) {
|
|
6619
6879
|
return snapshot?.presence ?? null;
|
|
6620
6880
|
}
|
|
6621
|
-
function rosterFor(participants, collaborationCapabilities, media, presence) {
|
|
6881
|
+
function rosterFor(participants, collaborationCapabilities, media, presence, devices) {
|
|
6622
6882
|
return Object.freeze(
|
|
6623
6883
|
participants.map(
|
|
6624
6884
|
(participant) => Object.freeze({
|
|
6625
6885
|
participantId: participant.participantId,
|
|
6886
|
+
identityId: devices.get(participant.participantId)?.identityId,
|
|
6887
|
+
deviceType: devices.get(participant.participantId)?.deviceType,
|
|
6626
6888
|
displayName: participant.displayName,
|
|
6627
6889
|
role: participant.role,
|
|
6628
6890
|
eligibleRoles: Object.freeze([...participant.eligibleRoles]),
|
|
@@ -6644,13 +6906,20 @@ function admissionQueueFor(requests) {
|
|
|
6644
6906
|
function selfFor(snapshot, roster) {
|
|
6645
6907
|
const participantId = snapshot?.participantId ?? null;
|
|
6646
6908
|
const participant = roster.find((candidate) => candidate.participantId === participantId);
|
|
6647
|
-
|
|
6909
|
+
const identityId = selfIdentityFor(snapshot, participant);
|
|
6910
|
+
if (!participant) return emptySelf(participantId, identityId);
|
|
6648
6911
|
const capabilities = participant.capabilities;
|
|
6649
6912
|
const allowed = new Set(capabilities);
|
|
6650
|
-
return Object.freeze({ participantId, displayName: participant.displayName, role: participant.role, capabilities, handRaised: participant.handRaised, can: (capability) => allowed.has(capability) });
|
|
6913
|
+
return Object.freeze({ participantId, identityId, displayName: participant.displayName, role: participant.role, capabilities, handRaised: participant.handRaised, can: (capability) => allowed.has(capability) });
|
|
6914
|
+
}
|
|
6915
|
+
function selfIdentityFor(snapshot, participant) {
|
|
6916
|
+
if (!snapshot) return void 0;
|
|
6917
|
+
if (snapshot.connection.phase === "terminal") return void 0;
|
|
6918
|
+
if (snapshot.control?.status === "ended") return void 0;
|
|
6919
|
+
return snapshot.participantIdentityId ?? participant?.identityId;
|
|
6651
6920
|
}
|
|
6652
|
-
function emptySelf(participantId) {
|
|
6653
|
-
return Object.freeze({ participantId, displayName: null, role: null, capabilities: Object.freeze([]), handRaised: false, can: () => false });
|
|
6921
|
+
function emptySelf(participantId, identityId) {
|
|
6922
|
+
return Object.freeze({ participantId, identityId, displayName: null, role: null, capabilities: Object.freeze([]), handRaised: false, can: () => false });
|
|
6654
6923
|
}
|
|
6655
6924
|
function capabilitiesFor(control, collaboration) {
|
|
6656
6925
|
const capabilities = /* @__PURE__ */ new Set();
|
|
@@ -6684,10 +6953,10 @@ function sameParticipantMedia(left, right) {
|
|
|
6684
6953
|
return left.media.microphone === right.media.microphone && left.media.camera === right.media.camera && left.media.screenShare === right.media.screenShare && left.media.screenShareHasAudio === right.media.screenShareHasAudio;
|
|
6685
6954
|
}
|
|
6686
6955
|
function sameParticipantPresence(left, right) {
|
|
6687
|
-
return left.presence.state === right.presence.state && left.presence.speaking === right.presence.speaking && left.presence.activeSpeaker === right.presence.activeSpeaker;
|
|
6956
|
+
return left.identityId === right.identityId && left.deviceType === right.deviceType && left.presence.state === right.presence.state && left.presence.speaking === right.presence.speaking && left.presence.activeSpeaker === right.presence.activeSpeaker;
|
|
6688
6957
|
}
|
|
6689
6958
|
function sameSelf(left, right) {
|
|
6690
|
-
return left.participantId === right.participantId && left.displayName === right.displayName && left.role === right.role && left.handRaised === right.handRaised && sameValues(left.capabilities, right.capabilities);
|
|
6959
|
+
return left.participantId === right.participantId && left.identityId === right.identityId && left.displayName === right.displayName && left.role === right.role && left.handRaised === right.handRaised && sameValues(left.capabilities, right.capabilities);
|
|
6691
6960
|
}
|
|
6692
6961
|
function sameArray(left, right, equal) {
|
|
6693
6962
|
return left.length === right.length && left.every((value, index) => equal(value, right[index]));
|