@chipmobilesdk/rn-camera 0.2.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,127 @@
1
1
  # @chipmobilesdk/rn-camera
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `start()` resolves when the preview is producing frames. With the VisionCamera adapter the session belongs to the `<Camera>` view, so the controller now waits for that view to report instead of calling the session active as soon as the request was made — `capture()` straight after `start()` used to fail with `notReady`. A camera that never produces a frame fails the start after `startTimeoutMs` (8 s by default) rather than leaving the session in `starting` forever, and the native session errors the preview reports map to `cameraInUse`, `cameraBlocked`, `permissionDenied`, `cameraUnavailable` or `interrupted`. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
8
+
9
+ `pause()` keeps the preview surface mounted and stops the camera, so `resume()` no longer rebuilds the device, the session and the photo output — the whole first-frame cost was paid on every resume, with a black flash to go with it.
10
+
11
+ Geometry comes from the camera. `onGeometryChange` fires again with the selected format's pixel size, the device's orientation and the position of the device that actually mounted, as soon as the surface reports them; the same values are published as `state.surface`. Mapping a rectangle on a portrait phone was being done against a nominal landscape 1920×1080 image.
12
+
13
+ `capture({ cameraPosition })` must name the camera that is running, and is refused with `notReady` otherwise: the photo came from the device the preview had mounted while the metadata claimed the camera that was asked for. `start({ cameraPosition })` switches.
14
+
15
+ A failed `requestPermission()` leaves a state the app can retry from rather than `requesting` forever. An engine can report that the camera was taken away (`subscribeToInterruptions`), and the session fails with that code instead of showing a frozen preview; `createTestCamera()` simulates it with `emitInterruption(code?)` and a failing permission request with `emitPermissionRequestFailure(true)`.
16
+
17
+ The preview surface belongs to its engine (`attachSurface`, `createSurfaceRegistry`), not to a module-level slot two controllers shared — a capture from one screen could run through another screen's surface. `engine` and `permissions` are required by `CameraControllerOptions` (as they always were at runtime), the unused `EngineOptions` type is gone, and the artifact store reports a typed `CameraError` rather than a bare `Error`.
18
+
19
+ ### Patch Changes
20
+
21
+ - Internal: no module in these packages is over 500 lines any more. The camera controller's session I/O moved to `sessionRuntime` and its contract to `contract.ts`; the Play adapter's shape translation to `iapShapes`; the purchase flow's failure mapping and its receipt-validation hook to modules of their own; the AI client's quota request to `quotaRequest`; the in-memory adapter's attachments and the fake ads engine's clock likewise. Behaviour is unchanged — the splits are by responsibility, and every package's suite passes as it stood. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
22
+
23
+ The repository also measures what it promised: `npm run perf` prints the median of nine runs of the deterministic hot paths against written budgets (recurrence expansion, theme construction, translation lookup, chart geometry, filter compilation), the demo app has a Performance screen reporting startup phases, time to interactive and the same benchmarks on Hermes, and `npm run check:file-size` keeps the limit from drifting. The budgets and the measured figures are recorded in `docs/standards.md` §VI.
24
+
25
+ ## 0.3.0
26
+
27
+ ### Minor Changes
28
+
29
+ - One diagnostic stream: install a sink with `installSdkSink` from
30
+ `@chipmobilesdk/rn-core` and it receives what every package observes (events,
31
+ failures, warnings) as `SdkDiagnostic`s, redacted. Listeners are notified through
32
+ rn-core, so a listener that throws no longer stops the others nor fails the call that
33
+ notified, and its error is reported rather than swallowed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
34
+
35
+ - rn-ads: a banner's auto-refresh is a new cycle, so its `loaded` and `impression`
36
+ events are no longer deduplicated away; the dedupe memory is bounded. A throwing
37
+ status, consent or placement listener is reported as an `internal_error` event.
38
+ - rn-i18n: new `onDiagnostic` config receives every diagnostic (`I18nDiagnostic`,
39
+ with a stable `code`); without it, warnings go to `console.warn` in development
40
+ builds only. The warn-once memory is bounded.
41
+ - rn-chart: now depends on rn-core for its diagnostics.
42
+ - rn-payment: status, entitlement and pending listeners report a throw to the logger
43
+ instead of swallowing it.
44
+
45
+ - The package's errors extend `SdkError` from the new `@chipmobilesdk/rn-core`
46
+ dependency: `isSdkError` recognises them alongside every other package's, and each
47
+ carries `pkg`, `code` and `retryable`. Names and codes are unchanged. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
48
+
49
+ - rn-ai and rn-camera: `retryable` is the new name of `recoverable`, which stays as a
50
+ deprecated alias.
51
+ - rn-auth: `retryable` is true for `network-unavailable`. `redact` also removes URLs,
52
+ e-mail addresses and UUIDs.
53
+ - rn-ads: `redact` also removes JWTs and e-mail addresses, and reduces an `AdsError`
54
+ to its code.
55
+ - rn-ai: an error message quoted from the gateway is pattern-redacted.
56
+ - rn-camera: `redact` also removes JWTs, e-mail addresses and UUIDs.
57
+ - rn-theme: `ThemeConfigError` has `code: 'invalid_config'` and `ThemeContrastError`
58
+ `code: 'contrast'` (new `ThemeErrorCode` type).
59
+ - rn-i18n: `I18nConfigError` has `code: 'invalid_config'` and `UnitConversionError`
60
+ `code: 'unit_conversion'` (new `I18nErrorCode` type).
61
+
62
+ - Native peers load through `lazyPeer` from `@chipmobilesdk/rn-core`, on first use,
63
+ and a missing one is each package's own typed error. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
64
+
65
+ - rn-auth: creating `auth` no longer loads react-native-app-auth or
66
+ react-native-keychain; each loads on first use. New entrypoints
67
+ `@chipmobilesdk/rn-auth/app-auth` and `/keychain`. The default platform comes from
68
+ rn-core.
69
+ - rn-camera: a preview whose VisionCamera feed cannot load fails the session with
70
+ `cameraUnavailable` (new `controller.reportSurfaceUnavailable()`), instead of
71
+ showing black with no error. A missing VisionCamera module is `cameraUnavailable`.
72
+ - rn-notification: `createMmkvStore` reports a fallback to memory as
73
+ `STATE_STORE_DEGRADED` (to the SDK sinks and a new `onDegraded` option) and
74
+ exposes `durable`.
75
+ - rn-payment: an iOS app validates its catalog against the iOS product ids; the
76
+ default platform used to be Android everywhere. A missing react-native-iap is
77
+ `PAYMENT_UNAVAILABLE`.
78
+ - rn-theme: `buildThemes` runs without React Native (new `hairlineWidth` option) and
79
+ importing the package no longer loads Unistyles; `applyThemeMode` loads it on
80
+ first use.
81
+ - rn-ads, rn-local-db: the same loading, with the same error codes as before.
82
+
83
+ - Port conformance suites: one list of checks that a package's fake and its real adapter
84
+ both pass, so a test written against the fake proves something about the device. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
85
+
86
+ - rn-core: `@chipmobilesdk/rn-core/testing` adds `runChecks`, `expectConformance`,
87
+ `ensure` and `createFaultRegistry` (deterministic fault injection by named point).
88
+ - rn-camera: a `./testing` entrypoint with `runPermissionPortConformance`,
89
+ `runCameraEngineConformance` and the test adapter, which stays on the main
90
+ entrypoint too until the next major. `createTestCamera(...).prompts()` counts system
91
+ prompts.
92
+ - rn-payment: `runProviderPortConformance` in `./testing`, passed by the fake store and
93
+ the Play adapter.
94
+ - rn-ads: `runAdsEngineConformance` in `./testing`, passed by the fake engine and the
95
+ AdMob adapter.
96
+ - rn-auth: a `./testing` entrypoint with `createMemorySecureStore`,
97
+ `runSecureTokenStoreConformance` and `runOidcAuthClientConformance`, passed by the
98
+ in-memory doubles and the keychain and app-auth adapters.
99
+
100
+ - One app-lifecycle source, and a startup that needs no extra frame. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
101
+
102
+ - rn-core: `appLifecycle()`, a shared, reference-counted `AppState` subscription that
103
+ reports changes only (`active`, `inactive`, `background`), with `onForeground` for a
104
+ return from the background and `createManualLifecycle` in `./testing`.
105
+ - rn-camera, rn-ads and rn-notification hear the lifecycle through it instead of
106
+ subscribing to `AppState` each, so an app holds one native listener. The camera
107
+ controller's core layer no longer imports `react-native`.
108
+ - rn-payment: `syncOnForeground(engine)` and `<PaymentProvider syncOnForeground>` call
109
+ `handleForeground()` on each return from the background. Off by default; calling
110
+ `handleForeground()` yourself works as before.
111
+ - rn-i18n: `i18n.initSync()` resolves the locale before the first render when the
112
+ storage adapter has the new optional `getItemSync`, and returns false otherwise.
113
+ `localeStorageFromKeyValue(port)` builds such an adapter from rn-core's
114
+ `KeyValuePort` (MMKV).
115
+
116
+ ### Patch Changes
117
+
118
+ - `createCameraController` accepts an explicit `undefined` for `store` and `logger`, so
119
+ apps compiling with `exactOptionalPropertyTypes` type-check against the package.
120
+ `VisionCameraFeed` leaves `torchMode` unset until the app first touches the torch,
121
+ instead of passing `undefined`. No other change. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
122
+ - Updated dependencies ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17)):
123
+ - @chipmobilesdk/rn-core@0.1.0
124
+
3
125
  ## 0.2.1
