@chipmobilesdk/rn-camera 0.1.1 → 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/CHANGELOG.md +107 -34
- package/README.md +250 -227
- package/package.json +44 -47
- package/skills/sdk-rn-camera/SKILL.md +8 -5
- package/src/config/types.ts +143 -149
- package/src/core/capabilities.ts +41 -44
- package/src/core/captureController.ts +120 -118
- package/src/core/controller.ts +516 -385
- package/src/core/sessionMachine.ts +104 -111
- package/src/diagnostics/errors.ts +92 -92
- package/src/diagnostics/logger.ts +26 -31
- package/src/geometry/coordinateMapping.ts +129 -129
- package/src/index.ts +78 -78
- package/src/react/CameraPreview.tsx +126 -120
- package/src/react/VisionCameraFeed.tsx +40 -18
- package/src/react/hooks.ts +56 -44
- package/src/storage/artifactStore.ts +141 -108
- package/src/testing/testCameraAdapter.ts +256 -249
package/README.md
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
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
|
-
|
|
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
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
6
|
|
|
7
7
|
> **License:** `UNLICENSED`. Published publicly for use by the owner's
|
|
8
8
|
> applications; no open-source license is granted.
|
|
9
|
-
|
|
10
|
-
- **Status**:
|
|
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
|
-
|
|
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
28
|
```bash
|
|
29
29
|
npm install @chipmobilesdk/rn-camera react-native-vision-camera react-native-nitro-modules react-native-nitro-image
|
|
30
30
|
cd ios && pod install && cd ..
|
|
@@ -33,29 +33,29 @@ cd ios && pod install && cd ..
|
|
|
33
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
34
|
VisionCamera 5.1.x in turn requires the Nitro modules and image packages shown
|
|
35
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
|
-
|
|
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
59
|
```tsx
|
|
60
60
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
61
61
|
import {
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
useCameraState,
|
|
67
67
|
type CapturedImage,
|
|
68
68
|
} from '@chipmobilesdk/rn-camera';
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
function CameraScreen() {
|
|
71
71
|
const [photo, setPhoto] = useState<CapturedImage | null>(null);
|
|
72
72
|
const controller = useCameraController({
|
|
@@ -94,12 +94,12 @@ function CameraScreen() {
|
|
|
94
94
|
// Your copy, your layout — the package supplies state, not UI.
|
|
95
95
|
return <PermissionGate onRequest={requestPermission} state={state.permission} />;
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<CameraPreview controller={controller} accessibilityLabel="Camera preview">
|
|
100
|
-
<ShutterButton onPress={async () => setPhoto(await controller.capture())} />
|
|
101
|
-
</CameraPreview>
|
|
102
|
-
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<CameraPreview controller={controller} accessibilityLabel="Camera preview">
|
|
100
|
+
<ShutterButton onPress={async () => setPhoto(await controller.capture())} />
|
|
101
|
+
</CameraPreview>
|
|
102
|
+
);
|
|
103
103
|
}
|
|
104
104
|
```
|
|
105
105
|
|
|
@@ -107,28 +107,28 @@ function CameraScreen() {
|
|
|
107
107
|
the `active` state. `getPermission()` only reads state; it does not prompt or
|
|
108
108
|
start a session. The controller hook releases the session when the screen
|
|
109
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
|
-
|
|
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
132
|
### You decide what is kept
|
|
133
133
|
|
|
134
134
|
The returned ownership metadata is decided at capture time by one thing —
|
|
@@ -139,56 +139,79 @@ whether you pass a `destination`:
|
|
|
139
139
|
| Ownership | `packageTemporary` | `appOwned` |
|
|
140
140
|
| Default VisionCamera write | Native temporary file | Your path, directly |
|
|
141
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
|
-
|
|
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
147
|
// cleanup() only acts when the controller was given an ArtifactStore and the
|
|
148
148
|
// store reports that it owns this URI.
|
|
149
149
|
await controller.cleanup(temp);
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
temporary
|
|
154
|
-
|
|
155
|
-
`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
|
|
164
182
|
Each controller permits one active session; starting the same controller again
|
|
165
|
-
reports `sessionConflict`.
|
|
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
|
|
166
189
|
across different controller instances, so an app should mount only one active
|
|
167
190
|
camera preview at a time.
|
|
168
|
-
|
|
169
|
-
### Capability gating
|
|
170
|
-
|
|
171
|
-
Ask before you render:
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
const caps = await controller.getCapabilities();
|
|
175
|
-
if (caps.hasTorch) { /* render the torch control */ }
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
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.
|
|
179
|
-
|
|
180
|
-
### Overlays
|
|
181
|
-
|
|
182
|
-
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:
|
|
183
|
-
|
|
184
|
-
```tsx
|
|
185
|
-
<CameraPreview controller={controller} onGeometryChange={setGeometry}>
|
|
186
|
-
<View style={myGuideFrame} pointerEvents="none" />
|
|
187
|
-
</CameraPreview>;
|
|
188
|
-
|
|
189
|
-
const inImageSpace = mapRectToImage(myGuideFrame, geometry);
|
|
190
|
-
```
|
|
191
|
-
|
|
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
|
+
|
|
192
215
|
The pure mapping functions account for scale, letterbox or crop, orientation,
|
|
193
216
|
and front-camera mirroring using the geometry you supply. In the current
|
|
194
217
|
`CameraPreview`, `onGeometryChange` reports nominal image dimensions
|
|
@@ -198,113 +221,113 @@ app-owned measured geometry when pixel-exact alignment is required.
|
|
|
198
221
|
|
|
199
222
|
### Quality and dimensions
|
|
200
223
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
## Testing without hardware
|
|
208
|
-
|
|
209
|
-
`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.
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
import { createCameraController, createTestCamera } from '@chipmobilesdk/rn-camera';
|
|
213
|
-
|
|
214
|
-
const camera = createTestCamera({ permission: 'granted' });
|
|
215
|
-
const controller = createCameraController({
|
|
216
|
-
engine: camera.engine,
|
|
217
|
-
permissions: camera.permissions,
|
|
218
|
-
store: camera.store,
|
|
219
|
-
subscribeToAppState: camera.subscribeToAppState,
|
|
220
|
-
sweepOnInit: false,
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
await controller.requestPermission();
|
|
224
|
-
await controller.getCapabilities();
|
|
225
|
-
await controller.start();
|
|
226
|
-
|
|
227
|
-
const image = await controller.capture();
|
|
228
|
-
|
|
229
|
-
// Drive any failure path:
|
|
230
|
-
camera.setCaptureError('captureFailed');
|
|
231
|
-
camera.setPermission('deniedPermanently');
|
|
232
|
-
camera.setAppState('background');
|
|
233
|
-
camera.emitInterruption();
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Add a module mapping so an accidental import of the native module resolves in Jest:
|
|
237
|
-
|
|
238
|
-
```js
|
|
239
|
-
moduleNameMapper: {
|
|
240
|
-
'^react-native-vision-camera$': '<rootDir>/__tests__/__mocks__/react-native-vision-camera.ts',
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
## Errors
|
|
245
|
-
|
|
246
|
-
Every failure is a `CameraError` with a `code` and a `recoverable` flag, so you branch on the code rather than string-matching a message:
|
|
247
|
-
|
|
248
|
-
`permissionDenied`, `cameraUnavailable`, `cameraBlocked`, `cameraInUse`, `sessionConflict`, `notReady`, `captureInProgress`, `captureFailed`, `storageFailure`, `unsupportedCapability`, `interrupted`.
|
|
249
|
-
|
|
250
|
-
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).
|
|
251
|
-
|
|
252
|
-
## Diagnostics
|
|
253
|
-
|
|
254
|
-
Logging is **off by default**, and event bodies carry codes and states only — never file paths or image data:
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
createCameraController({ logger: { log: event => myLogger.debug(event) } });
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
A `redact()` helper is exported for scrubbing paths and payload-shaped strings from anything else you log.
|
|
261
|
-
|
|
262
|
-
## Data behaviour (for App Privacy and Data Safety)
|
|
263
|
-
|
|
264
|
-
Use this when completing your store declarations:
|
|
265
|
-
|
|
266
|
-
| Question | Answer for this package |
|
|
267
|
-
|---|---|
|
|
268
|
-
| What data is involved? | Camera imagery generated on-device by explicit user action, plus technical metadata (dimensions, mime type, size, orientation, timestamp, camera position) |
|
|
269
|
-
| Transmitted off device? | **No.** The package makes no network requests of any kind |
|
|
270
|
-
| Shared with third parties? | **No** |
|
|
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** |
|
|
271
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 |
|
|
272
|
-
| Location attached? | **No.** No location data is read and no location permission is requested |
|
|
273
|
-
| Written to the photo library? | **No** |
|
|
274
|
-
| Tracking, ads, identifiers? | **None** |
|
|
275
|
-
| On-device AI or inference? | **None in this phase** |
|
|
276
|
-
|
|
277
|
-
**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.
|
|
278
|
-
|
|
279
|
-
## Native capability
|
|
280
|
-
|
|
281
|
-
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.
|
|
282
|
-
|
|
283
|
-
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.
|
|
284
|
-
|
|
285
|
-
## Reserved: local frame processing (not implemented)
|
|
286
|
-
|
|
287
|
-
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.
|
|
288
|
-
|
|
289
|
-
The reserved contract:
|
|
290
|
-
|
|
291
|
-
```ts
|
|
292
|
-
// NOT EXPORTED. Reserved shape for the local-AI phase.
|
|
293
|
-
interface FrameProcessorOptions {
|
|
294
|
-
maxFps: number; // frame-rate cap, to protect the UI thread and battery
|
|
295
|
-
inferenceMaxDimension: number; // lower than preview resolution
|
|
296
|
-
onFrame: (frame: ReservedFrame) => void;
|
|
297
|
-
backpressure: 'dropLatest' | 'dropOldest';
|
|
298
|
-
// Automatically paused when the app backgrounds. Frames never leave the device.
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
Still capture and real-time frame processing stay strictly separate.
|
|
303
|
-
|
|
304
|
-
## Public API
|
|
305
|
-
|
|
306
|
-
Everything is exported from `@chipmobilesdk/rn-camera`. Modules under `core/`, `native/`, `permissions/`, `storage/`, `geometry/`, `diagnostics/`, and `testing/` are private — do not deep-import them.
|
|
307
|
-
|
|
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
|
+
|
|
308
331
|
- Controller and state: `createCameraController`, `CameraController`,
|
|
309
332
|
`CameraControllerOptions`, `useCameraController`, `useCameraState`,
|
|
310
333
|
`CameraState`, `AppStatus`, `DEFAULT_TEMPORARY_TTL_MS`
|
|
@@ -321,9 +344,9 @@ Everything is exported from `@chipmobilesdk/rn-camera`. Modules under `core/`, `
|
|
|
321
344
|
- Testing: `createTestCamera`, `createMemoryFileSystem`,
|
|
322
345
|
`DEFAULT_TEST_CAPABILITIES`, and their configuration/handle types
|
|
323
346
|
- Metadata helpers: `known`, `unavailable`, `Known`, `Unavailable`, `Maybe`
|
|
324
|
-
|
|
325
|
-
Stable for semver: types, controller, preview, mapping, errors. Experimental and may change in a minor: the diagnostics logger shape and the test adapter configuration.
|
|
326
|
-
|
|
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
|
+
|
|
327
350
|
## License
|
|
328
351
|
|
|
329
352
|
`UNLICENSED`. The npm tarball is publicly downloadable, but no open-source
|
|
@@ -337,5 +360,5 @@ shortcut):
|
|
|
337
360
|
|
|
338
361
|
```bash
|
|
339
362
|
npm run typecheck --workspace @chipmobilesdk/rn-camera
|
|
340
|
-
npm run pack:
|
|
363
|
+
npm run pack:check --workspace @chipmobilesdk/rn-camera
|
|
341
364
|
```
|
package/package.json
CHANGED
|
@@ -1,47 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@chipmobilesdk/rn-camera",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"private": false,
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"main": "src/index.ts",
|
|
7
|
-
"types": "src/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./src/index.ts",
|
|
11
|
-
"react-native": "./src/index.ts",
|
|
12
|
-
"default": "./src/index.ts"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"src/index.ts",
|
|
17
|
-
"src/config",
|
|
18
|
-
"src/core",
|
|
19
|
-
"src/diagnostics",
|
|
20
|
-
"src/geometry",
|
|
21
|
-
"src/native",
|
|
22
|
-
"src/permissions",
|
|
23
|
-
"src/react",
|
|
24
|
-
"src/storage",
|
|
25
|
-
"src/testing",
|
|
26
|
-
"skills",
|
|
27
|
-
"README.md",
|
|
28
|
-
"CHANGELOG.md"
|
|
29
|
-
],
|
|
30
|
-
"sideEffects": false,
|
|
31
|
-
"scripts": {
|
|
32
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
-
"pack:
|
|
34
|
-
},
|
|
35
|
-
"peerDependencies": {
|
|
36
|
-
"react": ">=19",
|
|
37
|
-
"react-native": ">=0.85",
|
|
38
|
-
"react-native-vision-camera": ">=5.1.1"
|
|
39
|
-
},
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"registry": "https://registry.npmjs.org/"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@chipmobilesdk/rn-camera",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"react-native": "./src/index.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/index.ts",
|
|
17
|
+
"src/config",
|
|
18
|
+
"src/core",
|
|
19
|
+
"src/diagnostics",
|
|
20
|
+
"src/geometry",
|
|
21
|
+
"src/native",
|
|
22
|
+
"src/permissions",
|
|
23
|
+
"src/react",
|
|
24
|
+
"src/storage",
|
|
25
|
+
"src/testing",
|
|
26
|
+
"skills",
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"scripts": {
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"pack:check": "npm pack --dry-run"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=19",
|
|
37
|
+
"react-native": ">=0.85",
|
|
38
|
+
"react-native-vision-camera": ">=5.1.1"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
}
|
|
44
|
+
}
|