react-native-image-stitcher 0.7.0 → 0.8.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.
- package/CHANGELOG.md +180 -1
- package/android/build.gradle +35 -1
- package/android/src/main/cpp/CMakeLists.txt +64 -2
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +30 -11
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +4 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
- package/cpp/stitcher_frame_data.hpp +141 -0
- package/cpp/stitcher_frame_jsi.cpp +214 -0
- package/cpp/stitcher_frame_jsi.hpp +108 -0
- package/cpp/stitcher_proxy_jsi.cpp +109 -0
- package/cpp/stitcher_proxy_jsi.hpp +46 -0
- package/cpp/stitcher_worklet_dispatch.cpp +103 -0
- package/cpp/stitcher_worklet_dispatch.hpp +71 -0
- package/cpp/stitcher_worklet_registry.cpp +81 -0
- package/cpp/stitcher_worklet_registry.hpp +136 -0
- package/dist/camera/Camera.d.ts +62 -12
- package/dist/camera/Camera.js +30 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -1
- package/dist/stitching/StitcherFrame.d.ts +170 -0
- package/dist/stitching/StitcherFrame.js +4 -0
- package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
- package/dist/stitching/StitcherWorkletRegistry.js +78 -0
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/dist/stitching/useFrameProcessor.d.ts +119 -0
- package/dist/stitching/useFrameProcessor.js +196 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -10
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
- package/package.json +1 -1
- package/src/camera/Camera.tsx +93 -28
- package/src/index.ts +16 -0
- package/src/stitching/StitcherFrame.ts +197 -0
- package/src/stitching/StitcherWorkletRegistry.ts +156 -0
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- package/src/stitching/useFrameProcessor.ts +226 -0
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,184 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.8.0] — 2026-05-27
|
|
20
|
+
|
|
21
|
+
### Added — `useFrameProcessor` hook for host worklets
|
|
22
|
+
|
|
23
|
+
Hosts can now attach a `'worklet'`-prefixed function that fires on
|
|
24
|
+
every AR (and non-AR) capture frame, alongside the lib's own
|
|
25
|
+
first-party stitching. Use case: real-time OCR, packet detection,
|
|
26
|
+
ML inference, custom telemetry — anything that wants per-frame
|
|
27
|
+
pixel access in a worklet runtime.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { useFrameProcessor, type StitcherFrame }
|
|
31
|
+
from 'react-native-image-stitcher';
|
|
32
|
+
|
|
33
|
+
const fp = useFrameProcessor((frame: StitcherFrame) => {
|
|
34
|
+
'worklet';
|
|
35
|
+
// frame.toArrayBuffer(), frame.pose, frame.source ('ar' | 'vc'), …
|
|
36
|
+
}, []);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**AR mode** (iPhone via ARKit, Android via ARCore): worklets fire
|
|
40
|
+
on every AR frame at the device's native rate (~30 Hz on A35,
|
|
41
|
+
~60 Hz on iPhone 16 Pro). Auto-registered into a process-scope
|
|
42
|
+
native registry via `globalThis.__stitcherProxy.install(workletFn)`.
|
|
43
|
+
The AR-session dispatch path fans out to both the lib's first-party
|
|
44
|
+
stitching AND every registered host worklet, with **per-worklet
|
|
45
|
+
failure isolation** (one host worklet throwing does NOT break
|
|
46
|
+
others or the lib's stitching).
|
|
47
|
+
|
|
48
|
+
**Non-AR mode** (vision-camera): pass the hook's return through
|
|
49
|
+
`<Camera frameProcessor={fp}>` to enable. Honest tradeoff: vc's
|
|
50
|
+
`<Camera>` accepts ONE processor, so supplying a host processor
|
|
51
|
+
displaces the lib's first-party stitching in non-AR mode. Hosts
|
|
52
|
+
that want both running concurrently should use AR mode (which
|
|
53
|
+
natively composes both). Composition for non-AR is tracked as
|
|
54
|
+
v0.9+.
|
|
55
|
+
|
|
56
|
+
### Added — `StitcherFrame` contract
|
|
57
|
+
|
|
58
|
+
Unified frame shape across AR and non-AR modes (`src/stitching/
|
|
59
|
+
StitcherFrame.ts`):
|
|
60
|
+
|
|
61
|
+
- `width` / `height` / `pixelFormat` / `orientation` / `timestamp`
|
|
62
|
+
/ `toArrayBuffer()` — vc-shape parity
|
|
63
|
+
- `pose: { rotation: [x,y,z,w], translation?: [x,y,z] }` — always
|
|
64
|
+
present in AR mode; rotation-only in non-AR
|
|
65
|
+
- `source: 'ar' | 'vc'` discriminator for safe AR-field access
|
|
66
|
+
- `arDepth?`, `arAnchors?`, `arTrackingState?` — populated in AR
|
|
67
|
+
mode on supported devices
|
|
68
|
+
|
|
69
|
+
### Added — JSI proxy host object
|
|
70
|
+
|
|
71
|
+
`globalThis.__stitcherProxy` installed on lib bootstrap (iOS:
|
|
72
|
+
`StitcherJsiInstaller` RN module via `RCTBridgeProxy.runtime` in
|
|
73
|
+
bridgeless mode; Android: `StitcherJsiInstallerModule` via
|
|
74
|
+
`ReactApplicationContext.getJavaScriptContextHolder()`). Exposes
|
|
75
|
+
`install` / `uninstall` / `count` host functions backed by a
|
|
76
|
+
shared C++ `retailens::StitcherWorkletRegistry` (process-scope,
|
|
77
|
+
mutex-serialised, snapshot-isolated).
|
|
78
|
+
|
|
79
|
+
### Changed — AR-mode dispatch architecture
|
|
80
|
+
|
|
81
|
+
Internal-only refactor (strict additive BC for hosts that don't
|
|
82
|
+
use `useFrameProcessor`):
|
|
83
|
+
|
|
84
|
+
- **iOS**: `ARSessionDelegate.session(_:didUpdate:)` now routes
|
|
85
|
+
through `RNSARWorkletRuntime.dispatchFrame:pose:` instead of
|
|
86
|
+
directly invoking the engine. First-party callback (Phase 3c)
|
|
87
|
+
runs synchronously on the caller thread (preserves ARKit's
|
|
88
|
+
pool-reuse contract); host worklet fan-out (Phase 4b.i)
|
|
89
|
+
dispatches asynchronously onto a dedicated worklets-core
|
|
90
|
+
context.
|
|
91
|
+
|
|
92
|
+
- **Android**: `RNSARCameraView.onDrawFrame` now wraps the
|
|
93
|
+
existing `module.ingestFromARCameraView(...)` call in
|
|
94
|
+
`StitcherWorkletRuntime.runFirstParty { ... }` (Phase 3c) and
|
|
95
|
+
follows with `StitcherWorkletRuntime.dispatchToHostWorklets(...)`
|
|
96
|
+
(Phase 4b.iii). Per-frame fan-out runs every AR frame when host
|
|
97
|
+
worklets are registered (not just during capture).
|
|
98
|
+
|
|
99
|
+
### Performance posture
|
|
100
|
+
|
|
101
|
+
- **First-party-only deployments** (no `useFrameProcessor`):
|
|
102
|
+
zero per-frame cost added. `hasHostWorklets()` atomic-read
|
|
103
|
+
short-circuits before any dispatch path.
|
|
104
|
+
- **Host worklets registered, idle preview**: Android pays
|
|
105
|
+
~6-10ms per AR frame (NV21 pack + JNI byte copy + worklet
|
|
106
|
+
dispatch). iOS uses `CFBridgingRetain` (no per-frame copy,
|
|
107
|
+
but ARKit pool back-pressure on next frame). Both acceptable
|
|
108
|
+
for v0.8.0; future optimization → zero-copy NV21 transfer via
|
|
109
|
+
direct `ByteBuffer` (Android).
|
|
110
|
+
|
|
111
|
+
### Added — SSIM parity gate harness
|
|
112
|
+
|
|
113
|
+
`scripts/ssim-compare.py` — pixel-wise SSIM comparison between
|
|
114
|
+
panorama JPEGs (Pillow + numpy + scikit-image; threshold 0.98).
|
|
115
|
+
Procedure in `docs/phase-7-parity-gate.md`.
|
|
116
|
+
|
|
117
|
+
> **v0.8.0 release note:** the formal SSIM parity gate was NOT
|
|
118
|
+
> run for this release. Verification rests on manual visual
|
|
119
|
+
> inspection of v0.8.0 panorama output on iPhone 16 Pro (Phase
|
|
120
|
+
> 4b.i) and Galaxy A35 (Phase 4b.iii) — both produced stitched
|
|
121
|
+
> panoramas matching the v0.7.x behaviour subjectively. The
|
|
122
|
+
> harness is in place for v0.8.1+ / future releases where the
|
|
123
|
+
> gate is mandatory.
|
|
124
|
+
|
|
125
|
+
### Migration guide
|
|
126
|
+
|
|
127
|
+
No host-side changes required for the common case. Hosts that
|
|
128
|
+
want to attach worklets:
|
|
129
|
+
|
|
130
|
+
1. Add `react-native-worklets-core` if not already a peer dep
|
|
131
|
+
(already in v0.7.x's peer-deps list).
|
|
132
|
+
2. Replace `useFrameProcessor` imports from
|
|
133
|
+
`react-native-vision-camera` with the lib's own export:
|
|
134
|
+
```diff
|
|
135
|
+
- import { useFrameProcessor } from 'react-native-vision-camera';
|
|
136
|
+
+ import { useFrameProcessor } from 'react-native-image-stitcher';
|
|
137
|
+
```
|
|
138
|
+
3. Worklet body now receives `StitcherFrame` instead of vc's
|
|
139
|
+
`Frame` — see `src/stitching/StitcherFrame.ts` for the contract.
|
|
140
|
+
|
|
141
|
+
## [0.7.1] — 2026-05-26
|
|
142
|
+
|
|
143
|
+
### Fixed — CI binary-packaging bloat
|
|
144
|
+
|
|
145
|
+
The v0.7.0 release (and likely v0.5.1 before it — both built by
|
|
146
|
+
CI) shipped uncompressed binary archives that consumers downloaded
|
|
147
|
+
on every `npm install`. Sizes vs. the manual recipe used for
|
|
148
|
+
v0.6.0:
|
|
149
|
+
|
|
150
|
+
| Platform | v0.7.0 (CI, unstripped) | v0.7.1 (CI, stripped) | Saving |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| iOS zip | 43 MB | ~26 MB | -17 MB |
|
|
153
|
+
| Android zip | 165 MB | ~42 MB | -123 MB |
|
|
154
|
+
|
|
155
|
+
The lib itself is unchanged; consumers on the `^0.7.0` semver range
|
|
156
|
+
automatically pick up v0.7.1 and start getting the smaller download.
|
|
157
|
+
No source-code changes; binary-only re-release.
|
|
158
|
+
|
|
159
|
+
#### Root cause
|
|
160
|
+
|
|
161
|
+
- **iOS**: `scripts/build-opencv-ios.sh` produced an xcframework
|
|
162
|
+
containing both the device slice (`ios-arm64`) and the simulator
|
|
163
|
+
slice (`ios-arm64_x86_64-simulator`). vision-camera + ARKit
|
|
164
|
+
don't work on the simulator and the example app targets devices
|
|
165
|
+
only, so the simulator slice was dead weight in every download.
|
|
166
|
+
- **Android**: `scripts/build-opencv-android.sh` ran OpenCV's
|
|
167
|
+
`build_sdk.py` for all four NDK ABIs (per the script's own
|
|
168
|
+
contract — produces a multi-arch fat SDK). The lib's
|
|
169
|
+
`android/build.gradle` sets `ndk.abiFilters arm64-v8a` so only
|
|
170
|
+
arm64-v8a binaries reach any consumer APK, but the zip carried
|
|
171
|
+
`armeabi-v7a` / `x86` / `x86_64` libs in three sibling dirs
|
|
172
|
+
(`sdk/native/libs/`, `staticlibs/`, `3rdparty/libs/`) plus
|
|
173
|
+
`samples/` (~10 MB) and `apk/` (~5 MB) — none of it ever loaded
|
|
174
|
+
at runtime.
|
|
175
|
+
|
|
176
|
+
#### Fix
|
|
177
|
+
|
|
178
|
+
Both build scripts now strip the dead-weight pieces immediately
|
|
179
|
+
after the OpenCV build completes, before zipping for upload.
|
|
180
|
+
Sentinel checks fail loudly if a strip removes the required
|
|
181
|
+
arm64-v8a artifacts (defends against a future refactor of the
|
|
182
|
+
strip block). Pattern matches the manual recipe in
|
|
183
|
+
`feedback_binary_release_packaging.md` (project memory).
|
|
184
|
+
|
|
185
|
+
The iOS strip auto-detects the simulator entry's index in the
|
|
186
|
+
xcframework's `Info.plist::AvailableLibraries` via a
|
|
187
|
+
`plutil -convert json | python3` one-liner — the index isn't fixed
|
|
188
|
+
across OpenCV builds and previous manual recipes that hardcoded
|
|
189
|
+
`AvailableLibraries.1` would have silently stripped the wrong
|
|
190
|
+
slice if the order changed.
|
|
191
|
+
|
|
192
|
+
#### Compatibility
|
|
193
|
+
|
|
194
|
+
Strict additive over v0.7.0. No code changes — the lib's runtime
|
|
195
|
+
and public API surface are byte-identical.
|
|
196
|
+
|
|
19
197
|
## [0.7.0] — 2026-05-26
|
|
20
198
|
|
|
21
199
|
### Added — Tier 1: `useKeyframeStream`
|
|
@@ -1248,7 +1426,8 @@ Native module names also changed:
|
|
|
1248
1426
|
- iOS pod: `RetaiLensCaptureSDK` → `RNImageStitcher`
|
|
1249
1427
|
- iOS xcframework: shipped as `opencv2.xcframework` (linked from `RNImageStitcher.podspec`)
|
|
1250
1428
|
|
|
1251
|
-
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.7.
|
|
1429
|
+
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.7.1...HEAD
|
|
1430
|
+
[0.7.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.7.0...v0.7.1
|
|
1252
1431
|
[0.7.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.6.0...v0.7.0
|
|
1253
1432
|
[0.6.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.5.1...v0.6.0
|
|
1254
1433
|
[0.5.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.5.0...v0.5.1
|
package/android/build.gradle
CHANGED
|
@@ -88,12 +88,30 @@ android {
|
|
|
88
88
|
externalNativeBuild {
|
|
89
89
|
cmake {
|
|
90
90
|
arguments "-DOPENCV_ANDROID_SDK=${file("$projectDir/vendor/OpenCV-android-sdk").absolutePath}",
|
|
91
|
-
|
|
91
|
+
// v0.8.0 Phase 3 — switched from c++_static
|
|
92
|
+
// to c++_shared. Required for linking
|
|
93
|
+
// ReactAndroid::jsi (RN's prefab uses
|
|
94
|
+
// shared libc++). STL probe at
|
|
95
|
+
// android/src/main/cpp/CMakeLists.txt:99-118
|
|
96
|
+
// confirms OpenCV's libopencv_stitching.a is
|
|
97
|
+
// already built with __ndk1 (c++_shared), so
|
|
98
|
+
// the static archive link continues to
|
|
99
|
+
// work cleanly. Pre-Phase-3 it worked only
|
|
100
|
+
// because the JNI shim's .so boundary used
|
|
101
|
+
// POD types — fragile. Now properly aligned.
|
|
102
|
+
"-DANDROID_STL=c++_shared"
|
|
92
103
|
cppFlags "-std=c++17"
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
|
|
108
|
+
// v0.8.0 Phase 3 — consume React Native's prefab packages
|
|
109
|
+
// (ReactAndroid::jsi + fbjni::fbjni) for the JSI host object.
|
|
110
|
+
// RN 0.71+ ships these as prefabs; this lib targets RN 0.84.
|
|
111
|
+
buildFeatures {
|
|
112
|
+
prefab true
|
|
113
|
+
}
|
|
114
|
+
|
|
97
115
|
// ── JNI shim build path ─────────────────────────────────────────
|
|
98
116
|
// Gradle compiles cpp/image_stitcher_jni.cpp into
|
|
99
117
|
// libimage_stitcher.so for the ABIs filtered above. The shim
|
|
@@ -248,6 +266,22 @@ dependencies {
|
|
|
248
266
|
// still builds for non-camera consumers.
|
|
249
267
|
android.sourceSets.main.java.exclude '**/CvFlowGateFrameProcessor.kt'
|
|
250
268
|
}
|
|
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
|
+
}
|
|
251
285
|
}
|
|
252
286
|
|
|
253
287
|
// Helper from the React Native gradle convention to read host-app
|
|
@@ -80,6 +80,35 @@ 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
|
# ── Our shim ───────────────────────────────────────────────────────
|
|
84
113
|
add_library(image_stitcher SHARED
|
|
85
114
|
image_stitcher_jni.cpp
|
|
@@ -90,7 +119,30 @@ add_library(image_stitcher SHARED
|
|
|
90
119
|
# retry + dimension/memory instrumentation. Used to live in this
|
|
91
120
|
# file (image_stitcher_jni.cpp). See cpp/stitcher.hpp for design
|
|
92
121
|
# rationale.
|
|
93
|
-
"${SHARED_CPP_DIR}/stitcher.cpp"
|
|
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
146
|
|
|
95
147
|
target_include_directories(image_stitcher PRIVATE
|
|
96
148
|
"${OPENCV_INCLUDE_DIR}"
|
|
@@ -117,7 +169,17 @@ target_link_libraries(image_stitcher
|
|
|
117
169
|
opencv_stitching
|
|
118
170
|
-Wl,--no-whole-archive
|
|
119
171
|
opencv_java
|
|
120
|
-
log
|
|
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
183
|
|
|
122
184
|
target_compile_options(image_stitcher PRIVATE
|
|
123
185
|
-fvisibility=hidden
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// stitcher_jsi_install_jni.cpp — JNI binding for the Android-side
|
|
4
|
+
// JSI install (v0.8.0 Phase 4b.ii).
|
|
5
|
+
//
|
|
6
|
+
// Kotlin's `StitcherJsiInstallerModule.nativeInstall(jsiRuntimeRef)`
|
|
7
|
+
// calls into this file. We unbox the `jsi::Runtime*` from the
|
|
8
|
+
// Java `long` and hand it to the shared
|
|
9
|
+
// `retailens::installStitcherProxy(runtime)` function which sets
|
|
10
|
+
// `globalThis.__stitcherProxy`. Same destination as iOS — the
|
|
11
|
+
// host object class lives in `cpp/stitcher_proxy_jsi.{hpp,cpp}`.
|
|
12
|
+
//
|
|
13
|
+
// ## Why a `long` ref, not a JSI handle wrapper class
|
|
14
|
+
//
|
|
15
|
+
// `ReactApplicationContext.getJavaScriptContextHolder()` returns a
|
|
16
|
+
// `JavaScriptContextHolder` whose `.get()` returns a Java `long`
|
|
17
|
+
// that's the raw pointer to the C++ `jsi::Runtime*`. Same
|
|
18
|
+
// contract as worklets-core's `WorkletsModule.nativeInstall`
|
|
19
|
+
// (verified at the same call site). Caller is responsible for
|
|
20
|
+
// ensuring the runtime outlives this call — in practice, the
|
|
21
|
+
// runtime IS the JS thread's runtime which lives the whole
|
|
22
|
+
// process lifetime, so this is structurally always safe in our
|
|
23
|
+
// usage.
|
|
24
|
+
//
|
|
25
|
+
// ## Threading
|
|
26
|
+
//
|
|
27
|
+
// Kotlin invokes this from a `@ReactMethod(isBlockingSynchronousMethod
|
|
28
|
+
// = true)` so we're already on the JS thread. Synchronous JSI
|
|
29
|
+
// access is safe.
|
|
30
|
+
|
|
31
|
+
#include "stitcher_proxy_jsi.hpp"
|
|
32
|
+
|
|
33
|
+
// v0.8.0 Phase 4b.iii — per-frame fan-out support. The shared
|
|
34
|
+
// `dispatchToHostWorklets` posts to worklets-core's default context;
|
|
35
|
+
// this JNI file's `nativeDispatchToHostWorklets` constructs the
|
|
36
|
+
// `StitcherFrameData` from raw bytes + pose + dims and forwards it.
|
|
37
|
+
#include "stitcher_frame_data.hpp"
|
|
38
|
+
#include "stitcher_worklet_dispatch.hpp"
|
|
39
|
+
#include "stitcher_worklet_registry.hpp"
|
|
40
|
+
|
|
41
|
+
#include <react-native-worklets-core/WKTJsiWorkletContext.h>
|
|
42
|
+
|
|
43
|
+
#include <jni.h>
|
|
44
|
+
#include <jsi/jsi.h>
|
|
45
|
+
|
|
46
|
+
#include <android/log.h>
|
|
47
|
+
|
|
48
|
+
#include <cstdint>
|
|
49
|
+
#include <cstring>
|
|
50
|
+
#include <memory>
|
|
51
|
+
#include <utility>
|
|
52
|
+
#include <vector>
|
|
53
|
+
|
|
54
|
+
#define LOG_TAG "StitcherJsiInstaller"
|
|
55
|
+
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
|
56
|
+
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
|
57
|
+
|
|
58
|
+
extern "C" JNIEXPORT jboolean JNICALL
|
|
59
|
+
Java_io_imagestitcher_rn_StitcherJsiInstallerModule_nativeInstall(
|
|
60
|
+
JNIEnv* /*env*/, jobject /*thiz*/, jlong jsiRuntimeRef) {
|
|
61
|
+
if (jsiRuntimeRef == 0) {
|
|
62
|
+
// ReactApplicationContext.getJavaScriptContextHolder().get()
|
|
63
|
+
// returns 0 when the runtime isn't ready (rare — JS would have
|
|
64
|
+
// had to call us before its own runtime was up; impossible in
|
|
65
|
+
// practice). Defensive.
|
|
66
|
+
return JNI_FALSE;
|
|
67
|
+
}
|
|
68
|
+
auto* runtime = reinterpret_cast<facebook::jsi::Runtime*>(jsiRuntimeRef);
|
|
69
|
+
retailens::installStitcherProxy(*runtime);
|
|
70
|
+
LOGI("installed globalThis.__stitcherProxy on main JS runtime.");
|
|
71
|
+
return JNI_TRUE;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── v0.8.0 Phase 4b.iii — Android NV21 PixelBufferReader ──────────
|
|
75
|
+
//
|
|
76
|
+
// Owns a heap-allocated `std::vector<uint8_t>` of pre-copied NV21
|
|
77
|
+
// bytes. Constructed by `nativeDispatchToHostWorklets` after one
|
|
78
|
+
// JNI byte-array copy from Kotlin; outlives the AR render thread
|
|
79
|
+
// scope via `StitcherFrameData::pixelReader`'s `shared_ptr` —
|
|
80
|
+
// dropped when the host object is invalidated.
|
|
81
|
+
|
|
82
|
+
namespace {
|
|
83
|
+
|
|
84
|
+
class AndroidNV21BufferReader : public retailens::PixelBufferReader {
|
|
85
|
+
public:
|
|
86
|
+
explicit AndroidNV21BufferReader(std::vector<uint8_t>&& bytes)
|
|
87
|
+
: _bytes(std::move(bytes)) {}
|
|
88
|
+
|
|
89
|
+
std::size_t byteSize() const override { return _bytes.size(); }
|
|
90
|
+
|
|
91
|
+
std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) override {
|
|
92
|
+
if (dst == nullptr) return 0;
|
|
93
|
+
std::size_t n = std::min(maxBytes, _bytes.size());
|
|
94
|
+
if (n > 0) {
|
|
95
|
+
std::memcpy(dst, _bytes.data(), n);
|
|
96
|
+
}
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private:
|
|
101
|
+
std::vector<uint8_t> _bytes;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
} // namespace
|
|
105
|
+
|
|
106
|
+
// ─── v0.8.0 Phase 4b.iii — registry count accessor ─────────────────
|
|
107
|
+
//
|
|
108
|
+
// Cheap (microsecond) accessor for the per-frame gate in
|
|
109
|
+
// `RNSARCameraView.onDrawFrame`. Avoids the NV21 byte-pack cost
|
|
110
|
+
// when no host worklets are registered AND no capture is active.
|
|
111
|
+
// Same atomic-read the JSI host object's `count()` host function
|
|
112
|
+
// goes through.
|
|
113
|
+
extern "C" JNIEXPORT jint JNICALL
|
|
114
|
+
Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeRegistryCount(
|
|
115
|
+
JNIEnv* /*env*/, jobject /*thiz*/) {
|
|
116
|
+
return static_cast<jint>(
|
|
117
|
+
retailens::StitcherWorkletRegistry::shared().count());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── v0.8.0 Phase 4b.iii — per-frame dispatch JNI binding ──────────
|
|
121
|
+
//
|
|
122
|
+
// Called from Kotlin's `StitcherWorkletRuntime.dispatchToHostWorklets`
|
|
123
|
+
// after the first-party stitching block has returned (the AR-frame
|
|
124
|
+
// data is still in scope on the Kotlin side because
|
|
125
|
+
// `RNSARCameraView.onDrawFrame` reads the ARCore Frame, builds the
|
|
126
|
+
// NV21 byte[], invokes first-party via `runFirstParty { ... }`,
|
|
127
|
+
// THEN calls into here).
|
|
128
|
+
//
|
|
129
|
+
// The byte[] is COPIED into our owned vector — ARCore's pixel data
|
|
130
|
+
// becomes inaccessible shortly after `onDrawFrame` returns, and our
|
|
131
|
+
// async dispatch must outlive that scope. Cost: one ~3MB memcpy
|
|
132
|
+
// per frame at 1080p NV21 (~90 MB/s at 30 fps; <5 ms on a mid-range
|
|
133
|
+
// Android device). Fast-path early-exit when the registry is empty
|
|
134
|
+
// skips the copy entirely.
|
|
135
|
+
//
|
|
136
|
+
// trackingState: Kotlin passes one of "" / "notAvailable" / "limited"
|
|
137
|
+
// / "normal" (empty string = field unset → JS sees undefined).
|
|
138
|
+
extern "C" JNIEXPORT void JNICALL
|
|
139
|
+
Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeDispatchToHostWorklets(
|
|
140
|
+
JNIEnv* env, jobject /*thiz*/,
|
|
141
|
+
jbyteArray nv21Bytes,
|
|
142
|
+
jint width, jint height,
|
|
143
|
+
jdouble qx, jdouble qy, jdouble qz, jdouble qw,
|
|
144
|
+
jdouble tx, jdouble ty, jdouble tz,
|
|
145
|
+
jdouble timestampNs,
|
|
146
|
+
jstring trackingState) {
|
|
147
|
+
// Fast-path early-exit BEFORE the JNI byte-array copy. Saves the
|
|
148
|
+
// ~3MB memcpy + JSI host object alloc on every frame in the
|
|
149
|
+
// common first-party-only case.
|
|
150
|
+
if (retailens::StitcherWorkletRegistry::shared().count() == 0) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (nv21Bytes == nullptr) {
|
|
155
|
+
LOGE("nativeDispatchToHostWorklets: nv21Bytes is null");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const jsize byteLen = env->GetArrayLength(nv21Bytes);
|
|
160
|
+
if (byteLen <= 0) {
|
|
161
|
+
LOGE("nativeDispatchToHostWorklets: nv21Bytes is empty");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Copy into our owned vector. `GetByteArrayRegion` is the
|
|
166
|
+
// canonical "copy" path — `GetByteArrayElements + Release` MAY
|
|
167
|
+
// pin the JVM array (zero-copy) but the contract isn't
|
|
168
|
+
// guaranteed; we need our own buffer for the async dispatch
|
|
169
|
+
// anyway, so the explicit copy is cleaner.
|
|
170
|
+
std::vector<uint8_t> bytes(static_cast<std::size_t>(byteLen));
|
|
171
|
+
env->GetByteArrayRegion(
|
|
172
|
+
nv21Bytes, 0, byteLen,
|
|
173
|
+
reinterpret_cast<jbyte*>(bytes.data()));
|
|
174
|
+
|
|
175
|
+
// Extract trackingState string (may be null on the Kotlin side
|
|
176
|
+
// for non-AR or pre-tracking frames — guard accordingly).
|
|
177
|
+
std::string trackingStateStr;
|
|
178
|
+
if (trackingState != nullptr) {
|
|
179
|
+
const char* cs = env->GetStringUTFChars(trackingState, nullptr);
|
|
180
|
+
if (cs != nullptr) {
|
|
181
|
+
trackingStateStr = cs;
|
|
182
|
+
env->ReleaseStringUTFChars(trackingState, cs);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Build StitcherFrameData. Field semantics match the iOS
|
|
187
|
+
// `StitcherFrameHostObject::fromARFrame:pose:` factory; this is
|
|
188
|
+
// the Android equivalent path.
|
|
189
|
+
retailens::StitcherFrameData data;
|
|
190
|
+
data.source = "ar";
|
|
191
|
+
data.width = static_cast<int32_t>(width);
|
|
192
|
+
data.height = static_cast<int32_t>(height);
|
|
193
|
+
// ARCore's camera image is YUV_420_888 on Android, mapped to NV21
|
|
194
|
+
// by the existing `YuvImageConverter.packNV21` path — the byte[]
|
|
195
|
+
// we receive is interleaved Y then VU. Worklets gate on this
|
|
196
|
+
// string identifier (`'yuv'` vs `'unknown'`); v0.8.0 always
|
|
197
|
+
// emits `'yuv'` for AR mode on Android (NV21).
|
|
198
|
+
data.pixelFormat = "yuv";
|
|
199
|
+
// Android AR-mode camera image is always landscape-natural; the
|
|
200
|
+
// mapping matches iOS' coarse two-value set. Hosts that need
|
|
201
|
+
// exact display orientation read it from the device-orientation
|
|
202
|
+
// sensors (see `useDeviceOrientation` hook).
|
|
203
|
+
data.orientation = (width >= height) ? "landscape-right" : "portrait";
|
|
204
|
+
data.timestampNs = timestampNs;
|
|
205
|
+
data.qx = qx;
|
|
206
|
+
data.qy = qy;
|
|
207
|
+
data.qz = qz;
|
|
208
|
+
data.qw = qw;
|
|
209
|
+
data.tx = tx;
|
|
210
|
+
data.ty = ty;
|
|
211
|
+
data.tz = tz;
|
|
212
|
+
data.hasTranslation = true; // AR mode always has translation
|
|
213
|
+
data.arTrackingState = trackingStateStr;
|
|
214
|
+
data.pixelReader =
|
|
215
|
+
std::make_shared<AndroidNV21BufferReader>(std::move(bytes));
|
|
216
|
+
|
|
217
|
+
// Dispatch on worklets-core's default context. That context is
|
|
218
|
+
// initialised by JS' `Worklets.install()` (which runs at lib
|
|
219
|
+
// bootstrap when worklets-core's module is imported); by the
|
|
220
|
+
// time host worklets are registered, the default context is up.
|
|
221
|
+
// The shared dispatch helper handles the registry snapshot,
|
|
222
|
+
// host-object construction (inside the worklet thread), per-
|
|
223
|
+
// worklet failure isolation, and invalidation.
|
|
224
|
+
retailens::dispatchToHostWorklets(
|
|
225
|
+
RNWorklet::JsiWorkletContext::getDefaultInstance(),
|
|
226
|
+
std::move(data));
|
|
227
|
+
}
|
|
@@ -1001,14 +1001,17 @@ class IncrementalStitcher(
|
|
|
1001
1001
|
// per accepted frame on a mid-tier device. Pass null to use
|
|
1002
1002
|
// the legacy JPEG path.
|
|
1003
1003
|
//
|
|
1004
|
-
// OWNERSHIP:
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1004
|
+
// OWNERSHIP: wrapped in `TransferredNV21` (audit #4A,
|
|
1005
|
+
// v0.10.0). The wrapper enforces single-use: the engine
|
|
1006
|
+
// calls `.takeOnce()` on the producer thread before
|
|
1007
|
+
// dispatching to `workScope`; subsequent attempts to extract
|
|
1008
|
+
// the bytes throw. Callers MUST construct a fresh
|
|
1009
|
+
// `TransferredNV21` per frame and MUST NOT hand the same
|
|
1010
|
+
// instance to two consumers (e.g., a sync gate-eval + an
|
|
1011
|
+
// async workScope.launch). The Frame Processor plugin and
|
|
1012
|
+
// the AR camera view both allocate fresh NV21 arrays per
|
|
1013
|
+
// frame; the wrapper is a defensive-programming guard.
|
|
1014
|
+
nv21PixelData: TransferredNV21? = null,
|
|
1012
1015
|
nv21PixelWidth: Int = 0,
|
|
1013
1016
|
nv21PixelHeight: Int = 0,
|
|
1014
1017
|
) {
|
|
@@ -1215,11 +1218,20 @@ class IncrementalStitcher(
|
|
|
1215
1218
|
)
|
|
1216
1219
|
return
|
|
1217
1220
|
}
|
|
1221
|
+
// v0.10.0 audit #4A — extract the wrapped bytes ONCE on the
|
|
1222
|
+
// producer thread before dispatching to workScope. This
|
|
1223
|
+
// makes the transfer-of-ownership explicit + caught early:
|
|
1224
|
+
// if a caller accidentally passes the same TransferredNV21
|
|
1225
|
+
// instance to a sync consumer earlier, takeOnce() would
|
|
1226
|
+
// have already thrown there. Capturing `pixelBytes` by
|
|
1227
|
+
// value inside the coroutine sidesteps any chance of the
|
|
1228
|
+
// wrapper being read from two threads.
|
|
1229
|
+
val pixelBytes: ByteArray? = if (hasPixelData) nv21PixelData!!.takeOnce() else null
|
|
1218
1230
|
workScope.launch {
|
|
1219
1231
|
val state: WritableMap? = if (firstwins != null) {
|
|
1220
1232
|
val tele = if (hasPixelData) {
|
|
1221
1233
|
firstwins.addFramePixelData(
|
|
1222
|
-
nv21 =
|
|
1234
|
+
nv21 = pixelBytes!!,
|
|
1223
1235
|
nv21Width = nv21PixelWidth,
|
|
1224
1236
|
nv21Height = nv21PixelHeight,
|
|
1225
1237
|
qx = qx, qy = qy, qz = qz, qw = qw,
|
|
@@ -1246,7 +1258,7 @@ class IncrementalStitcher(
|
|
|
1246
1258
|
} else {
|
|
1247
1259
|
val tele = if (hasPixelData) {
|
|
1248
1260
|
hybrid!!.addFramePixelData(
|
|
1249
|
-
nv21 =
|
|
1261
|
+
nv21 = pixelBytes!!,
|
|
1250
1262
|
nv21Width = nv21PixelWidth,
|
|
1251
1263
|
nv21Height = nv21PixelHeight,
|
|
1252
1264
|
qx = qx, qy = qy, qz = qz, qw = qw,
|
|
@@ -1410,7 +1422,14 @@ class IncrementalStitcher(
|
|
|
1410
1422
|
// `addFramePixelData` instead of JPEG-decoding a
|
|
1411
1423
|
// separately-written path. Batch-keyframe mode
|
|
1412
1424
|
// ignores these (it uses `grayData` + `onAccept`).
|
|
1413
|
-
|
|
1425
|
+
//
|
|
1426
|
+
// v0.10.0 audit #4A — wrap in TransferredNV21 so the
|
|
1427
|
+
// engine takes ownership exactly once on the producer
|
|
1428
|
+
// thread (engine calls `.takeOnce()` before workScope).
|
|
1429
|
+
// Misuse (handing this same instance to two consumers)
|
|
1430
|
+
// throws at the second `.takeOnce()` site, not silently
|
|
1431
|
+
// corrupting frames.
|
|
1432
|
+
nv21PixelData = TransferredNV21(nv21Bytes),
|
|
1414
1433
|
nv21PixelWidth = width,
|
|
1415
1434
|
nv21PixelHeight = height,
|
|
1416
1435
|
onAccept = { targetPath ->
|
|
@@ -87,6 +87,10 @@ class RNImageStitcherPackage : ReactPackage {
|
|
|
87
87
|
RNSARSession(reactContext),
|
|
88
88
|
IncrementalStitcher(reactContext),
|
|
89
89
|
FileBridge(reactContext),
|
|
90
|
+
// v0.8.0 Phase 4b.ii — Android JSI installer for the
|
|
91
|
+
// host-worklet `__stitcherProxy` global. Mirror of
|
|
92
|
+
// iOS' `StitcherJsiInstaller`.
|
|
93
|
+
StitcherJsiInstallerModule(reactContext),
|
|
90
94
|
)
|
|
91
95
|
}
|
|
92
96
|
|