react-native-image-stitcher 0.14.1 → 0.15.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.md +35 -0
  3. package/RNImageStitcher.podspec +8 -1
  4. package/android/build.gradle +0 -16
  5. package/android/src/main/cpp/CMakeLists.txt +2 -63
  6. package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
  7. package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
  8. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
  9. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
  10. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
  11. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
  12. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +13 -64
  13. package/cpp/keyframe_gate.cpp +82 -23
  14. package/cpp/keyframe_gate.hpp +31 -2
  15. package/cpp/stitcher.cpp +208 -28
  16. package/cpp/tests/CMakeLists.txt +18 -12
  17. package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
  18. package/cpp/tests/warp_guard_test.cpp +48 -0
  19. package/cpp/warp_guard.hpp +41 -0
  20. package/dist/ar/useARSession.d.ts +9 -0
  21. package/dist/ar/useARSession.js +24 -2
  22. package/dist/camera/Camera.d.ts +31 -16
  23. package/dist/camera/Camera.js +27 -4
  24. package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
  25. package/dist/camera/CaptureStitchStatsToast.js +27 -7
  26. package/dist/camera/PanoramaSettings.d.ts +10 -223
  27. package/dist/camera/PanoramaSettings.js +6 -28
  28. package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
  29. package/dist/camera/PanoramaSettingsBridge.js +3 -102
  30. package/dist/camera/PanoramaSettingsModal.js +7 -1
  31. package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
  32. package/dist/camera/buildPanoramaInitialSettings.js +4 -0
  33. package/dist/camera/cameraErrorMessages.d.ts +32 -0
  34. package/dist/camera/cameraErrorMessages.js +53 -0
  35. package/dist/camera/selectCaptureDevice.d.ts +5 -1
  36. package/dist/camera/selectCaptureDevice.js +22 -2
  37. package/dist/camera/useCapture.js +38 -0
  38. package/dist/index.d.ts +5 -8
  39. package/dist/index.js +11 -34
  40. package/dist/stitching/incremental.d.ts +1 -117
  41. package/dist/stitching/stitchVideo.d.ts +0 -35
  42. package/dist/types.d.ts +0 -87
  43. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
  44. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
  45. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
  46. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
  47. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
  48. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
  49. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
  50. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
  51. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
  52. package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
  53. package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
  54. package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
  55. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
  56. package/package.json +3 -2
  57. package/src/ar/useARSession.ts +35 -5
  58. package/src/camera/Camera.tsx +63 -24
  59. package/src/camera/CaptureStitchStatsToast.tsx +58 -14
  60. package/src/camera/PanoramaSettings.ts +16 -289
  61. package/src/camera/PanoramaSettingsBridge.ts +3 -114
  62. package/src/camera/PanoramaSettingsModal.tsx +14 -1
  63. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
  64. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
  65. package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
  66. package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
  67. package/src/camera/buildPanoramaInitialSettings.ts +17 -0
  68. package/src/camera/cameraErrorMessages.ts +84 -0
  69. package/src/camera/selectCaptureDevice.ts +28 -3
  70. package/src/camera/useCapture.ts +44 -1
  71. package/src/index.ts +11 -40
  72. package/src/stitching/incremental.ts +3 -140
  73. package/src/stitching/stitchVideo.ts +0 -26
  74. package/src/types.ts +0 -95
  75. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
  76. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
  77. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
  78. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
  79. package/cpp/stitcher_frame_jsi.cpp +0 -214
  80. package/cpp/stitcher_frame_jsi.hpp +0 -108
  81. package/cpp/stitcher_proxy_jsi.cpp +0 -109
  82. package/cpp/stitcher_proxy_jsi.hpp +0 -46
  83. package/cpp/stitcher_worklet_dispatch.cpp +0 -103
  84. package/cpp/stitcher_worklet_dispatch.hpp +0 -71
  85. package/cpp/stitcher_worklet_registry.cpp +0 -91
  86. package/cpp/stitcher_worklet_registry.hpp +0 -146
  87. package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
  88. package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
  89. package/dist/stitching/IncrementalStitcherView.js +0 -157
  90. package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
  91. package/dist/stitching/StitcherWorkletRegistry.js +0 -78
  92. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
  93. package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
  94. package/dist/stitching/useFrameProcessor.d.ts +0 -119
  95. package/dist/stitching/useFrameProcessor.js +0 -196
  96. package/dist/stitching/useFrameStream.d.ts +0 -34
  97. package/dist/stitching/useFrameStream.js +0 -234
  98. package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
  99. package/dist/stitching/useThrottledFrameProcessor.js +0 -132
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
  101. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
  102. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
  104. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
  105. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
  106. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
  107. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  108. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
  109. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
  110. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
  111. package/src/stitching/IncrementalStitcherView.tsx +0 -198
  112. package/src/stitching/StitcherWorkletRegistry.ts +0 -156
  113. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
  114. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
  115. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
  116. package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
  117. package/src/stitching/useFrameProcessor.ts +0 -226
  118. package/src/stitching/useFrameStream.ts +0 -271
  119. package/src/stitching/useThrottledFrameProcessor.ts +0 -145
