@chipmobilesdk/rn-camera 0.1.1 → 0.2.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 +61 -2
- package/README.md +154 -131
- package/package.json +1 -1
- package/skills/sdk-rn-camera/SKILL.md +75 -71
- package/src/core/captureController.ts +11 -9
- package/src/core/controller.ts +186 -53
- package/src/react/CameraPreview.tsx +7 -2
- package/src/react/VisionCameraFeed.tsx +178 -155
- package/src/react/hooks.ts +24 -10
- package/src/storage/artifactStore.ts +37 -4
- package/src/testing/testCameraAdapter.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here. The package follows semantic versioning.
|
|
4
4
|
|
|
5
|
+
## [0.2.0] — 2026-11-01
|
|
6
|
+
|
|
7
|
+
Lifecycle and capture-path fixes. **No code change is required**; two behaviours
|
|
8
|
+
change (see Changed), and a store that should manage VisionCamera captures needs a
|
|
9
|
+
`FileSystemBridge` with `move()`.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`useCameraController` survives a StrictMode remount.** The cleanup disposed the
|
|
14
|
+
only controller and the remount kept it, so the screen showed `idle` forever and
|
|
15
|
+
backgrounding no longer released the camera. A disposed controller is now replaced,
|
|
16
|
+
and constructing one has no side effects (it subscribes on first use), so a
|
|
17
|
+
controller React builds and discards no longer leaks listeners.
|
|
18
|
+
- **`CameraPreview` subscribes to the controller.** It read `controller.state` once
|
|
19
|
+
per render, so the feed could stay unmounted after the session became active unless
|
|
20
|
+
the parent re-rendered. `useCameraState` now uses `useSyncExternalStore`.
|
|
21
|
+
- **A failed capture no longer leaves `capturing: true` published**, and `capturing:
|
|
22
|
+
true` is now published at all — the single-flight slot was taken after the first
|
|
23
|
+
emit. A raw native or engine error is turned into `CameraError('captureFailed')`
|
|
24
|
+
(or `storageFailure` when saving fails), with no file path in the message.
|
|
25
|
+
- **Foregrounding restarts only a session that backgrounding interrupted.** It used
|
|
26
|
+
to start any idle or stopped session while a preview was mounted — including one
|
|
27
|
+
the user had stopped, one never started, and one whose permission had been revoked.
|
|
28
|
+
A foreground that arrives while the release is still running now waits for it
|
|
29
|
+
instead of being dropped.
|
|
30
|
+
- **Use after dispose is defined.** `start`, `resume`, `capture`, and the permission
|
|
31
|
+
and capability reads reject with `CameraError('notReady')`; `stop`, `pause` and
|
|
32
|
+
`cleanup` are safe no-ops.
|
|
33
|
+
- **VisionCamera captures can be cleaned up.** They were saved in VisionCamera's own
|
|
34
|
+
temporary directory, outside the store, so `cleanup()` refused them and the 24-hour
|
|
35
|
+
sweep never saw them. The controller now moves each package-temporary capture into
|
|
36
|
+
the store (`ArtifactStore.adopt`), and the sweep spares captures adopted by the
|
|
37
|
+
running process. With no store, or a bridge without `move()`, the logger reports
|
|
38
|
+
`unmanaged:no-store` / `unmanaged:store-cannot-adopt` once.
|
|
39
|
+
- **No more "Camera is not active" error on every session start.** The feed sent
|
|
40
|
+
`torchMode="off"` on mount; VisionCamera applies it before the camera is active
|
|
41
|
+
and leaves the native rejection unhandled. The prop is now left unset until the
|
|
42
|
+
torch is first turned on.
|
|
43
|
+
- **One throwing state listener no longer breaks a transition** or starves the other
|
|
44
|
+
listeners; it is logged as `listener-threw`.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **`effectiveQuality` and `effectiveMaxDimension` report what was applied.** The
|
|
49
|
+
VisionCamera adapter echoed the request hints. Quality is fixed on the photo output
|
|
50
|
+
(0.9) and nothing resizes, so it now reports `0.9` and an unavailable max dimension.
|
|
51
|
+
- **Unmounting a preview over an idle controller leaves it `idle`**, rather than
|
|
52
|
+
driving it through `stopping` to `stopped` and calling the engine's `stop()`.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- `ArtifactStore.adopt?(uri)` and `FileSystemBridge.move?(from, to)`, both optional.
|
|
57
|
+
`createMemoryFileSystem()` implements `move`.
|
|
58
|
+
|
|
59
|
+
### Docs
|
|
60
|
+
|
|
61
|
+
- The README no longer claims quality and max-dimension hints are applied, or a
|
|
62
|
+
single active session per process.
|
|
63
|
+
|
|
5
64
|
## 0.1.0 — 2026-07-26
|
|
6
65
|
|
|
7
66
|
Initial release. New package, no existing consumers, **no migration required**.
|
|
@@ -10,8 +69,8 @@ Initial release. New package, no existing consumers, **no migration required**.
|
|
|
10
69
|
|
|
11
70
|
- **Permission handling** as a seven-state contract (`undetermined`, `requesting`, `granted`, `denied`, `deniedPermanently`, `unavailable`, `unsupported`). Reading status never prompts; requesting never re-prompts after a permanent denial; revocations made in system settings are pushed to subscribers without an app restart.
|
|
12
71
|
- **`CameraPreview`** surface with `cover`/`contain` resize modes, an overlay slot via `children`, and pass-through of consumer accessibility properties. Ships no styled UI and takes no theme dependency.
|
|
13
|
-
- **Still capture** returning a file reference plus metadata. Undeterminable fields (`fileSize`, `orientation`) are explicitly marked unavailable rather than guessed or omitted. Quality and max-dimension hints are
|
|
14
|
-
- **Session lifecycle** — start, pause, resume, stop — that releases the camera on background, unmount, and stop, restarts on foreground when a surface is still mounted, and reports a single active session per
|
|
72
|
+
- **Still capture** returning a file reference plus metadata. Undeterminable fields (`fileSize`, `orientation`) are explicitly marked unavailable rather than guessed or omitted. Quality and max-dimension hints are normalized and reported back as effective values.
|
|
73
|
+
- **Session lifecycle** — start, pause, resume, stop — that releases the camera on background, unmount, and stop, restarts on foreground when a surface is still mounted, and reports a single active session per controller.
|
|
15
74
|
- **Artifact ownership and cleanup**: an optional `destination` on the capture request switches the result to app-owned and writes there directly; everything else lands in the package cache directory. `cleanup()` is idempotent and refuses anything the package does not own. Orphans are swept at initialization under a 24-hour time-to-live.
|
|
16
75
|
- **Capability reporting** for camera positions, flash, torch, orientation, and camera-less devices, with capture-time flash modes and a continuous torch that is forced off on pause, stop, and background.
|
|
17
76
|
- **Coordinate mapping** — pure, bidirectional, accounting for scale, letterbox or crop, orientation, and front-camera mirroring, accurate within 2 device-independent pixels on a round trip.
|
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
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
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.
|
|
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.
|
|
9
6
|
|
|
10
|
-
|
|
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
11
|
- **Entrypoint**: `src/index.ts` — the only public entrypoint
|
|
12
12
|
- **Native code authored here**: none (see [Native capability](#native-capability))
|
|
13
13
|
|
|
@@ -25,14 +25,14 @@ The package is **infrastructure, not a feature**. It contains no business logic,
|
|
|
25
25
|
|
|
26
26
|
## Install (consuming app)
|
|
27
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.
|
|
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
36
|
|
|
37
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
38
|
|
|
@@ -56,57 +56,57 @@ That is the only declaration the package requires. No microphone, no photo libra
|
|
|
56
56
|
|
|
57
57
|
## Quick start
|
|
58
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
|
-
}
|
|
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
97
|
|
|
98
98
|
return (
|
|
99
99
|
<CameraPreview controller={controller} accessibilityLabel="Camera preview">
|
|
100
100
|
<ShutterButton onPress={async () => setPhoto(await controller.capture())} />
|
|
101
101
|
</CameraPreview>
|
|
102
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.
|
|
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
110
|
|
|
111
111
|
## Core concepts
|
|
112
112
|
|
|
@@ -129,42 +129,65 @@ image.fileSize.available ? image.fileSize.value : 'not reported by this platform
|
|
|
129
129
|
|
|
130
130
|
A bare optional could not tell "the platform does not report this" from "nobody asked", which is why the wrapper exists.
|
|
131
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 |
|
|
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
142
|
|
|
143
143
|
```ts
|
|
144
144
|
const temp = await controller.capture(); // package-owned
|
|
145
145
|
const kept = await controller.capture({ destination: '/my/app/x.jpg' }); // app-owned
|
|
146
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
|
-
|
|
153
|
-
temporary
|
|
154
|
-
|
|
155
|
-
`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
159
177
|
|
|
160
178
|
### The camera is released, not held
|
|
161
179
|
|
|
162
|
-
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 if the surface is still mounted.
|
|
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`.
|
|
163
184
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
168
191
|
|
|
169
192
|
### Capability gating
|
|
170
193
|
|
|
@@ -189,20 +212,20 @@ The package draws no bounding boxes, guide frames, or reticles — those are whe
|
|
|
189
212
|
const inImageSpace = mapRectToImage(myGuideFrame, geometry);
|
|
190
213
|
```
|
|
191
214
|
|
|
192
|
-
The pure mapping functions account for scale, letterbox or crop, orientation,
|
|
193
|
-
and front-camera mirroring using the geometry you supply. In the current
|
|
194
|
-
`CameraPreview`, `onGeometryChange` reports nominal image dimensions
|
|
195
|
-
(`1920 × 1080`), orientation `0`, and mirroring from camera position; it does
|
|
196
|
-
not yet read the selected native capture format. Use capture metadata or
|
|
197
|
-
app-owned measured geometry when pixel-exact alignment is required.
|
|
198
|
-
|
|
199
|
-
### Quality and dimensions
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
206
229
|
|
|
207
230
|
## Testing without hardware
|
|
208
231
|
|
|
@@ -268,7 +291,7 @@ Use this when completing your store declarations:
|
|
|
268
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) |
|
|
269
292
|
| Transmitted off device? | **No.** The package makes no network requests of any kind |
|
|
270
293
|
| Shared with third parties? | **No** |
|
|
271
|
-
| 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 |
|
|
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
295
|
| Location attached? | **No.** No location data is read and no location permission is requested |
|
|
273
296
|
| Written to the photo library? | **No** |
|
|
274
297
|
| Tracking, ads, identifiers? | **None** |
|
|
@@ -305,37 +328,37 @@ Still capture and real-time frame processing stay strictly separate.
|
|
|
305
328
|
|
|
306
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.
|
|
307
330
|
|
|
308
|
-
- Controller and state: `createCameraController`, `CameraController`,
|
|
309
|
-
`CameraControllerOptions`, `useCameraController`, `useCameraState`,
|
|
310
|
-
`CameraState`, `AppStatus`, `DEFAULT_TEMPORARY_TTL_MS`
|
|
311
|
-
- Preview and capture: `CameraPreview`, `CameraPreviewProps`, `CaptureRequest`,
|
|
312
|
-
`CapturedImage`, `StartOptions` and the camera/session/capability types
|
|
313
|
-
- Ports and adapters: `CameraEngine`, `EngineOptions`,
|
|
314
|
-
`createVisionCameraEngine`, `PermissionPort`,
|
|
315
|
-
`createVisionCameraPermissions`, `ArtifactStore`, `FileSystemBridge`,
|
|
316
|
-
`StoreOptions`, `createCacheArtifactStore`, `createArtifactStore`
|
|
317
|
-
- Mapping: `mapPointToImage`, `mapPointToPreview`, `mapRectToImage`,
|
|
318
|
-
`mapRectToPreview`, plus `Point`, `Rect`, and `PreviewGeometry`
|
|
319
|
-
- Diagnostics: `CameraError`, `CameraErrorCode`, `isCameraError`, `redact`, and
|
|
320
|
-
the logger event/type contracts
|
|
321
|
-
- Testing: `createTestCamera`, `createMemoryFileSystem`,
|
|
322
|
-
`DEFAULT_TEST_CAPABILITIES`, and their configuration/handle types
|
|
323
|
-
- Metadata helpers: `known`, `unavailable`, `Known`, `Unavailable`, `Maybe`
|
|
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`
|
|
324
347
|
|
|
325
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.
|
|
326
349
|
|
|
327
|
-
## License
|
|
328
|
-
|
|
329
|
-
`UNLICENSED`. The npm tarball is publicly downloadable, but no open-source
|
|
330
|
-
license or permission to copy, modify, or redistribute is granted.
|
|
331
|
-
|
|
332
|
-
## Demo and validation
|
|
333
|
-
|
|
334
|
-
The demo integration lives in `src/screens/CameraDemoScreen.tsx`. Run the
|
|
335
|
-
package commands from the repository root (camera currently has no root-level
|
|
336
|
-
shortcut):
|
|
337
|
-
|
|
338
|
-
```bash
|
|
339
|
-
npm run typecheck --workspace @chipmobilesdk/rn-camera
|
|
340
|
-
npm run pack:dry-run --workspace @chipmobilesdk/rn-camera
|
|
341
|
-
```
|
|
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
|
+
```
|
package/package.json
CHANGED
|
@@ -1,71 +1,75 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sdk-rn-camera
|
|
3
|
-
description: "Camera capture via @chipmobilesdk/rn-camera: controller, VisionCamera engine, permissions, artifact storage, hardware-free testing. Use when adding capture/preview, handling camera permissions, storing captures, or writing camera tests."
|
|
4
|
-
metadata:
|
|
5
|
-
package: "@chipmobilesdk/rn-camera"
|
|
6
|
-
packageVersion: "0.1.0"
|
|
7
|
-
user-invocable: true
|
|
8
|
-
disable-model-invocation: false
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# @chipmobilesdk/rn-camera
|
|
12
|
-
|
|
13
|
-
Camera capture built on VisionCamera, with an injectable engine, a permission port,
|
|
14
|
-
artifact storage for captures, and a test double that needs no hardware.
|
|
15
|
-
|
|
16
|
-
## Use this when
|
|
17
|
-
|
|
18
|
-
- Adding a capture or preview screen
|
|
19
|
-
- Requesting/handling camera permissions
|
|
20
|
-
- Persisting captured images and managing their lifetime
|
|
21
|
-
- Writing tests for a camera flow
|
|
22
|
-
- Answering a privacy/data-safety question about captures
|
|
23
|
-
|
|
24
|
-
## Release the camera on unmount
|
|
25
|
-
|
|
26
|
-
The camera is a hardware resource. It MUST be released when the screen loses focus or
|
|
27
|
-
unmounts — a retained camera drains battery and blocks other screens from acquiring it.
|
|
28
|
-
Verify teardown explicitly; this is the most common defect in camera code.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- `
|
|
51
|
-
- `
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
|
1
|
+
---
|
|
2
|
+
name: sdk-rn-camera
|
|
3
|
+
description: "Camera capture via @chipmobilesdk/rn-camera: controller, VisionCamera engine, permissions, artifact storage, hardware-free testing. Use when adding capture/preview, handling camera permissions, storing captures, or writing camera tests."
|
|
4
|
+
metadata:
|
|
5
|
+
package: "@chipmobilesdk/rn-camera"
|
|
6
|
+
packageVersion: "0.1.0"
|
|
7
|
+
user-invocable: true
|
|
8
|
+
disable-model-invocation: false
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# @chipmobilesdk/rn-camera
|
|
12
|
+
|
|
13
|
+
Camera capture built on VisionCamera, with an injectable engine, a permission port,
|
|
14
|
+
artifact storage for captures, and a test double that needs no hardware.
|
|
15
|
+
|
|
16
|
+
## Use this when
|
|
17
|
+
|
|
18
|
+
- Adding a capture or preview screen
|
|
19
|
+
- Requesting/handling camera permissions
|
|
20
|
+
- Persisting captured images and managing their lifetime
|
|
21
|
+
- Writing tests for a camera flow
|
|
22
|
+
- Answering a privacy/data-safety question about captures
|
|
23
|
+
|
|
24
|
+
## Release the camera on unmount
|
|
25
|
+
|
|
26
|
+
The camera is a hardware resource. It MUST be released when the screen loses focus or
|
|
27
|
+
unmounts — a retained camera drains battery and blocks other screens from acquiring it.
|
|
28
|
+
Verify teardown explicitly; this is the most common defect in camera code. Prefer
|
|
29
|
+
`useCameraController`: it disposes on unmount and survives StrictMode remounts. A
|
|
30
|
+
disposed controller rejects `start`/`capture` with `notReady`.
|
|
31
|
+
|
|
32
|
+
## Captures expire
|
|
33
|
+
|
|
34
|
+
`DEFAULT_TEMPORARY_TTL_MS` exists because cache-stored captures are temporary. If a
|
|
35
|
+
capture must survive, pass a `destination` (app-owned) or move it out deliberately —
|
|
36
|
+
do not assume persistence. Give the controller a `store` whose `FileSystemBridge`
|
|
37
|
+
implements `move()`, or VisionCamera captures stay outside it and `cleanup()` cannot
|
|
38
|
+
delete them. See `## Data behaviour` for what this means for App Privacy disclosures.
|
|
39
|
+
|
|
40
|
+
## Test without hardware
|
|
41
|
+
|
|
42
|
+
`createTestCamera`, `createMemoryFileSystem` and `DEFAULT_TEST_CAPABILITIES` let camera
|
|
43
|
+
flows be unit-tested with no device. Use these rather than mocking VisionCamera by hand —
|
|
44
|
+
see `## Testing without hardware`.
|
|
45
|
+
|
|
46
|
+
## Public API (v0.2.0)
|
|
47
|
+
|
|
48
|
+
Runtime:
|
|
49
|
+
|
|
50
|
+
- `createCameraController(...)`, `useCameraController`, `useCameraState`
|
|
51
|
+
- `createVisionCameraEngine(...)` — the default `CameraEngine`; injectable
|
|
52
|
+
- `createVisionCameraPermissions(...)` — implements `PermissionPort`
|
|
53
|
+
- `createArtifactStore(...)`, `createCacheArtifactStore(...)`
|
|
54
|
+
- `CameraPreview` (+ `CameraPreviewProps`)
|
|
55
|
+
- `CameraError`, `isCameraError`, `redact` — `redact` strips sensitive paths before logging
|
|
56
|
+
- Testing: `createTestCamera`, `createMemoryFileSystem`, `DEFAULT_TEST_CAPABILITIES`
|
|
57
|
+
|
|
58
|
+
Types: `CameraEngine`, `EngineOptions`, `PermissionPort`, `ArtifactStore`,
|
|
59
|
+
`FileSystemBridge`, `StoreOptions`, `CameraErrorCode`, `CameraLogEvent`,
|
|
60
|
+
`CameraLogEventType`, `CameraLogger`
|
|
61
|
+
|
|
62
|
+
Always pass camera log payloads through `redact` before logging.
|
|
63
|
+
|
|
64
|
+
## Where the detail lives
|
|
65
|
+
|
|
66
|
+
Read one section of `node_modules/@chipmobilesdk/rn-camera/README.md` (~340 lines):
|
|
67
|
+
|
|
68
|
+
| Need | Section |
|
|
69
|
+
|---|---|
|
|
70
|
+
| First integration | `## Install (consuming app)`, `## Quick start` |
|
|
71
|
+
| Engine / port / store model | `## Core concepts` |
|
|
72
|
+
| Tests | `## Testing without hardware` |
|
|
73
|
+
| Failure handling | `## Errors`, `## Diagnostics` |
|
|
74
|
+
| Privacy disclosures | `## Data behaviour (for App Privacy and Data Safety)` |
|
|
75
|
+
| Native config | `## Native capability`, `## Compatibility` |
|
|
@@ -93,26 +93,28 @@ export function finalizeResult(
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Serializes captures.
|
|
97
|
-
*
|
|
96
|
+
* Serializes captures. The slot is taken synchronously, before the operation
|
|
97
|
+
* runs, so two calls in the same tick cannot both pass the check — and the
|
|
98
|
+
* operation's own first `emit()` already sees `busy`. Holding the operation's
|
|
99
|
+
* promise instead set the slot only after that first emit, so `capturing: true`
|
|
100
|
+
* was never published at all.
|
|
98
101
|
*/
|
|
99
102
|
export class SingleFlight {
|
|
100
|
-
private
|
|
103
|
+
private active = false;
|
|
101
104
|
|
|
102
105
|
get busy(): boolean {
|
|
103
|
-
return this.
|
|
106
|
+
return this.active;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
async run(operation: () => Promise<CapturedImage>): Promise<CapturedImage> {
|
|
107
|
-
if (this.
|
|
110
|
+
if (this.active) {
|
|
108
111
|
throw new CameraError('captureInProgress', 'A capture is already in progress');
|
|
109
112
|
}
|
|
110
|
-
|
|
111
|
-
this.inFlight = promise;
|
|
113
|
+
this.active = true;
|
|
112
114
|
try {
|
|
113
|
-
return await
|
|
115
|
+
return await operation();
|
|
114
116
|
} finally {
|
|
115
|
-
this.
|
|
117
|
+
this.active = false;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
}
|