@chipmobilesdk/rn-camera 0.2.0 → 0.2.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
@@ -1,364 +1,364 @@
1
- # @chipmobilesdk/rn-camera
2
-
3
- Reusable camera infrastructure for ChipMobileSdk-family React Native apps: permission-gated preview, still capture with consistent metadata, a session lifecycle that reliably releases the camera, app-controlled artifact retention, capability-gated flash and torch, coordinate mapping for app-drawn overlays, and a test adapter.
4
-
5
- The package is **infrastructure, not a feature**. It contains no business logic, does not know what your photos are for, sends nothing over the network, and writes nothing to the device gallery.
6
-
7
- > **License:** `UNLICENSED`. Published publicly for use by the owner's
8
- > applications; no open-source license is granted.
9
-
10
- - **Status**: `0.2.x`, public npm package (see [CHANGELOG](CHANGELOG.md))
11
- - **Entrypoint**: `src/index.ts` — the only public entrypoint
12
- - **Native code authored here**: none (see [Native capability](#native-capability))
13
-
14
- ## Compatibility
15
-
16
- | | Supported |
17
- |---|---|
18
- | React | >= 19.2 |
19
- | React Native | >= 0.85 |
20
- | Android | `minSdkVersion` **24**, `targetSdkVersion` 36 |
21
- | iOS | 15.1+ |
22
- | `react-native-vision-camera` | 5.1.x |
23
-
24
- > **Android `minSdkVersion` stays at 24.** Secondary sources claim VisionCamera v5 requires 26. That is wrong for 5.1.1: its `gradle.properties` declares `VisionCamera_minSdkVersion=23` and its `build.gradle` resolves `rootProject.ext.minSdkVersion` first. Bumping to 26 would drop Android 7.0 and 7.1 devices for no reason.
25
-
26
- ## Install (consuming app)
27
-
28
- ```bash
29
- npm install @chipmobilesdk/rn-camera react-native-vision-camera react-native-nitro-modules react-native-nitro-image
30
- cd ios && pod install && cd ..
31
- ```
32
-
33
- `react-native-vision-camera` is a **peer dependency** — your app installs and links it once, exactly as it does `react-native-keychain` for `@chipmobilesdk/rn-auth`.
34
- VisionCamera 5.1.x in turn requires the Nitro modules and image packages shown
35
- above; they remain app-owned native dependencies.
36
-
37
- > **If your app also uses `react-native-unistyles`** (as the ChipMobileSdk demo app does), both it and VisionCamera depend on `react-native-nitro-modules`, and only one Nitro version can exist in a build. After installing, verify your theme system still works on both platforms before going further.
38
-
39
- ### iOS permissions
40
-
41
- ```xml
42
- <key>NSCameraUsageDescription</key>
43
- <string>Explain, in your own words, why your app needs the camera.</string>
44
- ```
45
-
46
- That is the only declaration the package requires. No microphone, no photo library, no location.
47
-
48
- ### Android permissions
49
-
50
- ```xml
51
- <uses-permission android:name="android.permission.CAMERA" />
52
- <uses-feature android:name="android.hardware.camera" android:required="false" />
53
- ```
54
-
55
- `required="false"` keeps your app installable on camera-less devices; the package reports `hasCameraHardware: false` there so you can render an unavailable state instead of failing.
56
-
57
- ## Quick start
58
-
59
- ```tsx
60
- import React, { useCallback, useEffect, useState } from 'react';
61
- import {
62
- CameraPreview,
63
- createVisionCameraEngine,
64
- createVisionCameraPermissions,
65
- useCameraController,
66
- useCameraState,
67
- type CapturedImage,
68
- } from '@chipmobilesdk/rn-camera';
69
-
70
- function CameraScreen() {
71
- const [photo, setPhoto] = useState<CapturedImage | null>(null);
72
- const controller = useCameraController({
73
- engine: createVisionCameraEngine(),
74
- permissions: createVisionCameraPermissions(),
75
- });
76
- const state = useCameraState(controller);
77
-
78
- useEffect(() => {
79
- void (async () => {
80
- await controller.getCapabilities();
81
- if (await controller.getPermission() === 'granted') {
82
- await controller.start();
83
- }
84
- })();
85
- }, [controller]);
86
-
87
- const requestPermission = useCallback(async () => {
88
- if (await controller.requestPermission() === 'granted') {
89
- await controller.start();
90
- }
91
- }, [controller]);
92
-
93
- if (state.permission !== 'granted') {
94
- // Your copy, your layout — the package supplies state, not UI.
95
- return <PermissionGate onRequest={requestPermission} state={state.permission} />;
96
- }
97
-
98
- return (
99
- <CameraPreview controller={controller} accessibilityLabel="Camera preview">
100
- <ShutterButton onPress={async () => setPhoto(await controller.capture())} />
101
- </CameraPreview>
102
- );
103
- }
104
- ```
105
-
106
- `CameraPreview` mounts the native feed only after `controller.start()` reaches
107
- the `active` state. `getPermission()` only reads state; it does not prompt or
108
- start a session. The controller hook releases the session when the screen
109
- unmounts.
110
-
111
- ## Core concepts
112
-
113
- ### Permission is a state, not a boolean
114
-
115
- Seven distinguishable states, because each one needs different UI: `undetermined`, `requesting`, `granted`, `denied`, `deniedPermanently`, `unavailable`, `unsupported`.
116
-
117
- `getPermission()` never prompts and never opens a session — safe to call on mount. Only `requestPermission()` prompts, and it refuses to re-prompt once the OS reports a permanent denial, reporting `deniedPermanently` instead so you can route the user to system settings. Revocations made in Settings while your app runs are pushed to subscribers without a restart.
118
-
119
- ### Capture returns a file reference
120
-
121
- `capture()` resolves to a **file URI plus metadata** — never a base64 string, never an in-memory buffer. Base64 inflates an image by roughly a third and holds the whole thing in JavaScript memory, and strings that size are exactly what ends up in crash logs. If you need bytes, read the file.
122
-
123
- Metadata the platform cannot determine is **explicitly marked unavailable** rather than omitted:
124
-
125
- ```ts
126
- const image = await controller.capture();
127
- image.fileSize.available ? image.fileSize.value : 'not reported by this platform';
128
- ```
129
-
130
- A bare optional could not tell "the platform does not report this" from "nobody asked", which is why the wrapper exists.
131
-
132
- ### You decide what is kept
133
-
134
- The returned ownership metadata is decided at capture time by one thing —
135
- whether you pass a `destination`:
136
-
137
- | | No `destination` | With `destination` |
138
- |---|---|---|
139
- | Ownership | `packageTemporary` | `appOwned` |
140
- | Default VisionCamera write | Native temporary file | Your path, directly |
141
- | Automatic persistence | No | Controlled by your destination |
142
-
143
- ```ts
144
- const temp = await controller.capture(); // package-owned
145
- const kept = await controller.capture({ destination: '/my/app/x.jpg' }); // app-owned
146
-
147
- // cleanup() only acts when the controller was given an ArtifactStore and the
148
- // store reports that it owns this URI.
149
- await controller.cleanup(temp);
150
- ```
151
-
152
- With a `store`, the controller moves every package-temporary capture out of
153
- VisionCamera's own temporary directory into the store (`ArtifactStore.adopt`), so
154
- `cleanup()` can delete it and the sweep can reach orphans. That move needs a
155
- `FileSystemBridge` that implements `move()`; the app supplies the bridge, because
156
- the package takes no filesystem dependency of its own:
157
-
158
- ```ts
159
- import * as RNFS from '@dr.pogodin/react-native-fs';
160
-
161
- const store = createArtifactStore({
162
- cacheDirectory: () => RNFS.CachesDirectoryPath,
163
- exists: RNFS.exists,
164
- unlink: RNFS.unlink,
165
- list: async dir => (await RNFS.readDir(dir)).filter(e => e.isFile()).map(e => e.path),
166
- createdAt: async path => (await RNFS.stat(path)).mtime.getTime(),
167
- ensureDirectory: async dir => { if (!(await RNFS.exists(dir))) await RNFS.mkdir(dir); },
168
- move: RNFS.moveFile,
169
- });
170
- ```
171
-
172
- With no `store`, captures stay in VisionCamera's directory, `cleanup()` is a no-op,
173
- no sweep runs, and an opted-in logger reports `unmanaged:no-store` once. The sweep
174
- runs when the controller is first used, uses a 24-hour default TTL, touches only
175
- URIs the store owns, and spares captures adopted by the running process.
176
- `cleanup()` is idempotent and refuses paths outside the store root.
177
-
178
- ### The camera is released, not held
179
-
180
- Backgrounding **stops and releases** the camera rather than pausing it. Both operating systems can reclaim it anyway, and holding it risks leaving the OS camera indicator lit while your app is not in front of the user. Returning to the foreground restarts the preview only if backgrounding interrupted a running session, the surface is still mounted, and permission is still granted — a camera the user stopped stays stopped.
181
-
182
- Each controller permits one active session; starting the same controller again
183
- reports `sessionConflict`.
184
-
185
- `useCameraController` disposes its controller on unmount and replaces it if React
186
- remounts the component (StrictMode does this in development). A disposed controller
187
- rejects `start`, `resume` and `capture` with `notReady`; `stop` and `cleanup` stay
188
- safe to call. The package does not implement a process-wide lock
189
- across different controller instances, so an app should mount only one active
190
- camera preview at a time.
191
-
192
- ### Capability gating
193
-
194
- Ask before you render:
195
-
196
- ```ts
197
- const caps = await controller.getCapabilities();
198
- if (caps.hasTorch) { /* render the torch control */ }
199
- ```
200
-
201
- Invoking something the device does not have returns `unsupportedCapability` rather than an unexpected failure. Flash mode (`auto`/`on`/`off`) applies at capture time; torch is continuous during preview and is forced off whenever the session pauses, stops, or the app backgrounds.
202
-
203
- ### Overlays
204
-
205
- The package draws no bounding boxes, guide frames, or reticles — those are where apps differ. It gives you the surface (`children` renders above the feed) and the geometry:
206
-
207
- ```tsx
208
- <CameraPreview controller={controller} onGeometryChange={setGeometry}>
209
- <View style={myGuideFrame} pointerEvents="none" />
210
- </CameraPreview>;
211
-
212
- const inImageSpace = mapRectToImage(myGuideFrame, geometry);
213
- ```
214
-
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.
221
-
222
- ### Quality and dimensions
223
-
224
- The VisionCamera 5.1 adapter configures the photo output once, at quality `0.9`,
225
- and does not resize. Each capture therefore reports `effectiveQuality: 0.9` and an
226
- unavailable `effectiveMaxDimension`, whatever the request asked for; the request
227
- hints are not applied. Perform app-side image processing when an exact upload size
228
- or quality is required.
229
-
230
- ## Testing without hardware
231
-
232
- `createTestCamera()` returns real implementations of the same published ports the production adapters implement — not a parallel mock, so it cannot drift from the contract without a compile error.
233
-
234
- ```ts
235
- import { createCameraController, createTestCamera } from '@chipmobilesdk/rn-camera';
236
-
237
- const camera = createTestCamera({ permission: 'granted' });
238
- const controller = createCameraController({
239
- engine: camera.engine,
240
- permissions: camera.permissions,
241
- store: camera.store,
242
- subscribeToAppState: camera.subscribeToAppState,
243
- sweepOnInit: false,
244
- });
245
-
246
- await controller.requestPermission();
247
- await controller.getCapabilities();
248
- await controller.start();
249
-
250
- const image = await controller.capture();
251
-
252
- // Drive any failure path:
253
- camera.setCaptureError('captureFailed');
254
- camera.setPermission('deniedPermanently');
255
- camera.setAppState('background');
256
- camera.emitInterruption();
257
- ```
258
-
259
- Add a module mapping so an accidental import of the native module resolves in Jest:
260
-
261
- ```js
262
- moduleNameMapper: {
263
- '^react-native-vision-camera$': '<rootDir>/__tests__/__mocks__/react-native-vision-camera.ts',
264
- }
265
- ```
266
-
267
- ## Errors
268
-
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:
270
-
271
- `permissionDenied`, `cameraUnavailable`, `cameraBlocked`, `cameraInUse`, `sessionConflict`, `notReady`, `captureInProgress`, `captureFailed`, `storageFailure`, `unsupportedCapability`, `interrupted`.
272
-
273
- A double-tapped shutter produces `captureInProgress` on the second call. The package does not queue (which would give the user two photos they never asked for) and does not coalesce (which would hide a deliberate second capture behind an accidental one).
274
-
275
- ## Diagnostics
276
-
277
- Logging is **off by default**, and event bodies carry codes and states only — never file paths or image data:
278
-
279
- ```ts
280
- createCameraController({ logger: { log: event => myLogger.debug(event) } });
281
- ```
282
-
283
- A `redact()` helper is exported for scrubbing paths and payload-shaped strings from anything else you log.
284
-
285
- ## Data behaviour (for App Privacy and Data Safety)
286
-
287
- Use this when completing your store declarations:
288
-
289
- | Question | Answer for this package |
290
- |---|---|
291
- | What data is involved? | Camera imagery generated on-device by explicit user action, plus technical metadata (dimensions, mime type, size, orientation, timestamp, camera position) |
292
- | Transmitted off device? | **No.** The package makes no network requests of any kind |
293
- | Shared with third parties? | **No** |
294
- | Stored persistently? | A capture is a file: the default adapter uses VisionCamera's native temporary location unless the app supplies a destination. The package has no history database and does not write to the gallery |
295
- | Location attached? | **No.** No location data is read and no location permission is requested |
296
- | Written to the photo library? | **No** |
297
- | Tracking, ads, identifiers? | **None** |
298
- | On-device AI or inference? | **None in this phase** |
299
-
300
- **If your app uploads captured images off-device, that collection is yours to declare** — the package cannot know you do it. The same applies to moderation and reporting obligations if captures become user-generated content.
301
-
302
- ## Native capability
303
-
304
- This package authors **zero native source**. Camera capability arrives entirely through the `react-native-vision-camera` peer dependency, mirroring how `@chipmobilesdk/rn-auth` obtains Keychain and OIDC capability.
305
-
306
- The ChipMobileSdk constitution prohibits native code in shared libraries by default. A camera cannot be implemented in TypeScript, so this is the exception in its narrowest available form: a maintained MIT library the consuming app installs, wrapped behind port interfaces this package owns.
307
-
308
- ## Reserved: local frame processing (not implemented)
309
-
310
- Phase one runs **no inference, emits no AI-derived labels, and sends no frame anywhere**. The extension point is documented now so the later local-AI phase can arrive as an additive minor release without disturbing anything above.
311
-
312
- The reserved contract:
313
-
314
- ```ts
315
- // NOT EXPORTED. Reserved shape for the local-AI phase.
316
- interface FrameProcessorOptions {
317
- maxFps: number; // frame-rate cap, to protect the UI thread and battery
318
- inferenceMaxDimension: number; // lower than preview resolution
319
- onFrame: (frame: ReservedFrame) => void;
320
- backpressure: 'dropLatest' | 'dropOldest';
321
- // Automatically paused when the app backgrounds. Frames never leave the device.
322
- }
323
- ```
324
-
325
- Still capture and real-time frame processing stay strictly separate.
326
-
327
- ## Public API
328
-
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.
330
-
331
- - Controller and state: `createCameraController`, `CameraController`,
332
- `CameraControllerOptions`, `useCameraController`, `useCameraState`,
333
- `CameraState`, `AppStatus`, `DEFAULT_TEMPORARY_TTL_MS`
334
- - Preview and capture: `CameraPreview`, `CameraPreviewProps`, `CaptureRequest`,
335
- `CapturedImage`, `StartOptions` and the camera/session/capability types
336
- - Ports and adapters: `CameraEngine`, `EngineOptions`,
337
- `createVisionCameraEngine`, `PermissionPort`,
338
- `createVisionCameraPermissions`, `ArtifactStore`, `FileSystemBridge`,
339
- `StoreOptions`, `createCacheArtifactStore`, `createArtifactStore`
340
- - Mapping: `mapPointToImage`, `mapPointToPreview`, `mapRectToImage`,
341
- `mapRectToPreview`, plus `Point`, `Rect`, and `PreviewGeometry`
342
- - Diagnostics: `CameraError`, `CameraErrorCode`, `isCameraError`, `redact`, and
343
- the logger event/type contracts
344
- - Testing: `createTestCamera`, `createMemoryFileSystem`,
345
- `DEFAULT_TEST_CAPABILITIES`, and their configuration/handle types
346
- - Metadata helpers: `known`, `unavailable`, `Known`, `Unavailable`, `Maybe`
347
-
348
- Stable for semver: types, controller, preview, mapping, errors. Experimental and may change in a minor: the diagnostics logger shape and the test adapter configuration.
349
-
350
- ## License
351
-
352
- `UNLICENSED`. The npm tarball is publicly downloadable, but no open-source
353
- license or permission to copy, modify, or redistribute is granted.
354
-
355
- ## Demo and validation
356
-
357
- The demo integration lives in `src/screens/CameraDemoScreen.tsx`. Run the
358
- package commands from the repository root (camera currently has no root-level
359
- shortcut):
360
-
361
- ```bash
362
- npm run typecheck --workspace @chipmobilesdk/rn-camera
363
- npm run pack:dry-run --workspace @chipmobilesdk/rn-camera
364
- ```
1
+ # @chipmobilesdk/rn-camera
2
+
3
+ Reusable camera infrastructure for ChipMobileSdk-family React Native apps: permission-gated preview, still capture with consistent metadata, a session lifecycle that reliably releases the camera, app-controlled artifact retention, capability-gated flash and torch, coordinate mapping for app-drawn overlays, and a test adapter.
4
+
5
+ The package is **infrastructure, not a feature**. It contains no business logic, does not know what your photos are for, sends nothing over the network, and writes nothing to the device gallery.
6
+
7
+ > **License:** `UNLICENSED`. Published publicly for use by the owner's
8
+ > applications; no open-source license is granted.
9
+
10
+ - **Status**: public npm package; version in [CHANGELOG](CHANGELOG.md)
11
+ - **Entrypoint**: `src/index.ts` — the only public entrypoint
12
+ - **Native code authored here**: none (see [Native capability](#native-capability))
13
+
14
+ ## Compatibility
15
+
16
+ | | Supported |
17
+ |---|---|
18
+ | React | >= 19.2 |
19
+ | React Native | >= 0.85 |
20
+ | Android | `minSdkVersion` **24**, `targetSdkVersion` 36 |
21
+ | iOS | 15.1+ |
22
+ | `react-native-vision-camera` | 5.1.x |
23
+
24
+ > **Android `minSdkVersion` stays at 24.** Secondary sources claim VisionCamera v5 requires 26. That is wrong for 5.1.1: its `gradle.properties` declares `VisionCamera_minSdkVersion=23` and its `build.gradle` resolves `rootProject.ext.minSdkVersion` first. Bumping to 26 would drop Android 7.0 and 7.1 devices for no reason.
25
+
26
+ ## Install (consuming app)
27
+
28
+ ```bash
29
+ npm install @chipmobilesdk/rn-camera react-native-vision-camera react-native-nitro-modules react-native-nitro-image
30
+ cd ios && pod install && cd ..
31
+ ```
32
+
33
+ `react-native-vision-camera` is a **peer dependency** — your app installs and links it once, exactly as it does `react-native-keychain` for `@chipmobilesdk/rn-auth`.
34
+ VisionCamera 5.1.x in turn requires the Nitro modules and image packages shown
35
+ above; they remain app-owned native dependencies.
36
+
37
+ > **If your app also uses `react-native-unistyles`** (as the ChipMobileSdk demo app does), both it and VisionCamera depend on `react-native-nitro-modules`, and only one Nitro version can exist in a build. After installing, verify your theme system still works on both platforms before going further.
38
+
39
+ ### iOS permissions
40
+
41
+ ```xml
42
+ <key>NSCameraUsageDescription</key>
43
+ <string>Explain, in your own words, why your app needs the camera.</string>
44
+ ```
45
+
46
+ That is the only declaration the package requires. No microphone, no photo library, no location.
47
+
48
+ ### Android permissions
49
+
50
+ ```xml
51
+ <uses-permission android:name="android.permission.CAMERA" />
52
+ <uses-feature android:name="android.hardware.camera" android:required="false" />
53
+ ```
54
+
55
+ `required="false"` keeps your app installable on camera-less devices; the package reports `hasCameraHardware: false` there so you can render an unavailable state instead of failing.
56
+
57
+ ## Quick start
58
+
59
+ ```tsx
60
+ import React, { useCallback, useEffect, useState } from 'react';
61
+ import {
62
+ CameraPreview,
63
+ createVisionCameraEngine,
64
+ createVisionCameraPermissions,
65
+ useCameraController,
66
+ useCameraState,
67
+ type CapturedImage,
68
+ } from '@chipmobilesdk/rn-camera';
69
+
70
+ function CameraScreen() {
71
+ const [photo, setPhoto] = useState<CapturedImage | null>(null);
72
+ const controller = useCameraController({
73
+ engine: createVisionCameraEngine(),
74
+ permissions: createVisionCameraPermissions(),
75
+ });
76
+ const state = useCameraState(controller);
77
+
78
+ useEffect(() => {
79
+ void (async () => {
80
+ await controller.getCapabilities();
81
+ if (await controller.getPermission() === 'granted') {
82
+ await controller.start();
83
+ }
84
+ })();
85
+ }, [controller]);
86
+
87
+ const requestPermission = useCallback(async () => {
88
+ if (await controller.requestPermission() === 'granted') {
89
+ await controller.start();
90
+ }
91
+ }, [controller]);
92
+
93
+ if (state.permission !== 'granted') {
94
+ // Your copy, your layout — the package supplies state, not UI.
95
+ return <PermissionGate onRequest={requestPermission} state={state.permission} />;
96
+ }
97
+
98
+ return (
99
+ <CameraPreview controller={controller} accessibilityLabel="Camera preview">
100
+ <ShutterButton onPress={async () => setPhoto(await controller.capture())} />
101
+ </CameraPreview>
102
+ );
103
+ }
104
+ ```
105
+
106
+ `CameraPreview` mounts the native feed only after `controller.start()` reaches
107
+ the `active` state. `getPermission()` only reads state; it does not prompt or
108
+ start a session. The controller hook releases the session when the screen
109
+ unmounts.
110
+
111
+ ## Core concepts
112
+
113
+ ### Permission is a state, not a boolean
114
+
115
+ Seven distinguishable states, because each one needs different UI: `undetermined`, `requesting`, `granted`, `denied`, `deniedPermanently`, `unavailable`, `unsupported`.
116
+
117
+ `getPermission()` never prompts and never opens a session — safe to call on mount. Only `requestPermission()` prompts, and it refuses to re-prompt once the OS reports a permanent denial, reporting `deniedPermanently` instead so you can route the user to system settings. Revocations made in Settings while your app runs are pushed to subscribers without a restart.
118
+
119
+ ### Capture returns a file reference
120
+
121
+ `capture()` resolves to a **file URI plus metadata** — never a base64 string, never an in-memory buffer. Base64 inflates an image by roughly a third and holds the whole thing in JavaScript memory, and strings that size are exactly what ends up in crash logs. If you need bytes, read the file.
122
+
123
+ Metadata the platform cannot determine is **explicitly marked unavailable** rather than omitted:
124
+
125
+ ```ts
126
+ const image = await controller.capture();
127
+ image.fileSize.available ? image.fileSize.value : 'not reported by this platform';
128
+ ```
129
+
130
+ A bare optional could not tell "the platform does not report this" from "nobody asked", which is why the wrapper exists.
131
+
132
+ ### You decide what is kept
133
+
134
+ The returned ownership metadata is decided at capture time by one thing —
135
+ whether you pass a `destination`:
136
+
137
+ | | No `destination` | With `destination` |
138
+ |---|---|---|
139
+ | Ownership | `packageTemporary` | `appOwned` |
140
+ | Default VisionCamera write | Native temporary file | Your path, directly |
141
+ | Automatic persistence | No | Controlled by your destination |
142
+
143
+ ```ts
144
+ const temp = await controller.capture(); // package-owned
145
+ const kept = await controller.capture({ destination: '/my/app/x.jpg' }); // app-owned
146
+
147
+ // cleanup() only acts when the controller was given an ArtifactStore and the
148
+ // store reports that it owns this URI.
149
+ await controller.cleanup(temp);
150
+ ```
151
+
152
+ With a `store`, the controller moves every package-temporary capture out of
153
+ VisionCamera's own temporary directory into the store (`ArtifactStore.adopt`), so
154
+ `cleanup()` can delete it and the sweep can reach orphans. That move needs a
155
+ `FileSystemBridge` that implements `move()`; the app supplies the bridge, because
156
+ the package takes no filesystem dependency of its own:
157
+
158
+ ```ts
159
+ import * as RNFS from '@dr.pogodin/react-native-fs';
160
+
161
+ const store = createArtifactStore({
162
+ cacheDirectory: () => RNFS.CachesDirectoryPath,
163
+ exists: RNFS.exists,
164
+ unlink: RNFS.unlink,
165
+ list: async dir => (await RNFS.readDir(dir)).filter(e => e.isFile()).map(e => e.path),
166
+ createdAt: async path => (await RNFS.stat(path)).mtime.getTime(),
167
+ ensureDirectory: async dir => { if (!(await RNFS.exists(dir))) await RNFS.mkdir(dir); },
168
+ move: RNFS.moveFile,
169
+ });
170
+ ```
171
+
172
+ With no `store`, captures stay in VisionCamera's directory, `cleanup()` is a no-op,
173
+ no sweep runs, and an opted-in logger reports `unmanaged:no-store` once. The sweep
174
+ runs when the controller is first used, uses a 24-hour default TTL, touches only
175
+ URIs the store owns, and spares captures adopted by the running process.
176
+ `cleanup()` is idempotent and refuses paths outside the store root.
177
+
178
+ ### The camera is released, not held
179
+
180
+ Backgrounding **stops and releases** the camera rather than pausing it. Both operating systems can reclaim it anyway, and holding it risks leaving the OS camera indicator lit while your app is not in front of the user. Returning to the foreground restarts the preview only if backgrounding interrupted a running session, the surface is still mounted, and permission is still granted — a camera the user stopped stays stopped.
181
+
182
+ Each controller permits one active session; starting the same controller again
183
+ reports `sessionConflict`.
184
+
185
+ `useCameraController` disposes its controller on unmount and replaces it if React
186
+ remounts the component (StrictMode does this in development). A disposed controller
187
+ rejects `start`, `resume` and `capture` with `notReady`; `stop` and `cleanup` stay
188
+ safe to call. The package does not implement a process-wide lock
189
+ across different controller instances, so an app should mount only one active
190
+ camera preview at a time.
191
+
192
+ ### Capability gating
193
+
194
+ Ask before you render:
195
+
196
+ ```ts
197
+ const caps = await controller.getCapabilities();
198
+ if (caps.hasTorch) { /* render the torch control */ }
199
+ ```
200
+
201
+ Invoking something the device does not have returns `unsupportedCapability` rather than an unexpected failure. Flash mode (`auto`/`on`/`off`) applies at capture time; torch is continuous during preview and is forced off whenever the session pauses, stops, or the app backgrounds.
202
+
203
+ ### Overlays
204
+
205
+ The package draws no bounding boxes, guide frames, or reticles — those are where apps differ. It gives you the surface (`children` renders above the feed) and the geometry:
206
+
207
+ ```tsx
208
+ <CameraPreview controller={controller} onGeometryChange={setGeometry}>
209
+ <View style={myGuideFrame} pointerEvents="none" />
210
+ </CameraPreview>;
211
+
212
+ const inImageSpace = mapRectToImage(myGuideFrame, geometry);
213
+ ```
214
+
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.
221
+
222
+ ### Quality and dimensions
223
+
224
+ The VisionCamera 5.1 adapter configures the photo output once, at quality `0.9`,
225
+ and does not resize. Each capture therefore reports `effectiveQuality: 0.9` and an
226
+ unavailable `effectiveMaxDimension`, whatever the request asked for; the request
227
+ hints are not applied. Perform app-side image processing when an exact upload size
228
+ or quality is required.
229
+
230
+ ## Testing without hardware
231
+
232
+ `createTestCamera()` returns real implementations of the same published ports the production adapters implement — not a parallel mock, so it cannot drift from the contract without a compile error.
233
+
234
+ ```ts
235
+ import { createCameraController, createTestCamera } from '@chipmobilesdk/rn-camera';
236
+
237
+ const camera = createTestCamera({ permission: 'granted' });
238
+ const controller = createCameraController({
239
+ engine: camera.engine,
240
+ permissions: camera.permissions,
241
+ store: camera.store,
242
+ subscribeToAppState: camera.subscribeToAppState,
243
+ sweepOnInit: false,
244
+ });
245
+
246
+ await controller.requestPermission();
247
+ await controller.getCapabilities();
248
+ await controller.start();
249
+
250
+ const image = await controller.capture();
251
+
252
+ // Drive any failure path:
253
+ camera.setCaptureError('captureFailed');
254
+ camera.setPermission('deniedPermanently');
255
+ camera.setAppState('background');
256
+ camera.emitInterruption();
257
+ ```
258
+
259
+ Add a module mapping so an accidental import of the native module resolves in Jest:
260
+
261
+ ```js
262
+ moduleNameMapper: {
263
+ '^react-native-vision-camera$': '<rootDir>/__tests__/__mocks__/react-native-vision-camera.ts',
264
+ }
265
+ ```
266
+
267
+ ## Errors
268
+
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:
270
+
271
+ `permissionDenied`, `cameraUnavailable`, `cameraBlocked`, `cameraInUse`, `sessionConflict`, `notReady`, `captureInProgress`, `captureFailed`, `storageFailure`, `unsupportedCapability`, `interrupted`.
272
+
273
+ A double-tapped shutter produces `captureInProgress` on the second call. The package does not queue (which would give the user two photos they never asked for) and does not coalesce (which would hide a deliberate second capture behind an accidental one).
274
+
275
+ ## Diagnostics
276
+
277
+ Logging is **off by default**, and event bodies carry codes and states only — never file paths or image data:
278
+
279
+ ```ts
280
+ createCameraController({ logger: { log: event => myLogger.debug(event) } });
281
+ ```
282
+
283
+ A `redact()` helper is exported for scrubbing paths and payload-shaped strings from anything else you log.
284
+
285
+ ## Data behaviour (for App Privacy and Data Safety)
286
+
287
+ Use this when completing your store declarations:
288
+
289
+ | Question | Answer for this package |
290
+ |---|---|
291
+ | What data is involved? | Camera imagery generated on-device by explicit user action, plus technical metadata (dimensions, mime type, size, orientation, timestamp, camera position) |
292
+ | Transmitted off device? | **No.** The package makes no network requests of any kind |
293
+ | Shared with third parties? | **No** |
294
+ | Stored persistently? | A capture is a file: the default adapter uses VisionCamera's native temporary location unless the app supplies a destination. The package has no history database and does not write to the gallery |
295
+ | Location attached? | **No.** No location data is read and no location permission is requested |
296
+ | Written to the photo library? | **No** |
297
+ | Tracking, ads, identifiers? | **None** |
298
+ | On-device AI or inference? | **None in this phase** |
299
+
300
+ **If your app uploads captured images off-device, that collection is yours to declare** — the package cannot know you do it. The same applies to moderation and reporting obligations if captures become user-generated content.
301
+
302
+ ## Native capability
303
+
304
+ This package authors **zero native source**. Camera capability arrives entirely through the `react-native-vision-camera` peer dependency, mirroring how `@chipmobilesdk/rn-auth` obtains Keychain and OIDC capability.
305
+
306
+ The ChipMobileSdk constitution prohibits native code in shared libraries by default. A camera cannot be implemented in TypeScript, so this is the exception in its narrowest available form: a maintained MIT library the consuming app installs, wrapped behind port interfaces this package owns.
307
+
308
+ ## Reserved: local frame processing (not implemented)
309
+
310
+ Phase one runs **no inference, emits no AI-derived labels, and sends no frame anywhere**. The extension point is documented now so the later local-AI phase can arrive as an additive minor release without disturbing anything above.
311
+
312
+ The reserved contract:
313
+
314
+ ```ts
315
+ // NOT EXPORTED. Reserved shape for the local-AI phase.
316
+ interface FrameProcessorOptions {
317
+ maxFps: number; // frame-rate cap, to protect the UI thread and battery
318
+ inferenceMaxDimension: number; // lower than preview resolution
319
+ onFrame: (frame: ReservedFrame) => void;
320
+ backpressure: 'dropLatest' | 'dropOldest';
321
+ // Automatically paused when the app backgrounds. Frames never leave the device.
322
+ }
323
+ ```
324
+
325
+ Still capture and real-time frame processing stay strictly separate.
326
+
327
+ ## Public API
328
+
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.
330
+
331
+ - Controller and state: `createCameraController`, `CameraController`,
332
+ `CameraControllerOptions`, `useCameraController`, `useCameraState`,
333
+ `CameraState`, `AppStatus`, `DEFAULT_TEMPORARY_TTL_MS`
334
+ - Preview and capture: `CameraPreview`, `CameraPreviewProps`, `CaptureRequest`,
335
+ `CapturedImage`, `StartOptions` and the camera/session/capability types
336
+ - Ports and adapters: `CameraEngine`, `EngineOptions`,
337
+ `createVisionCameraEngine`, `PermissionPort`,
338
+ `createVisionCameraPermissions`, `ArtifactStore`, `FileSystemBridge`,
339
+ `StoreOptions`, `createCacheArtifactStore`, `createArtifactStore`
340
+ - Mapping: `mapPointToImage`, `mapPointToPreview`, `mapRectToImage`,
341
+ `mapRectToPreview`, plus `Point`, `Rect`, and `PreviewGeometry`
342
+ - Diagnostics: `CameraError`, `CameraErrorCode`, `isCameraError`, `redact`, and
343
+ the logger event/type contracts
344
+ - Testing: `createTestCamera`, `createMemoryFileSystem`,
345
+ `DEFAULT_TEST_CAPABILITIES`, and their configuration/handle types
346
+ - Metadata helpers: `known`, `unavailable`, `Known`, `Unavailable`, `Maybe`
347
+
348
+ Stable for semver: types, controller, preview, mapping, errors. Experimental and may change in a minor: the diagnostics logger shape and the test adapter configuration.
349
+
350
+ ## License
351
+
352
+ `UNLICENSED`. The npm tarball is publicly downloadable, but no open-source
353
+ license or permission to copy, modify, or redistribute is granted.
354
+
355
+ ## Demo and validation
356
+
357
+ The demo integration lives in `src/screens/CameraDemoScreen.tsx`. Run the
358
+ package commands from the repository root (camera currently has no root-level
359
+ shortcut):
360
+
361
+ ```bash
362
+ npm run typecheck --workspace @chipmobilesdk/rn-camera
363
+ npm run pack:check --workspace @chipmobilesdk/rn-camera
364
+ ```