package/CHANGELOG.md CHANGED
@@ -16,6 +16,166 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.15.0] — 2026-06-07
20
+
21
+ ### Breaking — only `batch-keyframe` remains; host-worklet / frame-stream hooks removed
22
+
23
+ The live/incremental stitching engines (hybrid, slit-scan, firstwins) and the
24
+ third-party host-worklet / frame-stream observer API were archived (kept under
25
+ `archive/`, excluded from every build surface) so the SDK now ships only the
26
+ `batch-keyframe` capture path. Removed from the public API:
27
+
28
+ - **Hooks** `useFrameProcessor`, `useThrottledFrameProcessor`, `useFrameStream`
29
+ and their option types (`ThrottledFrameProcessorOptions`, `FrameStreamOptions`,
30
+ `SampledFrame`). To compose first-party stitching, use vision-camera's own
31
+ `useFrameProcessor` with `useStitcherWorklet().call(frame)` (see the example app).
32
+ - The **slit-scan / hybrid** panorama-engine settings types and their
33
+ native-config adapters (`slitscanSettingsToNativeConfig`,
34
+ `hybridSettingsToNativeConfig`).
35
+
36
+ A type-only break for the default batch-keyframe path; per the 0.x stability
37
+ policy this bumps a new MINOR.
38
+
39
+ ### Changed — iOS + Android unified on the manual `cv::detail` stitch pipeline
40
+
41
+ Both platforms now run the **same** manual `cv::detail` stitch pipeline
42
+ (`useManualPipeline=true` on both), so a given capture produces consistent,
43
+ more robust output regardless of platform. Previously iOS used the manual
44
+ pipeline while Android used the high-level `cv::Stitcher` — the two diverged on
45
+ resolution, exposure handling, and wide-capture robustness. The unified manual
46
+ path carries:
47
+
48
+ - **Exposure compensation** (`cv::detail::GainCompensator`, GAIN_BLOCKS) — evens
49
+ brightness/colour across frames before blending, removing the visible seam
50
+ steps the manual path previously had.
51
+ - **Matched registration / compositing resolution** (registration 0.6 MP,
52
+ composite 1.0 MP) on both platforms.
53
+ - The **cylindrical warp fallback** (below), so wide / 0.5× captures survive on
54
+ both platforms.
55
+
56
+ The decision was made after an on-device A/B (manual vs high-level at matched
57
+ resolution): with parity the manual path matched the high-level on quality and
58
+ was strictly more robust on wide captures. Background + the verification trail
59
+ are recorded in [`docs/stitch-pipeline-architecture.md`](docs/stitch-pipeline-architecture.md).
60
+
61
+ ### Added — cylindrical warp fallback for wide / 0.5× captures
62
+
63
+ When the configured (plane) warper would diverge on a wide or 0.5× ultra-wide
64
+ capture — a single frame's warp canvas exceeding the 100 MP guard — the stitcher
65
+ now auto-retries with the bounded cylindrical projection instead of failing with
66
+ `STITCH_CAMERA_PARAMS_FAIL`. Wide and ultra-wide (0.5×) panoramas that
67
+ previously errored out now complete. Because the pipeline is now unified
68
+ (above), this fallback applies on both iOS and Android.
69
+
70
+ ### Added — `userFacingStitchError()` for friendly recoverable-stitch copy
71
+
72
+ New public SDK export that maps a recoverable stitch `CameraErrorCode`
73
+ (`STITCH_NEED_MORE_IMGS`, `STITCH_CAMERA_PARAMS_FAIL`, `STITCH_HOMOGRAPHY_FAIL`,
74
+ `STITCH_OOM`) to friendly, action-guiding `{ title, message }` copy for a host
75
+ `Alert` / toast — so the user sees "pan more slowly" / "pivot in place" instead
76
+ of the raw `cv::Stitcher` diagnostic. Returns `null` for every non-recoverable
77
+ code (permission denied, device unavailable, generic finalize failure, unknown,
78
+ …), so the host falls back to its generic error UI. Call it from `onError`:
79
+
80
+ ```tsx
81
+ import { userFacingStitchError } from 'react-native-image-stitcher';
82
+
83
+ onError={(err) => {
84
+ const friendly = userFacingStitchError(err.code);
85
+ if (friendly) Alert.alert(friendly.title, friendly.message);
86
+ else reportGenericError(err);
87
+ }}
88
+ ```
89
+
90
+ Also exports the `UserFacingStitchError` type (`{ title, message }`). Lives in
91
+ the SDK (not per-host) so every consumer shows the same vetted guidance for the
92
+ same failure, and so the mapping is unit-testable in isolation.
93
+
94
+ ### Fixed — friendlier stitch-failure classification + example UX
95
+
96
+ - `STITCH_NEED_MORE_IMGS` now also classifies the manual pipeline's "0 valid
97
+ pairwise matches / frames may not overlap enough" failure, which previously
98
+ surfaced as a generic `PANORAMA_FINALIZE_FAILED`. Both insufficient-overlap
99
+ signals now map to the same recoverable "pan more slowly" outcome (and so pick
100
+ up the `userFacingStitchError` copy above).
101
+ - The example app now shows friendly, action-guiding guidance — via
102
+ `userFacingStitchError` (an Alert) on a stitch failure (`onError`), and a
103
+ transient **toast** when frames are dropped for insufficient overlap
104
+ (`onFramesDropped`) — shown only when **>30%** of the requested frames are
105
+ missing from the final stitch (e.g. ≥2 of 6), so minor drops stay silent. The toast (`CaptureStitchStatsToast`) also
106
+ gained optional `title` (bold, above the message) and `placement`
107
+ (`'top'` | `'center'`) props; the example shows a centered title+body toast.
108
+ Failure alerts now lead with the corrective ask as the title (e.g. "Please
109
+ pan more slowly" / "Try a shorter sweep") and explain the cause in the body.
110
+
111
+ ### Fixed — reach the ultra-wide by device-swap when a logical multi-cam can't (Samsung / Camera2)
112
+
113
+ `selectCaptureDevice` now device-swaps to a standalone ultra-wide camera when a
114
+ logical multi-cam device merely *lists* the ultra-wide but can't reach it by
115
+ `zoom` (its zoom range starts at 1.0 — common on Android / Camera2 / Samsung,
116
+ where the ultra-wide is a separate physical camera rather than a zoom target).
117
+ Previously such devices stayed on the multi-cam device and 0.5× showed the
118
+ wide-angle FOV. A logical device whose zoom range genuinely extends to the
119
+ ultra-wide (e.g. iOS virtual devices, `minZoom ≈ 0.5`) is still preferred and
120
+ lens-switches via zoom as before.
121
+
122
+ ### Added — time-budget keyframe force-accept (`maxKeyframeIntervalMs`)
123
+
124
+ The keyframe gate now force-accepts a keyframe when a configurable wall-clock
125
+ interval has elapsed since the last accepted keyframe — even if the novelty /
126
+ overlap threshold wasn't met — so a slow or static pan never leaves a temporal
127
+ gap. Default **2000 ms (2 s)**; `0` disables it. Configurable via the
128
+ `<Camera defaultMaxKeyframeIntervalMs>` prop, the `FrameSelectionSettings.maxKeyframeIntervalMs`
129
+ field, or the in-app settings panel. Applies to BOTH AR (plane-overlap) and
130
+ non-AR (flow) capture paths; force-accepted keyframes count toward
131
+ `maxKeyframes` (the cap still finalises the capture).
132
+
133
+ ### Added — inscribed-rect panorama crop (opt-in)
134
+
135
+ `<Camera maxInscribedRectCrop={true}>` (and the `enableMaxInscribedRectCrop`
136
+ panorama setting) crops the finished panorama to the largest axis-aligned
137
+ rectangle inscribed in the coverage mask — clean edges with no black corners
138
+ from unfilled projection regions. **It is opt-in; the default is off.** The
139
+ default crop stays the bounding box of non-black pixels, which preserves all
140
+ stitched content but can leave black corners. Inscribed-rect can shrink the
141
+ output substantially on lopsided or ultra-wide masks, so it isn't the default.
142
+
143
+ ### Fixed — Android keyframe-gate flow reason labels
144
+
145
+ `KeyframeGate.reasonFromCode` (Android) didn't map the v0.3.0 flow-strategy
146
+ reason codes 12–15, so accepted keyframes logged as `unknown(12)`. They now
147
+ read `ok-flow` / `first-flow` / `overlap-too-high (flow)` / `ok-flow-translation`,
148
+ matching the iOS labels. Logging only — keyframe selection is unchanged.
149
+
150
+ ## [0.14.2] — 2026-06-03
151
+
152
+ ### Fixed — AR preview blank on first entry (intermittent camera-handoff race)
153
+
154
+ `<Camera>` mounted the vision-camera preview before the device AR-support
155
+ probe (`isSupported()`) resolved: `isAvailable` starts `false`, so
156
+ `deriveEffectiveCaptureSource` returned `'non-ar'` and vision-camera's
157
+ AVCaptureSession grabbed the camera. When the probe resolved ~200-500 ms
158
+ later and the source flipped to AR, ARKit's `session.run()` raced the
159
+ still-open AVCaptureSession for the (mutually-exclusive) camera and lost
160
+ with `ARError "Required sensor failed."` — leaving a blank AR preview and an
161
+ "AR session has no current frame" error on the next capture. Being
162
+ timing-dependent it reproduced intermittently; toggling AR off→on recovered
163
+ (that path releases the camera cleanly first).
164
+
165
+ `useARSession` now exposes `supportProbed` (true once the one-shot
166
+ `isSupported()` probe settles — success or failure). `<Camera>` defers the
167
+ initial camera mount while AR is the intended source but support is still
168
+ unknown, rendering the "Switching camera…" placeholder instead of
169
+ vision-camera, so vision-camera never contends for the camera when AR is the
170
+ intent.
171
+
172
+ ### Fixed — consumer iOS pod build pulled in the lib's C++ gtest unit tests
173
+
174
+ `RNImageStitcher.podspec`'s `cpp/**/*.{h,hpp,cpp}` glob slurped the lib's own
175
+ `cpp/tests/*.cpp` (which `#include <gtest/gtest.h>`) into every host pod
176
+ build, failing with `'gtest/gtest.h' file not found`. Added
177
+ `s.exclude_files = ['cpp/tests/**/*']`.
178
+
19
179
  ## [0.14.1] — 2026-06-01
20
180
 
21
181
  ### Docs
package/README.md CHANGED
@@ -212,7 +212,9 @@ These mirror the in-app settings panel; most apps never set them.
212
212
  | `defaultFlowMaxTranslationCm` | `number` | `50` | Max IMU translation between keyframes; 0 = disabled. |
213
213
  | `defaultKeyframeMaxCount` | `number` | `6` | Keyframe cap per capture (3–10). |
214
214
  | `defaultKeyframeOverlapThreshold` | `number` | `0.20` | Min overlap to accept a keyframe (0.20–0.60). |
215
+ | `defaultMaxKeyframeIntervalMs` | `number` | `2000` | Time-budget force-accept: take a keyframe at least every N ms during a pan even if the overlap/novelty threshold isn't met, so a slow or static pan never leaves a temporal gap. Force-accepted keyframes still count toward the keyframe cap. `0` = disabled. AR + non-AR. Also exposed as the `FrameSelectionSettings.maxKeyframeIntervalMs` settings field and in the in-app settings panel. |
215
216
  | `defaultCompositingResolMP` / `defaultRegistrationResolMP` / `defaultSeamEstimationResolMP` | `number` | — | Forward-looking cv::Stitcher resolution knobs (currently no-ops). |
217
+ | `maxInscribedRectCrop` | `boolean` | `false` | Opt in with `true` to crop the panorama to the largest inscribed rectangle (clean edges, no black corners) instead of the bounding box. Default keeps the bounding-box crop (all stitched content; may show black corners). Inscribed-rect can shrink the output on lopsided / ultra-wide masks. |
216
218
 
217
219
  ### UI toggles
218
220
 
@@ -287,6 +289,39 @@ type CameraCaptureResult =
287
289
  `STITCH_HOMOGRAPHY_FAIL`, `STITCH_CAMERA_PARAMS_FAIL`, `STITCH_OOM`,
288
290
  `OUTPUT_WRITE_FAILED`, plus `VISION_CAMERA_RUNTIME`.
289
291
 
292
+ #### Friendly copy for recoverable stitch failures — `userFacingStitchError`
293
+
294
+ The four `STITCH_*` codes are *recoverable*: the user can usually fix them by
295
+ re-capturing (pan more slowly, pivot in place, shorten the sweep). For those,
296
+ the SDK exports `userFacingStitchError(code)` — it returns
297
+ `{ title, message }` of vetted, action-guiding copy you can drop straight into a
298
+ host `Alert`/toast (instead of surfacing the raw `cv::Stitcher` diagnostic), and
299
+ returns `null` for every non-recoverable code so you fall back to your generic
300
+ error UI:
301
+
302
+ ```tsx
303
+ import {
304
+ Camera,
305
+ userFacingStitchError,
306
+ type UserFacingStitchError,
307
+ } from 'react-native-image-stitcher';
308
+ import { Alert } from 'react-native';
309
+
310
+ <Camera
311
+ onError={(err) => {
312
+ const friendly: UserFacingStitchError | null = userFacingStitchError(err.code);
313
+ if (friendly) {
314
+ Alert.alert(friendly.title, friendly.message); // "pan more slowly", "pivot in place", …
315
+ } else {
316
+ reportGenericError(err); // permission denied, device unavailable, etc.
317
+ }
318
+ }}
319
+ />;
320
+ ```
321
+
322
+ It lives in the SDK (not per-host) so every consumer shows the same guidance for
323
+ the same failure. The `example/` app uses it end-to-end.
324
+
290
325
  ### Migration from 0.13.x
291
326
 
292
327
  - **Removed:** the `panGuide` and `panoramaGuidance` props (the
@@ -35,8 +35,15 @@ Pod::Spec.new do |s|
35
35
 
36
36
  # Sources: iOS-specific Swift/Obj-C/Obj-C++ AND the shared C++ port
37
37
  # (cpp/) that both iOS and Android compile from a single source.
38
+ # cpp/ glob is NON-RECURSIVE on purpose: it picks up the shared C++
39
+ # port (all top-level cpp/*.cpp) but skips the maintainer-only
40
+ # GoogleTest harnesses under cpp/tests/ (which would otherwise fail
41
+ # the pod with `'gtest/gtest.h' file not found`). NOTE: using
42
+ # `cpp/**` + `s.exclude_files = ['cpp/tests/**/*']` instead broke the
43
+ # vendored opencv2.xcframework header integration for the remaining
44
+ # cpp/ files — keep this as a single non-recursive glob.
38
45
  s.source_files = ['ios/Sources/**/*.{swift,h,m,mm}',
39
- 'cpp/**/*.{h,hpp,cpp}']
46
+ 'cpp/*.{h,hpp,cpp}']
40
47
  # Restrict the umbrella header to ONLY the iOS-side Obj-C `.h`
41
48
  # files. Without this, CocoaPods defaults every header in
42
49
  # `source_files` (including the C++ `.hpp` files under cpp/) to
@@ -267,22 +267,6 @@ dependencies {
267
267
  android.sourceSets.main.java.exclude '**/CvFlowGateFrameProcessor.kt'
268
268
  }
269
269
 
270
- // v0.8.0 Phase 4b.ii — react-native-worklets-core's Android
271
- // prefab (`rnworklets`) is consumed by the native shim
272
- // (`stitcher_worklet_registry.cpp` constructs
273
- // `RNWorklet::WorkletInvoker`s). `implementation` not
274
- // `compileOnly` because we need the prefab's `.so` available at
275
- // both link time AND runtime — without the runtime presence,
276
- // `dlopen` would fail when our `libimage_stitcher.so` is loaded.
277
- //
278
- // Host apps that use this lib already declare worklets-core as
279
- // a peer dep (see package.json's peerDependencies); RN
280
- // autolinking + Gradle deduplicates, so the host doesn't get
281
- // a second copy.
282
- if (findProject(':react-native-worklets-core') != null) {
283
- implementation project(':react-native-worklets-core')
284
- }
285
-
286
270
  // v0.10.0 audit #11A — Android JUnit test scaffold. JVM unit
287
271
  // tests for pure-Kotlin data wrappers + algorithm helpers that
288
272
  // don't need an Android device. Run via
@@ -80,34 +80,6 @@ if(NOT EXISTS "${SHARED_CPP_DIR}/keyframe_gate.hpp")
80
80
  "Expected react-native-image-stitcher/cpp/ — was the package layout broken?")
