@aperture-browser/session-react 0.0.0-init.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.
@@ -0,0 +1,2135 @@
1
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Stream from "effect/Stream";
4
+ import { DEFAULT_VIEWPORT, LiveSessionConnection, LiveSessionError, LiveSessionError as LiveSessionError$1, computeRenderMetrics, createViewportPreset, evdevKeycodeByCode, keyboardInputMessage, keyboardModifiers, shouldForwardBrowserShortcut, strictParseOptions, windowsVirtualKeyCodeForCodeOrKey } from "@aperture-browser/live-session";
5
+ import * as Option from "effect/Option";
6
+ import * as PubSub from "effect/PubSub";
7
+ import * as Schema from "effect/Schema";
8
+ import * as SubscriptionRef from "effect/SubscriptionRef";
9
+ import * as Data from "effect/Data";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { SessionsApi, apiClientLayer, baseUrlLayer } from "@aperture-browser/api-client";
12
+ import * as Layer from "effect/Layer";
13
+ import * as ManagedRuntime from "effect/ManagedRuntime";
14
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
15
+ import * as HttpClient from "effect/unstable/http/HttpClient";
16
+ //#region src/effect.tsx
17
+ const RuntimeContext = createContext(null);
18
+ /** A hook below needed the app runtime, but no RuntimeProvider is above the component. */
19
+ var RuntimeProviderMissingError = class extends Data.TaggedError("RuntimeProviderMissingError") {
20
+ message = "useRuntime must be used inside RuntimeProvider";
21
+ };
22
+ function RuntimeProvider({ runtime, baseUrl, children }) {
23
+ const value = useMemo(() => ({
24
+ runtime,
25
+ baseUrl
26
+ }), [runtime, baseUrl]);
27
+ return /* @__PURE__ */ jsx(RuntimeContext.Provider, {
28
+ value,
29
+ children
30
+ });
31
+ }
32
+ function useRuntimeContext() {
33
+ const context = useContext(RuntimeContext);
34
+ if (!context) throw new RuntimeProviderMissingError();
35
+ return context;
36
+ }
37
+ function useRuntime() {
38
+ return useRuntimeContext().runtime;
39
+ }
40
+ function useBaseUrl() {
41
+ return useRuntimeContext().baseUrl;
42
+ }
43
+ /**
44
+ * Runs an effect while the component is mounted, restarting it when `deps` change, like
45
+ * `useEffect`. Return `undefined` to run nothing. The fiber is interrupted synchronously
46
+ * on cleanup, so its synchronous finalizers have run before the next effect starts.
47
+ */
48
+ function useFork(effect, deps) {
49
+ const runtime = useRuntime();
50
+ useEffect(() => {
51
+ const program = effect();
52
+ if (!program) return;
53
+ const fiber = runtime.runFork(program);
54
+ return () => fiber.interruptUnsafe();
55
+ }, [runtime, ...deps]);
56
+ }
57
+ /**
58
+ * Returns an event handler that forks the effect `callback` builds. Fibers still running
59
+ * when the component unmounts are interrupted.
60
+ */
61
+ function useEffectCallback(callback, deps) {
62
+ const runtime = useRuntime();
63
+ const fibers = useRef(/* @__PURE__ */ new Set());
64
+ useEffect(() => {
65
+ const running = fibers.current;
66
+ return () => {
67
+ for (const fiber of running) fiber.interruptUnsafe();
68
+ running.clear();
69
+ };
70
+ }, []);
71
+ return useCallback((...args) => {
72
+ const fiber = runtime.runFork(callback(...args));
73
+ if (fiber.pollUnsafe()) return;
74
+ fibers.current.add(fiber);
75
+ fiber.addObserver(() => fibers.current.delete(fiber));
76
+ }, [runtime, ...deps]);
77
+ }
78
+ /**
79
+ * Returns a function that runs an API call for promise-based callers such as TanStack
80
+ * Query. Aborting the signal interrupts the request.
81
+ */
82
+ function useRunApi() {
83
+ const runtime = useRuntime();
84
+ return useCallback((call, options) => runtime.runPromise(call, options), [runtime]);
85
+ }
86
+ //#endregion
87
+ //#region src/hooks/use-live-session.ts
88
+ const heartbeatIntervalMs = 2e3;
89
+ const transportUnavailable = () => new LiveSessionConnection.LiveSessionError({ message: "live session transport is unavailable" });
90
+ const cursorIntervalMs = 40;
91
+ const collaborationPaintLifetimeMs = 7e3;
92
+ const pointerButtonCode = {
93
+ left: 272,
94
+ right: 273,
95
+ middle: 274
96
+ };
97
+ function useLiveSession({ sessionId, displayName = null, credentials, sessionToken, role, enabled, webrtcSupported, iceServers }) {
98
+ const identity = useMemo(() => collaborationIdentity(role, displayName), [displayName, role]);
99
+ const runtime = useRuntime();
100
+ const baseUrl = useBaseUrl();
101
+ const frameRef = useMemo(() => runtime.runSync(SubscriptionRef.make(null)), [runtime]);
102
+ const paintPubSub = useMemo(() => runtime.runSync(PubSub.unbounded()), [runtime]);
103
+ const frames = useMemo(() => SubscriptionRef.changes(frameRef), [frameRef]);
104
+ const paintEvents = useMemo(() => Stream.fromPubSub(paintPubSub), [paintPubSub]);
105
+ const publishFrame = useCallback((frame) => runtime.runSync(SubscriptionRef.set(frameRef, frame)), [frameRef, runtime]);
106
+ const publishPaint = useCallback((event) => PubSub.publishUnsafe(paintPubSub, event), [paintPubSub]);
107
+ const connectionRef = useRef(null);
108
+ const holderClientIdRef = useRef(null);
109
+ const clientIdRef = useRef("");
110
+ const claimPendingRef = useRef(false);
111
+ const releasePendingRef = useRef(false);
112
+ const cursorVisibleRef = useRef(false);
113
+ const lastCursorSentAtRef = useRef(0);
114
+ const participantsRef = useRef([]);
115
+ const followingClientIdRef = useRef(null);
116
+ const mediaSizeRef = useRef(null);
117
+ const transportRef = useRef(null);
118
+ const presentationRef = useRef(null);
119
+ const presentationSelectionRef = useRef(false);
120
+ const [phase, setPhase] = useState("idle");
121
+ const [targets, setTargets] = useState([]);
122
+ const [activeTargetId, setActiveTargetId] = useState(null);
123
+ const [mediaStream, setMediaStream] = useState(null);
124
+ const [mediaSize, setMediaSize] = useState(null);
125
+ const [transport, setTransport] = useState(null);
126
+ const [targetSwitching, setTargetSwitching] = useState(false);
127
+ const [presentationSwitching, setPresentationSwitching] = useState(false);
128
+ const [presentation, setPresentation] = useState(null);
129
+ const [recordings, setRecordings] = useState([]);
130
+ const [clientId, setClientId] = useState("");
131
+ const [holderClientId, setHolderClientId] = useState(null);
132
+ const [leaseMode, setLeaseMode] = useState(null);
133
+ const [participants, setParticipants] = useState([]);
134
+ const [cursors, setCursors] = useState(() => /* @__PURE__ */ new Map());
135
+ const [lastError, setLastError] = useState(null);
136
+ mediaSizeRef.current = mediaSize;
137
+ transportRef.current = transport;
138
+ presentationRef.current = presentation;
139
+ const handleMessage = useCallback((message) => {
140
+ switch (message.type) {
141
+ case "session.snapshot":
142
+ clientIdRef.current = message.clientId;
143
+ setClientId(message.clientId);
144
+ holderClientIdRef.current = message.holderClientId ?? null;
145
+ setHolderClientId(message.holderClientId ?? null);
146
+ setLeaseMode(message.mode ?? null);
147
+ participantsRef.current = message.participants;
148
+ setParticipants(message.participants);
149
+ followingClientIdRef.current = message.participants.find((participant) => participant.clientId === message.clientId)?.followingClientId ?? null;
150
+ setTargets(message.targets);
151
+ setActiveTargetId(message.activeTargetId ?? null);
152
+ setMediaSize(resolveMediaSize(message.targets, message.activeTargetId));
153
+ setRecordings(message.recordings);
154
+ setPresentation(message.presentation ?? null);
155
+ setLastError(null);
156
+ return;
157
+ case "targets.state":
158
+ setTargets(message.targets);
159
+ setActiveTargetId(message.activeTargetId ?? null);
160
+ setMediaSize(resolveMediaSize(message.targets, message.activeTargetId));
161
+ setTargetSwitching(false);
162
+ return;
163
+ case "presence.state":
164
+ participantsRef.current = message.participants;
165
+ setParticipants(message.participants);
166
+ followingClientIdRef.current = message.participants.find((participant) => participant.clientId === clientIdRef.current)?.followingClientId ?? null;
167
+ setCursors((current) => {
168
+ const followed = followingClientIdRef.current;
169
+ if (!followed) return current.size === 0 ? current : /* @__PURE__ */ new Map();
170
+ const cursor = current.get(followed);
171
+ return cursor ? /* @__PURE__ */ new Map([[followed, cursor]]) : /* @__PURE__ */ new Map();
172
+ });
173
+ return;
174
+ case "input.state":
175
+ holderClientIdRef.current = message.holderClientId ?? null;
176
+ setHolderClientId(message.holderClientId ?? null);
177
+ setLeaseMode(message.mode ?? null);
178
+ claimPendingRef.current = false;
179
+ releasePendingRef.current = false;
180
+ setLastError((current) => current?.code === "input_busy" || current?.code === "input_not_owned" ? null : current);
181
+ return;
182
+ case "presence.cursor":
183
+ if (message.clientId !== followingClientIdRef.current) return;
184
+ setCursors((current) => new Map(current).set(message.clientId, message));
185
+ return;
186
+ case "presence.cursor.clear":
187
+ setCursors((current) => {
188
+ if (!current.has(message.clientId)) return current;
189
+ const next = new Map(current);
190
+ next.delete(message.clientId);
191
+ return next;
192
+ });
193
+ return;
194
+ case "paint.point":
195
+ if (participantsRef.current.some((participant) => participant.clientId === message.clientId)) publishPaint({
196
+ type: "point",
197
+ message
198
+ });
199
+ return;
200
+ case "recordings.state":
201
+ setRecordings(message.recordings);
202
+ return;
203
+ case "presentation.state":
204
+ setPresentation(message.presentation);
205
+ return;
206
+ case "error":
207
+ claimPendingRef.current = false;
208
+ releasePendingRef.current = false;
209
+ setLastError({
210
+ code: message.code,
211
+ message: message.message
212
+ });
213
+ return;
214
+ default:
215
+ if (message.presentation) setPresentation(message.presentation);
216
+ if (!message.ok) setLastError({
217
+ code: message.code ?? "request_rejected",
218
+ message: message.message ?? "Live session command failed."
219
+ });
220
+ }
221
+ }, [publishPaint]);
222
+ useFork(() => {
223
+ if (!enabled || !sessionId || !credentials) {
224
+ connectionRef.current = null;
225
+ setPhase("idle");
226
+ setTargets([]);
227
+ setActiveTargetId(null);
228
+ setMediaStream(null);
229
+ setMediaSize(null);
230
+ setTransport(null);
231
+ setPresentation(null);
232
+ setTargetSwitching(false);
233
+ setPresentationSwitching(false);
234
+ setParticipants([]);
235
+ setCursors(/* @__PURE__ */ new Map());
236
+ setRecordings([]);
237
+ publishFrame(null);
238
+ publishPaint({ type: "clear" });
239
+ return;
240
+ }
241
+ return Effect.gen(function* () {
242
+ const connection = yield* LiveSessionConnection.make({
243
+ baseUrl,
244
+ sessionId,
245
+ credentials,
246
+ sessionToken,
247
+ identity,
248
+ iceServers,
249
+ webrtcSupported,
250
+ callbacks: {
251
+ onPhase: setPhase,
252
+ onMessage: handleMessage,
253
+ onFrame: publishFrame,
254
+ onStream: setMediaStream,
255
+ onTransport: setTransport,
256
+ onError: (message) => setLastError({
257
+ code: "connection_failed",
258
+ message
259
+ })
260
+ }
261
+ });
262
+ connectionRef.current = connection;
263
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
264
+ if (connectionRef.current === connection) connectionRef.current = null;
265
+ }));
266
+ return yield* Effect.never;
267
+ }).pipe(Effect.scoped, Effect.ensuring(Effect.sync(() => {
268
+ publishFrame(null);
269
+ publishPaint({ type: "clear" });
270
+ })));
271
+ }, [
272
+ baseUrl,
273
+ credentials,
274
+ enabled,
275
+ handleMessage,
276
+ iceServers,
277
+ identity,
278
+ publishFrame,
279
+ publishPaint,
280
+ sessionId,
281
+ sessionToken,
282
+ webrtcSupported
283
+ ]);
284
+ const sendReliable = useCallback((message) => connectionRef.current?.sendReliable(message) ?? false, []);
285
+ const sendRealtime = useCallback((message) => connectionRef.current?.sendRealtime(message) ?? false, []);
286
+ const runCommand = useEffectCallback((connection, type, payload) => connection.command(type, payload).pipe(Effect.catch((error) => Effect.sync(() => {
287
+ setTargetSwitching(false);
288
+ setLastError({
289
+ code: "request_rejected",
290
+ message: error.message
291
+ });
292
+ }))), []);
293
+ const command = useCallback((type, payload = {}) => {
294
+ const connection = connectionRef.current;
295
+ if (!connection) return false;
296
+ runCommand(connection, type, payload);
297
+ return true;
298
+ }, [runCommand]);
299
+ const request = useCallback((type, payload = {}) => Effect.suspend(() => connectionRef.current ? connectionRef.current.command(type, payload) : Effect.fail(transportUnavailable())), []);
300
+ const runSelectTarget = useEffectCallback((connection, targetId) => connection.command("target.select", { targetId }).pipe(Effect.match({
301
+ onSuccess: () => setActiveTargetId(targetId),
302
+ onFailure: (error) => setLastError({
303
+ code: "request_rejected",
304
+ message: error.message
305
+ })
306
+ }), Effect.ensuring(Effect.sync(() => setTargetSwitching(false)))), []);
307
+ const requestSelectTarget = useCallback((targetId) => Effect.suspend(() => {
308
+ const connection = connectionRef.current;
309
+ if (!connection) return Effect.fail(transportUnavailable());
310
+ setTargetSwitching(true);
311
+ return connection.command("target.select", { targetId }).pipe(Effect.tap(() => Effect.sync(() => setActiveTargetId(targetId))), Effect.tapError((error) => Effect.sync(() => setLastError({
312
+ code: "request_rejected",
313
+ message: error.message
314
+ }))), Effect.ensuring(Effect.sync(() => setTargetSwitching(false))), Effect.asVoid);
315
+ }), []);
316
+ const selectTarget = useCallback((targetId) => {
317
+ const connection = connectionRef.current;
318
+ if (!connection) return false;
319
+ setTargetSwitching(true);
320
+ runSelectTarget(connection, targetId);
321
+ return true;
322
+ }, [runSelectTarget]);
323
+ const selectPresentation = useCallback((selection) => Effect.suspend(() => {
324
+ const connection = connectionRef.current;
325
+ if (!connection) return Effect.fail(transportUnavailable());
326
+ if (presentationSelectionRef.current) return Effect.fail(new LiveSessionConnection.LiveSessionError({ message: "another presentation update is already in progress" }));
327
+ presentationSelectionRef.current = true;
328
+ setPresentationSwitching(true);
329
+ return Effect.gen(function* () {
330
+ switch (selection.kind) {
331
+ case "jpeg": return yield* connection.selectTransport("websocket");
332
+ case "webrtc-retry": return yield* connection.selectTransport("webrtc");
333
+ case "webrtc": {
334
+ const size = mediaSizeRef.current;
335
+ if (!size) return yield* new LiveSessionConnection.LiveSessionError({ message: "presentation size is unavailable" });
336
+ const restoreWebRTC = presentationRef.current?.quality?.profile !== selection.quality.profile && transportRef.current === "webrtc";
337
+ if (restoreWebRTC) yield* connection.selectTransport("websocket");
338
+ const result = yield* connection.command("presentation.quality.set", {
339
+ profile: selection.quality.profile,
340
+ width: size.canvasWidth,
341
+ height: size.canvasHeight,
342
+ fps: selection.quality.fps,
343
+ bitrateKbps: selection.quality.bitrateKbps
344
+ }).pipe(Effect.tapError(() => restoreWebRTC ? Effect.ignore(connection.selectTransport("webrtc")) : Effect.void));
345
+ if (result.presentation) setPresentation(result.presentation);
346
+ return yield* connection.selectTransport("webrtc");
347
+ }
348
+ default: return selection;
349
+ }
350
+ }).pipe(Effect.tapError((error) => Effect.sync(() => setLastError({
351
+ code: "presentation_update_failed",
352
+ message: error.message
353
+ }))), Effect.ensuring(Effect.sync(() => {
354
+ presentationSelectionRef.current = false;
355
+ setPresentationSwitching(false);
356
+ })));
357
+ }), []);
358
+ const claim = useCallback((targetId, mode) => {
359
+ if (role === "viewer" || !sendReliable({
360
+ type: "input.claim",
361
+ targetId,
362
+ mode
363
+ })) return false;
364
+ claimPendingRef.current = true;
365
+ releasePendingRef.current = false;
366
+ return true;
367
+ }, [role, sendReliable]);
368
+ const release = useCallback(() => {
369
+ if (releasePendingRef.current || holderClientIdRef.current !== clientIdRef.current || !sendReliable({ type: "input.release" })) return false;
370
+ releasePendingRef.current = true;
371
+ claimPendingRef.current = false;
372
+ return true;
373
+ }, [sendReliable]);
374
+ const setActiveTarget = useCallback((targetId) => targetId ? selectTarget(targetId) : false, [selectTarget]);
375
+ const follow = useCallback((followingClientId) => sendReliable({
376
+ type: "follow.set",
377
+ followingClientId: followingClientId ?? ""
378
+ }), [sendReliable]);
379
+ const sendCursor = useCallback((targetId, x, y, dimensions) => {
380
+ const now = performance.now();
381
+ if (now - lastCursorSentAtRef.current < cursorIntervalMs) return false;
382
+ lastCursorSentAtRef.current = now;
383
+ const sent = sendRealtime({
384
+ type: "presence.cursor",
385
+ targetId,
386
+ x: normalizedCoordinate(x, dimensions.width),
387
+ y: normalizedCoordinate(y, dimensions.height)
388
+ });
389
+ if (sent) cursorVisibleRef.current = true;
390
+ return sent;
391
+ }, [sendRealtime]);
392
+ const sendPaintPoint = useCallback((point) => point.phase === "move" ? sendRealtime({
393
+ type: "paint.point",
394
+ ...point
395
+ }) : sendReliable({
396
+ type: "paint.point",
397
+ ...point
398
+ }), [sendRealtime, sendReliable]);
399
+ const clearCursor = useCallback(() => {
400
+ if (!cursorVisibleRef.current || !sendReliable({ type: "presence.cursor.clear" })) return false;
401
+ cursorVisibleRef.current = false;
402
+ return true;
403
+ }, [sendReliable]);
404
+ const sendInputEvent = useCallback((message, realtime = false) => realtime ? sendRealtime(message) : sendReliable(message), [sendRealtime, sendReliable]);
405
+ const sendBrowserInput = useCallback((message, dimensions) => {
406
+ if (role === "viewer" || holderClientIdRef.current !== clientIdRef.current && !claimPendingRef.current) return false;
407
+ switch (message.type) {
408
+ case "input.mouse": {
409
+ const x = normalizedCoordinate(message.x, dimensions.width);
410
+ const y = normalizedCoordinate(message.y, dimensions.height);
411
+ if (message.action === "move") return sendInputEvent({
412
+ type: "input.pointer.motion.absolute",
413
+ targetId: message.targetId,
414
+ x,
415
+ y,
416
+ width: dimensions.width,
417
+ height: dimensions.height,
418
+ modifiers: message.modifiers ?? 0
419
+ }, true);
420
+ if (message.button === "none") return true;
421
+ const buttonCode = pointerButtonCode[message.button ?? "left"];
422
+ const sendButton = (pressed, clickCount) => sendInputEvent({
423
+ type: "input.pointer.button",
424
+ targetId: message.targetId,
425
+ x,
426
+ y,
427
+ width: dimensions.width,
428
+ height: dimensions.height,
429
+ buttonCode,
430
+ pressed,
431
+ clickCount,
432
+ modifiers: message.modifiers ?? 0
433
+ });
434
+ if (message.action === "down" || message.action === "up") return sendButton(message.action === "down", message.clickCount ?? 1);
435
+ const count = message.action === "doubleClick" ? 2 : 1;
436
+ for (let index = 0; index < count; index += 1) {
437
+ const clickCount = message.action === "doubleClick" ? index + 1 : 1;
438
+ sendButton(true, clickCount);
439
+ sendButton(false, clickCount);
440
+ }
441
+ return true;
442
+ }
443
+ case "input.wheel": {
444
+ const x = normalizedCoordinate(message.x, dimensions.width);
445
+ const y = normalizedCoordinate(message.y, dimensions.height);
446
+ sendInputEvent({
447
+ type: "input.pointer.scroll",
448
+ targetId: message.targetId,
449
+ x,
450
+ y,
451
+ width: dimensions.width,
452
+ height: dimensions.height,
453
+ horizontal: message.deltaX,
454
+ vertical: message.deltaY,
455
+ stopHorizontal: false,
456
+ stopVertical: false,
457
+ modifiers: message.modifiers ?? 0
458
+ });
459
+ return sendInputEvent({
460
+ type: "input.pointer.scroll",
461
+ targetId: message.targetId,
462
+ x,
463
+ y,
464
+ width: dimensions.width,
465
+ height: dimensions.height,
466
+ horizontal: 0,
467
+ vertical: 0,
468
+ stopHorizontal: message.deltaX !== 0,
469
+ stopVertical: message.deltaY !== 0,
470
+ modifiers: message.modifiers ?? 0
471
+ });
472
+ }
473
+ case "input.key": {
474
+ if (message.action === "char") return message.text ? sendInputEvent({
475
+ type: "input.keyboard.text",
476
+ targetId: message.targetId,
477
+ text: message.text
478
+ }) : false;
479
+ const keycode = evdevKeycodeByCode[message.code ?? ""];
480
+ if (!keycode) return false;
481
+ const windowsVirtualKeyCode = message.windowsVirtualKeyCode ?? windowsVirtualKeyCodeForCodeOrKey(message.code, message.key);
482
+ return sendInputEvent({
483
+ type: "input.keyboard.key",
484
+ targetId: message.targetId,
485
+ keycode,
486
+ pressed: message.action === "down",
487
+ key: message.key,
488
+ code: message.code,
489
+ text: message.text,
490
+ unmodifiedText: message.unmodifiedText,
491
+ modifiers: message.modifiers ?? 0,
492
+ windowsVirtualKeyCode,
493
+ nativeVirtualKeyCode: message.nativeVirtualKeyCode ?? windowsVirtualKeyCode,
494
+ location: message.location ?? 0,
495
+ autoRepeat: message.autoRepeat ?? false,
496
+ isKeypad: message.isKeypad ?? false
497
+ });
498
+ }
499
+ case "clipboard.copy": return sendShortcut(sendInputEvent, message.targetId, "KeyC");
500
+ case "clipboard.cut": return sendShortcut(sendInputEvent, message.targetId, "KeyX");
501
+ case "clipboard.paste": {
502
+ const item = message.items.find((candidate) => candidate.mimeType === "text/plain");
503
+ return item ? sendInputEvent({
504
+ type: "input.keyboard.text",
505
+ targetId: message.targetId,
506
+ text: item.data
507
+ }) : false;
508
+ }
509
+ default: return message;
510
+ }
511
+ }, [role, sendInputEvent]);
512
+ const hasControl = holderClientId === clientId && clientId !== "";
513
+ const followingClientId = participants.find((participant) => participant.clientId === clientId)?.followingClientId ?? null;
514
+ useFork(() => hasControl ? Effect.sleep(heartbeatIntervalMs).pipe(Effect.andThen(Effect.sync(() => {
515
+ if (holderClientIdRef.current === clientIdRef.current) sendReliable({ type: "input.heartbeat" });
516
+ })), Effect.forever) : void 0, [hasControl, sendReliable]);
517
+ return {
518
+ phase,
519
+ targets,
520
+ activeTargetId,
521
+ frames,
522
+ mediaStream,
523
+ mediaSize,
524
+ transport,
525
+ presentation,
526
+ mediaSwitching: targetSwitching || presentationSwitching,
527
+ recordings,
528
+ sendBrowserInput,
529
+ selectTarget,
530
+ requestSelectTarget,
531
+ command,
532
+ request,
533
+ selectPresentation,
534
+ reconnect: () => {
535
+ const connection = connectionRef.current;
536
+ if (connection) runtime.runSync(connection.reconnect);
537
+ },
538
+ collaboration: {
539
+ phase: phase === "error" ? "disconnected" : phase,
540
+ role,
541
+ clientId,
542
+ holderClientId,
543
+ leaseMode,
544
+ hasControl,
545
+ canRequestControl: role !== "viewer" && phase === "connected",
546
+ lastError,
547
+ participants,
548
+ cursors,
549
+ paintEvents,
550
+ followingClientId,
551
+ claim,
552
+ release,
553
+ setActiveTarget,
554
+ follow,
555
+ sendCursor,
556
+ sendPaintPoint,
557
+ clearCursor,
558
+ sendInput: sendBrowserInput
559
+ }
560
+ };
561
+ }
562
+ const StoredIdentity = Schema.Struct({
563
+ name: Schema.String.check(Schema.isLengthBetween(1, 48)),
564
+ avatarHash: Schema.String.check(Schema.isPattern(/^[0-9a-f]{32}$/))
565
+ });
566
+ const decodeStoredIdentity = Schema.decodeUnknownOption(Schema.fromJsonString(StoredIdentity), strictParseOptions);
567
+ function collaborationIdentity(role, accountName) {
568
+ const anonymous = loadAnonymousIdentity();
569
+ if (role !== "owner" || !accountName?.trim()) return anonymous;
570
+ return {
571
+ ...anonymous,
572
+ name: Array.from(accountName.trim()).slice(0, 48).join("")
573
+ };
574
+ }
575
+ function loadAnonymousIdentity() {
576
+ const storageKey = "aperture.collaboration.identity";
577
+ try {
578
+ const stored = window.localStorage.getItem(storageKey);
579
+ if (stored) {
580
+ const parsed = decodeStoredIdentity(stored);
581
+ if (Option.isSome(parsed)) return parsed.value;
582
+ }
583
+ const created = createAnonymousIdentity();
584
+ window.localStorage.setItem(storageKey, JSON.stringify(created));
585
+ return created;
586
+ } catch {
587
+ return createAnonymousIdentity();
588
+ }
589
+ }
590
+ function createAnonymousIdentity() {
591
+ const adjectives = [
592
+ "Amber",
593
+ "Brisk",
594
+ "Calm",
595
+ "Clever",
596
+ "Misty",
597
+ "Quiet",
598
+ "Swift",
599
+ "Warm"
600
+ ];
601
+ const animals = [
602
+ "Badger",
603
+ "Falcon",
604
+ "Fox",
605
+ "Koala",
606
+ "Otter",
607
+ "Panda",
608
+ "Raven",
609
+ "Tiger"
610
+ ];
611
+ const random = /* @__PURE__ */ new Uint8Array(18);
612
+ crypto.getRandomValues(random);
613
+ return {
614
+ name: `${adjectives[(random.at(0) ?? 0) % adjectives.length] ?? "Quiet"} ${animals[(random.at(1) ?? 0) % animals.length] ?? "Otter"}`,
615
+ avatarHash: Array.from(random.slice(2), (value) => value.toString(16).padStart(2, "0")).join("")
616
+ };
617
+ }
618
+ function resolveMediaSize(targets, activeTargetId) {
619
+ const viewport = targets.find((target) => target.id === activeTargetId)?.viewport;
620
+ return viewport ? {
621
+ width: viewport.width,
622
+ height: viewport.height,
623
+ canvasWidth: viewport.canvasWidth,
624
+ canvasHeight: viewport.canvasHeight,
625
+ deviceScaleFactor: viewport.deviceScaleFactor
626
+ } : null;
627
+ }
628
+ function normalizedCoordinate(value, length) {
629
+ if (length <= 1) return 0;
630
+ return Math.min(1, Math.max(0, value / length));
631
+ }
632
+ function sendShortcut(sendInputEvent, targetId, code) {
633
+ const controlKeycode = evdevKeycodeByCode.ControlLeft;
634
+ const shortcutKeycode = evdevKeycodeByCode[code];
635
+ if (!controlKeycode || !shortcutKeycode) return false;
636
+ const controlVirtualKeyCode = windowsVirtualKeyCodeForCodeOrKey("ControlLeft", "Control");
637
+ const shortcutKey = code === "KeyC" ? "c" : "x";
638
+ const shortcutVirtualKeyCode = windowsVirtualKeyCodeForCodeOrKey(code, shortcutKey);
639
+ sendInputEvent({
640
+ type: "input.keyboard.key",
641
+ targetId,
642
+ keycode: controlKeycode,
643
+ pressed: true,
644
+ key: "Control",
645
+ code: "ControlLeft",
646
+ windowsVirtualKeyCode: controlVirtualKeyCode,
647
+ nativeVirtualKeyCode: controlVirtualKeyCode,
648
+ modifiers: 2
649
+ });
650
+ sendInputEvent({
651
+ type: "input.keyboard.key",
652
+ targetId,
653
+ keycode: shortcutKeycode,
654
+ pressed: true,
655
+ key: shortcutKey,
656
+ code,
657
+ windowsVirtualKeyCode: shortcutVirtualKeyCode,
658
+ nativeVirtualKeyCode: shortcutVirtualKeyCode,
659
+ modifiers: 2
660
+ });
661
+ sendInputEvent({
662
+ type: "input.keyboard.key",
663
+ targetId,
664
+ keycode: shortcutKeycode,
665
+ pressed: false,
666
+ key: shortcutKey,
667
+ code,
668
+ windowsVirtualKeyCode: shortcutVirtualKeyCode,
669
+ nativeVirtualKeyCode: shortcutVirtualKeyCode,
670
+ modifiers: 2
671
+ });
672
+ return sendInputEvent({
673
+ type: "input.keyboard.key",
674
+ targetId,
675
+ keycode: controlKeycode,
676
+ pressed: false,
677
+ key: "Control",
678
+ code: "ControlLeft",
679
+ windowsVirtualKeyCode: controlVirtualKeyCode,
680
+ nativeVirtualKeyCode: controlVirtualKeyCode,
681
+ modifiers: 0
682
+ });
683
+ }
684
+ //#endregion
685
+ //#region src/components/collaboration-paint-overlay.tsx
686
+ const paintColors = [
687
+ "#f43f5e",
688
+ "#f97316",
689
+ "#eab308",
690
+ "#22c55e",
691
+ "#06b6d4",
692
+ "#8b5cf6"
693
+ ];
694
+ const paintWidth = 4;
695
+ const paintSendIntervalMs = 24;
696
+ const maximumPaintPoints = 2048;
697
+ const maximumPaintStrokes = 512;
698
+ function CollaborationPaintOverlay({ collaboration, targetId, enabled, visible, left, top, width, height }) {
699
+ const canvasRef = useRef(null);
700
+ const activeStrokeIdRef = useRef(null);
701
+ const activePointerIdRef = useRef(null);
702
+ const lastPointSentAtRef = useRef(0);
703
+ const strokesRef = useRef(/* @__PURE__ */ new Map());
704
+ const color = colorForClient(collaboration.clientId);
705
+ useFork(() => {
706
+ const canvas = canvasRef.current;
707
+ if (!canvas) return;
708
+ const pixelRatio = window.devicePixelRatio || 1;
709
+ canvas.width = Math.max(1, Math.round(width * pixelRatio));
710
+ canvas.height = Math.max(1, Math.round(height * pixelRatio));
711
+ const context = canvas.getContext("2d");
712
+ if (!context) return;
713
+ let animationFrame = null;
714
+ const participantIds = new Set(collaboration.participants.map((participant) => participant.clientId));
715
+ const disconnectedAt = Date.now();
716
+ for (const [key, stroke] of strokesRef.current) if (!stroke.ended && !participantIds.has(stroke.clientId)) strokesRef.current.set(key, {
717
+ ...stroke,
718
+ updatedAt: disconnectedAt,
719
+ ended: true
720
+ });
721
+ const draw = () => {
722
+ animationFrame = null;
723
+ context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
724
+ context.clearRect(0, 0, width, height);
725
+ const now = Date.now();
726
+ let fadingStrokeCount = 0;
727
+ for (const [key, stroke] of strokesRef.current) {
728
+ const age = now - stroke.updatedAt;
729
+ if (stroke.ended && age >= 7e3) {
730
+ strokesRef.current.delete(key);
731
+ continue;
732
+ }
733
+ if (stroke.ended) fadingStrokeCount += 1;
734
+ if (stroke.targetId !== targetId || stroke.points.length === 0) continue;
735
+ const opacity = stroke.ended ? 1 - age / collaborationPaintLifetimeMs : 1;
736
+ context.globalAlpha = opacity;
737
+ context.strokeStyle = stroke.color;
738
+ context.fillStyle = stroke.color;
739
+ context.lineWidth = stroke.width;
740
+ context.lineCap = "round";
741
+ context.lineJoin = "round";
742
+ const first = stroke.points[0];
743
+ if (!first) continue;
744
+ if (stroke.points.length === 1) {
745
+ context.beginPath();
746
+ context.arc(first.x * width, first.y * height, stroke.width / 2, 0, Math.PI * 2);
747
+ context.fill();
748
+ continue;
749
+ }
750
+ context.beginPath();
751
+ context.moveTo(first.x * width, first.y * height);
752
+ for (let index = 1; index < stroke.points.length - 1; index += 1) {
753
+ const point = stroke.points[index];
754
+ const next = stroke.points[index + 1];
755
+ if (!point || !next) continue;
756
+ context.quadraticCurveTo(point.x * width, point.y * height, (point.x + next.x) / 2 * width, (point.y + next.y) / 2 * height);
757
+ }
758
+ const last = stroke.points[stroke.points.length - 1];
759
+ if (last) context.lineTo(last.x * width, last.y * height);
760
+ context.stroke();
761
+ }
762
+ context.globalAlpha = 1;
763
+ if (fadingStrokeCount > 0) animationFrame = window.requestAnimationFrame(draw);
764
+ };
765
+ const requestDraw = () => {
766
+ if (animationFrame === null) animationFrame = window.requestAnimationFrame(draw);
767
+ };
768
+ const onPaint = (event) => {
769
+ if (event.type === "clear") {
770
+ strokesRef.current.clear();
771
+ requestDraw();
772
+ return;
773
+ }
774
+ const message = event.message;
775
+ const key = `${message.clientId}:${message.targetId}:${message.strokeId}`;
776
+ const existing = strokesRef.current.get(key);
777
+ if (message.phase !== "start" && !existing || existing?.ended && message.phase !== "start") return;
778
+ const updatedAt = Date.now();
779
+ if (message.phase === "start") {
780
+ for (const [candidateKey, stroke] of strokesRef.current) if (!stroke.ended && stroke.clientId === message.clientId) strokesRef.current.set(candidateKey, {
781
+ ...stroke,
782
+ updatedAt,
783
+ ended: true
784
+ });
785
+ }
786
+ let points = message.phase === "start" || !existing ? [{
787
+ x: message.x,
788
+ y: message.y
789
+ }] : [...existing.points, {
790
+ x: message.x,
791
+ y: message.y
792
+ }];
793
+ if (points.length > maximumPaintPoints) points = points.filter((_, index) => index % 2 === 0);
794
+ if (!existing && strokesRef.current.size >= maximumPaintStrokes) {
795
+ let oldestKey = null;
796
+ let oldestUpdatedAt = Number.POSITIVE_INFINITY;
797
+ for (const [candidateKey, stroke] of strokesRef.current) {
798
+ if (!stroke.ended) continue;
799
+ if (stroke.updatedAt < oldestUpdatedAt) {
800
+ oldestKey = candidateKey;
801
+ oldestUpdatedAt = stroke.updatedAt;
802
+ }
803
+ }
804
+ if (oldestKey) strokesRef.current.delete(oldestKey);
805
+ }
806
+ strokesRef.current.set(key, {
807
+ clientId: message.clientId,
808
+ targetId: message.targetId,
809
+ color: message.color,
810
+ width: message.width,
811
+ points,
812
+ updatedAt,
813
+ ended: message.phase === "end"
814
+ });
815
+ requestDraw();
816
+ };
817
+ draw();
818
+ return Stream.runForEach(collaboration.paintEvents, (event) => Effect.sync(() => onPaint(event))).pipe(Effect.ensuring(Effect.sync(() => {
819
+ if (animationFrame !== null) window.cancelAnimationFrame(animationFrame);
820
+ })));
821
+ }, [
822
+ collaboration.paintEvents,
823
+ collaboration.participants,
824
+ height,
825
+ targetId,
826
+ width
827
+ ]);
828
+ useEffect(() => {
829
+ activeStrokeIdRef.current = null;
830
+ activePointerIdRef.current = null;
831
+ }, [enabled, targetId]);
832
+ function sendPoint(event, phase) {
833
+ const strokeId = activeStrokeIdRef.current;
834
+ const point = normalizedPointer(event);
835
+ if (!strokeId || !point) return;
836
+ collaboration.sendPaintPoint({
837
+ targetId,
838
+ strokeId,
839
+ color,
840
+ width: paintWidth,
841
+ phase,
842
+ x: point.x,
843
+ y: point.y
844
+ });
845
+ }
846
+ function handlePointerDown(event) {
847
+ stopPointerEvent(event);
848
+ if (!event.isPrimary || event.button !== 0 || activePointerIdRef.current !== null) return;
849
+ event.currentTarget.focus();
850
+ activeStrokeIdRef.current = crypto.randomUUID();
851
+ activePointerIdRef.current = event.pointerId;
852
+ lastPointSentAtRef.current = performance.now();
853
+ try {
854
+ event.currentTarget.setPointerCapture(event.pointerId);
855
+ } catch {}
856
+ const point = normalizedPointer(event);
857
+ if (point) collaboration.sendCursor(targetId, point.x * width, point.y * height, {
858
+ width,
859
+ height
860
+ });
861
+ sendPoint(event, "start");
862
+ }
863
+ function handlePointerMove(event) {
864
+ stopPointerEvent(event);
865
+ if (activePointerIdRef.current !== null && event.pointerId !== activePointerIdRef.current) return;
866
+ const point = normalizedPointer(event);
867
+ if (point) collaboration.sendCursor(targetId, point.x * width, point.y * height, {
868
+ width,
869
+ height
870
+ });
871
+ if (!activeStrokeIdRef.current || performance.now() - lastPointSentAtRef.current < paintSendIntervalMs) return;
872
+ lastPointSentAtRef.current = performance.now();
873
+ sendPoint(event, "move");
874
+ }
875
+ function finishStroke(event) {
876
+ stopPointerEvent(event);
877
+ if (!activeStrokeIdRef.current || activePointerIdRef.current !== event.pointerId) return;
878
+ sendPoint(event, "end");
879
+ activeStrokeIdRef.current = null;
880
+ activePointerIdRef.current = null;
881
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
882
+ }
883
+ return /* @__PURE__ */ jsx("canvas", {
884
+ ref: canvasRef,
885
+ tabIndex: -1,
886
+ style: {
887
+ position: "absolute",
888
+ zIndex: 20,
889
+ touchAction: "none",
890
+ cursor: enabled ? "crosshair" : void 0,
891
+ pointerEvents: enabled ? void 0 : "none",
892
+ visibility: visible ? void 0 : "hidden",
893
+ left,
894
+ top,
895
+ width,
896
+ height
897
+ },
898
+ "aria-label": "Shared drawing overlay",
899
+ onPointerDown: handlePointerDown,
900
+ onPointerMove: handlePointerMove,
901
+ onPointerUp: finishStroke,
902
+ onPointerCancel: finishStroke,
903
+ onWheel: stopPointerEvent,
904
+ onContextMenu: stopPointerEvent
905
+ });
906
+ }
907
+ function normalizedPointer(event) {
908
+ const rect = event.currentTarget.getBoundingClientRect();
909
+ if (rect.width === 0 || rect.height === 0) return null;
910
+ return {
911
+ x: Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)),
912
+ y: Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height))
913
+ };
914
+ }
915
+ function stopPointerEvent(event) {
916
+ event.preventDefault();
917
+ event.stopPropagation();
918
+ }
919
+ function colorForClient(clientId) {
920
+ let hash = 0;
921
+ for (const character of clientId) hash = hash * 31 + character.charCodeAt(0) >>> 0;
922
+ return paintColors[hash % paintColors.length] ?? "#f43f5e";
923
+ }
924
+ //#endregion
925
+ //#region src/components/session-viewport.tsx
926
+ const MULTI_CLICK_MS = 500;
927
+ const MULTI_CLICK_DISTANCE = 5;
928
+ function SessionViewport({ control, viewport = control.viewport, localCursorEnabled = true, paintingEnabled = false, onPaintingEnabledChange, onNotice, className, style, renderOverlay }) {
929
+ const containerRef = useRef(null);
930
+ const imageRef = useRef(null);
931
+ const videoRef = useRef(null);
932
+ const rasterDecoderRef = useRef({
933
+ decodingFrame: null,
934
+ generation: 0,
935
+ pending: null
936
+ });
937
+ const frameMetadataRef = useRef(null);
938
+ const pressedKeysRef = useRef(/* @__PURE__ */ new Map());
939
+ const releasedKeysRef = useRef(/* @__PURE__ */ new Set());
940
+ const pointerCaptureRef = useRef(null);
941
+ const dragCleanupRef = useRef(null);
942
+ const implicitControlDesiredRef = useRef(false);
943
+ const cursorHintPointRef = useRef(null);
944
+ const lastClickRef = useRef(null);
945
+ const [cursorHintPoint, setCursorHintPoint] = useState(null);
946
+ const [frameMetadata, setFrameMetadata] = useState(null);
947
+ const [presentedMedia, setPresentedMedia] = useState(null);
948
+ const showingWebRTC = control.mediaPath === "webrtc-live" && control.mediaPhase === "live" && Boolean(control.mediaStream) && Boolean(control.activeTargetId);
949
+ const mediaTransitioning = showingWebRTC && (control.mediaSwitching || control.mediaTargetId !== control.activeTargetId || presentedMedia?.stream !== control.mediaStream || presentedMedia?.targetId !== control.activeTargetId);
950
+ const paintOverlayEnabled = paintingEnabled && (showingWebRTC || frameMetadata !== null) && control.collaboration.phase === "connected" && !control.mediaSwitching && !mediaTransitioning;
951
+ const inputDisabled = paintingEnabled || control.mediaSwitching || mediaTransitioning || control.collaboration.phase !== "connected" || control.collaboration.role === "viewer";
952
+ const displayedMediaSize = mediaTransitioning && presentedMedia ? presentedMedia.size : control.mediaSize;
953
+ const renderWidth = showingWebRTC ? displayedMediaSize?.width ?? viewport.width : frameMetadata?.width ?? viewport.width;
954
+ const renderHeight = showingWebRTC ? displayedMediaSize?.height ?? viewport.height : frameMetadata?.height ?? viewport.height;
955
+ const inputWidth = showingWebRTC ? renderWidth : viewport.width;
956
+ const inputHeight = showingWebRTC ? renderHeight : viewport.height;
957
+ const contentWidth = showingWebRTC ? Math.min(displayedMediaSize?.canvasWidth ?? renderWidth, Math.round(renderWidth * (displayedMediaSize?.deviceScaleFactor ?? 1))) : renderWidth;
958
+ const contentHeight = showingWebRTC ? Math.min(displayedMediaSize?.canvasHeight ?? renderHeight, Math.round(renderHeight * (displayedMediaSize?.deviceScaleFactor ?? 1))) : renderHeight;
959
+ const displayMetrics = computeRenderMetrics(control.browserViewportSize?.width ?? renderWidth, control.browserViewportSize?.height ?? renderHeight, renderWidth, renderHeight);
960
+ const disconnectedHint = resolveDisconnectedHint(control.phase);
961
+ const collaborationHint = resolveCollaborationHint(control.collaboration);
962
+ const cursorHint = disconnectedHint ?? collaborationHint;
963
+ const displayedCursorHintPoint = cursorHint ? cursorHintPointRef.current ?? cursorHintPoint : null;
964
+ const followedParticipant = control.collaboration.followingClientId ? control.collaboration.participants.find((participant) => participant.clientId === control.collaboration.followingClientId) : null;
965
+ const followedCursor = followedParticipant ? control.collaboration.cursors.get(followedParticipant.clientId) : null;
966
+ const followedCursorPoint = followedCursor?.targetId === control.activeTargetId ? {
967
+ x: displayMetrics.offsetX + followedCursor.x * displayMetrics.renderedWidth,
968
+ y: displayMetrics.offsetY + followedCursor.y * displayMetrics.renderedHeight
969
+ } : null;
970
+ useEffect(() => {
971
+ control.setInputDimensions({
972
+ width: inputWidth,
973
+ height: inputHeight
974
+ });
975
+ }, [
976
+ control.setInputDimensions,
977
+ inputHeight,
978
+ inputWidth
979
+ ]);
980
+ const releasePressedKeys = useCallback(() => {
981
+ const pressedKeys = [...pressedKeysRef.current.entries()].reverse();
982
+ pressedKeysRef.current.clear();
983
+ for (const [keyId, pressedKey] of pressedKeys) if (control.sendInput({
984
+ type: "input.key",
985
+ targetId: pressedKey.targetId,
986
+ action: "up",
987
+ ...pressedKey.input,
988
+ text: void 0,
989
+ modifiers: 0,
990
+ autoRepeat: false
991
+ })) releasedKeysRef.current.add(keyId);
992
+ }, [control.sendInput]);
993
+ useFork(() => {
994
+ const onFrame = (frame) => {
995
+ if (!frame) {
996
+ if (imageRef.current) resetImageFrames(imageRef.current, rasterDecoderRef.current);
997
+ else resetRasterFrameDecoder(rasterDecoderRef.current);
998
+ if (frameMetadataRef.current !== null) {
999
+ frameMetadataRef.current = null;
1000
+ setFrameMetadata(null);
1001
+ }
1002
+ return;
1003
+ }
1004
+ if (frame.targetId !== control.activeTargetId) return;
1005
+ if (imageRef.current && !showingWebRTC) enqueueImageFrame(imageRef.current, rasterDecoderRef.current, frame);
1006
+ else rasterDecoderRef.current.pending = frame;
1007
+ const currentMetadata = frameMetadataRef.current;
1008
+ if (currentMetadata?.width !== frame.width || currentMetadata?.height !== frame.height) {
1009
+ const nextMetadata = {
1010
+ width: frame.width,
1011
+ height: frame.height
1012
+ };
1013
+ frameMetadataRef.current = nextMetadata;
1014
+ setFrameMetadata(nextMetadata);
1015
+ }
1016
+ };
1017
+ return Stream.runForEach(control.frames, (frame) => Effect.sync(() => onFrame(frame))).pipe(Effect.ensuring(Effect.sync(() => {
1018
+ if (imageRef.current) resetImageFrames(imageRef.current, rasterDecoderRef.current);
1019
+ else resetRasterFrameDecoder(rasterDecoderRef.current);
1020
+ })));
1021
+ }, [
1022
+ control.activeTargetId,
1023
+ control.frames,
1024
+ showingWebRTC
1025
+ ]);
1026
+ useLayoutEffect(() => {
1027
+ const decoder = rasterDecoderRef.current;
1028
+ const targetId = control.activeTargetId;
1029
+ const targetFrame = decoder.pending?.targetId === targetId ? decoder.pending : decoder.decodingFrame?.targetId === targetId ? decoder.decodingFrame : null;
1030
+ const image = imageRef.current;
1031
+ if (image) resetImageFrames(image, decoder);
1032
+ else resetRasterFrameDecoder(decoder);
1033
+ if (targetFrame) {
1034
+ decoder.pending = targetFrame;
1035
+ const nextMetadata = {
1036
+ width: targetFrame.width,
1037
+ height: targetFrame.height
1038
+ };
1039
+ frameMetadataRef.current = nextMetadata;
1040
+ setFrameMetadata(nextMetadata);
1041
+ if (image) decodeNextImageFrame(image, decoder);
1042
+ } else if (frameMetadataRef.current !== null) {
1043
+ frameMetadataRef.current = null;
1044
+ setFrameMetadata(null);
1045
+ }
1046
+ }, [control.activeTargetId]);
1047
+ useEffect(() => {
1048
+ const image = imageRef.current;
1049
+ if (showingWebRTC || !image) return;
1050
+ decodeNextImageFrame(image, rasterDecoderRef.current);
1051
+ }, [frameMetadata, showingWebRTC]);
1052
+ useEffect(() => {
1053
+ if (!videoRef.current) return;
1054
+ videoRef.current.srcObject = showingWebRTC ? control.mediaStream : null;
1055
+ }, [control.mediaStream, showingWebRTC]);
1056
+ useEffect(() => {
1057
+ const video = videoRef.current;
1058
+ const stream = control.mediaStream;
1059
+ const targetId = control.activeTargetId;
1060
+ const size = control.mediaSize;
1061
+ if (!showingWebRTC || !video || !stream || !targetId || !size || control.mediaSwitching || control.mediaTargetId !== targetId || presentedMedia?.stream === stream && presentedMedia.targetId === targetId) return;
1062
+ const callback = video.requestVideoFrameCallback(() => {
1063
+ setPresentedMedia({
1064
+ stream,
1065
+ targetId,
1066
+ size
1067
+ });
1068
+ });
1069
+ return () => video.cancelVideoFrameCallback(callback);
1070
+ }, [
1071
+ control.activeTargetId,
1072
+ control.mediaSize,
1073
+ control.mediaStream,
1074
+ control.mediaSwitching,
1075
+ control.mediaTargetId,
1076
+ presentedMedia,
1077
+ showingWebRTC
1078
+ ]);
1079
+ useEffect(() => {
1080
+ const element = containerRef.current;
1081
+ if (!element) return;
1082
+ const syncSize = (width, height) => {
1083
+ control.setBrowserViewportSize({
1084
+ width,
1085
+ height
1086
+ });
1087
+ };
1088
+ const rect = element.getBoundingClientRect();
1089
+ syncSize(rect.width, rect.height);
1090
+ const observer = new ResizeObserver((entries) => {
1091
+ const entry = entries[0];
1092
+ if (entry) syncSize(entry.contentRect.width, entry.contentRect.height);
1093
+ });
1094
+ observer.observe(element);
1095
+ return () => observer.disconnect();
1096
+ }, [control.setBrowserViewportSize]);
1097
+ useEffect(() => {
1098
+ return () => {
1099
+ dragCleanupRef.current?.();
1100
+ releasePressedKeys();
1101
+ };
1102
+ }, [releasePressedKeys]);
1103
+ useEffect(() => {
1104
+ const handleWindowBlur = () => {
1105
+ releasePressedKeys();
1106
+ releaseImplicitControl();
1107
+ };
1108
+ const handleVisibilityChange = () => {
1109
+ if (document.visibilityState === "hidden") {
1110
+ releasePressedKeys();
1111
+ releaseImplicitControl();
1112
+ }
1113
+ };
1114
+ window.addEventListener("blur", handleWindowBlur);
1115
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1116
+ return () => {
1117
+ window.removeEventListener("blur", handleWindowBlur);
1118
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1119
+ };
1120
+ }, [releasePressedKeys, control.collaboration]);
1121
+ useEffect(() => {
1122
+ if (inputDisabled) {
1123
+ dragCleanupRef.current?.();
1124
+ releasePressedKeys();
1125
+ releaseImplicitControl();
1126
+ }
1127
+ }, [inputDisabled, releasePressedKeys]);
1128
+ useEffect(() => {
1129
+ if (paintingEnabled) {
1130
+ control.setCaptured(false);
1131
+ implicitControlDesiredRef.current = false;
1132
+ }
1133
+ if (control.collaboration.hasControl && control.collaboration.leaseMode === "implicit" && !implicitControlDesiredRef.current) control.collaboration.release();
1134
+ }, [
1135
+ control.collaboration.hasControl,
1136
+ control.collaboration.leaseMode,
1137
+ control.collaboration.release,
1138
+ control.setCaptured,
1139
+ paintingEnabled
1140
+ ]);
1141
+ function mapPointer(event, clamp) {
1142
+ const rect = containerRef.current?.getBoundingClientRect();
1143
+ if (!rect) return null;
1144
+ const metrics = computeRenderMetrics(rect.width, rect.height, renderWidth, renderHeight);
1145
+ const localX = event.clientX - rect.left - metrics.offsetX;
1146
+ const localY = event.clientY - rect.top - metrics.offsetY;
1147
+ if (!clamp && (localX < 0 || localY < 0 || localX > metrics.renderedWidth || localY > metrics.renderedHeight)) return null;
1148
+ const x = localX / metrics.scale * (inputWidth / renderWidth);
1149
+ const y = localY / metrics.scale * (inputHeight / renderHeight);
1150
+ if (clamp) return {
1151
+ x: Math.round(clampNumber(x, 0, Math.max(inputWidth - 1, 0))),
1152
+ y: Math.round(clampNumber(y, 0, Math.max(inputHeight - 1, 0)))
1153
+ };
1154
+ return {
1155
+ x: Math.round(x),
1156
+ y: Math.round(y)
1157
+ };
1158
+ }
1159
+ function resolveInputTarget() {
1160
+ if (control.phase !== "connected" || !control.activeTargetId || inputDisabled) return null;
1161
+ if (!control.captured) control.setCaptured(true);
1162
+ requestImplicitControl(control.activeTargetId);
1163
+ containerRef.current?.focus();
1164
+ return control.activeTargetId;
1165
+ }
1166
+ function requestImplicitControl(targetId) {
1167
+ implicitControlDesiredRef.current = true;
1168
+ if (!control.collaboration.hasControl) control.collaboration.claim(targetId, "implicit");
1169
+ }
1170
+ function releaseImplicitControl() {
1171
+ implicitControlDesiredRef.current = false;
1172
+ if (control.collaboration.hasControl && control.collaboration.leaseMode === "implicit") control.collaboration.release();
1173
+ }
1174
+ function preventViewportDefault(event) {
1175
+ event.preventDefault();
1176
+ event.stopPropagation();
1177
+ }
1178
+ function preventNativeDefault(event) {
1179
+ if (event.cancelable) event.preventDefault();
1180
+ event.stopPropagation();
1181
+ }
1182
+ function handleCaptureClick() {
1183
+ if (control.phase !== "connected" || inputDisabled) return;
1184
+ control.setCaptured(true);
1185
+ if (control.activeTargetId) requestImplicitControl(control.activeTargetId);
1186
+ containerRef.current?.focus();
1187
+ }
1188
+ function handlePointerEnter(event) {
1189
+ updateCursorHint(event);
1190
+ if (inputDisabled || !control.activeTargetId) return;
1191
+ control.setCaptured(true);
1192
+ requestImplicitControl(control.activeTargetId);
1193
+ }
1194
+ function handlePointerLeave() {
1195
+ cursorHintPointRef.current = null;
1196
+ setCursorHintPoint(null);
1197
+ control.collaboration.clearCursor();
1198
+ if (!pointerCaptureRef.current) {
1199
+ releasePressedKeys();
1200
+ releaseImplicitControl();
1201
+ control.setCaptured(false);
1202
+ }
1203
+ }
1204
+ function handlePointerClick() {
1205
+ if (!control.captured) handleCaptureClick();
1206
+ }
1207
+ function handlePointerMove(event) {
1208
+ updateCursorHint(event);
1209
+ const point = mapPointer(event, false);
1210
+ if (control.activeTargetId && point) control.collaboration.sendCursor(control.activeTargetId, point.x, point.y, {
1211
+ width: inputWidth,
1212
+ height: inputHeight
1213
+ });
1214
+ else if (!point) control.collaboration.clearCursor();
1215
+ if (inputDisabled) return;
1216
+ if (pointerCaptureRef.current?.pointerId === event.pointerId) return;
1217
+ const targetId = control.captured && event.buttons === 0 ? control.activeTargetId : null;
1218
+ if (!targetId) return;
1219
+ if (!point) return;
1220
+ preventViewportDefault(event);
1221
+ control.sendInput({
1222
+ type: "input.mouse",
1223
+ targetId,
1224
+ action: "move",
1225
+ x: point.x,
1226
+ y: point.y,
1227
+ button: "none",
1228
+ buttons: 0,
1229
+ clickCount: 0,
1230
+ modifiers: keyboardModifiers(event.nativeEvent)
1231
+ });
1232
+ }
1233
+ function handlePointerDown(event) {
1234
+ updateCursorHint(event);
1235
+ const targetId = resolveInputTarget();
1236
+ if (!targetId) return;
1237
+ const point = mapPointer(event, false);
1238
+ if (!point) return;
1239
+ const element = containerRef.current;
1240
+ if (!element) return;
1241
+ const button = resolveMouseButton(event.button);
1242
+ const now = Date.now();
1243
+ const clickCount = resolveClickCount(targetId, button, point, now, lastClickRef.current);
1244
+ lastClickRef.current = {
1245
+ targetId,
1246
+ button,
1247
+ point,
1248
+ time: now,
1249
+ clickCount
1250
+ };
1251
+ preventViewportDefault(event);
1252
+ pointerCaptureRef.current = {
1253
+ pointerId: event.pointerId,
1254
+ targetId,
1255
+ button,
1256
+ clickCount
1257
+ };
1258
+ dragCleanupRef.current?.();
1259
+ try {
1260
+ event.currentTarget.setPointerCapture(event.pointerId);
1261
+ } catch {}
1262
+ bindDragListeners({
1263
+ pointerId: event.pointerId,
1264
+ targetId,
1265
+ button,
1266
+ clickCount,
1267
+ element
1268
+ });
1269
+ control.sendInput({
1270
+ type: "input.mouse",
1271
+ targetId,
1272
+ action: "down",
1273
+ x: point.x,
1274
+ y: point.y,
1275
+ button,
1276
+ buttons: event.buttons,
1277
+ clickCount,
1278
+ modifiers: keyboardModifiers(event.nativeEvent)
1279
+ });
1280
+ }
1281
+ function handlePointerUp(event) {
1282
+ if (inputDisabled) return;
1283
+ const capturedPointer = pointerCaptureRef.current;
1284
+ const targetId = capturedPointer?.pointerId === event.pointerId ? capturedPointer.targetId : control.activeTargetId;
1285
+ if (!targetId) return;
1286
+ const point = mapPointer(event, capturedPointer?.pointerId === event.pointerId);
1287
+ if (!point) return;
1288
+ preventViewportDefault(event);
1289
+ control.sendInput({
1290
+ type: "input.mouse",
1291
+ targetId,
1292
+ action: "up",
1293
+ x: point.x,
1294
+ y: point.y,
1295
+ button: capturedPointer?.button ?? resolveMouseButton(event.button),
1296
+ buttons: event.buttons,
1297
+ clickCount: capturedPointer?.clickCount ?? 1,
1298
+ modifiers: keyboardModifiers(event.nativeEvent)
1299
+ });
1300
+ if (capturedPointer?.pointerId === event.pointerId) {
1301
+ pointerCaptureRef.current = null;
1302
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
1303
+ }
1304
+ }
1305
+ function handlePointerCancel(event) {
1306
+ const capturedPointer = pointerCaptureRef.current;
1307
+ if (capturedPointer?.pointerId !== event.pointerId) return;
1308
+ const point = mapPointer(event, true);
1309
+ pointerCaptureRef.current = null;
1310
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
1311
+ if (!point || inputDisabled) return;
1312
+ preventViewportDefault(event);
1313
+ control.sendInput({
1314
+ type: "input.mouse",
1315
+ targetId: capturedPointer.targetId,
1316
+ action: "up",
1317
+ x: point.x,
1318
+ y: point.y,
1319
+ button: capturedPointer.button,
1320
+ buttons: 0,
1321
+ clickCount: capturedPointer.clickCount,
1322
+ modifiers: keyboardModifiers(event.nativeEvent)
1323
+ });
1324
+ }
1325
+ function bindDragListeners({ pointerId, targetId, button, clickCount, element }) {
1326
+ const handleMove = (event) => {
1327
+ if (event.pointerId !== pointerId) return;
1328
+ const point = mapPointer(event, true);
1329
+ preventNativeDefault(event);
1330
+ if (!point) return;
1331
+ control.collaboration.sendCursor(targetId, point.x, point.y, {
1332
+ width: inputWidth,
1333
+ height: inputHeight
1334
+ });
1335
+ control.sendInput({
1336
+ type: "input.mouse",
1337
+ targetId,
1338
+ action: "move",
1339
+ x: point.x,
1340
+ y: point.y,
1341
+ button,
1342
+ buttons: event.buttons || mouseButtonsForButton(button),
1343
+ clickCount,
1344
+ modifiers: keyboardModifiers(event)
1345
+ });
1346
+ };
1347
+ const finish = (event, canceled) => {
1348
+ if (event.pointerId !== pointerId) return;
1349
+ const point = mapPointer(event, true);
1350
+ if (!mapPointer(event, false)) control.collaboration.clearCursor();
1351
+ preventNativeDefault(event);
1352
+ if (point) {
1353
+ control.sendInput({
1354
+ type: "input.mouse",
1355
+ targetId,
1356
+ action: "up",
1357
+ x: point.x,
1358
+ y: point.y,
1359
+ button,
1360
+ buttons: 0,
1361
+ clickCount,
1362
+ modifiers: keyboardModifiers(event)
1363
+ });
1364
+ if (!canceled) lastClickRef.current = {
1365
+ targetId,
1366
+ button,
1367
+ point,
1368
+ time: Date.now(),
1369
+ clickCount
1370
+ };
1371
+ }
1372
+ cleanup();
1373
+ };
1374
+ const handleUp = (event) => finish(event, false);
1375
+ const handleCancel = (event) => finish(event, true);
1376
+ const cleanup = () => {
1377
+ window.removeEventListener("pointermove", handleMove, true);
1378
+ window.removeEventListener("pointerup", handleUp, true);
1379
+ window.removeEventListener("pointercancel", handleCancel, true);
1380
+ try {
1381
+ if (element.hasPointerCapture(pointerId)) element.releasePointerCapture(pointerId);
1382
+ } catch {}
1383
+ if (pointerCaptureRef.current?.pointerId === pointerId) pointerCaptureRef.current = null;
1384
+ if (dragCleanupRef.current === cleanup) dragCleanupRef.current = null;
1385
+ };
1386
+ dragCleanupRef.current = cleanup;
1387
+ window.addEventListener("pointermove", handleMove, true);
1388
+ window.addEventListener("pointerup", handleUp, true);
1389
+ window.addEventListener("pointercancel", handleCancel, true);
1390
+ }
1391
+ function handleWheel(event) {
1392
+ const targetId = resolveInputTarget();
1393
+ if (!targetId) return;
1394
+ const point = mapPointer(event, false);
1395
+ if (!point) return;
1396
+ preventViewportDefault(event);
1397
+ const wheelScale = wheelDeltaScale(event.deltaMode, inputHeight);
1398
+ control.sendInput({
1399
+ type: "input.wheel",
1400
+ targetId,
1401
+ x: point.x,
1402
+ y: point.y,
1403
+ deltaX: event.deltaX * wheelScale,
1404
+ deltaY: event.deltaY * wheelScale,
1405
+ modifiers: keyboardModifiers(event.nativeEvent)
1406
+ });
1407
+ }
1408
+ function handleKeyDown(event) {
1409
+ if (event.key === "Escape" && paintingEnabled) {
1410
+ event.preventDefault();
1411
+ event.stopPropagation();
1412
+ onPaintingEnabledChange?.(false);
1413
+ return;
1414
+ }
1415
+ if (event.key === "Escape") {
1416
+ if (control.captured) {
1417
+ event.preventDefault();
1418
+ event.stopPropagation();
1419
+ control.setCaptured(false);
1420
+ releaseImplicitControl();
1421
+ }
1422
+ return;
1423
+ }
1424
+ const keyId = event.code || event.key;
1425
+ if (event.repeat && releasedKeysRef.current.has(keyId)) {
1426
+ event.preventDefault();
1427
+ event.stopPropagation();
1428
+ return;
1429
+ }
1430
+ const targetId = control.activeTargetId;
1431
+ if (inputDisabled || !targetId || !control.captured && focusedElement(containerRef.current) !== containerRef.current && !containerRef.current?.contains(event.target)) return;
1432
+ event.preventDefault();
1433
+ event.stopPropagation();
1434
+ const clipboardShortcut = resolveClipboardShortcut(event.nativeEvent);
1435
+ if (clipboardShortcut === "copy") {
1436
+ control.sendInput({
1437
+ type: "clipboard.copy",
1438
+ targetId
1439
+ });
1440
+ return;
1441
+ }
1442
+ if (clipboardShortcut === "cut") {
1443
+ control.sendInput({
1444
+ type: "clipboard.cut",
1445
+ targetId
1446
+ });
1447
+ return;
1448
+ }
1449
+ if (clipboardShortcut === "paste") {
1450
+ pasteClipboard(control, onNotice);
1451
+ return;
1452
+ }
1453
+ if (!shouldForwardBrowserShortcut(event.nativeEvent)) return;
1454
+ const input = keyboardInputMessage(event.nativeEvent, "down");
1455
+ if (control.sendInput({
1456
+ type: "input.key",
1457
+ targetId,
1458
+ action: "down",
1459
+ ...input
1460
+ })) {
1461
+ releasedKeysRef.current.delete(keyId);
1462
+ pressedKeysRef.current.set(keyId, {
1463
+ targetId,
1464
+ input
1465
+ });
1466
+ }
1467
+ }
1468
+ function handleKeyUp(event) {
1469
+ const keyId = event.code || event.key;
1470
+ if (releasedKeysRef.current.delete(keyId)) {
1471
+ event.preventDefault();
1472
+ event.stopPropagation();
1473
+ return;
1474
+ }
1475
+ const pressedKey = pressedKeysRef.current.get(keyId);
1476
+ const targetId = pressedKey?.targetId ?? control.activeTargetId;
1477
+ const canForwardUntrackedKey = !inputDisabled && (control.captured || focusedElement(containerRef.current) === containerRef.current || Boolean(containerRef.current?.contains(event.target)));
1478
+ if (!targetId || !pressedKey && !canForwardUntrackedKey) return;
1479
+ if (event.key === "Escape") return;
1480
+ event.preventDefault();
1481
+ event.stopPropagation();
1482
+ if (!pressedKey && resolveClipboardShortcut(event.nativeEvent)) return;
1483
+ if (!pressedKey && !shouldForwardBrowserShortcut(event.nativeEvent)) return;
1484
+ if (control.sendInput({
1485
+ type: "input.key",
1486
+ targetId,
1487
+ action: "up",
1488
+ ...keyboardInputMessage(event.nativeEvent, "up")
1489
+ })) pressedKeysRef.current.delete(keyId);
1490
+ }
1491
+ function handleBlur(event) {
1492
+ if (event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget)) return;
1493
+ releasePressedKeys();
1494
+ releaseImplicitControl();
1495
+ control.setCaptured(false);
1496
+ }
1497
+ function updateCursorHint(event) {
1498
+ const rect = containerRef.current?.getBoundingClientRect();
1499
+ if (!rect) return;
1500
+ const point = {
1501
+ x: event.clientX - rect.left,
1502
+ y: event.clientY - rect.top
1503
+ };
1504
+ cursorHintPointRef.current = point;
1505
+ if (cursorHint) setCursorHintPoint(point);
1506
+ }
1507
+ const status = resolveViewportStatus(control.phase, frameMetadata !== null, control.mediaPhase, control.mediaPath, showingWebRTC);
1508
+ const placeholder = resolveViewportPlaceholder(control.phase, control.mediaPhase, control.mediaSwitching);
1509
+ const visibleCollaborationError = collaborationHint || isInputOwnershipError(control.collaboration.lastError) ? null : control.collaboration.lastError;
1510
+ return /* @__PURE__ */ jsxs("div", {
1511
+ ref: containerRef,
1512
+ tabIndex: 0,
1513
+ onKeyDown: handleKeyDown,
1514
+ onKeyUp: handleKeyUp,
1515
+ onBlur: handleBlur,
1516
+ onClick: handlePointerClick,
1517
+ onPointerMove: handlePointerMove,
1518
+ onFocus: () => {
1519
+ if (!inputDisabled && control.activeTargetId) requestImplicitControl(control.activeTargetId);
1520
+ },
1521
+ onPointerEnter: handlePointerEnter,
1522
+ onPointerLeave: handlePointerLeave,
1523
+ onPointerDown: handlePointerDown,
1524
+ onPointerUp: handlePointerUp,
1525
+ onPointerCancel: handlePointerCancel,
1526
+ onWheel: handleWheel,
1527
+ onContextMenu: (event) => {
1528
+ if (control.captured) event.preventDefault();
1529
+ },
1530
+ "aria-busy": mediaTransitioning,
1531
+ "aria-disabled": inputDisabled,
1532
+ className,
1533
+ style: {
1534
+ position: "relative",
1535
+ display: "flex",
1536
+ flex: "1 1 0%",
1537
+ minHeight: 0,
1538
+ alignItems: "center",
1539
+ justifyContent: "center",
1540
+ overflow: "hidden",
1541
+ outline: "none",
1542
+ touchAction: "none",
1543
+ cursor: localCursorEnabled ? void 0 : "none",
1544
+ ...style
1545
+ },
1546
+ children: [
1547
+ showingWebRTC ? /* @__PURE__ */ jsx("div", {
1548
+ style: {
1549
+ position: "relative",
1550
+ overflow: "hidden",
1551
+ background: "black",
1552
+ transition: "opacity 150ms",
1553
+ opacity: mediaTransitioning ? .75 : 1,
1554
+ width: displayMetrics.renderedWidth,
1555
+ height: displayMetrics.renderedHeight
1556
+ },
1557
+ children: /* @__PURE__ */ jsx("video", {
1558
+ ref: videoRef,
1559
+ autoPlay: true,
1560
+ muted: true,
1561
+ playsInline: true,
1562
+ "data-viewport-width": renderWidth,
1563
+ "data-viewport-height": renderHeight,
1564
+ style: {
1565
+ position: "absolute",
1566
+ top: 0,
1567
+ left: 0,
1568
+ maxWidth: "none",
1569
+ objectFit: "fill",
1570
+ width: displayMetrics.renderedWidth * ((displayedMediaSize?.canvasWidth ?? contentWidth) / contentWidth),
1571
+ height: displayMetrics.renderedHeight * ((displayedMediaSize?.canvasHeight ?? contentHeight) / contentHeight)
1572
+ }
1573
+ })
1574
+ }) : frameMetadata ? /* @__PURE__ */ jsx("div", {
1575
+ style: {
1576
+ position: "relative",
1577
+ overflow: "hidden",
1578
+ background: "black",
1579
+ width: displayMetrics.renderedWidth,
1580
+ height: displayMetrics.renderedHeight
1581
+ },
1582
+ children: /* @__PURE__ */ jsx("img", {
1583
+ ref: imageRef,
1584
+ alt: "",
1585
+ draggable: false,
1586
+ style: {
1587
+ position: "absolute",
1588
+ inset: 0,
1589
+ width: "100%",
1590
+ height: "100%",
1591
+ objectFit: "contain"
1592
+ }
1593
+ })
1594
+ }) : null,
1595
+ control.activeTargetId ? /* @__PURE__ */ jsx(CollaborationPaintOverlay, {
1596
+ collaboration: control.collaboration,
1597
+ targetId: control.activeTargetId,
1598
+ enabled: paintOverlayEnabled,
1599
+ visible: !control.mediaSwitching && !mediaTransitioning && (showingWebRTC || frameMetadata !== null),
1600
+ left: displayMetrics.offsetX,
1601
+ top: displayMetrics.offsetY,
1602
+ width: displayMetrics.renderedWidth,
1603
+ height: displayMetrics.renderedHeight
1604
+ }) : null,
1605
+ renderOverlay?.({
1606
+ status,
1607
+ placeholder: showingWebRTC || frameMetadata ? null : placeholder,
1608
+ cursorHint: cursorHint && displayedCursorHintPoint ? {
1609
+ text: cursorHint,
1610
+ ...displayedCursorHintPoint
1611
+ } : null,
1612
+ followedCursor: followedCursorPoint && followedParticipant ? {
1613
+ name: followedParticipant.name,
1614
+ ...followedCursorPoint
1615
+ } : null,
1616
+ collaborationError: visibleCollaborationError?.message ?? null
1617
+ })
1618
+ ]
1619
+ });
1620
+ }
1621
+ function resolveDisconnectedHint(phase) {
1622
+ if (phase === "disconnected" || phase === "error") return "Session disconnected";
1623
+ return null;
1624
+ }
1625
+ function resolveCollaborationHint(collaboration) {
1626
+ if (collaboration.holderClientId !== null && collaboration.holderClientId !== collaboration.clientId) return "Input in use";
1627
+ if (collaboration.hasControl) return null;
1628
+ if (collaboration.lastError?.code === "input_busy" || collaboration.lastError?.code === "input_not_owned") return "Input in use";
1629
+ if (collaboration.lastError?.code === "input_unavailable") return "Input unavailable";
1630
+ return null;
1631
+ }
1632
+ function isInputOwnershipError(error) {
1633
+ return error?.code === "input_busy" || error?.code === "input_not_owned";
1634
+ }
1635
+ function enqueueImageFrame(image, decoder, frame) {
1636
+ decoder.pending = frame;
1637
+ decodeNextImageFrame(image, decoder);
1638
+ }
1639
+ function decodeNextImageFrame(image, decoder) {
1640
+ if (decoder.decodingFrame !== null || decoder.pending === null) return;
1641
+ const frame = decoder.pending;
1642
+ const generation = decoder.generation;
1643
+ decoder.pending = null;
1644
+ decoder.decodingFrame = frame;
1645
+ setImageFrame(image, frame).then(() => {
1646
+ if (decoder.generation !== generation) return;
1647
+ decoder.decodingFrame = null;
1648
+ decodeNextImageFrame(image, decoder);
1649
+ });
1650
+ }
1651
+ async function setImageFrame(image, frame) {
1652
+ const previousSource = image.src;
1653
+ const source = URL.createObjectURL(frame.data);
1654
+ image.src = source;
1655
+ try {
1656
+ await image.decode();
1657
+ } catch {} finally {
1658
+ if (previousSource.startsWith("blob:")) URL.revokeObjectURL(previousSource);
1659
+ if (image.src !== source) URL.revokeObjectURL(source);
1660
+ }
1661
+ }
1662
+ function resetImageFrames(image, decoder) {
1663
+ resetRasterFrameDecoder(decoder);
1664
+ const source = image.src;
1665
+ image.removeAttribute("src");
1666
+ if (source.startsWith("blob:")) URL.revokeObjectURL(source);
1667
+ }
1668
+ function resetRasterFrameDecoder(decoder) {
1669
+ decoder.generation += 1;
1670
+ decoder.decodingFrame = null;
1671
+ decoder.pending = null;
1672
+ }
1673
+ function resolveViewportPlaceholder(phase, mediaPhase, switching) {
1674
+ if (phase === "connecting") return "connecting";
1675
+ if (phase === "connected" && mediaPhase === "connecting") return "connecting-media";
1676
+ if (phase === "connected" && switching) return "switching";
1677
+ if (phase === "disconnected" || phase === "error") return "disconnected";
1678
+ return "waiting";
1679
+ }
1680
+ function resolveClipboardShortcut(event) {
1681
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) return null;
1682
+ switch (event.code) {
1683
+ case "KeyC": return "copy";
1684
+ case "KeyX": return "cut";
1685
+ case "KeyV": return "paste";
1686
+ default: return null;
1687
+ }
1688
+ }
1689
+ function resolveMouseButton(button) {
1690
+ if (button === 0) return "left";
1691
+ if (button === 1) return "middle";
1692
+ if (button === 2) return "right";
1693
+ return "none";
1694
+ }
1695
+ function resolveClickCount(targetId, button, point, time, previous) {
1696
+ if (previous && previous.targetId === targetId && previous.button === button && time - previous.time <= MULTI_CLICK_MS && Math.abs(previous.point.x - point.x) <= MULTI_CLICK_DISTANCE && Math.abs(previous.point.y - point.y) <= MULTI_CLICK_DISTANCE) return Math.min(previous.clickCount + 1, 3);
1697
+ return 1;
1698
+ }
1699
+ function mouseButtonsForButton(button) {
1700
+ switch (button) {
1701
+ case "left": return 1;
1702
+ case "right": return 2;
1703
+ case "middle": return 4;
1704
+ case "none": return 0;
1705
+ default: return button;
1706
+ }
1707
+ }
1708
+ function clampNumber(value, min, max) {
1709
+ return Math.min(Math.max(value, min), max);
1710
+ }
1711
+ function wheelDeltaScale(deltaMode, viewportHeight) {
1712
+ if (deltaMode === WheelEvent.DOM_DELTA_LINE) return 16;
1713
+ if (deltaMode === WheelEvent.DOM_DELTA_PAGE) return viewportHeight;
1714
+ return 1;
1715
+ }
1716
+ async function pasteClipboard(control, onNotice) {
1717
+ if (!control.activeTargetId) return;
1718
+ let text = "";
1719
+ try {
1720
+ text = await navigator.clipboard.readText();
1721
+ } catch (error) {
1722
+ console.warn("Clipboard read failed", error);
1723
+ onNotice?.({
1724
+ level: "error",
1725
+ message: "Clipboard read failed"
1726
+ });
1727
+ return;
1728
+ }
1729
+ if (!text) return;
1730
+ control.sendInput({
1731
+ type: "clipboard.paste",
1732
+ targetId: control.activeTargetId,
1733
+ items: [{
1734
+ mimeType: "text/plain",
1735
+ data: text
1736
+ }]
1737
+ });
1738
+ }
1739
+ function resolveViewportStatus(phase, hasFrame, mediaPhase, mediaPath, showingWebRTC) {
1740
+ if (phase !== "connected") return "offline";
1741
+ if (mediaPhase === "live" && showingWebRTC) return "webrtc";
1742
+ if (!hasFrame) return "offline";
1743
+ return mediaPath === "websocket-live" ? "websocket" : "webrtc";
1744
+ }
1745
+ function focusedElement(node) {
1746
+ const root = node?.getRootNode();
1747
+ return root instanceof Document || root instanceof ShadowRoot ? root.activeElement : null;
1748
+ }
1749
+ //#endregion
1750
+ //#region src/hooks/use-browser-control.ts
1751
+ const emptyIceServers$1 = [];
1752
+ function useBrowserControl({ sessionId, credentials, displayName, sessionToken, collaborationRole = "owner", enabled = true, webrtcProducerSupported = false, webrtcIceServers = emptyIceServers$1, onNotice }) {
1753
+ const runtime = useRuntime();
1754
+ const onNoticeRef = useRef(onNotice);
1755
+ onNoticeRef.current = onNotice;
1756
+ const notify = (level, message) => Effect.sync(() => onNoticeRef.current?.({
1757
+ level,
1758
+ message
1759
+ }));
1760
+ const live = useLiveSession({
1761
+ sessionId,
1762
+ displayName,
1763
+ credentials,
1764
+ sessionToken,
1765
+ role: collaborationRole,
1766
+ enabled: Boolean(enabled && sessionId && credentials),
1767
+ webrtcSupported: webrtcProducerSupported,
1768
+ iceServers: webrtcIceServers
1769
+ });
1770
+ const [targetOrder, setTargetOrder] = useState([]);
1771
+ const targets = useMemo(() => mergeTargetsInCurrentOrder(targetOrder, live.targets), [live.targets, targetOrder]);
1772
+ const liveTargetsRef = useRef(live.targets);
1773
+ liveTargetsRef.current = live.targets;
1774
+ const phaseRef = useRef(live.phase);
1775
+ phaseRef.current = live.phase;
1776
+ const [viewport, setViewportState] = useState(DEFAULT_VIEWPORT);
1777
+ const [browserViewportSize, setBrowserViewportSizeState] = useState(null);
1778
+ const [viewportAutoSync, setViewportAutoSyncState] = useState(false);
1779
+ const [captured, setCaptured] = useState(false);
1780
+ const [recordingBusy, setRecordingBusy] = useState(false);
1781
+ const activeTargetIdRef = useRef(null);
1782
+ const viewportRef = useRef(viewport);
1783
+ const inputDimensionsRef = useRef(DEFAULT_VIEWPORT);
1784
+ const browserViewportSizeRef = useRef(null);
1785
+ const viewportAutoSyncRef = useRef(false);
1786
+ activeTargetIdRef.current = live.activeTargetId;
1787
+ viewportRef.current = viewport;
1788
+ browserViewportSizeRef.current = browserViewportSize;
1789
+ viewportAutoSyncRef.current = viewportAutoSync;
1790
+ useEffect(() => {
1791
+ if (live.phase !== "connected") setCaptured(false);
1792
+ }, [live.phase]);
1793
+ useEffect(() => {
1794
+ if (!live.mediaSize) return;
1795
+ const current = viewportRef.current;
1796
+ if (current.width === live.mediaSize.width && current.height === live.mediaSize.height && current.deviceScaleFactor === live.mediaSize.deviceScaleFactor) return;
1797
+ setViewportState(createBrowserViewport(live.mediaSize, live.mediaSize.deviceScaleFactor));
1798
+ }, [live.mediaSize]);
1799
+ const sendInput = useCallback((message) => {
1800
+ if (!enabled || !sessionId || !credentials) return false;
1801
+ return live.sendBrowserInput(message, inputDimensionsRef.current);
1802
+ }, [
1803
+ credentials,
1804
+ enabled,
1805
+ live,
1806
+ sessionId
1807
+ ]);
1808
+ const activateTarget = useCallback((targetId) => {
1809
+ live.selectTarget(targetId);
1810
+ }, [live]);
1811
+ const reorderTargets = useCallback((sourceTargetId, destinationTargetId, placement) => {
1812
+ if (sourceTargetId === destinationTargetId) return;
1813
+ setTargetOrder((order) => {
1814
+ const current = mergeTargetsInCurrentOrder(order, liveTargetsRef.current).map((target) => target.id);
1815
+ const sourceIndex = current.indexOf(sourceTargetId);
1816
+ const destinationIndex = current.indexOf(destinationTargetId);
1817
+ const source = current[sourceIndex];
1818
+ if (sourceIndex === -1 || destinationIndex === -1 || !source) return order;
1819
+ const next = [...current];
1820
+ next.splice(sourceIndex, 1);
1821
+ const requestedIndex = placement === "after" ? destinationIndex + 1 : destinationIndex;
1822
+ const nextIndex = sourceIndex < requestedIndex ? requestedIndex - 1 : requestedIndex;
1823
+ if (nextIndex === sourceIndex) return order;
1824
+ next.splice(nextIndex, 0, source);
1825
+ return next;
1826
+ });
1827
+ }, []);
1828
+ const createAndSelectTarget = useCallback((url) => runtime.runPromise(live.request("target.create", { url }).pipe(Effect.map((result) => result.targetId ?? null), Effect.catch((error) => notify("error", errorMessage(error, "Tab could not be created")).pipe(Effect.as(null))))), [live, runtime]);
1829
+ const createTarget = useCallback((url = "about:blank") => {
1830
+ createAndSelectTarget(url);
1831
+ }, [createAndSelectTarget]);
1832
+ const duplicateTarget = useCallback((target) => {
1833
+ createAndSelectTarget(target.url || "about:blank").then((targetId) => {
1834
+ if (targetId) reorderTargets(targetId, target.id, "after");
1835
+ });
1836
+ }, [createAndSelectTarget, reorderTargets]);
1837
+ const closeTarget = useCallback((targetId) => {
1838
+ live.command("target.close", { targetId });
1839
+ }, [live]);
1840
+ const navigate = useCallback((url) => {
1841
+ const targetId = activeTargetIdRef.current;
1842
+ if (targetId) live.command("page.navigate", {
1843
+ targetId,
1844
+ url
1845
+ });
1846
+ }, [live]);
1847
+ const reload = useCallback((targetId) => {
1848
+ live.command("page.reload", { targetId });
1849
+ }, [live]);
1850
+ const stopLoading = useCallback(() => {
1851
+ const targetId = activeTargetIdRef.current;
1852
+ if (targetId) live.command("page.stop-loading", { targetId });
1853
+ }, [live]);
1854
+ const historyBack = useCallback(() => {
1855
+ const targetId = activeTargetIdRef.current;
1856
+ if (targetId) live.command("page.history-back", { targetId });
1857
+ }, [live]);
1858
+ const historyForward = useCallback(() => {
1859
+ const targetId = activeTargetIdRef.current;
1860
+ if (targetId) live.command("page.history-forward", { targetId });
1861
+ }, [live]);
1862
+ const commitViewport = useCallback((preset) => {
1863
+ setViewportState(preset);
1864
+ const targetId = activeTargetIdRef.current;
1865
+ if (targetId) live.command("viewport.set", {
1866
+ targetId,
1867
+ width: preset.width,
1868
+ height: preset.height,
1869
+ deviceScaleFactor: preset.deviceScaleFactor
1870
+ });
1871
+ }, [live]);
1872
+ const setViewport = useCallback((preset) => {
1873
+ viewportAutoSyncRef.current = false;
1874
+ setViewportAutoSyncState(false);
1875
+ commitViewport(preset);
1876
+ }, [commitViewport]);
1877
+ const setBrowserViewportSize = useCallback((size) => {
1878
+ const next = {
1879
+ width: Math.round(size.width),
1880
+ height: Math.round(size.height)
1881
+ };
1882
+ if (next.width < 1 || next.height < 1) return;
1883
+ const current = browserViewportSizeRef.current;
1884
+ if (current?.width === next.width && current.height === next.height) return;
1885
+ browserViewportSizeRef.current = next;
1886
+ setBrowserViewportSizeState(next);
1887
+ if (viewportAutoSyncRef.current) commitViewport(createBrowserViewport(next, viewportRef.current.deviceScaleFactor));
1888
+ }, [commitViewport]);
1889
+ const setViewportAutoSync = useCallback((nextEnabled) => {
1890
+ viewportAutoSyncRef.current = nextEnabled;
1891
+ setViewportAutoSyncState(nextEnabled);
1892
+ const size = browserViewportSizeRef.current;
1893
+ if (nextEnabled && size) commitViewport(createBrowserViewport(size, viewportRef.current.deviceScaleFactor));
1894
+ }, [commitViewport]);
1895
+ const setViewportToBrowserSize = useCallback(() => {
1896
+ const size = browserViewportSizeRef.current;
1897
+ if (!size) return;
1898
+ viewportAutoSyncRef.current = false;
1899
+ setViewportAutoSyncState(false);
1900
+ commitViewport(createBrowserViewport(size, viewportRef.current.deviceScaleFactor));
1901
+ }, [commitViewport]);
1902
+ const settleRecording = (effect, failure) => effect.pipe(Effect.catch((error) => notify("error", errorMessage(error, failure))), Effect.ensuring(Effect.sync(() => setRecordingBusy(false))));
1903
+ const runStartRecording = useEffectCallback((mode, targetId) => settleRecording(live.request("recording.start", {
1904
+ mode,
1905
+ targetId
1906
+ }), "Recording failed to start"), [live]);
1907
+ const startRecording = useCallback((mode) => {
1908
+ const targetId = activeTargetIdRef.current;
1909
+ if (!targetId || collaborationRole !== "owner" || recordingBusy) return;
1910
+ setRecordingBusy(true);
1911
+ runStartRecording(mode, targetId);
1912
+ }, [
1913
+ collaborationRole,
1914
+ recordingBusy,
1915
+ runStartRecording
1916
+ ]);
1917
+ const runStopRecording = useEffectCallback((credentials, sessionId, recordingId) => settleRecording(live.request("recording.stop", { recordingId }).pipe(Effect.andThen(SessionsApi.use((sessions) => sessions.downloadSessionRecording(credentials, sessionId, recordingId, sessionToken))), Effect.flatMap(({ blob, filename }) => {
1918
+ const recording = live.recordings.find((candidate) => candidate.recordingId === recordingId);
1919
+ return downloadBlob(blob, filename ?? `${sessionId}-${recording?.targetId ?? "target"}-${recordingId}.webm`);
1920
+ }), Effect.andThen(notify("success", "Recording saved"))), "Recording failed to stop"), [live, sessionToken]);
1921
+ const stopRecording = useCallback((recordingId) => {
1922
+ if (!sessionId || !credentials || recordingBusy) return;
1923
+ setRecordingBusy(true);
1924
+ runStopRecording(credentials, sessionId, recordingId);
1925
+ }, [
1926
+ credentials,
1927
+ recordingBusy,
1928
+ runStopRecording,
1929
+ sessionId
1930
+ ]);
1931
+ const runCancelRecording = useEffectCallback((recordingId) => settleRecording(live.request("recording.cancel", { recordingId }).pipe(Effect.andThen(notify("success", "Recording stopped"))), "Recording failed to stop"), [live]);
1932
+ const cancelRecording = useCallback((recordingId) => {
1933
+ if (recordingBusy) return;
1934
+ setRecordingBusy(true);
1935
+ runCancelRecording(recordingId);
1936
+ }, [recordingBusy, runCancelRecording]);
1937
+ const runSetRemoteCursor = useEffectCallback((visible) => live.request("presentation.cursor.set", { visible }).pipe(Effect.catch((error) => notify("error", errorMessage(error, "Remote cursor could not be updated")))), [live]);
1938
+ const setRemoteCursorEnabled = useCallback((visible) => {
1939
+ if (sessionId && credentials) runSetRemoteCursor(visible);
1940
+ }, [
1941
+ credentials,
1942
+ runSetRemoteCursor,
1943
+ sessionId
1944
+ ]);
1945
+ const runSelectPresentation = useEffectCallback((selection) => live.selectPresentation(selection).pipe(Effect.catch((error) => notify("error", errorMessage(error, "Presentation could not be updated")))), [live]);
1946
+ const selectMediaStream = useCallback((selection) => {
1947
+ if (!enabled || !sessionId || !credentials || live.mediaSwitching) return false;
1948
+ runSelectPresentation(selection);
1949
+ return true;
1950
+ }, [
1951
+ credentials,
1952
+ enabled,
1953
+ live.mediaSwitching,
1954
+ runSelectPresentation,
1955
+ sessionId
1956
+ ]);
1957
+ const setWebRTCStreamSettings = useCallback((settings) => selectMediaStream({
1958
+ kind: "webrtc",
1959
+ quality: settings
1960
+ }), [selectMediaStream]);
1961
+ const commands = useMemo(() => {
1962
+ const connected = (effect) => Effect.suspend(() => phaseRef.current === "connected" ? effect : Effect.fail(new LiveSessionError({ message: "the session is not connected" })));
1963
+ const request = (type, payload) => connected(live.request(type, payload));
1964
+ const onActiveTarget = (type, payload = {}) => connected(Effect.suspend(() => {
1965
+ const targetId = activeTargetIdRef.current;
1966
+ return targetId ? live.request(type, {
1967
+ targetId,
1968
+ ...payload
1969
+ }) : Effect.fail(new LiveSessionError({ message: "no tab is active" }));
1970
+ })).pipe(Effect.asVoid);
1971
+ return {
1972
+ navigate: (url) => onActiveTarget("page.navigate", { url }),
1973
+ historyBack: () => onActiveTarget("page.history-back"),
1974
+ historyForward: () => onActiveTarget("page.history-forward"),
1975
+ reload: (targetId) => targetId ? request("page.reload", { targetId }).pipe(Effect.asVoid) : onActiveTarget("page.reload"),
1976
+ stopLoading: () => onActiveTarget("page.stop-loading"),
1977
+ createTarget: (url = "about:blank") => request("target.create", { url }).pipe(Effect.map((result) => result.targetId ?? null)),
1978
+ closeTarget: (targetId) => request("target.close", { targetId }).pipe(Effect.asVoid),
1979
+ activateTarget: (targetId) => connected(live.requestSelectTarget(targetId))
1980
+ };
1981
+ }, [live.request, live.requestSelectTarget]);
1982
+ const activeTarget = useMemo(() => targets.find((target) => target.id === live.activeTargetId) ?? null, [live.activeTargetId, targets]);
1983
+ const mediaPath = live.transport === "webrtc" ? "webrtc-live" : "websocket-live";
1984
+ const mediaPhase = live.phase === "connected" ? "live" : live.phase === "connecting" ? "connecting" : live.phase === "error" ? "failed" : "idle";
1985
+ return {
1986
+ phase: live.phase,
1987
+ targets,
1988
+ activeTargetId: live.activeTargetId,
1989
+ activeTarget,
1990
+ frames: live.frames,
1991
+ mediaPhase,
1992
+ mediaStream: live.mediaStream,
1993
+ mediaStreamSettings: live.presentation?.quality ?? null,
1994
+ mediaVideoProfiles: live.presentation?.profiles ?? [],
1995
+ mediaSize: live.mediaSize,
1996
+ mediaPath,
1997
+ mediaTargetId: live.activeTargetId,
1998
+ mediaSwitching: live.mediaSwitching,
1999
+ viewport,
2000
+ browserViewportSize,
2001
+ viewportAutoSync,
2002
+ captured,
2003
+ recordings: live.recordings,
2004
+ recordingBusy,
2005
+ remoteCursorEnabled: live.presentation?.cursorVisible ?? true,
2006
+ collaboration: live.collaboration,
2007
+ commands,
2008
+ setCaptured,
2009
+ setInputDimensions: (size) => {
2010
+ inputDimensionsRef.current = size;
2011
+ },
2012
+ setViewport,
2013
+ setBrowserViewportSize,
2014
+ setViewportAutoSync,
2015
+ setViewportToBrowserSize,
2016
+ setWebRTCStreamSettings,
2017
+ selectMediaStream,
2018
+ sendInput,
2019
+ activateTarget,
2020
+ reorderTargets,
2021
+ createTarget,
2022
+ duplicateTarget,
2023
+ closeTarget,
2024
+ navigate,
2025
+ reload,
2026
+ stopLoading,
2027
+ historyBack,
2028
+ historyForward,
2029
+ startRecording,
2030
+ stopRecording,
2031
+ cancelRecording,
2032
+ setRemoteCursorEnabled,
2033
+ reconnect: live.reconnect
2034
+ };
2035
+ }
2036
+ function mergeTargetsInCurrentOrder(order, nextTargets) {
2037
+ const nextById = new Map(nextTargets.map((target) => [target.id, target]));
2038
+ const seen = /* @__PURE__ */ new Set();
2039
+ const ordered = [];
2040
+ for (const targetId of order) {
2041
+ const nextTarget = nextById.get(targetId);
2042
+ if (nextTarget) {
2043
+ ordered.push(nextTarget);
2044
+ seen.add(nextTarget.id);
2045
+ }
2046
+ }
2047
+ for (const nextTarget of nextTargets) if (!seen.has(nextTarget.id)) ordered.push(nextTarget);
2048
+ return ordered;
2049
+ }
2050
+ function createBrowserViewport(size, deviceScaleFactor) {
2051
+ return createViewportPreset(size.width, size.height, deviceScaleFactor);
2052
+ }
2053
+ const downloadBlob = (blob, filename) => Effect.acquireUseRelease(Effect.sync(() => URL.createObjectURL(blob)), (url) => Effect.sync(() => {
2054
+ const link = document.createElement("a");
2055
+ link.href = url;
2056
+ link.download = filename;
2057
+ link.click();
2058
+ }), (url) => Effect.sync(() => URL.revokeObjectURL(url)).pipe(Effect.delay(0)));
2059
+ function errorMessage(cause, fallback) {
2060
+ return cause instanceof Error && cause.message ? cause.message : fallback;
2061
+ }
2062
+ //#endregion
2063
+ //#region src/runtime.ts
2064
+ const makeApertureRuntime = (options = {}) => ManagedRuntime.make(Layer.mergeAll(apiClientLayer.pipe(Layer.provide(options.baseUrl ? baseUrlLayer(options.baseUrl) : Layer.empty), Layer.provide(FetchHttpClient.layer)), Layer.succeed(HttpClient.TracerPropagationEnabled, false)));
2065
+ //#endregion
2066
+ //#region src/provider.tsx
2067
+ function ApertureProvider({ baseUrl, children }) {
2068
+ const [runtime, setRuntime] = useState(null);
2069
+ useEffect(() => {
2070
+ const created = makeApertureRuntime({ baseUrl });
2071
+ setRuntime(created);
2072
+ return () => {
2073
+ setRuntime(null);
2074
+ created.dispose();
2075
+ };
2076
+ }, [baseUrl]);
2077
+ return runtime ? /* @__PURE__ */ jsx(RuntimeProvider, {
2078
+ runtime,
2079
+ baseUrl,
2080
+ children
2081
+ }) : null;
2082
+ }
2083
+ //#endregion
2084
+ //#region src/shared-session.ts
2085
+ function parseShareToken(token) {
2086
+ const sessionId = token.slice(4, 40);
2087
+ const role = token.startsWith("ape_") ? "editor" : token.startsWith("apv_") ? "viewer" : null;
2088
+ if (!role || token[40] !== "_" || !sessionId || !token.slice(41)) return null;
2089
+ return {
2090
+ token,
2091
+ sessionId,
2092
+ role
2093
+ };
2094
+ }
2095
+ const emptyIceServers = [];
2096
+ function useSharedSession({ token, displayName, onNotice }) {
2097
+ const share = useMemo(() => parseShareToken(token), [token]);
2098
+ const credentials = useMemo(() => share && {
2099
+ kind: "bearer",
2100
+ token: share.token,
2101
+ authorityType: null,
2102
+ tenantId: null,
2103
+ selectedTenantId: null
2104
+ }, [share]);
2105
+ const [state, setState] = useState({ kind: "loading" });
2106
+ useFork(() => share && credentials ? Effect.sync(() => setState({ kind: "loading" })).pipe(Effect.andThen(SessionsApi.use((sessions) => sessions.getBrowserStatus(credentials, share.sessionId))), Effect.match({
2107
+ onFailure: (error) => setState({ kind: error.status === 410 ? "expired" : "unavailable" }),
2108
+ onSuccess: (browser) => setState(browser.sessionId === share.sessionId ? {
2109
+ kind: "ready",
2110
+ browser
2111
+ } : { kind: "unavailable" })
2112
+ })) : void 0, [credentials, share]);
2113
+ const browser = state.kind === "ready" ? state.browser : null;
2114
+ const control = useBrowserControl({
2115
+ sessionId: share?.sessionId ?? null,
2116
+ credentials: browser ? credentials : null,
2117
+ displayName,
2118
+ sessionToken: share?.token,
2119
+ collaborationRole: share?.role ?? "viewer",
2120
+ enabled: browser !== null,
2121
+ webrtcProducerSupported: browser?.media.mode === "auto" && browser.media.webrtcProducer,
2122
+ webrtcIceServers: browser?.media.iceServers ?? emptyIceServers,
2123
+ onNotice
2124
+ });
2125
+ return {
2126
+ status: share ? state.kind : "invalid",
2127
+ share,
2128
+ browser,
2129
+ control
2130
+ };
2131
+ }
2132
+ //#endregion
2133
+ export { makeApertureRuntime as a, collaborationPaintLifetimeMs as c, RuntimeProviderMissingError as d, useBaseUrl as f, useRuntime as g, useRunApi as h, ApertureProvider as i, useLiveSession as l, useFork as m, parseShareToken as n, useBrowserControl as o, useEffectCallback as p, useSharedSession as r, SessionViewport as s, LiveSessionError$1 as t, RuntimeProvider as u };
2134
+
2135
+ //# sourceMappingURL=headless-CKFacFX3.js.map