react-native-image-stitcher 0.4.0 → 0.5.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.
@@ -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
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * useFrameProcessorDriver — vision-camera Frame Processor + gyro
5
+ * driver for the incremental panorama engine. Replaces
6
+ * `useIncrementalJSDriver` in non-AR captures.
7
+ *
8
+ * Why this exists (vs the JS-driver predecessor)
9
+ *
10
+ * The JS driver takes a JPEG snapshot every ~250 ms and feeds the
11
+ * path to `IncrementalStitcher.processFrameAtPath`. That path
12
+ * has three costs:
13
+ *
14
+ * 1. JPEG encode (`takeSnapshot` ≈ 30–80 ms on iPhone 16 Pro)
15
+ * 2. Disk write of the JPEG
16
+ * 3. JPEG decode + cv::Mat alloc inside the engine
17
+ *
18
+ * Per-frame round-trip ~80 ms means ~4 Hz max throughput, and
19
+ * ~80 ms latency between "this is the moment to accept" and "this
20
+ * frame is in the engine". Both numbers caused operator-felt lag
21
+ * on long shelf pans.
22
+ *
23
+ * This hook uses vision-camera's Frame Processor instead. The
24
+ * worklet runs on the camera producer thread at the native frame
25
+ * rate (30 fps on iOS). Each frame goes through a JSI plugin
26
+ * (`cv_flow_gate_process_frame`) directly into
27
+ * `IncrementalStitcher.consumeFrame` — the SAME entry point AR
28
+ * mode uses, with the engine's existing KeyframeGate making the
29
+ * accept/reject decision. Rejected frames cost ~3–8 ms; accepted
30
+ * frames take the same deep-copy + workQueue path AR mode takes.
31
+ *
32
+ * Net wins: no JPEG round-trip on rejected frames, no disk thrash
33
+ * during recording, lower latency to accept, full 30 fps gate
34
+ * evaluation budget.
35
+ *
36
+ * Pose synthesis
37
+ *
38
+ * Non-AR mode has no ARKit pose. We integrate the gyroscope on
39
+ * the JS thread (`react-native-sensors`), accumulate yaw + pitch,
40
+ * and publish them via Reanimated `useSharedValue` so the worklet
41
+ * can read them WITHOUT a thread hop. Translation is reported as
42
+ * zero (no IMU translation; this is a known limitation we share
43
+ * with the legacy driver — drift ~1–2°/min over a 30 s capture is
44
+ * below the gate's overlap threshold and rarely matters).
45
+ *
46
+ * Quaternion synthesis (q = q_yaw * q_pitch * q_roll, Tait-Bryan
47
+ * YPR order to match the legacy driver's body-frame intent):
48
+ * q_yaw = (0, sin(yaw/2), 0, cos(yaw/2))
49
+ * q_pitch = (sin(pitch/2), 0, 0, cos(pitch/2))
50
+ * q_roll = (0, 0, sin(roll/2), cos(roll/2))
51
+ *
52
+ * Expanded (cy, sy = cos/sin(yaw/2); analogous for cp/sp, cr/sr):
53
+ * qx = cy*sp*cr + sy*cp*sr
54
+ * qy = sy*cp*cr - cy*sp*sr
55
+ * qz = cy*cp*sr - sy*sp*cr
56
+ * qw = cy*cp*cr + sy*sp*sr
57
+ *
58
+ * When roll=0 this collapses to the 2-axis form
59
+ * `(cy*sp, sy*cp, -sy*sp, cy*cp)` the legacy driver used, so
60
+ * captures held perfectly level produce identical poses to the
61
+ * pre-roll behaviour.
62
+ *
63
+ * Intrinsics are synthesised from the actual frame dimensions
64
+ * (`frame.width`, `frame.height`) plus the host-provided
65
+ * horizontal/vertical FoV defaults. The stitcher derives its FoV-
66
+ * overlap window from these, so the assumed FoV matters for the
67
+ * gate's overlap math but not for the panorama itself (the
68
+ * stitcher feature-matches + RANSACs the final alignment).
69
+ *
70
+ * Throttling
71
+ *
72
+ * `evalEveryNFrames` controls how often the worklet calls the
73
+ * plugin. Default 1 (every frame). Set higher to amortise the
74
+ * plugin call + consumeFrame's gate evaluation across multiple
75
+ * producer-thread frames on lower-end devices. Independent of —
76
+ * and stacks on top of — the stitcher's own internal
77
+ * `flowEvalEveryNFrames` (see `KeyframeGate.swift`); both
78
+ * throttles can be active simultaneously and the effective cadence
79
+ * is `evalEveryNFrames * flowEvalEveryNFrames`.
80
+ *
81
+ * Lifecycle
82
+ *
83
+ * `start()` subscribes to the gyro and resets pose accumulators.
84
+ * `stop()` unsubscribes and resets. The returned `frameProcessor`
85
+ * is meant to be passed to `<Camera frameProcessor={...} />` —
86
+ * it's stable as long as the plugin reference and the FoV props
87
+ * haven't changed. Returns `null` when the plugin isn't loaded
88
+ * yet; pass `null`-or-fallback to the Camera in that case.
89
+ *
90
+ * Pairing with `IncrementalStitcher.start({frameSourceMode})`
91
+ *
92
+ * The plugin's per-frame call into `consumeFrameFromPlugin` is
93
+ * gated by `IncrementalStitcher.frameProcessorIngestEnabled`,
94
+ * which is TRUE only when the stitcher was started with
95
+ * `frameSourceMode === 'frameProcessor'`. Hosts MUST call
96
+ * `incrementalStitcher.start({ frameSourceMode: 'frameProcessor',
97
+ * ... })` to actually get frames into the engine — otherwise the
98
+ * worklet runs to completion but the wrapper drops the call.
99
+ * `Camera.tsx` does this wiring automatically when the host opts
100
+ * into this driver.
101
+ */
102
+ Object.defineProperty(exports, "__esModule", { value: true });
103
+ exports.useFrameProcessorDriver = useFrameProcessorDriver;
104
+ const react_1 = require("react");
105
+ const react_native_sensors_1 = require("react-native-sensors");
106
+ // Reanimated's `useSharedValue` is the documented vision-camera
107
+ // idiom, but it's a heavy peer dep. `react-native-worklets-core`
108
+ // (already a transitive dep via vision-camera v4 on RN 0.84) exposes
109
+ // the same API surface (a `value` getter/setter readable from
110
+ // worklets and the JS thread) and is sufficient for our use.
111
+ const react_native_worklets_core_1 = require("react-native-worklets-core");
112
+ const react_native_vision_camera_1 = require("react-native-vision-camera");
113
+ function useFrameProcessorDriver(options = {}) {
114
+ const { gyroIntervalMs = 33, fovHorizDegrees = 65, fovVertDegrees = 50, evalEveryNFrames = 1, } = options;
115
+ // ── Plugin acquisition ──────────────────────────────────────────
116
+ //
117
+ // `initFrameProcessorPlugin` can return `undefined` if called
118
+ // before vision-camera's plugin registry has finished initialising
119
+ // (race observed in F8.1.a). We retry on a fixed timer instead of
120
+ // firing on every render — the earlier render-driven pattern
121
+ // (adversarial-review H3) re-invoked `initFrameProcessorPlugin`
122
+ // 60+ times per second during recording, and the vision-camera
123
+ // contract for repeated lookups is undocumented.
124
+ //
125
+ // Pattern: mount-once useEffect, try synchronously, fall back to a
126
+ // 16-ms retry timer until success or unmount.
127
+ const [plugin, setPlugin] = (0, react_1.useState)(null);
128
+ (0, react_1.useEffect)(() => {
129
+ let cancelled = false;
130
+ let timerId = null;
131
+ const tryAcquire = () => {
132
+ if (cancelled)
133
+ return;
134
+ const p = react_native_vision_camera_1.VisionCameraProxy.initFrameProcessorPlugin('cv_flow_gate_process_frame', {});
135
+ if (p != null) {
136
+ setPlugin(p);
137
+ return;
138
+ }
139
+ // ~one display-frame retry — matches the F8.1.a observation
140
+ // that the registry becomes ready by the next render tick.
141
+ timerId = setTimeout(tryAcquire, 16);
142
+ };
143
+ tryAcquire();
144
+ return () => {
145
+ cancelled = true;
146
+ if (timerId != null)
147
+ clearTimeout(timerId);
148
+ };
149
+ // Empty deps on purpose — runs ONCE on mount. Re-acquiring on
150
+ // re-render would race with worklet binding.
151
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
+ }, []);
153
+ // ── Shared values (worklet ↔ JS thread) ─────────────────────────
154
+ //
155
+ // Reanimated guarantees coherent reads from the producer thread.
156
+ // We write yaw/pitch on the JS thread (gyro callbacks); the worklet
157
+ // reads them every frame. No round-trip cost — these are mapped
158
+ // into the worklet's runtime by the Reanimated bridge.
159
+ //
160
+ // FoV-derived values (the "half-angle tangent reciprocal"
161
+ // f-numerators) are pre-computed on the JS thread + published via
162
+ // shared values so the worklet's dependency array shrinks to just
163
+ // `[plugin]`. Earlier draft baked `fovHorizDegrees` /
164
+ // `fovVertDegrees` into the closure → worklet re-serialised on
165
+ // every host re-render that changed the prop refs (adversarial-
166
+ // review M1).
167
+ const sharedYaw = (0, react_native_worklets_core_1.useSharedValue)(0);
168
+ const sharedPitch = (0, react_native_worklets_core_1.useSharedValue)(0);
169
+ // F8.3-followup-roll — integrate gyroscope Z (out-of-screen for a
170
+ // portrait device) to track wrist-twist roll. Field captures with
171
+ // casual hand-hold rarely stay perfectly level; without this the
172
+ // pose stream lies and the cv::Stitcher's intrinsic estimator may
173
+ // pick a worse projection mode.
174
+ const sharedRoll = (0, react_native_worklets_core_1.useSharedValue)(0);
175
+ const sharedFrameCounter = (0, react_native_worklets_core_1.useSharedValue)(0);
176
+ const sharedEvalEveryN = (0, react_native_worklets_core_1.useSharedValue)(Math.max(1, evalEveryNFrames));
177
+ const sharedFxNumerator = (0, react_native_worklets_core_1.useSharedValue)(1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2)));
178
+ const sharedFyNumerator = (0, react_native_worklets_core_1.useSharedValue)(1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2)));
179
+ // Keep prop-derived shared values in sync. Cheap re-renders;
180
+ // these don't trigger worklet rebuild.
181
+ (0, react_1.useEffect)(() => {
182
+ sharedEvalEveryN.value = Math.max(1, evalEveryNFrames);
183
+ }, [evalEveryNFrames, sharedEvalEveryN]);
184
+ (0, react_1.useEffect)(() => {
185
+ sharedFxNumerator.value =
186
+ 1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2));
187
+ }, [fovHorizDegrees, sharedFxNumerator]);
188
+ (0, react_1.useEffect)(() => {
189
+ sharedFyNumerator.value =
190
+ 1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2));
191
+ }, [fovVertDegrees, sharedFyNumerator]);
192
+ // ── Lifecycle state (JS thread only) ────────────────────────────
193
+ const gyroSubRef = (0, react_1.useRef)(null);
194
+ const lastGyroAtRef = (0, react_1.useRef)(null);
195
+ const isRunningRef = (0, react_1.useRef)(false);
196
+ const stop = (0, react_1.useCallback)(() => {
197
+ if (gyroSubRef.current) {
198
+ gyroSubRef.current.unsubscribe();
199
+ gyroSubRef.current = null;
200
+ }
201
+ isRunningRef.current = false;
202
+ sharedYaw.value = 0;
203
+ sharedPitch.value = 0;
204
+ sharedRoll.value = 0;
205
+ sharedFrameCounter.value = 0;
206
+ lastGyroAtRef.current = null;
207
+ }, [sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
208
+ const start = (0, react_1.useCallback)(() => {
209
+ if (isRunningRef.current)
210
+ return;
211
+ sharedYaw.value = 0;
212
+ sharedPitch.value = 0;
213
+ sharedRoll.value = 0;
214
+ sharedFrameCounter.value = 0;
215
+ lastGyroAtRef.current = null;
216
+ isRunningRef.current = true;
217
+ // Gyro integration. Each sample carries angular velocity in
218
+ // rad/s; multiply by dt to accumulate displacement. Axes for a
219
+ // device held portrait:
220
+ // y = horizontal pan (yaw, about world-Y)
221
+ // x = vertical tilt (pitch, about world-X)
222
+ // z = wrist-twist roll (about world-Z, normal to the screen)
223
+ // Signs match the legacy `useIncrementalJSDriver` for x/y; z
224
+ // follows the same right-hand-rule convention. If field
225
+ // captures show inverted roll, flip the sign on `z * dt` below.
226
+ (0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, gyroIntervalMs);
227
+ gyroSubRef.current = react_native_sensors_1.gyroscope.subscribe({
228
+ next: ({ x, y, z }) => {
229
+ const now = Date.now();
230
+ if (lastGyroAtRef.current === null) {
231
+ lastGyroAtRef.current = now;
232
+ return;
233
+ }
234
+ const dt = (now - lastGyroAtRef.current) / 1000.0;
235
+ lastGyroAtRef.current = now;
236
+ sharedYaw.value += y * dt;
237
+ sharedPitch.value += x * dt;
238
+ sharedRoll.value += z * dt;
239
+ },
240
+ error: (err) => {
241
+ // eslint-disable-next-line no-console
242
+ console.warn('[useFrameProcessorDriver] gyro error', err);
243
+ },
244
+ });
245
+ }, [gyroIntervalMs, sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
246
+ // ── Worklet ─────────────────────────────────────────────────────
247
+ //
248
+ // Memoised: rebuilt only when the plugin acquires (null → defined)
249
+ // or when the FoV props change (cheap math but they're in the
250
+ // closure so they must be in the deps). Shared values are NOT in
251
+ // the deps — Reanimated wires their .value reads through the
252
+ // worklet's frozen runtime independently of React's render cycle.
253
+ const frameProcessor = (0, react_native_vision_camera_1.useFrameProcessor)((frame) => {
254
+ 'worklet';
255
+ if (plugin == null)
256
+ return;
257
+ // Throttle: only every Nth frame. Counter increments first so
258
+ // frame #0 is "due" (N>=1 always divides 0). Cheaper than
259
+ // calling the plugin on rejected frames; saves the ~1 µs
260
+ // marshalling cost per skip.
261
+ sharedFrameCounter.value += 1;
262
+ const N = sharedEvalEveryN.value;
263
+ if (N > 1 && (sharedFrameCounter.value % N) !== 0)
264
+ return;
265
+ // Synthesise quaternion from accumulated yaw + pitch + roll.
266
+ // YPR Tait-Bryan order: q = q_yaw * q_pitch * q_roll. When
267
+ // roll=0 this reduces to the legacy 2-axis form (cy*sp, sy*cp,
268
+ // -sy*sp, cy*cp), so captures held level produce identical
269
+ // poses to the pre-F8.3-followup-roll behaviour. See the
270
+ // expanded math in the file header doc-comment.
271
+ const halfYaw = sharedYaw.value / 2;
272
+ const halfPitch = sharedPitch.value / 2;
273
+ const halfRoll = sharedRoll.value / 2;
274
+ const cy_ = Math.cos(halfYaw);
275
+ const sy_ = Math.sin(halfYaw);
276
+ const cp = Math.cos(halfPitch);
277
+ const sp = Math.sin(halfPitch);
278
+ const cr = Math.cos(halfRoll);
279
+ const sr = Math.sin(halfRoll);
280
+ const qx = cy_ * sp * cr + sy_ * cp * sr;
281
+ const qy = sy_ * cp * cr - cy_ * sp * sr;
282
+ const qz = cy_ * cp * sr - sy_ * sp * cr;
283
+ const qw = cy_ * cp * cr + sy_ * sp * sr;
284
+ // Intrinsics from FoV + actual frame dims.
285
+ // fx = w * (1 / (2 * tan(fovH/2))) (the parenthesised half
286
+ // is the precomputed `sharedFxNumerator` — see M1 fix).
287
+ const w = frame.width;
288
+ const h = frame.height;
289
+ const fx = w * sharedFxNumerator.value;
290
+ const fy = h * sharedFyNumerator.value;
291
+ plugin.call(frame, {
292
+ tx: 0, ty: 0, tz: 0,
293
+ qx, qy, qz, qw,
294
+ fx, fy,
295
+ cx: w / 2, cy: h / 2,
296
+ imageWidth: w, imageHeight: h,
297
+ timestampMs: 0,
298
+ // 2 == RNSARTrackingState.tracking — we always claim "good
299
+ // tracking" because there's no ARKit signal to differentiate
300
+ // (matches legacy useIncrementalJSDriver semantics).
301
+ trackingStateRaw: 2,
302
+ });
303
+ // Deps array intentionally minimal: only `plugin` actually
304
+ // requires worklet rebuild. All FoV / pose / counter / cadence
305
+ // values flow through stable shared-value refs that Reanimated
306
+ // wires through the producer-thread runtime independently of
307
+ // React's render cycle. (Adversarial-review M1.)
308
+ }, [plugin]);
309
+ // ── Return handle ───────────────────────────────────────────────
310
+ //
311
+ // Returns a getter for `isRunning` so callers always see the live
312
+ // state (the hook itself doesn't re-render on start/stop — that's
313
+ // intentional, avoids stale-Camera-prop churn).
314
+ return (0, react_1.useMemo)(() => ({
315
+ start,
316
+ stop,
317
+ frameProcessor: plugin != null ? frameProcessor : null,
318
+ get isRunning() { return isRunningRef.current; },
319
+ }), [start, stop, plugin, frameProcessor]);
320
+ }
321
+ //# sourceMappingURL=useFrameProcessorDriver.js.map
@@ -44,6 +44,11 @@ exports.useIncrementalJSDriver = useIncrementalJSDriver;
44
44
  const react_1 = require("react");
45
45
  const react_native_1 = require("react-native");
46
46
  const react_native_sensors_1 = require("react-native-sensors");
47
+ // One-shot deprecation flag — module-scoped so multiple host
48
+ // instances of the hook all share the same gate and we only emit
49
+ // the warning the first time anyone calls .start() in this
50
+ // process.
51
+ let deprecationWarningEmitted = false;
47
52
  function getNativeIncremental() {
48
53
  const m = react_native_1.NativeModules['IncrementalStitcher'];
49
54
  if (!m || typeof m !== 'object')
@@ -88,6 +93,22 @@ function useIncrementalJSDriver(options = {}) {
88
93
  // non-AR mode.
89
94
  if (isRunningRef.current)
90
95
  return;
96
+ // F8.5 — one-shot deprecation warning. v0.5.0 introduced
97
+ // `useFrameProcessorDriver` (vision-camera producer-thread
98
+ // path, native frame rate, no JPEG round-trip). The legacy
99
+ // takeSnapshot path stays available for one minor cycle to
100
+ // give hosts time to migrate, then is removed in v0.6.
101
+ if (!deprecationWarningEmitted) {
102
+ deprecationWarningEmitted = true;
103
+ // eslint-disable-next-line no-console
104
+ console.warn('[react-native-image-stitcher] `useIncrementalJSDriver` '
105
+ + 'is DEPRECATED as of v0.5.0 and will be REMOVED in '
106
+ + 'v0.6.0. Migrate to `useFrameProcessorDriver` (or '
107
+ + 'simply let `<Camera>` use its default driver — no host '
108
+ + 'code change needed). Opt-out via the `legacyDriver` '
109
+ + 'prop on `<Camera>` if you need to stay on the legacy '
110
+ + 'path temporarily.');
111
+ }
91
112
  const native = getNativeIncremental();
92
113
  if (!native)
93
114
  return;