@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 +122 -0
- package/README.md +64 -9
- package/package.json +10 -2
- package/skills/sdk-rn-camera/SKILL.md +21 -3
- package/src/config/types.ts +8 -0
- package/src/core/captureController.ts +11 -1
- package/src/core/contract.ts +109 -0
- package/src/core/controller.ts +117 -163
- package/src/core/sessionRuntime.ts +242 -0
- package/src/diagnostics/errors.ts +17 -15
- package/src/diagnostics/logger.ts +22 -1
- package/src/index.ts +3 -1
- package/src/native/cameraEngine.ts +35 -10
- package/src/native/visionCameraPeer.ts +30 -0
- package/src/native/visionSurface.ts +43 -18
- package/src/permissions/permissionPort.ts +24 -20
- package/src/react/CameraPreview.tsx +53 -9
- package/src/react/VisionCameraFeed.tsx +70 -10
- package/src/react/hooks.ts +1 -1
- package/src/storage/artifactStore.ts +9 -2
- package/src/testing/conformance.ts +140 -0
- package/src/testing/index.ts +27 -0
- package/src/testing/testCameraAdapter.ts +47 -35
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.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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`
|
|
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`, `
|
|
337
|
-
`
|
|
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.
|
|
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
|
|
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`, `
|
|
58
|
-
`
|
|
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
|
|
package/src/config/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|