react-native-image-stitcher 0.4.1 → 0.5.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 +165 -0
- package/README.md +1 -0
- package/android/build.gradle +33 -0
- package/android/src/main/java/io/imagestitcher/rn/CvFlowGateFrameProcessor.kt +163 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +148 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +431 -23
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +65 -7
- package/dist/camera/Camera.d.ts +68 -1
- package/dist/camera/Camera.js +102 -16
- package/dist/camera/CameraView.d.ts +17 -5
- package/dist/camera/CameraView.js +28 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -1
- package/dist/stitching/incremental.d.ts +13 -4
- package/dist/stitching/useFrameProcessorDriver.d.ts +148 -0
- package/dist/stitching/useFrameProcessorDriver.js +321 -0
- package/dist/stitching/useIncrementalJSDriver.js +21 -0
- package/ios/Package.swift +35 -21
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +188 -8
- package/ios/Sources/RNImageStitcher/KeyframeGateFrameProcessor.mm +196 -0
- package/package.json +3 -1
- package/src/camera/Camera.tsx +190 -15
- package/src/camera/CameraView.tsx +50 -0
- package/src/index.ts +12 -0
- package/src/stitching/incremental.ts +12 -3
- package/src/stitching/useFrameProcessorDriver.ts +407 -0
- package/src/stitching/useIncrementalJSDriver.ts +24 -0
package/dist/camera/Camera.d.ts
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import React from 'react';
|
|
42
42
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
43
|
+
import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera';
|
|
43
44
|
export type CaptureSource = 'ar' | 'non-ar';
|
|
44
45
|
export type CameraLens = '1x' | '0.5x';
|
|
45
46
|
export type StitchMode = 'auto' | 'panorama' | 'scans';
|
|
@@ -88,7 +89,16 @@ export type CameraCaptureResult = {
|
|
|
88
89
|
* Errors surfaced via `onError`. Classified codes so consumers can
|
|
89
90
|
* branch on the kind of failure (toast vs retry vs report).
|
|
90
91
|
*/
|
|
91
|
-
export type CameraErrorCode = 'CAMERA_PERMISSION_DENIED' | 'CAMERA_DEVICE_UNAVAILABLE' | 'PHOTO_CAPTURE_FAILED' | 'PANORAMA_START_FAILED' | 'PANORAMA_FINALIZE_FAILED' | 'STITCH_NEED_MORE_IMGS' | 'STITCH_HOMOGRAPHY_FAIL' | 'STITCH_CAMERA_PARAMS_FAIL' | 'STITCH_OOM' | 'OUTPUT_WRITE_FAILED'
|
|
92
|
+
export type CameraErrorCode = 'CAMERA_PERMISSION_DENIED' | 'CAMERA_DEVICE_UNAVAILABLE' | 'PHOTO_CAPTURE_FAILED' | 'PANORAMA_START_FAILED' | 'PANORAMA_FINALIZE_FAILED' | 'STITCH_NEED_MORE_IMGS' | 'STITCH_HOMOGRAPHY_FAIL' | 'STITCH_CAMERA_PARAMS_FAIL' | 'STITCH_OOM' | 'OUTPUT_WRITE_FAILED'
|
|
93
|
+
/**
|
|
94
|
+
* Vision-camera surfaced a runtime error that isn't a known
|
|
95
|
+
* transient lifecycle event (those are swallowed inside the SDK's
|
|
96
|
+
* `<CameraView>`). Examples that DO reach the host as this code:
|
|
97
|
+
* `format/invalid-format`, `capture/recording-canceled`,
|
|
98
|
+
* `device/microphone-permission-denied`, ... The full error
|
|
99
|
+
* object is on `.cause` for inspection.
|
|
100
|
+
*/
|
|
101
|
+
| 'VISION_CAMERA_RUNTIME' | 'UNKNOWN';
|
|
92
102
|
export declare class CameraError extends Error {
|
|
93
103
|
readonly code: CameraErrorCode;
|
|
94
104
|
readonly cause?: unknown;
|
|
@@ -130,6 +140,24 @@ export interface CameraProps {
|
|
|
130
140
|
enablePanoramaMode?: boolean;
|
|
131
141
|
showSettingsButton?: boolean;
|
|
132
142
|
style?: StyleProp<ViewStyle>;
|
|
143
|
+
/**
|
|
144
|
+
* Which incremental stitcher engine to drive. Default
|
|
145
|
+
* `'batch-keyframe'` — collects accepted JPEGs and runs
|
|
146
|
+
* `cv::Stitcher` once at finalize time. This is the v0.4+
|
|
147
|
+
* production default and what the v0.5 Frame Processor migration
|
|
148
|
+
* exercises.
|
|
149
|
+
*
|
|
150
|
+
* Switch to a live engine (`'firstwins-rectilinear'` or
|
|
151
|
+
* `'hybrid'`) for low-latency in-flight stitching. Live engines
|
|
152
|
+
* exercise the F8.6 pixel-buffer ingest path (skipping the JPEG
|
|
153
|
+
* encode/decode round-trip; ~30–50 ms saved per accept) when the
|
|
154
|
+
* Frame Processor driver is active.
|
|
155
|
+
*
|
|
156
|
+
* See `docs/f8-frame-processor-plan.md` and the v0.5.0
|
|
157
|
+
* CHANGELOG for the trade-offs between batch-keyframe and live
|
|
158
|
+
* engines.
|
|
159
|
+
*/
|
|
160
|
+
engine?: 'batch-keyframe' | 'hybrid' | 'slitscan-rotate' | 'slitscan-both' | 'firstwins' | 'firstwins-zoomed' | 'firstwins-rectilinear' | 'slitscan';
|
|
133
161
|
/**
|
|
134
162
|
* Optional destination directory for captures. When set, the lib
|
|
135
163
|
* lands tap-photos at `${outputDir}/photo-${ts}.jpg` and panoramas
|
|
@@ -163,6 +191,45 @@ export interface CameraProps {
|
|
|
163
191
|
onLensChange?: (lens: CameraLens) => void;
|
|
164
192
|
onFramesDropped?: (info: FramesDroppedInfo) => void;
|
|
165
193
|
onError?: (err: CameraError) => void;
|
|
194
|
+
/**
|
|
195
|
+
* Optional vision-camera frame processor. Only attached to the
|
|
196
|
+
* non-AR preview (AR mode uses ARCameraView, which doesn't expose
|
|
197
|
+
* a worklet seam). Build the worklet on the host side with
|
|
198
|
+
* `useFrameProcessor` from `react-native-vision-camera`.
|
|
199
|
+
*
|
|
200
|
+
* Introduced for F8 (FrameProcessor port) — see
|
|
201
|
+
* `docs/f8-frame-processor-plan.md`.
|
|
202
|
+
*
|
|
203
|
+
* As of v0.5 (F8.3) this prop is **deprecated for the standard
|
|
204
|
+
* non-AR capture flow**: the SDK now installs its own frame
|
|
205
|
+
* processor via `useFrameProcessorDriver` that pipes pixel
|
|
206
|
+
* buffers into the incremental stitcher with synthesised pose.
|
|
207
|
+
* Setting this prop in the default mode will be IGNORED with a
|
|
208
|
+
* one-time console.warn — supplying your own worklet would race
|
|
209
|
+
* with the SDK's pixel-buffer feed.
|
|
210
|
+
*
|
|
211
|
+
* Three coexistence rules:
|
|
212
|
+
* * Default (modern non-AR): SDK owns the worklet, this prop
|
|
213
|
+
* is ignored.
|
|
214
|
+
* * `legacyDriver={true}`: SDK uses the old `useIncrementalJSDriver`
|
|
215
|
+
* (takeSnapshot path). Honoured for diagnostics or as an
|
|
216
|
+
* escape hatch.
|
|
217
|
+
* * AR mode: vision-camera Camera isn't mounted, this prop is
|
|
218
|
+
* irrelevant.
|
|
219
|
+
*/
|
|
220
|
+
frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
|
|
221
|
+
/**
|
|
222
|
+
* Opt back into the legacy `useIncrementalJSDriver` for non-AR
|
|
223
|
+
* captures (the v0.4 path: `takeSnapshot` → JPEG → cache file →
|
|
224
|
+
* `IncrementalStitcher.processFrameAtPath`).
|
|
225
|
+
*
|
|
226
|
+
* Default `false` (use the new `useFrameProcessorDriver`, which
|
|
227
|
+
* runs the gate on the camera producer thread at native frame
|
|
228
|
+
* rate via a vision-camera Frame Processor plugin). The legacy
|
|
229
|
+
* path will be removed in v0.6 — set this only if you hit a
|
|
230
|
+
* specific issue with the new driver and need to ship a fix.
|
|
231
|
+
*/
|
|
232
|
+
legacyDriver?: boolean;
|
|
166
233
|
}
|
|
167
234
|
/**
|
|
168
235
|
* The public `<Camera>` component.
|
package/dist/camera/Camera.js
CHANGED
|
@@ -98,6 +98,7 @@ const useCapture_1 = require("./useCapture");
|
|
|
98
98
|
const useDeviceOrientation_1 = require("./useDeviceOrientation");
|
|
99
99
|
const incremental_1 = require("../stitching/incremental");
|
|
100
100
|
const useIncrementalJSDriver_1 = require("../stitching/useIncrementalJSDriver");
|
|
101
|
+
const useFrameProcessorDriver_1 = require("../stitching/useFrameProcessorDriver");
|
|
101
102
|
const useIncrementalStitcher_1 = require("../stitching/useIncrementalStitcher");
|
|
102
103
|
const useIMUTranslationGate_1 = require("../sensors/useIMUTranslationGate");
|
|
103
104
|
const paths_1 = require("../utils/paths");
|
|
@@ -270,7 +271,7 @@ function extractPanoramaOverrides(props) {
|
|
|
270
271
|
* The public `<Camera>` component.
|
|
271
272
|
*/
|
|
272
273
|
function Camera(props) {
|
|
273
|
-
const { defaultCaptureSource = 'ar', defaultLens = '1x', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, } = props;
|
|
274
|
+
const { defaultCaptureSource = 'ar', defaultLens = '1x', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, frameProcessor: hostFrameProcessor, legacyDriver = false, engine = 'batch-keyframe', } = props;
|
|
274
275
|
const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
|
|
275
276
|
// ── State ───────────────────────────────────────────────────────
|
|
276
277
|
const [arPreference, setArPreference] = (0, react_1.useState)(defaultCaptureSource === 'ar');
|
|
@@ -434,10 +435,40 @@ function Camera(props) {
|
|
|
434
435
|
// imperative pattern (start on hold-start, stop on hold-end) avoids
|
|
435
436
|
// the re-render churn entirely.
|
|
436
437
|
const jsDriver = (0, useIncrementalJSDriver_1.useIncrementalJSDriver)();
|
|
437
|
-
//
|
|
438
|
+
// F8.3 — vision-camera Frame Processor variant. Always
|
|
439
|
+
// instantiated so we don't have conditional hook calls; only one
|
|
440
|
+
// of the two drivers actually .start()s per capture. Stop() on
|
|
441
|
+
// an idle driver is a no-op.
|
|
442
|
+
const fpDriver = (0, useFrameProcessorDriver_1.useFrameProcessorDriver)();
|
|
443
|
+
// Safety: ensure both drivers are stopped if the component unmounts
|
|
438
444
|
// mid-recording. Empty deps so this only fires on unmount.
|
|
439
445
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
440
|
-
(0, react_1.useEffect)(() => () => { jsDriver.stop(); }, []);
|
|
446
|
+
(0, react_1.useEffect)(() => () => { jsDriver.stop(); fpDriver.stop(); }, []);
|
|
447
|
+
// F8.3 — one-shot deprecation warning when the host supplies their
|
|
448
|
+
// own `frameProcessor` while running in the default (Frame
|
|
449
|
+
// Processor driver) mode. Two worklets racing on the same
|
|
450
|
+
// producer thread would corrupt the engine's workQueue ordering;
|
|
451
|
+
// the SDK's own worklet wins and the host's is ignored. Hosts
|
|
452
|
+
// that *need* a custom worklet must opt into `legacyDriver={true}`
|
|
453
|
+
// (which switches off the SDK's worklet entirely).
|
|
454
|
+
const hostFrameProcessorIgnoredWarnedRef = (0, react_1.useRef)(false);
|
|
455
|
+
if (hostFrameProcessor != null
|
|
456
|
+
&& !legacyDriver
|
|
457
|
+
&& !hostFrameProcessorIgnoredWarnedRef.current) {
|
|
458
|
+
hostFrameProcessorIgnoredWarnedRef.current = true;
|
|
459
|
+
// eslint-disable-next-line no-console
|
|
460
|
+
console.warn('[react-native-image-stitcher] The `frameProcessor` prop on '
|
|
461
|
+
+ '<Camera> is ignored when the default driver is active '
|
|
462
|
+
+ '(legacyDriver=false). Either remove the prop or set '
|
|
463
|
+
+ 'legacyDriver={true} to opt into the legacy path.');
|
|
464
|
+
}
|
|
465
|
+
// The Frame Processor worklet actually bound to vision-camera's
|
|
466
|
+
// Camera. Resolution order:
|
|
467
|
+
// 1. Legacy mode: honor the host's prop (or null).
|
|
468
|
+
// 2. Modern mode: SDK driver's worklet, regardless of host's prop.
|
|
469
|
+
const effectiveFrameProcessor = legacyDriver
|
|
470
|
+
? (hostFrameProcessor ?? null)
|
|
471
|
+
: fpDriver.frameProcessor;
|
|
441
472
|
// ── Subscribe to engine state for live keyframe thumbs ──────────
|
|
442
473
|
(0, react_1.useEffect)(() => {
|
|
443
474
|
const sub = (0, incremental_1.subscribeIncrementalState)((state) => {
|
|
@@ -493,6 +524,17 @@ function Camera(props) {
|
|
|
493
524
|
const accepted = incrementalState?.acceptedCount ?? 0;
|
|
494
525
|
if (accepted > lastAcceptedCountRef.current) {
|
|
495
526
|
lastAcceptedCountRef.current = accepted;
|
|
527
|
+
// F8.3 review-of-review (M3 revert): originally gated this to
|
|
528
|
+
// `legacyDriver` because the Frame Processor driver doesn't
|
|
529
|
+
// consult `imuGate` for its own pose synthesis. That ignored a
|
|
530
|
+
// load-bearing side effect: `imuGate.resetAnchor()` bounds the
|
|
531
|
+
// IIR-integrator drift window per-accept, and
|
|
532
|
+
// `imuGate.getTotalAbsMetres()` is read at finalize time
|
|
533
|
+
// (Camera.tsx:1097) as `imuTranslationMetres` into the native
|
|
534
|
+
// stitchMode auto-resolver (PANORAMA vs SCANS). Without the
|
|
535
|
+
// per-accept reset, long FP-driver captures let IIR drift
|
|
536
|
+
// compound → inflated metres → biased toward SCANS. Keep the
|
|
537
|
+
// reset firing for ALL non-AR modes.
|
|
496
538
|
if (isNonAR) {
|
|
497
539
|
imuGate.resetAnchor();
|
|
498
540
|
}
|
|
@@ -609,26 +651,44 @@ function Camera(props) {
|
|
|
609
651
|
snapshotEveryNAccepts: 1,
|
|
610
652
|
frameRotationDegrees: orientationRotation,
|
|
611
653
|
captureOrientation: deviceOrientation,
|
|
612
|
-
|
|
654
|
+
// F8.3 — non-AR captures pick between the new Frame Processor
|
|
655
|
+
// driver (default) and the legacy JS-snapshot driver (opt-in
|
|
656
|
+
// via `legacyDriver={true}`). AR captures always use the
|
|
657
|
+
// ARSession-driven path.
|
|
658
|
+
frameSourceMode: isNonAR
|
|
659
|
+
? (legacyDriver ? 'jsDriver' : 'frameProcessor')
|
|
660
|
+
: 'arSession',
|
|
613
661
|
composeWidth: 1920,
|
|
614
662
|
composeHeight: 1080,
|
|
615
663
|
canvasWidth: 5000,
|
|
616
664
|
canvasHeight: 5000,
|
|
617
|
-
engine
|
|
665
|
+
engine,
|
|
618
666
|
config: (0, PanoramaSettingsBridge_1.panoramaSettingsToNativeConfig)({
|
|
619
667
|
...settings,
|
|
620
668
|
captureSource: effectiveCaptureSource,
|
|
621
669
|
}),
|
|
622
670
|
});
|
|
671
|
+
// F8.3 review-of-review (M3 revert): `imuGate.resetAnchor()`
|
|
672
|
+
// is load-bearing for the stitchMode auto-resolver (see the
|
|
673
|
+
// matching comment on the per-accept reset useEffect above).
|
|
674
|
+
// Keep firing it on every capture start, not just legacy mode.
|
|
623
675
|
imuGate.resetAnchor();
|
|
624
|
-
// Start
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
676
|
+
// Start the non-AR frame source. AR mode feeds natively from
|
|
677
|
+
// ARSession so both drivers stay idle in that path.
|
|
678
|
+
// * Default: Frame Processor driver — worklet runs on the
|
|
679
|
+
// producer thread, plugin calls `consumeFrameFromPlugin`
|
|
680
|
+
// directly. No camera ref needed (vision-camera owns it).
|
|
681
|
+
// * Legacy: JS driver — `takeSnapshot` + `processFrameAtPath`
|
|
682
|
+
// via the cameraRef.
|
|
683
|
+
// Imperative-pattern rationale: see the useIncrementalJSDriver
|
|
684
|
+
// comment above re. why this isn't a useEffect.
|
|
630
685
|
if (isNonAR) {
|
|
631
|
-
|
|
686
|
+
if (legacyDriver) {
|
|
687
|
+
jsDriver.start(visionCameraRef);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
fpDriver.start();
|
|
691
|
+
}
|
|
632
692
|
}
|
|
633
693
|
}
|
|
634
694
|
catch (err) {
|
|
@@ -644,16 +704,22 @@ function Camera(props) {
|
|
|
644
704
|
effectiveCaptureSource,
|
|
645
705
|
imuGate,
|
|
646
706
|
jsDriver,
|
|
707
|
+
fpDriver,
|
|
708
|
+
legacyDriver,
|
|
709
|
+
engine,
|
|
647
710
|
onError,
|
|
648
711
|
]);
|
|
649
712
|
const handleHoldEnd = (0, react_1.useCallback)(async () => {
|
|
650
713
|
if (statusPhase !== 'recording')
|
|
651
714
|
return;
|
|
652
715
|
setStatusPhase('stitching');
|
|
653
|
-
// Stop pumping new
|
|
654
|
-
// racing the final cv::Stitcher pass against late-arriving
|
|
655
|
-
//
|
|
716
|
+
// Stop pumping new frames before finalizing so the engine isn't
|
|
717
|
+
// racing the final cv::Stitcher pass against late-arriving
|
|
718
|
+
// keyframes. Both stop() calls are no-ops when the
|
|
719
|
+
// corresponding driver wasn't started (AR mode, or the inactive
|
|
720
|
+
// driver in non-AR mode).
|
|
656
721
|
jsDriver.stop();
|
|
722
|
+
fpDriver.stop();
|
|
657
723
|
try {
|
|
658
724
|
// Compose the panorama output path: host-controlled if
|
|
659
725
|
// `outputDir` is set, else the lib's canonical capture dir
|
|
@@ -723,6 +789,7 @@ function Camera(props) {
|
|
|
723
789
|
onError,
|
|
724
790
|
recordingStartedAt,
|
|
725
791
|
jsDriver,
|
|
792
|
+
fpDriver,
|
|
726
793
|
// F10 Phase 2 review N1 — these four were missing pre-fix. The
|
|
727
794
|
// callback reads `settings.debug` (to gate the stitchToast),
|
|
728
795
|
// `isNonAR` (to decide whether to read IMU totalAbs translation),
|
|
@@ -755,7 +822,26 @@ function Camera(props) {
|
|
|
755
822
|
// the very first buffered preview frame. Android takeSnapshot
|
|
756
823
|
// works either way. Pattern matches AuditCaptureScreen.tsx
|
|
757
824
|
// which has run on `video` (true) for months without issue.
|
|
758
|
-
video: true, flash: "off", style: react_native_1.StyleSheet.absoluteFill
|
|
825
|
+
video: true, flash: "off", style: react_native_1.StyleSheet.absoluteFill,
|
|
826
|
+
// F8 (FrameProcessor port) — host-supplied worklet runs on
|
|
827
|
+
// the camera producer thread for every frame. Only wired
|
|
828
|
+
// in non-AR mode; AR mode uses ARCameraView which doesn't
|
|
829
|
+
// expose a frame-processor seam. See
|
|
830
|
+
// docs/f8-frame-processor-plan.md.
|
|
831
|
+
cameraProps: effectiveFrameProcessor != null
|
|
832
|
+
? { frameProcessor: effectiveFrameProcessor }
|
|
833
|
+
: undefined, onError: (err) => {
|
|
834
|
+
// CameraView already filters known transient lifecycle
|
|
835
|
+
// errors (screen-lock, etc.) before invoking this. What
|
|
836
|
+
// reaches here is a real vision-camera runtime issue:
|
|
837
|
+
// pull `code`/`message` defensively (the type is
|
|
838
|
+
// `unknown` from CameraView's perspective) and wrap in
|
|
839
|
+
// a SDK-typed `CameraError` so hosts get a stable shape.
|
|
840
|
+
const e = err;
|
|
841
|
+
const codeStr = e?.code ?? 'unknown';
|
|
842
|
+
const msg = e?.message ?? String(err);
|
|
843
|
+
onError?.(new CameraError('VISION_CAMERA_RUNTIME', `${codeStr}: ${msg}`, err));
|
|
844
|
+
} })),
|
|
759
845
|
react_1.default.createElement(CaptureStatusOverlay_1.CaptureStatusOverlay, { phase: statusPhase, topInset: insets.top, recordingStartedAt: recordingStartedAt ?? undefined }),
|
|
760
846
|
settings.debug && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
761
847
|
react_1.default.createElement(CaptureOrientationPill_1.CaptureOrientationPill, { orientation: deviceOrientation, topInset: insets.top }),
|
|
@@ -55,11 +55,23 @@ export interface CameraViewProps {
|
|
|
55
55
|
x: number;
|
|
56
56
|
y: number;
|
|
57
57
|
}) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Forwarded from vision-camera's `<Camera onError>` AFTER lifecycle
|
|
60
|
+
* errors are filtered. The SDK's built-in filter swallows:
|
|
61
|
+
*
|
|
62
|
+
* * `system/camera-is-restricted` — screen-lock / DoNotDisturb
|
|
63
|
+
* temporarily revokes camera access; vision-camera re-acquires
|
|
64
|
+
* on resume. Logged to console.warn, NOT surfaced.
|
|
65
|
+
* * `system/camera-has-been-disconnected` — another app grabbed
|
|
66
|
+
* the camera. Same auto-recovery.
|
|
67
|
+
* * `device/camera-already-in-use` — same class as above.
|
|
68
|
+
*
|
|
69
|
+
* Real errors (permission denials, hardware failures, malformed
|
|
70
|
+
* format requests) are forwarded. Hosts can therefore safely
|
|
71
|
+
* pipe this to a redbox / Crashlytics without getting paged on
|
|
72
|
+
* routine screen-lock events.
|
|
73
|
+
*/
|
|
74
|
+
onError?: (error: unknown) => void;
|
|
58
75
|
}
|
|
59
|
-
/**
|
|
60
|
-
* A forwardRef'd wrapper that exposes the underlying Camera ref
|
|
61
|
-
* to callers (so ``cameraRef.current.takePhoto()`` keeps working),
|
|
62
|
-
* while presenting a smaller API on the outside.
|
|
63
|
-
*/
|
|
64
76
|
export declare const CameraView: React.ForwardRefExoticComponent<CameraViewProps & React.RefAttributes<Camera | null>>;
|
|
65
77
|
//# sourceMappingURL=CameraView.d.ts.map
|
|
@@ -62,7 +62,33 @@ const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
|
62
62
|
* to callers (so ``cameraRef.current.takePhoto()`` keeps working),
|
|
63
63
|
* while presenting a smaller API on the outside.
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
// Error codes vision-camera reports for transient lifecycle events.
|
|
66
|
+
// Filtered out of the SDK's onError forward (see `handleVcError` in
|
|
67
|
+
// the body): the camera self-recovers when the device comes back into
|
|
68
|
+
// the foreground / regains permission / the other app releases the
|
|
69
|
+
// device. Surfacing these as host errors causes spurious crash
|
|
70
|
+
// reports during routine phone-lock / app-switch operations.
|
|
71
|
+
const VC_LIFECYCLE_ERROR_CODES = new Set([
|
|
72
|
+
'system/camera-is-restricted', // screen lock, DoNotDisturb, MDM policy
|
|
73
|
+
'system/camera-has-been-disconnected', // another app grabbed the camera
|
|
74
|
+
'device/camera-already-in-use', // same class as above
|
|
75
|
+
]);
|
|
76
|
+
exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash = 'off', isActive = true, video = false, guidance, style, cameraProps, onError, }, ref) {
|
|
77
|
+
// Error filter — see `VC_LIFECYCLE_ERROR_CODES` for the swallow
|
|
78
|
+
// list rationale. `code` on vision-camera's `CameraRuntimeError`
|
|
79
|
+
// is typed as a string; treat any non-string defensively as a
|
|
80
|
+
// "forward it" so we don't accidentally swallow unknown errors.
|
|
81
|
+
const handleVcError = (err) => {
|
|
82
|
+
const code = err?.code;
|
|
83
|
+
if (typeof code === 'string' && VC_LIFECYCLE_ERROR_CODES.has(code)) {
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.warn('[react-native-image-stitcher] vision-camera reported a '
|
|
86
|
+
+ `transient lifecycle error (${code}); the camera will `
|
|
87
|
+
+ 'auto-recover on resume. Not forwarding to onError.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
onError?.(err);
|
|
91
|
+
};
|
|
66
92
|
// Internal ref so we can both attach to <Camera> and forward outward.
|
|
67
93
|
const innerRef = (0, react_1.useRef)(null);
|
|
68
94
|
(0, react_1.useImperativeHandle)(ref, () => innerRef.current);
|
|
@@ -81,7 +107,7 @@ exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash
|
|
|
81
107
|
// `outputOrientation="device"` rotates the pixels to match
|
|
82
108
|
// how the user is holding the phone, so the saved JPEG is
|
|
83
109
|
// "what you see is what was taken".
|
|
84
|
-
outputOrientation: "device", torch: flash === 'on' ? 'on' : 'off', ...cameraProps }),
|
|
110
|
+
outputOrientation: "device", torch: flash === 'on' ? 'on' : 'off', onError: handleVcError, ...cameraProps }),
|
|
85
111
|
guidance ? (react_1.default.createElement(react_native_1.View, { style: styles.guidance, pointerEvents: "none", accessible: true, accessibilityRole: "text" },
|
|
86
112
|
react_1.default.createElement(react_native_1.Text, { style: styles.guidanceText, numberOfLines: 2 }, guidance))) : null));
|
|
87
113
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -65,5 +65,8 @@ export { IncrementalOutcome, incrementalStitcherIsAvailable, subscribeIncrementa
|
|
|
65
65
|
export type { IncrementalState } from './stitching/incremental';
|
|
66
66
|
export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
|
|
67
67
|
export { useIncrementalJSDriver } from './stitching/useIncrementalJSDriver';
|
|
68
|
+
export type { UseIncrementalJSDriverOptions, IncrementalJSDriverHandle, } from './stitching/useIncrementalJSDriver';
|
|
69
|
+
export { useFrameProcessorDriver } from './stitching/useFrameProcessorDriver';
|
|
70
|
+
export type { UseFrameProcessorDriverOptions, FrameProcessorDriverHandle, } from './stitching/useFrameProcessorDriver';
|
|
68
71
|
export { stitchVideo } from './stitching/stitchVideo';
|
|
69
72
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* adds RetaiLens-specific features on top.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.stitchVideo = exports.useIncrementalJSDriver = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
25
|
+
exports.stitchVideo = exports.useFrameProcessorDriver = exports.useIncrementalJSDriver = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
26
26
|
// ─────────────────────────────────────────────────────────────────────
|
|
27
27
|
// Layer 1 — the high-level <Camera> component
|
|
28
28
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -141,6 +141,11 @@ var useIncrementalStitcher_1 = require("./stitching/useIncrementalStitcher");
|
|
|
141
141
|
Object.defineProperty(exports, "useIncrementalStitcher", { enumerable: true, get: function () { return useIncrementalStitcher_1.useIncrementalStitcher; } });
|
|
142
142
|
var useIncrementalJSDriver_1 = require("./stitching/useIncrementalJSDriver");
|
|
143
143
|
Object.defineProperty(exports, "useIncrementalJSDriver", { enumerable: true, get: function () { return useIncrementalJSDriver_1.useIncrementalJSDriver; } });
|
|
144
|
+
// F8.3 — vision-camera Frame Processor variant of the non-AR
|
|
145
|
+
// driver. Preferred over `useIncrementalJSDriver` in v0.5+; the
|
|
146
|
+
// JS driver stays exported as a deprecated fallback until v0.6.
|
|
147
|
+
var useFrameProcessorDriver_1 = require("./stitching/useFrameProcessorDriver");
|
|
148
|
+
Object.defineProperty(exports, "useFrameProcessorDriver", { enumerable: true, get: function () { return useFrameProcessorDriver_1.useFrameProcessorDriver; } });
|
|
144
149
|
// ── Batch stitching ───────────────────────────────────────────────────
|
|
145
150
|
// Feed a video file straight to OpenCV's cv::Stitcher, bypassing the
|
|
146
151
|
// incremental pipeline. Useful when you have content captured
|
|
@@ -194,11 +194,20 @@ export interface IncrementalStartOptions {
|
|
|
194
194
|
* - 'jsDriver' — engine skips AR-session registration; JS
|
|
195
195
|
* feeds frames via `processFrameAtPath`. Use in iOS non-AR
|
|
196
196
|
* captures (vision-camera + gyro). No AR session required.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
197
|
+
* LEGACY; deprecated in v0.5, removed in v0.6.
|
|
198
|
+
*
|
|
199
|
+
* - 'frameProcessor' (F8.3 iOS / F8.4 Android, v0.5+) — engine
|
|
200
|
+
* flips on `frameProcessorIngestEnabled` so the vision-camera
|
|
201
|
+
* Frame Processor plugin (`cv_flow_gate_process_frame`) can
|
|
202
|
+
* feed pixel data directly into the engine's gate path. iOS
|
|
203
|
+
* passes the `CVPixelBuffer` straight to `consumeFrame`;
|
|
204
|
+
* Android extracts the Y plane to a ByteArray and encodes
|
|
205
|
+
* accepted frames to JPEG inline (the platform-specific
|
|
206
|
+
* engine-input divergence is tracked as F8.6). Use in non-AR
|
|
207
|
+
* captures driven by `useFrameProcessorDriver`. Pairs with
|
|
208
|
+
* `Camera`'s default driver mode.
|
|
200
209
|
*/
|
|
201
|
-
frameSourceMode?: 'arSession' | 'jsDriver';
|
|
210
|
+
frameSourceMode?: 'arSession' | 'jsDriver' | 'frameProcessor';
|
|
202
211
|
/** Compose-resolution width in pixels (default 720 for portrait, 960 for landscape). */
|
|
203
212
|
composeWidth?: number;
|
|
204
213
|
/** Compose-resolution height in pixels (default 960 for portrait, 720 for landscape). */
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFrameProcessorDriver — vision-camera Frame Processor + gyro
|
|
3
|
+
* driver for the incremental panorama engine. Replaces
|
|
4
|
+
* `useIncrementalJSDriver` in non-AR captures.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists (vs the JS-driver predecessor)
|
|
7
|
+
*
|
|
8
|
+
* The JS driver takes a JPEG snapshot every ~250 ms and feeds the
|
|
9
|
+
* path to `IncrementalStitcher.processFrameAtPath`. That path
|
|
10
|
+
* has three costs:
|
|
11
|
+
*
|
|
12
|
+
* 1. JPEG encode (`takeSnapshot` ≈ 30–80 ms on iPhone 16 Pro)
|
|
13
|
+
* 2. Disk write of the JPEG
|
|
14
|
+
* 3. JPEG decode + cv::Mat alloc inside the engine
|
|
15
|
+
*
|
|
16
|
+
* Per-frame round-trip ~80 ms means ~4 Hz max throughput, and
|
|
17
|
+
* ~80 ms latency between "this is the moment to accept" and "this
|
|
18
|
+
* frame is in the engine". Both numbers caused operator-felt lag
|
|
19
|
+
* on long shelf pans.
|
|
20
|
+
*
|
|
21
|
+
* This hook uses vision-camera's Frame Processor instead. The
|
|
22
|
+
* worklet runs on the camera producer thread at the native frame
|
|
23
|
+
* rate (30 fps on iOS). Each frame goes through a JSI plugin
|
|
24
|
+
* (`cv_flow_gate_process_frame`) directly into
|
|
25
|
+
* `IncrementalStitcher.consumeFrame` — the SAME entry point AR
|
|
26
|
+
* mode uses, with the engine's existing KeyframeGate making the
|
|
27
|
+
* accept/reject decision. Rejected frames cost ~3–8 ms; accepted
|
|
28
|
+
* frames take the same deep-copy + workQueue path AR mode takes.
|
|
29
|
+
*
|
|
30
|
+
* Net wins: no JPEG round-trip on rejected frames, no disk thrash
|
|
31
|
+
* during recording, lower latency to accept, full 30 fps gate
|
|
32
|
+
* evaluation budget.
|
|
33
|
+
*
|
|
34
|
+
* Pose synthesis
|
|
35
|
+
*
|
|
36
|
+
* Non-AR mode has no ARKit pose. We integrate the gyroscope on
|
|
37
|
+
* the JS thread (`react-native-sensors`), accumulate yaw + pitch,
|
|
38
|
+
* and publish them via Reanimated `useSharedValue` so the worklet
|
|
39
|
+
* can read them WITHOUT a thread hop. Translation is reported as
|
|
40
|
+
* zero (no IMU translation; this is a known limitation we share
|
|
41
|
+
* with the legacy driver — drift ~1–2°/min over a 30 s capture is
|
|
42
|
+
* below the gate's overlap threshold and rarely matters).
|
|
43
|
+
*
|
|
44
|
+
* Quaternion synthesis (q = q_yaw * q_pitch * q_roll, Tait-Bryan
|
|
45
|
+
* YPR order to match the legacy driver's body-frame intent):
|
|
46
|
+
* q_yaw = (0, sin(yaw/2), 0, cos(yaw/2))
|
|
47
|
+
* q_pitch = (sin(pitch/2), 0, 0, cos(pitch/2))
|
|
48
|
+
* q_roll = (0, 0, sin(roll/2), cos(roll/2))
|
|
49
|
+
*
|
|
50
|
+
* Expanded (cy, sy = cos/sin(yaw/2); analogous for cp/sp, cr/sr):
|
|
51
|
+
* qx = cy*sp*cr + sy*cp*sr
|
|
52
|
+
* qy = sy*cp*cr - cy*sp*sr
|
|
53
|
+
* qz = cy*cp*sr - sy*sp*cr
|
|
54
|
+
* qw = cy*cp*cr + sy*sp*sr
|
|
55
|
+
*
|
|
56
|
+
* When roll=0 this collapses to the 2-axis form
|
|
57
|
+
* `(cy*sp, sy*cp, -sy*sp, cy*cp)` the legacy driver used, so
|
|
58
|
+
* captures held perfectly level produce identical poses to the
|
|
59
|
+
* pre-roll behaviour.
|
|
60
|
+
*
|
|
61
|
+
* Intrinsics are synthesised from the actual frame dimensions
|
|
62
|
+
* (`frame.width`, `frame.height`) plus the host-provided
|
|
63
|
+
* horizontal/vertical FoV defaults. The stitcher derives its FoV-
|
|
64
|
+
* overlap window from these, so the assumed FoV matters for the
|
|
65
|
+
* gate's overlap math but not for the panorama itself (the
|
|
66
|
+
* stitcher feature-matches + RANSACs the final alignment).
|
|
67
|
+
*
|
|
68
|
+
* Throttling
|
|
69
|
+
*
|
|
70
|
+
* `evalEveryNFrames` controls how often the worklet calls the
|
|
71
|
+
* plugin. Default 1 (every frame). Set higher to amortise the
|
|
72
|
+
* plugin call + consumeFrame's gate evaluation across multiple
|
|
73
|
+
* producer-thread frames on lower-end devices. Independent of —
|
|
74
|
+
* and stacks on top of — the stitcher's own internal
|
|
75
|
+
* `flowEvalEveryNFrames` (see `KeyframeGate.swift`); both
|
|
76
|
+
* throttles can be active simultaneously and the effective cadence
|
|
77
|
+
* is `evalEveryNFrames * flowEvalEveryNFrames`.
|
|
78
|
+
*
|
|
79
|
+
* Lifecycle
|
|
80
|
+
*
|
|
81
|
+
* `start()` subscribes to the gyro and resets pose accumulators.
|
|
82
|
+
* `stop()` unsubscribes and resets. The returned `frameProcessor`
|
|
83
|
+
* is meant to be passed to `<Camera frameProcessor={...} />` —
|
|
84
|
+
* it's stable as long as the plugin reference and the FoV props
|
|
85
|
+
* haven't changed. Returns `null` when the plugin isn't loaded
|
|
86
|
+
* yet; pass `null`-or-fallback to the Camera in that case.
|
|
87
|
+
*
|
|
88
|
+
* Pairing with `IncrementalStitcher.start({frameSourceMode})`
|
|
89
|
+
*
|
|
90
|
+
* The plugin's per-frame call into `consumeFrameFromPlugin` is
|
|
91
|
+
* gated by `IncrementalStitcher.frameProcessorIngestEnabled`,
|
|
92
|
+
* which is TRUE only when the stitcher was started with
|
|
93
|
+
* `frameSourceMode === 'frameProcessor'`. Hosts MUST call
|
|
94
|
+
* `incrementalStitcher.start({ frameSourceMode: 'frameProcessor',
|
|
95
|
+
* ... })` to actually get frames into the engine — otherwise the
|
|
96
|
+
* worklet runs to completion but the wrapper drops the call.
|
|
97
|
+
* `Camera.tsx` does this wiring automatically when the host opts
|
|
98
|
+
* into this driver.
|
|
99
|
+
*/
|
|
100
|
+
import type { ReadonlyFrameProcessor } from 'react-native-vision-camera';
|
|
101
|
+
export interface UseFrameProcessorDriverOptions {
|
|
102
|
+
/**
|
|
103
|
+
* Gyro sample interval in ms (~30 Hz default). Drives the JS-
|
|
104
|
+
* thread pose integration loop; not the producer-thread plugin
|
|
105
|
+
* call rate (the plugin runs at vision-camera's frame rate,
|
|
106
|
+
* usually 30 fps).
|
|
107
|
+
*/
|
|
108
|
+
gyroIntervalMs?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Approximate horizontal FoV of the device camera, used to
|
|
111
|
+
* synthesise `fx` from frame width. Default 65° matches a typical
|
|
112
|
+
* mid-tier smartphone main camera. Host apps that know the actual
|
|
113
|
+
* FoV (e.g. via `Camera.getCameraFormat`) should pass it here —
|
|
114
|
+
* the engine's overlap gate gets a slightly better estimate.
|
|
115
|
+
*/
|
|
116
|
+
fovHorizDegrees?: number;
|
|
117
|
+
/**
|
|
118
|
+
* Approximate vertical FoV of the device camera, used to
|
|
119
|
+
* synthesise `fy` from frame height. Default 50° matches a
|
|
120
|
+
* typical 4:3 phone camera in landscape; for 16:9 portrait you
|
|
121
|
+
* probably want ~75°.
|
|
122
|
+
*/
|
|
123
|
+
fovVertDegrees?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Evaluate the plugin every Nth producer-thread frame. Default 1
|
|
126
|
+
* (every frame). Higher values reduce the producer-thread cost
|
|
127
|
+
* linearly at the price of acceptance latency — N=3 with 30 fps
|
|
128
|
+
* source = up to 100 ms before a key moment is evaluated.
|
|
129
|
+
*/
|
|
130
|
+
evalEveryNFrames?: number;
|
|
131
|
+
}
|
|
132
|
+
export interface FrameProcessorDriverHandle {
|
|
133
|
+
/** Subscribe to the gyro + reset pose accumulators. Idempotent. */
|
|
134
|
+
start: () => void;
|
|
135
|
+
/** Unsubscribe + reset pose. */
|
|
136
|
+
stop: () => void;
|
|
137
|
+
/**
|
|
138
|
+
* Pass this to `<Camera frameProcessor={...} />`. `null` until
|
|
139
|
+
* the JSI plugin is loaded (typically resolves within ~1 frame of
|
|
140
|
+
* mount); the consumer should fall back to undefined / a no-op
|
|
141
|
+
* processor in that window.
|
|
142
|
+
*/
|
|
143
|
+
frameProcessor: ReadonlyFrameProcessor | null;
|
|
144
|
+
/** Whether `start()` has been called and `stop()` hasn't. */
|
|
145
|
+
isRunning: boolean;
|
|
146
|
+
}
|
|
147
|
+
export declare function useFrameProcessorDriver(options?: UseFrameProcessorDriverOptions): FrameProcessorDriverHandle;
|
|
148
|
+
//# sourceMappingURL=useFrameProcessorDriver.d.ts.map
|