@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.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/headless-CKFacFX3.js +2135 -0
- package/dist/headless-CKFacFX3.js.map +1 -0
- package/dist/headless.d.ts +313 -0
- package/dist/headless.js +2 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +2212 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +70 -0
|
@@ -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
|