@limrun/ui 0.14.3 → 0.15.1
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 +17 -0
- package/dist/components/remote-control.d.ts +16 -76
- package/dist/core/ax-fetcher.d.ts +5 -0
- package/dist/core/camera.d.ts +58 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1453 -1402
- package/package.json +2 -3
- package/src/components/remote-control.tsx +299 -416
- package/src/core/ax-fetcher.ts +15 -5
- package/src/core/camera.test.ts +155 -0
- package/src/core/camera.ts +133 -0
- package/src/index.ts +8 -2
package/README.md
CHANGED
|
@@ -6,6 +6,23 @@ types and helpers for building inspect, search, and agent interfaces.
|
|
|
6
6
|
|
|
7
7
|
See [examples](../../examples/) to see how it can be used.
|
|
8
8
|
|
|
9
|
+
## Simulated cameras
|
|
10
|
+
|
|
11
|
+
`RemoteControl` responds automatically when an app in the instance opens,
|
|
12
|
+
closes, or switches a virtual camera. Camera facing is a preference: the
|
|
13
|
+
component requests the browser's `user` or `environment` camera with an
|
|
14
|
+
`ideal` constraint and falls back to an available input. Repeated requests for
|
|
15
|
+
the same facing reuse the live track. Facing changes restart capture so mobile
|
|
16
|
+
browsers can release one camera before opening the other.
|
|
17
|
+
|
|
18
|
+
WebRTC and the browser choose the capture resolution. The simulated camera's
|
|
19
|
+
host output follows the decoded frame dimensions, including width/height swaps
|
|
20
|
+
when captured video rotates. `onCameraDemandChange` receives fresh
|
|
21
|
+
`MediaStreamTrack.getSettings()` status metadata after track changes.
|
|
22
|
+
|
|
23
|
+
Camera API types (`CameraRequest`, `CameraResult`, `CameraFacingMode`,
|
|
24
|
+
`CameraCaptureInfo`, and `CameraStreamStats`) are exported from `@limrun/ui`.
|
|
25
|
+
|
|
9
26
|
Related browser workflow packages are published separately:
|
|
10
27
|
|
|
11
28
|
- `@limrun/apple-auth` handles Apple ID login, signing credentials, and App
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
-
import { AxStatus } from '../core/ax-fetcher';
|
|
2
|
+
import { AxStatus, AndroidElementTreeOptions } from '../core/ax-fetcher';
|
|
3
3
|
import { AxElement, AxSnapshot } from '../core/ax-tree';
|
|
4
|
+
import { CameraCaptureInfo } from '../core/camera';
|
|
5
|
+
export type { CameraCaptureInfo, CameraFacingMode, CameraRequest, CameraResult } from '../core/camera';
|
|
4
6
|
declare global {
|
|
5
7
|
interface Window {
|
|
6
8
|
debugRemoteControl?: boolean;
|
|
7
9
|
}
|
|
8
10
|
}
|
|
9
|
-
interface RemoteControlProps {
|
|
11
|
+
export interface RemoteControlProps {
|
|
10
12
|
url: string;
|
|
11
13
|
token: string;
|
|
12
14
|
className?: string;
|
|
@@ -105,6 +107,7 @@ interface RemoteControlProps {
|
|
|
105
107
|
* @default 2000
|
|
106
108
|
*/
|
|
107
109
|
axMaxBackoffMs?: number;
|
|
110
|
+
androidElementTreeOptions?: AndroidElementTreeOptions;
|
|
108
111
|
/**
|
|
109
112
|
* Fires whenever the iOS simulator's camera demand state changes —
|
|
110
113
|
* i.e. an app inside the sim called
|
|
@@ -114,12 +117,10 @@ interface RemoteControlProps {
|
|
|
114
117
|
* UI can render a status indicator ("simulator is using your
|
|
115
118
|
* camera", etc.).
|
|
116
119
|
*
|
|
117
|
-
* `active` reflects whether the sim is currently asking for
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* (in which case the limulator side switches to a black-frame
|
|
122
|
-
* fallback so the app keeps ticking).
|
|
120
|
+
* `active` reflects whether the sim is currently asking for frames.
|
|
121
|
+
* `granted` is set on capture acknowledgements: `true` after a new
|
|
122
|
+
* track, reused track, or constraint update; `false` when capture
|
|
123
|
+
* fails. The host switches to black frames after a failure.
|
|
123
124
|
*
|
|
124
125
|
* `camera` (optional, only meaningful when `granted === true`)
|
|
125
126
|
* carries what `MediaStreamTrack.getSettings()` reported for the
|
|
@@ -127,8 +128,9 @@ interface RemoteControlProps {
|
|
|
127
128
|
* mode. Use it to render a richer status indicator (e.g.
|
|
128
129
|
* "Camera · 1920×1080 · 30 fps · FaceTime HD").
|
|
129
130
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
131
|
+
* iOS instances always fire this callback. Android instances fire it
|
|
132
|
+
* when the server supports camera injection (Android 15+); older
|
|
133
|
+
* Android instances have no camera-injector path and stay silent.
|
|
132
134
|
*/
|
|
133
135
|
onCameraDemandChange?: (active: boolean, granted?: boolean, camera?: CameraCaptureInfo) => void;
|
|
134
136
|
/**
|
|
@@ -149,48 +151,15 @@ interface RemoteControlProps {
|
|
|
149
151
|
*/
|
|
150
152
|
onCameraStats?: (stats: CameraStreamStats | null) => void;
|
|
151
153
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* - `'auto'` (default): no extra constraint. The browser captures
|
|
155
|
-
* at its webcam's native max; WebRTC's quality scaler may step
|
|
156
|
-
* down resolution on the encoder side under bandwidth pressure
|
|
157
|
-
* while still feeding the simulator at the pool's native size.
|
|
158
|
-
* - `'1080p'` / `'720p'` / `'480p'`: hard cap applied via
|
|
159
|
-
* `getUserMedia` constraints (for new captures) and
|
|
160
|
-
* `track.applyConstraints` (for the currently-active track),
|
|
161
|
-
* matching the way Meet/Zoom expose a "Send resolution" picker.
|
|
162
|
-
*
|
|
163
|
-
* Bumping or lowering the cap mid-stream is supported; the
|
|
164
|
-
* change takes effect within a frame or two as the webcam
|
|
165
|
-
* re-negotiates.
|
|
166
|
-
*/
|
|
167
|
-
cameraResolutionCap?: CameraResolutionCap;
|
|
168
|
-
/**
|
|
169
|
-
* Aspect ratio the simulator's virtual camera should report to apps.
|
|
170
|
-
* Picking a value here triggers a `cameraAspect` WS message to the
|
|
171
|
-
* host, which rebuilds its IOSurface ring at the matching dimensions
|
|
172
|
-
* (16:9 → 1920×1080, 4:3 → 1440×1080, 1:1 → 1080×1080, 9:16 →
|
|
173
|
-
* 1080×1920) and signals the in-sim dylib to re-handshake. iOS apps
|
|
174
|
-
* see CMSampleBuffers at the new dimensions within a frame or two.
|
|
175
|
-
*
|
|
176
|
-
* The browser still captures whatever the webcam offers; the host
|
|
177
|
-
* aspect-fills (cover, center-crop) into the new pool. Switching
|
|
178
|
-
* aspect at runtime is intentionally cheap so users can A/B preview
|
|
179
|
-
* styles without restarting the simulator.
|
|
180
|
-
*
|
|
181
|
-
* `undefined` leaves the pool untouched (the host's boot default —
|
|
182
|
-
* 16:9 / 1920×1080 — applies).
|
|
183
|
-
*/
|
|
184
|
-
cameraAspect?: CameraAspect;
|
|
185
|
-
/**
|
|
186
|
-
* iOS only: when true, capture the user's microphone via
|
|
154
|
+
* When true, capture the user's microphone via
|
|
187
155
|
* `getUserMedia({ audio: true })` and stream it live into the
|
|
188
|
-
*
|
|
156
|
+
* device's mock microphone. This is a manual override: apps
|
|
189
157
|
* that actively consume microphone input also trigger capture
|
|
190
158
|
* automatically, like camera demand. The component handles the
|
|
191
159
|
* browser permission prompt, WebRTC track attachment, and
|
|
192
160
|
* `microphoneStart`/`microphoneStop` signaling. Progress and failures
|
|
193
|
-
* are reported via `onMicrophoneStateChange`. Ignored on Android
|
|
161
|
+
* are reported via `onMicrophoneStateChange`. Ignored on Android
|
|
162
|
+
* servers without camera/mic injection support (Android 14 and older).
|
|
194
163
|
*/
|
|
195
164
|
microphoneEnabled?: boolean;
|
|
196
165
|
/**
|
|
@@ -216,34 +185,6 @@ export interface MicrophoneState {
|
|
|
216
185
|
/** Device label of the captured microphone, when available. */
|
|
217
186
|
label?: string;
|
|
218
187
|
}
|
|
219
|
-
/**
|
|
220
|
-
* Resolution caps a host app can request on the outbound camera.
|
|
221
|
-
* `'auto'` is "let the browser decide" (no constraints beyond the
|
|
222
|
-
* 30 fps ceiling); the other options clamp width/height to the
|
|
223
|
-
* named target. Aspect ratio is preserved.
|
|
224
|
-
*/
|
|
225
|
-
export type CameraResolutionCap = 'auto' | '1080p' | '720p' | '480p';
|
|
226
|
-
/**
|
|
227
|
-
* Aspect ratios exposed to the operator for the simulated camera.
|
|
228
|
-
* The host maps each label to concrete IOSurface dimensions; values
|
|
229
|
-
* the host doesn't recognise are silently ignored.
|
|
230
|
-
*/
|
|
231
|
-
export type CameraAspect = '16:9' | '4:3' | '1:1' | '9:16';
|
|
232
|
-
/**
|
|
233
|
-
* Snapshot of the browser's webcam capture, mirrored from
|
|
234
|
-
* `MediaStreamTrack.getSettings()`. Forwarded to the host alongside
|
|
235
|
-
* `cameraResult` and exposed to the host app via
|
|
236
|
-
* `onCameraDemandChange` so it can render a status indicator without
|
|
237
|
-
* having to call `getStats()` itself.
|
|
238
|
-
*/
|
|
239
|
-
export interface CameraCaptureInfo {
|
|
240
|
-
width?: number;
|
|
241
|
-
height?: number;
|
|
242
|
-
frameRate?: number;
|
|
243
|
-
deviceId?: string;
|
|
244
|
-
label?: string;
|
|
245
|
-
facingMode?: string;
|
|
246
|
-
}
|
|
247
188
|
/**
|
|
248
189
|
* Live outbound-camera quality snapshot. Derived from
|
|
249
190
|
* `RTCPeerConnection.getStats()` and rate-derived deltas, sampled
|
|
@@ -308,4 +249,3 @@ export interface RemoteControlHandle {
|
|
|
308
249
|
}
|
|
309
250
|
export declare const withAuthenticationToken: (url: string, token: string) => string;
|
|
310
251
|
export declare const RemoteControl: React.ForwardRefExoticComponent<RemoteControlProps & React.RefAttributes<RemoteControlHandle>>;
|
|
311
|
-
export {};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { AxPlatform, AxSnapshot } from './ax-tree';
|
|
2
2
|
export type AxStatus = 'idle' | 'starting' | 'ready' | 'unavailable' | 'error';
|
|
3
3
|
export type AxFetcherSendFn = (payload: Record<string, unknown>) => boolean;
|
|
4
|
+
export interface AndroidElementTreeOptions {
|
|
5
|
+
waitForIdleTimeoutMs?: number;
|
|
6
|
+
}
|
|
4
7
|
export interface AxFetcherOptions {
|
|
5
8
|
platform: AxPlatform;
|
|
6
9
|
send: AxFetcherSendFn;
|
|
7
10
|
onSnapshot: (snapshot: AxSnapshot | null) => void;
|
|
11
|
+
androidElementTreeOptions?: AndroidElementTreeOptions;
|
|
8
12
|
onStatusChange?: (status: AxStatus, error?: string) => void;
|
|
9
13
|
baseIntervalMs?: number;
|
|
10
14
|
maxBackoffMs?: number;
|
|
@@ -16,6 +20,7 @@ export declare class AxFetcher {
|
|
|
16
20
|
private readonly onStatusChange?;
|
|
17
21
|
private readonly baseIntervalMs;
|
|
18
22
|
private readonly maxBackoffMs;
|
|
23
|
+
private readonly androidElementTreeOptions?;
|
|
19
24
|
private readonly pending;
|
|
20
25
|
private running;
|
|
21
26
|
private timer;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type CameraFacingMode = 'user' | 'environment';
|
|
2
|
+
/** Camera selection requested by the instance host. */
|
|
3
|
+
export interface CameraRequest {
|
|
4
|
+
active: boolean;
|
|
5
|
+
facingMode?: CameraFacingMode;
|
|
6
|
+
}
|
|
7
|
+
/** Browser capture metadata returned to the instance host. */
|
|
8
|
+
export interface CameraCaptureInfo {
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
frameRate?: number;
|
|
12
|
+
deviceId?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
facingMode?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Acknowledgement sent after camera capture changes or is reused. */
|
|
17
|
+
export interface CameraResult {
|
|
18
|
+
type: 'cameraResult';
|
|
19
|
+
granted: boolean;
|
|
20
|
+
camera?: CameraCaptureInfo;
|
|
21
|
+
}
|
|
22
|
+
export interface CameraOperationContext {
|
|
23
|
+
generation: number;
|
|
24
|
+
isCurrent: () => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Serializes camera mutations while invalidating queued or suspended work.
|
|
28
|
+
*
|
|
29
|
+
* `enqueue` increments the generation before returning, so an operation
|
|
30
|
+
* waiting on browser permission becomes stale as soon as a newer request
|
|
31
|
+
* arrives, even though the newer operation has not started yet.
|
|
32
|
+
*/
|
|
33
|
+
export declare class CameraOperationCoordinator {
|
|
34
|
+
private generation;
|
|
35
|
+
private tail;
|
|
36
|
+
get currentGeneration(): number;
|
|
37
|
+
enqueue(operation: (context: CameraOperationContext) => Promise<void>): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Queues related work without superseding an already queued request.
|
|
40
|
+
*
|
|
41
|
+
* The operation retains the generation current at enqueue time and becomes
|
|
42
|
+
* stale if a later camera request increments it before this work starts.
|
|
43
|
+
*/
|
|
44
|
+
enqueueCurrent(operation: (context: CameraOperationContext) => Promise<void>): Promise<void>;
|
|
45
|
+
private append;
|
|
46
|
+
invalidate(): void;
|
|
47
|
+
}
|
|
48
|
+
export declare function parseCameraRequest(message: unknown): CameraRequest | null;
|
|
49
|
+
/**
|
|
50
|
+
* Builds best-effort capture constraints.
|
|
51
|
+
*
|
|
52
|
+
* Facing is a preference so browsers can fall back when a phone-facing label
|
|
53
|
+
* or exact mode is unavailable. The browser chooses capture dimensions.
|
|
54
|
+
*/
|
|
55
|
+
export declare function cameraCaptureConstraints(request: CameraRequest): MediaTrackConstraints;
|
|
56
|
+
export declare function shouldReacquireCamera(current: Pick<CameraRequest, 'facingMode'> | undefined, requested: Pick<CameraRequest, 'facingMode'>): boolean;
|
|
57
|
+
export declare function cameraCaptureInfoFromTrack(track: MediaStreamTrack): CameraCaptureInfo;
|
|
58
|
+
export declare function createGrantedCameraResult(track: MediaStreamTrack): CameraResult;
|