@aperture-browser/session-react 0.0.0-init.0 → 0.0.0-nightly.101.c63f90ec6164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -57
- package/dist/{headless-CKFacFX3.js → headless-0KVuQrnF.js} +201 -107
- package/dist/headless-0KVuQrnF.js.map +1 -0
- package/dist/headless.d.ts +47 -40
- package/dist/headless.js +2 -2
- package/dist/index.d.ts +23 -12
- package/dist/index.js +737 -350
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/dist/headless-CKFacFX3.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,77 +1,28 @@
|
|
|
1
1
|
# @aperture-browser/session-react
|
|
2
2
|
|
|
3
|
-
React components for embedding a live [Aperture](https://github.com/tarik02/aperture) browser session.
|
|
3
|
+
React components and hooks for embedding a live [Aperture](https://github.com/tarik02/aperture) browser session, with the built-in UI or headless.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install @aperture-browser/session-react effect react react-dom
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```tsx
|
|
10
|
-
import {
|
|
10
|
+
import { useMemo } from "react";
|
|
11
|
+
import { ApertureSession, shareSessionAccess } from "@aperture-browser/session-react";
|
|
11
12
|
import "@aperture-browser/session-react/styles.css";
|
|
12
13
|
|
|
13
14
|
export function SharedBrowser({ token }: { token: string }) {
|
|
15
|
+
const access = useMemo(() => shareSessionAccess(token, "https://aperture.example"), [token]);
|
|
14
16
|
return (
|
|
15
17
|
<div style={{ height: 600 }}>
|
|
16
|
-
<ApertureSession
|
|
18
|
+
<ApertureSession access={access} />
|
|
17
19
|
</div>
|
|
18
20
|
);
|
|
19
21
|
}
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
`
|
|
24
|
+
`@aperture-browser/session-react/headless` has the same session without any UI, for apps that build their own controls.
|
|
23
25
|
|
|
24
|
-
-
|
|
25
|
-
- `features`: turns parts of the UI off, e.g. `{ tabs: false, menus: false }`. Each defaults to on: `tabs`, `navigation` (back, forward, reload), `addressBar`, `presence` (who else is connected), `drawing`, `menus` (stream, viewport, recording and input settings), `statusBadge`, `toaster` (turn it off if your app renders its own [sonner](https://sonner.emilkowal.ski) toaster).
|
|
26
|
-
- `theme`: `"light"`, `"dark"` or `"system"` (the default).
|
|
26
|
+
Documentation: https://aperture-browser-docs.pages.dev/docs/packages/session-react
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`styles.css` styles everything inside the component's `.aperture-root` element: theme colors and a scoped reset. It does not touch your page's `html`, `body` or `:root`, and menus and tooltips render inside the root. It does include Tailwind utility classes, which your own unlayered CSS can override. For full isolation use [`@aperture-browser/session-element`](https://www.npmjs.com/package/@aperture-browser/session-element), which renders into Shadow DOM.
|
|
31
|
-
|
|
32
|
-
The component uses your page's font. To pick others, set `--aperture-font-sans` and `--aperture-font-mono` (used for URLs) on it or any ancestor, e.g. `--aperture-font-sans: "Inter", sans-serif`.
|
|
33
|
-
|
|
34
|
-
## Headless
|
|
35
|
-
|
|
36
|
-
`@aperture-browser/session-react/headless` has the session without any UI: no tab strip, toolbar, menus, stylesheet or UI dependencies. Build your own controls around it.
|
|
37
|
-
|
|
38
|
-
```tsx
|
|
39
|
-
import {
|
|
40
|
-
ApertureProvider,
|
|
41
|
-
SessionViewport,
|
|
42
|
-
useSharedSession,
|
|
43
|
-
} from "@aperture-browser/session-react/headless";
|
|
44
|
-
|
|
45
|
-
function Browser({ token }: { token: string }) {
|
|
46
|
-
const { status, control } = useSharedSession({ token, onNotice: console.warn });
|
|
47
|
-
if (status !== "ready") return <p>{status}</p>;
|
|
48
|
-
return (
|
|
49
|
-
<>
|
|
50
|
-
{control.targets.map((tab) => (
|
|
51
|
-
<button key={tab.id} onClick={() => control.activateTarget(tab.id)}>
|
|
52
|
-
{tab.title}
|
|
53
|
-
</button>
|
|
54
|
-
))}
|
|
55
|
-
<button onClick={() => control.historyBack()}>Back</button>
|
|
56
|
-
<SessionViewport control={control} style={{ height: 600 }} />
|
|
57
|
-
</>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export const App = ({ token }: { token: string }) => (
|
|
62
|
-
<ApertureProvider baseUrl="https://aperture.example">
|
|
63
|
-
<Browser token={token} />
|
|
64
|
-
</ApertureProvider>
|
|
65
|
-
);
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
- `useSharedSession` resolves the token (`status` is `loading`, `ready`, `invalid`, `expired` or `unavailable`) and connects once it is ready. `control` lists the tabs and navigates, opens, closes and activates them.
|
|
69
|
-
- `SessionViewport` shows the active tab and forwards input to it. It is styled inline and takes `className` and `style`. `renderOverlay` receives the connection status, a placeholder reason while there is no picture yet, and cursor hints, for drawing your own overlays.
|
|
70
|
-
- `onNotice` receives the errors and confirmations the full UI shows as toasts.
|
|
71
|
-
- `control.commands` has the same actions as Effects that succeed once Aperture has carried them out and fail with a `LiveSessionError` otherwise, e.g. `yield* control.commands.navigate(url)`.
|
|
72
|
-
|
|
73
|
-
## Lower-level pieces
|
|
74
|
-
|
|
75
|
-
`SharedSession` is the same view without its own runtime, tooltip provider and toaster, for apps that provide them (`RuntimeProvider` with `makeApertureRuntime`, and `TooltipProvider`). `useBrowserControl` and `BrowserControlPane` drive and render a session for other credentials. These are what the Aperture web app is built from.
|
|
76
|
-
|
|
77
|
-
Requires `effect` 4 and React 19.
|
|
28
|
+
For consumer-authenticated access, pass a stable `access` object with `{ kind: "relay", baseUrl: "/aperture", sessionId }`. Your backend owns authentication through `@aperture-browser/session-relay`; the browser sends no Aperture credentials. `useSession({ access })` exposes the same connection headlessly.
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
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";
|
|
4
|
+
import { DEFAULT_VIEWPORT, LiveSessionConnection, LiveSessionError, LiveSessionError as LiveSessionError$1, computeRenderMetrics, createViewportPreset, downloadSessionRecording, evdevKeycodeByCode, getSessionStatus, getTargetThumbnail, keyboardInputMessage, keyboardModifiers, shouldForwardBrowserShortcut, strictParseOptions, windowsVirtualKeyCodeForCodeOrKey } from "@aperture-browser/live-session";
|
|
5
5
|
import * as Option from "effect/Option";
|
|
6
6
|
import * as PubSub from "effect/PubSub";
|
|
7
7
|
import * as Schema from "effect/Schema";
|
|
8
8
|
import * as SubscriptionRef from "effect/SubscriptionRef";
|
|
9
9
|
import * as Data from "effect/Data";
|
|
10
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { SessionsApi, apiClientLayer, baseUrlLayer } from "@aperture-browser/api-client";
|
|
12
11
|
import * as Layer from "effect/Layer";
|
|
13
12
|
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
14
|
-
import * as FetchHttpClient from "effect/
|
|
15
|
-
import * as HttpClient from "effect/
|
|
13
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
14
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
15
|
+
import { apiClientLayer, baseUrlLayer } from "@aperture-browser/api-client";
|
|
16
|
+
import * as Redacted from "effect/Redacted";
|
|
16
17
|
//#region src/effect.tsx
|
|
17
18
|
const RuntimeContext = createContext(null);
|
|
18
19
|
/** A hook below needed the app runtime, but no RuntimeProvider is above the component. */
|
|
@@ -94,10 +95,10 @@ const pointerButtonCode = {
|
|
|
94
95
|
right: 273,
|
|
95
96
|
middle: 274
|
|
96
97
|
};
|
|
97
|
-
function useLiveSession({
|
|
98
|
-
const identity = useMemo(() => collaborationIdentity(
|
|
98
|
+
function useLiveSession({ access, displayName = null, enabled, webrtcSupported, iceServers, autoSize }) {
|
|
99
|
+
const identity = useMemo(() => collaborationIdentity(displayName), [displayName]);
|
|
100
|
+
const [role, setRole] = useState("viewer");
|
|
99
101
|
const runtime = useRuntime();
|
|
100
|
-
const baseUrl = useBaseUrl();
|
|
101
102
|
const frameRef = useMemo(() => runtime.runSync(SubscriptionRef.make(null)), [runtime]);
|
|
102
103
|
const paintPubSub = useMemo(() => runtime.runSync(PubSub.unbounded()), [runtime]);
|
|
103
104
|
const frames = useMemo(() => SubscriptionRef.changes(frameRef), [frameRef]);
|
|
@@ -127,6 +128,7 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
127
128
|
const [presentationSwitching, setPresentationSwitching] = useState(false);
|
|
128
129
|
const [presentation, setPresentation] = useState(null);
|
|
129
130
|
const [recordings, setRecordings] = useState([]);
|
|
131
|
+
const [viewportOwnership, setViewportOwnership] = useState(null);
|
|
130
132
|
const [clientId, setClientId] = useState("");
|
|
131
133
|
const [holderClientId, setHolderClientId] = useState(null);
|
|
132
134
|
const [leaseMode, setLeaseMode] = useState(null);
|
|
@@ -139,6 +141,7 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
139
141
|
const handleMessage = useCallback((message) => {
|
|
140
142
|
switch (message.type) {
|
|
141
143
|
case "session.snapshot":
|
|
144
|
+
setRole(message.role);
|
|
142
145
|
clientIdRef.current = message.clientId;
|
|
143
146
|
setClientId(message.clientId);
|
|
144
147
|
holderClientIdRef.current = message.holderClientId ?? null;
|
|
@@ -152,6 +155,10 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
152
155
|
setMediaSize(resolveMediaSize(message.targets, message.activeTargetId));
|
|
153
156
|
setRecordings(message.recordings);
|
|
154
157
|
setPresentation(message.presentation ?? null);
|
|
158
|
+
setViewportOwnership(message.autoSize === void 0 ? null : {
|
|
159
|
+
ownerClientId: message.viewportOwnerClientId ?? null,
|
|
160
|
+
autoSize: message.autoSize
|
|
161
|
+
});
|
|
155
162
|
setLastError(null);
|
|
156
163
|
return;
|
|
157
164
|
case "targets.state":
|
|
@@ -179,6 +186,12 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
179
186
|
releasePendingRef.current = false;
|
|
180
187
|
setLastError((current) => current?.code === "input_busy" || current?.code === "input_not_owned" ? null : current);
|
|
181
188
|
return;
|
|
189
|
+
case "viewport.state":
|
|
190
|
+
setViewportOwnership({
|
|
191
|
+
ownerClientId: message.viewportOwnerClientId ?? null,
|
|
192
|
+
autoSize: message.autoSize
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
182
195
|
case "presence.cursor":
|
|
183
196
|
if (message.clientId !== followingClientIdRef.current) return;
|
|
184
197
|
setCursors((current) => new Map(current).set(message.clientId, message));
|
|
@@ -220,9 +233,10 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
220
233
|
}
|
|
221
234
|
}, [publishPaint]);
|
|
222
235
|
useFork(() => {
|
|
223
|
-
if (!enabled ||
|
|
236
|
+
if (!enabled || access === null) {
|
|
224
237
|
connectionRef.current = null;
|
|
225
238
|
setPhase("idle");
|
|
239
|
+
setRole("viewer");
|
|
226
240
|
setTargets([]);
|
|
227
241
|
setActiveTargetId(null);
|
|
228
242
|
setMediaStream(null);
|
|
@@ -234,17 +248,17 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
234
248
|
setParticipants([]);
|
|
235
249
|
setCursors(/* @__PURE__ */ new Map());
|
|
236
250
|
setRecordings([]);
|
|
251
|
+
setViewportOwnership(null);
|
|
237
252
|
publishFrame(null);
|
|
238
253
|
publishPaint({ type: "clear" });
|
|
239
254
|
return;
|
|
240
255
|
}
|
|
256
|
+
setRole("viewer");
|
|
241
257
|
return Effect.gen(function* () {
|
|
242
258
|
const connection = yield* LiveSessionConnection.make({
|
|
243
|
-
|
|
244
|
-
sessionId,
|
|
245
|
-
credentials,
|
|
246
|
-
sessionToken,
|
|
259
|
+
access,
|
|
247
260
|
identity,
|
|
261
|
+
autoSize,
|
|
248
262
|
iceServers,
|
|
249
263
|
webrtcSupported,
|
|
250
264
|
callbacks: {
|
|
@@ -269,16 +283,14 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
269
283
|
publishPaint({ type: "clear" });
|
|
270
284
|
})));
|
|
271
285
|
}, [
|
|
272
|
-
|
|
273
|
-
|
|
286
|
+
autoSize,
|
|
287
|
+
access,
|
|
274
288
|
enabled,
|
|
275
289
|
handleMessage,
|
|
276
290
|
iceServers,
|
|
277
291
|
identity,
|
|
278
292
|
publishFrame,
|
|
279
293
|
publishPaint,
|
|
280
|
-
sessionId,
|
|
281
|
-
sessionToken,
|
|
282
294
|
webrtcSupported
|
|
283
295
|
]);
|
|
284
296
|
const sendReliable = useCallback((message) => connectionRef.current?.sendReliable(message) ?? false, []);
|
|
@@ -525,6 +537,7 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
525
537
|
presentation,
|
|
526
538
|
mediaSwitching: targetSwitching || presentationSwitching,
|
|
527
539
|
recordings,
|
|
540
|
+
viewportOwnership,
|
|
528
541
|
sendBrowserInput,
|
|
529
542
|
selectTarget,
|
|
530
543
|
requestSelectTarget,
|
|
@@ -560,13 +573,13 @@ function useLiveSession({ sessionId, displayName = null, credentials, sessionTok
|
|
|
560
573
|
};
|
|
561
574
|
}
|
|
562
575
|
const StoredIdentity = Schema.Struct({
|
|
563
|
-
name: Schema.String.check(Schema.
|
|
576
|
+
name: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(48)),
|
|
564
577
|
avatarHash: Schema.String.check(Schema.isPattern(/^[0-9a-f]{32}$/))
|
|
565
578
|
});
|
|
566
579
|
const decodeStoredIdentity = Schema.decodeUnknownOption(Schema.fromJsonString(StoredIdentity), strictParseOptions);
|
|
567
|
-
function collaborationIdentity(
|
|
580
|
+
function collaborationIdentity(accountName) {
|
|
568
581
|
const anonymous = loadAnonymousIdentity();
|
|
569
|
-
if (
|
|
582
|
+
if (!accountName?.trim()) return anonymous;
|
|
570
583
|
return {
|
|
571
584
|
...anonymous,
|
|
572
585
|
name: Array.from(accountName.trim()).slice(0, 48).join("")
|
|
@@ -1749,7 +1762,8 @@ function focusedElement(node) {
|
|
|
1749
1762
|
//#endregion
|
|
1750
1763
|
//#region src/hooks/use-browser-control.ts
|
|
1751
1764
|
const emptyIceServers$1 = [];
|
|
1752
|
-
|
|
1765
|
+
const AUTO_SIZE_DEFAULT_STORAGE_KEY = "aperture.viewport.auto-size-default";
|
|
1766
|
+
function useBrowserControl({ access, displayName, enabled = true, webrtcProducerSupported = false, webrtcIceServers = emptyIceServers$1, onNotice }) {
|
|
1753
1767
|
const runtime = useRuntime();
|
|
1754
1768
|
const onNoticeRef = useRef(onNotice);
|
|
1755
1769
|
onNoticeRef.current = onNotice;
|
|
@@ -1757,16 +1771,20 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1757
1771
|
level,
|
|
1758
1772
|
message
|
|
1759
1773
|
}));
|
|
1774
|
+
const [viewportAutoSizeDefault, setViewportAutoSizeDefaultState] = useState(loadAutoSizeDefault);
|
|
1775
|
+
const [viewportAutoSync, setViewportAutoSyncState] = useState(() => viewportAutoSizeDefault);
|
|
1776
|
+
const viewportAutoSyncRef = useRef(viewportAutoSync);
|
|
1777
|
+
viewportAutoSyncRef.current = viewportAutoSync;
|
|
1778
|
+
const autoSizePreference = useCallback(() => viewportAutoSyncRef.current, []);
|
|
1760
1779
|
const live = useLiveSession({
|
|
1761
|
-
|
|
1780
|
+
access,
|
|
1762
1781
|
displayName,
|
|
1763
|
-
|
|
1764
|
-
sessionToken,
|
|
1765
|
-
role: collaborationRole,
|
|
1766
|
-
enabled: Boolean(enabled && sessionId && credentials),
|
|
1782
|
+
enabled: enabled && access !== null,
|
|
1767
1783
|
webrtcSupported: webrtcProducerSupported,
|
|
1768
|
-
iceServers: webrtcIceServers
|
|
1784
|
+
iceServers: webrtcIceServers,
|
|
1785
|
+
autoSize: autoSizePreference
|
|
1769
1786
|
});
|
|
1787
|
+
const collaborationRole = live.collaboration.role;
|
|
1770
1788
|
const [targetOrder, setTargetOrder] = useState([]);
|
|
1771
1789
|
const targets = useMemo(() => mergeTargetsInCurrentOrder(targetOrder, live.targets), [live.targets, targetOrder]);
|
|
1772
1790
|
const liveTargetsRef = useRef(live.targets);
|
|
@@ -1775,18 +1793,17 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1775
1793
|
phaseRef.current = live.phase;
|
|
1776
1794
|
const [viewport, setViewportState] = useState(DEFAULT_VIEWPORT);
|
|
1777
1795
|
const [browserViewportSize, setBrowserViewportSizeState] = useState(null);
|
|
1778
|
-
const [viewportAutoSync, setViewportAutoSyncState] = useState(false);
|
|
1779
1796
|
const [captured, setCaptured] = useState(false);
|
|
1780
1797
|
const [recordingBusy, setRecordingBusy] = useState(false);
|
|
1798
|
+
const canRecord = collaborationRole === "owner" || collaborationRole === "editor";
|
|
1799
|
+
const loadTargetThumbnail = useMemo(() => access === null ? null : (targetId) => getTargetThumbnail(access, targetId), [access]);
|
|
1781
1800
|
const activeTargetIdRef = useRef(null);
|
|
1782
1801
|
const viewportRef = useRef(viewport);
|
|
1783
1802
|
const inputDimensionsRef = useRef(DEFAULT_VIEWPORT);
|
|
1784
1803
|
const browserViewportSizeRef = useRef(null);
|
|
1785
|
-
const viewportAutoSyncRef = useRef(false);
|
|
1786
1804
|
activeTargetIdRef.current = live.activeTargetId;
|
|
1787
1805
|
viewportRef.current = viewport;
|
|
1788
1806
|
browserViewportSizeRef.current = browserViewportSize;
|
|
1789
|
-
viewportAutoSyncRef.current = viewportAutoSync;
|
|
1790
1807
|
useEffect(() => {
|
|
1791
1808
|
if (live.phase !== "connected") setCaptured(false);
|
|
1792
1809
|
}, [live.phase]);
|
|
@@ -1797,13 +1814,12 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1797
1814
|
setViewportState(createBrowserViewport(live.mediaSize, live.mediaSize.deviceScaleFactor));
|
|
1798
1815
|
}, [live.mediaSize]);
|
|
1799
1816
|
const sendInput = useCallback((message) => {
|
|
1800
|
-
if (!enabled ||
|
|
1817
|
+
if (!enabled || access === null) return false;
|
|
1801
1818
|
return live.sendBrowserInput(message, inputDimensionsRef.current);
|
|
1802
1819
|
}, [
|
|
1803
|
-
|
|
1820
|
+
access,
|
|
1804
1821
|
enabled,
|
|
1805
|
-
live
|
|
1806
|
-
sessionId
|
|
1822
|
+
live
|
|
1807
1823
|
]);
|
|
1808
1824
|
const activateTarget = useCallback((targetId) => {
|
|
1809
1825
|
live.selectTarget(targetId);
|
|
@@ -1859,21 +1875,54 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1859
1875
|
const targetId = activeTargetIdRef.current;
|
|
1860
1876
|
if (targetId) live.command("page.history-forward", { targetId });
|
|
1861
1877
|
}, [live]);
|
|
1862
|
-
const
|
|
1878
|
+
const viewportOwnership = live.viewportOwnership;
|
|
1879
|
+
const viewportOwnershipRef = useRef(viewportOwnership);
|
|
1880
|
+
viewportOwnershipRef.current = viewportOwnership;
|
|
1881
|
+
const viewportAutoSizeActive = viewportAutoSync && (viewportOwnership === null || viewportOwnership.ownerClientId === live.collaboration.clientId);
|
|
1882
|
+
const viewportAutoSizeActiveRef = useRef(viewportAutoSizeActive);
|
|
1883
|
+
viewportAutoSizeActiveRef.current = viewportAutoSizeActive;
|
|
1884
|
+
useEffect(() => {
|
|
1885
|
+
if (viewportOwnership !== null) {
|
|
1886
|
+
viewportAutoSyncRef.current = viewportOwnership.autoSize;
|
|
1887
|
+
setViewportAutoSyncState(viewportOwnership.autoSize);
|
|
1888
|
+
}
|
|
1889
|
+
}, [viewportOwnership]);
|
|
1890
|
+
const runAutoSizeViewport = useEffectCallback((payload) => live.request("viewport.set", payload).pipe(Effect.ignore), [live.request]);
|
|
1891
|
+
const commitViewport = useCallback((preset, autoSize) => {
|
|
1863
1892
|
setViewportState(preset);
|
|
1864
1893
|
const targetId = activeTargetIdRef.current;
|
|
1865
|
-
if (targetId)
|
|
1894
|
+
if (targetId === null) return;
|
|
1895
|
+
const payload = {
|
|
1866
1896
|
targetId,
|
|
1867
1897
|
width: preset.width,
|
|
1868
1898
|
height: preset.height,
|
|
1869
1899
|
deviceScaleFactor: preset.deviceScaleFactor
|
|
1900
|
+
};
|
|
1901
|
+
if (autoSize && viewportOwnershipRef.current !== null) runAutoSizeViewport({
|
|
1902
|
+
...payload,
|
|
1903
|
+
autoSize: true
|
|
1870
1904
|
});
|
|
1871
|
-
|
|
1905
|
+
else live.command("viewport.set", payload);
|
|
1906
|
+
}, [live.command, runAutoSizeViewport]);
|
|
1907
|
+
const setViewportAutoSync = useCallback((nextEnabled) => {
|
|
1908
|
+
if (viewportAutoSyncRef.current === nextEnabled) return;
|
|
1909
|
+
viewportAutoSyncRef.current = nextEnabled;
|
|
1910
|
+
setViewportAutoSyncState(nextEnabled);
|
|
1911
|
+
if (viewportOwnershipRef.current !== null) live.command("viewport.auto-size.set", { enabled: nextEnabled });
|
|
1912
|
+
}, [live.command]);
|
|
1913
|
+
useEffect(() => {
|
|
1914
|
+
const size = browserViewportSizeRef.current;
|
|
1915
|
+
if (viewportAutoSizeActive && live.phase === "connected" && live.activeTargetId !== null && size !== null) commitViewport(createBrowserViewport(size, viewportRef.current.deviceScaleFactor), true);
|
|
1916
|
+
}, [
|
|
1917
|
+
commitViewport,
|
|
1918
|
+
live.activeTargetId,
|
|
1919
|
+
live.phase,
|
|
1920
|
+
viewportAutoSizeActive
|
|
1921
|
+
]);
|
|
1872
1922
|
const setViewport = useCallback((preset) => {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
}, [commitViewport]);
|
|
1923
|
+
setViewportAutoSync(false);
|
|
1924
|
+
commitViewport(preset, false);
|
|
1925
|
+
}, [commitViewport, setViewportAutoSync]);
|
|
1877
1926
|
const setBrowserViewportSize = useCallback((size) => {
|
|
1878
1927
|
const next = {
|
|
1879
1928
|
width: Math.round(size.width),
|
|
@@ -1884,49 +1933,53 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1884
1933
|
if (current?.width === next.width && current.height === next.height) return;
|
|
1885
1934
|
browserViewportSizeRef.current = next;
|
|
1886
1935
|
setBrowserViewportSizeState(next);
|
|
1887
|
-
if (
|
|
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));
|
|
1936
|
+
if (viewportAutoSizeActiveRef.current) commitViewport(createBrowserViewport(next, viewportRef.current.deviceScaleFactor), true);
|
|
1894
1937
|
}, [commitViewport]);
|
|
1895
1938
|
const setViewportToBrowserSize = useCallback(() => {
|
|
1896
1939
|
const size = browserViewportSizeRef.current;
|
|
1897
1940
|
if (!size) return;
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1941
|
+
setViewportAutoSync(false);
|
|
1942
|
+
commitViewport(createBrowserViewport(size, viewportRef.current.deviceScaleFactor), false);
|
|
1943
|
+
}, [commitViewport, setViewportAutoSync]);
|
|
1944
|
+
const takeOverViewport = useCallback(() => {
|
|
1945
|
+
viewportAutoSyncRef.current = true;
|
|
1946
|
+
setViewportAutoSyncState(true);
|
|
1947
|
+
live.command("viewport.owner.claim");
|
|
1948
|
+
}, [live.command]);
|
|
1949
|
+
const setViewportAutoSizeDefault = useCallback((nextEnabled) => {
|
|
1950
|
+
setViewportAutoSizeDefaultState(nextEnabled);
|
|
1951
|
+
try {
|
|
1952
|
+
window.localStorage.setItem(AUTO_SIZE_DEFAULT_STORAGE_KEY, String(nextEnabled));
|
|
1953
|
+
} catch {}
|
|
1954
|
+
}, []);
|
|
1902
1955
|
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", {
|
|
1956
|
+
const runStartRecording = useEffectCallback((mode, targetId, settings) => settleRecording(live.request("recording.start", {
|
|
1904
1957
|
mode,
|
|
1905
|
-
targetId
|
|
1958
|
+
targetId,
|
|
1959
|
+
...settings
|
|
1906
1960
|
}), "Recording failed to start"), [live]);
|
|
1907
|
-
const startRecording = useCallback((mode) => {
|
|
1961
|
+
const startRecording = useCallback((mode, settings) => {
|
|
1908
1962
|
const targetId = activeTargetIdRef.current;
|
|
1909
|
-
if (!targetId ||
|
|
1963
|
+
if (!targetId || !canRecord || recordingBusy) return;
|
|
1910
1964
|
setRecordingBusy(true);
|
|
1911
|
-
runStartRecording(mode, targetId);
|
|
1965
|
+
runStartRecording(mode, targetId, settings);
|
|
1912
1966
|
}, [
|
|
1913
|
-
|
|
1967
|
+
canRecord,
|
|
1914
1968
|
recordingBusy,
|
|
1915
1969
|
runStartRecording
|
|
1916
1970
|
]);
|
|
1917
|
-
const runStopRecording = useEffectCallback((
|
|
1971
|
+
const runStopRecording = useEffectCallback((access, recordingId) => settleRecording(live.request("recording.stop", { recordingId }).pipe(Effect.andThen(downloadSessionRecording(access, recordingId)), Effect.flatMap(({ blob, filename }) => {
|
|
1918
1972
|
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
|
|
1973
|
+
return downloadBlob(blob, filename ?? `${access.sessionId}-${recording?.targetId ?? "target"}-${recordingId}.webm`);
|
|
1974
|
+
}), Effect.andThen(notify("success", "Recording saved"))), "Recording failed to stop"), [live]);
|
|
1921
1975
|
const stopRecording = useCallback((recordingId) => {
|
|
1922
|
-
if (
|
|
1976
|
+
if (access === null || recordingBusy) return;
|
|
1923
1977
|
setRecordingBusy(true);
|
|
1924
|
-
runStopRecording(
|
|
1978
|
+
runStopRecording(access, recordingId);
|
|
1925
1979
|
}, [
|
|
1926
|
-
|
|
1980
|
+
access,
|
|
1927
1981
|
recordingBusy,
|
|
1928
|
-
runStopRecording
|
|
1929
|
-
sessionId
|
|
1982
|
+
runStopRecording
|
|
1930
1983
|
]);
|
|
1931
1984
|
const runCancelRecording = useEffectCallback((recordingId) => settleRecording(live.request("recording.cancel", { recordingId }).pipe(Effect.andThen(notify("success", "Recording stopped"))), "Recording failed to stop"), [live]);
|
|
1932
1985
|
const cancelRecording = useCallback((recordingId) => {
|
|
@@ -1936,23 +1989,28 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1936
1989
|
}, [recordingBusy, runCancelRecording]);
|
|
1937
1990
|
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
1991
|
const setRemoteCursorEnabled = useCallback((visible) => {
|
|
1939
|
-
if (
|
|
1992
|
+
if (access !== null) runSetRemoteCursor(visible);
|
|
1993
|
+
}, [access, runSetRemoteCursor]);
|
|
1994
|
+
const [watchableAutomation, setWatchableAutomation] = useState(false);
|
|
1995
|
+
const commandRef = useRef(live.command);
|
|
1996
|
+
commandRef.current = live.command;
|
|
1997
|
+
useEffect(() => {
|
|
1998
|
+
if (live.phase === "connected" && canRecord) commandRef.current("automation.pacing.set", { pacing: watchableAutomation ? "watchable" : "normal" });
|
|
1940
1999
|
}, [
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2000
|
+
live.phase,
|
|
2001
|
+
canRecord,
|
|
2002
|
+
watchableAutomation
|
|
1944
2003
|
]);
|
|
1945
2004
|
const runSelectPresentation = useEffectCallback((selection) => live.selectPresentation(selection).pipe(Effect.catch((error) => notify("error", errorMessage(error, "Presentation could not be updated")))), [live]);
|
|
1946
2005
|
const selectMediaStream = useCallback((selection) => {
|
|
1947
|
-
if (!enabled ||
|
|
2006
|
+
if (!enabled || access === null || live.mediaSwitching) return false;
|
|
1948
2007
|
runSelectPresentation(selection);
|
|
1949
2008
|
return true;
|
|
1950
2009
|
}, [
|
|
1951
|
-
|
|
2010
|
+
access,
|
|
1952
2011
|
enabled,
|
|
1953
2012
|
live.mediaSwitching,
|
|
1954
|
-
runSelectPresentation
|
|
1955
|
-
sessionId
|
|
2013
|
+
runSelectPresentation
|
|
1956
2014
|
]);
|
|
1957
2015
|
const setWebRTCStreamSettings = useCallback((settings) => selectMediaStream({
|
|
1958
2016
|
kind: "webrtc",
|
|
@@ -1999,10 +2057,16 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1999
2057
|
viewport,
|
|
2000
2058
|
browserViewportSize,
|
|
2001
2059
|
viewportAutoSync,
|
|
2060
|
+
viewportAutoSizeActive,
|
|
2061
|
+
viewportOwnership,
|
|
2062
|
+
viewportAutoSizeDefault,
|
|
2002
2063
|
captured,
|
|
2003
2064
|
recordings: live.recordings,
|
|
2065
|
+
canRecord,
|
|
2066
|
+
loadTargetThumbnail,
|
|
2004
2067
|
recordingBusy,
|
|
2005
2068
|
remoteCursorEnabled: live.presentation?.cursorVisible ?? true,
|
|
2069
|
+
watchableAutomation,
|
|
2006
2070
|
collaboration: live.collaboration,
|
|
2007
2071
|
commands,
|
|
2008
2072
|
setCaptured,
|
|
@@ -2013,6 +2077,8 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
2013
2077
|
setBrowserViewportSize,
|
|
2014
2078
|
setViewportAutoSync,
|
|
2015
2079
|
setViewportToBrowserSize,
|
|
2080
|
+
setViewportAutoSizeDefault,
|
|
2081
|
+
takeOverViewport,
|
|
2016
2082
|
setWebRTCStreamSettings,
|
|
2017
2083
|
selectMediaStream,
|
|
2018
2084
|
sendInput,
|
|
@@ -2030,6 +2096,7 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
2030
2096
|
stopRecording,
|
|
2031
2097
|
cancelRecording,
|
|
2032
2098
|
setRemoteCursorEnabled,
|
|
2099
|
+
setWatchableAutomation,
|
|
2033
2100
|
reconnect: live.reconnect
|
|
2034
2101
|
};
|
|
2035
2102
|
}
|
|
@@ -2047,6 +2114,13 @@ function mergeTargetsInCurrentOrder(order, nextTargets) {
|
|
|
2047
2114
|
for (const nextTarget of nextTargets) if (!seen.has(nextTarget.id)) ordered.push(nextTarget);
|
|
2048
2115
|
return ordered;
|
|
2049
2116
|
}
|
|
2117
|
+
function loadAutoSizeDefault() {
|
|
2118
|
+
try {
|
|
2119
|
+
return window.localStorage.getItem(AUTO_SIZE_DEFAULT_STORAGE_KEY) === "true";
|
|
2120
|
+
} catch {
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2050
2124
|
function createBrowserViewport(size, deviceScaleFactor) {
|
|
2051
2125
|
return createViewportPreset(size.width, size.height, deviceScaleFactor);
|
|
2052
2126
|
}
|
|
@@ -2061,7 +2135,7 @@ function errorMessage(cause, fallback) {
|
|
|
2061
2135
|
}
|
|
2062
2136
|
//#endregion
|
|
2063
2137
|
//#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)));
|
|
2138
|
+
const makeApertureRuntime = (options = {}) => ManagedRuntime.make(Layer.mergeAll(FetchHttpClient.layer, apiClientLayer.pipe(Layer.provide(options.baseUrl ? baseUrlLayer(options.baseUrl) : Layer.empty), Layer.provide(FetchHttpClient.layer)), Layer.succeed(HttpClient.TracerPropagationEnabled, false)));
|
|
2065
2139
|
//#endregion
|
|
2066
2140
|
//#region src/provider.tsx
|
|
2067
2141
|
function ApertureProvider({ baseUrl, children }) {
|
|
@@ -2081,55 +2155,75 @@ function ApertureProvider({ baseUrl, children }) {
|
|
|
2081
2155
|
}) : null;
|
|
2082
2156
|
}
|
|
2083
2157
|
//#endregion
|
|
2084
|
-
//#region src/
|
|
2085
|
-
|
|
2158
|
+
//#region src/session.ts
|
|
2159
|
+
/** Convert an editor/viewer share link into direct session access. */
|
|
2160
|
+
function shareSessionAccess(token, baseUrl) {
|
|
2086
2161
|
const sessionId = token.slice(4, 40);
|
|
2087
|
-
|
|
2088
|
-
if (!role || token[40] !== "_" || !sessionId || !token.slice(41)) return null;
|
|
2162
|
+
if (!(token.startsWith("ape_") || token.startsWith("apv_")) || token[40] !== "_" || token.length <= 41) return null;
|
|
2089
2163
|
return {
|
|
2090
|
-
|
|
2164
|
+
kind: "direct",
|
|
2165
|
+
baseUrl,
|
|
2091
2166
|
sessionId,
|
|
2092
|
-
|
|
2167
|
+
credentials: {
|
|
2168
|
+
kind: "bearer",
|
|
2169
|
+
token: Redacted.make(token),
|
|
2170
|
+
authorityType: null,
|
|
2171
|
+
tenantId: null,
|
|
2172
|
+
selectedTenantId: null
|
|
2173
|
+
}
|
|
2093
2174
|
};
|
|
2094
2175
|
}
|
|
2095
2176
|
const emptyIceServers = [];
|
|
2096
|
-
function
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2177
|
+
function failedStatus(httpStatus) {
|
|
2178
|
+
if (httpStatus === 401 || httpStatus === 403) return "denied";
|
|
2179
|
+
if (httpStatus === 410) return "expired";
|
|
2180
|
+
return "unavailable";
|
|
2181
|
+
}
|
|
2182
|
+
function useSession({ access, displayName, onNotice }) {
|
|
2183
|
+
const [state, setState] = useState({
|
|
2184
|
+
access: null,
|
|
2185
|
+
status: "loading",
|
|
2186
|
+
browser: null
|
|
2187
|
+
});
|
|
2188
|
+
useFork(() => {
|
|
2189
|
+
if (access === null) return;
|
|
2190
|
+
setState({
|
|
2191
|
+
access,
|
|
2192
|
+
status: "loading",
|
|
2193
|
+
browser: null
|
|
2194
|
+
});
|
|
2195
|
+
return getSessionStatus(access).pipe(Effect.match({
|
|
2196
|
+
onFailure: (error) => setState({
|
|
2197
|
+
access,
|
|
2198
|
+
status: failedStatus(error.status),
|
|
2199
|
+
browser: null
|
|
2200
|
+
}),
|
|
2201
|
+
onSuccess: (browser) => setState(access.kind === "direct" && browser.sessionId !== access.sessionId ? {
|
|
2202
|
+
access,
|
|
2203
|
+
status: "unavailable",
|
|
2204
|
+
browser: null
|
|
2205
|
+
} : {
|
|
2206
|
+
access,
|
|
2207
|
+
status: "ready",
|
|
2208
|
+
browser
|
|
2209
|
+
})
|
|
2210
|
+
}));
|
|
2211
|
+
}, [access]);
|
|
2212
|
+
const browser = state.access === access ? state.browser : null;
|
|
2114
2213
|
const control = useBrowserControl({
|
|
2115
|
-
|
|
2116
|
-
credentials: browser ? credentials : null,
|
|
2214
|
+
access: browser === null ? null : access,
|
|
2117
2215
|
displayName,
|
|
2118
|
-
sessionToken: share?.token,
|
|
2119
|
-
collaborationRole: share?.role ?? "viewer",
|
|
2120
|
-
enabled: browser !== null,
|
|
2121
2216
|
webrtcProducerSupported: browser?.media.mode === "auto" && browser.media.webrtcProducer,
|
|
2122
2217
|
webrtcIceServers: browser?.media.iceServers ?? emptyIceServers,
|
|
2123
2218
|
onNotice
|
|
2124
2219
|
});
|
|
2125
2220
|
return {
|
|
2126
|
-
status:
|
|
2127
|
-
share,
|
|
2221
|
+
status: access === null ? "invalid" : state.access !== access ? "loading" : state.status,
|
|
2128
2222
|
browser,
|
|
2129
2223
|
control
|
|
2130
2224
|
};
|
|
2131
2225
|
}
|
|
2132
2226
|
//#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,
|
|
2227
|
+
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, shareSessionAccess as n, useBrowserControl as o, useEffectCallback as p, useSession as r, SessionViewport as s, LiveSessionError$1 as t, RuntimeProvider as u };
|
|
2134
2228
|
|
|
2135
|
-
//# sourceMappingURL=headless-
|
|
2229
|
+
//# sourceMappingURL=headless-0KVuQrnF.js.map
|