react-native-image-stitcher 0.14.2 → 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 (116) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +35 -0
  3. package/RNImageStitcher.podspec +8 -7
  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/camera/Camera.d.ts +31 -16
  21. package/dist/camera/Camera.js +10 -2
  22. package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
  23. package/dist/camera/CaptureStitchStatsToast.js +27 -7
  24. package/dist/camera/PanoramaSettings.d.ts +10 -223
  25. package/dist/camera/PanoramaSettings.js +6 -28
  26. package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
  27. package/dist/camera/PanoramaSettingsBridge.js +3 -102
  28. package/dist/camera/PanoramaSettingsModal.js +7 -1
  29. package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
  30. package/dist/camera/buildPanoramaInitialSettings.js +4 -0
  31. package/dist/camera/cameraErrorMessages.d.ts +32 -0
  32. package/dist/camera/cameraErrorMessages.js +53 -0
  33. package/dist/camera/selectCaptureDevice.d.ts +5 -1
  34. package/dist/camera/selectCaptureDevice.js +22 -2
  35. package/dist/camera/useCapture.js +38 -0
  36. package/dist/index.d.ts +5 -8
  37. package/dist/index.js +11 -34
  38. package/dist/stitching/incremental.d.ts +1 -117
  39. package/dist/stitching/stitchVideo.d.ts +0 -35
  40. package/dist/types.d.ts +0 -87
  41. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
  42. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
  43. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
  44. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
  45. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
  46. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
  47. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
  48. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
  49. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
  50. package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
  51. package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
  52. package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
  53. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
  54. package/package.json +3 -2
  55. package/src/camera/Camera.tsx +43 -22
  56. package/src/camera/CaptureStitchStatsToast.tsx +58 -14
  57. package/src/camera/PanoramaSettings.ts +16 -289
  58. package/src/camera/PanoramaSettingsBridge.ts +3 -114
  59. package/src/camera/PanoramaSettingsModal.tsx +14 -1
  60. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
  61. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
  62. package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
  63. package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
  64. package/src/camera/buildPanoramaInitialSettings.ts +17 -0
  65. package/src/camera/cameraErrorMessages.ts +84 -0
  66. package/src/camera/selectCaptureDevice.ts +28 -3
  67. package/src/camera/useCapture.ts +44 -1
  68. package/src/index.ts +11 -40
  69. package/src/stitching/incremental.ts +3 -140
  70. package/src/stitching/stitchVideo.ts +0 -26
  71. package/src/types.ts +0 -95
  72. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
  73. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
  74. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
  75. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
  76. package/cpp/stitcher_frame_jsi.cpp +0 -214
  77. package/cpp/stitcher_frame_jsi.hpp +0 -108
  78. package/cpp/stitcher_proxy_jsi.cpp +0 -109
  79. package/cpp/stitcher_proxy_jsi.hpp +0 -46
  80. package/cpp/stitcher_worklet_dispatch.cpp +0 -103
  81. package/cpp/stitcher_worklet_dispatch.hpp +0 -71
  82. package/cpp/stitcher_worklet_registry.cpp +0 -91
  83. package/cpp/stitcher_worklet_registry.hpp +0 -146
  84. package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
  85. package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
  86. package/dist/stitching/IncrementalStitcherView.js +0 -157
  87. package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
  88. package/dist/stitching/StitcherWorkletRegistry.js +0 -78
  89. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
  90. package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
  91. package/dist/stitching/useFrameProcessor.d.ts +0 -119
  92. package/dist/stitching/useFrameProcessor.js +0 -196
  93. package/dist/stitching/useFrameStream.d.ts +0 -34
  94. package/dist/stitching/useFrameStream.js +0 -234
  95. package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
  96. package/dist/stitching/useThrottledFrameProcessor.js +0 -132
  97. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
  98. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
  99. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
  100. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
  101. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
  102. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
  103. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
  104. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  105. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
  106. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
  107. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
  108. package/src/stitching/IncrementalStitcherView.tsx +0 -198
  109. package/src/stitching/StitcherWorkletRegistry.ts +0 -156
  110. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
  111. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
  112. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
  113. package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
  114. package/src/stitching/useFrameProcessor.ts +0 -226
  115. package/src/stitching/useFrameStream.ts +0 -271
  116. package/src/stitching/useThrottledFrameProcessor.ts +0 -145
package/CHANGELOG.md CHANGED
@@ -16,6 +16,137 @@ 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
+
19
150
  ## [0.14.2] — 2026-06-03
20
151
 
21
152
  ### Fixed — AR preview blank on first entry (intermittent camera-handoff race)
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,14 +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}']
40
- # Exclude the lib's own C++ unit tests — they #include <gtest/gtest.h>,
41
- # which consumer apps don't vendor. The `cpp/**/*.cpp` glob above
42
- # otherwise slurps cpp/tests/*.cpp into every host pod build, failing
43
- # with `'gtest/gtest.h' file not found`. Tests build only in the lib's
44
- # CI / example app, never in a consumer.
45
- s.exclude_files = ['cpp/tests/**/*']
46
+ 'cpp/*.{h,hpp,cpp}']
46
47
  # Restrict the umbrella header to ONLY the iOS-side Obj-C `.h`
47
48
  # files. Without this, CocoaPods defaults every header in
48
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.