@q9labsai/chalk-client 5.0.0 → 5.1.0
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 +34 -0
- package/README.md +39 -0
- package/dist/access/base64-url.d.ts +0 -1
- package/dist/access/grant.d.ts +0 -1
- package/dist/access/manager.d.ts +0 -1
- package/dist/chat-files/http-transport.d.ts +0 -1
- package/dist/chat-files/index.d.ts +0 -1
- package/dist/chat-files/types.d.ts +0 -1
- package/dist/{chunk-2HJIJVE2.js → chunk-3ZN7IXOZ.js} +341 -3
- package/dist/{chunk-O2NOZKJP.js → chunk-DWKTGBHR.js} +20 -8
- package/dist/{chunk-AVXJ4HQU.js → chunk-K7PNW3KI.js} +156 -35
- package/dist/{chunk-CMVO7USN.js → chunk-KHHULLBZ.js} +1 -1
- package/dist/{chunk-IPAN4LUZ.js → chunk-RJRSLXVT.js} +863 -409
- package/dist/client.d.ts +76 -1
- package/dist/collaboration/index.d.ts +0 -1
- package/dist/collaboration/types.d.ts +0 -1
- package/dist/collaboration/wire.d.ts +0 -1
- package/dist/connection/dependencies.d.ts +5 -3
- package/dist/connection/diagnostics.d.ts +1 -2
- package/dist/connection/index.d.ts +1 -1
- package/dist/connection/lifecycle.d.ts +2 -3
- package/dist/connection/media-devices.d.ts +0 -1
- package/dist/connection/production.d.ts +0 -1
- package/dist/connection/types.d.ts +2 -2
- package/dist/effect.d.ts +0 -1
- package/dist/effect.js +88 -4
- package/dist/foreground-deadline.d.ts +0 -1
- package/dist/generated/http-api.d.ts +76 -1
- package/dist/generated/schemas.d.ts +366 -7
- package/dist/generated/sync.d.ts +0 -1
- package/dist/generated/whiteboard-v1.d.ts +50 -7
- package/dist/index.d.ts +0 -1
- package/dist/index.js +7 -5
- package/dist/media/camera-uplink.d.ts +0 -1
- package/dist/media/capture-error.d.ts +0 -1
- package/dist/media/client.d.ts +3 -2
- package/dist/media/cloudflare-sfu.d.ts +0 -1
- package/dist/media/index.d.ts +0 -1
- package/dist/media/index.js +1 -1
- package/dist/media/observers.d.ts +0 -1
- package/dist/media/plane.d.ts +0 -1
- package/dist/media/remote-track-response.d.ts +0 -1
- package/dist/media/target.d.ts +0 -1
- package/dist/media/tracks.d.ts +0 -1
- package/dist/media/transport.d.ts +0 -1
- package/dist/media/types.d.ts +0 -1
- package/dist/public-invites/client.d.ts +0 -1
- package/dist/public-invites/index.d.ts +0 -1
- package/dist/public-invites/index.js +2 -2
- package/dist/public-invites/types.d.ts +0 -1
- package/dist/server/access-refresh.d.ts +0 -1
- package/dist/server/{chunk-C4XITY5U.js → chunk-XQOQTCUL.js} +22 -0
- package/dist/server/client.d.ts +0 -1
- package/dist/server/errors.d.cts +8 -1
- package/dist/server/errors.d.ts +8 -2
- package/dist/server/index.cjs +31 -7
- package/dist/server/index.d.cts +1 -1
- package/dist/server/index.d.ts +1 -2
- package/dist/server/index.js +10 -7
- package/dist/server/server-only.d.ts +0 -1
- package/dist/server/server-only.js +1 -1
- package/dist/server/transport.d.ts +0 -1
- package/dist/server/types.d.ts +0 -1
- package/dist/space-client/chat-controller-helpers.d.ts +0 -1
- package/dist/space-client/chat-controller.d.ts +0 -1
- package/dist/space-client/controller-effects.d.ts +0 -1
- package/dist/space-client/core.d.ts +0 -1
- package/dist/space-client/effect.d.ts +0 -1
- package/dist/space-client/errors.d.ts +4 -2
- package/dist/space-client/index.d.ts +1 -2
- package/dist/space-client/media-controller.d.ts +3 -2
- package/dist/space-client/media-device-selection.d.ts +0 -1
- package/dist/space-client/offline-actions-storage.d.ts +0 -1
- package/dist/space-client/offline-actions.d.ts +0 -1
- package/dist/space-client/participants-controller.d.ts +0 -1
- package/dist/space-client/promise-facade.d.ts +0 -1
- package/dist/space-client/reactions-controller.d.ts +0 -1
- package/dist/space-client/recording-controller.d.ts +0 -1
- package/dist/space-client/reload-rejoin.d.ts +0 -1
- package/dist/space-client/space-client.d.ts +0 -1
- package/dist/space-client/store.d.ts +0 -1
- package/dist/space-client/types.d.ts +7 -2
- package/dist/space-client/whiteboard-controller.d.ts +0 -1
- package/dist/sync/browser.d.ts +0 -1
- package/dist/sync/canonical.d.ts +0 -1
- package/dist/sync/dial-deadline.d.ts +3 -0
- package/dist/sync/errors.d.ts +0 -1
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/react-native.d.ts +0 -1
- package/dist/sync/types.d.ts +0 -1
- package/dist/sync/v1-client.d.ts +2 -2
- package/dist/sync/v1-codec.d.ts +0 -1
- package/dist/sync/v1-collaboration-state.d.ts +1 -1
- package/dist/sync/v1-command-scheduler.d.ts +0 -1
- package/dist/sync/v1-create.d.ts +0 -1
- package/dist/sync/v1-deferred.d.ts +0 -1
- package/dist/sync/v1-error.d.ts +0 -1
- package/dist/sync/v1-frame-signature.d.ts +0 -1
- package/dist/sync/v1-health.d.ts +15 -0
- package/dist/sync/v1-live-target-coordinator.d.ts +0 -1
- package/dist/sync/v1-persistence.d.ts +0 -1
- package/dist/sync/v1-platform-persistence.d.ts +0 -1
- package/dist/sync/v1-reducer.d.ts +3 -1
- package/dist/sync/v1-types.d.ts +10 -1
- package/dist/webhooks/errors.d.ts +0 -1
- package/dist/webhooks/generated/event-v1.d.ts +0 -1
- package/dist/webhooks/generated/fixtures-v1.d.ts +0 -1
- package/dist/webhooks/index.d.ts +0 -1
- package/dist/webhooks/processor.d.ts +0 -1
- package/dist/webhooks/server-only.d.ts +0 -1
- package/dist/webhooks/test.d.ts +0 -1
- package/dist/webhooks/types.d.ts +0 -1
- package/dist/webhooks/validate.d.ts +0 -1
- package/dist/webhooks/verify.d.ts +0 -1
- package/dist/whiteboard/file-download-transport.d.ts +0 -1
- package/dist/whiteboard/index.d.ts +5 -1
- package/dist/whiteboard/types.d.ts +28 -2
- package/dist/whiteboard/v1-client.d.ts +3 -2
- package/dist/whiteboard/v1-codec.d.ts +0 -1
- package/dist/whiteboard/v1-create.d.ts +0 -1
- package/dist/whiteboard/v1-multipart.d.ts +0 -1
- package/dist/whiteboard/v1-persistence.d.ts +0 -1
- package/package.json +4 -2
- package/dist/access/base64-url.d.ts.map +0 -1
- package/dist/access/grant.d.ts.map +0 -1
- package/dist/access/grant.test.helpers.d.ts +0 -10
- package/dist/access/grant.test.helpers.d.ts.map +0 -1
- package/dist/access/manager.d.ts.map +0 -1
- package/dist/chat-files/http-transport.d.ts.map +0 -1
- package/dist/chat-files/index.d.ts.map +0 -1
- package/dist/chat-files/types.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/collaboration/index.d.ts.map +0 -1
- package/dist/collaboration/types.d.ts.map +0 -1
- package/dist/collaboration/wire.d.ts.map +0 -1
- package/dist/connection/dependencies.d.ts.map +0 -1
- package/dist/connection/diagnostics.d.ts.map +0 -1
- package/dist/connection/index.d.ts.map +0 -1
- package/dist/connection/lifecycle.d.ts.map +0 -1
- package/dist/connection/media-devices.d.ts.map +0 -1
- package/dist/connection/production.d.ts.map +0 -1
- package/dist/connection/types.d.ts.map +0 -1
- package/dist/effect.d.ts.map +0 -1
- package/dist/foreground-deadline.d.ts.map +0 -1
- package/dist/generated/http-api.d.ts.map +0 -1
- package/dist/generated/schemas.d.ts.map +0 -1
- package/dist/generated/sync.d.ts.map +0 -1
- package/dist/generated/whiteboard-v1.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/media/camera-uplink.d.ts.map +0 -1
- package/dist/media/capture-error.d.ts.map +0 -1
- package/dist/media/client.d.ts.map +0 -1
- package/dist/media/cloudflare-sfu.d.ts.map +0 -1
- package/dist/media/index.d.ts.map +0 -1
- package/dist/media/observers.d.ts.map +0 -1
- package/dist/media/plane.d.ts.map +0 -1
- package/dist/media/remote-track-response.d.ts.map +0 -1
- package/dist/media/target.d.ts.map +0 -1
- package/dist/media/tracks.d.ts.map +0 -1
- package/dist/media/transport.d.ts.map +0 -1
- package/dist/media/types.d.ts.map +0 -1
- package/dist/public-invites/client.d.ts.map +0 -1
- package/dist/public-invites/index.d.ts.map +0 -1
- package/dist/public-invites/types.d.ts.map +0 -1
- package/dist/server/access-refresh.d.ts.map +0 -1
- package/dist/server/client.d.ts.map +0 -1
- package/dist/server/errors.d.ts.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/server-only.d.ts.map +0 -1
- package/dist/server/transport.d.ts.map +0 -1
- package/dist/server/types.d.ts.map +0 -1
- package/dist/space-client/chat-controller-helpers.d.ts.map +0 -1
- package/dist/space-client/chat-controller.d.ts.map +0 -1
- package/dist/space-client/controller-effects.d.ts.map +0 -1
- package/dist/space-client/core.d.ts.map +0 -1
- package/dist/space-client/core.test.helpers.d.ts +0 -15
- package/dist/space-client/core.test.helpers.d.ts.map +0 -1
- package/dist/space-client/effect.d.ts.map +0 -1
- package/dist/space-client/errors.d.ts.map +0 -1
- package/dist/space-client/index.d.ts.map +0 -1
- package/dist/space-client/media-controller.d.ts.map +0 -1
- package/dist/space-client/media-device-selection.d.ts.map +0 -1
- package/dist/space-client/offline-actions-storage.d.ts.map +0 -1
- package/dist/space-client/offline-actions.d.ts.map +0 -1
- package/dist/space-client/participants-controller.d.ts.map +0 -1
- package/dist/space-client/promise-facade.d.ts.map +0 -1
- package/dist/space-client/reactions-controller.d.ts.map +0 -1
- package/dist/space-client/recording-controller.d.ts.map +0 -1
- package/dist/space-client/reload-rejoin.d.ts.map +0 -1
- package/dist/space-client/space-client.d.ts.map +0 -1
- package/dist/space-client/store.d.ts.map +0 -1
- package/dist/space-client/terminal.test.helpers.d.ts +0 -136
- package/dist/space-client/terminal.test.helpers.d.ts.map +0 -1
- package/dist/space-client/types.d.ts.map +0 -1
- package/dist/space-client/whiteboard-controller.d.ts.map +0 -1
- package/dist/sync/browser.d.ts.map +0 -1
- package/dist/sync/canonical.d.ts.map +0 -1
- package/dist/sync/errors.d.ts.map +0 -1
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/react-native.d.ts.map +0 -1
- package/dist/sync/types.d.ts.map +0 -1
- package/dist/sync/v1-client.d.ts.map +0 -1
- package/dist/sync/v1-codec.d.ts.map +0 -1
- package/dist/sync/v1-collaboration-state.d.ts.map +0 -1
- package/dist/sync/v1-command-scheduler.d.ts.map +0 -1
- package/dist/sync/v1-create.d.ts.map +0 -1
- package/dist/sync/v1-deferred.d.ts.map +0 -1
- package/dist/sync/v1-error.d.ts.map +0 -1
- package/dist/sync/v1-frame-signature.d.ts.map +0 -1
- package/dist/sync/v1-live-target-coordinator.d.ts.map +0 -1
- package/dist/sync/v1-persistence.d.ts.map +0 -1
- package/dist/sync/v1-platform-persistence.d.ts.map +0 -1
- package/dist/sync/v1-reducer.d.ts.map +0 -1
- package/dist/sync/v1-types.d.ts.map +0 -1
- package/dist/webhooks/errors.d.ts.map +0 -1
- package/dist/webhooks/generated/event-v1.d.ts.map +0 -1
- package/dist/webhooks/generated/fixtures-v1.d.ts.map +0 -1
- package/dist/webhooks/index.d.ts.map +0 -1
- package/dist/webhooks/processor.d.ts.map +0 -1
- package/dist/webhooks/server-only.d.ts.map +0 -1
- package/dist/webhooks/test.d.ts.map +0 -1
- package/dist/webhooks/types.d.ts.map +0 -1
- package/dist/webhooks/validate.d.ts.map +0 -1
- package/dist/webhooks/verify.d.ts.map +0 -1
- package/dist/whiteboard/file-download-transport.d.ts.map +0 -1
- package/dist/whiteboard/index.d.ts.map +0 -1
- package/dist/whiteboard/types.d.ts.map +0 -1
- package/dist/whiteboard/v1-client.d.ts.map +0 -1
- package/dist/whiteboard/v1-codec.d.ts.map +0 -1
- package/dist/whiteboard/v1-create.d.ts.map +0 -1
- package/dist/whiteboard/v1-multipart.d.ts.map +0 -1
- package/dist/whiteboard/v1-persistence.d.ts.map +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AccessGrantError,
|
|
3
3
|
requireParsedAccessGrant
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3ZN7IXOZ.js";
|
|
5
5
|
import {
|
|
6
6
|
CloudflareSFUClient,
|
|
7
7
|
createCloudflareSFUHTTPTransport,
|
|
8
8
|
describeMediaCaptureError
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-DWKTGBHR.js";
|
|
10
10
|
import {
|
|
11
11
|
ChatAttachmentMimeTypeSchema,
|
|
12
12
|
ChatReactionSetFrameSchema,
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
V1SyncError,
|
|
17
17
|
createBrowserSyncLifecycle,
|
|
18
18
|
createBrowserWebSocketFactory,
|
|
19
|
-
createV1SyncClient
|
|
20
|
-
|
|
19
|
+
createV1SyncClient,
|
|
20
|
+
scheduleSyncDialDeadline
|
|
21
|
+
} from "./chunk-K7PNW3KI.js";
|
|
21
22
|
|
|
22
23
|
// src/access/manager.ts
|
|
23
24
|
import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
|
|
@@ -80,7 +81,7 @@ function fetchAccessGrant(provider, input) {
|
|
|
80
81
|
try: () => validateFetchedAccess(currentAccess, next, now, shouldReplaceMediaConnection),
|
|
81
82
|
catch: (cause) => new ConnectionAccessFailure({ code: "access.invalid", cause })
|
|
82
83
|
});
|
|
83
|
-
return [{ access: next, automaticMediaReplacement:
|
|
84
|
+
return [{ access: next, automaticMediaReplacement: currentAccess !== null && !input.replaceMediaConnection && !hasSameMediaConnection(currentAccess, next) }, { access: next }];
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
function connectionAccessRequest(input, replaceMediaConnection) {
|
|
@@ -114,9 +115,10 @@ function validateExpiration(access, now) {
|
|
|
114
115
|
}
|
|
115
116
|
function validateRefresh(previous, next, replaceMediaConnection) {
|
|
116
117
|
if (!hasSameSubject(previous, next)) throw new TypeError("Access grant refresh changed its subject");
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
if (replaceMediaConnection && hasSameMediaConnection(previous, next)) throw new TypeError("Access grant replacement did not change its media binding");
|
|
119
|
+
}
|
|
120
|
+
function hasSameMediaConnection(previous, next) {
|
|
121
|
+
return previous.media.provider === next.media.provider && mediaBinding(previous) === mediaBinding(next);
|
|
120
122
|
}
|
|
121
123
|
function hasSameSubject(previous, next) {
|
|
122
124
|
const left = previous.subject;
|
|
@@ -383,6 +385,11 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
383
385
|
presentation: {
|
|
384
386
|
name: "presentation_v1",
|
|
385
387
|
exactFields: ["name"]
|
|
388
|
+
},
|
|
389
|
+
view: {
|
|
390
|
+
name: "view_v1",
|
|
391
|
+
exactFields: ["name"],
|
|
392
|
+
extendedExactFields: ["name", "origins"]
|
|
386
393
|
}
|
|
387
394
|
},
|
|
388
395
|
identity: {
|
|
@@ -399,7 +406,7 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
399
406
|
},
|
|
400
407
|
{
|
|
401
408
|
id: "live",
|
|
402
|
-
accepts: ["submit_update", "submit_update_part", "request_snapshot", "snapshot_ack", "clear", "set_draw_permission", "set_presentation", "cursor", "ping"],
|
|
409
|
+
accepts: ["submit_update", "submit_update_part", "request_snapshot", "snapshot_ack", "clear", "set_draw_permission", "set_presentation", "cursor", "view", "ping"],
|
|
403
410
|
heartbeatIntervalMs: 2e4,
|
|
404
411
|
missedDeadlinesBeforeClose: 2
|
|
405
412
|
}
|
|
@@ -479,6 +486,14 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
479
486
|
clientExactFields: ["type", "x", "y"],
|
|
480
487
|
serverExactFields: ["type", "participant_id", "display_name", "x", "y", "occurred_at"]
|
|
481
488
|
},
|
|
489
|
+
view: {
|
|
490
|
+
maxCoordinate: 1e6,
|
|
491
|
+
maxExtent: 2e6,
|
|
492
|
+
epochField: "origin_epoch",
|
|
493
|
+
originFields: ["origin_participant_id", "origin_sequence"],
|
|
494
|
+
clientExactFields: ["type", "x", "y", "width", "height", "bring"],
|
|
495
|
+
serverExactFields: ["type", "participant_id", "display_name", "x", "y", "width", "height", "bring", "occurred_at"]
|
|
496
|
+
},
|
|
482
497
|
welcome: {
|
|
483
498
|
exactFields: ["type", "protocol", "participant_id", "participant_generation", "capabilities", "participant_capabilities", "scene_id", "revision", "can_draw"],
|
|
484
499
|
extendedExactFields: ["type", "protocol", "participant_id", "participant_generation", "capabilities", "participant_capabilities", "scene_id", "revision", "can_draw", "presenting"]
|
|
@@ -508,8 +523,8 @@ var WhiteboardV1ProtocolMetadata = {
|
|
|
508
523
|
exactFields: ["type", "correlation_id", "operation", "code", "recoverable", "message"]
|
|
509
524
|
}
|
|
510
525
|
},
|
|
511
|
-
clientFrames: ["hello", "submit_update", "submit_update_part", "request_snapshot", "snapshot_ack", "clear", "set_draw_permission", "set_presentation", "cursor", "ping"],
|
|
512
|
-
serverFrames: ["welcome", "snapshot_page", "update", "update_part", "commit", "cursor", "permission_updated", "presentation_updated", "reset_required", "operation_error", "pong"],
|
|
526
|
+
clientFrames: ["hello", "submit_update", "submit_update_part", "request_snapshot", "snapshot_ack", "clear", "set_draw_permission", "set_presentation", "cursor", "view", "ping"],
|
|
527
|
+
serverFrames: ["welcome", "snapshot_page", "update", "update_part", "commit", "cursor", "view", "permission_updated", "presentation_updated", "reset_required", "operation_error", "pong"],
|
|
513
528
|
receiptOperations: ["submit_update", "clear", "set_draw_permission", "set_presentation"],
|
|
514
529
|
receiptOutcomes: ["committed", "duplicate"],
|
|
515
530
|
resetReasons: ["scene_changed", "cursor_expired", "gap"],
|
|
@@ -612,7 +627,7 @@ function isClientFrame(value) {
|
|
|
612
627
|
if (!isRecord2(value)) return false;
|
|
613
628
|
switch (value.type) {
|
|
614
629
|
case "hello":
|
|
615
|
-
return (hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.hello.exactFields) || hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.hello.extendedExactFields) &&
|
|
630
|
+
return (hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.hello.exactFields) || hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.hello.extendedExactFields) && isExtensions(value.extensions)) && value.protocol === "whiteboard-v1" && typeof value.token === "string" && utf8Bytes(value.token) > 0 && utf8Bytes(value.token) <= WhiteboardV1ProtocolLimits.tokenBytes && (value.cursor === null || isCursor(value.cursor));
|
|
616
631
|
case "submit_update":
|
|
617
632
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.submitUpdate.exactFields) && isBoundedIdentifier(value.operation_id, WhiteboardV1ProtocolLimits.operationIdMinBytes, WhiteboardV1ProtocolLimits.operationIdMaxBytes) && isUuid(value.scene_id) && typeof value.sync_all === "boolean" && isElementBatch(value.elements, WhiteboardV1ProtocolLimits.elementBatchMaxItems);
|
|
618
633
|
case "submit_update_part":
|
|
@@ -629,17 +644,32 @@ function isClientFrame(value) {
|
|
|
629
644
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.setPresentation.exactFields) && isBoundedIdentifier(value.operation_id, WhiteboardV1ProtocolLimits.operationIdMinBytes, WhiteboardV1ProtocolLimits.operationIdMaxBytes) && typeof value.presenting === "boolean";
|
|
630
645
|
case "cursor":
|
|
631
646
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.cursor.clientExactFields) && isFiniteNumber(value.x) && isFiniteNumber(value.y) && encodedWhiteboardV1FrameBytes(value) <= WhiteboardV1ProtocolLimits.cursorFrameBytes;
|
|
647
|
+
case "view":
|
|
648
|
+
return isViewFields(value, WhiteboardV1ProtocolMetadata.frames.view.clientExactFields) && isViewRect(value) && typeof value.bring === "boolean" && encodedWhiteboardV1FrameBytes(value) <= WhiteboardV1ProtocolLimits.cursorFrameBytes;
|
|
632
649
|
case "ping":
|
|
633
650
|
return hasExactKeys(value, ["type"]);
|
|
634
651
|
default:
|
|
635
652
|
return false;
|
|
636
653
|
}
|
|
637
654
|
}
|
|
655
|
+
function isViewFields(value, fields) {
|
|
656
|
+
if (hasExactRecordKeys(value, fields)) return true;
|
|
657
|
+
const originFields = [...fields, ...WhiteboardV1ProtocolMetadata.frames.view.originFields];
|
|
658
|
+
const validFields = hasExactRecordKeys(value, originFields) || hasExactRecordKeys(value, [...originFields, WhiteboardV1ProtocolMetadata.frames.view.epochField]) && isUuid(value.origin_epoch);
|
|
659
|
+
return validFields && isUuid(value.origin_participant_id) && Number.isSafeInteger(value.origin_sequence) && Number(value.origin_sequence) > 0;
|
|
660
|
+
}
|
|
661
|
+
function isViewRect(value) {
|
|
662
|
+
return isFiniteNumber(value.x) && isFiniteNumber(value.y) && isFiniteNumber(value.width) && isFiniteNumber(value.height) && value.width > 0 && value.height > 0 && Math.abs(value.x) <= WhiteboardV1ProtocolMetadata.frames.view.maxCoordinate && Math.abs(value.y) <= WhiteboardV1ProtocolMetadata.frames.view.maxCoordinate && value.width <= WhiteboardV1ProtocolMetadata.frames.view.maxExtent && value.height <= WhiteboardV1ProtocolMetadata.frames.view.maxExtent;
|
|
663
|
+
}
|
|
638
664
|
function isAppState(value) {
|
|
639
665
|
return value === null || hasExactKeys(value, WhiteboardV1ProtocolMetadata.sharedAppState.exactFields) && typeof value.view_background_color === "string" && utf8Bytes(value.view_background_color) <= 64;
|
|
640
666
|
}
|
|
641
|
-
function
|
|
642
|
-
return
|
|
667
|
+
function isExtension(value, extension) {
|
|
668
|
+
return hasExactKeys(value, extension.exactFields) && value.name === extension.name;
|
|
669
|
+
}
|
|
670
|
+
function isExtensions(value) {
|
|
671
|
+
const { presentation, view } = WhiteboardV1ProtocolMetadata.extensions;
|
|
672
|
+
return Array.isArray(value) && (value.length === 1 || value.length === 2) && isExtension(value[0], presentation) && (value.length === 1 || isExtension(value[1], view) || hasExactKeys(value[1], view.extendedExactFields) && value[1].name === view.name && value[1].origins === true);
|
|
643
673
|
}
|
|
644
674
|
function isWelcome(value) {
|
|
645
675
|
const exact = hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.welcome.exactFields) || hasExactRecordKeys(value, WhiteboardV1ProtocolMetadata.frames.welcome.extendedExactFields) && typeof value.presenting === "boolean";
|
|
@@ -660,6 +690,8 @@ function isServerFrame(value) {
|
|
|
660
690
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.commit.exactFields) && isBoundedIdentifier(value.operation_id, WhiteboardV1ProtocolLimits.operationIdMinBytes, WhiteboardV1ProtocolLimits.operationIdMaxBytes) && typeof value.outcome === "string" && WhiteboardV1ProtocolMetadata.receiptOutcomes.some((outcome) => outcome === value.outcome) && isUuid(value.scene_id) && isUnsignedDecimal(value.revision);
|
|
661
691
|
case "cursor":
|
|
662
692
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.cursor.serverExactFields) && isUuid(value.participant_id) && typeof value.display_name === "string" && utf8Bytes(value.display_name) > 0 && utf8Bytes(value.display_name) <= WhiteboardV1ProtocolLimits.displayNameMaxBytes && isFiniteNumber(value.x) && isFiniteNumber(value.y) && typeof value.occurred_at === "string" && !Number.isNaN(Date.parse(value.occurred_at)) && encodedWhiteboardV1FrameBytes(value) <= WhiteboardV1ProtocolLimits.cursorFrameBytes;
|
|
693
|
+
case "view":
|
|
694
|
+
return isViewFields(value, WhiteboardV1ProtocolMetadata.frames.view.serverExactFields) && isUuid(value.participant_id) && typeof value.display_name === "string" && utf8Bytes(value.display_name) > 0 && utf8Bytes(value.display_name) <= WhiteboardV1ProtocolLimits.displayNameMaxBytes && isViewRect(value) && typeof value.bring === "boolean" && typeof value.occurred_at === "string" && !Number.isNaN(Date.parse(value.occurred_at)) && encodedWhiteboardV1FrameBytes(value) <= WhiteboardV1ProtocolLimits.cursorFrameBytes;
|
|
663
695
|
case "permission_updated":
|
|
664
696
|
return hasExactKeys(value, WhiteboardV1ProtocolMetadata.frames.permissionUpdated.exactFields) && isUuid(value.participant_id) && typeof value.can_draw === "boolean";
|
|
665
697
|
case "presentation_updated":
|
|
@@ -890,6 +922,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
890
922
|
#options;
|
|
891
923
|
#store;
|
|
892
924
|
#listeners = /* @__PURE__ */ new Set();
|
|
925
|
+
#latestViews = /* @__PURE__ */ new Map();
|
|
893
926
|
#summaryListeners = /* @__PURE__ */ new Set();
|
|
894
927
|
#operations = /* @__PURE__ */ new Map();
|
|
895
928
|
#reservedOperationIds = /* @__PURE__ */ new Set();
|
|
@@ -900,6 +933,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
900
933
|
#updateAssembler = new WhiteboardV1UpdateAssembler();
|
|
901
934
|
#updateAssemblyTimers = /* @__PURE__ */ new Map();
|
|
902
935
|
#socket = null;
|
|
936
|
+
#cancelDial;
|
|
903
937
|
#started = false;
|
|
904
938
|
#startupGeneration = 0;
|
|
905
939
|
#phase = "idle";
|
|
@@ -909,8 +943,12 @@ var ChalkWhiteboardV1Client = class {
|
|
|
909
943
|
#capabilities = [];
|
|
910
944
|
#canDraw = false;
|
|
911
945
|
#presentationSupported = false;
|
|
946
|
+
#viewsSupported = false;
|
|
912
947
|
#presenting = false;
|
|
913
|
-
#
|
|
948
|
+
#hello = "versioned-view";
|
|
949
|
+
#viewSequence = 0;
|
|
950
|
+
#viewEpoch = crypto.randomUUID();
|
|
951
|
+
#pendingBring = null;
|
|
914
952
|
#summaryStatus = "unsubscribed";
|
|
915
953
|
#summaryError = null;
|
|
916
954
|
#latestSnapshot = null;
|
|
@@ -957,7 +995,8 @@ var ChalkWhiteboardV1Client = class {
|
|
|
957
995
|
if (this.#started && this.#startPromise) return this.#startPromise;
|
|
958
996
|
this.#started = true;
|
|
959
997
|
globalThis.document?.addEventListener("visibilitychange", this.#visibilityChanged);
|
|
960
|
-
this.#
|
|
998
|
+
this.#hello = "versioned-view";
|
|
999
|
+
this.#viewsSupported = false;
|
|
961
1000
|
this.#phase = "connecting";
|
|
962
1001
|
this.#publishSummary("loading", null);
|
|
963
1002
|
const generation = ++this.#startupGeneration;
|
|
@@ -981,11 +1020,15 @@ var ChalkWhiteboardV1Client = class {
|
|
|
981
1020
|
}
|
|
982
1021
|
#stopNow() {
|
|
983
1022
|
this.#started = false;
|
|
1023
|
+
this.#viewsSupported = false;
|
|
1024
|
+
this.#pendingBring = null;
|
|
1025
|
+
this.#latestViews.clear();
|
|
984
1026
|
this.#removeVisibilityListener();
|
|
985
1027
|
this.#startupGeneration += 1;
|
|
986
1028
|
this.#unsubscribeLifecycle?.();
|
|
987
1029
|
this.#unsubscribeLifecycle = void 0;
|
|
988
1030
|
this.#clearReconnect();
|
|
1031
|
+
this.#clearDial();
|
|
989
1032
|
this.#clearHeartbeat();
|
|
990
1033
|
this.#clearOperationRetryTimers();
|
|
991
1034
|
this.#clearUpdateAssemblies();
|
|
@@ -1008,12 +1051,18 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1008
1051
|
subscribe(listener) {
|
|
1009
1052
|
this.#listeners.add(listener);
|
|
1010
1053
|
if (this.#phase !== "live") return () => this.#listeners.delete(listener);
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1014
|
-
if (this.#snapshots.size === 0 && !this.#waitingForOperations) void this.#requestSnapshot().catch(() => void 0);
|
|
1054
|
+
const currentSnapshot = this.#currentSnapshot();
|
|
1055
|
+
if (currentSnapshot) listener(currentSnapshot);
|
|
1056
|
+
for (const view of this.#latestViews.values()) listener({ ...view, bring: false });
|
|
1057
|
+
if (!currentSnapshot && this.#snapshots.size === 0 && !this.#waitingForOperations) void this.#requestSnapshot().catch(() => void 0);
|
|
1015
1058
|
return () => this.#listeners.delete(listener);
|
|
1016
1059
|
}
|
|
1060
|
+
#currentSnapshot() {
|
|
1061
|
+
const snapshot = this.#latestSnapshot;
|
|
1062
|
+
if (snapshot?.sceneId !== this.#sceneId) return null;
|
|
1063
|
+
if (snapshot?.revision !== this.#revision) return null;
|
|
1064
|
+
return snapshot;
|
|
1065
|
+
}
|
|
1017
1066
|
subscribeSummary(listener) {
|
|
1018
1067
|
this.#summaryListeners.add(listener);
|
|
1019
1068
|
this.#notifySummaryListener(listener, this.#summary());
|
|
@@ -1034,7 +1083,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1034
1083
|
});
|
|
1035
1084
|
}
|
|
1036
1085
|
sendCursor(input) {
|
|
1037
|
-
if (this.#
|
|
1086
|
+
if (!this.#presenceReady || !this.#canDraw) return;
|
|
1038
1087
|
const now = this.#now();
|
|
1039
1088
|
if (now - this.#lastCursorAt < 1e3 / WhiteboardV1ProtocolLimits.cursorRatePerSecond) return;
|
|
1040
1089
|
const frame = { type: "cursor", x: input.x, y: input.y };
|
|
@@ -1042,6 +1091,40 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1042
1091
|
this.#lastCursorAt = now;
|
|
1043
1092
|
this.#send(frame);
|
|
1044
1093
|
}
|
|
1094
|
+
get supportsViews() {
|
|
1095
|
+
return this.#started && this.#viewsSupported && this.#viewExtensionEnabled;
|
|
1096
|
+
}
|
|
1097
|
+
get #viewExtensionEnabled() {
|
|
1098
|
+
return this.#hello === "view" || this.#hello === "versioned-view";
|
|
1099
|
+
}
|
|
1100
|
+
get #presenceReady() {
|
|
1101
|
+
return this.#phase === "live" && this.#summaryStatus === "ready" && this.#snapshots.size === 0;
|
|
1102
|
+
}
|
|
1103
|
+
sendView(view) {
|
|
1104
|
+
if (!this.#started || !this.#viewExtensionEnabled) return false;
|
|
1105
|
+
const frame = this.#viewFrame(view);
|
|
1106
|
+
encodeWhiteboardV1ClientFrame(frame);
|
|
1107
|
+
if (!this.#presenceReady) {
|
|
1108
|
+
if (view.bring) this.#pendingBring = { sceneId: this.#sceneId, view: { ...view } };
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
1111
|
+
if (view.bring) this.#pendingBring = null;
|
|
1112
|
+
this.#send(frame);
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
#viewFrame(view) {
|
|
1116
|
+
const rectangle = { type: "view", x: view.x, y: view.y, width: view.width, height: view.height, bring: view.bring };
|
|
1117
|
+
if (view.bring || this.#hello !== "versioned-view") return rectangle;
|
|
1118
|
+
if (view.originParticipantId !== void 0) return { ...rectangle, origin_participant_id: view.originParticipantId, origin_sequence: view.originSequence, ...view.originEpoch !== void 0 ? { origin_epoch: view.originEpoch } : {} };
|
|
1119
|
+
if (!this.#participantId) return rectangle;
|
|
1120
|
+
return { ...rectangle, origin_participant_id: this.#participantId, origin_sequence: ++this.#viewSequence, origin_epoch: this.#viewEpoch };
|
|
1121
|
+
}
|
|
1122
|
+
#flushPendingBring() {
|
|
1123
|
+
if (!this.#presenceReady) return;
|
|
1124
|
+
const pending = this.#pendingBring;
|
|
1125
|
+
this.#pendingBring = null;
|
|
1126
|
+
if (pending && (pending.sceneId === null || pending.sceneId === this.#sceneId)) this.sendView(pending.view);
|
|
1127
|
+
}
|
|
1045
1128
|
requestSnapshot() {
|
|
1046
1129
|
this.#assertLive("request_snapshot");
|
|
1047
1130
|
return this.#requestSnapshot();
|
|
@@ -1128,7 +1211,21 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1128
1211
|
this.#phase = "connecting";
|
|
1129
1212
|
const socket = this.#options.webSocket.connect(this.#options.url);
|
|
1130
1213
|
this.#socket = socket;
|
|
1131
|
-
|
|
1214
|
+
this.#cancelDial = scheduleSyncDialDeadline(
|
|
1215
|
+
this.#clock(),
|
|
1216
|
+
() => {
|
|
1217
|
+
this.#cancelDial = void 0;
|
|
1218
|
+
socket.close(CLIENT_RESTART_CLOSE_CODE, "WebSocket dial timeout");
|
|
1219
|
+
this.#disconnected(socket, CLIENT_RESTART_CLOSE_CODE);
|
|
1220
|
+
this.#publishSummary("recovering", failure("start_scene_subscription", "unavailable", true, "The board connection timed out before opening."));
|
|
1221
|
+
},
|
|
1222
|
+
this.#options.dialTimeoutMs
|
|
1223
|
+
);
|
|
1224
|
+
socket.onopen = () => {
|
|
1225
|
+
if (socket !== this.#socket) return;
|
|
1226
|
+
this.#clearDial();
|
|
1227
|
+
void this.#authenticate(socket);
|
|
1228
|
+
};
|
|
1132
1229
|
socket.onmessage = (event) => {
|
|
1133
1230
|
this.#inbound = this.#inbound.then(() => this.#receive(socket, event.data));
|
|
1134
1231
|
};
|
|
@@ -1142,7 +1239,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1142
1239
|
this.#phase = "authenticating";
|
|
1143
1240
|
this.#presentationSupported = false;
|
|
1144
1241
|
const cursor = this.#sceneId && this.#revision ? { scene_id: this.#sceneId, revision: this.#revision } : null;
|
|
1145
|
-
this.#send(this.#
|
|
1242
|
+
this.#send(helloFrame(this.#hello, token, cursor));
|
|
1146
1243
|
} catch {
|
|
1147
1244
|
socket.close(CLIENT_RESTART_CLOSE_CODE, "whiteboard authentication failed");
|
|
1148
1245
|
}
|
|
@@ -1193,6 +1290,22 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1193
1290
|
occurredAt: frame.occurred_at
|
|
1194
1291
|
});
|
|
1195
1292
|
return;
|
|
1293
|
+
case "view": {
|
|
1294
|
+
this.#requireLive();
|
|
1295
|
+
const event = {
|
|
1296
|
+
type: "view",
|
|
1297
|
+
participantId: frame.participant_id,
|
|
1298
|
+
displayName: frame.display_name,
|
|
1299
|
+
x: frame.x,
|
|
1300
|
+
y: frame.y,
|
|
1301
|
+
width: frame.width,
|
|
1302
|
+
height: frame.height,
|
|
1303
|
+
bring: frame.bring,
|
|
1304
|
+
occurredAt: frame.occurred_at
|
|
1305
|
+
};
|
|
1306
|
+
this.#emit(frame.origin_participant_id ? { ...event, originParticipantId: frame.origin_participant_id, originSequence: frame.origin_sequence, ...frame.origin_epoch ? { originEpoch: frame.origin_epoch } : {} } : event);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1196
1309
|
case "permission_updated":
|
|
1197
1310
|
this.#requireLive();
|
|
1198
1311
|
if (frame.participant_id === this.#participantId) this.#canDraw = frame.can_draw;
|
|
@@ -1200,6 +1313,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1200
1313
|
return;
|
|
1201
1314
|
case "presentation_updated":
|
|
1202
1315
|
this.#requireLive();
|
|
1316
|
+
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1203
1317
|
this.#sceneId = frame.scene_id;
|
|
1204
1318
|
this.#revision = frame.revision;
|
|
1205
1319
|
this.#presenting = frame.presenting;
|
|
@@ -1207,6 +1321,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1207
1321
|
return;
|
|
1208
1322
|
case "reset_required":
|
|
1209
1323
|
this.#requireLive();
|
|
1324
|
+
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1210
1325
|
this.#sceneId = frame.scene_id;
|
|
1211
1326
|
this.#revision = null;
|
|
1212
1327
|
this.#publishSummary("recovering", failure("request_snapshot", "cursor_reset_required", true, "Whiteboard snapshot recovery is required."));
|
|
@@ -1227,6 +1342,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1227
1342
|
}
|
|
1228
1343
|
#update(frame) {
|
|
1229
1344
|
this.#requireLive();
|
|
1345
|
+
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1230
1346
|
this.#sceneId = frame.scene_id;
|
|
1231
1347
|
this.#revision = frame.revision;
|
|
1232
1348
|
this.#publishSummary("ready", null);
|
|
@@ -1261,8 +1377,10 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1261
1377
|
#welcome(frame) {
|
|
1262
1378
|
if (this.#phase !== "authenticating") throw new Error("unexpected whiteboard welcome");
|
|
1263
1379
|
this.#phase = "live";
|
|
1380
|
+
this.#viewsSupported = this.#hello === "view" || this.#hello === "versioned-view";
|
|
1264
1381
|
this.#reconnectAttempt = 0;
|
|
1265
1382
|
this.#participantId = frame.participant_id;
|
|
1383
|
+
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1266
1384
|
this.#sceneId = frame.scene_id;
|
|
1267
1385
|
this.#revision = frame.revision;
|
|
1268
1386
|
this.#capabilities = [...frame.capabilities];
|
|
@@ -1325,6 +1443,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1325
1443
|
}
|
|
1326
1444
|
#completeSnapshot(frame, assembly, elements) {
|
|
1327
1445
|
this.#snapshots.delete(frame.request_id);
|
|
1446
|
+
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1328
1447
|
this.#sceneId = frame.scene_id;
|
|
1329
1448
|
this.#revision = frame.revision;
|
|
1330
1449
|
this.#publishSummary("ready", null);
|
|
@@ -1388,6 +1507,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1388
1507
|
if (!entry) return;
|
|
1389
1508
|
this.#operations.delete(frame.operation_id);
|
|
1390
1509
|
this.#clearOperationRetryTimer(frame.operation_id);
|
|
1510
|
+
if (this.#sceneId !== frame.scene_id) this.#latestViews.clear();
|
|
1391
1511
|
this.#sceneId = frame.scene_id;
|
|
1392
1512
|
this.#revision = frame.revision;
|
|
1393
1513
|
this.#awaitingOperationIds.delete(frame.operation_id);
|
|
@@ -1450,10 +1570,11 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1450
1570
|
#disconnected(socket, closeCode) {
|
|
1451
1571
|
if (socket !== this.#socket) return;
|
|
1452
1572
|
const rejectedPresentationHello = this.#rejectedPresentationHello(closeCode);
|
|
1573
|
+
const wasLive = this.#phase === "live";
|
|
1453
1574
|
this.#resetDisconnectedState();
|
|
1454
1575
|
if (!this.#started || !this.#transportAvailable) return;
|
|
1455
1576
|
if (rejectedPresentationHello) {
|
|
1456
|
-
this.#
|
|
1577
|
+
this.#fallbackHello();
|
|
1457
1578
|
this.#scheduleReconnect();
|
|
1458
1579
|
return;
|
|
1459
1580
|
}
|
|
@@ -1461,13 +1582,19 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1461
1582
|
this.#failRejectedConnection(closeCode);
|
|
1462
1583
|
return;
|
|
1463
1584
|
}
|
|
1585
|
+
if (wasLive) this.#hello = "versioned-view";
|
|
1464
1586
|
this.#scheduleReconnect();
|
|
1465
1587
|
}
|
|
1466
1588
|
#rejectedPresentationHello(closeCode) {
|
|
1467
|
-
return this.#phase === "authenticating" &&
|
|
1589
|
+
return this.#phase === "authenticating" && this.#hello !== "legacy" && terminalCloseCode(closeCode);
|
|
1590
|
+
}
|
|
1591
|
+
#fallbackHello() {
|
|
1592
|
+
this.#hello = this.#hello === "versioned-view" ? "view" : this.#hello === "view" ? "presentation" : "legacy";
|
|
1593
|
+
if (this.#hello !== "view") this.#pendingBring = null;
|
|
1468
1594
|
}
|
|
1469
1595
|
#resetDisconnectedState() {
|
|
1470
1596
|
this.#socket = null;
|
|
1597
|
+
this.#clearDial();
|
|
1471
1598
|
this.#clearHeartbeat();
|
|
1472
1599
|
this.#clearUpdateAssemblies();
|
|
1473
1600
|
this.#phase = "connecting";
|
|
@@ -1484,6 +1611,10 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1484
1611
|
if (this.#initialSnapshot) rejectDeferred(this.#initialSnapshot, error(terminalFailure));
|
|
1485
1612
|
this.#rejectOperationCallers(terminalFailure);
|
|
1486
1613
|
}
|
|
1614
|
+
#clearDial() {
|
|
1615
|
+
this.#cancelDial?.();
|
|
1616
|
+
this.#cancelDial = void 0;
|
|
1617
|
+
}
|
|
1487
1618
|
#scheduleReconnect() {
|
|
1488
1619
|
this.#clearReconnect();
|
|
1489
1620
|
const configuredDelay = this.#options.reconnectDelayMs;
|
|
@@ -1527,11 +1658,21 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1527
1658
|
}
|
|
1528
1659
|
#emit(event) {
|
|
1529
1660
|
if (event.type === "snapshot") this.#latestSnapshot = event;
|
|
1661
|
+
if (event.type === "view") {
|
|
1662
|
+
this.#latestViews.delete(event.participantId);
|
|
1663
|
+
this.#latestViews.set(event.participantId, event);
|
|
1664
|
+
if (this.#latestViews.size > 256) {
|
|
1665
|
+
const oldest = this.#latestViews.keys().next().value;
|
|
1666
|
+
if (oldest !== void 0) this.#latestViews.delete(oldest);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1530
1669
|
for (const listener of this.#listeners) listener(event);
|
|
1531
1670
|
}
|
|
1532
1671
|
#publishSummary(status, summaryError) {
|
|
1533
1672
|
this.#summaryStatus = status;
|
|
1534
1673
|
this.#summaryError = summaryError;
|
|
1674
|
+
if (status === "failed") this.#pendingBring = null;
|
|
1675
|
+
if (status === "ready") this.#flushPendingBring();
|
|
1535
1676
|
const summary = this.#summary();
|
|
1536
1677
|
try {
|
|
1537
1678
|
this.#options.onSummary?.(summary);
|
|
@@ -1752,6 +1893,12 @@ function operationName(frame) {
|
|
|
1752
1893
|
function retryableOperationCode(code) {
|
|
1753
1894
|
return code === "unavailable" || code === "overloaded" || code === "rate_limited" || code === "storage_unavailable";
|
|
1754
1895
|
}
|
|
1896
|
+
function helloFrame(level, token, cursor) {
|
|
1897
|
+
const hello = { type: "hello", protocol: "whiteboard-v1", token, cursor };
|
|
1898
|
+
if (level === "legacy") return hello;
|
|
1899
|
+
if (level === "presentation") return { ...hello, extensions: [{ name: "presentation_v1" }] };
|
|
1900
|
+
return { ...hello, extensions: [{ name: "presentation_v1" }, level === "versioned-view" ? { name: "view_v1", origins: true } : { name: "view_v1" }] };
|
|
1901
|
+
}
|
|
1755
1902
|
function terminalCloseCode(closeCode) {
|
|
1756
1903
|
return closeCode === 1008 || closeCode === 1009;
|
|
1757
1904
|
}
|
|
@@ -1792,6 +1939,12 @@ function createChalkWhiteboardV1Client(options) {
|
|
|
1792
1939
|
});
|
|
1793
1940
|
}
|
|
1794
1941
|
|
|
1942
|
+
// src/whiteboard/index.ts
|
|
1943
|
+
var ChalkWhiteboardViewBounds = {
|
|
1944
|
+
maxCoordinate: WhiteboardV1ProtocolMetadata.frames.view.maxCoordinate,
|
|
1945
|
+
maxExtent: WhiteboardV1ProtocolMetadata.frames.view.maxExtent
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1795
1948
|
// src/connection/types.ts
|
|
1796
1949
|
var ConnectionError = class extends Error {
|
|
1797
1950
|
code;
|
|
@@ -2123,6 +2276,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2123
2276
|
syncNoticeUnresponsive: false,
|
|
2124
2277
|
mediaSnapshot: null,
|
|
2125
2278
|
failure: null,
|
|
2279
|
+
startInitialMedia: null,
|
|
2126
2280
|
initialMedia: (intent) => Effect2.tryPromise({
|
|
2127
2281
|
try: () => intent.microphone || intent.camera ? platform.mediaDevices.getUserMedia({ audio: intent.microphone, video: intent.camera }) : Promise.resolve(streamFromTracks([])),
|
|
2128
2282
|
catch: (cause) => cause
|
|
@@ -2267,8 +2421,14 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2267
2421
|
model.syncBindingCleanup?.();
|
|
2268
2422
|
model.syncNoticeUnresponsive = false;
|
|
2269
2423
|
let latestSnapshot = null;
|
|
2424
|
+
let observedDialTimeouts = 0;
|
|
2270
2425
|
let snapshotQueued = false;
|
|
2271
2426
|
const unsubscribe = sync.subscribe((snapshot) => {
|
|
2427
|
+
const count = snapshot.connection.dialTimeoutCount ?? 0;
|
|
2428
|
+
if (sync === model.sync && count > observedDialTimeouts) {
|
|
2429
|
+
observedDialTimeouts = count;
|
|
2430
|
+
diagnostics.record({ event: "sync_dial_timeout", state: model.state, epoch: model.epoch, attempt: count });
|
|
2431
|
+
}
|
|
2272
2432
|
if (sync === model.sync && snapshot.connection.phase === "live") {
|
|
2273
2433
|
const notice = snapshot.connection.noticeUnresponsive === true;
|
|
2274
2434
|
if (notice !== model.syncNoticeUnresponsive) {
|
|
@@ -2330,7 +2490,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2330
2490
|
const unsubscribe = sync.subscribe(observe);
|
|
2331
2491
|
observe(sync.getSnapshot());
|
|
2332
2492
|
return Effect2.sync(unsubscribe);
|
|
2333
|
-
}).pipe((effect) => withinForegroundBudget(effect, timeoutMs, lifecycleFailure("
|
|
2493
|
+
}).pipe((effect) => withinForegroundBudget(effect, timeoutMs, lifecycleFailure("sync_hello_timeout", true, "Sync did not become live before the startup deadline")));
|
|
2334
2494
|
const trace = (step, effect) => {
|
|
2335
2495
|
const span = diagnostics.startSpan({ step, state: model.state, epoch: model.epoch });
|
|
2336
2496
|
return effect.pipe(
|
|
@@ -2388,7 +2548,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2388
2548
|
[
|
|
2389
2549
|
trace(
|
|
2390
2550
|
"start_media",
|
|
2391
|
-
foreign7(() => media.start(stream), "media_start_failed", "The media layer could not start")
|
|
2551
|
+
model.startInitialMedia ? model.startInitialMedia(media, stream).pipe(Effect2.mapError((cause) => lifecycleFailure("media_start_failed", true, "The media layer could not start", cause))) : foreign7(() => media.start(stream), "media_start_failed", "The media layer could not start")
|
|
2392
2552
|
),
|
|
2393
2553
|
trace("start_sync", foreign7(() => sync.start(), "sync_start_failed", "The sync layer could not start").pipe(Effect2.andThen(trace("wait_for_sync_live", waitForSyncLive(sync, boundedInteger(options.syncStartupTimeoutMs, START_TIMEOUT_MS, 1, 6e4))))))
|
|
2394
2554
|
],
|
|
@@ -2845,8 +3005,9 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2845
3005
|
},
|
|
2846
3006
|
getDiagnostics: () => diagnostics.snapshot(),
|
|
2847
3007
|
getJoinTrace: () => diagnostics.joinTrace(),
|
|
2848
|
-
setInitialMedia: (provider, intent) => Effect2.sync(() => {
|
|
3008
|
+
setInitialMedia: (provider, intent, start) => Effect2.sync(() => {
|
|
2849
3009
|
model.initialMedia = provider;
|
|
3010
|
+
model.startInitialMedia = start ?? null;
|
|
2850
3011
|
model.initialMediaIntent = intent ?? null;
|
|
2851
3012
|
}),
|
|
2852
3013
|
configureJoin: (intent) => Effect2.sync(() => {
|
|
@@ -2907,7 +3068,7 @@ function snapshotFor(model, access) {
|
|
|
2907
3068
|
state: model.state,
|
|
2908
3069
|
subject: subjectFor(access),
|
|
2909
3070
|
episode: episodeFor(access, control),
|
|
2910
|
-
connection: Object.freeze({ sync: model.syncSnapshot?.connection.phase === "live" && model.syncNoticeUnresponsive ? "unresponsive" : syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
|
|
3071
|
+
connection: Object.freeze({ syncHealth: model.syncSnapshot?.connection.health, sync: model.syncSnapshot?.connection.phase === "live" && model.syncNoticeUnresponsive ? "unresponsive" : syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
|
|
2911
3072
|
failure: model.failure ? Object.freeze({ ...model.failure }) : null,
|
|
2912
3073
|
episodeEnded: model.episodeEndConfirmed,
|
|
2913
3074
|
removedByHost: model.removedByHost
|
|
@@ -3027,6 +3188,7 @@ function mediaPhase(phase) {
|
|
|
3027
3188
|
}
|
|
3028
3189
|
function syncTerminalFailure(snapshot) {
|
|
3029
3190
|
const reason = snapshot.connection.terminalReason;
|
|
3191
|
+
if (reason === "state_digest_recovery_failed") return lifecycleFailure("state_digest_recovery_failed", false, "The Episode state could not be recovered from an authoritative snapshot");
|
|
3030
3192
|
if (reason === "episode_ended") return lifecycleFailure("episode_ended", false, "The Episode has ended");
|
|
3031
3193
|
if (reason === "participant_inactive" || reason === "participant_removed" || reason === "stale_participant_generation") return lifecycleFailure("invalid_access", false, "Participant access is no longer valid");
|
|
3032
3194
|
return lifecycleFailure("sync_start_failed", true, "The Sync transport needs recovery");
|
|
@@ -3045,6 +3207,8 @@ var CONNECTION_ERROR_CODE_MAP = {
|
|
|
3045
3207
|
device_constraint_invalid: "media.device_constraint_invalid",
|
|
3046
3208
|
media_capture_failed: "media.capture_failed",
|
|
3047
3209
|
sync_start_failed: "connection.sync_start_failed",
|
|
3210
|
+
state_digest_recovery_failed: "connection.state_digest_recovery_failed",
|
|
3211
|
+
sync_hello_timeout: "connection.sync_hello_timeout",
|
|
3048
3212
|
media_start_failed: "connection.media_start_failed",
|
|
3049
3213
|
join_cleanup_unconfirmed: "connection.join_cleanup_unconfirmed",
|
|
3050
3214
|
sync_recovery_exhausted: "connection.sync_recovery_exhausted",
|
|
@@ -3067,17 +3231,32 @@ var INVALID_PAYLOAD_CODE_BY_ACTION = /* @__PURE__ */ new Map([
|
|
|
3067
3231
|
["acceptMediaRequest", "media.request_invalid"],
|
|
3068
3232
|
["declineMediaRequest", "media.request_invalid"]
|
|
3069
3233
|
]);
|
|
3070
|
-
|
|
3234
|
+
var MEDIA_TIMEOUT_FAILURE = { code: "media.timeout", recoverable: true, message: "Media did not respond in time. Try again." };
|
|
3235
|
+
var SYNC_FAILURES = /* @__PURE__ */ new Map([
|
|
3236
|
+
["not_live", { failure: { code: "connection.invalid_state", recoverable: true, message: "Wait for the call to connect, then try again." } }],
|
|
3237
|
+
["signaling_timeout", { failure: MEDIA_TIMEOUT_FAILURE }],
|
|
3238
|
+
["negotiation_timeout", { failure: MEDIA_TIMEOUT_FAILURE }],
|
|
3239
|
+
["media_timeout", { failure: MEDIA_TIMEOUT_FAILURE }],
|
|
3240
|
+
["retry_exhausted", { failure: MEDIA_TIMEOUT_FAILURE, scope: "media" }],
|
|
3241
|
+
["reaction_ack_timeout", { failure: { code: "collaboration.unavailable", recoverable: true, message: "Couldn\u2019t confirm your reaction. Try again." } }],
|
|
3242
|
+
["overloaded", { failure: { code: "collaboration.unavailable", recoverable: true, message: "The call is busy right now. Try again." } }],
|
|
3243
|
+
["rate_limited", { failure: { code: "command.rate_limited", recoverable: true, message: "You are sending messages too fast. Wait a moment, then try again." } }]
|
|
3244
|
+
]);
|
|
3245
|
+
function normalizeMediaError(cause) {
|
|
3246
|
+
return normalizeClientError(cause, "client.internal_error", "media");
|
|
3247
|
+
}
|
|
3248
|
+
function normalizeClientError(cause, fallback = "client.internal_error", scope) {
|
|
3071
3249
|
if (cause instanceof SpaceClientError) return cause;
|
|
3072
|
-
if (isConnectionError(cause)) {
|
|
3250
|
+
if (cause instanceof ConnectionLifecycleFailure || isConnectionError(cause)) {
|
|
3073
3251
|
return new SpaceClientError({
|
|
3074
|
-
code: connectionErrorCode(cause.code, cause.action),
|
|
3252
|
+
code: connectionErrorCode(cause.code, cause instanceof ConnectionLifecycleFailure ? null : cause.action),
|
|
3075
3253
|
recoverable: cause.recoverable,
|
|
3076
3254
|
message: cause.message
|
|
3077
3255
|
});
|
|
3078
3256
|
}
|
|
3079
|
-
if (cause instanceof V1SyncError
|
|
3080
|
-
|
|
3257
|
+
if (cause instanceof V1SyncError) {
|
|
3258
|
+
const policy = SYNC_FAILURES.get(cause.code);
|
|
3259
|
+
if (policy && (!policy.scope || policy.scope === scope)) return new SpaceClientError(policy.failure);
|
|
3081
3260
|
}
|
|
3082
3261
|
if (isAccessInvalid(cause)) {
|
|
3083
3262
|
return new SpaceClientError({ code: "access.invalid", recoverable: true, message: "The access grant was rejected" });
|
|
@@ -3146,6 +3325,7 @@ var SpaceStore = class _SpaceStore {
|
|
|
3146
3325
|
const error2 = connectionError(snapshot.failure);
|
|
3147
3326
|
const next = {
|
|
3148
3327
|
status: connectionStatus(snapshot),
|
|
3328
|
+
syncHealth: snapshot.connection.syncHealth,
|
|
3149
3329
|
episode: snapshot.episode,
|
|
3150
3330
|
lastError: sameError(current.lastError, error2) ? current.lastError : error2,
|
|
3151
3331
|
episodeEnded: snapshot.episodeEnded ?? false,
|
|
@@ -3199,27 +3379,15 @@ var SpaceStoreService = class extends Context4.Service()("@chalk/client/SpaceSto
|
|
|
3199
3379
|
};
|
|
3200
3380
|
var makeSpaceStoreLayer = Layer4.effect(SpaceStoreService, SpaceStore.make());
|
|
3201
3381
|
var makeFakeSpaceStoreLayer = (store = new SpaceStore()) => Layer4.succeed(SpaceStoreService, store);
|
|
3202
|
-
function mapCode(code) {
|
|
3203
|
-
return {
|
|
3204
|
-
invalid_access: "access.invalid",
|
|
3205
|
-
access_unavailable: "access.unavailable",
|
|
3206
|
-
episode_ended: "episode.ended",
|
|
3207
|
-
invalid_payload: "chat.payload_invalid",
|
|
3208
|
-
permission_denied: "media.permission_denied",
|
|
3209
|
-
unsupported_environment: "environment.unsupported",
|
|
3210
|
-
rate_limited: "command.rate_limited",
|
|
3211
|
-
command_rejected: "command.rejected"
|
|
3212
|
-
}[code] ?? "client.internal_error";
|
|
3213
|
-
}
|
|
3214
3382
|
function connectionError(failure2) {
|
|
3215
|
-
return failure2 ? empty({ code:
|
|
3383
|
+
return failure2 ? empty({ code: connectionErrorCode(failure2.code, failure2.action), recoverable: failure2.recoverable, message: failure2.message }) : null;
|
|
3216
3384
|
}
|
|
3217
3385
|
function connectionStatus(snapshot) {
|
|
3218
3386
|
const interrupted = INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.sync) || INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.media);
|
|
3219
3387
|
return snapshot.state === "live" && interrupted ? "reconnecting" : snapshot.state;
|
|
3220
3388
|
}
|
|
3221
3389
|
function sameConnection(left, right) {
|
|
3222
|
-
return left.status === right.status && sameEpisode(left.episode, right.episode) && left.lastError === right.lastError && left.episodeEnded === right.episodeEnded && left.removedByHost === right.removedByHost;
|
|
3390
|
+
return left.status === right.status && left.syncHealth === right.syncHealth && sameEpisode(left.episode, right.episode) && left.lastError === right.lastError && left.episodeEnded === right.episodeEnded && left.removedByHost === right.removedByHost;
|
|
3223
3391
|
}
|
|
3224
3392
|
function sameEpisode(left, right) {
|
|
3225
3393
|
if (left === right) return true;
|
|
@@ -3231,20 +3399,6 @@ function sameError(left, right) {
|
|
|
3231
3399
|
if (!left || !right) return false;
|
|
3232
3400
|
return left.code === right.code && left.recoverable === right.recoverable && left.message === right.message;
|
|
3233
3401
|
}
|
|
3234
|
-
function publicCode(code) {
|
|
3235
|
-
switch (code) {
|
|
3236
|
-
case "device_not_found":
|
|
3237
|
-
return "media.device_not_found";
|
|
3238
|
-
case "device_busy":
|
|
3239
|
-
return "media.device_busy";
|
|
3240
|
-
case "device_constraint_invalid":
|
|
3241
|
-
return "media.device_constraint_invalid";
|
|
3242
|
-
case "media_capture_failed":
|
|
3243
|
-
return "media.capture_failed";
|
|
3244
|
-
default:
|
|
3245
|
-
return mapCode(code);
|
|
3246
|
-
}
|
|
3247
|
-
}
|
|
3248
3402
|
|
|
3249
3403
|
// src/space-client/core.ts
|
|
3250
3404
|
import { Clock as Clock5, Context as Context13, Duration as Duration2, Effect as Effect12, Layer as Layer13 } from "effect";
|
|
@@ -3786,8 +3940,11 @@ function chatError(message) {
|
|
|
3786
3940
|
}
|
|
3787
3941
|
|
|
3788
3942
|
// src/space-client/media-controller.ts
|
|
3789
|
-
import { Clock as Clock3, Context as Context6, Effect as Effect5, Exit as Exit2, Layer as Layer6, Semaphore } from "effect";
|
|
3943
|
+
import { Clock as Clock3, Context as Context6, Deferred as Deferred2, Effect as Effect5, Exit as Exit2, Layer as Layer6, Semaphore } from "effect";
|
|
3790
3944
|
var MEDIA_SOURCES = ["microphone", "camera", "screen"];
|
|
3945
|
+
function sourceWorker(enabled) {
|
|
3946
|
+
return { desired: { enabled, revision: 0, previousEnabled: enabled }, result: { revision: 0, status: "confirmed" }, sentRevision: 0, initialRevision: 0, localEnabled: enabled, pause: null, serverSequence: -1, forcedRevision: null };
|
|
3947
|
+
}
|
|
3791
3948
|
var MediaControllerService = class extends Context6.Service()("@chalk/client/MediaController") {
|
|
3792
3949
|
};
|
|
3793
3950
|
var makeMediaController = (connection, store, selection) => Effect5.gen(function* () {
|
|
@@ -3812,7 +3969,8 @@ var makeMediaController = (connection, store, selection) => Effect5.gen(function
|
|
|
3812
3969
|
);
|
|
3813
3970
|
yield* connection.setInitialMedia(
|
|
3814
3971
|
(intent) => controller.captureInitial(intent),
|
|
3815
|
-
() => controller.initialIntent()
|
|
3972
|
+
() => controller.initialIntent(),
|
|
3973
|
+
(media, stream) => controller.startInitial(media, stream)
|
|
3816
3974
|
);
|
|
3817
3975
|
yield* Effect5.addFinalizer(() => Effect5.sync(() => controller.dispose()));
|
|
3818
3976
|
controller.refreshDevicesInBackground();
|
|
@@ -3826,17 +3984,17 @@ var MediaControllerRuntime = class {
|
|
|
3826
3984
|
#now;
|
|
3827
3985
|
#fork;
|
|
3828
3986
|
#tracks = /* @__PURE__ */ new Map();
|
|
3829
|
-
#
|
|
3830
|
-
#
|
|
3831
|
-
#pendingEnables = /* @__PURE__ */ new Map();
|
|
3987
|
+
#sources = { microphone: sourceWorker(true), camera: sourceWorker(true), screen: sourceWorker(false) };
|
|
3988
|
+
#syncLive = false;
|
|
3832
3989
|
#pendingSources = /* @__PURE__ */ new Set();
|
|
3990
|
+
#pendingCaptures = /* @__PURE__ */ new Set();
|
|
3833
3991
|
#requestGenerations = /* @__PURE__ */ new Map();
|
|
3834
|
-
#
|
|
3992
|
+
#camera = { status: "idle" };
|
|
3835
3993
|
#requests = Object.freeze([]);
|
|
3836
3994
|
#ports = null;
|
|
3837
|
-
#screenEndedPending = false;
|
|
3838
3995
|
#screenAudioTrack = null;
|
|
3839
3996
|
#unsubscribeMedia = null;
|
|
3997
|
+
#unsubscribePause = null;
|
|
3840
3998
|
#unsubscribeRequests = null;
|
|
3841
3999
|
#unsubscribeSync = null;
|
|
3842
4000
|
#unsubscribeConnection = null;
|
|
@@ -3854,328 +4012,396 @@ var MediaControllerRuntime = class {
|
|
|
3854
4012
|
this.#unsubscribePorts = connection.subscribePorts((ports) => this.#bind(ports));
|
|
3855
4013
|
}
|
|
3856
4014
|
configure(intent) {
|
|
3857
|
-
if (intent.microphone !== void 0) this.#
|
|
3858
|
-
if (intent.camera !== void 0) this.#
|
|
4015
|
+
if (intent.microphone !== void 0) this.#recordIntent("microphone", intent.microphone);
|
|
4016
|
+
if (intent.camera !== void 0) this.#recordIntent("camera", intent.camera);
|
|
3859
4017
|
}
|
|
3860
|
-
setMicrophoneEnabled = (enabled) => this.#set("microphone", enabled);
|
|
3861
|
-
setCameraEnabled = (enabled) => this.#set("camera", enabled);
|
|
3862
|
-
|
|
4018
|
+
setMicrophoneEnabled = (enabled) => this.#mediaAction(enabled, this.#set("microphone", enabled));
|
|
4019
|
+
setCameraEnabled = (enabled) => this.#mediaAction(enabled, this.#set("camera", enabled));
|
|
4020
|
+
captureCameraPreview = () => this.#cameraSource("preview").pipe(Effect5.mapError(normalizeMediaError));
|
|
4021
|
+
setScreenShareEnabled = (enabled, options) => enabled ? this.#mediaAction(true, this.#startScreen(options)) : this.#stopScreen();
|
|
3863
4022
|
selectMicrophone = (deviceId) => Effect5.sync(() => {
|
|
3864
4023
|
this.#selection.selectCapture("microphone", deviceId);
|
|
3865
4024
|
this.#store.select("microphone", deviceId);
|
|
3866
|
-
}).pipe(Effect5.mapError(
|
|
4025
|
+
}).pipe(Effect5.mapError(normalizeMediaError));
|
|
3867
4026
|
selectCamera = (deviceId) => Effect5.sync(() => {
|
|
4027
|
+
const changed = this.#store.getSnapshot().media.selection.camera !== deviceId;
|
|
3868
4028
|
this.#selection.selectCapture("camera", deviceId);
|
|
4029
|
+
if (changed && (!this.#sources.camera.localEnabled || !this.#hasTrack("camera"))) {
|
|
4030
|
+
this.#transitionCamera({ status: "ended" });
|
|
4031
|
+
}
|
|
3869
4032
|
this.#store.select("camera", deviceId);
|
|
3870
|
-
}).pipe(Effect5.mapError(
|
|
4033
|
+
}).pipe(Effect5.mapError(normalizeMediaError));
|
|
3871
4034
|
selectSpeaker = (deviceId) => foreign2(() => this.#selection.selectSpeaker(deviceId)).pipe(
|
|
3872
4035
|
Effect5.tap(() => Effect5.sync(() => this.#store.select("speaker", deviceId))),
|
|
3873
|
-
Effect5.mapError(
|
|
4036
|
+
Effect5.mapError(normalizeMediaError)
|
|
3874
4037
|
);
|
|
3875
4038
|
acceptRequest = (requestId) => {
|
|
3876
4039
|
return Effect5.suspend(() => {
|
|
3877
4040
|
const request = this.#requests.find((candidate) => candidate.requestId === requestId);
|
|
3878
4041
|
if (!request) return Effect5.fail(new SpaceClientError({ code: "media.request_invalid", recoverable: false, message: "The media request is no longer active" }));
|
|
3879
|
-
return (request.kind === "unmute" ? this
|
|
4042
|
+
return (request.kind === "unmute" ? this.setMicrophoneEnabled(true) : this.setCameraEnabled(true)).pipe(
|
|
3880
4043
|
Effect5.tap(
|
|
3881
4044
|
() => Effect5.sync(() => {
|
|
3882
4045
|
this.#removeRequest(requestId);
|
|
3883
4046
|
})
|
|
3884
4047
|
)
|
|
3885
4048
|
);
|
|
3886
|
-
}).pipe(Effect5.mapError(
|
|
4049
|
+
}).pipe(Effect5.mapError(normalizeMediaError));
|
|
3887
4050
|
};
|
|
3888
4051
|
declineRequest = (requestId) => {
|
|
3889
4052
|
return Effect5.sync(() => {
|
|
3890
4053
|
this.#removeRequest(requestId);
|
|
3891
|
-
}).pipe(Effect5.mapError(
|
|
4054
|
+
}).pipe(Effect5.mapError(normalizeMediaError));
|
|
3892
4055
|
};
|
|
3893
4056
|
initialIntent() {
|
|
3894
|
-
return {
|
|
4057
|
+
return { microphone: this.#sources.microphone.desired.enabled, camera: this.#sources.camera.desired.enabled };
|
|
3895
4058
|
}
|
|
3896
4059
|
captureInitial(intent) {
|
|
4060
|
+
for (const source of ["microphone", "camera"]) this.#sources[source].initialRevision = this.#sources[source].desired.revision;
|
|
4061
|
+
if (!intent.camera) return this.#captureInitial(intent);
|
|
4062
|
+
return this.#useCamera(
|
|
4063
|
+
(track) => Effect5.sync(() => this.#setTrack("camera", track)).pipe(
|
|
4064
|
+
Effect5.andThen(Effect5.suspend(() => intent.microphone && !this.#hasTrack("microphone") ? this.#captureSource("microphone").pipe(Effect5.tap((audio) => Effect5.sync(() => this.#setTrack("microphone", audio)))) : Effect5.void)),
|
|
4065
|
+
Effect5.map(() => {
|
|
4066
|
+
this.#pauseInitialTracks();
|
|
4067
|
+
this.#publish();
|
|
4068
|
+
return streamFromTracks([...this.#localTracks().values()]);
|
|
4069
|
+
})
|
|
4070
|
+
),
|
|
4071
|
+
(signal) => this.#captureInitial(intent, signal).pipe(
|
|
4072
|
+
Effect5.map((stream) => {
|
|
4073
|
+
const track = stream.getTracks().find((track2) => track2.kind === "video");
|
|
4074
|
+
if (!track) throw new TypeError("Initial capture did not return a camera track");
|
|
4075
|
+
return track;
|
|
4076
|
+
})
|
|
4077
|
+
)
|
|
4078
|
+
);
|
|
4079
|
+
}
|
|
4080
|
+
#captureInitial(intent, signal) {
|
|
3897
4081
|
if (!intent.microphone && !intent.camera) {
|
|
3898
|
-
this.#
|
|
4082
|
+
this.#pauseInitialTracks();
|
|
3899
4083
|
this.#publish();
|
|
3900
|
-
return Effect5.succeed(streamFromTracks([...this.#
|
|
4084
|
+
return Effect5.succeed(streamFromTracks([...this.#localTracks().values()]));
|
|
3901
4085
|
}
|
|
3902
|
-
return
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
)
|
|
4086
|
+
return this.#captureStream(
|
|
4087
|
+
() => this.#selection.getUserMedia({ audio: intent.microphone, video: intent.camera }),
|
|
4088
|
+
(stream) => Effect5.try({ try: () => selectInitialTracks(stream, intent), catch: (cause) => cause }).pipe(
|
|
4089
|
+
Effect5.tap(
|
|
4090
|
+
(tracks) => Effect5.sync(() => {
|
|
4091
|
+
for (const [source, track] of tracks) this.#setTrack(source, track);
|
|
4092
|
+
this.#pauseInitialTracks();
|
|
4093
|
+
this.#publish();
|
|
4094
|
+
})
|
|
4095
|
+
),
|
|
4096
|
+
Effect5.tap(() => this.#refreshDevices().pipe(Effect5.ignore)),
|
|
4097
|
+
Effect5.map(() => streamFromTracks([...this.#localTracks().values()])),
|
|
4098
|
+
Effect5.catch(
|
|
4099
|
+
(cause) => Effect5.sync(() => {
|
|
4100
|
+
stopStream(stream);
|
|
4101
|
+
this.#publish();
|
|
4102
|
+
}).pipe(Effect5.andThen(Effect5.fail(captureError(cause, "join", "Camera or microphone permission was denied"))))
|
|
3920
4103
|
)
|
|
3921
4104
|
),
|
|
3922
|
-
|
|
3923
|
-
|
|
4105
|
+
(cause) => cause,
|
|
4106
|
+
signal
|
|
4107
|
+
).pipe(Effect5.catch((cause) => Effect5.fail(cause instanceof ConnectionError ? cause : captureError(cause, "join", "Camera or microphone permission was denied"))));
|
|
3924
4108
|
}
|
|
3925
4109
|
refreshDevicesInBackground() {
|
|
3926
4110
|
this.#fork(this.#refreshDevices().pipe(Effect5.ignore));
|
|
3927
4111
|
}
|
|
3928
4112
|
dispose() {
|
|
3929
|
-
this.#
|
|
3930
|
-
this.#unsubscribeRequests?.();
|
|
3931
|
-
this.#unsubscribeSync?.();
|
|
4113
|
+
this.#cancelCaptures();
|
|
4114
|
+
for (const unsubscribe of [this.#unsubscribeMedia, this.#unsubscribeRequests, this.#unsubscribePause, this.#unsubscribeSync]) unsubscribe?.();
|
|
3932
4115
|
this.#unsubscribeConnection?.();
|
|
3933
4116
|
this.#unsubscribeScreenEnded?.();
|
|
3934
4117
|
this.#unsubscribePorts?.();
|
|
3935
4118
|
this.#unsubscribeMedia = null;
|
|
3936
4119
|
this.#unsubscribeRequests = null;
|
|
4120
|
+
this.#unsubscribePause = null;
|
|
3937
4121
|
this.#unsubscribeSync = null;
|
|
3938
4122
|
this.#unsubscribeConnection = null;
|
|
3939
4123
|
this.#unsubscribeScreenEnded = null;
|
|
3940
4124
|
this.#unsubscribePorts = null;
|
|
3941
4125
|
this.#ports = null;
|
|
4126
|
+
this.#resetObservations();
|
|
3942
4127
|
this.#screenAudioTrack?.stop();
|
|
3943
4128
|
this.#screenAudioTrack = null;
|
|
3944
4129
|
for (const track of this.#tracks.values()) track.stop();
|
|
3945
|
-
this.#
|
|
4130
|
+
this.#clearTracks();
|
|
3946
4131
|
this.#clearRequests();
|
|
3947
4132
|
this.#publish();
|
|
3948
4133
|
}
|
|
4134
|
+
#mediaAction(enabling, operation) {
|
|
4135
|
+
return Effect5.suspend(() => !enabling || this.#store.getSnapshot().connection.status === "live" ? operation : Effect5.fail(normalizeMediaError(new V1SyncError("Media enable requires a live call", "not_live"))));
|
|
4136
|
+
}
|
|
3949
4137
|
#set(source, enabled) {
|
|
3950
|
-
|
|
3951
|
-
let intentRevision = 0;
|
|
3952
|
-
let earlyPause = null;
|
|
3953
|
-
const localPause = Effect5.suspend(() => {
|
|
3954
|
-
previousIntent = this.#intent[source];
|
|
3955
|
-
intentRevision = (this.#intentRevisions.get(source) ?? 0) + 1;
|
|
3956
|
-
this.#intentRevisions.set(source, intentRevision);
|
|
3957
|
-
this.#intent[source] = enabled;
|
|
3958
|
-
if (enabled) this.#pendingEnables.set(source, intentRevision);
|
|
3959
|
-
this.#ports?.media.setLocalSourceIntent?.(source, enabled);
|
|
3960
|
-
if (!enabled && this.#ports) earlyPause = this.#beginLocalPause(this.#ports, source);
|
|
3961
|
-
return Effect5.void;
|
|
3962
|
-
});
|
|
3963
|
-
return localPause.pipe(
|
|
3964
|
-
Effect5.andThen(
|
|
3965
|
-
this.#serialize(
|
|
3966
|
-
source,
|
|
3967
|
-
this.#connection.runCommand((ports) => {
|
|
3968
|
-
let captured = null;
|
|
3969
|
-
let prepared = false;
|
|
3970
|
-
const action = source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled";
|
|
3971
|
-
return Effect5.suspend(() => {
|
|
3972
|
-
if (!enabled) {
|
|
3973
|
-
let paused = false;
|
|
3974
|
-
return (earlyPause ? foreign2(() => earlyPause) : this.#localTarget(ports, source, false)).pipe(
|
|
3975
|
-
Effect5.tap(
|
|
3976
|
-
() => Effect5.sync(() => {
|
|
3977
|
-
paused = true;
|
|
3978
|
-
})
|
|
3979
|
-
),
|
|
3980
|
-
Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(false)) : foreign2(() => ports.sync.setCameraEnabled(false))),
|
|
3981
|
-
Effect5.asVoid,
|
|
3982
|
-
Effect5.catch((cause) => {
|
|
3983
|
-
if (paused && !isSyncRejection(cause) || this.#intentRevisions.get(source) !== intentRevision) return Effect5.fail(cause);
|
|
3984
|
-
this.#intent[source] = previousIntent;
|
|
3985
|
-
ports.media.setLocalSourceIntent?.(source, previousIntent);
|
|
3986
|
-
return this.#localTarget(ports, source, previousIntent).pipe(
|
|
3987
|
-
Effect5.catch(() => Effect5.void),
|
|
3988
|
-
Effect5.andThen(Effect5.fail(cause))
|
|
3989
|
-
);
|
|
3990
|
-
})
|
|
3991
|
-
);
|
|
3992
|
-
}
|
|
3993
|
-
const acquire = enabled && !this.#tracks.has(source) ? this.#captureSource(source).pipe(
|
|
3994
|
-
Effect5.tap(
|
|
3995
|
-
(track) => Effect5.sync(() => {
|
|
3996
|
-
captured = track;
|
|
3997
|
-
try {
|
|
3998
|
-
this.#assertActivePorts(ports, action);
|
|
3999
|
-
} catch (cause) {
|
|
4000
|
-
track.stop();
|
|
4001
|
-
throw cause;
|
|
4002
|
-
}
|
|
4003
|
-
this.#tracks.set(source, track);
|
|
4004
|
-
ports.media.prepareLocalTrack(source, track);
|
|
4005
|
-
prepared = true;
|
|
4006
|
-
this.#publish();
|
|
4007
|
-
})
|
|
4008
|
-
)
|
|
4009
|
-
) : Effect5.void;
|
|
4010
|
-
return acquire.pipe(
|
|
4011
|
-
Effect5.asVoid,
|
|
4012
|
-
Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(enabled)) : foreign2(() => ports.sync.setCameraEnabled(enabled))),
|
|
4013
|
-
Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, action))),
|
|
4014
|
-
Effect5.asVoid,
|
|
4015
|
-
Effect5.catch(
|
|
4016
|
-
(cause) => this.#rollbackCapture({
|
|
4017
|
-
action,
|
|
4018
|
-
cause,
|
|
4019
|
-
fallback: Effect5.void,
|
|
4020
|
-
media: ports.media,
|
|
4021
|
-
permissionMessage: `${source} permission was denied`,
|
|
4022
|
-
prepared,
|
|
4023
|
-
restoreIntent: () => {
|
|
4024
|
-
if (this.#intentRevisions.get(source) === intentRevision) this.#intent[source] = previousIntent;
|
|
4025
|
-
},
|
|
4026
|
-
source,
|
|
4027
|
-
track: captured
|
|
4028
|
-
})
|
|
4029
|
-
)
|
|
4030
|
-
);
|
|
4031
|
-
});
|
|
4032
|
-
})
|
|
4033
|
-
)
|
|
4034
|
-
)
|
|
4035
|
-
).pipe(
|
|
4036
|
-
Effect5.mapError(normalizeClientError),
|
|
4037
|
-
Effect5.ensuring(
|
|
4038
|
-
Effect5.sync(() => {
|
|
4039
|
-
if (this.#pendingEnables.get(source) === intentRevision) this.#pendingEnables.delete(source);
|
|
4040
|
-
})
|
|
4041
|
-
)
|
|
4042
|
-
);
|
|
4138
|
+
return this.#act(source, enabled);
|
|
4043
4139
|
}
|
|
4044
4140
|
#startScreen(options) {
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
ports.media.prepareLocalTrack("screen", screenTrack);
|
|
4089
|
-
prepared = true;
|
|
4090
|
-
this.#tracks.set("screen", screenTrack);
|
|
4091
|
-
this.#screenAudioTrack = audio ?? null;
|
|
4092
|
-
if (audio) {
|
|
4093
|
-
ports.media.prepareLocalTrack("screen_audio", audio);
|
|
4094
|
-
audio.addEventListener("ended", () => this.#handleScreenAudioEnded(audio, ports));
|
|
4095
|
-
}
|
|
4096
|
-
screenTrack.addEventListener("ended", () => this.#handleScreenEnded(screenTrack));
|
|
4097
|
-
this.#screenEndedPending = false;
|
|
4098
|
-
this.#publish();
|
|
4099
|
-
})
|
|
4100
|
-
),
|
|
4101
|
-
Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
|
|
4102
|
-
Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
|
|
4103
|
-
Effect5.asVoid,
|
|
4104
|
-
Effect5.catch(
|
|
4105
|
-
(cause) => (prepared ? this.#clearScreenAudio(ports.media) : Effect5.void).pipe(
|
|
4106
|
-
Effect5.ignore,
|
|
4107
|
-
Effect5.andThen(this.#rollbackCapture({ action: "startScreenShare", cause, fallback: Effect5.sync(() => stopStream(stream)), media: ports.media, permissionMessage: "Screen sharing permission was denied", prepared, source: "screen", track }))
|
|
4108
|
-
)
|
|
4109
|
-
)
|
|
4110
|
-
),
|
|
4111
|
-
(stream, exit) => Effect5.sync(() => {
|
|
4112
|
-
if (Exit2.isFailure(exit)) stopStream(stream);
|
|
4113
|
-
})
|
|
4114
|
-
);
|
|
4115
|
-
});
|
|
4141
|
+
return this.#act("screen", true, options);
|
|
4142
|
+
}
|
|
4143
|
+
#stopScreen() {
|
|
4144
|
+
return this.#act("screen", false);
|
|
4145
|
+
}
|
|
4146
|
+
#pauseTracks(source) {
|
|
4147
|
+
const track = this.#track(source);
|
|
4148
|
+
if (track) track.enabled = false;
|
|
4149
|
+
if (source === "screen" && this.#screenAudioTrack) this.#screenAudioTrack.enabled = false;
|
|
4150
|
+
}
|
|
4151
|
+
#recordIntent(source, enabled, options) {
|
|
4152
|
+
const state = this.#sources[source];
|
|
4153
|
+
const desired = { enabled, revision: state.desired.revision + 1, options, previousEnabled: state.localEnabled };
|
|
4154
|
+
state.desired = desired;
|
|
4155
|
+
if (!enabled) {
|
|
4156
|
+
state.localEnabled = false;
|
|
4157
|
+
this.#pauseTracks(source);
|
|
4158
|
+
if (source === "screen") {
|
|
4159
|
+
this.#track("screen")?.stop();
|
|
4160
|
+
this.#screenAudioTrack?.stop();
|
|
4161
|
+
}
|
|
4162
|
+
const ports = this.#ports;
|
|
4163
|
+
if (ports) {
|
|
4164
|
+
ports.media.setLocalSourceIntent?.(source, false);
|
|
4165
|
+
const previous = state.pause;
|
|
4166
|
+
const next = this.#beginLocalPause(ports, source);
|
|
4167
|
+
state.pause = previous ? Promise.allSettled([previous, next]).then((results) => {
|
|
4168
|
+
for (const result of results) if (result.status === "rejected") throw result.reason;
|
|
4169
|
+
}) : next;
|
|
4170
|
+
void state.pause.catch(() => void 0);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
return desired;
|
|
4174
|
+
}
|
|
4175
|
+
#act(source, enabled, options) {
|
|
4176
|
+
return Effect5.suspend(() => {
|
|
4177
|
+
const desired = this.#recordIntent(source, enabled, options);
|
|
4178
|
+
return this.#serialize(source, this.#work(source)).pipe(
|
|
4179
|
+
Effect5.andThen(
|
|
4180
|
+
Effect5.suspend(() => {
|
|
4181
|
+
if (enabled && this.#store.getSnapshot().connection.status !== "live") return this.#mediaAction(true, Effect5.void);
|
|
4182
|
+
const result = this.#sources[source].result;
|
|
4183
|
+
return result.revision === desired.revision && result.status === "failed" ? Effect5.fail(result.cause) : Effect5.void;
|
|
4116
4184
|
})
|
|
4117
4185
|
)
|
|
4118
|
-
)
|
|
4119
|
-
).pipe(
|
|
4120
|
-
|
|
4186
|
+
);
|
|
4187
|
+
}).pipe(Effect5.mapError(normalizeMediaError));
|
|
4188
|
+
}
|
|
4189
|
+
#work(source) {
|
|
4190
|
+
return Effect5.gen({ self: this }, function* () {
|
|
4191
|
+
const state = this.#sources[source];
|
|
4192
|
+
while (state.result.revision !== state.desired.revision || state.result.status === "failed" && state.result.cleanup) {
|
|
4193
|
+
const intent = state.desired;
|
|
4194
|
+
yield* this.#runRevision(source, intent);
|
|
4195
|
+
if (state.desired.revision === intent.revision) return;
|
|
4196
|
+
}
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
#runRevision(source, intent) {
|
|
4200
|
+
return Effect5.gen({ self: this }, function* () {
|
|
4201
|
+
const state = this.#sources[source];
|
|
4202
|
+
const pause = state.pause;
|
|
4203
|
+
state.pause = null;
|
|
4204
|
+
const paused = yield* Effect5.exit(pause ? foreign2(() => pause) : Effect5.void);
|
|
4205
|
+
if (state.desired.revision !== intent.revision) return;
|
|
4206
|
+
const ports = this.#ports;
|
|
4207
|
+
const cleanup = source === "screen" && !intent.enabled ? yield* Effect5.exit(this.#stopScreenLocally(ports)) : Exit2.succeed(void 0);
|
|
4208
|
+
if (state.desired.revision !== intent.revision) return;
|
|
4209
|
+
if (ports && this.#syncLive) yield* this.#executeRevision(ports, source, intent);
|
|
4210
|
+
else if (intent.enabled) state.result = { revision: intent.revision, status: "failed", cause: new V1SyncError("Media enable requires a live call", "not_live"), cleanup: !intent.previousEnabled };
|
|
4211
|
+
yield* paused;
|
|
4212
|
+
yield* cleanup;
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
4215
|
+
#executeRevision(ports, source, intent) {
|
|
4216
|
+
return Effect5.gen({ self: this }, function* () {
|
|
4217
|
+
const state = this.#sources[source];
|
|
4218
|
+
if (state.result.revision === intent.revision && state.result.status === "failed") {
|
|
4219
|
+
yield* this.#connection.runCommand((p) => this.#send(p, source, intent, false));
|
|
4220
|
+
if (state.result.revision === intent.revision && state.result.status === "failed") state.result.cleanup = false;
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
const operation = source === "screen" ? this.#prepareScreen(ports, intent) : this.#prepareDevice(ports, source, intent);
|
|
4224
|
+
const result = yield* Effect5.exit(this.#connection.runCommand(() => operation));
|
|
4225
|
+
if (state.desired.revision !== intent.revision) return;
|
|
4226
|
+
if (Exit2.isSuccess(result)) {
|
|
4227
|
+
state.result = { revision: intent.revision, status: "confirmed" };
|
|
4228
|
+
state.localEnabled = state.forcedRevision === intent.revision ? false : intent.enabled;
|
|
4229
|
+
} else {
|
|
4230
|
+
const cause = yield* Effect5.flip(result);
|
|
4231
|
+
yield* this.#failRevision(ports, source, intent, cause);
|
|
4232
|
+
}
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
#failRevision(ports, source, intent, cause) {
|
|
4236
|
+
return Effect5.sync(() => {
|
|
4237
|
+
const state = this.#sources[source];
|
|
4238
|
+
if (!intent.enabled && ports.sync.getSnapshot().connection.phase !== "live") return;
|
|
4239
|
+
const previous = intent.previousEnabled;
|
|
4240
|
+
if (state.desired.revision === intent.revision) state.result = { revision: intent.revision, status: "failed", cause, cleanup: !intent.enabled || !previous };
|
|
4241
|
+
});
|
|
4242
|
+
}
|
|
4243
|
+
#prepareDevice(ports, source, intent) {
|
|
4244
|
+
if (!intent.enabled) return this.#send(ports, source, intent, false);
|
|
4245
|
+
let captured = null;
|
|
4246
|
+
let prepared = false;
|
|
4247
|
+
const action = source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled";
|
|
4248
|
+
return this.#mediaAction(
|
|
4249
|
+
true,
|
|
4250
|
+
Effect5.suspend(() => {
|
|
4251
|
+
this.#assertCurrentIntent(source, intent.revision);
|
|
4252
|
+
ports.media.setLocalSourceIntent?.(source, true);
|
|
4253
|
+
const retire = source === "microphone" ? this.#retireEndedMicrophone(ports.media) : Effect5.void;
|
|
4254
|
+
const acquire = Effect5.suspend(() => {
|
|
4255
|
+
this.#assertActivePorts(ports, action);
|
|
4256
|
+
this.#assertCurrentIntent(source, intent.revision);
|
|
4257
|
+
return source === "camera" && !intent.previousEnabled || !this.#hasTrack(source) ? this.#captureForPublish(
|
|
4258
|
+
source,
|
|
4259
|
+
ports.media,
|
|
4260
|
+
(track) => Effect5.try({
|
|
4261
|
+
try: () => {
|
|
4262
|
+
captured = track;
|
|
4263
|
+
this.#assertActivePorts(ports, action);
|
|
4264
|
+
this.#assertCurrentIntent(source, intent.revision);
|
|
4265
|
+
this.#setTrack(source, track);
|
|
4266
|
+
ports.media.prepareLocalTrack(source, track);
|
|
4267
|
+
prepared = true;
|
|
4268
|
+
this.#publish();
|
|
4269
|
+
},
|
|
4270
|
+
catch: (cause) => cause
|
|
4271
|
+
})
|
|
4272
|
+
) : Effect5.void;
|
|
4273
|
+
});
|
|
4274
|
+
return retire.pipe(Effect5.andThen(acquire), Effect5.andThen(this.#send(ports, source, intent, true)));
|
|
4275
|
+
})
|
|
4276
|
+
).pipe(Effect5.catch((cause) => this.#rollbackCapture({ action, cause, fallback: Effect5.void, media: ports.media, permissionMessage: `${source} permission was denied`, prepared, source, track: captured })));
|
|
4277
|
+
}
|
|
4278
|
+
#stopScreenLocally(ports) {
|
|
4279
|
+
const track = this.#track("screen");
|
|
4280
|
+
const clearVideo = ports ? foreign2(() => ports.media.clearPreparedLocalTrack("screen")) : Effect5.void;
|
|
4281
|
+
const clear = ports ? this.#clearScreenAudio(ports.media).pipe(Effect5.matchEffect({ onFailure: (cause) => clearVideo.pipe(Effect5.andThen(Effect5.fail(cause))), onSuccess: () => clearVideo })) : Effect5.void;
|
|
4282
|
+
return clear.pipe(
|
|
4121
4283
|
Effect5.ensuring(
|
|
4122
4284
|
Effect5.sync(() => {
|
|
4123
|
-
if (this.#
|
|
4285
|
+
if (this.#track("screen") === track) this.#deleteTrack("screen");
|
|
4286
|
+
track?.stop();
|
|
4287
|
+
this.#publish();
|
|
4124
4288
|
})
|
|
4125
4289
|
)
|
|
4126
4290
|
);
|
|
4127
4291
|
}
|
|
4128
|
-
#
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4292
|
+
#prepareScreen(ports, intent) {
|
|
4293
|
+
if (!intent.enabled) return this.#send(ports, "screen", intent, false);
|
|
4294
|
+
return this.#mediaAction(
|
|
4295
|
+
true,
|
|
4296
|
+
Effect5.suspend(() => {
|
|
4297
|
+
this.#assertCurrentIntent("screen", intent.revision);
|
|
4298
|
+
ports.media.setLocalSourceIntent?.("screen", true);
|
|
4299
|
+
if (this.#hasTrack("screen") && !intent.options) return this.#send(ports, "screen", intent, true);
|
|
4300
|
+
let track = null;
|
|
4301
|
+
let prepared = false;
|
|
4302
|
+
return this.#captureStream(
|
|
4303
|
+
() => this.#selection.getDisplayMedia(displayMediaConstraints(intent.options)),
|
|
4304
|
+
(stream) => Effect5.gen({ self: this }, function* () {
|
|
4305
|
+
this.#assertActivePorts(ports, "startScreenShare");
|
|
4306
|
+
this.#assertCurrentIntent("screen", intent.revision);
|
|
4307
|
+
track = requireDisplayVideoTrack(stream);
|
|
4308
|
+
const previous = this.#track("screen");
|
|
4309
|
+
if (previous) {
|
|
4310
|
+
yield* this.#send(ports, "screen", intent, false);
|
|
4311
|
+
yield* this.#clearScreenAudio(ports.media);
|
|
4312
|
+
yield* foreign2(() => ports.media.clearPreparedLocalTrack("screen"));
|
|
4313
|
+
this.#deleteTrack("screen");
|
|
4314
|
+
previous.stop();
|
|
4315
|
+
}
|
|
4316
|
+
this.#assertActivePorts(ports, "startScreenShare");
|
|
4317
|
+
this.#assertCurrentIntent("screen", intent.revision);
|
|
4318
|
+
const audio = stream.getAudioTracks()[0];
|
|
4319
|
+
for (const extra of stream.getAudioTracks().slice(1)) extra.stop();
|
|
4320
|
+
ports.media.prepareLocalTrack("screen", track);
|
|
4321
|
+
prepared = true;
|
|
4322
|
+
this.#setTrack("screen", track);
|
|
4323
|
+
this.#screenAudioTrack = audio ?? null;
|
|
4324
|
+
if (audio) {
|
|
4325
|
+
ports.media.prepareLocalTrack("screen_audio", audio);
|
|
4326
|
+
audio.addEventListener("ended", () => this.#handleScreenAudioEnded(audio, ports));
|
|
4327
|
+
}
|
|
4328
|
+
track.addEventListener("ended", () => this.#handleScreenEnded(track ?? void 0));
|
|
4329
|
+
this.#publish();
|
|
4330
|
+
yield* this.#send(ports, "screen", intent, true);
|
|
4331
|
+
}).pipe(
|
|
4332
|
+
Effect5.catch(
|
|
4333
|
+
(cause) => (prepared ? this.#clearScreenAudio(ports.media) : Effect5.void).pipe(
|
|
4334
|
+
Effect5.ignore,
|
|
4335
|
+
Effect5.andThen(this.#rollbackCapture({ action: "startScreenShare", cause, fallback: Effect5.sync(() => stopStream(stream)), media: ports.media, permissionMessage: "Screen sharing permission was denied", prepared, source: "screen", track }))
|
|
4336
|
+
)
|
|
4337
|
+
)
|
|
4338
|
+
),
|
|
4339
|
+
(cause) => captureError(cause, "startScreenShare", "Screen sharing permission was denied")
|
|
4340
|
+
);
|
|
4341
|
+
})
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4344
|
+
#assertCurrentIntent(source, revision) {
|
|
4345
|
+
if (this.#sources[source].desired.revision !== revision) throw cancelledCaptureError();
|
|
4346
|
+
}
|
|
4347
|
+
#resetObservations() {
|
|
4348
|
+
for (const source of MEDIA_SOURCES) {
|
|
4349
|
+
const state = this.#sources[source];
|
|
4350
|
+
state.result = { revision: state.desired.revision, status: "confirmed" };
|
|
4351
|
+
state.pause = null;
|
|
4352
|
+
state.serverSequence = -1;
|
|
4353
|
+
state.forcedRevision = null;
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
#pauseInitialTracks() {
|
|
4357
|
+
for (const source of ["microphone", "camera"]) {
|
|
4358
|
+
const enabled = this.#sources[source].desired.enabled;
|
|
4359
|
+
const track = this.#track(source);
|
|
4360
|
+
if (track && !enabled) track.enabled = false;
|
|
4361
|
+
this.#sources[source].localEnabled = !!track && enabled;
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
startInitial(media, stream) {
|
|
4365
|
+
const publish = foreign2(async () => {
|
|
4366
|
+
const revisions = { microphone: this.#sources.microphone.initialRevision, camera: this.#sources.camera.initialRevision };
|
|
4367
|
+
const tracks = stream.getTracks().filter((track) => {
|
|
4368
|
+
const source = track.kind === "audio" ? "microphone" : "camera";
|
|
4369
|
+
const state = this.#sources[source];
|
|
4370
|
+
track.enabled = state.desired.enabled;
|
|
4371
|
+
if (!state.desired.enabled) {
|
|
4372
|
+
this.#deleteTrack(source);
|
|
4373
|
+
if (source === "microphone") track.stop();
|
|
4374
|
+
}
|
|
4375
|
+
return state.desired.enabled;
|
|
4376
|
+
});
|
|
4377
|
+
for (const source of ["microphone", "camera"]) this.#sources[source].sentRevision = revisions[source];
|
|
4378
|
+
await media.start(streamFromTracks(tracks));
|
|
4379
|
+
for (const source of ["microphone", "camera"]) {
|
|
4380
|
+
const state = this.#sources[source];
|
|
4381
|
+
if (state.desired.revision === revisions[source]) state.result = { revision: revisions[source], status: "confirmed" };
|
|
4382
|
+
}
|
|
4137
4383
|
});
|
|
4138
|
-
return
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
if (this.#tracks.get("screen") === track) this.#tracks.delete("screen");
|
|
4160
|
-
track?.stop();
|
|
4161
|
-
this.#screenEndedPending = false;
|
|
4162
|
-
this.#publish();
|
|
4163
|
-
})
|
|
4164
|
-
),
|
|
4165
|
-
Effect5.asVoid,
|
|
4166
|
-
Effect5.catch(
|
|
4167
|
-
(cause) => paused && !isSyncRejection(cause) || this.#intentRevisions.get("screen") !== intentRevision ? Effect5.fail(cause) : Effect5.sync(() => ports.media.setLocalSourceIntent?.("screen", true)).pipe(
|
|
4168
|
-
Effect5.andThen(this.#localTarget(ports, "screen", true)),
|
|
4169
|
-
Effect5.catch(() => Effect5.void),
|
|
4170
|
-
Effect5.andThen(Effect5.fail(cause))
|
|
4171
|
-
)
|
|
4172
|
-
)
|
|
4173
|
-
);
|
|
4174
|
-
})
|
|
4175
|
-
)
|
|
4176
|
-
)
|
|
4177
|
-
)
|
|
4178
|
-
).pipe(Effect5.mapError(normalizeClientError));
|
|
4384
|
+
return this.#serialize("microphone", this.#serialize("camera", publish));
|
|
4385
|
+
}
|
|
4386
|
+
#send(ports, source, intent, enabled) {
|
|
4387
|
+
return this.#mediaAction(
|
|
4388
|
+
enabled,
|
|
4389
|
+
foreign2(async () => {
|
|
4390
|
+
this.#assertCurrentIntent(source, intent.revision);
|
|
4391
|
+
this.#assertActivePorts(ports, source === "screen" ? "startScreenShare" : source === "camera" ? "setCameraEnabled" : "setMicrophoneEnabled");
|
|
4392
|
+
const state = this.#sources[source];
|
|
4393
|
+
state.sentRevision = intent.revision;
|
|
4394
|
+
await (source === "microphone" ? ports.sync.setMicrophoneEnabled(enabled) : source === "camera" ? ports.sync.setCameraEnabled(enabled) : ports.sync.setScreenShareEnabled(enabled));
|
|
4395
|
+
this.#assertActivePorts(ports, source === "screen" ? "startScreenShare" : source === "camera" ? "setCameraEnabled" : "setMicrophoneEnabled");
|
|
4396
|
+
})
|
|
4397
|
+
);
|
|
4398
|
+
}
|
|
4399
|
+
#reconcileIntents() {
|
|
4400
|
+
const live = this.#connection.getSnapshot().state === "live" && this.#ports?.sync.getSnapshot().connection.phase === "live";
|
|
4401
|
+
const resumed = live && !this.#syncLive;
|
|
4402
|
+
this.#syncLive = live;
|
|
4403
|
+
if (!resumed) return;
|
|
4404
|
+
for (const source of MEDIA_SOURCES) this.#fork(this.#serialize(source, this.#work(source)).pipe(Effect5.ignore));
|
|
4179
4405
|
}
|
|
4180
4406
|
#clearScreenAudio(media) {
|
|
4181
4407
|
return foreign2(() => media.clearPreparedLocalTrack("screen_audio")).pipe(
|
|
@@ -4215,26 +4441,241 @@ var MediaControllerRuntime = class {
|
|
|
4215
4441
|
)
|
|
4216
4442
|
);
|
|
4217
4443
|
}
|
|
4218
|
-
#
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4444
|
+
#transitionCamera(next) {
|
|
4445
|
+
const previous = this.#camera;
|
|
4446
|
+
const sameOwner = "owner" in previous && "owner" in next && previous.owner === next.owner;
|
|
4447
|
+
if (previous.status === "live") previous.track.removeEventListener("ended", previous.onEnded);
|
|
4448
|
+
this.#camera = next;
|
|
4449
|
+
if (!sameOwner && "owner" in previous) {
|
|
4450
|
+
previous.owner.cancel();
|
|
4451
|
+
if (previous.status === "live") previous.track.stop();
|
|
4452
|
+
}
|
|
4453
|
+
if (next.status === "live") next.track.addEventListener("ended", next.onEnded, { once: true });
|
|
4454
|
+
this.#publish();
|
|
4455
|
+
this.#retireCameraPublication(previous, next);
|
|
4456
|
+
}
|
|
4457
|
+
#retireCameraPublication(previous, next) {
|
|
4458
|
+
if (previous.status !== "live" || previous.publication === null || previous.publication === "initial") return;
|
|
4459
|
+
if (next.status === "live" && next.track === previous.track && next.publication !== null) return;
|
|
4460
|
+
const media = previous.publication;
|
|
4461
|
+
void Promise.resolve().then(() => media.clearPreparedLocalTrack("camera", { stopTrack: false })).then(() => this.#enablePrivateCamera(previous.owner)).catch((cause) => Effect5.runSync(Effect5.logWarning("Camera publication cleanup failed", cause)));
|
|
4462
|
+
}
|
|
4463
|
+
#enablePrivateCamera(owner) {
|
|
4464
|
+
const state = this.#camera;
|
|
4465
|
+
if (state.status === "live" && state.owner === owner && state.publication === null) state.track.enabled = true;
|
|
4466
|
+
}
|
|
4467
|
+
#track(source) {
|
|
4468
|
+
if (source !== "camera") return this.#tracks.get(source);
|
|
4469
|
+
return this.#camera.status === "live" && this.#camera.publication !== null && this.#camera.track.readyState === "live" ? this.#camera.track : void 0;
|
|
4470
|
+
}
|
|
4471
|
+
#hasTrack(source) {
|
|
4472
|
+
return this.#track(source) !== void 0;
|
|
4473
|
+
}
|
|
4474
|
+
#localTracks() {
|
|
4475
|
+
const tracks = new Map(this.#tracks);
|
|
4476
|
+
const camera = this.#track("camera");
|
|
4477
|
+
if (camera) tracks.set("camera", camera);
|
|
4478
|
+
return tracks;
|
|
4479
|
+
}
|
|
4480
|
+
#setTrack(source, track) {
|
|
4481
|
+
if (source !== "camera") {
|
|
4482
|
+
this.#tracks.set(source, track);
|
|
4483
|
+
return;
|
|
4484
|
+
}
|
|
4485
|
+
const state = this.#camera;
|
|
4486
|
+
if (!("owner" in state)) throw cancelledCaptureError();
|
|
4487
|
+
if (state.status === "live" && state.track !== track) throw cancelledCaptureError();
|
|
4488
|
+
const onEnded = () => {
|
|
4489
|
+
if (this.#camera.status === "live" && this.#camera.track === track) this.#transitionCamera({ status: "ended" });
|
|
4490
|
+
};
|
|
4491
|
+
this.#transitionCamera({ status: "live", owner: state.owner, track, publication: this.#ports?.media ?? "initial", onEnded });
|
|
4492
|
+
}
|
|
4493
|
+
#deleteTrack(source) {
|
|
4494
|
+
if (source !== "camera") {
|
|
4495
|
+
this.#tracks.delete(source);
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
const state = this.#camera;
|
|
4499
|
+
if (state.status !== "live") return;
|
|
4500
|
+
if (state.owner.references > 0 && state.track.readyState === "live") {
|
|
4501
|
+
this.#transitionCamera({ ...state, publication: null });
|
|
4502
|
+
} else this.#transitionCamera({ status: "ended" });
|
|
4503
|
+
}
|
|
4504
|
+
#clearTracks() {
|
|
4505
|
+
this.#tracks.clear();
|
|
4506
|
+
this.#transitionCamera({ status: "idle" });
|
|
4507
|
+
}
|
|
4508
|
+
#cameraSource(purpose, acquire = (signal) => this.#captureSource("camera", signal)) {
|
|
4509
|
+
return Effect5.suspend(() => {
|
|
4510
|
+
this.#discardEndedCamera();
|
|
4511
|
+
const current = this.#camera;
|
|
4512
|
+
if (current.status === "live") return this.#liveCameraSource(current, purpose);
|
|
4513
|
+
if (current.status === "capturing") return this.#borrowCamera(current.owner);
|
|
4514
|
+
return this.#borrowCamera(this.#beginCameraCapture(acquire));
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
#discardEndedCamera() {
|
|
4518
|
+
if (this.#camera.status === "live" && this.#camera.track.readyState !== "live") this.#transitionCamera({ status: "ended" });
|
|
4519
|
+
}
|
|
4520
|
+
#retireEndedMicrophone(media) {
|
|
4521
|
+
const track = this.#tracks.get("microphone");
|
|
4522
|
+
if (track?.readyState !== "ended") return Effect5.void;
|
|
4523
|
+
return foreign2(() => media.clearPreparedLocalTrack("microphone", { stopTrack: false })).pipe(
|
|
4524
|
+
Effect5.andThen(
|
|
4525
|
+
Effect5.sync(() => {
|
|
4526
|
+
if (this.#tracks.get("microphone") === track) this.#tracks.delete("microphone");
|
|
4527
|
+
})
|
|
4528
|
+
)
|
|
4529
|
+
);
|
|
4530
|
+
}
|
|
4531
|
+
#liveCameraSource(current, purpose) {
|
|
4532
|
+
if (current.publication !== null && !current.track.enabled && purpose === "preview") {
|
|
4533
|
+
const track = current.track.clone();
|
|
4534
|
+
track.enabled = true;
|
|
4535
|
+
const release = () => {
|
|
4536
|
+
this.#pendingCaptures.delete(release);
|
|
4537
|
+
track.stop();
|
|
4538
|
+
};
|
|
4539
|
+
this.#pendingCaptures.add(release);
|
|
4540
|
+
return Effect5.succeed({ track, release });
|
|
4541
|
+
}
|
|
4542
|
+
return this.#borrowCamera(current.owner);
|
|
4543
|
+
}
|
|
4544
|
+
#beginCameraCapture(acquire) {
|
|
4545
|
+
const abort = new AbortController();
|
|
4546
|
+
const owner = {
|
|
4547
|
+
result: Deferred2.makeUnsafe(),
|
|
4548
|
+
references: 0,
|
|
4549
|
+
cancel: () => {
|
|
4550
|
+
abort.abort();
|
|
4551
|
+
Deferred2.doneUnsafe(owner.result, Effect5.fail(cancelledCaptureError()));
|
|
4552
|
+
}
|
|
4553
|
+
};
|
|
4554
|
+
this.#transitionCamera({ status: "capturing", owner });
|
|
4555
|
+
this.#fork(
|
|
4556
|
+
acquire(abort.signal).pipe(
|
|
4557
|
+
Effect5.tap(
|
|
4558
|
+
(track) => Effect5.try({
|
|
4559
|
+
try: () => {
|
|
4560
|
+
const state = this.#camera;
|
|
4561
|
+
if (abort.signal.aborted || !("owner" in state) || state.owner !== owner) {
|
|
4562
|
+
track.stop();
|
|
4563
|
+
throw cancelledCaptureError();
|
|
4564
|
+
}
|
|
4565
|
+
const onEnded = () => {
|
|
4566
|
+
if (this.#camera.status === "live" && this.#camera.owner === owner) this.#transitionCamera({ status: "ended" });
|
|
4567
|
+
};
|
|
4568
|
+
this.#transitionCamera({ status: "live", owner, track, publication: state.status === "live" ? state.publication : null, onEnded });
|
|
4569
|
+
if (track.readyState !== "live") {
|
|
4570
|
+
this.#transitionCamera({ status: "ended" });
|
|
4571
|
+
throw cancelledCaptureError();
|
|
4572
|
+
}
|
|
4573
|
+
},
|
|
4574
|
+
catch: (cause) => cause
|
|
4575
|
+
})
|
|
4576
|
+
),
|
|
4577
|
+
Effect5.onExit((exit) => Deferred2.done(owner.result, exit)),
|
|
4578
|
+
Effect5.ignore
|
|
4579
|
+
)
|
|
4580
|
+
);
|
|
4581
|
+
return owner;
|
|
4582
|
+
}
|
|
4583
|
+
#borrowCamera(owner) {
|
|
4584
|
+
owner.references++;
|
|
4585
|
+
let released = false;
|
|
4586
|
+
const release = () => {
|
|
4587
|
+
if (released) return;
|
|
4588
|
+
released = true;
|
|
4589
|
+
owner.references--;
|
|
4590
|
+
const current = this.#camera;
|
|
4591
|
+
if (owner.references === 0 && "owner" in current && current.owner === owner && (current.status !== "live" || current.publication === null)) this.#transitionCamera({ status: "ended" });
|
|
4592
|
+
};
|
|
4593
|
+
return Deferred2.await(owner.result).pipe(
|
|
4594
|
+
Effect5.flatMap((track) => track.readyState === "live" ? Effect5.succeed({ track, release }) : Effect5.fail(cancelledCaptureError())),
|
|
4595
|
+
Effect5.onExit(
|
|
4596
|
+
(exit) => Effect5.sync(() => {
|
|
4597
|
+
if (Exit2.isFailure(exit)) release();
|
|
4234
4598
|
})
|
|
4235
4599
|
)
|
|
4236
4600
|
);
|
|
4237
4601
|
}
|
|
4602
|
+
#useCamera(use, acquire) {
|
|
4603
|
+
return Effect5.suspend(() => {
|
|
4604
|
+
let preview;
|
|
4605
|
+
return this.#cameraSource("publication", acquire).pipe(
|
|
4606
|
+
Effect5.tap(
|
|
4607
|
+
(value) => Effect5.sync(() => {
|
|
4608
|
+
preview = value;
|
|
4609
|
+
})
|
|
4610
|
+
),
|
|
4611
|
+
Effect5.flatMap((value) => use(value.track)),
|
|
4612
|
+
Effect5.ensuring(Effect5.sync(() => preview?.release()))
|
|
4613
|
+
);
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4616
|
+
#captureForPublish(source, media, prepare) {
|
|
4617
|
+
return source === "camera" ? foreign2(() => media.clearPreparedLocalTrack("camera", { stopTrack: false })).pipe(Effect5.retry({ times: 1 }), Effect5.andThen(this.#useCamera(prepare))) : this.#captureSource(source).pipe(Effect5.flatMap(prepare));
|
|
4618
|
+
}
|
|
4619
|
+
#captureStream(operation, use, onCaptureError = (cause) => cause, signal) {
|
|
4620
|
+
return Effect5.suspend(() => {
|
|
4621
|
+
let cancelled = false;
|
|
4622
|
+
let captured = null;
|
|
4623
|
+
const cancel = () => {
|
|
4624
|
+
cancelled = true;
|
|
4625
|
+
stopStream(captured);
|
|
4626
|
+
};
|
|
4627
|
+
this.#pendingCaptures.add(cancel);
|
|
4628
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
4629
|
+
if (signal?.aborted) cancel();
|
|
4630
|
+
return foreign2(async () => {
|
|
4631
|
+
if (cancelled) throw cancelledCaptureError();
|
|
4632
|
+
const stream = await operation();
|
|
4633
|
+
captured = stream;
|
|
4634
|
+
if (cancelled) {
|
|
4635
|
+
stopStream(stream);
|
|
4636
|
+
throw cancelledCaptureError();
|
|
4637
|
+
}
|
|
4638
|
+
return stream;
|
|
4639
|
+
}).pipe(
|
|
4640
|
+
Effect5.mapError(onCaptureError),
|
|
4641
|
+
Effect5.flatMap(use),
|
|
4642
|
+
Effect5.tap(() => cancelled ? Effect5.fail(cancelledCaptureError()) : Effect5.void),
|
|
4643
|
+
Effect5.onExit(
|
|
4644
|
+
(exit) => Effect5.sync(() => {
|
|
4645
|
+
this.#pendingCaptures.delete(cancel);
|
|
4646
|
+
signal?.removeEventListener("abort", cancel);
|
|
4647
|
+
if (Exit2.isFailure(exit)) cancel();
|
|
4648
|
+
})
|
|
4649
|
+
)
|
|
4650
|
+
);
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
#cancelCaptures() {
|
|
4654
|
+
this.#transitionCamera({ status: "ended" });
|
|
4655
|
+
for (const cancel of this.#pendingCaptures) cancel();
|
|
4656
|
+
this.#pendingCaptures.clear();
|
|
4657
|
+
}
|
|
4658
|
+
#captureSource(source, signal) {
|
|
4659
|
+
return this.#captureStream(
|
|
4660
|
+
() => this.#selection.getUserMedia({ audio: source === "microphone", video: source === "camera" }),
|
|
4661
|
+
(stream) => this.#refreshDevices().pipe(
|
|
4662
|
+
Effect5.ignore,
|
|
4663
|
+
Effect5.andThen(
|
|
4664
|
+
Effect5.try({
|
|
4665
|
+
try: () => {
|
|
4666
|
+
const track = stream.getTracks().find((candidate) => candidate.kind === (source === "microphone" ? "audio" : "video"));
|
|
4667
|
+
if (!track) throw new TypeError(`Media capture did not return a ${source} track`);
|
|
4668
|
+
for (const candidate of stream.getTracks()) if (candidate !== track) candidate.stop();
|
|
4669
|
+
return track;
|
|
4670
|
+
},
|
|
4671
|
+
catch: (cause) => cause
|
|
4672
|
+
})
|
|
4673
|
+
)
|
|
4674
|
+
),
|
|
4675
|
+
(cause) => captureError(cause, source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled", `${source} permission was denied`),
|
|
4676
|
+
signal
|
|
4677
|
+
);
|
|
4678
|
+
}
|
|
4238
4679
|
#refreshDevices() {
|
|
4239
4680
|
return foreign2(() => this.#selection.enumerateDevices()).pipe(
|
|
4240
4681
|
Effect5.tap(
|
|
@@ -4248,11 +4689,15 @@ var MediaControllerRuntime = class {
|
|
|
4248
4689
|
);
|
|
4249
4690
|
}
|
|
4250
4691
|
#discardCaptured(source, track, media, prepared) {
|
|
4692
|
+
if (source === "camera")
|
|
4693
|
+
return Effect5.sync(() => {
|
|
4694
|
+
if (this.#track(source) === track) this.#deleteTrack(source);
|
|
4695
|
+
});
|
|
4251
4696
|
const clear = prepared ? foreign2(() => media.clearPreparedLocalTrack(source)).pipe(Effect5.ignore) : Effect5.void;
|
|
4252
4697
|
return clear.pipe(
|
|
4253
4698
|
Effect5.andThen(
|
|
4254
4699
|
Effect5.sync(() => {
|
|
4255
|
-
if (this.#
|
|
4700
|
+
if (this.#track(source) === track) this.#deleteTrack(source);
|
|
4256
4701
|
track.stop();
|
|
4257
4702
|
})
|
|
4258
4703
|
)
|
|
@@ -4260,7 +4705,6 @@ var MediaControllerRuntime = class {
|
|
|
4260
4705
|
}
|
|
4261
4706
|
#rollbackCapture(input) {
|
|
4262
4707
|
if (isAccessInvalid2(input.cause)) return Effect5.fail(input.cause);
|
|
4263
|
-
input.restoreIntent?.();
|
|
4264
4708
|
const rollback = input.track ? this.#discardCaptured(input.source, input.track, input.media, input.prepared) : input.fallback;
|
|
4265
4709
|
const captureWasUnavailable = input.action === "startScreenShare" && !input.prepared && input.track === null;
|
|
4266
4710
|
const failure2 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
|
|
@@ -4270,21 +4714,33 @@ var MediaControllerRuntime = class {
|
|
|
4270
4714
|
if (this.#connection.getSnapshot().state === "live" && this.#ports?.sync === ports.sync && this.#ports.media === ports.media) return;
|
|
4271
4715
|
throw new ConnectionError({ code: "invalid_state", action, recoverable: false, message: `${action} belongs to an inactive connection` });
|
|
4272
4716
|
}
|
|
4717
|
+
#pausePublication(ports, source, revision) {
|
|
4718
|
+
const state = this.#sources[source];
|
|
4719
|
+
if (state.desired.revision !== revision || ports.sync.getSnapshot().control?.status === "ended") return;
|
|
4720
|
+
state.forcedRevision = revision;
|
|
4721
|
+
state.localEnabled = false;
|
|
4722
|
+
this.#pauseTracks(source);
|
|
4723
|
+
ports.media.setLocalSourceIntent?.(source, false);
|
|
4724
|
+
if (ports.media.closeForcedLocalPublication) this.#fork(foreign2(() => ports.media.closeForcedLocalPublication(source)).pipe(Effect5.ignore));
|
|
4725
|
+
this.#publish();
|
|
4726
|
+
}
|
|
4273
4727
|
#bind(ports) {
|
|
4274
|
-
this.#
|
|
4275
|
-
this.#unsubscribeRequests?.();
|
|
4276
|
-
this.#unsubscribeSync?.();
|
|
4728
|
+
if (ports && this.#ports?.sync === ports.sync && this.#ports.media === ports.media) return;
|
|
4729
|
+
for (const unsubscribe of [this.#unsubscribeMedia, this.#unsubscribeRequests, this.#unsubscribePause, this.#unsubscribeSync]) unsubscribe?.();
|
|
4277
4730
|
this.#unsubscribeMedia = null;
|
|
4278
4731
|
this.#unsubscribeRequests = null;
|
|
4732
|
+
this.#unsubscribePause = null;
|
|
4279
4733
|
this.#unsubscribeSync = null;
|
|
4280
|
-
if (this.#ports?.media !== ports?.media) this.#observedPublicationIds.clear();
|
|
4281
4734
|
this.#ports = ports;
|
|
4735
|
+
this.#syncLive = false;
|
|
4736
|
+
this.#bindCamera(ports);
|
|
4282
4737
|
if (!ports) {
|
|
4738
|
+
this.#resetObservations();
|
|
4739
|
+
this.#cancelCaptures();
|
|
4283
4740
|
this.#screenAudioTrack?.stop();
|
|
4284
4741
|
this.#screenAudioTrack = null;
|
|
4285
4742
|
for (const track of this.#tracks.values()) track.stop();
|
|
4286
|
-
this.#
|
|
4287
|
-
this.#screenEndedPending = false;
|
|
4743
|
+
this.#clearTracks();
|
|
4288
4744
|
this.#clearRequests();
|
|
4289
4745
|
this.#publish();
|
|
4290
4746
|
return;
|
|
@@ -4293,56 +4749,63 @@ var MediaControllerRuntime = class {
|
|
|
4293
4749
|
this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
|
|
4294
4750
|
this.#publish();
|
|
4295
4751
|
});
|
|
4752
|
+
this.#unsubscribePause = this.#subscribePauses(ports);
|
|
4296
4753
|
this.#unsubscribeRequests = ports.sync.onDirectedRequest((request) => this.#request(request));
|
|
4297
|
-
this.#unsubscribeSync = ports
|
|
4298
|
-
this.#removeRequestsFromMissingParticipants(snapshot);
|
|
4299
|
-
this.#applyRemoteMediaState(ports, snapshot);
|
|
4300
|
-
this.#applyForcedMediaState(ports, snapshot);
|
|
4301
|
-
});
|
|
4754
|
+
this.#unsubscribeSync = this.#subscribeSync(ports);
|
|
4302
4755
|
this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
|
|
4303
|
-
this.#applyForcedMediaState(ports, ports.sync.getSnapshot());
|
|
4304
4756
|
this.#publish();
|
|
4305
4757
|
}
|
|
4306
|
-
#
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4758
|
+
#subscribePauses(ports) {
|
|
4759
|
+
return ports.sync.onMediaPause?.((pause) => {
|
|
4760
|
+
const state = this.#sources[pause.source];
|
|
4761
|
+
if (pause.revision <= state.serverSequence) return;
|
|
4762
|
+
state.serverSequence = pause.revision;
|
|
4763
|
+
this.#pausePublication(ports, pause.source, state.sentRevision);
|
|
4764
|
+
}) ?? null;
|
|
4310
4765
|
}
|
|
4311
|
-
#
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
if (
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
}
|
|
4319
|
-
const previousPublicationId = this.#observedPublicationIds.get(source);
|
|
4320
|
-
if (!this.#observedPublicationIds.delete(source)) continue;
|
|
4321
|
-
const local = ports.media.getSnapshot().localTracks.find((publication) => publication.source === source);
|
|
4322
|
-
if (local && local.publicationId !== previousPublicationId) continue;
|
|
4323
|
-
this.#pauseForcedSource(ports, source);
|
|
4766
|
+
#rememberPublications(snapshot) {
|
|
4767
|
+
const publications = /* @__PURE__ */ new Map();
|
|
4768
|
+
const own = (snapshot.media?.items ?? []).filter((item) => item.participantId === snapshot.participantId && item.enabled && item.publicationId !== null);
|
|
4769
|
+
for (const item of own) {
|
|
4770
|
+
if (item.source === "screen_audio" || item.publicationId === null) continue;
|
|
4771
|
+
const state = this.#sources[item.source];
|
|
4772
|
+
if (state.localEnabled && state.sentRevision === state.desired.revision) publications.set(item.source, { revision: state.sentRevision, publicationId: item.publicationId });
|
|
4324
4773
|
}
|
|
4774
|
+
return publications;
|
|
4325
4775
|
}
|
|
4326
|
-
#
|
|
4327
|
-
const
|
|
4328
|
-
|
|
4329
|
-
this.#
|
|
4776
|
+
#pauseLostPublications(ports, snapshot, publications) {
|
|
4777
|
+
for (const [source, previous] of publications) {
|
|
4778
|
+
const current = snapshot.media?.items.find((item) => item.participantId === snapshot.participantId && item.source === source);
|
|
4779
|
+
if (!current || !current.enabled && (current.publicationId === previous.publicationId || current.publicationId === null)) this.#pausePublication(ports, source, previous.revision);
|
|
4330
4780
|
}
|
|
4331
|
-
const track = this.#tracks.get(source);
|
|
4332
|
-
if (track) track.enabled = false;
|
|
4333
|
-
ports.media.setLocalSourceIntent?.(source, false);
|
|
4334
|
-
if (ports.media.closeForcedLocalPublication) void ports.media.closeForcedLocalPublication(source).catch(() => void 0);
|
|
4335
|
-
if (pendingEnable) ports.media.setLocalSourceIntent?.(source, true);
|
|
4336
|
-
this.#publish();
|
|
4337
4781
|
}
|
|
4338
|
-
#
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4782
|
+
#subscribeSync(ports) {
|
|
4783
|
+
let recovering = /* @__PURE__ */ new Map();
|
|
4784
|
+
let lastLive = ports.sync.getSnapshot();
|
|
4785
|
+
let wasLive = lastLive.connection.phase === "live";
|
|
4786
|
+
return ports.sync.subscribe((snapshot) => {
|
|
4787
|
+
const live = snapshot.connection.phase === "live";
|
|
4788
|
+
if (wasLive && !live) recovering = this.#rememberPublications(lastLive);
|
|
4789
|
+
if (live && !wasLive) {
|
|
4790
|
+
this.#pauseLostPublications(ports, snapshot, recovering);
|
|
4791
|
+
recovering.clear();
|
|
4792
|
+
}
|
|
4793
|
+
if (live) lastLive = snapshot;
|
|
4794
|
+
wasLive = live;
|
|
4795
|
+
this.#removeRequestsFromMissingParticipants(snapshot);
|
|
4796
|
+
this.#applyRemoteMediaState(ports, snapshot);
|
|
4797
|
+
this.#reconcileIntents();
|
|
4798
|
+
});
|
|
4799
|
+
}
|
|
4800
|
+
#bindCamera(ports) {
|
|
4801
|
+
if (!ports) return;
|
|
4802
|
+
const state = this.#camera;
|
|
4803
|
+
if (state.status === "live" && state.publication === "initial") this.#transitionCamera({ ...state, publication: ports.media });
|
|
4342
4804
|
}
|
|
4343
|
-
#
|
|
4344
|
-
const
|
|
4345
|
-
|
|
4805
|
+
#applyRemoteMediaState(ports, snapshot) {
|
|
4806
|
+
for (const publication of ports.media.getSnapshot().remoteTracks) {
|
|
4807
|
+
publication.track.enabled = snapshot.media?.items.some((item) => item.participantId === publication.participantId && item.source === (publication.source === "screen_audio" ? "screen" : publication.source) && item.enabled) ?? false;
|
|
4808
|
+
}
|
|
4346
4809
|
}
|
|
4347
4810
|
#request(request) {
|
|
4348
4811
|
if (request.expires_at_ms <= this.#now()) return;
|
|
@@ -4387,10 +4850,7 @@ var MediaControllerRuntime = class {
|
|
|
4387
4850
|
this.#requests = Object.freeze([]);
|
|
4388
4851
|
}
|
|
4389
4852
|
#handleConnectionChange() {
|
|
4390
|
-
|
|
4391
|
-
this.#screenEndedPending = false;
|
|
4392
|
-
this.#fork(this.#stopScreen().pipe(Effect5.ignore));
|
|
4393
|
-
}
|
|
4853
|
+
this.#reconcileIntents();
|
|
4394
4854
|
this.#publish();
|
|
4395
4855
|
}
|
|
4396
4856
|
#handleScreenAudioEnded(track, ports) {
|
|
@@ -4404,12 +4864,8 @@ var MediaControllerRuntime = class {
|
|
|
4404
4864
|
).pipe(Effect5.ignore)
|
|
4405
4865
|
);
|
|
4406
4866
|
}
|
|
4407
|
-
#handleScreenEnded(track = this.#
|
|
4408
|
-
if (!track || this.#
|
|
4409
|
-
if (this.#connection.getSnapshot().state === "reconnecting") {
|
|
4410
|
-
this.#screenEndedPending = true;
|
|
4411
|
-
return;
|
|
4412
|
-
}
|
|
4867
|
+
#handleScreenEnded(track = this.#track("screen")) {
|
|
4868
|
+
if (!track || this.#track("screen") !== track) return;
|
|
4413
4869
|
this.#fork(this.#stopScreen().pipe(Effect5.ignore));
|
|
4414
4870
|
}
|
|
4415
4871
|
#publish() {
|
|
@@ -4417,9 +4873,9 @@ var MediaControllerRuntime = class {
|
|
|
4417
4873
|
const snapshot = this.#ports?.media.getSnapshot();
|
|
4418
4874
|
const state = this.#connection.getSnapshot().state;
|
|
4419
4875
|
const local = Object.freeze({
|
|
4420
|
-
microphone: localMedia("microphone", this.#
|
|
4421
|
-
camera: localMedia("camera", this.#
|
|
4422
|
-
screen: localMedia("screen", this.#
|
|
4876
|
+
microphone: localMedia("microphone", this.#localTracks(), snapshot, this.#sources.microphone.localEnabled, state, this.#pendingSources.has("microphone")),
|
|
4877
|
+
camera: localMedia("camera", this.#localTracks(), snapshot, this.#sources.camera.localEnabled, state, this.#pendingSources.has("camera")),
|
|
4878
|
+
screen: localMedia("screen", this.#localTracks(), snapshot, false, state, this.#pendingSources.has("screen"))
|
|
4423
4879
|
});
|
|
4424
4880
|
const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
|
|
4425
4881
|
const media = Object.freeze({
|
|
@@ -4436,6 +4892,9 @@ var MediaControllerRuntime = class {
|
|
|
4436
4892
|
if (!sameMediaSlice(current.media, media)) this.#store.updateMedia(media);
|
|
4437
4893
|
}
|
|
4438
4894
|
};
|
|
4895
|
+
function cancelledCaptureError() {
|
|
4896
|
+
return new ConnectionError({ code: "invalid_state", action: null, recoverable: false, message: "Media capture was cancelled before its result could be used" });
|
|
4897
|
+
}
|
|
4439
4898
|
function foreign2(operation) {
|
|
4440
4899
|
return Effect5.tryPromise({ try: operation, catch: (cause) => cause });
|
|
4441
4900
|
}
|
|
@@ -4535,12 +4994,6 @@ function isPermissionDenied(cause) {
|
|
|
4535
4994
|
function isAccessInvalid2(cause) {
|
|
4536
4995
|
return typeof cause === "object" && cause !== null && "code" in cause && (cause.code === "access.invalid" || cause.code === "invalid_access");
|
|
4537
4996
|
}
|
|
4538
|
-
function isSyncRejection(cause) {
|
|
4539
|
-
return isAccessInvalid2(cause) || cause instanceof V1SyncError && cause.code === "terminal_failure";
|
|
4540
|
-
}
|
|
4541
|
-
function canApplyForcedMediaState(snapshot) {
|
|
4542
|
-
return !!snapshot.media && !!snapshot.participantId && snapshot.control?.status !== "ended" && snapshot.connection.phase !== "terminal";
|
|
4543
|
-
}
|
|
4544
4997
|
|
|
4545
4998
|
// src/space-client/offline-actions.ts
|
|
4546
4999
|
import { Context as Context7, Effect as Effect6, Layer as Layer7 } from "effect";
|
|
@@ -5470,8 +5923,8 @@ var SpaceClientCore = class {
|
|
|
5470
5923
|
subscribe = (listener) => this.#store.subscribe(listener);
|
|
5471
5924
|
join(options = {}) {
|
|
5472
5925
|
return Effect12.sync(() => this.#requireActive()).pipe(
|
|
5473
|
-
Effect12.tap(() => Effect12.sync(() => this.controllers.media.configure(options))),
|
|
5474
5926
|
Effect12.andThen(this.#connection.configureJoin(options)),
|
|
5927
|
+
Effect12.tap(() => Effect12.sync(() => this.controllers.media.configure(options))),
|
|
5475
5928
|
Effect12.andThen(this.#connection.join()),
|
|
5476
5929
|
Effect12.andThen(options.displayName === void 0 ? Effect12.void : this.controllers.participants.renameSelf(options.displayName)),
|
|
5477
5930
|
Effect12.catchTag("ConnectionLifecycleFailure", (failure2) => Effect12.fail(failure2.code === "episode_ended" ? new SpaceClientError({ code: "episode.ended", recoverable: failure2.recoverable, message: failure2.message }) : failure2)),
|
|
@@ -5671,6 +6124,7 @@ export {
|
|
|
5671
6124
|
InMemoryChalkWhiteboardV1PendingOperationStore,
|
|
5672
6125
|
ChalkWhiteboardV1Client,
|
|
5673
6126
|
createChalkWhiteboardV1Client,
|
|
6127
|
+
ChalkWhiteboardViewBounds,
|
|
5674
6128
|
makeProductionConnectionPlatformLayer,
|
|
5675
6129
|
ConnectionLifecycleService,
|
|
5676
6130
|
makeConnectionLifecycleLayer,
|