81
81
  endif()
82
82
 
83
- # ── React Native prefab packages for JSI ──────────────────────────
84
- #
85
- # v0.8.0 Phase 3 — activating the previously-deferred JSI integration.
86
- # The shared C++ host object (cpp/stitcher_frame_jsi.cpp) depends on
87
- # `facebook::jsi`. ReactAndroid ships JSI as a prefab starting
88
- # RN 0.71+; the lib targets RN 0.84 so this is always available.
89
- #
90
- # `buildFeatures { prefab true }` in android/build.gradle enables
91
- # consumption + `ANDROID_STL=c++_shared` aligns the STL with what
92
- # the prefabs require. The Phase-2 STL probe (`llvm-nm
93
- # libopencv_stitching.a | grep '__ndk1'`) confirmed OpenCV's
94
- # stitching archive was already built with c++_shared (768
95
- # __ndk1 symbols + 0 __cxx11 / NSt3) — switching the lib's flag
96
- # from c++_static to c++_shared just aligns + matches. The
97
- # previous c++_static was working only because the JNI shim's
98
- # `.so` boundary used POD/C types; the new c++_shared is properly
99
- # matched throughout.
100
- find_package(ReactAndroid REQUIRED CONFIG)
101
- find_package(fbjni REQUIRED CONFIG)
102
-
103
- # v0.8.0 Phase 4b.ii — react-native-worklets-core prefab. The
104
- # Gradle module name is `react-native-worklets-core`; inside it
105
- # publishes a library named `rnworklets` (matches vc's consumption
106
- # pattern in node_modules/react-native-vision-camera/android/CMakeLists.txt).
107
- # We consume both the headers (for `WKTJsiWorklet.h` etc.) AND
108
- # the .so (for `RNWorklet::WorkletInvoker` + `JsiWrapper::unwrap`
109
- # symbols, which are defined in worklets-core's WKTJsiWrapper.cpp).
110
- find_package(react-native-worklets-core REQUIRED CONFIG)
111
83
 
