react-native-image-stitcher 0.1.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 +96 -0
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +189 -0
- package/RNImageStitcher.podspec +76 -0
- package/android/build.gradle +224 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +124 -0
- package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
- package/cpp/ar_frame_pose.h +63 -0
- package/cpp/keyframe_gate.cpp +927 -0
- package/cpp/keyframe_gate.hpp +240 -0
- package/cpp/stitcher.cpp +2207 -0
- package/cpp/stitcher.hpp +275 -0
- package/dist/ar/useARSession.d.ts +102 -0
- package/dist/ar/useARSession.js +133 -0
- package/dist/camera/ARCameraView.d.ts +93 -0
- package/dist/camera/ARCameraView.js +170 -0
- package/dist/camera/Camera.d.ts +134 -0
- package/dist/camera/Camera.js +688 -0
- package/dist/camera/CameraShutter.d.ts +80 -0
- package/dist/camera/CameraShutter.js +237 -0
- package/dist/camera/CameraView.d.ts +65 -0
- package/dist/camera/CameraView.js +117 -0
- package/dist/camera/CaptureControlsBar.d.ts +87 -0
- package/dist/camera/CaptureControlsBar.js +82 -0
- package/dist/camera/CaptureHeader.d.ts +62 -0
- package/dist/camera/CaptureHeader.js +81 -0
- package/dist/camera/CapturePreview.d.ts +70 -0
- package/dist/camera/CapturePreview.js +188 -0
- package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
- package/dist/camera/CaptureStatusOverlay.js +326 -0
- package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
- package/dist/camera/CaptureThumbnailStrip.js +177 -0
- package/dist/camera/IncrementalPanGuide.d.ts +83 -0
- package/dist/camera/IncrementalPanGuide.js +267 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
- package/dist/camera/PanoramaBandOverlay.js +399 -0
- package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
- package/dist/camera/PanoramaConfirmModal.js +128 -0
- package/dist/camera/PanoramaGuidance.d.ts +79 -0
- package/dist/camera/PanoramaGuidance.js +246 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
- package/dist/camera/PanoramaSettingsModal.js +611 -0
- package/dist/camera/ViewportCropOverlay.d.ts +46 -0
- package/dist/camera/ViewportCropOverlay.js +67 -0
- package/dist/camera/useCapture.d.ts +111 -0
- package/dist/camera/useCapture.js +160 -0
- package/dist/camera/useDeviceOrientation.d.ts +48 -0
- package/dist/camera/useDeviceOrientation.js +131 -0
- package/dist/camera/useVideoCapture.d.ts +79 -0
- package/dist/camera/useVideoCapture.js +151 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +39 -0
- package/dist/quality/normaliseOrientation.d.ts +36 -0
- package/dist/quality/normaliseOrientation.js +62 -0
- package/dist/quality/runQualityCheck.d.ts +41 -0
- package/dist/quality/runQualityCheck.js +98 -0
- package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
- package/dist/sensors/useIMUTranslationGate.js +235 -0
- package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
- package/dist/stitching/IncrementalStitcherView.js +157 -0
- package/dist/stitching/incremental.d.ts +930 -0
- package/dist/stitching/incremental.js +133 -0
- package/dist/stitching/stitchFrames.d.ts +55 -0
- package/dist/stitching/stitchFrames.js +56 -0
- package/dist/stitching/stitchVideo.d.ts +119 -0
- package/dist/stitching/stitchVideo.js +57 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
- package/dist/stitching/useIncrementalJSDriver.js +199 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
- package/dist/stitching/useIncrementalStitcher.js +172 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +15 -0
- package/ios/Package.swift +72 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
- package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
- package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
- package/package.json +73 -0
- package/react-native.config.js +34 -0
- package/scripts/opencv-version.txt +1 -0
- package/scripts/postinstall-fetch-binaries.js +286 -0
- package/src/ar/useARSession.ts +210 -0
- package/src/camera/.gitkeep +0 -0
- package/src/camera/ARCameraView.tsx +256 -0
- package/src/camera/Camera.tsx +1053 -0
- package/src/camera/CameraShutter.tsx +292 -0
- package/src/camera/CameraView.tsx +157 -0
- package/src/camera/CaptureControlsBar.tsx +204 -0
- package/src/camera/CaptureHeader.tsx +184 -0
- package/src/camera/CapturePreview.tsx +318 -0
- package/src/camera/CaptureStatusOverlay.tsx +391 -0
- package/src/camera/CaptureThumbnailStrip.tsx +277 -0
- package/src/camera/IncrementalPanGuide.tsx +328 -0
- package/src/camera/PanoramaBandOverlay.tsx +498 -0
- package/src/camera/PanoramaConfirmModal.tsx +206 -0
- package/src/camera/PanoramaGuidance.tsx +327 -0
- package/src/camera/PanoramaSettingsModal.tsx +1357 -0
- package/src/camera/ViewportCropOverlay.tsx +81 -0
- package/src/camera/useCapture.ts +279 -0
- package/src/camera/useDeviceOrientation.ts +140 -0
- package/src/camera/useVideoCapture.ts +236 -0
- package/src/index.ts +53 -0
- package/src/quality/.gitkeep +0 -0
- package/src/quality/normaliseOrientation.ts +79 -0
- package/src/quality/runQualityCheck.ts +131 -0
- package/src/sensors/useIMUTranslationGate.ts +347 -0
- package/src/stitching/.gitkeep +0 -0
- package/src/stitching/IncrementalStitcherView.tsx +198 -0
- package/src/stitching/incremental.ts +1021 -0
- package/src/stitching/stitchFrames.ts +88 -0
- package/src/stitching/stitchVideo.ts +153 -0
- package/src/stitching/useIncrementalJSDriver.ts +273 -0
- package/src/stitching/useIncrementalStitcher.ts +252 -0
- package/src/types.ts +78 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* useVideoCapture — video recording + frame extraction API.
|
|
4
|
+
*
|
|
5
|
+
* Sibling of ``useCapture`` for the "sweep a shelf with a video" flow
|
|
6
|
+
* that feeds the stitcher. The hook's state machine is:
|
|
7
|
+
*
|
|
8
|
+
* idle → recording → stopping → idle (success)
|
|
9
|
+
* idle → recording → idle (recording errored)
|
|
10
|
+
* idle → recording → stopping → idle (user cancelled)
|
|
11
|
+
*
|
|
12
|
+
* API shape — `startRecording` returns a `Promise<VideoFile>` that:
|
|
13
|
+
* - resolves with the recorded file when the user releases the
|
|
14
|
+
* shutter and `stopRecording()` is called (vision-camera fires
|
|
15
|
+
* `onRecordingFinished`),
|
|
16
|
+
* - rejects if vision-camera fires `onRecordingError` at any point
|
|
17
|
+
* (e.g. `<Camera>` was rendered without `video={true}`, disk
|
|
18
|
+
* full mid-recording, permission revoked).
|
|
19
|
+
*
|
|
20
|
+
* Why a single future instead of separate finished/error callbacks?
|
|
21
|
+
* Lets host code use the natural async/await pattern. Earlier we
|
|
22
|
+
* used a callback + a parked resolver in the host screen; that
|
|
23
|
+
* masked start-time errors (the resolver hung forever) and produced
|
|
24
|
+
* confusing cascade errors when `stopRecording` ran against a
|
|
25
|
+
* non-recording camera.
|
|
26
|
+
*
|
|
27
|
+
* Frame extraction lives behind the `extractFrames` method but is
|
|
28
|
+
* deferred to the higher-level `stitchVideo()` SDK API now — host
|
|
29
|
+
* apps shouldn't have to manage their own tmp dir. This shim is
|
|
30
|
+
* kept for parity / future use; it currently throws.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { useCallback, useRef, useState } from 'react';
|
|
34
|
+
import { Camera, type VideoFile } from 'react-native-vision-camera';
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export type VideoCaptureState =
|
|
38
|
+
| 'idle'
|
|
39
|
+
| 'recording'
|
|
40
|
+
| 'stopping'
|
|
41
|
+
| 'extracting';
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
export interface UseVideoCaptureReturn {
|
|
45
|
+
state: VideoCaptureState;
|
|
46
|
+
/**
|
|
47
|
+
* Begin recording on the given camera ref. Returns a Promise that
|
|
48
|
+
* resolves with the resulting `VideoFile` once `stopRecording()`
|
|
49
|
+
* has been called and vision-camera writes the file to disk, or
|
|
50
|
+
* rejects on any recording error.
|
|
51
|
+
*
|
|
52
|
+
* Callers who only need fire-and-forget can ignore the returned
|
|
53
|
+
* promise; the hook still tracks state internally.
|
|
54
|
+
*/
|
|
55
|
+
startRecording: (
|
|
56
|
+
cameraRef: React.RefObject<Camera | null>,
|
|
57
|
+
) => Promise<VideoFile>;
|
|
58
|
+
/**
|
|
59
|
+
* Tell vision-camera to stop the active recording. If no recording
|
|
60
|
+
* is in progress (e.g. it errored at start) this is a safe no-op.
|
|
61
|
+
* The recorded file flows back through the promise returned by
|
|
62
|
+
* `startRecording`, NOT through this method's return.
|
|
63
|
+
*/
|
|
64
|
+
stopRecording: (
|
|
65
|
+
cameraRef: React.RefObject<Camera | null>,
|
|
66
|
+
) => Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Cancel the active recording without delivering the file. The
|
|
69
|
+
* promise from `startRecording` will reject with a cancellation
|
|
70
|
+
* error so awaiters unblock instead of receiving a stale file.
|
|
71
|
+
*/
|
|
72
|
+
cancelRecording: (
|
|
73
|
+
cameraRef: React.RefObject<Camera | null>,
|
|
74
|
+
) => Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Decode an mp4 into N evenly-spaced still frames, written to
|
|
77
|
+
* ``outputDir``. Throws NOT_IMPLEMENTED — call `stitchVideo` from
|
|
78
|
+
* the SDK index instead, which combines extract + stitch in one
|
|
79
|
+
* native bridge call so the JS thread never has to round-trip
|
|
80
|
+
* frame paths.
|
|
81
|
+
*/
|
|
82
|
+
extractFrames: (opts: ExtractFramesOptions) => Promise<ExtractFramesResult>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
export interface ExtractFramesOptions {
|
|
87
|
+
videoPath: string;
|
|
88
|
+
outputDir: string;
|
|
89
|
+
frameCount: number;
|
|
90
|
+
/** JPEG quality [0-100]. Defaults to 85. */
|
|
91
|
+
quality?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
export interface ExtractFramesResult {
|
|
96
|
+
framePaths: string[];
|
|
97
|
+
durationMs: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
export function useVideoCapture(): UseVideoCaptureReturn {
|
|
102
|
+
const [state, setState] = useState<VideoCaptureState>('idle');
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The active recording's resolve/reject handles. Set when
|
|
106
|
+
* `startRecording` is called; cleared when vision-camera fires
|
|
107
|
+
* `onRecordingFinished` / `onRecordingError`, or when the host
|
|
108
|
+
* calls `cancelRecording`.
|
|
109
|
+
*
|
|
110
|
+
* Stored in a ref (not state) because vision-camera's callbacks
|
|
111
|
+
* fire outside React's render cycle and need synchronous access
|
|
112
|
+
* to the latest handles.
|
|
113
|
+
*/
|
|
114
|
+
const recordingFutureRef = useRef<{
|
|
115
|
+
resolve: (video: VideoFile) => void;
|
|
116
|
+
reject: (err: unknown) => void;
|
|
117
|
+
} | null>(null);
|
|
118
|
+
|
|
119
|
+
const startRecording = useCallback(
|
|
120
|
+
async (cameraRef: React.RefObject<Camera | null>): Promise<VideoFile> => {
|
|
121
|
+
if (!cameraRef.current) {
|
|
122
|
+
throw new Error('useVideoCapture.startRecording: cameraRef is null');
|
|
123
|
+
}
|
|
124
|
+
if (recordingFutureRef.current !== null) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
'useVideoCapture.startRecording: a recording is already in progress',
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Brief settle before kicking off a fresh recording.
|
|
131
|
+
// vision-camera's AVCaptureSession needs ~100 ms after a
|
|
132
|
+
// previous stopRecording completes before it can cleanly
|
|
133
|
+
// start a new one — without this, rapid record→stop→record
|
|
134
|
+
// cycles (typical when the user does a panorama, then another
|
|
135
|
+
// panorama right after) can crash the session.
|
|
136
|
+
await new Promise<void>((r) => setTimeout(r, 150));
|
|
137
|
+
|
|
138
|
+
return new Promise<VideoFile>((resolve, reject) => {
|
|
139
|
+
recordingFutureRef.current = { resolve, reject };
|
|
140
|
+
setState('recording');
|
|
141
|
+
cameraRef.current!.startRecording({
|
|
142
|
+
onRecordingFinished: (video) => {
|
|
143
|
+
const future = recordingFutureRef.current;
|
|
144
|
+
recordingFutureRef.current = null;
|
|
145
|
+
setState('idle');
|
|
146
|
+
// If the future was cleared (cancelled), drop the file
|
|
147
|
+
// on the floor. vision-camera still wrote it to disk;
|
|
148
|
+
// that's a known cleanup gap for a future iteration.
|
|
149
|
+
future?.resolve(video);
|
|
150
|
+
},
|
|
151
|
+
onRecordingError: (err) => {
|
|
152
|
+
const future = recordingFutureRef.current;
|
|
153
|
+
recordingFutureRef.current = null;
|
|
154
|
+
setState('idle');
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.error('[useVideoCapture] recording error', err);
|
|
157
|
+
future?.reject(err);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
[],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const stopRecording = useCallback(
|
|
166
|
+
async (cameraRef: React.RefObject<Camera | null>): Promise<void> => {
|
|
167
|
+
// No-op if there's nothing to stop — protects against the
|
|
168
|
+
// cascade where startRecording errored synchronously and the
|
|
169
|
+
// host's release handler still calls stop.
|
|
170
|
+
if (recordingFutureRef.current === null) return;
|
|
171
|
+
if (!cameraRef.current) return;
|
|
172
|
+
setState('stopping');
|
|
173
|
+
try {
|
|
174
|
+
await cameraRef.current.stopRecording();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// The native call can throw "no-recording-in-progress" if
|
|
177
|
+
// the recording was already finalised by an error path.
|
|
178
|
+
// The future has its own error handling; we just absorb so
|
|
179
|
+
// the host's await doesn't see a confusing secondary error.
|
|
180
|
+
// eslint-disable-next-line no-console
|
|
181
|
+
console.warn('[useVideoCapture] stopRecording threw', err);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
[],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const cancelRecording = useCallback(
|
|
188
|
+
async (cameraRef: React.RefObject<Camera | null>): Promise<void> => {
|
|
189
|
+
const future = recordingFutureRef.current;
|
|
190
|
+
// Clear FIRST so the eventual onRecordingFinished/Error
|
|
191
|
+
// callback no-ops on a null ref instead of fulfilling a
|
|
192
|
+
// promise the caller has already moved past.
|
|
193
|
+
recordingFutureRef.current = null;
|
|
194
|
+
if (future) {
|
|
195
|
+
future.reject(new Error('useVideoCapture: recording cancelled'));
|
|
196
|
+
}
|
|
197
|
+
if (!cameraRef.current) {
|
|
198
|
+
setState('idle');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setState('stopping');
|
|
202
|
+
try {
|
|
203
|
+
await cameraRef.current.stopRecording();
|
|
204
|
+
} catch {
|
|
205
|
+
// Expected when no recording is in flight.
|
|
206
|
+
} finally {
|
|
207
|
+
setState('idle');
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
[],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const extractFrames = useCallback(
|
|
214
|
+
async (_opts: ExtractFramesOptions): Promise<ExtractFramesResult> => {
|
|
215
|
+
setState('extracting');
|
|
216
|
+
try {
|
|
217
|
+
throw new Error(
|
|
218
|
+
'[react-native-image-stitcher] useVideoCapture.extractFrames is not '
|
|
219
|
+
+ 'available — use `stitchVideo()` from the SDK index, which '
|
|
220
|
+
+ 'combines extract + stitch in a single native call.',
|
|
221
|
+
);
|
|
222
|
+
} finally {
|
|
223
|
+
setState('idle');
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
[],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
state,
|
|
231
|
+
startRecording,
|
|
232
|
+
stopRecording,
|
|
233
|
+
cancelRecording,
|
|
234
|
+
extractFrames,
|
|
235
|
+
};
|
|
236
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* react-native-image-stitcher — public API surface.
|
|
4
|
+
*
|
|
5
|
+
* Single component (`<Camera>`) + supporting types + the two public
|
|
6
|
+
* hooks the design doc calls out (`useARSession`, `useIMUTranslationGate`).
|
|
7
|
+
* Everything else (internal sub-components, drivers, bridges) is
|
|
8
|
+
* deliberately NOT re-exported so the v0.1.0 → 1.0 stability window
|
|
9
|
+
* doesn't lock us into an inflated public surface.
|
|
10
|
+
*
|
|
11
|
+
* If you need access to something that used to be exported and isn't
|
|
12
|
+
* now, please open an issue describing the use-case before reaching
|
|
13
|
+
* into the package internals.
|
|
14
|
+
*
|
|
15
|
+
* Public/private split: this lib is the open-source foundation. The
|
|
16
|
+
* `retailens-camera-sdk` package depends on this lib and adds
|
|
17
|
+
* RetaiLens-specific features (measurement, packet detection, etc.)
|
|
18
|
+
* on top. Consumers wanting those features install
|
|
19
|
+
* `retailens-camera-sdk` instead.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ── The main component ────────────────────────────────────────────────────
|
|
23
|
+
export { Camera, CameraError } from './camera/Camera';
|
|
24
|
+
export type {
|
|
25
|
+
CameraProps,
|
|
26
|
+
CameraCaptureResult,
|
|
27
|
+
CameraErrorCode,
|
|
28
|
+
CaptureSource,
|
|
29
|
+
CameraLens,
|
|
30
|
+
StitchMode,
|
|
31
|
+
Blender,
|
|
32
|
+
SeamFinder,
|
|
33
|
+
Warper,
|
|
34
|
+
FramesDroppedInfo,
|
|
35
|
+
} from './camera/Camera';
|
|
36
|
+
|
|
37
|
+
// ── AR foundation (public per design doc) ─────────────────────────────────
|
|
38
|
+
// Hosts that want raw AR pose access (e.g., to build their own
|
|
39
|
+
// measurement/detection on top) consume these directly.
|
|
40
|
+
export { useARSession, ARTrackingState } from './ar/useARSession';
|
|
41
|
+
export type {
|
|
42
|
+
UseARSessionReturn,
|
|
43
|
+
FramePose,
|
|
44
|
+
} from './ar/useARSession';
|
|
45
|
+
|
|
46
|
+
// ── IMU translation gate (public per design doc R5) ───────────────────────
|
|
47
|
+
// Hosts running their own non-AR capture flow can reuse this hook to
|
|
48
|
+
// get the same gating logic <Camera> uses internally.
|
|
49
|
+
export { useIMUTranslationGate } from './sensors/useIMUTranslationGate';
|
|
50
|
+
export type {
|
|
51
|
+
UseIMUTranslationGateOptions,
|
|
52
|
+
UseIMUTranslationGateReturn,
|
|
53
|
+
} from './sensors/useIMUTranslationGate';
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* normaliseOrientation — bake EXIF rotation into a JPEG's pixels.
|
|
4
|
+
*
|
|
5
|
+
* vision-camera writes photos with the camera sensor's native
|
|
6
|
+
* landscape pixels and an EXIF Orientation tag describing how to
|
|
7
|
+
* rotate them for display. Most consumers (iOS UIImage, RN's
|
|
8
|
+
* `<Image>`) honour the tag — but enough don't (Sentry breadcrumbs,
|
|
9
|
+
* share sheets, the cv::Stitcher itself, third-party image
|
|
10
|
+
* pipelines) that "what's on disk" diverges from "what the user
|
|
11
|
+
* sees" unless we eagerly normalise.
|
|
12
|
+
*
|
|
13
|
+
* This helper round-trips the file through the SDK's native
|
|
14
|
+
* stitcher module, which decodes the JPEG with EXIF rotation
|
|
15
|
+
* applied and re-encodes a clean JPEG with no orientation
|
|
16
|
+
* metadata. Idempotent on already-normalised files.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { NativeModules, Platform } from 'react-native';
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
export interface NormaliseOrientationResult {
|
|
23
|
+
/** Image width in pixels AFTER rotation has been applied. */
|
|
24
|
+
width: number;
|
|
25
|
+
/** Image height in pixels AFTER rotation. */
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bake the EXIF rotation of `imagePath` into pixels in-place.
|
|
32
|
+
*
|
|
33
|
+
* Returns the post-rotation dimensions so the caller can update
|
|
34
|
+
* its own width/height fields. No-op on platforms that don't
|
|
35
|
+
* have the native module yet (Android until Phase 3) — falls back
|
|
36
|
+
* to the input shape so callers don't have to special-case
|
|
37
|
+
* platform availability.
|
|
38
|
+
*/
|
|
39
|
+
export async function normaliseOrientation(
|
|
40
|
+
imagePath: string,
|
|
41
|
+
fallback?: { width: number; height: number },
|
|
42
|
+
): Promise<NormaliseOrientationResult> {
|
|
43
|
+
const native: unknown =
|
|
44
|
+
(NativeModules as Record<string, unknown>)['BatchStitcher'];
|
|
45
|
+
const fn =
|
|
46
|
+
native
|
|
47
|
+
&& typeof native === 'object'
|
|
48
|
+
&& typeof (native as { normaliseOrientation?: unknown }).normaliseOrientation === 'function'
|
|
49
|
+
? (native as {
|
|
50
|
+
normaliseOrientation: (
|
|
51
|
+
o: { imagePath: string },
|
|
52
|
+
) => Promise<NormaliseOrientationResult>;
|
|
53
|
+
}).normaliseOrientation
|
|
54
|
+
: null;
|
|
55
|
+
|
|
56
|
+
if (!fn) {
|
|
57
|
+
// Native module not registered (typically Android in current
|
|
58
|
+
// builds). Skip normalisation and report the caller's
|
|
59
|
+
// fallback dimensions if provided, otherwise zeroes — keeps
|
|
60
|
+
// the API total without forcing every caller to wrap a
|
|
61
|
+
// try/catch.
|
|
62
|
+
if (fallback) return fallback;
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn(
|
|
65
|
+
`[capture-sdk] normaliseOrientation: native module not available on ${Platform.OS}. `
|
|
66
|
+
+ 'Photo orientation may render incorrectly until the native module is registered.',
|
|
67
|
+
);
|
|
68
|
+
return { width: 0, height: 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
return await fn({ imagePath });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.warn('[capture-sdk] normaliseOrientation failed', err);
|
|
76
|
+
if (fallback) return fallback;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* runQualityCheck — public entry point for the SDK's blur + brightness
|
|
4
|
+
* quality gate. Delegates to the native module
|
|
5
|
+
* `RNImageStitcherQualityChecker` when registered (iOS today via
|
|
6
|
+
* `ios/Sources/RNImageStitcher/QualityChecker.swift`, Android in
|
|
7
|
+
* Phase 3); falls back to a conservative pass-through shim when the
|
|
8
|
+
* native module is absent so dev / Jest runs don't crash on a missing
|
|
9
|
+
* NativeModules entry.
|
|
10
|
+
*
|
|
11
|
+
* The shim NEVER fails an image — false negatives in production are
|
|
12
|
+
* worse than missing data. The native path is the only branch that
|
|
13
|
+
* can return `passed=false`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { NativeModules, Platform } from 'react-native';
|
|
17
|
+
|
|
18
|
+
import type { QualityIssue, QualityReport, QualityThresholds } from '../types';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
let warnedOnce = false;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Numeric scores produced by the native module. The bridge resolves
|
|
26
|
+
* with `{ blurScore, brightnessScore }` — issues are computed in JS
|
|
27
|
+
* so the same threshold logic applies on iOS + Android even if a
|
|
28
|
+
* platform's native impl evolves independently.
|
|
29
|
+
*/
|
|
30
|
+
interface NativeQualityScores {
|
|
31
|
+
blurScore: number;
|
|
32
|
+
brightnessScore: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Analyse an image file and return a quality report.
|
|
38
|
+
*
|
|
39
|
+
* @param imagePath Filesystem path to the image (with or without
|
|
40
|
+
* `file://` prefix — the native module strips it).
|
|
41
|
+
* @param thresholds Cut-offs the report scores against.
|
|
42
|
+
*/
|
|
43
|
+
export async function runQualityCheck(
|
|
44
|
+
imagePath: string,
|
|
45
|
+
thresholds: QualityThresholds,
|
|
46
|
+
): Promise<QualityReport> {
|
|
47
|
+
const native = (NativeModules as Record<string, unknown>)['RNImageStitcherQualityChecker'];
|
|
48
|
+
|
|
49
|
+
// Native path: registered + has the bridged `measure` method.
|
|
50
|
+
if (
|
|
51
|
+
native
|
|
52
|
+
&& typeof native === 'object'
|
|
53
|
+
&& typeof (native as { measure?: unknown }).measure === 'function'
|
|
54
|
+
) {
|
|
55
|
+
const scores: NativeQualityScores =
|
|
56
|
+
await (native as { measure: (path: string) => Promise<NativeQualityScores> })
|
|
57
|
+
.measure(imagePath);
|
|
58
|
+
return scoreToReport(scores, thresholds);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Shim fallback — never reached when the SDK's native module is linked
|
|
62
|
+
// into the host app correctly. Surfaces a one-time warning in dev so
|
|
63
|
+
// misconfiguration is loud rather than silent.
|
|
64
|
+
if (!warnedOnce && __DEV__) {
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.warn(
|
|
67
|
+
'[react-native-image-stitcher] QualityChecker native module not '
|
|
68
|
+
+ `found on ${Platform.OS}; falling back to optimistic shim. Check `
|
|
69
|
+
+ 'autolinking + a clean `pod install` (iOS) / `gradle clean` (Android).',
|
|
70
|
+
);
|
|
71
|
+
warnedOnce = true;
|
|
72
|
+
}
|
|
73
|
+
void thresholds;
|
|
74
|
+
return {
|
|
75
|
+
passed: true,
|
|
76
|
+
blurScore: Number.POSITIVE_INFINITY,
|
|
77
|
+
brightnessScore: 128,
|
|
78
|
+
issues: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Apply thresholds to native scores → produce the report shape the JS
|
|
85
|
+
* surface promises. Pure function so it's trivially unit-testable.
|
|
86
|
+
*
|
|
87
|
+
* Exported for tests; not part of the SDK's public API.
|
|
88
|
+
*/
|
|
89
|
+
export function scoreToReport(
|
|
90
|
+
scores: NativeQualityScores,
|
|
91
|
+
thresholds: QualityThresholds,
|
|
92
|
+
): QualityReport {
|
|
93
|
+
const issues: QualityIssue[] = [];
|
|
94
|
+
|
|
95
|
+
if (scores.blurScore < thresholds.minBlurScore) {
|
|
96
|
+
issues.push({
|
|
97
|
+
type: 'blur',
|
|
98
|
+
message:
|
|
99
|
+
`Image is too blurry (Laplacian variance ${scores.blurScore.toFixed(1)} `
|
|
100
|
+
+ `< ${thresholds.minBlurScore}). Hold the camera steady and retry.`,
|
|
101
|
+
severity: 'error',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (scores.brightnessScore < thresholds.minBrightness) {
|
|
105
|
+
issues.push({
|
|
106
|
+
type: 'brightness_low',
|
|
107
|
+
message:
|
|
108
|
+
`Image is too dark (mean luminance ${scores.brightnessScore.toFixed(0)} `
|
|
109
|
+
+ `< ${thresholds.minBrightness}). Add light or move closer to a lit area.`,
|
|
110
|
+
severity: 'warning',
|
|
111
|
+
});
|
|
112
|
+
} else if (scores.brightnessScore > thresholds.maxBrightness) {
|
|
113
|
+
issues.push({
|
|
114
|
+
type: 'brightness_high',
|
|
115
|
+
message:
|
|
116
|
+
`Image is overexposed (mean luminance ${scores.brightnessScore.toFixed(0)} `
|
|
117
|
+
+ `> ${thresholds.maxBrightness}). Reduce light or move out of direct sunlight.`,
|
|
118
|
+
severity: 'warning',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
// `passed` is the strict gate — only `error`-severity issues block.
|
|
124
|
+
// Brightness warnings are advisory; SOS is still computable from a
|
|
125
|
+
// dim or bright photo, but a blurry one isn't.
|
|
126
|
+
passed: issues.every((i) => i.severity !== 'error'),
|
|
127
|
+
blurScore: scores.blurScore,
|
|
128
|
+
brightnessScore: scores.brightnessScore,
|
|
129
|
+
issues,
|
|
130
|
+
};
|
|
131
|
+
}
|