4
126
 
5
127
  ### Patch Changes
package/README.md CHANGED
@@ -213,11 +213,39 @@ const inImageSpace = mapRectToImage(myGuideFrame, geometry);
213
213
  ```
214
214
 
215
215
  The pure mapping functions account for scale, letterbox or crop, orientation,
216
- and front-camera mirroring using the geometry you supply. In the current
217
- `CameraPreview`, `onGeometryChange` reports nominal image dimensions
218
- (`1920 × 1080`), orientation `0`, and mirroring from camera position; it does
219
- not yet read the selected native capture format. Use capture metadata or
220
- app-owned measured geometry when pixel-exact alignment is required.
216
+ and front-camera mirroring using the geometry you supply.
217
+
218
+ `onGeometryChange` fires first with nominal dimensions (`1920 × 1080`,
219
+ orientation `0`) because the preview has been measured before the camera has,
220
+ and again with the real ones as soon as the surface reports: the selected
221
+ format's pixel size, the device's orientation, and the position of the device
222
+ that actually mounted. The same values are on `state.surface`, which is `null`
223
+ until then — map coordinates once it is set, or a rectangle will be mapped
224
+ against a landscape image on a portrait phone.
225
+
226
+ ### Starting, pausing, and what "active" means
227
+
228
+ `start()` resolves when the preview is producing frames, not when the request
229
+ was made: with the VisionCamera adapter the session belongs to the `<Camera>`
230
+ view, so the controller waits for it to report and `capture()` straight after
231
+ `start()` works. A camera that never produces a frame fails the start after
232
+ `startTimeoutMs` (8 s by default) instead of leaving the session in `starting`.
233
+
234
+ `pause()` keeps the surface mounted and stops the camera; `resume()` therefore
235
+ does not rebuild the device, the session and the photo output. The surface is
236
+ unmounted only when the session stops, fails, or loses permission.
237
+
238
+ An engine can report that the camera was taken away — another app, an incoming
239
+ call, a disconnected device — through `subscribeToInterruptions`. The session
240
+ fails with that code rather than showing a frozen preview; `createTestCamera()`
241
+ simulates it with `emitInterruption(code?)`.
242
+
243
+ ### Which camera a capture comes from
244
+
245
+ `capture({ cameraPosition })` must name the camera that is running: a photo
246
+ comes from the device the preview mounted, so a request for the other one is
247
+ refused with `notReady` rather than returning an image labelled with a camera
248
+ that did not take it. Call `start({ cameraPosition })` to switch first.
221
249
 
222
250
  ### Quality and dimensions
223
251
 
@@ -256,6 +284,31 @@ camera.setAppState('background');
256
284
  camera.emitInterruption();
257
285
  ```
