@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 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 { ApertureSession } from "@aperture-browser/session-react";
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 baseUrl="https://aperture.example" token={token} />
18
+ <ApertureSession access={access} />
17
19
  </div>
18
20
  );
19
21
  }
20
22
  ```
21
23
 
22
- `token` is a share link's editor (`ape_…`) or viewer (`apv_…`) token; the component grants exactly what the link grants. Props:
24
+ `@aperture-browser/session-react/headless` has the same session without any UI, for apps that build their own controls.
23
25
 
24
- - `baseUrl`: the Aperture instance, when it is not the page's own origin. The instance must list your origin in `embed_allowed_origins`.
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
- ## Styles
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/unstable/http/FetchHttpClient";
15
- import * as HttpClient from "effect/unstable/http/HttpClient";
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({ sessionId, displayName = null, credentials, sessionToken, role, enabled, webrtcSupported, iceServers }) {
98
- const identity = useMemo(() => collaborationIdentity(role, displayName), [displayName, role]);
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 || !sessionId || !credentials) {
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
- baseUrl,
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
- baseUrl,
273
- credentials,
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.isLengthBetween(1, 48)),
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(role, accountName) {
580
+ function collaborationIdentity(accountName) {
568
581
  const anonymous = loadAnonymousIdentity();
569
- if (role !== "owner" || !accountName?.trim()) return anonymous;
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
- function useBrowserControl({ sessionId, credentials, displayName, sessionToken, collaborationRole = "owner", enabled = true, webrtcProducerSupported = false, webrtcIceServers = emptyIceServers$1, onNotice }) {
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
- sessionId,
1780
+ access,
1762
1781
  displayName,
1763
- credentials,
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 || !sessionId || !credentials) return false;
1817
+ if (!enabled || access === null) return false;
1801
1818
  return live.sendBrowserInput(message, inputDimensionsRef.current);
1802
1819
  }, [
1803
- credentials,
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 commitViewport = useCallback((preset) => {
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) live.command("viewport.set", {
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
- }, [live]);
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
- viewportAutoSyncRef.current = false;
1874
- setViewportAutoSyncState(false);
1875
- commitViewport(preset);
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 (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));
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
- viewportAutoSyncRef.current = false;
1899
- setViewportAutoSyncState(false);
1900
- commitViewport(createBrowserViewport(size, viewportRef.current.deviceScaleFactor));
1901
- }, [commitViewport]);
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 || collaborationRole !== "owner" || recordingBusy) return;
1962
+ if (!targetId || !canRecord || recordingBusy) return;
1910
1963
  setRecordingBusy(true);
1911
1964
  runStartRecording(mode, targetId);
1912
1965
  }, [
1913
- collaborationRole,
1966
+ canRecord,
1914
1967
  recordingBusy,
1915
1968
  runStartRecording
1916
1969
  ]);
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 }) => {
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, sessionToken]);
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 (!sessionId || !credentials || recordingBusy) return;
1975
+ if (access === null || recordingBusy) return;
1923
1976
  setRecordingBusy(true);
1924
- runStopRecording(credentials, sessionId, recordingId);
1977
+ runStopRecording(access, recordingId);
1925
1978
  }, [
1926
- credentials,
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 (sessionId && credentials) runSetRemoteCursor(visible);
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
- credentials,
1942
- runSetRemoteCursor,
1943
- sessionId
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 || !sessionId || !credentials || live.mediaSwitching) return false;
2005
+ if (!enabled || access === null || live.mediaSwitching) return false;
1948
2006
  runSelectPresentation(selection);
1949
2007
  return true;
1950
2008
  }, [
1951
- credentials,
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/shared-session.ts
2085
- function parseShareToken(token) {
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
- const role = token.startsWith("ape_") ? "editor" : token.startsWith("apv_") ? "viewer" : null;
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
- token,
2163
+ kind: "direct",
2164
+ baseUrl,
2091
2165
  sessionId,
2092
- role
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 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;
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
- sessionId: share?.sessionId ?? null,
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: share ? state.kind : "invalid",
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, parseShareToken as n, useBrowserControl as o, useEffectCallback as p, useSharedSession as r, SessionViewport as s, LiveSessionError$1 as t, RuntimeProvider as u };
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-CKFacFX3.js.map
2228
+ //# sourceMappingURL=headless-CPoXwbFz.js.map