@aperture-browser/session-react 0.0.0-init.0 → 0.0.0-nightly.100.78b4e7ef6edc
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-CPoXwbFz.js} +196 -103
- package/dist/headless-CPoXwbFz.js.map +1 -0
- package/dist/headless.d.ts +46 -39
- package/dist/headless.js +2 -2
- package/dist/index.d.ts +23 -12
- package/dist/index.js +475 -303
- 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,21 +1933,25 @@ 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
1956
|
const runStartRecording = useEffectCallback((mode, targetId) => settleRecording(live.request("recording.start", {
|
|
1904
1957
|
mode,
|
|
@@ -1906,27 +1959,26 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1906
1959
|
}), "Recording failed to start"), [live]);
|
|
1907
1960
|
const startRecording = useCallback((mode) => {
|
|
1908
1961
|
const targetId = activeTargetIdRef.current;
|
|
1909
|
-
if (!targetId ||
|
|
1962
|
+
if (!targetId || !canRecord || recordingBusy) return;
|
|
1910
1963
|
setRecordingBusy(true);
|
|
1911
1964
|
runStartRecording(mode, targetId);
|
|
1912
1965
|
}, [
|
|
1913
|
-
|
|
1966
|
+
canRecord,
|
|
1914
1967
|
recordingBusy,
|
|
1915
1968
|
runStartRecording
|
|
1916
1969
|
]);
|
|
1917
|
-
const runStopRecording = useEffectCallback((
|
|
1970
|
+
const runStopRecording = useEffectCallback((access, recordingId) => settleRecording(live.request("recording.stop", { recordingId }).pipe(Effect.andThen(downloadSessionRecording(access, recordingId)), Effect.flatMap(({ blob, filename }) => {
|
|
1918
1971
|
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
|
|
1972
|
+
return downloadBlob(blob, filename ?? `${access.sessionId}-${recording?.targetId ?? "target"}-${recordingId}.webm`);
|
|
1973
|
+
}), Effect.andThen(notify("success", "Recording saved"))), "Recording failed to stop"), [live]);
|
|
1921
1974
|
const stopRecording = useCallback((recordingId) => {
|
|
1922
|
-
if (
|
|
1975
|
+
if (access === null || recordingBusy) return;
|
|
1923
1976
|
setRecordingBusy(true);
|
|
1924
|
-
runStopRecording(
|
|
1977
|
+
runStopRecording(access, recordingId);
|
|
1925
1978
|
}, [
|
|
1926
|
-
|
|
1979
|
+
access,
|
|
1927
1980
|
recordingBusy,
|
|
1928
|
-
runStopRecording
|
|
1929
|
-
sessionId
|
|
1981
|
+
runStopRecording
|
|
1930
1982
|
]);
|
|
1931
1983
|
const runCancelRecording = useEffectCallback((recordingId) => settleRecording(live.request("recording.cancel", { recordingId }).pipe(Effect.andThen(notify("success", "Recording stopped"))), "Recording failed to stop"), [live]);
|
|
1932
1984
|
const cancelRecording = useCallback((recordingId) => {
|
|
@@ -1936,23 +1988,28 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1936
1988
|
}, [recordingBusy, runCancelRecording]);
|
|
1937
1989
|
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
1990
|
const setRemoteCursorEnabled = useCallback((visible) => {
|
|
1939
|
-
if (
|
|
1991
|
+
if (access !== null) runSetRemoteCursor(visible);
|
|
1992
|
+
}, [access, runSetRemoteCursor]);
|
|
1993
|
+
const [watchableAutomation, setWatchableAutomation] = useState(false);
|
|
1994
|
+
const commandRef = useRef(live.command);
|
|
1995
|
+
commandRef.current = live.command;
|
|
1996
|
+
useEffect(() => {
|
|
1997
|
+
if (live.phase === "connected" && canRecord) commandRef.current("automation.pacing.set", { pacing: watchableAutomation ? "watchable" : "normal" });
|
|
1940
1998
|
}, [
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1999
|
+
live.phase,
|
|
2000
|
+
canRecord,
|
|
2001
|
+
watchableAutomation
|
|
1944
2002
|
]);
|
|
1945
2003
|
const runSelectPresentation = useEffectCallback((selection) => live.selectPresentation(selection).pipe(Effect.catch((error) => notify("error", errorMessage(error, "Presentation could not be updated")))), [live]);
|
|
1946
2004
|
const selectMediaStream = useCallback((selection) => {
|
|
1947
|
-
if (!enabled ||
|
|
2005
|
+
if (!enabled || access === null || live.mediaSwitching) return false;
|
|
1948
2006
|
runSelectPresentation(selection);
|
|
1949
2007
|
return true;
|
|
1950
2008
|
}, [
|
|
1951
|
-
|
|
2009
|
+
access,
|
|
1952
2010
|
enabled,
|
|
1953
2011
|
live.mediaSwitching,
|
|
1954
|
-
runSelectPresentation
|
|
1955
|
-
sessionId
|
|
2012
|
+
runSelectPresentation
|
|
1956
2013
|
]);
|
|
1957
2014
|
const setWebRTCStreamSettings = useCallback((settings) => selectMediaStream({
|
|
1958
2015
|
kind: "webrtc",
|
|
@@ -1999,10 +2056,16 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
1999
2056
|
viewport,
|
|
2000
2057
|
browserViewportSize,
|
|
2001
2058
|
viewportAutoSync,
|
|
2059
|
+
viewportAutoSizeActive,
|
|
2060
|
+
viewportOwnership,
|
|
2061
|
+
viewportAutoSizeDefault,
|
|
2002
2062
|
captured,
|
|
2003
2063
|
recordings: live.recordings,
|
|
2064
|
+
canRecord,
|
|
2065
|
+
loadTargetThumbnail,
|
|
2004
2066
|
recordingBusy,
|
|
2005
2067
|
remoteCursorEnabled: live.presentation?.cursorVisible ?? true,
|
|
2068
|
+
watchableAutomation,
|
|
2006
2069
|
collaboration: live.collaboration,
|
|
2007
2070
|
commands,
|
|
2008
2071
|
setCaptured,
|
|
@@ -2013,6 +2076,8 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
2013
2076
|
setBrowserViewportSize,
|
|
2014
2077
|
setViewportAutoSync,
|
|
2015
2078
|
setViewportToBrowserSize,
|
|
2079
|
+
setViewportAutoSizeDefault,
|
|
2080
|
+
takeOverViewport,
|
|
2016
2081
|
setWebRTCStreamSettings,
|
|
2017
2082
|
selectMediaStream,
|
|
2018
2083
|
sendInput,
|
|
@@ -2030,6 +2095,7 @@ function useBrowserControl({ sessionId, credentials, displayName, sessionToken,
|
|
|
2030
2095
|
stopRecording,
|
|
2031
2096
|
cancelRecording,
|
|
2032
2097
|
setRemoteCursorEnabled,
|
|
2098
|
+
setWatchableAutomation,
|
|
2033
2099
|
reconnect: live.reconnect
|
|
2034
2100
|
};
|
|
2035
2101
|
}
|
|
@@ -2047,6 +2113,13 @@ function mergeTargetsInCurrentOrder(order, nextTargets) {
|
|
|
2047
2113
|
for (const nextTarget of nextTargets) if (!seen.has(nextTarget.id)) ordered.push(nextTarget);
|
|
2048
2114
|
return ordered;
|
|
2049
2115
|
}
|
|
2116
|
+
function loadAutoSizeDefault() {
|
|
2117
|
+
try {
|
|
2118
|
+
return window.localStorage.getItem(AUTO_SIZE_DEFAULT_STORAGE_KEY) === "true";
|
|
2119
|
+
} catch {
|
|
2120
|
+
return false;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2050
2123
|
function createBrowserViewport(size, deviceScaleFactor) {
|
|
2051
2124
|
return createViewportPreset(size.width, size.height, deviceScaleFactor);
|
|
2052
2125
|
}
|
|
@@ -2061,7 +2134,7 @@ function errorMessage(cause, fallback) {
|
|
|
2061
2134
|
}
|
|
2062
2135
|
//#endregion
|
|
2063
2136
|
//#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)));
|
|
2137
|
+
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
2138
|
//#endregion
|
|
2066
2139
|
//#region src/provider.tsx
|
|
2067
2140
|
function ApertureProvider({ baseUrl, children }) {
|
|
@@ -2081,55 +2154,75 @@ function ApertureProvider({ baseUrl, children }) {
|
|
|
2081
2154
|
}) : null;
|
|
2082
2155
|
}
|
|
2083
2156
|
//#endregion
|
|
2084
|
-
//#region src/
|
|
2085
|
-
|
|
2157
|
+
//#region src/session.ts
|
|
2158
|
+
/** Convert an editor/viewer share link into direct session access. */
|
|
2159
|
+
function shareSessionAccess(token, baseUrl) {
|
|
2086
2160
|
const sessionId = token.slice(4, 40);
|
|
2087
|
-
|
|
2088
|
-
if (!role || token[40] !== "_" || !sessionId || !token.slice(41)) return null;
|
|
2161
|
+
if (!(token.startsWith("ape_") || token.startsWith("apv_")) || token[40] !== "_" || token.length <= 41) return null;
|
|
2089
2162
|
return {
|
|
2090
|
-
|
|
2163
|
+
kind: "direct",
|
|
2164
|
+
baseUrl,
|
|
2091
2165
|
sessionId,
|
|
2092
|
-
|
|
2166
|
+
credentials: {
|
|
2167
|
+
kind: "bearer",
|
|
2168
|
+
token: Redacted.make(token),
|
|
2169
|
+
authorityType: null,
|
|
2170
|
+
tenantId: null,
|
|
2171
|
+
selectedTenantId: null
|
|
2172
|
+
}
|
|
2093
2173
|
};
|
|
2094
2174
|
}
|
|
2095
2175
|
const emptyIceServers = [];
|
|
2096
|
-
function
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2176
|
+
function failedStatus(httpStatus) {
|
|
2177
|
+
if (httpStatus === 401 || httpStatus === 403) return "denied";
|
|
2178
|
+
if (httpStatus === 410) return "expired";
|
|
2179
|
+
return "unavailable";
|
|
2180
|
+
}
|
|
2181
|
+
function useSession({ access, displayName, onNotice }) {
|
|
2182
|
+
const [state, setState] = useState({
|
|
2183
|
+
access: null,
|
|
2184
|
+
status: "loading",
|
|
2185
|
+
browser: null
|
|
2186
|
+
});
|
|
2187
|
+
useFork(() => {
|
|
2188
|
+
if (access === null) return;
|
|
2189
|
+
setState({
|
|
2190
|
+
access,
|
|
2191
|
+
status: "loading",
|
|
2192
|
+
browser: null
|
|
2193
|
+
});
|
|
2194
|
+
return getSessionStatus(access).pipe(Effect.match({
|
|
2195
|
+
onFailure: (error) => setState({
|
|
2196
|
+
access,
|
|
2197
|
+
status: failedStatus(error.status),
|
|
2198
|
+
browser: null
|
|
2199
|
+
}),
|
|
2200
|
+
onSuccess: (browser) => setState(access.kind === "direct" && browser.sessionId !== access.sessionId ? {
|
|
2201
|
+
access,
|
|
2202
|
+
status: "unavailable",
|
|
2203
|
+
browser: null
|
|
2204
|
+
} : {
|
|
2205
|
+
access,
|
|
2206
|
+
status: "ready",
|
|
2207
|
+
browser
|
|
2208
|
+
})
|
|
2209
|
+
}));
|
|
2210
|
+
}, [access]);
|
|
2211
|
+
const browser = state.access === access ? state.browser : null;
|
|
2114
2212
|
const control = useBrowserControl({
|
|
2115
|
-
|
|
2116
|
-
credentials: browser ? credentials : null,
|
|
2213
|
+
access: browser === null ? null : access,
|
|
2117
2214
|
displayName,
|
|
2118
|
-
sessionToken: share?.token,
|
|
2119
|
-
collaborationRole: share?.role ?? "viewer",
|
|
2120
|
-
enabled: browser !== null,
|
|
2121
2215
|
webrtcProducerSupported: browser?.media.mode === "auto" && browser.media.webrtcProducer,
|
|
2122
2216
|
webrtcIceServers: browser?.media.iceServers ?? emptyIceServers,
|
|
2123
2217
|
onNotice
|
|
2124
2218
|
});
|
|
2125
2219
|
return {
|
|
2126
|
-
status:
|
|
2127
|
-
share,
|
|
2220
|
+
status: access === null ? "invalid" : state.access !== access ? "loading" : state.status,
|
|
2128
2221
|
browser,
|
|
2129
2222
|
control
|
|
2130
2223
|
};
|
|
2131
2224
|
}
|
|
2132
2225
|
//#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,
|
|
2226
|
+
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
2227
|
|
|
2135
|
-
//# sourceMappingURL=headless-
|
|
2228
|
+
//# sourceMappingURL=headless-CPoXwbFz.js.map
|