react-native-image-stitcher 0.7.1 → 0.9.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 +241 -0
- 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 +21 -3
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
- package/android/src/main/java/io/imagestitcher/rn/SaveFrameAsJpegPlugin.kt +162 -0
- 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 +6 -0
- package/dist/index.js +30 -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/dist/stitching/useFrameStream.d.ts +34 -0
- package/dist/stitching/useFrameStream.js +219 -0
- package/dist/stitching/useThrottledFrameProcessor.d.ts +33 -0
- package/dist/stitching/useThrottledFrameProcessor.js +132 -0
- package/dist/types.d.ts +87 -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/SaveFrameAsJpegPlugin.mm +185 -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 +35 -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/__tests__/useThrottledFrameProcessor.test.ts +178 -0
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- package/src/stitching/useFrameProcessor.ts +226 -0
- package/src/stitching/useFrameStream.ts +255 -0
- package/src/stitching/useThrottledFrameProcessor.ts +145 -0
- package/src/types.ts +95 -0
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,247 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.9.0] — 2026-05-27
|
|
20
|
+
|
|
21
|
+
### Added — layered frame-access helpers
|
|
22
|
+
|
|
23
|
+
Three new primitives completing the Tier 2 surface in the
|
|
24
|
+
three-tier extensibility pattern. See `docs/frame-access-tiers.md`
|
|
25
|
+
for the full decision flow + use-case mapping.
|
|
26
|
+
|
|
27
|
+
#### Layer 1 — `save_frame_as_jpeg` vc Frame Processor plugin (native)
|
|
28
|
+
|
|
29
|
+
Worklet-callable JPEG encoder. Registers on both platforms:
|
|
30
|
+
|
|
31
|
+
- **iOS** — `SaveFrameAsJpegPlugin.mm` (CIImage → CGImage → UIImage
|
|
32
|
+
→ UIImageJPEGRepresentation → atomic NSData write). Registered
|
|
33
|
+
via `+ (void)load` hook into `FrameProcessorPluginRegistry`.
|
|
34
|
+
- **Android** — `SaveFrameAsJpegPlugin.kt` wrapping the lib's
|
|
35
|
+
existing `YuvImageConverter.encodeJpegFromNV21` encoder (the
|
|
36
|
+
same one used by `RNSARCameraView`'s keyframe-accept callback).
|
|
37
|
+
Registered alongside `cv_flow_gate_process_frame` in
|
|
38
|
+
`RNImageStitcherPackage.ensureFrameProcessorPluginRegistered`.
|
|
39
|
+
|
|
40
|
+
Plugin contract (identical on both platforms):
|
|
41
|
+
- Args: `path` (string, REQUIRED), `quality` (number 0-100,
|
|
42
|
+
default 75, clamped `[1, 100]`)
|
|
43
|
+
- Returns: `{ ok: true, path, width, height }` OR
|
|
44
|
+
`{ ok: false, error: "..." }`
|
|
45
|
+
|
|
46
|
+
Hosts can call this directly from their own `useFrameProcessor`
|
|
47
|
+
worklet for custom rate-control logic; most consumers use it
|
|
48
|
+
indirectly via Layer 3.
|
|
49
|
+
|
|
50
|
+
#### Layer 2 — `useThrottledFrameProcessor` hook
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
const fp = useThrottledFrameProcessor(
|
|
54
|
+
(frame) => {
|
|
55
|
+
'worklet';
|
|
56
|
+
// Worklet-native processing at sub-frame-rate
|
|
57
|
+
},
|
|
58
|
+
{ sampleHz: 2 },
|
|
59
|
+
[],
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Pure TS throttle gate over `useFrameProcessor` (v0.8.0). Worklet
|
|
64
|
+
fires up to `sampleHz` times per second; ticks too close together
|
|
65
|
+
dropped via a monotonic-time `useSharedValue` gate.
|
|
66
|
+
|
|
67
|
+
**Use for**: worklet-native processing — native OCR via
|
|
68
|
+
Vision.framework / ML Kit wrapped as vc Frame Processor plugins,
|
|
69
|
+
TFLite ML inference, LiDAR depth (`frame.arDepth`). Direct
|
|
70
|
+
buffer/pose/depth access in the worklet; bridge small bbox-result
|
|
71
|
+
payloads to JS via `runOnJS`.
|
|
72
|
+
|
|
73
|
+
`sampleHz` clamped to `[0.5, 30]`.
|
|
74
|
+
|
|
75
|
+
#### Layer 3 — `useFrameStream` hook
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const fp = useFrameStream(
|
|
79
|
+
{ sampleHz: 2, quality: 75 },
|
|
80
|
+
(sample) => {
|
|
81
|
+
// JS-thread callback: sample.jpegPath, sample.pose, sample.timestamp
|
|
82
|
+
setThumbnail(sample.jpegPath);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Composes Layer 2 + Layer 1 + `runOnJS` bridge to deliver
|
|
88
|
+
`SampledFrame` objects to a JS-thread handler. Slot-reuse
|
|
89
|
+
strategy bounds disk usage to ~4 stale JPEGs.
|
|
90
|
+
|
|
91
|
+
**Use for**: JS-thread consumers — file-path OCR libraries (RN
|
|
92
|
+
modules wrapping ML Kit), cloud upload, thumbnail preview UI,
|
|
93
|
+
JS-side ML (TF.js, transformers.js).
|
|
94
|
+
|
|
95
|
+
`sampleHz` clamped to `[0.5, 10]`; `quality` clamped `[1, 100]`.
|
|
96
|
+
|
|
97
|
+
#### Types
|
|
98
|
+
|
|
99
|
+
- `SampledFrame` — `{ jpegPath, pose, timestamp, width, height }`
|
|
100
|
+
- `FrameStreamOptions` — `{ sampleHz, quality?, outputDir? }`
|
|
101
|
+
- `ThrottledFrameProcessorOptions` — `{ sampleHz }`
|
|
102
|
+
|
|
103
|
+
All exported from `react-native-image-stitcher`.
|
|
104
|
+
|
|
105
|
+
### Documentation
|
|
106
|
+
|
|
107
|
+
- `docs/frame-access-tiers.md` — new comprehensive reference for
|
|
108
|
+
all four host-facing hooks (`useKeyframeStream`,
|
|
109
|
+
`useThrottledFrameProcessor`, `useFrameStream`,
|
|
110
|
+
`useFrameProcessor`) with decision flow, cost envelope, use-case
|
|
111
|
+
mapping, AR vs non-AR mode tradeoff.
|
|
112
|
+
|
|
113
|
+
### Example app
|
|
114
|
+
|
|
115
|
+
`example/App.tsx` now mounts `useFrameStream` at 2 Hz with a
|
|
116
|
+
visible thumbnail overlay (bottom-right corner) — visual proof of
|
|
117
|
+
the Layer 1 + 2 + 3 pipeline working end-to-end on both iPhone
|
|
118
|
+
(60 Hz AR) and Galaxy A35 (30 Hz AR).
|
|
119
|
+
|
|
120
|
+
### Compatibility
|
|
121
|
+
|
|
122
|
+
- Strict additive over v0.8.0. No host changes required.
|
|
123
|
+
- Works in both AR and non-AR modes via v0.8.0's unified
|
|
124
|
+
`useFrameProcessor`.
|
|
125
|
+
- New hooks return `useFrameProcessor`-shape objects compatible
|
|
126
|
+
with `<Camera frameProcessor={...}>` (Phase 5 from v0.8.0).
|
|
127
|
+
|
|
128
|
+
### Notes
|
|
129
|
+
|
|
130
|
+
- Formal SSIM parity gate (Phase 7 of the v0.9.0 plan) was NOT
|
|
131
|
+
run for this release — the layered design doesn't touch
|
|
132
|
+
first-party stitching, so a regression is structurally unlikely.
|
|
133
|
+
Harness still in place from v0.8.0 (`scripts/ssim-compare.py`)
|
|
134
|
+
for any host that wants to run it locally.
|
|
135
|
+
|
|
136
|
+
[0.9.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.8.0...v0.9.0
|
|
137
|
+
|
|
138
|
+
## [0.8.0] — 2026-05-27
|
|
139
|
+
|
|
140
|
+
### Added — `useFrameProcessor` hook for host worklets
|
|
141
|
+
|
|
142
|
+
Hosts can now attach a `'worklet'`-prefixed function that fires on
|
|
143
|
+
every AR (and non-AR) capture frame, alongside the lib's own
|
|
144
|
+
first-party stitching. Use case: real-time OCR, packet detection,
|
|
145
|
+
ML inference, custom telemetry — anything that wants per-frame
|
|
146
|
+
pixel access in a worklet runtime.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useFrameProcessor, type StitcherFrame }
|
|
150
|
+
from 'react-native-image-stitcher';
|
|
151
|
+
|
|
152
|
+
const fp = useFrameProcessor((frame: StitcherFrame) => {
|
|
153
|
+
'worklet';
|
|
154
|
+
// frame.toArrayBuffer(), frame.pose, frame.source ('ar' | 'vc'), …
|
|
155
|
+
}, []);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**AR mode** (iPhone via ARKit, Android via ARCore): worklets fire
|
|
159
|
+
on every AR frame at the device's native rate (~30 Hz on A35,
|
|
160
|
+
~60 Hz on iPhone 16 Pro). Auto-registered into a process-scope
|
|
161
|
+
native registry via `globalThis.__stitcherProxy.install(workletFn)`.
|
|
162
|
+
The AR-session dispatch path fans out to both the lib's first-party
|
|
163
|
+
stitching AND every registered host worklet, with **per-worklet
|
|
164
|
+
failure isolation** (one host worklet throwing does NOT break
|
|
165
|
+
others or the lib's stitching).
|
|
166
|
+
|
|
167
|
+
**Non-AR mode** (vision-camera): pass the hook's return through
|
|
168
|
+
`<Camera frameProcessor={fp}>` to enable. Honest tradeoff: vc's
|
|
169
|
+
`<Camera>` accepts ONE processor, so supplying a host processor
|
|
170
|
+
displaces the lib's first-party stitching in non-AR mode. Hosts
|
|
171
|
+
that want both running concurrently should use AR mode (which
|
|
172
|
+
natively composes both). Composition for non-AR is tracked as
|
|
173
|
+
v0.9+.
|
|
174
|
+
|
|
175
|
+
### Added — `StitcherFrame` contract
|
|
176
|
+
|
|
177
|
+
Unified frame shape across AR and non-AR modes (`src/stitching/
|
|
178
|
+
StitcherFrame.ts`):
|
|
179
|
+
|
|
180
|
+
- `width` / `height` / `pixelFormat` / `orientation` / `timestamp`
|
|
181
|
+
/ `toArrayBuffer()` — vc-shape parity
|
|
182
|
+
- `pose: { rotation: [x,y,z,w], translation?: [x,y,z] }` — always
|
|
183
|
+
present in AR mode; rotation-only in non-AR
|
|
184
|
+
- `source: 'ar' | 'vc'` discriminator for safe AR-field access
|
|
185
|
+
- `arDepth?`, `arAnchors?`, `arTrackingState?` — populated in AR
|
|
186
|
+
mode on supported devices
|
|
187
|
+
|
|
188
|
+
### Added — JSI proxy host object
|
|
189
|
+
|
|
190
|
+
`globalThis.__stitcherProxy` installed on lib bootstrap (iOS:
|
|
191
|
+
`StitcherJsiInstaller` RN module via `RCTBridgeProxy.runtime` in
|
|
192
|
+
bridgeless mode; Android: `StitcherJsiInstallerModule` via
|
|
193
|
+
`ReactApplicationContext.getJavaScriptContextHolder()`). Exposes
|
|
194
|
+
`install` / `uninstall` / `count` host functions backed by a
|
|
195
|
+
shared C++ `retailens::StitcherWorkletRegistry` (process-scope,
|
|
196
|
+
mutex-serialised, snapshot-isolated).
|
|
197
|
+
|
|
198
|
+
### Changed — AR-mode dispatch architecture
|
|
199
|
+
|
|
200
|
+
Internal-only refactor (strict additive BC for hosts that don't
|
|
201
|
+
use `useFrameProcessor`):
|
|
202
|
+
|
|
203
|
+
- **iOS**: `ARSessionDelegate.session(_:didUpdate:)` now routes
|
|
204
|
+
through `RNSARWorkletRuntime.dispatchFrame:pose:` instead of
|
|
205
|
+
directly invoking the engine. First-party callback (Phase 3c)
|
|
206
|
+
runs synchronously on the caller thread (preserves ARKit's
|
|
207
|
+
pool-reuse contract); host worklet fan-out (Phase 4b.i)
|
|
208
|
+
dispatches asynchronously onto a dedicated worklets-core
|
|
209
|
+
context.
|
|
210
|
+
|
|
211
|
+
- **Android**: `RNSARCameraView.onDrawFrame` now wraps the
|
|
212
|
+
existing `module.ingestFromARCameraView(...)` call in
|
|
213
|
+
`StitcherWorkletRuntime.runFirstParty { ... }` (Phase 3c) and
|
|
214
|
+
follows with `StitcherWorkletRuntime.dispatchToHostWorklets(...)`
|
|
215
|
+
(Phase 4b.iii). Per-frame fan-out runs every AR frame when host
|
|
216
|
+
worklets are registered (not just during capture).
|
|
217
|
+
|
|
218
|
+
### Performance posture
|
|
219
|
+
|
|
220
|
+
- **First-party-only deployments** (no `useFrameProcessor`):
|
|
221
|
+
zero per-frame cost added. `hasHostWorklets()` atomic-read
|
|
222
|
+
short-circuits before any dispatch path.
|
|
223
|
+
- **Host worklets registered, idle preview**: Android pays
|
|
224
|
+
~6-10ms per AR frame (NV21 pack + JNI byte copy + worklet
|
|
225
|
+
dispatch). iOS uses `CFBridgingRetain` (no per-frame copy,
|
|
226
|
+
but ARKit pool back-pressure on next frame). Both acceptable
|
|
227
|
+
for v0.8.0; future optimization → zero-copy NV21 transfer via
|
|
228
|
+
direct `ByteBuffer` (Android).
|
|
229
|
+
|
|
230
|
+
### Added — SSIM parity gate harness
|
|
231
|
+
|
|
232
|
+
`scripts/ssim-compare.py` — pixel-wise SSIM comparison between
|
|
233
|
+
panorama JPEGs (Pillow + numpy + scikit-image; threshold 0.98).
|
|
234
|
+
Procedure in `docs/phase-7-parity-gate.md`.
|
|
235
|
+
|
|
236
|
+
> **v0.8.0 release note:** the formal SSIM parity gate was NOT
|
|
237
|
+
> run for this release. Verification rests on manual visual
|
|
238
|
+
> inspection of v0.8.0 panorama output on iPhone 16 Pro (Phase
|
|
239
|
+
> 4b.i) and Galaxy A35 (Phase 4b.iii) — both produced stitched
|
|
240
|
+
> panoramas matching the v0.7.x behaviour subjectively. The
|
|
241
|
+
> harness is in place for v0.8.1+ / future releases where the
|
|
242
|
+
> gate is mandatory.
|
|
243
|
+
|
|
244
|
+
### Migration guide
|
|
245
|
+
|
|
246
|
+
No host-side changes required for the common case. Hosts that
|
|
247
|
+
want to attach worklets:
|
|
248
|
+
|
|
249
|
+
1. Add `react-native-worklets-core` if not already a peer dep
|
|
250
|
+
(already in v0.7.x's peer-deps list).
|
|
251
|
+
2. Replace `useFrameProcessor` imports from
|
|
252
|
+
`react-native-vision-camera` with the lib's own export:
|
|
253
|
+
```diff
|
|
254
|
+
- import { useFrameProcessor } from 'react-native-vision-camera';
|
|
255
|
+
+ import { useFrameProcessor } from 'react-native-image-stitcher';
|
|
256
|
+
```
|
|
257
|
+
3. Worklet body now receives `StitcherFrame` instead of vc's
|
|
258
|
+
`Frame` — see `src/stitching/StitcherFrame.ts` for the contract.
|
|
259
|
+
|
|
19
260
|
## [0.7.1] — 2026-05-26
|
|
20
261
|
|
|
21
262
|
### Fixed — CI binary-packaging bloat
|
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
|
+
}
|