@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.
- package/README.md +155 -73
- package/dist/DeviceClientProvider.d.ts +18 -0
- package/dist/DeviceClientProvider.d.ts.map +1 -0
- package/dist/DeviceClientProvider.mjs +47 -0
- package/dist/DeviceClientProvider.mjs.map +1 -0
- package/dist/DeviceScreen.d.ts +4 -4
- package/dist/DeviceScreen.d.ts.map +1 -1
- package/dist/DeviceScreen.mjs.map +1 -1
- package/dist/device-client-store.d.ts +9 -0
- package/dist/device-client-store.d.ts.map +1 -0
- package/dist/device-client-store.mjs +22 -0
- package/dist/device-client-store.mjs.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +7 -4
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +10 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.mjs +5 -5
- package/dist/types.mjs.map +1 -1
- package/dist/useActiveDeviceClient.d.ts +1 -0
- package/dist/useActiveDeviceClient.d.ts.map +1 -1
- package/dist/useActiveDeviceClient.mjs +1 -0
- package/dist/useActiveDeviceClient.mjs.map +1 -1
- package/dist/useAndroidDevice.d.ts +1 -0
- package/dist/useAndroidDevice.d.ts.map +1 -1
- package/dist/useAndroidDevice.mjs +44 -31
- package/dist/useAndroidDevice.mjs.map +1 -1
- package/dist/useDeviceClient.d.ts +4 -0
- package/dist/useDeviceClient.d.ts.map +1 -0
- package/dist/useDeviceClient.mjs +72 -0
- package/dist/useDeviceClient.mjs.map +1 -0
- package/dist/useDeviceLocation.d.ts.map +1 -1
- package/dist/useDeviceLocation.mjs +5 -3
- package/dist/useDeviceLocation.mjs.map +1 -1
- package/dist/useDeviceScreenClient.d.ts +4 -0
- package/dist/useDeviceScreenClient.d.ts.map +1 -0
- package/dist/useDeviceScreenClient.mjs +23 -0
- package/dist/useDeviceScreenClient.mjs.map +1 -0
- package/dist/useIosDevice.d.ts +2 -0
- package/dist/useIosDevice.d.ts.map +1 -1
- package/dist/useIosDevice.mjs +53 -35
- package/dist/useIosDevice.mjs.map +1 -1
- package/package.json +8 -1
- package/src/DeviceClientProvider.tsx +63 -0
- package/src/DeviceScreen.tsx +4 -4
- package/src/device-client-store.ts +28 -0
- package/src/index.ts +11 -4
- package/src/types.ts +23 -4
- package/src/useActiveDeviceClient.ts +1 -0
- package/src/useAndroidDevice.ts +42 -22
- package/src/useDeviceClient.ts +84 -0
- package/src/useDeviceLocation.ts +4 -2
- package/src/useDeviceScreenClient.ts +38 -0
- package/src/useIosDevice.ts +71 -30
package/README.md
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
# @expo/hub-client
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
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
|
-
##
|
|
21
|
+
## Connect to a device
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 {
|
|
34
|
-
|
|
35
|
-
export function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
<
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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 {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
130
|
+
Reading `activity` with `useDeviceClient` would update the component whenever any part
|
|
131
|
+
of `activity` changes.
|
|
101
132
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
148
|
+
so pass the token to the provider:
|
|
111
149
|
|
|
112
150
|
```tsx
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
{
|
|
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
|
-
|
|
121
|
-
`Authorization: Bearer <token>`, as a `serve-sim.token.<token>` or
|
|
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"}
|
package/dist/DeviceScreen.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CSSProperties } from 'react';
|
|
2
|
-
import { type
|
|
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:
|
|
13
|
-
export declare function deviceScreenSurfaceStyle(status:
|
|
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, }:
|
|
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,
|
|
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
|
-
*
|
|
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
|
|
7
|
-
*
|
|
8
|
-
* - {@link
|
|
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';
|