258
286
 
287
+ ### Conformance
288
+
289
+ `@chipmobilesdk/rn-camera/testing` also ships the checks every `PermissionPort` and
290
+ `CameraEngine` must pass. The package runs them against the test adapter and against the
291
+ VisionCamera adapters; run them against your own adapter, or from a demo screen on a
292
+ device, the same way:
293
+
294
+ ```ts
295
+ import { expectConformance } from '@chipmobilesdk/rn-core/testing';
296
+ import { runPermissionPortConformance } from '@chipmobilesdk/rn-camera/testing';
297
+
298
+ expectConformance(
299
+ await runPermissionPortConformance({
300
+ create(initial) {
301
+ // a port over a system whose permission starts in `initial`
302
+ return { port, setSystemState, prompts };
303
+ },
304
+ }),
305
+ );
306
+ ```
307
+
308
+ `createTestCamera` and its helpers are exported from `./testing` too; the main
309
+ entrypoint keeps them until the next major release. `camera.prompts()` counts how often
310
+ `request()` showed the system prompt.
311
+
259
312
  Add a module mapping so an accidental import of the native module resolves in Jest:
260
313
 
261
314
  ```js
@@ -266,7 +319,8 @@ moduleNameMapper: {
266
319
 
267
320
  ## Errors
268
321
 
269
- Every failure is a `CameraError` with a `code` and a `recoverable` flag, so you branch on the code rather than string-matching a message:
322
+ Every failure is a `CameraError` with a `code` and a `retryable` flag (`recoverable` is kept as its deprecated alias), so you branch on the code rather than string-matching a message. `CameraError` extends `SdkError` from `@chipmobilesdk/rn-core`, so `isSdkError(error)` recognises it alongside every other package's errors, `error.pkg` names this package, and `error.redactedCause` (when present) is the underlying failure reduced to log-safe text.
323
+
270
324
 
271
325
  `permissionDenied`, `cameraUnavailable`, `cameraBlocked`, `cameraInUse`, `sessionConflict`, `notReady`, `captureInProgress`, `captureFailed`, `storageFailure`, `unsupportedCapability`, `interrupted`.
272
326
 
@@ -326,15 +380,16 @@ Still capture and real-time frame processing stay strictly separate.
326
380
 
327
381
  ## Public API
328
382
 
329
- Everything is exported from `@chipmobilesdk/rn-camera`. Modules under `core/`, `native/`, `permissions/`, `storage/`, `geometry/`, `diagnostics/`, and `testing/` are private — do not deep-import them.
383
+ Everything is exported from `@chipmobilesdk/rn-camera`, test doubles and conformance suites also from `@chipmobilesdk/rn-camera/testing`. Modules under `core/`, `native/`, `permissions/`, `storage/`, `geometry/`, `diagnostics/`, and `testing/` are private — do not deep-import them.
330
384
 
331
385
  - Controller and state: `createCameraController`, `CameraController`,
332
386
  `CameraControllerOptions`, `useCameraController`, `useCameraState`,
333
387
  `CameraState`, `AppStatus`, `DEFAULT_TEMPORARY_TTL_MS`
334
388
  - Preview and capture: `CameraPreview`, `CameraPreviewProps`, `CaptureRequest`,
335
389
  `CapturedImage`, `StartOptions` and the camera/session/capability types
336
- - Ports and adapters: `CameraEngine`, `EngineOptions`,
337
- `createVisionCameraEngine`, `PermissionPort`,
390
+ - Ports and adapters: `CameraEngine`, `createVisionCameraEngine`,
391
+ `VisionSurface`, `SurfaceInfo`, `SurfaceRegistry`, `createSurfaceRegistry`,
392
+ `PermissionPort`,
338
393
  `createVisionCameraPermissions`, `ArtifactStore`, `FileSystemBridge`,
339
394
  `StoreOptions`, `createCacheArtifactStore`, `createArtifactStore`
340
395
  - Mapping: `mapPointToImage`, `mapPointToPreview`, `mapRectToImage`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chipmobilesdk/rn-camera",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "main": "src/index.ts",
@@ -10,6 +10,11 @@
10
10
  "types": "./src/index.ts",
11
11
  "react-native": "./src/index.ts",
12
12
  "default": "./src/index.ts"
13
+ },
14
+ "./testing": {
15
+ "types": "./src/testing/index.ts",
16
+ "react-native": "./src/testing/index.ts",
17
+ "default": "./src/testing/index.ts"
13
18
  }
14
19
  },
15
20
  "files": [
@@ -29,9 +34,12 @@
29
34
  ],
30
35
  "sideEffects": false,
31
36
  "scripts": {
32
- "typecheck": "tsc -p tsconfig.json --noEmit",
37
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
33
38
  "pack:check": "npm pack --dry-run"
34
39
  },
40
+ "dependencies": {
41
+ "@chipmobilesdk/rn-core": "^0.1.0"
42
+ },
35
43
  "peerDependencies": {
36
44
  "react": ">=19",
37
45
  "react-native": ">=0.85",
@@ -53,10 +53,28 @@ Runtime:
53
53
  - `CameraPreview` (+ `CameraPreviewProps`)
54
54
  - `CameraError`, `isCameraError`, `redact` — `redact` strips sensitive paths before logging
55
55
  - Testing: `createTestCamera`, `createMemoryFileSystem`, `DEFAULT_TEST_CAPABILITIES`
56
+ - Conformance (`@chipmobilesdk/rn-camera/testing`): `runPermissionPortConformance`,
57
+ `runCameraEngineConformance` — run against any adapter you write or inject
56
58
 
57
- Types: `CameraEngine`, `EngineOptions`, `PermissionPort`, `ArtifactStore`,
58
- `FileSystemBridge`, `StoreOptions`, `CameraErrorCode`, `CameraLogEvent`,
59
- `CameraLogEventType`, `CameraLogger`
59
+ Types: `CameraEngine`, `PermissionPort`, `ArtifactStore`, `FileSystemBridge`,
60
+ `StoreOptions`, `VisionSurface`, `SurfaceInfo`, `SurfaceRegistry`,
61
+ `CameraErrorCode`, `CameraLogEvent`, `CameraLogEventType`, `CameraLogger`
62
+
63
+ ## Three things the lifecycle guarantees
64
+
65
+ - **`start()` resolves when frames arrive.** `capture()` straight after it works, and a
66
+ camera that never produces a frame fails the start (8 s by default) instead of leaving
67
+ the session in `starting`.
68
+ - **`pause()` keeps the surface mounted** and stops the camera, so `resume()` is cheap.
69
+ - **`capture({ cameraPosition })` must name the running camera.** A photo comes from the
70
+ device the preview mounted; asking for the other one is `notReady`. Switch with
71
+ `start({ cameraPosition })`.
72
+
73
+ ## Coordinate mapping waits for the surface
74
+
75
+ `state.surface` is `null` until the preview reports its real format, and
76
+ `onGeometryChange` fires again with those numbers. Map rectangles after it is set —
77
+ before that the geometry is a nominal landscape 1920×1080.
60
78
 
61
79
  Always pass camera log payloads through `redact` before logging.
62
80
 
@@ -21,6 +21,8 @@ export type FlashMode = 'auto' | 'on' | 'off';
21
21
  export type ResizeMode = 'cover' | 'contain';
22
22
  export type ArtifactOwnership = 'packageTemporary' | 'appOwned';
23
23
 
24
+ import type { SurfaceInfo } from '../native/visionSurface';
25
+
24
26
  export type SessionStatus =
25
27
  'idle' | 'starting' | 'active' | 'paused' | 'stopping' | 'stopped' | 'error';
26
28
 
@@ -129,6 +131,12 @@ export interface CameraState {
129
131
  readonly flashMode: FlashMode;
130
132
  readonly torchEnabled: boolean;
131
133
  readonly capturing: boolean;
134
+ /**
135
+ * What the mounted preview surface reported about itself: the capture
136
+ * dimensions of the selected format, its orientation, and the position of
137
+ * the device actually running. `null` until a surface reports (CAM-10).
138
+ */
139
+ readonly surface: SurfaceInfo | null;
132
140
  readonly error: CameraErrorLike | null;
133
141
  }
134
142
 
@@ -67,6 +67,15 @@ export function assertCaptureAllowed(context: CaptureContext, request: CaptureRe
67
67
 
68
68
  const position = request.cameraPosition ?? context.cameraPosition;
69
69
  assertSupported(supportsPosition(context.capabilities, position), `${position} camera`);
70
+ if (position !== context.cameraPosition) {
71
+ // The photo would come from the camera that is running, while the
72
+ // metadata said it came from the one that was asked for (CAM-06).
73
+ throw new CameraError(
74
+ 'notReady',
75
+ `The ${context.cameraPosition} camera is running. Call start({ cameraPosition: ` +
76
+ `'${position}' }) before capturing with the ${position} camera.`,
77
+ );
78
+ }
70
79
  }
71
80
 
72
81
  /**
@@ -83,7 +92,8 @@ export function finalizeResult(
83
92
  return {
84
93
  ...raw,
85
94
  ownership,
86
- cameraPosition: raw.cameraPosition ?? request.cameraPosition ?? context.cameraPosition,
95
+ // The camera that took it, which the guard above has made the running one.
96
+ cameraPosition: raw.cameraPosition ?? context.cameraPosition,
87
97
  timestamp: raw.timestamp ?? Date.now(),
88
98
  effectiveQuality: raw.effectiveQuality ?? clampQuality(request.quality),
89
99
  effectiveMaxDimension: raw.effectiveMaxDimension ?? clampMaxDimension(request.maxDimension),
@@ -0,0 +1,109 @@
1
+ /**
2
+ * What a camera controller is, for the app that drives it.
3
+ *
4
+ * The contract lives apart from the orchestration in `controller.ts`: it is
5
+ * what a consumer reads, and what a test double has to satisfy, while the file
6
+ * next to it is how this package happens to satisfy it.
7
+ */
8
+
9
+ import type {
10
+ CameraPermissionState,
11
+ CameraState,
12
+ CapabilityProfile,
13
+ CapturedImage,
14
+ CaptureRequest,
15
+ FlashMode,
16
+ StartOptions,
17
+ } from '../config/types';
18
+ import type { CameraError } from '../diagnostics/errors';
19
+ import type { CameraLogger } from '../diagnostics/logger';
20
+ import type { CameraEngine } from '../native/cameraEngine';
21
+ import type { SurfaceInfo, VisionSurface } from '../native/visionSurface';
22
+ import type { PermissionPort } from '../permissions/permissionPort';
23
+ import type { ArtifactStore } from '../storage/artifactStore';
24
+
25
+ export const DEFAULT_TEMPORARY_TTL_MS = 24 * 60 * 60 * 1000;
26
+
27
+ export type AppStatus = 'active' | 'background';
28
+
29
+ export interface CameraControllerOptions {
30
+ /** Required. Use the default adapters, or `createTestCamera()` in tests. */
31
+ readonly engine: CameraEngine;
32
+ readonly permissions: PermissionPort;
33
+ readonly store?: ArtifactStore | undefined;
34
+ readonly logger?: CameraLogger | undefined;
35
+ readonly temporaryTtlMs?: number;
36
+ /**
37
+ * App foreground/background source. Defaults to rn-core's shared `appLifecycle()`,
38
+ * where `inactive` counts as background. Injectable so lifecycle rules are
39
+ * testable without an app (FR-015).
40
+ */
41
+ readonly subscribeToAppState?: (listener: (status: AppStatus) => void) => () => void;
42
+ /**
43
+ * Set false to skip the sweep that runs when the controller is first used
44
+ * (tests). Defaults to true.
45
+ */
46
+ readonly sweepOnInit?: boolean;
47
+ /**
48
+ * How long to wait for a preview surface to report frames before failing the
49
+ * start (FR-014). Only applies to an engine whose session belongs to the
50
+ * surface (`awaitsSurfaceStart`). Defaults to 8 s.
51
+ */
52
+ readonly startTimeoutMs?: number;
53
+ /** Test seam for that timeout. */
54
+ readonly scheduler?: {
55
+ setTimeout(callback: () => void, ms: number): unknown;
56
+ clearTimeout(handle: unknown): void;
57
+ };
58
+ }
59
+
60
+ export interface CameraController {
61
+ readonly state: CameraState;
62
+ subscribe(listener: (state: CameraState) => void): () => void;
63
+
64
+ getPermission(): Promise<CameraPermissionState>;
65
+ requestPermission(): Promise<CameraPermissionState>;
66
+
67
+ start(options?: StartOptions): Promise<void>;
68
+ pause(): Promise<void>;
69
+ resume(): Promise<void>;
70
+ stop(): Promise<void>;
71
+
72
+ capture(request?: CaptureRequest): Promise<CapturedImage>;
73
+ cleanup(image: CapturedImage | string): Promise<void>;
74
+
75
+ getCapabilities(): Promise<CapabilityProfile>;
76
+ setFlashMode(mode: FlashMode): void;
77
+ setTorch(enabled: boolean): Promise<void>;
78
+
79
+ /** Marks the preview surface mounted, enabling foreground auto-restart. */
80
+ setSurfaceMounted(mounted: boolean): void;
81
+ /**
82
+ * Attaches the mounted preview surface to the engine, for an engine that
83
+ * captures through it. Returns the detach function; a no-op for engines that
84
+ * own their own session.
85
+ */
86
+ attachSurface(surface: VisionSurface): () => void;
87
+ /**
88
+ * The preview reported frames: the session is active for real, not merely
89
+ * requested (CAM-08). What the surface turned out to be — the format's pixel
90
+ * size, the orientation, the device's own position — is published as
91
+ * `state.surface` for coordinate mapping.
92
+ */
93
+ reportSurfaceStarted(info: SurfaceInfo): void;
94
+ /** The preview's own session failed, or the OS took the camera away. */
95
+ reportSurfaceFailed(code: CameraError['code']): void;
96
+ /**
97
+ * Called by `CameraPreview` when the native preview surface cannot load (the
98
+ * VisionCamera module is missing or unlinked). Fails the session with
99
+ * `cameraUnavailable` rather than leaving a black preview and no error.
100
+ */
101
+ reportSurfaceUnavailable(): void;
102
+ /**
103
+ * Releases ports, listeners, and the camera. Safe to call twice. Afterwards
104
+ * `start`, `resume`, `capture` and the permission and capability reads reject
105
+ * with `notReady`; `stop`, `pause` and `cleanup` stay safe no-ops so teardown
106
+ * code never has to check.
107
+ */
108
+ dispose(): Promise<void>;
109
+ }