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.
Files changed (38) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +16 -34
  3. package/RNImageStitcher.podspec +26 -1
  4. package/android/build.gradle +54 -0
  5. package/android/src/main/cpp/CMakeLists.txt +46 -3
  6. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +55 -6
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
  11. package/cpp/stitcher_frame_jsi.cpp +214 -0
  12. package/cpp/stitcher_frame_jsi.hpp +108 -0
  13. package/cpp/stitcher_proxy_jsi.cpp +109 -0
  14. package/cpp/stitcher_proxy_jsi.hpp +46 -0
  15. package/cpp/stitcher_worklet_dispatch.cpp +103 -0
  16. package/cpp/stitcher_worklet_dispatch.hpp +71 -0
  17. package/cpp/stitcher_worklet_registry.cpp +91 -0
  18. package/cpp/stitcher_worklet_registry.hpp +146 -0
  19. package/dist/camera/ARCameraView.d.ts +20 -0
  20. package/dist/camera/ARCameraView.js +23 -1
  21. package/dist/camera/Camera.d.ts +12 -0
  22. package/dist/camera/Camera.js +2 -2
  23. package/dist/camera/CaptureMemoryPill.d.ts +4 -3
  24. package/dist/camera/CaptureMemoryPill.js +4 -3
  25. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  26. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  27. package/ios/Sources/RNImageStitcher/RNSARSession.swift +44 -6
  28. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  29. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  30. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
  31. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
  32. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  33. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +160 -0
  34. package/package.json +1 -1
  35. package/src/camera/ARCameraView.tsx +51 -2
  36. package/src/camera/Camera.tsx +15 -0
  37. package/src/camera/CaptureMemoryPill.tsx +4 -3
  38. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // StitcherFrameHostObject.h — Obj-C facade for the v0.8.0
