@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 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
- * frames. `granted` is set only on the call that follows a
119
- * `getUserMedia` attempt: `true` when the user accepted the
120
- * browser prompt, `false` when they denied or the call failed
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
- * Only iOS instances ever fire this callback; Android instances
131
- * have no camera-injector path and stay silent.
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
- * Optional resolution cap for outbound camera capture.
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
- * simulator's mock microphone. This is a manual override: iOS apps
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;