@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 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,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 (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
- 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 || collaborationRole !== "owner" || recordingBusy) return;
1963
+ if (!targetId || !canRecord || recordingBusy) return;
1910
1964
  setRecordingBusy(true);
1911
- runStartRecording(mode, targetId);
1965
+ runStartRecording(mode, targetId, settings);
1912
1966
  }, [
1913
- collaborationRole,
1967
+ canRecord,
1914
1968
  recordingBusy,
1915
1969
  runStartRecording
1916
1970
  ]);
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 }) => {
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, sessionToken]);
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 (!sessionId || !credentials || recordingBusy) return;
1976
+ if (access === null || recordingBusy) return;
1923
1977
  setRecordingBusy(true);
1924
- runStopRecording(credentials, sessionId, recordingId);
1978
+ runStopRecording(access, recordingId);
1925
1979
  }, [
1926
- credentials,
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 (sessionId && credentials) runSetRemoteCursor(visible);
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
- credentials,
1942
- runSetRemoteCursor,
1943
- sessionId
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 || !sessionId || !credentials || live.mediaSwitching) return false;
2006
+ if (!enabled || access === null || live.mediaSwitching) return false;
1948
2007
  runSelectPresentation(selection);
1949
2008
  return true;
1950
2009
  }, [
1951
- credentials,
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/shared-session.ts
2085
- function parseShareToken(token) {
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
- const role = token.startsWith("ape_") ? "editor" : token.startsWith("apv_") ? "viewer" : null;
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
- token,
2164
+ kind: "direct",
2165
+ baseUrl,
2091
2166
  sessionId,
2092
- role
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 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;
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
- sessionId: share?.sessionId ?? null,
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: share ? state.kind : "invalid",
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, parseShareToken as n, useBrowserControl as o, useEffectCallback as p, useSharedSession as r, SessionViewport as s, LiveSessionError$1 as t, RuntimeProvider as u };
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-CKFacFX3.js.map
2229
+ //# sourceMappingURL=headless-0KVuQrnF.js.map