4
+ // `StitcherFrame` JSI host object. Header is intentionally
5
+ // Obj-C-only (no `<jsi/jsi.h>` import) so this can land in the
6
+ // public CocoaPods umbrella without breaking `use_frameworks!` hosts
7
+ // (same rationale as `KeyframeGateBridge.h`).
8
+ //
9
+ // The C++ JSI host object class lives in the .mm; this facade
10
+ // exposes only what cross-module callers need:
11
+ //
12
+ // - Factory `+ fromARFrame:pose:` that the AR worklet runtime
13
+ // calls per ARFrame to construct a host object backed by the
14
+ // current AR session's frame.
15
+ // - Opaque accessor `- (void *)jsiHostObjectPtr` returning the
16
+ // `std::shared_ptr<facebook::jsi::HostObject> *` (boxed) that
17
+ // the worklet runtime hands to `jsi::Object::createFromHostObject`.
18
+ //
19
+ // Lifetime: the Obj-C wrapper holds the C++ shared_ptr; ARC frees
20
+ // the wrapper when nothing references it. Worklet runtime
21
+ // invalidates the underlying ARFrame retain when the dispatch
22
+ // returns; after invalidation, JSI access throws.
23
+
24
+ #pragma once
25
+
26
+ #import <Foundation/Foundation.h>
27
+ #import <ARKit/ARKit.h>
28
+
29
+ @class RNSARFramePose;
30
+
31
+ NS_ASSUME_NONNULL_BEGIN
32
+
33
+ NS_SWIFT_NAME(StitcherFrameHostObject)
34
+ @interface StitcherFrameHostObject : NSObject
35
+
36
+ /// Construct a host object backed by the supplied ARFrame + pose.
37
+ /// Retains the ARFrame for the host object's lifetime — caller can
38
+ /// safely release their reference.
39
+ ///
40
+ /// Thread: safe to call from the ARSession delegate queue; the
41
+ /// resulting host object's JSI access must happen on the worklet
42
+ /// runtime's thread (separate queue).
43
+ + (instancetype)fromARFrame:(ARFrame *)arFrame pose:(RNSARFramePose *)pose;
44
+
45
+ /// Mark the host object's underlying ARFrame as no longer accessible.
46
+ /// Subsequent JSI property reads return `undefined` or throw,
47
+ /// depending on the property. Idempotent.
48
+ - (void)invalidate;
49
+
50
+ /// Opaque pointer to a `std::shared_ptr<facebook::jsi::HostObject>`.
51
+ /// The worklet runtime (Obj-C++ context with JSI available) casts
52
+ /// this back via `*reinterpret_cast<std::shared_ptr<facebook::jsi::HostObject>*>(ptr)`
53
+ /// to hand to `jsi::Object::createFromHostObject`.
54
+ ///
55
+ /// Returns `NULL` if the host object has been invalidated.
56
+ - (nullable void *)jsiHostObjectPtr;
57
+
58
+ @end
59
+
60
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,214 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // StitcherFrameHostObject.mm — iOS-specific wrapper for the shared
4
+ // `retailens::StitcherFrameJsiHostObject` (defined in
5
+ // `cpp/stitcher_frame_jsi.{hpp,cpp}`).
6
+ //
7
+ // Owns:
8
+ // - The Obj-C facade callable from Swift / other Obj-C / .mm files.
9
+ // - The iOS-specific `PixelBufferReader` impl (wraps a
10
+ // `CVPixelBufferRef` from `ARFrame.capturedImage`; lock / memcpy
11
+ // / unlock pattern).
12
+ // - The Obj-C → C++ extraction logic that builds a
13
+ // `retailens::StitcherFrameData` from an `ARFrame` + the lib's
14
+ // `RNSARFramePose`.
15
+ //
16
+ // Does NOT own:
17
+ // - The JSI `get` / `getPropertyNames` dispatch. That lives in
18
+ // `cpp/stitcher_frame_jsi.cpp` and is identical to the Android
19
+ // implementation (DRY across platforms).
20
+
21
+ #import "StitcherFrameHostObject.h"
22
+
23
+ #import <Foundation/Foundation.h>
24
+ #import <CoreVideo/CVPixelBuffer.h>
25
+ #import <CoreMedia/CoreMedia.h>
26
+ #import <os/log.h>
27
+
28
+ #include <jsi/jsi.h>
29
+
30
+ #include <algorithm>
31
+ #include <cstring>
32
+ #include <memory>
33
+ #include <string>
34
+ #include <utility>
35
+
36
+ #include "stitcher_frame_data.hpp"
37
+ #include "stitcher_frame_jsi.hpp"
38
+
39
+ using namespace facebook;
40
+
41
+ // Forward-declare the Swift `RNSARFramePose` Obj-C surface we need.
42
+ // This matches the pattern in `KeyframeGateFrameProcessor.mm`
43
+ // (forward-declaring `IncrementalStitcher`) — avoids depending on
44
+ // the autogenerated `RNImageStitcher-Swift.h`, which is created at
45
+ // build time and not always available to .mm files in this pod.
46
+ //
47
+ // MUST stay in sync with `RNSARSession.swift::RNSARFramePose` —
48
+ // adding a new field there means adding it here too.
49
+ @class RNSARFramePose;
50
+ @interface RNSARFramePose : NSObject
51
+ @property (nonatomic, readonly) double tx;
52
+ @property (nonatomic, readonly) double ty;
53
+ @property (nonatomic, readonly) double tz;
54
+ @property (nonatomic, readonly) double qx;
55
+ @property (nonatomic, readonly) double qy;
56
+ @property (nonatomic, readonly) double qz;
57
+ @property (nonatomic, readonly) double qw;
58
+ @property (nonatomic, readonly) NSInteger imageWidth;
59
+ @property (nonatomic, readonly) NSInteger imageHeight;
60
+ @property (nonatomic, readonly) double timestampMs;
61
+ @end
62
+
63
+ #pragma mark - iOS PixelBufferReader
64
+
65
+ namespace {
66
+
67
+ /// iOS-specific `retailens::PixelBufferReader` impl. See the base
68
+ /// class docstring for the general contract (thread-affinity,
69
+ /// invalidation semantics, Y-plane-only constraint). This subclass
70
+ /// adds:
71
+ /// - `CVPixelBuffer` lock/memcpy/unlock per copyTo
72
+ /// - `CFBridgingRetain` of the parent `ARFrame` so ARKit's
73
+ /// pool can't reclaim the underlying buffer mid-read
74
+ class IOSPixelBufferReader : public retailens::PixelBufferReader {
75
+ public:
76
+ explicit IOSPixelBufferReader(ARFrame* arFrame) {
77
+ // Retain the ARFrame for our lifetime. CFBridgingRetain hands
78
+ // ARC ownership to our void*. Released in destructor.
79
+ _retainedFrame = (void*)CFBridgingRetain(arFrame);
80
+ CVPixelBufferRef pixelBuffer = arFrame.capturedImage;
81
+ if (pixelBuffer != NULL) {
82
+ _bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
83
+ _height = CVPixelBufferGetHeight(pixelBuffer);
84
+ }
85
+ }
86
+
87
+ ~IOSPixelBufferReader() override {
88
+ // Transfer ownership back to ARC, which then releases.
89
+ if (_retainedFrame != nullptr) {
90
+ ARFrame* frame = CFBridgingRelease(_retainedFrame);
91
+ (void)frame;
92
+ _retainedFrame = nullptr;
93
+ }
94
+ }
95
+
96
+ std::size_t byteSize() const override {
97
+ return _bytesPerRow * _height;
98
+ }
99
+
100
+ std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) override {
101
+ if (_retainedFrame == nullptr) return 0;
102
+ ARFrame* frame = (__bridge ARFrame*)_retainedFrame;
103
+ CVPixelBufferRef pixelBuffer = frame.capturedImage;
104
+ if (pixelBuffer == NULL) return 0;
105
+
106
+ CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
107
+ const uint8_t* src = (const uint8_t*)CVPixelBufferGetBaseAddress(pixelBuffer);
108
+ std::size_t toCopy = std::min<std::size_t>(byteSize(), maxBytes);
109
+ if (src != nullptr && toCopy > 0) {
110
+ std::memcpy(dst, src, toCopy);
111
+ } else {
112
+ toCopy = 0;
113
+ }
114
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
115
+ return toCopy;
116
+ }
117
+
118
+ private:
119
+ void* _retainedFrame = nullptr; // CFBridgingRetain'd ARFrame
120
+ std::size_t _bytesPerRow = 0;
121
+ std::size_t _height = 0;
122
+ };
123
+
124
+ } // anonymous namespace
125
+
126
+ #pragma mark - Obj-C facade
127
+
128
+ @implementation StitcherFrameHostObject {
129
+ std::shared_ptr<retailens::StitcherFrameJsiHostObject> _hostObject;
130
+ }
131
+
132
+ + (instancetype)fromARFrame:(ARFrame*)arFrame pose:(RNSARFramePose*)pose {
133
+ StitcherFrameHostObject* obj = [[self alloc] init];
134
+
135
+ retailens::StitcherFrameData data;
136
+ data.source = "ar";
137
+ data.width = static_cast<int32_t>(pose.imageWidth);
138
+ data.height = static_cast<int32_t>(pose.imageHeight);
139
+ // ARKit's `kCVPixelFormatType_420YpCbCr8BiPlanarFullRange` (NV12)
140
+ // is reported as "yuv". Other formats (rare in ARKit; possible if
141
+ // ARWorldTrackingConfiguration.videoFormat is overridden to BGRA)
142
+ // → "unknown" + os_log warning so worklets that gate on
143
+ // `pixelFormat === 'yuv'` can be debugged without a screen recording.
144
+ OSType pf = CVPixelBufferGetPixelFormatType(arFrame.capturedImage);
145
+ if (pf == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ||
146
+ pf == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
147
+ data.pixelFormat = "yuv";
148
+ } else {
149
+ data.pixelFormat = "unknown";
150
+ os_log_error(OS_LOG_DEFAULT,
151
+ "[StitcherFrame] unexpected ARKit pixel format 0x%x; "
152
+ "worklet receives pixelFormat='unknown' and toArrayBuffer() "
153
+ "bytes are first-plane only (layout undefined for unknown "
154
+ "formats). See StitcherFrame.ts docstring.", (unsigned int)pf);
155
+ }
156
+ // ARKit doesn't have a `Frame.orientation` per se; pose carries
157
+ // the imageWidth >= imageHeight discriminator the lib uses
158
+ // elsewhere (`isLandscape`). v0.8.0 ships a coarse mapping;
159
+ // worklets that need exact UI orientation can read it from
160
+ // device-orientation sensors.
161
+ data.orientation =
162
+ (pose.imageWidth >= pose.imageHeight) ? "landscape-right" : "portrait";
163
+ // `ARFrame.timestamp` is CFAbsoluteTime (seconds since epoch).
164
+ // Convert to ns to match vc Frame.timestamp.
165
+ data.timestampNs = arFrame.timestamp * 1e9;
166
+
167
+ data.qx = pose.qx;
168
+ data.qy = pose.qy;
169
+ data.qz = pose.qz;
170
+ data.qw = pose.qw;
171
+ data.tx = pose.tx;
172
+ data.ty = pose.ty;
173
+ data.tz = pose.tz;
174
+ data.hasTranslation = true; // AR mode always has translation
175
+
176
+ switch (arFrame.camera.trackingState) {
177
+ case ARTrackingStateNotAvailable:
178
+ data.arTrackingState = "notAvailable";
179
+ break;
180
+ case ARTrackingStateLimited:
181
+ data.arTrackingState = "limited";
182
+ break;
183
+ case ARTrackingStateNormal:
184
+ data.arTrackingState = "normal";
185
+ break;
186
+ }
187
+
188
+ data.pixelReader = std::make_shared<IOSPixelBufferReader>(arFrame);
189
+
190
+ // Use the static factory (private ctor enforces shared_ptr
191
+ // ownership — required for `shared_from_this()` inside the JSI
192
+ // `toArrayBuffer` lambda).
193
+ obj->_hostObject =
194
+ retailens::StitcherFrameJsiHostObject::create(std::move(data));
195
+ return obj;
196
+ }
197
+
198
+ - (void)invalidate {
199
+ if (_hostObject) {
200
+ _hostObject->invalidate();
201
+ }
202
+ }
203
+
204
+ - (void*)jsiHostObjectPtr {
205
+ if (!_hostObject) return NULL;
206
+ // Box a heap-allocated copy of the shared_ptr to the abstract
207
+ // `jsi::HostObject` base. Caller (worklet runtime) does:
208
+ // auto sp = static_cast<std::shared_ptr<jsi::HostObject>*>(ptr);
209
+ // auto jsObj = jsi::Object::createFromHostObject(rt, *sp);
210
+ // delete sp;
211
+ return new std::shared_ptr<jsi::HostObject>(_hostObject);
212
+ }
213
+
214
+ @end
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // StitcherJsiInstaller.h — RN module that installs the
4
+ // `globalThis.__stitcherProxy` JSI host object on the main JS
5
+ // runtime. Called once at lib boot from the TS layer
6
+ // (`src/index.ts` or the `useFrameProcessor` hook) via a
7
+ // synchronous JS bridge call.
8
+ //
9
+ // The proxy exposes two host functions:
10
+ //
11
+ // __stitcherProxy.install(workletFn) → string ID
12
+ // __stitcherProxy.uninstall(id) → undefined
13
+ //
14
+ // `install` wraps the worklet function into a
15
+ // `RNWorklet::WorkletInvoker` and stores it in the C++
16
+ // `retailens::StitcherWorkletRegistry` singleton (in
17
+ // `cpp/stitcher_worklet_registry.{hpp,cpp}`). The AR worklet
18
+ // runtime's per-frame dispatch reads from that registry to fan
19
+ // out invocations.
20
+ //
21
+ // Why a RN module (not a vanilla NSObject installable):
22
+ // - Hosts can't reliably reach into the JSI runtime from JS
23
+ // without a native sync method to broker the install.
24
+ // - `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD` is the documented
25
+ // pattern for "JS calls a native method synchronously to
26
+ // install JSI bindings on the main runtime". vision-camera
27
+ // uses the same pattern (`VisionCameraInstaller.mm`).
28
+ // - In RN's bridgeless mode the legacy `RCTCxxBridge.runtime`
29
+ // accessor still works (vc has a comment to migrate but it
30
+ // hasn't been needed yet — same applies to us).
31
+
32
+ #pragma once
33
+
34
+ #import <Foundation/Foundation.h>
35
+ #import <React/RCTBridgeModule.h>
36
+
37
+ NS_ASSUME_NONNULL_BEGIN
38
+
39
+ @interface StitcherJsiInstaller : NSObject <RCTBridgeModule>
40
+ @end
41
+
42
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,160 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // StitcherJsiInstaller.mm — implementation. Installs
4
+ // `globalThis.__stitcherProxy` on the main JS runtime.
5
+ //
6
+ // ## Why a host object rather than two globalThis functions
7
+ //
8
+ // We could install `__stitcherProxy_install` + `__stitcherProxy_uninstall`
9
+ // directly on `globalThis`. Wrapping them in a host object is
10
+ // slightly more code but:
11
+ // - Namespaces the proxy under a single global property
12
+ // (easier to feature-detect; one `if (globalThis.__stitcherProxy)`
13
+ // instead of two).
14
+ // - Matches vc's pattern (`global.VisionCameraProxy`), so future
15
+ // readers recognise the shape.
16
+ // - Keeps room to grow (e.g., add `__stitcherProxy.snapshot()` for
17
+ // diagnostics) without polluting globalThis further.
18
+
19
+ #import "StitcherJsiInstaller.h"
20
+
21
+ #import <Foundation/Foundation.h>
22
+ #import <React/RCTBridge.h>
23
+ #import <React/RCTBridge+Private.h>
24
+ #import <React/RCTUtils.h>
25
+ #import <ReactCommon/CallInvoker.h>
26
+ // `RCTCxxBridge` (and its bridgeless-mode `RCTBridgeProxy` forwarder)
27
+ // exposes `-jsCallInvoker` returning `std::shared_ptr<CallInvoker>`,
28
+ // but the property declaration lives in `<ReactCommon/RCTTurboModule.h>`
29
+ // which isn't on our pod's HEADER_SEARCH_PATHS (worklets-core gets it
30
+ // via its own ReactCommon dep). Rather than enlarging our pod's
31
+ // dependency surface, forward-declare the property in an anonymous
32
+ // category — the runtime dispatches to RN's actual implementation.
33
+ // Pattern matches `WKTJsiWorkletContext.cpp`'s approach to keep the
34
+ // pod self-contained.
35
+ @interface RCTCxxBridge ()
36
+ @property (nonatomic, readonly) std::shared_ptr<facebook::react::CallInvoker> jsCallInvoker;
37
+ @end
38
+ #import <os/log.h>
39
+
40
+ #include <jsi/jsi.h>
41
+
42
+ #include "stitcher_proxy_jsi.hpp"
43
+ // v0.11.1 — worklets-core JsiWorkletContext. We initialize the
44
+ // SINGLETON default instance here so that other contexts in this
45
+ // library that use the 2-arg `JsiWorkletContext(name, workletInvoker)`
46
+ // constructor inherit a working `_jsCallInvoker` (and thus their
47
+ // `runOnJS` / `Worklets.createRunOnJS` callbacks actually route back
48
+ // to the main JS thread). Specifically: `RNSARWorkletRuntime`'s AR-
49
+ // side worklet context (see `RNSARWorkletRuntime.mm:155`) uses the
50
+ // 2-arg ctor; pre-v0.11.1 that left its inherited `_jsCallInvoker`
51
+ // nullptr, and `invokeOnJsThread` silently no-op'd (see
52
+ // `WKTJsiWorkletContext.cpp:124-131`). Test 2 of the v0.11.0
53
+ // manual-verification checklist surfaced this as "AR-mode host
54
+ // worklets register but their runOnJS callbacks never fire."
55
+ #include "WKTJsiWorkletContext.h"
56
+
57
+ using namespace facebook;
58
+
59
+ // The host object class + install logic moved to shared C++ in
60
+ // `cpp/stitcher_proxy_jsi.{hpp,cpp}` (v0.8.0 Phase 4b.ii). The
61
+ // Android JNI installer reuses the same `install` / `uninstall` /
62
+ // `count` host functions verbatim — the JSI dispatch is identical
63
+ // across platforms (matches the StitcherFrame host object's design).
64
+
65
+ #pragma mark - RN module
66
+
67
+ @implementation StitcherJsiInstaller
68
+
69
+ // RN injects `_bridge` at module init (legacy bridge → RCTBridge*;
70
+ // bridgeless / new arch → RCTBridgeProxy*, which forwards `runtime`
71
+ // access via NSProxy `forwardInvocation:`). Using the injected
72
+ // `_bridge` instead of `[RCTBridge currentBridge]` is the
73
+ // bridgeless-compatible idiom — `currentBridge` is nil under new
74
+ // arch. Pattern lifted from `react-native-worklets-core/ios/Worklets.mm`.
75
+ @synthesize bridge = _bridge;
76
+
77
+ RCT_EXPORT_MODULE()
78
+
79
+ + (BOOL)requiresMainQueueSetup {
80
+ return YES;
81
+ }
82
+
83
+ - (void)setBridge:(RCTBridge*)bridge {
84
+ _bridge = bridge;
85
+ }
86
+
87
+ // Synchronous install method. JS calls this once at lib bootstrap
88
+ // to install the global proxy on the main JS runtime. Returns
89
+ // `@YES` on success or `@NO` if the JSI runtime wasn't reachable
90
+ // (remote debug mode pre-Hermes; bridge not yet ready; etc.).
91
+ //
92
+ // `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD` is the documented
93
+ // pattern for "run native code synchronously on the JS thread to
94
+ // install JSI bindings." Same pattern worklets-core + vision-camera
95
+ // use for their installs.
96
+ //
97
+ // **Bridgeless mode:** `_bridge` is an `RCTBridgeProxy` (NSProxy
98
+ // subclass) that forwards `-runtime` / `-jsCallInvoker` invocations
99
+ // to the underlying RCTHost-backed runtime. The `(RCTCxxBridge*)`
100
+ // cast is a no-op at runtime (NSProxy ignores static type) but
101
+ // keeps the Obj-C compiler happy about property access.
102
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
103
+ if (_bridge == nil) {
104
+ os_log_error(OS_LOG_DEFAULT,
105
+ "[StitcherJsiInstaller] _bridge is nil; the module was "
106
+ "instantiated without bridge injection. Cannot install "
107
+ "__stitcherProxy.");
108
+ return @NO;
109
+ }
110
+
111
+ RCTCxxBridge* cxxBridge = (RCTCxxBridge*)_bridge;
112
+ if (cxxBridge.runtime == nullptr) {
113
+ os_log_error(OS_LOG_DEFAULT,
114
+ "[StitcherJsiInstaller] _bridge.runtime is nullptr; the JS "
115
+ "runtime hasn't been initialized yet OR remote debugger is "
116
+ "attached. Cannot install __stitcherProxy.");
117
+ return @NO;
118
+ }
119
+
120
+ jsi::Runtime& runtime = *(jsi::Runtime*)cxxBridge.runtime;
121
+ retailens::installStitcherProxy(runtime);
122
+
123
+ // v0.11.1 — initialize the singleton default JsiWorkletContext so
124
+ // that downstream 2-arg ctors (RNSARWorkletRuntime) inherit a
125
+ // working `_jsCallInvoker`. Without this, AR-mode host worklets'
126
+ // `runOnJS` / `Worklets.createRunOnJS` callbacks silently no-op
127
+ // (`WKTJsiWorkletContext.cpp:124-131` early-returns when
128
+ // `_jsCallInvoker == nullptr`). See file-top comment for the full
129
+ // diagnosis (Test 2 of v0.11.0 manual-verification checklist).
130
+ //
131
+ // Idempotent at the worklets-core level: re-initialization is
132
+ // tolerated; the default instance is a process-scope singleton
133
+ // and we're called once per JS-runtime bootstrap. In bridgeless
134
+ // mode `cxxBridge.jsCallInvoker` is forwarded via RCTBridgeProxy
135
+ // to the underlying RCTHost's `CallInvoker` (same forwarding
136
+ // pattern as `cxxBridge.runtime` above).
137
+ auto jsCallInvoker = cxxBridge.jsCallInvoker;
138
+ if (jsCallInvoker == nullptr) {
139
+ os_log_error(OS_LOG_DEFAULT,
140
+ "[StitcherJsiInstaller] cxxBridge.jsCallInvoker is nullptr; "
141
+ "AR-mode host worklets' runOnJS will not fire. Proxy installed "
142
+ "but worklet-bridging is impaired.");
143
+ // Proxy is still installed; only the runOnJS path is impaired.
144
+ // Return @YES so JS callers don't fall back to the JS-side registry.
145
+ return @YES;
146
+ }
147
+ auto jsInvokerAdapter =
148
+ [jsCallInvoker](std::function<void()>&& fp) {
149
+ jsCallInvoker->invokeAsync(std::move(fp));
150
+ };
151
+ RNWorklet::JsiWorkletContext::getDefaultInstance()->initialize(
152
+ "stitcher.default", &runtime, jsInvokerAdapter);
153
+
154
+ os_log_info(OS_LOG_DEFAULT,
155
+ "[StitcherJsiInstaller] installed globalThis.__stitcherProxy "
156
+ "AND initialized default JsiWorkletContext on main JS runtime.");
157
+ return @YES;
158
+ }
159
+
160
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-image-stitcher",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,7 +30,7 @@
30
30
  * developer verification.