112
84
  # ── Our shim ───────────────────────────────────────────────────────
113
85
  add_library(image_stitcher SHARED
@@ -119,30 +91,7 @@ add_library(image_stitcher SHARED
119
91
  # retry + dimension/memory instrumentation. Used to live in this
120
92
  # file (image_stitcher_jni.cpp). See cpp/stitcher.hpp for design
121
93
  # rationale.
122
- "${SHARED_CPP_DIR}/stitcher.cpp"
123
- # v0.8.0 Phase 3 — shared JSI host object for `StitcherFrame`.
124
- # Compiles to identical dispatch on both platforms; iOS consumes
125
- # it via the .mm shim at
126
- # `ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm`.
127
- # See cpp/stitcher_frame_jsi.hpp for the class API.
128
- "${SHARED_CPP_DIR}/stitcher_frame_jsi.cpp"
129
- # v0.8.0 Phase 4b.ii — shared C++ registry of host-supplied
130
- # worklets + the `globalThis.__stitcherProxy` host object that
131
- # JS calls into. iOS picked these up via the podspec glob in
132
- # Phase 4b.i; Android adds them here.
133
- "${SHARED_CPP_DIR}/stitcher_worklet_registry.cpp"
134
- "${SHARED_CPP_DIR}/stitcher_proxy_jsi.cpp"
135
- # v0.8.0 Phase 4b.iii — shared per-frame fan-out helper. Posts
136
- # a `StitcherFrameData` onto worklets-core's default context's
137
- # worklet thread; iterates the host worklet registry; invalidates
138
- # the JSI host object after dispatch completes.
139
- "${SHARED_CPP_DIR}/stitcher_worklet_dispatch.cpp"
140
- # v0.8.0 Phase 4b.ii — Android JNI bindings for the JSI install
141
- # (`StitcherJsiInstallerModule.nativeInstall`). Reaches into the
142
- # main JS runtime via the `long` JSI handle Kotlin pulls from
143
- # `ReactApplicationContext.getJavaScriptContextHolder()`. See
144
- # worklets-core's `WorkletsModule.java` for the canonical pattern.
145
- stitcher_jsi_install_jni.cpp)
94
+ "${SHARED_CPP_DIR}/stitcher.cpp")
146
95
 
