@expo/hub-client 1.3.0-canary-20261005-eaf97d1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +155 -73
  2. package/dist/DeviceClientProvider.d.ts +18 -0
  3. package/dist/DeviceClientProvider.d.ts.map +1 -0
  4. package/dist/DeviceClientProvider.mjs +47 -0
  5. package/dist/DeviceClientProvider.mjs.map +1 -0
  6. package/dist/DeviceScreen.d.ts +4 -4
  7. package/dist/DeviceScreen.d.ts.map +1 -1
  8. package/dist/DeviceScreen.mjs.map +1 -1
  9. package/dist/device-client-store.d.ts +9 -0
  10. package/dist/device-client-store.d.ts.map +1 -0
  11. package/dist/device-client-store.mjs +22 -0
  12. package/dist/device-client-store.mjs.map +1 -0
  13. package/dist/index.d.ts +7 -4
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.mjs +7 -4
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/types.d.ts +10 -4
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/types.mjs +5 -5
  20. package/dist/types.mjs.map +1 -1
  21. package/dist/useActiveDeviceClient.d.ts +1 -0
  22. package/dist/useActiveDeviceClient.d.ts.map +1 -1
  23. package/dist/useActiveDeviceClient.mjs +1 -0
  24. package/dist/useActiveDeviceClient.mjs.map +1 -1
  25. package/dist/useAndroidDevice.d.ts +1 -0
  26. package/dist/useAndroidDevice.d.ts.map +1 -1
  27. package/dist/useAndroidDevice.mjs +44 -31
  28. package/dist/useAndroidDevice.mjs.map +1 -1
  29. package/dist/useDeviceClient.d.ts +4 -0
  30. package/dist/useDeviceClient.d.ts.map +1 -0
  31. package/dist/useDeviceClient.mjs +72 -0
  32. package/dist/useDeviceClient.mjs.map +1 -0
  33. package/dist/useDeviceLocation.d.ts.map +1 -1
  34. package/dist/useDeviceLocation.mjs +5 -3
  35. package/dist/useDeviceLocation.mjs.map +1 -1
  36. package/dist/useDeviceScreenClient.d.ts +4 -0
  37. package/dist/useDeviceScreenClient.d.ts.map +1 -0
  38. package/dist/useDeviceScreenClient.mjs +23 -0
  39. package/dist/useDeviceScreenClient.mjs.map +1 -0
  40. package/dist/useIosDevice.d.ts +2 -0
  41. package/dist/useIosDevice.d.ts.map +1 -1
  42. package/dist/useIosDevice.mjs +53 -35
  43. package/dist/useIosDevice.mjs.map +1 -1
  44. package/package.json +8 -1
  45. package/src/DeviceClientProvider.tsx +63 -0
  46. package/src/DeviceScreen.tsx +4 -4
  47. package/src/device-client-store.ts +28 -0
  48. package/src/index.ts +11 -4
  49. package/src/types.ts +23 -4
  50. package/src/useActiveDeviceClient.ts +1 -0
  51. package/src/useAndroidDevice.ts +42 -22
  52. package/src/useDeviceClient.ts +84 -0
  53. package/src/useDeviceLocation.ts +4 -2
  54. package/src/useDeviceScreenClient.ts +38 -0
  55. package/src/useIosDevice.ts +71 -30
package/README.md CHANGED
@@ -1,19 +1,16 @@
1
1
  # @expo/hub-client
2
2
 