31
31
  */
32
32
 
33
- import React, { forwardRef, useImperativeHandle, useRef } from 'react';
33
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
34
34
  import {
35
35
  NativeModules,
36
36
  Platform,
@@ -41,6 +41,9 @@ import {
41
41
  type ViewStyle,
42
42
  } from 'react-native';
43
43
 
44
+ import { ensureStitcherProxyInstalled } from '../stitching/ensureStitcherProxyInstalled';
45
+ import type { StitcherFrameProcessor } from '../stitching/StitcherFrame';
46
+
44
47
 
45
48
  // React Native looks up the component by its NATIVE name.
46
49
  // iOS: comes from `ARCameraViewManager.m`'s
@@ -63,6 +66,25 @@ export interface ARCameraViewProps {
63
66
  * components without rewriting their guidance text plumbing.
64
67
  */
65
68
  guidance?: string;
69
+ /**
70
+ * Optional host worklet invoked once per AR frame, ALONGSIDE the
71
+ * lib's first-party stitching (composition, not replacement). The
72
+ * worklet receives a `StitcherFrame` enriched with AR metadata —
73
+ * `source: 'ar'`, world-space `pose` (rotation + translation),
74
+ * `arTrackingState`, and (when supported) `arDepth` / `arAnchors`.
75
+ *
76
+ * Must be a `'worklet'`-prefixed function. Registration installs the
77
+ * native `__stitcherProxy` JSI host object on first use and fans the
78
+ * worklet out from the AR session's per-frame dispatch. If the
79
+ * native install is unavailable (e.g. remote debugging), the worklet
80
+ * silently never fires — no crash.
81
+ *
82
+ * The non-AR equivalent is vision-camera's own `useFrameProcessor`
83
+ * passed via `<Camera frameProcessor={...}>`; the two modes run on
84
+ * different runtimes with different frame shapes, hence the separate
85
+ * prop.
86
+ */
87
+ arFrameProcessor?: StitcherFrameProcessor;
66
88
  }
67
89
 
68
90
 
@@ -145,7 +167,7 @@ type RecordingCallbacks = {
145
167
 
146
168
  export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
147
169
  function ARCameraView(
148
- { style, guidance },
170
+ { style, guidance, arFrameProcessor },
149
171
  ref,
150
172
  ): React.JSX.Element {
151
173
  // Held across the start→stop lifecycle so stopRecording's
@@ -153,6 +175,33 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
153
175
  // pair vision-camera uses.
154
176
  const recordingCallbacksRef = useRef<RecordingCallbacks | null>(null);
155
177
 
178
+ // AR frame-processor registration. Installs the native
179
+ // `__stitcherProxy` (idempotent) and registers the host worklet so
180
+ // the AR session's per-frame fan-out invokes it; unregisters on
181
+ // unmount or when the worklet identity changes. No-op when no
182
+ // worklet is supplied or the native install is unavailable.
183
+ useEffect(() => {
184
+ if (arFrameProcessor == null) {
185
+ return undefined;
186
+ }
187
+ if (!ensureStitcherProxyInstalled()) {
188
+ return undefined;
189
+ }
190
+ const proxy = (globalThis as {
191
+ __stitcherProxy?: {
192
+ install(fn: StitcherFrameProcessor): string;
193
+ uninstall(id: string): void;
194
+ };
195
+ }).__stitcherProxy;
196
+ if (proxy == null) {
197
+ return undefined;
198
+ }
199
+ const id = proxy.install(arFrameProcessor);
200
+ return () => {
201
+ proxy.uninstall(id);
202
+ };
203
+ }, [arFrameProcessor]);
204
+
156
205
  useImperativeHandle(ref, () => ({
157
206
  takePhoto: async (options = {}) => {
158
207
  const native: any =
@@ -66,6 +66,7 @@ import type {
66
66
  } from 'react-native-vision-camera';
67
67
 
68
68
  import { useARSession } from '../ar/useARSession';
69
+ import type { StitcherFrameProcessor } from '../stitching/StitcherFrame';
69
70
  import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
70
71
  import { CameraShutter } from './CameraShutter';
71
72
  import { CameraView } from './CameraView';
@@ -751,6 +752,18 @@ export interface CameraProps {
751
752
  */
752
753
  frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
753
754
 
755
+ /**
756
+ * AR-mode host worklet, invoked once per ARKit / ARCore frame
757
+ * ALONGSIDE the lib's first-party stitching (composition, not
758
+ * replacement). Receives a `StitcherFrame` tagged `source: 'ar'`
759
+ * with world-space `pose` + `arTrackingState`. Only fires in AR
760
+ * capture (`captureSource === 'ar'`); the non-AR equivalent is
761
+ * `frameProcessor` above (the two modes use different runtimes and
762
+ * frame shapes). Must be a `'worklet'`-prefixed function; if the
763
+ * native install is unavailable it silently never fires.
764
+ */
765
+ arFrameProcessor?: StitcherFrameProcessor;
766
+
754
767
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────────
755
768
  /**
756
769
  * Which device holds the non-AR panorama capture accepts.
@@ -1157,6 +1170,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
1157
1170
  capturePreviewActions,
1158
1171
  onCapturePreviewClose,
1159
1172
  frameProcessor: hostFrameProcessor,
1173
+ arFrameProcessor,
1160
1174
  engine = 'batch-keyframe',
1161
1175
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
1162
1176
  panMode = 'vertical',
@@ -2420,6 +2434,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
2420
2434
  <ARCameraView
2421
2435
  ref={arViewRef}
2422
2436
  style={StyleSheet.absoluteFill}
2437
+ arFrameProcessor={arFrameProcessor}
2423
2438
  />
2424
2439
  ) : (
2425
2440
  <CameraView
@@ -17,9 +17,10 @@
17
17
  * (false comfort exactly where OOM happens). Falls back to 1500/2200 if the
18
18
  * RAM read is unavailable.
19
19
  *
20
- * Backed by the `getMemoryFootprintMB()` native module (iOS: `task_info`
21
- * `phys_footprint`; Android: `/proc/self/statm` RSS — the SAME number the C++
22
- * `[memstat]` logs report). Returns -1 if the native call fails.
20
+ * Backed by the `getMemoryFootprintMB()` native module (iOS:
21
+ * `task_info(TASK_VM_INFO)` `phys_footprint`; Android: `/proc/self/statm` RSS
22
+ * resident pages, unthrottled the SAME number the C++ `[memstat]` logs
23
+ * report). Returns -1 if the native call fails.
23
24
  *
24
25
  * Mount this pill inside a `settings.debug`-gated branch — it
25
26
  * polls native every 500 ms and is unwanted in production builds.