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.
@@ -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' | 'UNKNOWN';
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.
@@ -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
- // Safety: ensure the driver is stopped if the component unmounts
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
- frameSourceMode: isNonAR ? 'jsDriver' : 'arSession',
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: 'batch-keyframe',
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 pumping vision-camera snapshots into the engine for
625
- // non-AR captures. AR mode feeds frames natively from the
626
- // ARSession, so the JS driver stays idle in that path. This
627
- // mirrors AuditCaptureScreen.handleHoldStart's `androidDriver.start`
628
- // imperative call see the comment near `useIncrementalJSDriver`
629
- // for why this is NOT done via useEffect.
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
- jsDriver.start(visionCameraRef);
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 snapshots before finalizing so the engine isn't
654
- // racing the final cv::Stitcher pass against late-arriving keyframes.
655
- // No-op in AR mode where jsDriver was never started.
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
- exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash = 'off', isActive = true, video = false, guidance, style, cameraProps, }, ref) {
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
- * Android ignores this option — its engine always accepts
199
- * JS-driven frames.
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