3
- React hooks and a `DeviceScreen` component that mirror a live iOS simulator or Android
4
- emulator in the browser. This is the device-client layer of
5
- [Expo Device Hub](https://github.com/expo/expo-device-hub): it connects to the Hub's streaming
6
- backends (serve-sim for iOS, serve-emu for Android), paints the video, and forwards touch,
7
- gesture, and keyboard input back to the device.
3
+ Render live iOS simulators and Android emulators in a React app. Connect through
4
+ [Expo Device Hub](https://github.com/expo/expo-device-hub), show the device with
5
+ `DeviceScreen`, and send touch, keyboard, and device controls.
8
6
 
9
7
  ## Requirements
10
8
 
11
9
  - A running Expo Device Hub server. Either start an Expo app that has the
12
10
  `expo-device-hub` DevTools plugin installed, or run `npx expo-device-hub` standalone.
13
- The hooks talk to the Hub's `/vendor/serve-sim` and `/vendor/serve-emu` routes.
11
+ The provider connects to the Hub's `/vendor/serve-sim` and `/vendor/serve-emu` routes.
14
12
  - React 18 or newer.
15
- - A browser. The package uses `WebSocket`, `EventSource`, WebCodecs, Media Source
16
- Extensions, and WebRTC, so it is not meant to run in Node.
13
+ - A browser to show the video and send input.
17
14
 
18
15
  ## Install
19
16
 
@@ -21,104 +18,153 @@ gesture, and keyboard input back to the device.
21
18
  npm install @expo/hub-client
22
19
  ```
23
20
 
24
- ## Render a live device screen
21
+ ## Connect to a device
25
22
 
26
- `useActiveDeviceClient` opens one connection to the selected device and returns a
27
- `DeviceClient`: the live connection state plus the controls. `DeviceScreen` paints that
28
- client's video and forwards pointer, gesture, and keyboard input. It is absolutely positioned
29
- and fills its parent, so give the parent a size and `position: relative`. `displayScreen`
30
- gives you the orientation-corrected screen size once the stream reports it.
23
+ Wrap your screen, controls and metrics in `DeviceClientProvider`. They share one
24
+ connection, and each component reads the client properties it needs.
25
+
26
+ ### Add the provider
27
+
28
+ Wrap the components that need the device connection. The components below all use this
29
+ provider:
31
30
 
32
31
  ```tsx
33
- import { DeviceScreen, displayScreen, useActiveDeviceClient } from '@expo/hub-client';
34
-
35
- export function LiveDevice({ udid }: { udid: string }) {
36
- // The second argument is where the Hub server is mounted on the current origin:
37
- // '' for the origin root (`npx expo-device-hub`), or
38
- // '/_expo/plugins/expo-device-hub' inside `expo start`. A full origin such as
39
- // 'http://localhost:3400' also works.
40
- const client = useActiveDeviceClient(
41
- { platform: 'ios', device: udid, streamMode: 'mjpeg' },
42
- '',
32
+ import { DeviceClientProvider } from '@expo/hub-client';
33
+
34
+ export function LiveSession({ udid }: { udid: string }) {
35
+ return (
36
+ <DeviceClientProvider
37
+ key={udid}
38
+ platform="ios"
39
+ options={{
40
+ baseUrl: 'http://localhost:3400/vendor/serve-sim',
41
+ device: udid,
42
+ streamMode: 'webrtc',
43
+ }}
44
+ >
45
+ <Screen />
46
+ <Controls />
47
+ <Cpu />
48
+ </DeviceClientProvider>
43
49
  );
50
+ }
51
+ ```
44
52
 
45
- const screen = displayScreen(client.screen);
46
- const aspectRatio = screen ? `${screen.width} / ${screen.height}` : '9 / 19.5';
53
+ For Android, use `platform="android"`, your serve-emu URL, and an adb serial such as
54
+ `'emulator-5554'` for `device`.
55
+
56
+ When using the Expo DevTools plugin, the server URL includes
57
+ `/_expo/plugins/expo-device-hub` before `/vendor/serve-sim` or `/vendor/serve-emu`.
58
+
59
+ `streamMode` is required: `'mjpeg'`, `'h264'`, or `'webrtc'`. iOS supports all three.
60
+ Android maps a mode it cannot serve to one it can.
61
+
62
+ Use one provider per session. When switching sessions, set its `key` to the session ID
63
+ so the old connection closes. Set `options.enabled` to `false` to disconnect, or use
64
+ `platform={null}` while no device is selected.
65
+
66
+ ### Read state and controls
67
+
68
+ Destructure everything you need from one `useDeviceClient()` call:
69
+
70
+ ```tsx
71
+ import { useDeviceClient } from '@expo/hub-client';
72
+
73
+ function Controls() {
74
+ const { status, pressButton, rotate, reload } = useDeviceClient();
75
+ const connected = status === 'streaming';
47
76
 
48
77
  return (
49
- <div style={{ position: 'relative', width: 360, aspectRatio }}>
50
- <DeviceScreen client={client} borderRadius={24} />
78
+ <div>
79
+ <span>{status}</span>
80
+ <button disabled={!connected} onClick={() => pressButton('home')}>Home</button>
81
+ <button disabled={!connected} onClick={rotate}>Rotate</button>
82
+ <button disabled={!connected} onClick={reload}>Reload</button>
51
83
  </div>
52
84
  );
53
85
  }
54
86
  ```
55
87
 
56
- - `platform` is `'ios'` or `'android'`. `device` is the simulator UDID or the adb serial. Pass
57
- `null` instead of the target object to render an idle screen without connecting.
58
- - `streamMode` is required and has no default: `'mjpeg'`, `'h264'`, or `'webrtc'`. iOS
59
- supports all three. Android maps a mode it cannot serve to one it can.
60
- - `client.status` moves through `'idle'`, `'connecting'`, `'streaming'`, and `'error'`
61
- (Android also reports `'reconnecting'`). `client.error` holds the last failure message.
88
+ Status and control changes update this component. FPS, metrics, and log changes do not.
89
+ Status can be `'idle'`, `'connecting'`, `'streaming'` or `'error'`. Android also reports
90
+ `'reconnecting'` while restoring a stream. Read `error` for the last failure message.
62
91
 
63
- To talk to one backend directly, use the platform hooks with the backend's base URL:
92
+ ### Show the screen
93
+
94
+ Use `useDeviceScreenClient` with `DeviceScreen`. It follows screen and input changes
95
+ without updating for metrics or logs.
96
+ `DeviceScreen` fills its parent, so give the parent a size and `position: relative`.
97
+ `displayScreen` gives you the screen size after accounting for rotation.
64
98
 
65
99
  ```tsx
66
- import { useAndroidDeviceClient, useIosDeviceClient } from '@expo/hub-client';
67
-
68
- const ios = useIosDeviceClient({
69
- baseUrl: 'http://localhost:3400/vendor/serve-sim',
70
- device: udid,
71
- streamMode: 'h264',
72
- });
73
-
74
- const android = useAndroidDeviceClient({
75
- baseUrl: 'http://localhost:3400/vendor/serve-emu',
76
- device: 'emulator-5554',
77
- streamMode: 'h264',
78
- });
100
+ import { DeviceScreen, displayScreen, useDeviceScreenClient } from '@expo/hub-client';
101
+
102
+ function Screen() {
103
+ const client = useDeviceScreenClient();
104
+ const screen = displayScreen(client.screen);
105
+ const aspectRatio = screen ? `${screen.width} / ${screen.height}` : '9 / 19.5';
106
+
107
+ return (
108
+ <div style={{ position: 'relative', width: 360, aspectRatio }}>
109
+ <DeviceScreen client={client} />
110
+ </div>
111
+ );
112
+ }
79
113
  ```
80
114
 
81
- When embedding the iOS screen on another site, pass the public serve-sim mount that serves
82
- `/api` and `/helper` as `baseUrl`, for example `https://sim.example.test/preview/session`.
83
- The stream and input URLs then use that server. Start serve-sim with
84
- `--cors-origin <origin>` for the origin of the embedding page, for example
85
- `--cors-origin http://localhost:8081`. Without it, the exec-ws socket closes, and logs,
86
- events, metrics and UI requests stop, even when both servers run on `localhost`.
115
+ ### Show CPU usage
87
116
 
88
- A serve-sim started with `--require-token`, such as an EAS Simulator Preview session, needs
89
- its session token on every request. Pass it as `token`:
117
+ Use `useDeviceClientSelector` when you need one value from a larger object. This component
118
+ updates when the latest CPU percentage changes, even if memory or network readings change.
90
119
 
91
120
  ```tsx
92
- const ios = useIosDeviceClient({
93
- baseUrl: 'https://sim.example.test/preview/session',
94
- device: udid,
95
- streamMode: 'mjpeg',
96
- token,
97
- });
121
+ import { useDeviceClientSelector } from '@expo/hub-client';
122
+
123
+ function Cpu() {
124
+ const cpuPct = useDeviceClientSelector((client) => client.activity?.samples.at(-1)?.cpuPct);
125
+
126
+ return <span>{cpuPct == null ? 'Waiting for metrics' : `CPU ${cpuPct.toFixed(1)}%`}</span>;
127
+ }
98
128
  ```
99
129
 
100
- The token does not replace `--cors-origin`.
130
+ Reading `activity` with `useDeviceClient` would update the component whenever any part
131
+ of `activity` changes.
101
132
 
102
- If the whole Device Hub is remote, pass its public mount to `useActiveDeviceClient`, for
103
- example `https://hub.example.test/device-hub`.
133
+ ### Keep updates focused
134
+
135
+ - Read state in your component body so it updates with the device. Data read only in an
136
+ event handler can be out of date. Controls such as `client.rotate()` use the latest callback.
137
+ - Pick named properties when destructuring. Spreading the client or using `{ status, ...rest }`
138
+ subscribes to every property. Reading `activity.samples` subscribes to the whole `activity` object.
139
+ - Keep selectors simple. Return a number, string, boolean, or an existing object. If you
140
+ build a new object, pass a comparison function as the second argument.
141
+ - Treat the client and its data as read-only.
104
142
 
105
143
  ## Connect to a token-gated Hub
106
144
 
107
145
  A Hub started with `npx expo-device-hub --require-token` needs its session token on every
108
146
  request. The Hub dashboard itself needs nothing extra: opening the link with the token gives
109
147
  the browser a cookie that covers every request. A page on another origin has no such cookie,
110
- so pass the token to the hook:
148
+ so pass the token to the provider:
111
149
 
112
150
  ```tsx
113
- const client = useActiveDeviceClient(
114
- { platform: 'ios', device: udid, streamMode: 'mjpeg' },
115
- 'http://127.0.0.1:3400',
116
- { token },
117
- );
151
+ <DeviceClientProvider
152
+ key={udid}
153
+ platform="ios"
154
+ options={{
155
+ baseUrl: 'http://127.0.0.1:3400/vendor/serve-sim',
156
+ device: udid,
157
+ streamMode: 'mjpeg',
158
+ token,
159
+ }}
160
+ >
161
+ <Screen />
162
+ </DeviceClientProvider>
118
163
  ```
119
164
 
120
- The platform hooks take the same `token` option. The client sends it as
121
- `Authorization: Bearer <token>`, as a `serve-sim.token.<token>` or `serve-emu.token.<token>`
165
+ For Android, use `platform="android"` and the `/vendor/serve-emu` URL.
166
+ The client sends the token as `Authorization: Bearer <token>`, as a `serve-sim.token.<token>` or
167
+ `serve-emu.token.<token>`
122
168
  WebSocket subprotocol, and as `?token=` only where a browser cannot set a header (the MJPEG
123
169
  `<img>`, `EventSource`).
124
170
 
@@ -168,6 +214,42 @@ and app permissions are only available on some backends. Check `client.capabilit
168
214
  you show their controls. The full `DeviceClient` contract, with a comment on every field, is
169
215
  in [`src/types.ts`](./src/types.ts).
170
216
 
217
+ ## Remote connections
218
+
219
+ Set `options.baseUrl` on `DeviceClientProvider` to the remote streaming server:
220
+
221
+ - iOS: `https://hub.example.test/device-hub/vendor/serve-sim`
222
+ - Android: `https://hub.example.test/device-hub/vendor/serve-emu`
223
+
224
+ ### iOS embedding
225
+
226
+ When embedding the iOS screen on another site, pass the public serve-sim mount that serves
227
+ `/api` and `/helper` as `baseUrl`, for example `https://sim.example.test/preview/session`.
228
+ The stream and input URLs then use that server. Start serve-sim with
229
+ `--cors-origin <origin>` for the origin of the embedding page, for example
230
+ `--cors-origin http://localhost:8081`. Without it, the exec-ws socket closes, and logs,
231
+ events, metrics and UI requests stop, even when both servers run on `localhost`.
232
+
233
+ A serve-sim started with `--require-token`, such as an EAS Simulator Preview session, needs
234
+ its session token on every request. Pass it as `options.token`:
235
+
236
+ ```tsx
237
+ <DeviceClientProvider
238
+ key={udid}
239
+ platform="ios"
240
+ options={{
241
+ baseUrl: 'https://sim.example.test/preview/session',
242
+ device: udid,
243
+ streamMode: 'mjpeg',
244
+ token,
245
+ }}
246
+ >
247
+ <Screen />
248
+ </DeviceClientProvider>
249
+ ```
250
+
251
+ The token does not replace `--cors-origin`.
252
+
171
253
  ## License
172
254
 
173
255
  MIT
@@ -0,0 +1,18 @@
1
+ import { type ReactNode } from "react";
2
+ import type { DeviceClient, DeviceConnectionOptions, DevicePlatform } from "./types";
3
+ export declare const DeviceClientStoreContext: import("react").Context<{
4
+ getSnapshot: () => DeviceClient;
5
+ getServerSnapshot: () => DeviceClient;
6
+ subscribe(listener: () => void): () => void;
7
+ publish(client: DeviceClient): void;
8
+ } | null>;
9
+ export type DeviceClientProviderProps = {
10
+ platform: DevicePlatform | null;
11
+ options: DeviceConnectionOptions;
12
+ children: ReactNode;
13
+ };
14
+ /** Own one connection while children subscribe to individual client values. */
15
+ export declare function DeviceClientProvider({ platform, options, children }: DeviceClientProviderProps): import("react").JSX.Element;
16
+ /** Subscribe to a client value. Object selections can supply their own equality function. */
17
+ export declare function useDeviceClientSelector<Selection>(selector: (client: DeviceClient) => Selection, isEqual?: (previous: Selection, next: Selection) => boolean): Selection;
18
+ //# sourceMappingURL=DeviceClientProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeviceClientProvider.d.ts","sourceRoot":"","sources":["../src/DeviceClientProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAwD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI7F,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAKrF,eAAO,MAAM,wBAAwB;;;;;SAAgD,CAAC;AAEtF,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IAChC,OAAO,EAAE,uBAAuB,CAAC;IACjC,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,yBAAyB,+BAwB9F;AAED,6FAA6F;AAC7F,wBAAgB,uBAAuB,CAAC,SAAS,EAC/C,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,SAAS,EAC7C,OAAO,GAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,KAAK,OAAmB,GACrE,SAAS,CAcX"}
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useLayoutEffect, useState } from "react";
3
+ import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
4
+ import { createDeviceClientStore } from "./device-client-store.mjs";
5
+ import { useAndroidDeviceClient } from "./useAndroidDevice.mjs";
6
+ import { useIosDeviceClient } from "./useIosDevice.mjs";
7
+ import { NOOP_DEVICE_CLIENT } from "./useNoopDeviceClient.mjs";
8
+ export const DeviceClientStoreContext = /*#__PURE__*/ createContext(null);
9
+ /** Own one connection while children subscribe to individual client values. */ export function DeviceClientProvider({ platform, options, children }) {
10
+ const ios = useIosDeviceClient({
11
+ ...options,
12
+ enabled: platform === "ios" && options.enabled !== false
13
+ });
14
+ const android = useAndroidDeviceClient({
15
+ ...options,
16
+ enabled: platform === "android" && options.enabled !== false
17
+ });
18
+ const client = platform === "ios" ? ios : platform === "android" ? android : NOOP_DEVICE_CLIENT;
19
+ const [store] = useState(()=>{
20
+ // Hydration must start with the same idle media even when WebCodecs is browser-only.
21
+ const serverClient = platform ? {
22
+ ...NOOP_DEVICE_CLIENT,
23
+ platform,
24
+ videoKind: platform === "android" ? "canvas" : "img"
25
+ } : NOOP_DEVICE_CLIENT;
26
+ return createDeviceClientStore(client, serverClient);
27
+ });
28
+ useLayoutEffect(()=>{
29
+ store.publish(client);
30
+ }, [
31
+ store,
32
+ client
33
+ ]);
34
+ return /*#__PURE__*/ _jsx(DeviceClientStoreContext.Provider, {
35
+ value: store,
36
+ children: children
37
+ });
38
+ }
39
+ /** Subscribe to a client value. Object selections can supply their own equality function. */ export function useDeviceClientSelector(selector, isEqual = Object.is) {
40
+ const store = useContext(DeviceClientStoreContext);
41
+ if (!store) {
42
+ throw new Error("useDeviceClientSelector requires a DeviceClientProvider. Wrap this component in a provider to share its device connection.");
43
+ }
44
+ return useSyncExternalStoreWithSelector(store.subscribe, store.getSnapshot, store.getServerSnapshot, selector, isEqual);
45
+ }
46
+
47
+ //# sourceMappingURL=DeviceClientProvider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/DeviceClientProvider.tsx"],"sourcesContent":["import { createContext, useContext, useLayoutEffect, useState, type ReactNode } from \"react\";\nimport { useSyncExternalStoreWithSelector } from \"use-sync-external-store/with-selector\";\n\nimport { createDeviceClientStore, type DeviceClientStore } from \"./device-client-store\";\nimport type { DeviceClient, DeviceConnectionOptions, DevicePlatform } from \"./types\";\nimport { useAndroidDeviceClient } from \"./useAndroidDevice\";\nimport { useIosDeviceClient } from \"./useIosDevice\";\nimport { NOOP_DEVICE_CLIENT } from \"./useNoopDeviceClient\";\n\nexport const DeviceClientStoreContext = createContext<DeviceClientStore | null>(null);\n\nexport type DeviceClientProviderProps = {\n platform: DevicePlatform | null;\n options: DeviceConnectionOptions;\n children: ReactNode;\n};\n\n/** Own one connection while children subscribe to individual client values. */\nexport function DeviceClientProvider({ platform, options, children }: DeviceClientProviderProps) {\n const ios = useIosDeviceClient({\n ...options,\n enabled: platform === \"ios\" && options.enabled !== false,\n });\n const android = useAndroidDeviceClient({\n ...options,\n enabled: platform === \"android\" && options.enabled !== false,\n });\n const client = platform === \"ios\" ? ios : platform === \"android\" ? android : NOOP_DEVICE_CLIENT;\n const [store] = useState(() => {\n // Hydration must start with the same idle media even when WebCodecs is browser-only.\n const serverClient: DeviceClient = platform\n ? { ...NOOP_DEVICE_CLIENT, platform, videoKind: platform === \"android\" ? \"canvas\" : \"img\" }\n : NOOP_DEVICE_CLIENT;\n return createDeviceClientStore(client, serverClient);\n });\n\n useLayoutEffect(() => {\n store.publish(client);\n }, [store, client]);\n return (\n <DeviceClientStoreContext.Provider value={store}>{children}</DeviceClientStoreContext.Provider>\n );\n}\n\n/** Subscribe to a client value. Object selections can supply their own equality function. */\nexport function useDeviceClientSelector<Selection>(\n selector: (client: DeviceClient) => Selection,\n isEqual: (previous: Selection, next: Selection) => boolean = Object.is,\n): Selection {\n const store = useContext(DeviceClientStoreContext);\n if (!store) {\n throw new Error(\n \"useDeviceClientSelector requires a DeviceClientProvider. Wrap this component in a provider to share its device connection.\",\n );\n }\n return useSyncExternalStoreWithSelector(\n store.subscribe,\n store.getSnapshot,\n store.getServerSnapshot,\n selector,\n isEqual,\n );\n}\n"],"names":["createContext","useContext","useLayoutEffect","useState","useSyncExternalStoreWithSelector","createDeviceClientStore","useAndroidDeviceClient","useIosDeviceClient","NOOP_DEVICE_CLIENT","DeviceClientStoreContext","DeviceClientProvider","platform","options","children","ios","enabled","android","client","store","serverClient","videoKind","publish","Provider","value","useDeviceClientSelector","selector","isEqual","Object","is","Error","subscribe","getSnapshot","getServerSnapshot"],"mappings":";AAAA,SAASA,aAAa,EAAEC,UAAU,EAAEC,eAAe,EAAEC,QAAQ,QAAwB,QAAQ;AAC7F,SAASC,gCAAgC,QAAQ,wCAAwC;AAEzF,SAASC,uBAAuB,QAAgC,4BAAwB;AAExF,SAASC,sBAAsB,QAAQ,yBAAqB;AAC5D,SAASC,kBAAkB,QAAQ,qBAAiB;AACpD,SAASC,kBAAkB,QAAQ,4BAAwB;AAE3D,OAAO,MAAMC,yCAA2BT,cAAwC,MAAM;AAQtF,6EAA6E,GAC7E,OAAO,SAASU,qBAAqB,EAAEC,QAAQ,EAAEC,OAAO,EAAEC,QAAQ,EAA6B;IAC7F,MAAMC,MAAMP,mBAAmB;QAC7B,GAAGK,OAAO;QACVG,SAASJ,aAAa,SAASC,QAAQG,OAAO,KAAK;IACrD;IACA,MAAMC,UAAUV,uBAAuB;QACrC,GAAGM,OAAO;QACVG,SAASJ,aAAa,aAAaC,QAAQG,OAAO,KAAK;IACzD;IACA,MAAME,SAASN,aAAa,QAAQG,MAAMH,aAAa,YAAYK,UAAUR;IAC7E,MAAM,CAACU,MAAM,GAAGf,SAAS;QACvB,qFAAqF;QACrF,MAAMgB,eAA6BR,WAC/B;YAAE,GAAGH,kBAAkB;YAAEG;YAAUS,WAAWT,aAAa,YAAY,WAAW;QAAM,IACxFH;QACJ,OAAOH,wBAAwBY,QAAQE;IACzC;IAEAjB,gBAAgB;QACdgB,MAAMG,OAAO,CAACJ;IAChB,GAAG;QAACC;QAAOD;KAAO;IAClB,qBACE,KAACR,yBAAyBa,QAAQ;QAACC,OAAOL;kBAAQL;;AAEtD;AAEA,2FAA2F,GAC3F,OAAO,SAASW,wBACdC,QAA6C,EAC7CC,UAA6DC,OAAOC,EAAE;IAEtE,MAAMV,QAAQjB,WAAWQ;IACzB,IAAI,CAACS,OAAO;QACV,MAAM,IAAIW,MACR;IAEJ;IACA,OAAOzB,iCACLc,MAAMY,SAAS,EACfZ,MAAMa,WAAW,EACjBb,MAAMc,iBAAiB,EACvBP,UACAC;AAEJ"}
@@ -1,5 +1,5 @@
1
1
  import { type CSSProperties } from 'react';
2
- import { type DeviceScreenProps } from './types';
2
+ import { type DeviceScreenInputProps } from './types';
3
3
  export declare const DEVICE_SCREEN_SURFACE_LAYOUT_STYLE: CSSProperties;
4
4
  export declare const DEVICE_SCREEN_STATUS_LAYOUT_STYLE: CSSProperties;
5
5
  /**
@@ -9,8 +9,8 @@ export declare const DEVICE_SCREEN_STATUS_LAYOUT_STYLE: CSSProperties;
9
9
  * cover or blank it — that is what keeps an Android capture-source switch or
10
10
  * a brief socket drop from flashing black.
11
11
  */
12
- export declare function deviceScreenPresentsMedia(status: DeviceScreenProps['client']['status']): boolean;
13
- export declare function deviceScreenSurfaceStyle(status: DeviceScreenProps['client']['status']): CSSProperties;
12
+ export declare function deviceScreenPresentsMedia(status: DeviceScreenInputProps['client']['status']): boolean;
13
+ export declare function deviceScreenSurfaceStyle(status: DeviceScreenInputProps['client']['status']): CSSProperties;
14
14
  /** Keep media geometry tied to the same synchronous size container as the frame. */
15
15
  export declare function deviceScreenMediaStyle(rotation: number): CSSProperties;
16
16
  /**
@@ -28,5 +28,5 @@ export declare function deviceScreenMediaStyle(rotation: number): CSSProperties;
28
28
  * non-portrait device, only the video element is CSS-rotated — the input
29
29
  * overlay stays display-aligned.
30
30
  */
31
- export declare function DeviceScreen({ client, borderRadius, squircle, agentInteraction, }: DeviceScreenProps): import("react").JSX.Element;
31
+ export declare function DeviceScreen({ client, borderRadius, squircle, agentInteraction, }: DeviceScreenInputProps): import("react").JSX.Element;
32
32
  //# sourceMappingURL=DeviceScreen.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceScreen.d.ts","sourceRoot":"","sources":["../src/DeviceScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAOnB,MAAM,OAAO,CAAC;AAMf,OAAO,EACL,KAAK,iBAAiB,EAGvB,MAAM,SAAS,CAAC;AAUjB,eAAO,MAAM,kCAAkC,EAAE,aAKhD,CAAC;AAEF,eAAO,MAAM,iCAAiC,EAAE,aAa/C,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAEhG;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAC5C,aAAa,CAKf;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CA8BtE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,gBAAgB,GACjB,EAAE,iBAAiB,+BAyUnB"}
1
+ {"version":3,"file":"DeviceScreen.d.ts","sourceRoot":"","sources":["../src/DeviceScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAOnB,MAAM,OAAO,CAAC;AAMf,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,SAAS,CAAC;AAUjB,eAAO,MAAM,kCAAkC,EAAE,aAKhD,CAAC;AAEF,eAAO,MAAM,iCAAiC,EAAE,aAa/C,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAErG;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,GACjD,aAAa,CAKf;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CA8BtE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,gBAAgB,GACjB,EAAE,sBAAsB,+BAyUxB"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/DeviceScreen.tsx"],"sourcesContent":["import {\n type CSSProperties,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react';\n\nimport { streamGeometry } from './orientation';\nimport { wheelDeltaToPixels } from './scroll-wheel';\nimport { AgentInteractionIndicator } from './AgentInteractionIndicator';\nimport { TouchIndicator } from './TouchIndicator';\nimport {\n type DeviceScreenProps,\n type KeyboardInput,\n type MultiTouchSample,\n} from './types';\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\n\ntype Point = { x: number; y: number };\n\n// Custom round cursor matching serve-sim's finger dot, so taps feel placed.\nconst FINGER_CURSOR =\n `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='12' cy='12' r='9' fill='rgba(255,255,255,0.45)' stroke='rgba(0,0,0,0.55)' stroke-width='1.25'/%3E%3C/svg%3E\") 12 12, pointer`;\n\nexport const DEVICE_SCREEN_SURFACE_LAYOUT_STYLE: CSSProperties = {\n position: 'absolute',\n inset: 0,\n containerType: 'size',\n overflow: 'hidden',\n};\n\nexport const DEVICE_SCREEN_STATUS_LAYOUT_STYLE: CSSProperties = {\n position: 'absolute',\n inset: 0,\n boxSizing: 'border-box',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: 24,\n textAlign: 'center',\n pointerEvents: 'none',\n backgroundColor: 'rgba(0, 0, 0, 0.55)',\n fontSize: 13,\n fontFamily: 'var(--expo-font-mono)',\n};\n\n/**\n * Whether the surface should present the media element as-is. While\n * `reconnecting`, the element still holds the last frame (a canvas keeps its\n * pixels, a video keeps the final frame of an ended track), so nothing must\n * cover or blank it — that is what keeps an Android capture-source switch or\n * a brief socket drop from flashing black.\n */\nexport function deviceScreenPresentsMedia(status: DeviceScreenProps['client']['status']): boolean {\n return status === 'streaming' || status === 'reconnecting';\n}\n\nexport function deviceScreenSurfaceStyle(\n status: DeviceScreenProps['client']['status']\n): CSSProperties {\n return {\n ...DEVICE_SCREEN_SURFACE_LAYOUT_STYLE,\n ...(deviceScreenPresentsMedia(status) ? {} : { backgroundColor: '#000' }),\n };\n}\n\n/** Keep media geometry tied to the same synchronous size container as the frame. */\nexport function deviceScreenMediaStyle(rotation: number): CSSProperties {\n const rotatesSideways = Math.abs(rotation) === 90;\n const style: CSSProperties =\n rotation === 0\n ? {\n display: 'block',\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n objectFit: 'cover',\n }\n : {\n display: 'block',\n position: 'absolute',\n top: '50%',\n left: '50%',\n width: rotatesSideways ? '100cqh' : '100%',\n height: rotatesSideways ? '100cqw' : '100%',\n transform: `translate(-50%, -50%) rotate(${rotation}deg)`,\n transformOrigin: 'center center',\n objectFit: 'cover',\n };\n\n return {\n ...style,\n userSelect: 'none',\n WebkitUserSelect: 'none',\n pointerEvents: 'none',\n };\n}\n\n/**\n * Shared renderer for a live {@link DeviceClient}, used in place of the static\n * `<img>` inside {@link PhoneFrame}. It paints whatever element the active\n * implementation asks for (`<canvas>` for H.264, `<img>` for MJPEG, or\n * `<video>` for WebRTC) and forwards normalized pointer input.\n *\n * Input is measured in *display* space (the hook remaps to the device's raw\n * frame for the current orientation). Single-finger drags go through\n * `client.sendTouch`; two-finger pinch/pan (real touch, or Alt-drag with a\n * mouse) goes through `client.sendMultiTouch` when the backend supports it, and\n * mouse-wheel / trackpad scrolling goes through `client.sendScroll` as a native\n * scroll when the backend supports it. When the stream is rotated for a\n * non-portrait device, only the video element is CSS-rotated — the input\n * overlay stays display-aligned.\n */\nexport function DeviceScreen({\n client,\n borderRadius,\n squircle,\n agentInteraction,\n}: DeviceScreenProps) {\n const {\n videoKind,\n attachVideo,\n sendTouch,\n sendMultiTouch,\n sendScroll,\n sendKey,\n screen,\n status,\n error,\n } = client;\n const canMulti = !!sendMultiTouch;\n\n const surfaceRef = useRef<HTMLDivElement | null>(null);\n\n // A focused device surface owns physical keyboard input. Track held keys so\n // modifiers never remain stuck in the simulator if focus/window visibility is\n // lost before the browser delivers their keyup.\n const pressedKeysRef = useRef(new Map<string, KeyboardInput>());\n const releasePressedKeys = useCallback(() => {\n for (const input of pressedKeysRef.current.values()) {\n sendKey({ ...input, phase: 'up', repeat: false });\n }\n pressedKeysRef.current.clear();\n }, [sendKey]);\n\n useEffect(() => {\n const onWindowBlur = () => releasePressedKeys();\n const onVisibilityChange = () => {\n if (document.hidden) releasePressedKeys();\n };\n window.addEventListener('blur', onWindowBlur);\n document.addEventListener('visibilitychange', onVisibilityChange);\n return () => {\n window.removeEventListener('blur', onWindowBlur);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n releasePressedKeys();\n };\n }, [releasePressedKeys]);\n\n const keyboardInputFrom = (\n event: ReactKeyboardEvent<HTMLDivElement>,\n phase: KeyboardInput['phase'],\n ): KeyboardInput => ({\n phase,\n code: event.code,\n key: event.key,\n repeat: event.repeat,\n });\n\n const onKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n // Keep the remote surface escapable for keyboard-only users while leaving a\n // plain Escape available to the simulator/emulator.\n if (event.key === 'Escape' && event.shiftKey) {\n event.preventDefault();\n releasePressedKeys();\n event.currentTarget.blur();\n return;\n }\n if (event.nativeEvent.isComposing) return;\n const input = keyboardInputFrom(event, 'down');\n if (!sendKey(input)) return;\n event.preventDefault();\n pressedKeysRef.current.set(event.code || event.key, input);\n };\n\n const onKeyUp = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n const keyId = event.code || event.key;\n const wasPressed = pressedKeysRef.current.delete(keyId);\n const handled = sendKey(keyboardInputFrom(event, 'up'));\n if (wasPressed || handled) event.preventDefault();\n };\n\n // ── pointer state ──\n const pointersRef = useRef(new Map<number, Point>());\n const modeRef = useRef<'none' | 'single' | 'alt' | 'two'>('none');\n const singleIdRef = useRef<number | null>(null);\n const altShiftRef = useRef(false);\n const panOffsetRef = useRef<Point>({ x: 0, y: 0 });\n const [fingers, setFingers] = useState<{ a: Point; b: Point } | null>(null);\n\n // rAF move throttle (latest-wins) for both single and multi.\n const pendingRef = useRef<{ single?: Point; multi?: MultiTouchSample }>({});\n const rafRef = useRef(0);\n const flush = () => {\n rafRef.current = 0;\n const pend = pendingRef.current;\n pendingRef.current = {};\n if (pend.single) sendTouch({ phase: 'move', ...pend.single });\n if (pend.multi) {\n sendMultiTouch?.(pend.multi);\n setFingers({ a: pend.multi.a, b: pend.multi.b });\n }\n };\n const queueFrame = () => {\n if (!rafRef.current) rafRef.current = requestAnimationFrame(flush);\n };\n\n const pointFrom = (clientX: number, clientY: number): Point | null => {\n const rect = surfaceRef.current?.getBoundingClientRect();\n if (!rect || rect.width === 0 || rect.height === 0) return null;\n return { x: clamp01((clientX - rect.left) / rect.width), y: clamp01((clientY - rect.top) / rect.height) };\n };\n\n // Second finger position for Alt-drag: mirror around center (pinch) or a\n // locked offset (pan, with Shift) — matches serve-sim.\n const altSecondFinger = (p: Point): Point =>\n altShiftRef.current\n ? { x: clamp01(p.x + panOffsetRef.current.x), y: clamp01(p.y + panOffsetRef.current.y) }\n : { x: 1 - p.x, y: 1 - p.y };\n\n const endMulti = (a: Point, b: Point) => {\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = 0;\n }\n pendingRef.current = {};\n sendMultiTouch?.({ phase: 'end', a, b });\n setFingers(null);\n };\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (event.pointerType === 'mouse' && event.button !== 0) return;\n const p = pointFrom(event.clientX, event.clientY);\n if (!p) return;\n event.preventDefault();\n surfaceRef.current?.focus({ preventScroll: true });\n try {\n surfaceRef.current?.setPointerCapture(event.pointerId);\n } catch {}\n pointersRef.current.set(event.pointerId, p);\n\n // Alt-drag with a mouse → synthetic pinch/pan.\n if (canMulti && event.pointerType === 'mouse' && event.altKey && modeRef.current === 'none') {\n modeRef.current = 'alt';\n altShiftRef.current = event.shiftKey;\n panOffsetRef.current = { x: 1 - 2 * p.x, y: 1 - 2 * p.y };\n const b = altSecondFinger(p);\n setFingers({ a: p, b });\n sendMultiTouch?.({ phase: 'begin', a: p, b });\n return;\n }\n\n // Second finger down → real two-finger gesture.\n if (canMulti && pointersRef.current.size >= 2 && modeRef.current !== 'alt') {\n if (modeRef.current === 'single') {\n sendTouch({ phase: 'end', ...p });\n singleIdRef.current = null;\n }\n const [a, b] = [...pointersRef.current.values()];\n modeRef.current = 'two';\n setFingers({ a, b });\n sendMultiTouch?.({ phase: 'begin', a, b });\n return;\n }\n\n if (modeRef.current === 'none') {\n modeRef.current = 'single';\n singleIdRef.current = event.pointerId;\n sendTouch({ phase: 'begin', ...p });\n }\n };\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n const stored = pointersRef.current.get(event.pointerId);\n if (!stored) return;\n const native = event.nativeEvent;\n const coalesced =\n typeof native.getCoalescedEvents === 'function' ? native.getCoalescedEvents() : null;\n const last = coalesced && coalesced.length > 0 ? coalesced[coalesced.length - 1] : event;\n const p = pointFrom(last.clientX, last.clientY);\n if (!p) return;\n event.preventDefault();\n pointersRef.current.set(event.pointerId, p);\n\n if (modeRef.current === 'alt') {\n pendingRef.current.multi = { phase: 'move', a: p, b: altSecondFinger(p) };\n queueFrame();\n } else if (modeRef.current === 'two') {\n const [a, b] = [...pointersRef.current.values()];\n pendingRef.current.multi = { phase: 'move', a, b };\n queueFrame();\n } else if (modeRef.current === 'single' && event.pointerId === singleIdRef.current) {\n pendingRef.current.single = p;\n queueFrame();\n }\n };\n\n const onPointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {\n const stored = pointersRef.current.get(event.pointerId);\n if (!stored) return;\n event.preventDefault();\n const p = pointFrom(event.clientX, event.clientY) ?? stored;\n try {\n surfaceRef.current?.releasePointerCapture(event.pointerId);\n } catch {}\n\n if (modeRef.current === 'alt') {\n endMulti(p, altSecondFinger(p));\n modeRef.current = 'none';\n pointersRef.current.clear();\n return;\n }\n if (modeRef.current === 'two') {\n const [a, b] = [...pointersRef.current.values()];\n endMulti(a ?? p, b ?? p);\n modeRef.current = 'none';\n pointersRef.current.clear();\n return;\n }\n if (modeRef.current === 'single' && event.pointerId === singleIdRef.current) {\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = 0;\n }\n pendingRef.current = {};\n sendTouch({ phase: 'end', ...p });\n modeRef.current = 'none';\n singleIdRef.current = null;\n }\n pointersRef.current.delete(event.pointerId);\n };\n\n // Scroll-to-pan: wheel/trackpad scrolling over the device is forwarded as a\n // native scroll (see `client.sendScroll`) so iOS pans content exactly as it\n // would for a physical wheel. A non-passive listener, because React's\n // `onWheel` cannot preventDefault the page scroll. Never fights an\n // in-progress drag on the same surface.\n useEffect(() => {\n const el = surfaceRef.current;\n if (!el || !sendScroll) return;\n const onWheel = (event: WheelEvent) => {\n if (modeRef.current !== 'none') return;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n const dxPx = wheelDeltaToPixels(event.deltaX, event.deltaMode, rect.width);\n const dyPx = wheelDeltaToPixels(event.deltaY, event.deltaMode, rect.height);\n if (dxPx === 0 && dyPx === 0) return;\n // Anchor the pan under the cursor, clamped to the display; express the\n // delta as a fraction of the rendered display so the server can rescale\n // to device pixels. Browser wheel deltas already reflect the natural-\n // scroll setting, so the sign passes straight through.\n sendScroll({\n dx: dxPx / rect.width,\n dy: dyPx / rect.height,\n x: clamp01((event.clientX - rect.left) / rect.width),\n y: clamp01((event.clientY - rect.top) / rect.height),\n });\n event.preventDefault();\n event.stopPropagation();\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => el.removeEventListener('wheel', onWheel);\n }, [sendScroll]);\n\n // ── display geometry (rotation for non-portrait devices) ──\n const geometry = streamGeometry(screen);\n const rotation = geometry.rotationDegrees;\n\n const surfaceStyle: CSSProperties = {\n ...deviceScreenSurfaceStyle(status),\n borderRadius,\n ...(squircle ? ({ cornerShape: 'superellipse(1.3)' } as Record<string, unknown>) : {}),\n };\n\n const mediaStyle = deviceScreenMediaStyle(rotation);\n\n return (\n <div style={surfaceStyle}>\n {videoKind === 'canvas' ? (\n <canvas ref={attachVideo} style={mediaStyle} />\n ) : videoKind === 'video' ? (\n <video ref={attachVideo} autoPlay muted playsInline style={mediaStyle} />\n ) : (\n <img ref={attachVideo} alt=\"Device screen\" draggable={false} style={mediaStyle} />\n )}\n\n {/* Input overlay: display-aligned, captures all pointer events. */}\n <div\n ref={surfaceRef}\n role=\"application\"\n aria-label=\"Interactive device screen. Focus to control it with your keyboard. Press Shift and Escape to stop keyboard control.\"\n tabIndex={0}\n style={{\n position: 'absolute',\n inset: 0,\n cursor: FINGER_CURSOR,\n touchAction: 'none',\n borderRadius,\n outline: 'none',\n }}\n onBlur={() => {\n releasePressedKeys();\n }}\n onKeyDown={onKeyDown}\n onKeyUp={onKeyUp}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n onContextMenu={(event) => event.preventDefault()}\n />\n\n {/* Two-finger indicator dots. */}\n {fingers && (\n <>\n <TouchIndicator point={fingers.a} />\n <TouchIndicator point={fingers.b} />\n </>\n )}\n\n <AgentInteractionIndicator interaction={agentInteraction} />\n\n {!deviceScreenPresentsMedia(status) && (\n <div\n style={{\n ...DEVICE_SCREEN_STATUS_LAYOUT_STYLE,\n color: status === 'error' ? '#fca5a5' : 'rgba(255, 255, 255, 0.7)',\n }}>\n {status === 'error'\n ? (error ?? 'Disconnected')\n : status === 'connecting'\n ? 'Connecting…'\n : 'Not connected'}\n </div>\n )}\n </div>\n );\n}\n"],"names":["useCallback","useEffect","useRef","useState","streamGeometry","wheelDeltaToPixels","AgentInteractionIndicator","TouchIndicator","clamp01","n","Math","min","max","FINGER_CURSOR","DEVICE_SCREEN_SURFACE_LAYOUT_STYLE","position","inset","containerType","overflow","DEVICE_SCREEN_STATUS_LAYOUT_STYLE","boxSizing","display","alignItems","justifyContent","padding","textAlign","pointerEvents","backgroundColor","fontSize","fontFamily","deviceScreenPresentsMedia","status","deviceScreenSurfaceStyle","deviceScreenMediaStyle","rotation","rotatesSideways","abs","style","width","height","objectFit","top","left","transform","transformOrigin","userSelect","WebkitUserSelect","DeviceScreen","client","borderRadius","squircle","agentInteraction","videoKind","attachVideo","sendTouch","sendMultiTouch","sendScroll","sendKey","screen","error","canMulti","surfaceRef","pressedKeysRef","Map","releasePressedKeys","input","current","values","phase","repeat","clear","onWindowBlur","onVisibilityChange","document","hidden","window","addEventListener","removeEventListener","keyboardInputFrom","event","code","key","onKeyDown","shiftKey","preventDefault","currentTarget","blur","nativeEvent","isComposing","set","onKeyUp","keyId","wasPressed","delete","handled","pointersRef","modeRef","singleIdRef","altShiftRef","panOffsetRef","x","y","fingers","setFingers","pendingRef","rafRef","flush","pend","single","multi","a","b","queueFrame","requestAnimationFrame","pointFrom","clientX","clientY","rect","getBoundingClientRect","altSecondFinger","p","endMulti","cancelAnimationFrame","onPointerDown","pointerType","button","focus","preventScroll","setPointerCapture","pointerId","altKey","size","onPointerMove","stored","get","native","coalesced","getCoalescedEvents","last","length","onPointerUp","releasePointerCapture","el","onWheel","dxPx","deltaX","deltaMode","dyPx","deltaY","dx","dy","stopPropagation","passive","geometry","rotationDegrees","surfaceStyle","cornerShape","mediaStyle","div","canvas","ref","video","autoPlay","muted","playsInline","img","alt","draggable","role","aria-label","tabIndex","cursor","touchAction","outline","onBlur","onPointerCancel","onContextMenu","point","interaction","color"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/DeviceScreen.tsx"],"sourcesContent":["import {\n type CSSProperties,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react';\n\nimport { streamGeometry } from './orientation';\nimport { wheelDeltaToPixels } from './scroll-wheel';\nimport { AgentInteractionIndicator } from './AgentInteractionIndicator';\nimport { TouchIndicator } from './TouchIndicator';\nimport {\n type DeviceScreenInputProps,\n type KeyboardInput,\n type MultiTouchSample,\n} from './types';\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\n\ntype Point = { x: number; y: number };\n\n// Custom round cursor matching serve-sim's finger dot, so taps feel placed.\nconst FINGER_CURSOR =\n `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='12' cy='12' r='9' fill='rgba(255,255,255,0.45)' stroke='rgba(0,0,0,0.55)' stroke-width='1.25'/%3E%3C/svg%3E\") 12 12, pointer`;\n\nexport const DEVICE_SCREEN_SURFACE_LAYOUT_STYLE: CSSProperties = {\n position: 'absolute',\n inset: 0,\n containerType: 'size',\n overflow: 'hidden',\n};\n\nexport const DEVICE_SCREEN_STATUS_LAYOUT_STYLE: CSSProperties = {\n position: 'absolute',\n inset: 0,\n boxSizing: 'border-box',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: 24,\n textAlign: 'center',\n pointerEvents: 'none',\n backgroundColor: 'rgba(0, 0, 0, 0.55)',\n fontSize: 13,\n fontFamily: 'var(--expo-font-mono)',\n};\n\n/**\n * Whether the surface should present the media element as-is. While\n * `reconnecting`, the element still holds the last frame (a canvas keeps its\n * pixels, a video keeps the final frame of an ended track), so nothing must\n * cover or blank it — that is what keeps an Android capture-source switch or\n * a brief socket drop from flashing black.\n */\nexport function deviceScreenPresentsMedia(status: DeviceScreenInputProps['client']['status']): boolean {\n return status === 'streaming' || status === 'reconnecting';\n}\n\nexport function deviceScreenSurfaceStyle(\n status: DeviceScreenInputProps['client']['status']\n): CSSProperties {\n return {\n ...DEVICE_SCREEN_SURFACE_LAYOUT_STYLE,\n ...(deviceScreenPresentsMedia(status) ? {} : { backgroundColor: '#000' }),\n };\n}\n\n/** Keep media geometry tied to the same synchronous size container as the frame. */\nexport function deviceScreenMediaStyle(rotation: number): CSSProperties {\n const rotatesSideways = Math.abs(rotation) === 90;\n const style: CSSProperties =\n rotation === 0\n ? {\n display: 'block',\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n objectFit: 'cover',\n }\n : {\n display: 'block',\n position: 'absolute',\n top: '50%',\n left: '50%',\n width: rotatesSideways ? '100cqh' : '100%',\n height: rotatesSideways ? '100cqw' : '100%',\n transform: `translate(-50%, -50%) rotate(${rotation}deg)`,\n transformOrigin: 'center center',\n objectFit: 'cover',\n };\n\n return {\n ...style,\n userSelect: 'none',\n WebkitUserSelect: 'none',\n pointerEvents: 'none',\n };\n}\n\n/**\n * Shared renderer for a live {@link DeviceClient}, used in place of the static\n * `<img>` inside {@link PhoneFrame}. It paints whatever element the active\n * implementation asks for (`<canvas>` for H.264, `<img>` for MJPEG, or\n * `<video>` for WebRTC) and forwards normalized pointer input.\n *\n * Input is measured in *display* space (the hook remaps to the device's raw\n * frame for the current orientation). Single-finger drags go through\n * `client.sendTouch`; two-finger pinch/pan (real touch, or Alt-drag with a\n * mouse) goes through `client.sendMultiTouch` when the backend supports it, and\n * mouse-wheel / trackpad scrolling goes through `client.sendScroll` as a native\n * scroll when the backend supports it. When the stream is rotated for a\n * non-portrait device, only the video element is CSS-rotated — the input\n * overlay stays display-aligned.\n */\nexport function DeviceScreen({\n client,\n borderRadius,\n squircle,\n agentInteraction,\n}: DeviceScreenInputProps) {\n const {\n videoKind,\n attachVideo,\n sendTouch,\n sendMultiTouch,\n sendScroll,\n sendKey,\n screen,\n status,\n error,\n } = client;\n const canMulti = !!sendMultiTouch;\n\n const surfaceRef = useRef<HTMLDivElement | null>(null);\n\n // A focused device surface owns physical keyboard input. Track held keys so\n // modifiers never remain stuck in the simulator if focus/window visibility is\n // lost before the browser delivers their keyup.\n const pressedKeysRef = useRef(new Map<string, KeyboardInput>());\n const releasePressedKeys = useCallback(() => {\n for (const input of pressedKeysRef.current.values()) {\n sendKey({ ...input, phase: 'up', repeat: false });\n }\n pressedKeysRef.current.clear();\n }, [sendKey]);\n\n useEffect(() => {\n const onWindowBlur = () => releasePressedKeys();\n const onVisibilityChange = () => {\n if (document.hidden) releasePressedKeys();\n };\n window.addEventListener('blur', onWindowBlur);\n document.addEventListener('visibilitychange', onVisibilityChange);\n return () => {\n window.removeEventListener('blur', onWindowBlur);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n releasePressedKeys();\n };\n }, [releasePressedKeys]);\n\n const keyboardInputFrom = (\n event: ReactKeyboardEvent<HTMLDivElement>,\n phase: KeyboardInput['phase'],\n ): KeyboardInput => ({\n phase,\n code: event.code,\n key: event.key,\n repeat: event.repeat,\n });\n\n const onKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n // Keep the remote surface escapable for keyboard-only users while leaving a\n // plain Escape available to the simulator/emulator.\n if (event.key === 'Escape' && event.shiftKey) {\n event.preventDefault();\n releasePressedKeys();\n event.currentTarget.blur();\n return;\n }\n if (event.nativeEvent.isComposing) return;\n const input = keyboardInputFrom(event, 'down');\n if (!sendKey(input)) return;\n event.preventDefault();\n pressedKeysRef.current.set(event.code || event.key, input);\n };\n\n const onKeyUp = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n const keyId = event.code || event.key;\n const wasPressed = pressedKeysRef.current.delete(keyId);\n const handled = sendKey(keyboardInputFrom(event, 'up'));\n if (wasPressed || handled) event.preventDefault();\n };\n\n // ── pointer state ──\n const pointersRef = useRef(new Map<number, Point>());\n const modeRef = useRef<'none' | 'single' | 'alt' | 'two'>('none');\n const singleIdRef = useRef<number | null>(null);\n const altShiftRef = useRef(false);\n const panOffsetRef = useRef<Point>({ x: 0, y: 0 });\n const [fingers, setFingers] = useState<{ a: Point; b: Point } | null>(null);\n\n // rAF move throttle (latest-wins) for both single and multi.\n const pendingRef = useRef<{ single?: Point; multi?: MultiTouchSample }>({});\n const rafRef = useRef(0);\n const flush = () => {\n rafRef.current = 0;\n const pend = pendingRef.current;\n pendingRef.current = {};\n if (pend.single) sendTouch({ phase: 'move', ...pend.single });\n if (pend.multi) {\n sendMultiTouch?.(pend.multi);\n setFingers({ a: pend.multi.a, b: pend.multi.b });\n }\n };\n const queueFrame = () => {\n if (!rafRef.current) rafRef.current = requestAnimationFrame(flush);\n };\n\n const pointFrom = (clientX: number, clientY: number): Point | null => {\n const rect = surfaceRef.current?.getBoundingClientRect();\n if (!rect || rect.width === 0 || rect.height === 0) return null;\n return { x: clamp01((clientX - rect.left) / rect.width), y: clamp01((clientY - rect.top) / rect.height) };\n };\n\n // Second finger position for Alt-drag: mirror around center (pinch) or a\n // locked offset (pan, with Shift) — matches serve-sim.\n const altSecondFinger = (p: Point): Point =>\n altShiftRef.current\n ? { x: clamp01(p.x + panOffsetRef.current.x), y: clamp01(p.y + panOffsetRef.current.y) }\n : { x: 1 - p.x, y: 1 - p.y };\n\n const endMulti = (a: Point, b: Point) => {\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = 0;\n }\n pendingRef.current = {};\n sendMultiTouch?.({ phase: 'end', a, b });\n setFingers(null);\n };\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (event.pointerType === 'mouse' && event.button !== 0) return;\n const p = pointFrom(event.clientX, event.clientY);\n if (!p) return;\n event.preventDefault();\n surfaceRef.current?.focus({ preventScroll: true });\n try {\n surfaceRef.current?.setPointerCapture(event.pointerId);\n } catch {}\n pointersRef.current.set(event.pointerId, p);\n\n // Alt-drag with a mouse → synthetic pinch/pan.\n if (canMulti && event.pointerType === 'mouse' && event.altKey && modeRef.current === 'none') {\n modeRef.current = 'alt';\n altShiftRef.current = event.shiftKey;\n panOffsetRef.current = { x: 1 - 2 * p.x, y: 1 - 2 * p.y };\n const b = altSecondFinger(p);\n setFingers({ a: p, b });\n sendMultiTouch?.({ phase: 'begin', a: p, b });\n return;\n }\n\n // Second finger down → real two-finger gesture.\n if (canMulti && pointersRef.current.size >= 2 && modeRef.current !== 'alt') {\n if (modeRef.current === 'single') {\n sendTouch({ phase: 'end', ...p });\n singleIdRef.current = null;\n }\n const [a, b] = [...pointersRef.current.values()];\n modeRef.current = 'two';\n setFingers({ a, b });\n sendMultiTouch?.({ phase: 'begin', a, b });\n return;\n }\n\n if (modeRef.current === 'none') {\n modeRef.current = 'single';\n singleIdRef.current = event.pointerId;\n sendTouch({ phase: 'begin', ...p });\n }\n };\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n const stored = pointersRef.current.get(event.pointerId);\n if (!stored) return;\n const native = event.nativeEvent;\n const coalesced =\n typeof native.getCoalescedEvents === 'function' ? native.getCoalescedEvents() : null;\n const last = coalesced && coalesced.length > 0 ? coalesced[coalesced.length - 1] : event;\n const p = pointFrom(last.clientX, last.clientY);\n if (!p) return;\n event.preventDefault();\n pointersRef.current.set(event.pointerId, p);\n\n if (modeRef.current === 'alt') {\n pendingRef.current.multi = { phase: 'move', a: p, b: altSecondFinger(p) };\n queueFrame();\n } else if (modeRef.current === 'two') {\n const [a, b] = [...pointersRef.current.values()];\n pendingRef.current.multi = { phase: 'move', a, b };\n queueFrame();\n } else if (modeRef.current === 'single' && event.pointerId === singleIdRef.current) {\n pendingRef.current.single = p;\n queueFrame();\n }\n };\n\n const onPointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {\n const stored = pointersRef.current.get(event.pointerId);\n if (!stored) return;\n event.preventDefault();\n const p = pointFrom(event.clientX, event.clientY) ?? stored;\n try {\n surfaceRef.current?.releasePointerCapture(event.pointerId);\n } catch {}\n\n if (modeRef.current === 'alt') {\n endMulti(p, altSecondFinger(p));\n modeRef.current = 'none';\n pointersRef.current.clear();\n return;\n }\n if (modeRef.current === 'two') {\n const [a, b] = [...pointersRef.current.values()];\n endMulti(a ?? p, b ?? p);\n modeRef.current = 'none';\n pointersRef.current.clear();\n return;\n }\n if (modeRef.current === 'single' && event.pointerId === singleIdRef.current) {\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = 0;\n }\n pendingRef.current = {};\n sendTouch({ phase: 'end', ...p });\n modeRef.current = 'none';\n singleIdRef.current = null;\n }\n pointersRef.current.delete(event.pointerId);\n };\n\n // Scroll-to-pan: wheel/trackpad scrolling over the device is forwarded as a\n // native scroll (see `client.sendScroll`) so iOS pans content exactly as it\n // would for a physical wheel. A non-passive listener, because React's\n // `onWheel` cannot preventDefault the page scroll. Never fights an\n // in-progress drag on the same surface.\n useEffect(() => {\n const el = surfaceRef.current;\n if (!el || !sendScroll) return;\n const onWheel = (event: WheelEvent) => {\n if (modeRef.current !== 'none') return;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n const dxPx = wheelDeltaToPixels(event.deltaX, event.deltaMode, rect.width);\n const dyPx = wheelDeltaToPixels(event.deltaY, event.deltaMode, rect.height);\n if (dxPx === 0 && dyPx === 0) return;\n // Anchor the pan under the cursor, clamped to the display; express the\n // delta as a fraction of the rendered display so the server can rescale\n // to device pixels. Browser wheel deltas already reflect the natural-\n // scroll setting, so the sign passes straight through.\n sendScroll({\n dx: dxPx / rect.width,\n dy: dyPx / rect.height,\n x: clamp01((event.clientX - rect.left) / rect.width),\n y: clamp01((event.clientY - rect.top) / rect.height),\n });\n event.preventDefault();\n event.stopPropagation();\n };\n el.addEventListener('wheel', onWheel, { passive: false });\n return () => el.removeEventListener('wheel', onWheel);\n }, [sendScroll]);\n\n // ── display geometry (rotation for non-portrait devices) ──\n const geometry = streamGeometry(screen);\n const rotation = geometry.rotationDegrees;\n\n const surfaceStyle: CSSProperties = {\n ...deviceScreenSurfaceStyle(status),\n borderRadius,\n ...(squircle ? ({ cornerShape: 'superellipse(1.3)' } as Record<string, unknown>) : {}),\n };\n\n const mediaStyle = deviceScreenMediaStyle(rotation);\n\n return (\n <div style={surfaceStyle}>\n {videoKind === 'canvas' ? (\n <canvas ref={attachVideo} style={mediaStyle} />\n ) : videoKind === 'video' ? (\n <video ref={attachVideo} autoPlay muted playsInline style={mediaStyle} />\n ) : (\n <img ref={attachVideo} alt=\"Device screen\" draggable={false} style={mediaStyle} />\n )}\n\n {/* Input overlay: display-aligned, captures all pointer events. */}\n <div\n ref={surfaceRef}\n role=\"application\"\n aria-label=\"Interactive device screen. Focus to control it with your keyboard. Press Shift and Escape to stop keyboard control.\"\n tabIndex={0}\n style={{\n position: 'absolute',\n inset: 0,\n cursor: FINGER_CURSOR,\n touchAction: 'none',\n borderRadius,\n outline: 'none',\n }}\n onBlur={() => {\n releasePressedKeys();\n }}\n onKeyDown={onKeyDown}\n onKeyUp={onKeyUp}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n onContextMenu={(event) => event.preventDefault()}\n />\n\n {/* Two-finger indicator dots. */}\n {fingers && (\n <>\n <TouchIndicator point={fingers.a} />\n <TouchIndicator point={fingers.b} />\n </>\n )}\n\n <AgentInteractionIndicator interaction={agentInteraction} />\n\n {!deviceScreenPresentsMedia(status) && (\n <div\n style={{\n ...DEVICE_SCREEN_STATUS_LAYOUT_STYLE,\n color: status === 'error' ? '#fca5a5' : 'rgba(255, 255, 255, 0.7)',\n }}>\n {status === 'error'\n ? (error ?? 'Disconnected')\n : status === 'connecting'\n ? 'Connecting…'\n : 'Not connected'}\n </div>\n )}\n </div>\n );\n}\n"],"names":["useCallback","useEffect","useRef","useState","streamGeometry","wheelDeltaToPixels","AgentInteractionIndicator","TouchIndicator","clamp01","n","Math","min","max","FINGER_CURSOR","DEVICE_SCREEN_SURFACE_LAYOUT_STYLE","position","inset","containerType","overflow","DEVICE_SCREEN_STATUS_LAYOUT_STYLE","boxSizing","display","alignItems","justifyContent","padding","textAlign","pointerEvents","backgroundColor","fontSize","fontFamily","deviceScreenPresentsMedia","status","deviceScreenSurfaceStyle","deviceScreenMediaStyle","rotation","rotatesSideways","abs","style","width","height","objectFit","top","left","transform","transformOrigin","userSelect","WebkitUserSelect","DeviceScreen","client","borderRadius","squircle","agentInteraction","videoKind","attachVideo","sendTouch","sendMultiTouch","sendScroll","sendKey","screen","error","canMulti","surfaceRef","pressedKeysRef","Map","releasePressedKeys","input","current","values","phase","repeat","clear","onWindowBlur","onVisibilityChange","document","hidden","window","addEventListener","removeEventListener","keyboardInputFrom","event","code","key","onKeyDown","shiftKey","preventDefault","currentTarget","blur","nativeEvent","isComposing","set","onKeyUp","keyId","wasPressed","delete","handled","pointersRef","modeRef","singleIdRef","altShiftRef","panOffsetRef","x","y","fingers","setFingers","pendingRef","rafRef","flush","pend","single","multi","a","b","queueFrame","requestAnimationFrame","pointFrom","clientX","clientY","rect","getBoundingClientRect","altSecondFinger","p","endMulti","cancelAnimationFrame","onPointerDown","pointerType","button","focus","preventScroll","setPointerCapture","pointerId","altKey","size","onPointerMove","stored","get","native","coalesced","getCoalescedEvents","last","length","onPointerUp","releasePointerCapture","el","onWheel","dxPx","deltaX","deltaMode","dyPx","deltaY","dx","dy","stopPropagation","passive","geometry","rotationDegrees","surfaceStyle","cornerShape","mediaStyle","div","canvas","ref","video","autoPlay","muted","playsInline","img","alt","draggable","role","aria-label","tabIndex","cursor","touchAction","outline","onBlur","onPointerCancel","onContextMenu","point","interaction","color"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { DeviceClient } from "./types";
2
+ export declare function createDeviceClientStore(initialClient?: DeviceClient, serverClient?: DeviceClient): {
3
+ getSnapshot: () => DeviceClient;
4
+ getServerSnapshot: () => DeviceClient;
5
+ subscribe(listener: () => void): () => void;
6
+ publish(client: DeviceClient): void;
7
+ };
8
+ export type DeviceClientStore = ReturnType<typeof createDeviceClientStore>;
9
+ //# sourceMappingURL=device-client-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-client-store.d.ts","sourceRoot":"","sources":["../src/device-client-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C,wBAAgB,uBAAuB,CACrC,aAAa,GAAE,YAAiC,EAChD,YAAY,GAAE,YAA4B;;;wBAQpB,MAAM,IAAI;oBAMd,YAAY;EAM/B;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { NOOP_DEVICE_CLIENT } from "./useNoopDeviceClient.mjs";
2
+ export function createDeviceClientStore(initialClient = NOOP_DEVICE_CLIENT, serverClient = initialClient) {
3
+ let snapshot = initialClient;
4
+ const listeners = new Set();
5
+ return {
6
+ getSnapshot: ()=>snapshot,
7
+ getServerSnapshot: ()=>serverClient,
8
+ subscribe (listener) {
9
+ listeners.add(listener);
10
+ return ()=>{
11
+ listeners.delete(listener);
12
+ };
13
+ },
14
+ publish (client) {
15
+ if (Object.is(snapshot, client)) return;
16
+ snapshot = client;
17
+ for (const listener of listeners)listener();
18
+ }
19
+ };
20
+ }
21
+
22
+ //# sourceMappingURL=device-client-store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/device-client-store.ts"],"sourcesContent":["import type { DeviceClient } from \"./types\";\nimport { NOOP_DEVICE_CLIENT } from \"./useNoopDeviceClient\";\n\nexport function createDeviceClientStore(\n initialClient: DeviceClient = NOOP_DEVICE_CLIENT,\n serverClient: DeviceClient = initialClient,\n) {\n let snapshot = initialClient;\n const listeners = new Set<() => void>();\n\n return {\n getSnapshot: () => snapshot,\n getServerSnapshot: () => serverClient,\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n publish(client: DeviceClient) {\n if (Object.is(snapshot, client)) return;\n snapshot = client;\n for (const listener of listeners) listener();\n },\n };\n}\n\nexport type DeviceClientStore = ReturnType<typeof createDeviceClientStore>;\n"],"names":["NOOP_DEVICE_CLIENT","createDeviceClientStore","initialClient","serverClient","snapshot","listeners","Set","getSnapshot","getServerSnapshot","subscribe","listener","add","delete","publish","client","Object","is"],"mappings":"AACA,SAASA,kBAAkB,QAAQ,4BAAwB;AAE3D,OAAO,SAASC,wBACdC,gBAA8BF,kBAAkB,EAChDG,eAA6BD,aAAa;IAE1C,IAAIE,WAAWF;IACf,MAAMG,YAAY,IAAIC;IAEtB,OAAO;QACLC,aAAa,IAAMH;QACnBI,mBAAmB,IAAML;QACzBM,WAAUC,QAAoB;YAC5BL,UAAUM,GAAG,CAACD;YACd,OAAO;gBACLL,UAAUO,MAAM,CAACF;YACnB;QACF;QACAG,SAAQC,MAAoB;YAC1B,IAAIC,OAAOC,EAAE,CAACZ,UAAUU,SAAS;YACjCV,WAAWU;YACX,KAAK,MAAMJ,YAAYL,UAAWK;QACpC;IACF;AACF"}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * The common device-client interface + its two implementations.
2
+ * Shared device connections, screens and controls.
3
3
  *
4
4
  * - {@link DeviceScreen} — the component rendered inside `PhoneFrame` (replaces
5
5
  * the static `<img>`), shared by both platforms.
6
- * - {@link useIosDeviceClient} / {@link useAndroidDeviceClient} — the serve-sim
7
- * and serve-emu implementations of the connection hook.
8
- * - {@link useActiveDeviceClient} — picks + connects the selected one.
6
+ * - {@link DeviceClientProvider} owns the selected device connection.
7
+ * - {@link useDeviceClient} and {@link useDeviceClientSelector} read state and controls.
8
+ * - {@link useDeviceScreenClient} reads the inputs needed by {@link DeviceScreen}.
9
9
  * - {@link KeyboardCapture} + {@link useCoarsePointer} — phone-keyboard typing
10
10
  * for touch clients, feeding `DeviceClient.sendKeyEvents`.
11
11
  *
@@ -14,6 +14,9 @@
14
14
  export * from './types';
15
15
  export { areRecordingControlsLocked } from './screen-recording';
16
16
  export { DeviceScreen } from './DeviceScreen';
17
+ export { DeviceClientProvider, useDeviceClientSelector, type DeviceClientProviderProps, } from './DeviceClientProvider';
18
+ export { useDeviceClient } from './useDeviceClient';
19
+ export { useDeviceScreenClient } from './useDeviceScreenClient';
17
20
  export { KeyboardCapture, type KeyboardCaptureProps } from './KeyboardCapture';
18
21
  export { AGENT_INTERACTION_IDLE_TIMEOUT_MS, agentInteractionCursorExpiresAt, agentInteractionEndMs, agentInteractionPointsAt, } from './agent-interaction-animation';
19
22
  export { displayScreen, streamGeometry } from './orientation';