react-native-image-stitcher 0.16.1 → 0.17.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 +66 -0
- package/README.md +16 -34
- package/RNImageStitcher.podspec +26 -1
- package/android/build.gradle +54 -0
- package/android/src/main/cpp/CMakeLists.txt +46 -3
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +55 -6
- 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/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 +91 -0
- package/cpp/stitcher_worklet_registry.hpp +146 -0
- package/dist/camera/ARCameraView.d.ts +20 -0
- package/dist/camera/ARCameraView.js +23 -1
- package/dist/camera/Camera.d.ts +12 -0
- package/dist/camera/Camera.js +2 -2
- package/dist/camera/CaptureMemoryPill.d.ts +4 -3
- package/dist/camera/CaptureMemoryPill.js +4 -3
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +44 -6
- 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 +160 -0
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +51 -2
- package/src/camera/Camera.tsx +15 -0
- package/src/camera/CaptureMemoryPill.tsx +4 -3
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
14
|
> during 0.x are bumped to a new MINOR (e.g., 0.1 → 0.2), and the
|
|
15
15
|
> upgrade path is documented in this CHANGELOG.
|
|
16
16
|
|
|
17
|
+
## [0.17.0] — 2026-06-19
|
|
18
|
+
|
|
19
|
+
### Added — `arFrameProcessor`: observe AR frames with a host worklet
|
|
20
|
+
|
|
21
|
+
`<Camera>` gains an **`arFrameProcessor`** prop — a `'worklet'` invoked once per
|
|
22
|
+
**ARKit / ARCore frame** while in AR capture, dispatched natively and running
|
|
23
|
+
*alongside* first-party stitching (composition, not replacement). The worklet
|
|
24
|
+
receives a `StitcherFrame` tagged `source: 'ar'` with the world-space `pose` and
|
|
25
|
+
`arTrackingState`. It fires during preview too (continuous observation), at zero
|
|
26
|
+
per-frame cost when no worklet is registered.
|
|
27
|
+
|
|
28
|
+
This restores the previously-archived AR host-worklet capability and re-exposes
|
|
29
|
+
it as an explicit prop (rather than the old auto-registering hook). Under the
|
|
30
|
+
hood it installs `globalThis.__stitcherProxy` (JSI) on first use and fans frames
|
|
31
|
+
out through a shared C++ proxy / registry / dispatch layer on both platforms
|
|
32
|
+
(verified against `react-native-worklets-core` 1.6.3).
|
|
33
|
+
|
|
34
|
+
The non-AR equivalent remains `frameProcessor` (vision-camera); the two modes use
|
|
35
|
+
different runtimes and frame shapes, hence the separate prop. The
|
|
36
|
+
`StitcherFrame` / `StitcherFrameProcessor` type names are unchanged.
|
|
37
|
+
|
|
38
|
+
Verified on device: the worklet fires per frame on **iOS (ARKit, iPhone 16 Pro)**
|
|
39
|
+
and **Android (ARCore, Galaxy A35)**.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **Example app crashed at launch on Android** (`PlatformConstants could not be
|
|
44
|
+
found`). The v0.16.2 OpenCV-reuse demo added an app-level `externalNativeBuild`
|
|
45
|
+
to `example/android/app/build.gradle` that displaced React Native's own
|
|
46
|
+
New-Architecture app native build (so core TurboModules weren't compiled in).
|
|
47
|
+
Removed it; React Native owns the app native build again. **Example-app only —
|
|
48
|
+
the published SDK was never affected.**
|
|
49
|
+
|
|
50
|
+
## [0.16.2] — 2026-06-17
|
|
51
|
+
|
|
52
|
+
### Added — reuse the bundled OpenCV from your host app's native code (Android)
|
|
53
|
+
|
|
54
|
+
A host app's own native (C++/NDK) code can now reuse the **same** custom
|
|
55
|
+
OpenCV this library bundles (4.10.0, arm64-v8a) — **including `cv::Stitcher`**
|
|
56
|
+
— with no second copy of `libopencv_java4.so` in the APK.
|
|
57
|
+
|
|
58
|
+
The Android build now publishes the location of its vendored OpenCV SDK via
|
|
59
|
+
`rootProject.ext.rnisOpenCVDir` (and `rnisOpenCVAndroidSdkDir`). A consumer
|
|
60
|
+
points its `externalNativeBuild` at `-DOpenCV_DIR=${rootProject.ext.rnisOpenCVDir}`,
|
|
61
|
+
calls `find_package(OpenCV)`, and links the shared `opencv_java` (core /
|
|
62
|
+
imgproc / calib3d / … resolved at runtime from the already-shipped `.so`)
|
|
63
|
+
plus the whole-archived static `opencv_stitching` (`cv::Stitcher`). A
|
|
64
|
+
build-verified consumer ships in the example app
|
|
65
|
+
(`example/android/app/src/main/cpp/`).
|
|
66
|
+
|
|
67
|
+
This is additive — no public API or runtime-behaviour change. AGP
|
|
68
|
+
`prefabPublishing` was evaluated and is unworkable for prebuilt OpenCV
|
|
69
|
+
(prefab only exports libraries the module itself builds), so OpenCV's own
|
|
70
|
+
first-class CMake package is used instead. iOS reuse (the vendored
|
|
71
|
+
`opencv2.xcframework`) is unchanged.
|
|
72
|
+
|
|
73
|
+
### Docs
|
|
74
|
+
|
|
75
|
+
Documentation site refreshed: an easier **Getting started**, a complete
|
|
76
|
+
**`<Camera>` API** reference (every prop, the v0.16 guidance params —
|
|
77
|
+
`rectCrop` / `showPreview` / `panMode` / `panGuidance` / `maxPanDurationMs` /
|
|
78
|
+
`panTooFastThreshold` / `lateralBudgetCm` / `guidanceCopy` — and the
|
|
79
|
+
`stitcher` / `frameSelection` settings-JSON tables), a fully-loaded
|
|
80
|
+
**Complete example**, the v0.16 **Capture result & errors** union, and new
|
|
81
|
+
**Sharing OpenCV** / **Bring your own OpenCV** guides.
|
|
82
|
+
|
|
17
83
|
## [0.16.1] — 2026-06-16
|
|
18
84
|
|
|
19
85
|
### Changed — high-level `cv::Stitcher` is now the default pipeline
|
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ Peer dependencies (the host app provides these):
|
|
|
29
29
|
"react": ">=18.0.0",
|
|
30
30
|
"react-native": ">=0.72.0",
|
|
31
31
|
"react-native-vision-camera": ">=4.7.0",
|
|
32
|
+
"react-native-worklets-core": ">=1.3.0",
|
|
32
33
|
"react-native-sensors": ">=7.0.0",
|
|
33
|
-
"expo-sensors": ">=14.0.0",
|
|
34
34
|
"react-native-safe-area-context": ">=4.0.0"
|
|
35
35
|
}
|
|
36
36
|
```
|
|
@@ -71,50 +71,32 @@ cd android && ./gradlew :app:assembleDebug # Android
|
|
|
71
71
|
> See [Orientation support](#orientation-support) for the full story
|
|
72
72
|
> (landscape *is* supported on iOS if you need it).
|
|
73
73
|
|
|
74
|
-
The minimum:
|
|
75
|
-
`
|
|
74
|
+
The minimum: mount `<Camera>` with an `onCapture` handler. It fires once
|
|
75
|
+
per capture attempt — gate on `result.ok` before reading the output.
|
|
76
76
|
|
|
77
77
|
```tsx
|
|
78
|
-
import {
|
|
79
|
-
Camera,
|
|
80
|
-
type CameraCaptureResult,
|
|
81
|
-
type CameraError,
|
|
82
|
-
} from 'react-native-image-stitcher';
|
|
78
|
+
import { Camera, type CameraCaptureResult } from 'react-native-image-stitcher';
|
|
83
79
|
|
|
84
80
|
export function CaptureScreen() {
|
|
85
|
-
const handleCapture = (result: CameraCaptureResult) => {
|
|
86
|
-
// `onCapture` fires on success AND failure — gate on `ok` first.
|
|
87
|
-
if (!result.ok) {
|
|
88
|
-
console.warn('capture failed:', result.error.code, result.error.message);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
// Non-fatal quality signals (e.g. <70% of frames used). Always present.
|
|
92
|
-
if (result.warnings.length > 0) {
|
|
93
|
-
console.warn('warnings:', result.warnings.map((w) => w.code));
|
|
94
|
-
}
|
|
95
|
-
if (result.type === 'photo') {
|
|
96
|
-
console.log('Photo:', result.uri, result.width, result.height);
|
|
97
|
-
} else {
|
|
98
|
-
console.log(
|
|
99
|
-
'Panorama:',
|
|
100
|
-
result.uri,
|
|
101
|
-
`${result.framesIncluded}/${result.framesRequested} frames`,
|
|
102
|
-
`stitched as ${result.stitchModeResolved ?? 'n/a'}`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
81
|
return (
|
|
108
82
|
<Camera
|
|
109
|
-
onCapture={
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
83
|
+
onCapture={(result: CameraCaptureResult) => {
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
console.warn('capture failed:', result.error.code);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// result.type is 'photo' or 'panorama'; both carry uri/width/height.
|
|
89
|
+
console.log(result.type, result.uri, result.width, result.height);
|
|
90
|
+
}}
|
|
113
91
|
/>
|
|
114
92
|
);
|
|
115
93
|
}
|
|
116
94
|
```
|
|
117
95
|
|
|
96
|
+
> **Camera permission is the host's job.** The SDK never requests it for
|
|
97
|
+
> you — resolve it (e.g. with vision-camera's `useCameraPermission`)
|
|
98
|
+
> before mounting `<Camera>`.
|
|
99
|
+
|
|
118
100
|
### A complete capture screen
|
|
119
101
|
|
|
120
102
|
A realistic screen: requests permission up front, shows a capture
|
package/RNImageStitcher.podspec
CHANGED
|
@@ -60,6 +60,18 @@ Pod::Spec.new do |s|
|
|
|
60
60
|
|
|
61
61
|
s.dependency 'React-Core'
|
|
62
62
|
|
|
63
|
+
# react-native-worklets-core — provides the `RNWorklet::WorkletInvoker`
|
|
64
|
+
# + `JsiWorkletContext` primitives the AR-mode JSI fan-out is built on
|
|
65
|
+
# (StitcherJsiInstaller.mm / RNSARWorkletRuntime.mm + the shared
|
|
66
|
+
# cpp/stitcher_worklet_{registry,dispatch}.cpp). In practice this pod
|
|
67
|
+
# is already in every host's graph (vision-camera depends on it), but
|
|
68
|
+
# declaring it here makes the dependency explicit and guarantees its
|
|
69
|
+
# headers are present even for a host that uses AR mode without
|
|
70
|
+
# vision-camera. The bare `WKTJsiWorklet.h` includes in the .mm files
|
|
71
|
+
# resolve via the HEADER_SEARCH_PATHS entry below (the package's own
|
|
72
|
+
# node_modules copy of the worklets-core cpp/ dir).
|
|
73
|
+
s.dependency 'react-native-worklets-core'
|
|
74
|
+
|
|
63
75
|
# ─────────────────────────────────────────────────────────────────────
|
|
64
76
|
# OpenCV — pre-built custom xcframework fetched by postinstall
|
|
65
77
|
# ─────────────────────────────────────────────────────────────────────
|
|
@@ -84,6 +96,19 @@ Pod::Spec.new do |s|
|
|
|
84
96
|
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
|
|
85
97
|
'CLANG_CXX_LIBRARY' => 'libc++',
|
|
86
98
|
'OTHER_CPLUSPLUSFLAGS' => '$(inherited) -std=c++17',
|
|
87
|
-
|
|
99
|
+
# HEADER_SEARCH_PATHS:
|
|
100
|
+
# - "${PODS_TARGET_SRCROOT}/cpp" — the shared C++ port's own
|
|
101
|
+
# headers (keyframe_gate.hpp, stitcher_frame_jsi.hpp, …).
|
|
102
|
+
# - the worklets-core cpp/ dir — so the bare `#include
|
|
103
|
+
# "WKTJsiWorklet.h"` / "WKTJsiWorkletContext.h" lines in
|
|
104
|
+
# StitcherJsiInstaller.mm + RNSARWorkletRuntime.mm resolve.
|
|
105
|
+
# PODS_ROOT is `<host>/ios/Pods`; the package's worklets-core
|
|
106
|
+
# copy lives at `<host>/node_modules/react-native-worklets-core/
|
|
107
|
+
# cpp`, i.e. `${PODS_ROOT}/../node_modules/...`. (The shared
|
|
108
|
+
# cpp/*.cpp files instead use the namespace-prefixed
|
|
109
|
+
# `<react-native-worklets-core/WKTJsiWorklet.h>` form, which
|
|
110
|
+
# resolves against `${PODS_ROOT}/Headers/Public` — already on
|
|
111
|
+
# the inherited path — and works on Android's prefab too.)
|
|
112
|
+
'HEADER_SEARCH_PATHS' => '$(inherited) "${PODS_TARGET_SRCROOT}/cpp" "${PODS_ROOT}/../node_modules/react-native-worklets-core/cpp"',
|
|
88
113
|
}
|
|
89
114
|
end
|
package/android/build.gradle
CHANGED
|
@@ -112,6 +112,40 @@ android {
|
|
|
112
112
|
prefab true
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// ── Host OpenCV reuse: why NOT prefab publishing ─────────────────
|
|
116
|
+
//
|
|
117
|
+
// Goal: let a HOST app's own native (C++/NDK) code reuse the SAME
|
|
118
|
+
// custom OpenCV (4.10.0, arm64-v8a) this AAR already bundles — both
|
|
119
|
+
// libopencv_java4.so (cv::Mat, imgproc, features2d, calib3d, flann,
|
|
120
|
+
// photo, video …) AND cv::Stitcher (which lives in the static
|
|
121
|
+
// archive libopencv_stitching.a, NOT in the fat .so).
|
|
122
|
+
//
|
|
123
|
+
// We evaluated AGP `prefabPublishing` first (the idiomatic AAR way)
|
|
124
|
+
// and it CANNOT carry this OpenCV. Empirically verified: AGP's
|
|
125
|
+
// prefab modules only export libraries the module's own
|
|
126
|
+
// externalNativeBuild PRODUCES (here: just `image_stitcher`). A
|
|
127
|
+
// prebuilt jniLib (.so) or a prebuilt static archive (.a) is not an
|
|
128
|
+
// accepted prefab `libraryName` — the configure fails with
|
|
129
|
+
// `[CXX1404] did not find implicitly required targets`. So neither
|
|
130
|
+
// libopencv_java4.so nor libopencv_stitching.a can ride a prefab.
|
|
131
|
+
//
|
|
132
|
+
// Instead we expose the bundled OpenCV's OWN first-class CMake
|
|
133
|
+
// package — which already defines every module (incl. opencv_stitching
|
|
134
|
+
// as a STATIC IMPORTED target and opencv_java as a SHARED IMPORTED
|
|
135
|
+
// target) — to consumers via a rootProject ext property. A host
|
|
136
|
+
// points its externalNativeBuild at `-DOpenCV_DIR=<that dir>`, does
|
|
137
|
+
// `find_package(OpenCV REQUIRED)`, then links the SHARED `opencv_java`
|
|
138
|
+
// (cv::Mat & friends resolved at runtime from the AAR's already-shipped
|
|
139
|
+
// libopencv_java4.so — NO second copy) plus the whole-archived STATIC
|
|
140
|
+
// `opencv_stitching` (cv::Stitcher, a small private copy since it isn't
|
|
141
|
+
// in the fat .so). This is the idiomatic Android OpenCV-consumption
|
|
142
|
+
// path. See example/android/app for the working consumer.
|
|
143
|
+
//
|
|
144
|
+
// Set unconditionally at configure time so it's readable from the
|
|
145
|
+
// host app module regardless of project evaluation order.
|
|
146
|
+
rootProject.ext.rnisOpenCVAndroidSdkDir = "$opencvSdkDir/native"
|
|
147
|
+
rootProject.ext.rnisOpenCVDir = "$opencvSdkDir/native/jni"
|
|
148
|
+
|
|
115
149
|
// ── JNI shim build path ─────────────────────────────────────────
|
|
116
150
|
// Gradle compiles cpp/image_stitcher_jni.cpp into
|
|
117
151
|
// libimage_stitcher.so for the ABIs filtered above. The shim
|
|
@@ -267,6 +301,26 @@ dependencies {
|
|
|
267
301
|
android.sourceSets.main.java.exclude '**/CvFlowGateFrameProcessor.kt'
|
|
268
302
|
}
|
|
269
303
|
|
|
304
|
+
// v0.8.0 Phase 4b.ii/iii — react-native-worklets-core, consumed
|
|
305
|
+
// for its `rnworklets` PREFAB module (RNWorklet::JsiWorkletContext /
|
|
306
|
+
// WorkletInvoker) by the AR frame-processor JSI fan-out
|
|
307
|
+
// (src/main/cpp/stitcher_jsi_install_jni.cpp + the shared cpp/
|
|
308
|
+
// stitcher_*_jsi.cpp). `find_package(react-native-worklets-core ...)`
|
|
309
|
+
// in CMakeLists.txt needs this gradle project on the path so AGP
|
|
310
|
+
// wires the prefab into the native build. Same consumption pattern
|
|
311
|
+
// react-native-vision-camera uses for Frame Processors.
|
|
312
|
+
//
|
|
313
|
+
// `findProject(...)` guard mirrors the vision-camera block above:
|
|
314
|
+
// the SDK still compiles in hosts that don't install worklets-core
|
|
315
|
+
// (those hosts simply don't use the AR frame processor — the JSI
|
|
316
|
+
// sources still compile, but the prefab link only happens when the
|
|
317
|
+
// host provides worklets-core). Autolinking adds the project for
|
|
318
|
+
// any host that depends on vision-camera (which transitively pulls
|
|
319
|
+
// in worklets-core).
|
|
320
|
+
if (findProject(':react-native-worklets-core') != null) {
|
|
321
|
+
implementation project(':react-native-worklets-core')
|
|
322
|
+
}
|
|
323
|
+
|
|
270
324
|
// v0.10.0 audit #11A — Android JUnit test scaffold. JVM unit
|
|
271
325
|
// tests for pure-Kotlin data wrappers + algorithm helpers that
|
|
272
326
|
// don't need an Android device. Run via
|
|
@@ -66,6 +66,20 @@ add_library(opencv_stitching STATIC IMPORTED)
|
|
|
66
66
|
set_target_properties(opencv_stitching PROPERTIES
|
|
67
67
|
IMPORTED_LOCATION "${OPENCV_STITCHING_A}")
|
|
68
68
|
|
|
69
|
+
# ── React Native + worklets-core prefabs (v0.8.0 Phase 4b.ii/iii) ──
|
|
70
|
+
#
|
|
71
|
+
# The AR frame-processor's JSI fan-out (stitcher_jsi_install_jni.cpp +
|
|
72
|
+
# the shared cpp/stitcher_*_jsi.cpp) needs RN's JSI runtime, fbjni, and
|
|
73
|
+
# react-native-worklets-core's `RNWorklet::JsiWorkletContext` /
|
|
74
|
+
# `WorkletInvoker`. RN 0.71+ ships jsi + the native-modules umbrella as
|
|
75
|
+
# prefab packages; worklets-core ships a `rnworklets` prefab module.
|
|
76
|
+
# `buildFeatures.prefab true` (android/build.gradle) makes these
|
|
77
|
+
# discoverable. We mirror react-native-vision-camera's consumption of
|
|
78
|
+
# the SAME prefabs in this example app (verified working on RN 0.84.1).
|
|
79
|
+
find_package(ReactAndroid REQUIRED CONFIG)
|
|
80
|
+
find_package(fbjni REQUIRED CONFIG)
|
|
81
|
+
find_package(react-native-worklets-core REQUIRED CONFIG)
|
|
82
|
+
|
|
69
83
|
# ── Shared C++ port (KeyframeGate) ────────────────────────────────
|
|
70
84
|
#
|
|
71
85
|
# `cpp/` at the SDK root holds C++ that's compiled into BOTH the iOS
|
|
@@ -91,12 +105,26 @@ add_library(image_stitcher SHARED
|
|
|
91
105
|
# retry + dimension/memory instrumentation. Used to live in this
|
|
92
106
|
# file (image_stitcher_jni.cpp). See cpp/stitcher.hpp for design
|
|
93
107
|
# rationale.
|
|
94
|
-
"${SHARED_CPP_DIR}/stitcher.cpp"
|
|
108
|
+
"${SHARED_CPP_DIR}/stitcher.cpp"
|
|
109
|
+
# v0.8.0 Phase 4b.ii/iii — AR frame-processor JSI fan-out. The JNI
|
|
110
|
+
# binding (stitcher_jsi_install_jni.cpp) installs
|
|
111
|
+
# `globalThis.__stitcherProxy` and fans each AR frame out to the
|
|
112
|
+
# registered host worklets. The 4 shared cpp/ JSI files below are
|
|
113
|
+
# the SAME source the iOS pod compiles — one cross-platform JSI
|
|
114
|
+
# surface (proxy host object + native worklet registry + per-frame
|
|
115
|
+
# dispatch helper + StitcherFrame JSI host object).
|
|
116
|
+
stitcher_jsi_install_jni.cpp
|
|
117
|
+
"${SHARED_CPP_DIR}/stitcher_proxy_jsi.cpp"
|
|
118
|
+
"${SHARED_CPP_DIR}/stitcher_worklet_registry.cpp"
|
|
119
|
+
"${SHARED_CPP_DIR}/stitcher_worklet_dispatch.cpp"
|
|
120
|
+
"${SHARED_CPP_DIR}/stitcher_frame_jsi.cpp")
|
|
95
121
|
|
|
96
122
|
target_include_directories(image_stitcher PRIVATE
|
|
97
123
|
"${OPENCV_INCLUDE_DIR}"
|
|
98
124
|
# cpp/ holds keyframe_gate.hpp + ar_frame_pose.h that the JNI
|
|
99
|
-
# bindings include without relative-path spelunking.
|
|
125
|
+
# bindings include without relative-path spelunking. Also the
|
|
126
|
+
# shared JSI sources (stitcher_proxy_jsi.hpp, stitcher_frame_data.hpp,
|
|
127
|
+
# stitcher_worklet_*.hpp, stitcher_frame_jsi.hpp).
|
|
100
128
|
"${SHARED_CPP_DIR}")
|
|
101
129
|
|
|
102
130
|
# Link order matters:
|
|
@@ -118,7 +146,22 @@ target_link_libraries(image_stitcher
|
|
|
118
146
|
opencv_stitching
|
|
119
147
|
-Wl,--no-whole-archive
|
|
120
148
|
opencv_java
|
|
121
|
-
log
|
|
149
|
+
log
|
|
150
|
+
# v0.8.0 Phase 4b.ii/iii — JSI fan-out deps. Mirrors
|
|
151
|
+
# react-native-vision-camera's link set on RN 0.84.1:
|
|
152
|
+
# ReactAndroid::jsi — facebook::jsi::Runtime / Value / Object
|
|
153
|
+
# ReactAndroid::reactnative — RN's native-modules umbrella prefab
|
|
154
|
+
# (RN >= 0.76; CallInvoker + friends that
|
|
155
|
+
# worklets-core's context depends on)
|
|
156
|
+
# fbjni::fbjni — JNI helpers worklets-core links against
|
|
157
|
+
# react-native-worklets-core::rnworklets — JsiWorkletContext +
|
|
158
|
+
# WorkletInvoker. Carries its own
|
|
159
|
+
# include dir so <react-native-worklets-core/
|
|
160
|
+
# WKTJsiWorkletContext.h> resolves.
|
|
161
|
+
ReactAndroid::jsi
|
|
162
|
+
ReactAndroid::reactnative
|
|
163
|
+
fbjni::fbjni
|
|
164
|
+
react-native-worklets-core::rnworklets)
|
|
122
165
|
|
|
123
166
|
target_compile_options(image_stitcher PRIVATE
|
|
124
167
|
-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
|
+
}
|
|
@@ -101,6 +101,12 @@ class RNImageStitcherPackage : ReactPackage {
|
|
|
101
101
|
RNSARSession(reactContext),
|
|
102
102
|
IncrementalStitcher(reactContext),
|
|
103
103
|
FileBridge(reactContext),
|
|
104
|
+
// v0.8.0 Phase 4b.ii — surfaces `NativeModules.StitcherJsiInstaller`
|
|
105
|
+
// so JS' `ensureStitcherProxyInstalled()` can call its
|
|
106
|
+
// blocking-sync `install()` to install `globalThis.__stitcherProxy`
|
|
107
|
+
// on the main JS runtime (AR frame-processor host-worklet
|
|
108
|
+
// registration). Mirror of iOS' StitcherJsiInstaller.
|
|
109
|
+
StitcherJsiInstallerModule(reactContext),
|
|
104
110
|
)
|
|
105
111
|
}
|
|
106
112
|
|
|
@@ -383,17 +383,30 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
383
383
|
cameraPosWorld,
|
|
384
384
|
)
|
|
385
385
|
|
|
386
|
+
// v0.8.0 Phase 4b.iii — ensure the host-worklet runtime is
|
|
387
|
+
// installed before any per-frame fan-out can run. Idempotent
|
|
388
|
+
// (AtomicBoolean CAS): the first frame starts the dispatch
|
|
389
|
+
// thread; every later frame is a single atomic read. Kept on
|
|
390
|
+
// the GL thread because that's the only thread guaranteed to
|
|
391
|
+
// run once the AR session is live.
|
|
392
|
+
StitcherWorkletRuntime.installIfNeeded()
|
|
393
|
+
|
|
386
394
|
// Push pose into the AR session log. Mirrors iOS' delegate
|
|
387
395
|
// path; the existing RNSARFramePose / appendPose
|
|
388
396
|
// contract was already in place for Phase 4.
|
|
389
397
|
appendPose(camera, frame.timestamp)
|
|
390
398
|
|
|
391
|
-
// Forward to the incremental stitcher
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
399
|
+
// Forward to the incremental stitcher when capture is engaged,
|
|
400
|
+
// OR when an AR frame-processor host worklet is registered (the
|
|
401
|
+
// v0.8.0 Phase 4b.iii fan-out forwards preview frames whenever
|
|
402
|
+
// host worklets exist, even with capture off — the host worklet
|
|
403
|
+
// observes the live AR stream). `forwardToIncremental` does the
|
|
404
|
+
// NV21 pack once and gates the first-party ingest internally on
|
|
405
|
+
// `ingestActive`; the host-worklet dispatch is gated on the
|
|
406
|
+
// native registry count. `hasHostWorklets()` is a cheap atomic
|
|
407
|
+
// read (microseconds) so the common capture-off / no-worklet
|
|
408
|
+
// preview path stays near-free.
|
|
409
|
+
if (ingestActive || StitcherWorkletRuntime.hasHostWorklets()) {
|
|
397
410
|
forwardToIncremental(frame, camera)
|
|
398
411
|
}
|
|
399
412
|
|
|
@@ -656,6 +669,42 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
656
669
|
},
|
|
657
670
|
)
|
|
658
671
|
} // closes `if (ingestActive)` (v0.8.0 Phase 4b.iii)
|
|
672
|
+
|
|
673
|
+
// ── v0.8.0 Phase 4b.iii — AR frame-processor host-worklet fan-out ──
|
|
674
|
+
//
|
|
675
|
+
// After the first-party stitching ingest (above), fan the SAME
|
|
676
|
+
// already-packed NV21 frame + pose out to every host worklet the
|
|
677
|
+
// JS `arFrameProcessor` registered via `__stitcherProxy.install`.
|
|
678
|
+
// This is independent of `ingestActive`: a host worklet observes
|
|
679
|
+
// the live AR stream whether or not the user has engaged capture
|
|
680
|
+
// (the onDrawFrame gate already let us in when host worklets
|
|
681
|
+
// exist). `dispatchToHostWorklets` does a cheap native
|
|
682
|
+
// registry-count fast-path early-exit + (only when worklets are
|
|
683
|
+
// registered) copies the bytes into an owned native buffer and
|
|
684
|
+
// dispatches asynchronously on worklets-core's default context,
|
|
685
|
+
// so the GL render thread is NOT blocked on worklet execution.
|
|
686
|
+
//
|
|
687
|
+
// We reuse `packed.nv21` (full NV21: Y plane then interleaved
|
|
688
|
+
// VU) + `qarr` / `tArr` (already read above) — no extra Image
|
|
689
|
+
// hold, no second pack. ARCore camera pose is full 6DoF, so
|
|
690
|
+
// translation is always valid.
|
|
691
|
+
val arTracking = when (camera.trackingState) {
|
|
692
|
+
TrackingState.TRACKING -> "normal"
|
|
693
|
+
TrackingState.PAUSED -> "limited"
|
|
694
|
+
TrackingState.STOPPED -> "notAvailable"
|
|
695
|
+
else -> "notAvailable"
|
|
696
|
+
}
|
|
697
|
+
StitcherWorkletRuntime.dispatchToHostWorklets(
|
|
698
|
+
nv21Bytes = packed.nv21,
|
|
699
|
+
width = packed.width,
|
|
700
|
+
height = packed.height,
|
|
701
|
+
qx = qarr[0].toDouble(), qy = qarr[1].toDouble(),
|
|
702
|
+
qz = qarr[2].toDouble(), qw = qarr[3].toDouble(),
|
|
703
|
+
tx = tArr[0].toDouble(), ty = tArr[1].toDouble(),
|
|
704
|
+
tz = tArr[2].toDouble(),
|
|
705
|
+
timestampNs = frame.timestamp.toDouble(),
|
|
706
|
+
trackingState = arTracking,
|
|
707
|
+
)
|
|
659
708
|
}
|
|
660
709
|
|
|
661
710
|
/// v0.13.2 — map the JS physical device orientation to the
|