147
96
  target_include_directories(image_stitcher PRIVATE
148
97
  "${OPENCV_INCLUDE_DIR}"
@@ -169,17 +118,7 @@ target_link_libraries(image_stitcher
169
118
  opencv_stitching
170
119
  -Wl,--no-whole-archive
171
120
  opencv_java
172
- log
173
- # v0.8.0 Phase 3 — JSI for the shared C++ host object
174
- # (cpp/stitcher_frame_jsi.cpp's `facebook::jsi::HostObject`
175
- # subclass). fbjni for the Phase 3c JNI bridge between Kotlin
176
- # worklet runtime + C++ host object construction.
177
- ReactAndroid::jsi
178
- fbjni::fbjni
179
- # v0.8.0 Phase 4b.ii — worklets-core's `RNWorklet::WorkletInvoker`
180
- # is constructed in the C++ registry's `install` method and
181
- # invoked from the Android per-frame dispatch path.
182
- react-native-worklets-core::rnworklets)
121
+ log)
183
122
 
184
123
  target_compile_options(image_stitcher PRIVATE
185
124
  -fvisibility=hidden
@@ -116,6 +116,20 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
116
116
  ? retailens::StitchMode::Panorama
117
117
  : retailens::StitchMode::Scans;
118
118
 
119
+ // 2026-06-07 — unify on the manual cv::detail pipeline. It won the
120
+ // on-device A/B: equals the high-level cv::Stitcher on quality after
121
+ // parity AND is strictly more robust — the cylindrical fallback, warp
122
+ // guard, and exposure comp all live only in the manual path, so the
123
+ // high-level path garbages wide/0.5x captures. Mirrors iOS'
124
+ // OpenCVStitcher.mm. See docs/stitch-pipeline-architecture.md §7.
125
+ cfg.useManualPipeline = true;
126
+ // Match iOS' parity resolution: the manual entry's default registration
127
+ // is 0.3 MP (vs the high-level's 0.6); bump to 0.6 unless the caller set
128
+ // an explicit value. (compositingResolMP already arrives as 1.0.)
129
+ if (cfg.registrationResolMP <= 0.0) {
130
+ cfg.registrationResolMP = 0.6;
131
+ }
132
+
119
133
  const std::string outPath = jstring_to_string(env, outputPath);
120
134
 
121
135
  retailens::StitchResult result = retailens::stitchFramePaths(
@@ -110,6 +110,19 @@ Java_io_imagestitcher_rn_KeyframeGate_nativeSetFlowMaxTranslationM(
110
110
  gate(handle)->setFlowMaxTranslationM(static_cast<double>(metres));
111
111
  }
112
112
 
113
+ // Wall-clock keyframe-interval budget (milliseconds). Force-accepts a
114
+ // frame once the elapsed time since the last accepted keyframe exceeds
115
+ // this value (applies to BOTH Pose and Flow strategies); 0 disables.
116
+ // Passed straight through — no unit conversion. See
117
+ // setMaxKeyframeIntervalMs doc in keyframe_gate.hpp. Android JNI
118
+ // counterpart of the iOS bridge method in KeyframeGateBridge.
119
+ JNIEXPORT void JNICALL
120
+ Java_io_imagestitcher_rn_KeyframeGate_nativeSetMaxKeyframeIntervalMs(
121
+ JNIEnv*, jclass, jlong handle, jdouble ms)
122
+ {
123
+ gate(handle)->setMaxKeyframeIntervalMs(static_cast<double>(ms));
124
+ }
125
+
113
126
  // 2026-05-14 — Android JNI for the percentile setter so JS Settings
114
127
  // can tune novelty aggregation on Android (was iOS-only until now).
115
128
  // See setFlowNoveltyPercentile doc in keyframe_gate.hpp.