react-native-image-stitcher 0.16.2 → 0.18.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 (52) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/RNImageStitcher.podspec +26 -1
  3. package/android/build.gradle +20 -0
  4. package/android/src/main/cpp/CMakeLists.txt +46 -3
  5. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +436 -0
  6. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +711 -6
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +338 -0
  11. package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
  12. package/cpp/camera_frame_jsi.cpp +357 -0
  13. package/cpp/camera_frame_jsi.hpp +108 -0
  14. package/cpp/stitcher_proxy_jsi.cpp +140 -0
  15. package/cpp/stitcher_proxy_jsi.hpp +62 -0
  16. package/cpp/stitcher_worklet_dispatch.cpp +103 -0
  17. package/cpp/stitcher_worklet_dispatch.hpp +71 -0
  18. package/cpp/stitcher_worklet_registry.cpp +91 -0
  19. package/cpp/stitcher_worklet_registry.hpp +146 -0
  20. package/dist/camera/ARCameraView.d.ts +77 -0
  21. package/dist/camera/ARCameraView.js +90 -1
  22. package/dist/camera/Camera.d.ts +63 -4
  23. package/dist/camera/Camera.js +2 -2
  24. package/dist/camera/CaptureMemoryPill.d.ts +4 -3
  25. package/dist/camera/CaptureMemoryPill.js +4 -3
  26. package/dist/index.d.ts +2 -1
  27. package/dist/stitching/ARFrameMeta.d.ts +100 -0
  28. package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
  29. package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
  30. package/dist/stitching/CameraFrame.js +4 -0
  31. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  32. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  33. package/dist/stitching/useStitcherWorklet.d.ts +4 -4
  34. package/dist/stitching/useStitcherWorklet.js +4 -4
  35. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
  36. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +137 -2
  37. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +83 -0
  38. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
  39. package/ios/Sources/RNImageStitcher/RNSARSession.swift +336 -40
  40. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  41. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  42. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  43. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +160 -0
  44. package/package.json +1 -1
  45. package/src/camera/ARCameraView.tsx +211 -2
  46. package/src/camera/Camera.tsx +81 -4
  47. package/src/camera/CaptureMemoryPill.tsx +4 -3
  48. package/src/index.ts +7 -3
  49. package/src/stitching/ARFrameMeta.ts +107 -0
  50. package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
  51. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
  52. package/src/stitching/useStitcherWorklet.ts +9 -9
@@ -0,0 +1,313 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // RNSARWorkletRuntime.mm — Obj-C++ implementation. See the header
4
+ // for the API contract. This file owns:
5
+ //
6
+ // - The dispatch queue the worklet runtime is pinned to
7
+ // - The `std::shared_ptr<RNWorklet::JsiWorkletContext>` itself
8
+ // - The registry of host worklets (Phase 4 wiring will populate
9
+ // this via a JSI plugin entry point)
10
+ //
11
+ // Phase 3b scope: construct the context + expose the API. No
12
+ // dispatch logic yet — `dispatchFrame:pose:` is a stub. Phase 3c
13
+ // fills in (a) the host-object construction + worklet invocation,
14
+ // (b) the first-party stitching callback, (c) the migration in
15
+ // `RNSARSession.delegate`.
16
+ //
17
+ // ## Singleton lifetime note (for Leaks-tool readers)
18
+ //
19
+ // `+ shared` uses `dispatch_once`, so the singleton lives for the
20
+ // process lifetime — same pattern as most Obj-C singletons. This
21
+ // means the dispatch queue (created in `init`) + the JsiWorkletContext
22
+ // (constructed lazily in `installIfNeeded`) + the `workletCallInvoker`
23
+ // lambda that captures the queue are ALL retained until process
24
+ // termination. Xcode Instruments → Leaks will flag this as "leaked
25
+ // allocation rooted at the singleton" — that's noise, not a real leak
26
+ // (process termination reclaims it). Phase 3c will keep this shape.
27
+
28
+ #import "RNSARWorkletRuntime.h"
29
+ #import "CameraFrameHostObject.h"
30
+
31
+ #import <Foundation/Foundation.h>
32
+ #import <os/log.h>
33
+
34
+ #include <jsi/jsi.h>
35
+ // worklets-core headers — use quotes-include since the pod
36
+ // publishes them via HEADER_SEARCH_PATHS, not as a framework
37
+ // module map. Same pattern KeyframeGateFrameProcessor.mm uses
38
+ // for vision-camera headers (which are reachable via <angle>
39
+ // only because vc's podspec sets `define_module` differently).
40
+ #include "WKTJsiWorkletContext.h"
41
+ #include "WKTJsiWorklet.h"
42
+
43
+ #include "stitcher_worklet_registry.hpp"
44
+
45
+ #include <exception>
46
+ #include <memory>
47
+ #include <utility>
48
+ #include <vector>
49
+
50
+ // Forward-declare `RNSARFramePose` — same pattern as
51
+ // CameraFrameHostObject.mm. We don't read its fields here in
52
+ // Phase 3b (the stub doesn't unpack the pose), but Phase 3c will.
53
+ @class RNSARFramePose;
54
+
55
+ @implementation RNSARWorkletRuntime {
56
+ /// Dispatch queue the worklet runtime's `workletCallInvoker`
57
+ /// posts onto. Serial; `DISPATCH_QUEUE_SERIAL` matches the
58
+ /// existing `IncrementalStitcher::workQueue` cost envelope
59
+ /// (one-at-a-time frame ingest).
60
+ ///
61
+ /// Phase 3c will configure `ARSession.delegateQueue` to point
62
+ /// at the same queue so the delegate fires on the worklet
63
+ /// thread — eliminates a thread hop per frame + makes the
64
+ /// "first-party first, host worklets after" ordering trivial
65
+ /// to enforce (all on one queue).
66
+ dispatch_queue_t _dispatchQueue;
67
+
68
+ /// The wrapped worklet-runtime context. Constructed lazily on
69
+ /// `-installIfNeeded`; held for the singleton's lifetime
70
+ /// (process-wide).
71
+ std::shared_ptr<RNWorklet::JsiWorkletContext> _ctx;
72
+
73
+ /// Single-flight install guard. `BOOL` is sufficient because
74
+ /// `-installIfNeeded` synchronises on `_installLock` below.
75
+ BOOL _installed;
76
+
77
+ /// Lock for `_installed` + `_ctx`. Construction may race with
78
+ /// concurrent first-mount calls from multiple `<Camera>`
79
+ /// instances; serialise to ensure exactly-once init.
80
+ NSLock *_installLock;
81
+
82
+ // Phase 4 will add the host-worklet registry here. Storage
83
+ // shape (NSMutableArray of boxed shared_ptrs vs C++ vector
84
+ // ivar) is intentionally NOT pre-committed in Phase 3b — let
85
+ // the JSI plugin's actual register/unregister implementation
86
+ // pick the natural shape.
87
+
88
+ /// Phase 3c — first-party callback installed by RNSARSession.
89
+ /// Invoked synchronously on the caller thread per AR frame.
90
+ /// Cleared on RNSARSession.stop() to avoid retain cycles.
91
+ ///
92
+ /// Atomic property protects against the delegate firing
93
+ /// concurrently with a setFirstPartyCallback: call on a
94
+ /// different thread (rare but possible: setter on main thread
95
+ /// from RNSARSession.start while a delayed delegate frame
96
+ /// arrives).
97
+ RNSARFirstPartyCallback _firstPartyCallback;
98
+
99
+ /// Lock for `_firstPartyCallback` reads + writes. The
100
+ /// `_installLock` above is dispatch-queue-scoped (install);
101
+ /// callback rotation is a separate concern.
102
+ NSLock *_callbackLock;
103
+ }
104
+
105
+ + (instancetype)shared {
106
+ static RNSARWorkletRuntime *sInstance;
107
+ static dispatch_once_t once;
108
+ dispatch_once(&once, ^{ sInstance = [[self alloc] init]; });
109
+ return sInstance;
110
+ }
111
+
112
+ - (instancetype)init {
113
+ if ((self = [super init])) {
114
+ _dispatchQueue = dispatch_queue_create(
115
+ "io.imagestitcher.ar-worklet-runtime", DISPATCH_QUEUE_SERIAL);
116
+ _installed = NO;
117
+ _installLock = [[NSLock alloc] init];
118
+ _callbackLock = [[NSLock alloc] init];
119
+ _firstPartyCallback = nil;
120
+ }
121
+ return self;
122
+ }
123
+
124
+ - (void)setFirstPartyCallback:(RNSARFirstPartyCallback)callback {
125
+ [_callbackLock lock];
126
+ // Copy the block to move it from stack to heap (ARC handles
127
+ // the copy semantics for blocks assigned to strong ivars).
128
+ _firstPartyCallback = [callback copy];
129
+ [_callbackLock unlock];
130
+ }
131
+
132
+ - (void)installIfNeeded {
133
+ [_installLock lock];
134
+ if (_installed) {
135
+ [_installLock unlock];
136
+ return;
137
+ }
138
+
139
+ // Build the `workletCallInvoker`. `RNWorklet::JsiWorkletContext`
140
+ // accepts a `std::function<void(std::function<void()>&&)>` that
141
+ // posts a task onto whatever thread the runtime should execute
142
+ // on. We post onto `_dispatchQueue` (a serial GCD queue).
143
+ //
144
+ // The captured `fp` is moved into a `std::shared_ptr` so the
145
+ // dispatch_async block (which can only capture copyable types)
146
+ // can hold + invoke it. Without the shared_ptr indirection
147
+ // we'd hit `std::function` copy-construction on the
148
+ // non-copyable forward closure.
149
+ dispatch_queue_t queue = _dispatchQueue;
150
+ auto invoker = [queue](std::function<void()>&& fp) {
151
+ auto fpHolder = std::make_shared<std::function<void()>>(std::move(fp));
152
+ dispatch_async(queue, ^{ (*fpHolder)(); });
153
+ };
154
+
155
+ _ctx = std::make_shared<RNWorklet::JsiWorkletContext>(
156
+ "stitcher.ar", std::move(invoker));
157
+ _installed = YES;
158
+ [_installLock unlock];
159
+ }
160
+
161
+ - (BOOL)isInstalled {
162
+ [_installLock lock];
163
+ BOOL result = _installed;
164
+ [_installLock unlock];
165
+ return result;
166
+ }
167
+
168
+ - (void)dispatchFrame:(ARFrame *)arFrame pose:(RNSARFramePose *)pose {
169
+ // ── Phase 3c — first-party (synchronous on caller thread) ────
170
+ //
171
+ // The callback (installed by RNSARSession.start) wraps the
172
+ // existing `incrementalConsumer.consumeFrame(...)` call path,
173
+ // so net behavior is byte-identical to the v0.7.x direct call.
174
+ //
175
+ // **Why first-party runs on the CALLER thread (not the worklet
176
+ // thread):** ARKit's pool reuse contract requires the pixel
177
+ // buffer to be consumed before this method returns. The Swift
178
+ // consumer does that synchronously inside `consumeFrame(...)`
179
+ // (converts NV12 → cv::Mat synchronously, then defers heavier
180
+ // work to its own queue). If we posted the callback onto
181
+ // `_dispatchQueue`, the delegate would return before
182
+ // `consumeFrame` ran, ARKit could reclaim the buffer, and we'd
183
+ // get torn frames.
184
+ //
185
+ // Pull the callback under the lock so a concurrent
186
+ // `setFirstPartyCallback:` doesn't race with our invocation.
187
+ [_callbackLock lock];
188
+ RNSARFirstPartyCallback cb = _firstPartyCallback;
189
+ [_callbackLock unlock];
190
+ if (cb != nil) {
191
+ cb(arFrame, pose);
192
+ }
193
+
194
+ // ── Phase 4b — host-worklet fan-out (async on worklet thread) ──
195
+ //
196
+ // Snapshot the native registry. Fast-path early-exit when no
197
+ // host worklets are registered — saves the host-object alloc
198
+ // + dispatch_async hop on every frame (the common case in
199
+ // first-party-only deployments).
200
+ auto invokers = retailens::StitcherWorkletRegistry::shared().snapshot();
201
+ if (invokers.empty()) {
202
+ return;
203
+ }
204
+
205
+ // Construction must happen on the caller thread. The
206
+ // `IOSPixelBufferReader` ctor takes a `CFBridgingRetain(arFrame)`
207
+ // so the underlying CVPixelBuffer stays alive until the host
208
+ // object's `invalidate` runs. ARKit's pool will throttle the
209
+ // *next* frame's delegate call while we hold this retain
210
+ // (acceptable for Phase 4b minimum-viable; a per-frame buffer
211
+ // copy is a known optimization for later if throughput
212
+ // suffers).
213
+ CameraFrameHostObject *hostObj =
214
+ [CameraFrameHostObject fromARFrame:arFrame pose:pose];
215
+
216
+ // Hand the host object's jsi::HostObject shared_ptr (boxed as
217
+ // void*) into the lambda. The lambda will:
218
+ // 1. Cast back to `std::shared_ptr<jsi::HostObject>*`
219
+ // 2. Construct the JS-side `jsi::Object` from the host object
220
+ // 3. Invoke each registered WorkletInvoker with the JS-side
221
+ // object as its single argument
222
+ // 4. Delete the boxed shared_ptr
223
+ // 5. Invalidate the host object on caller-side retained ref
224
+ //
225
+ // The dispatch is via worklets-core's `JsiWorkletContext::
226
+ // invokeOnWorkletThread` — internally posts onto our serial
227
+ // `_dispatchQueue` via the `workletCallInvoker` we set up in
228
+ // `installIfNeeded`.
229
+ //
230
+ // `hostObj` (the Obj-C facade) is captured by the block; ARC
231
+ // retains it for the block's lifetime, so the host object
232
+ // outlives the dispatch. We invalidate AFTER all worklets
233
+ // return.
234
+ void *hostObjPtr = [hostObj jsiHostObjectPtr];
235
+ if (hostObjPtr == NULL) {
236
+ // Host object construction failed (e.g., ARFrame was nil).
237
+ // Skip fan-out.
238
+ os_log_error(OS_LOG_DEFAULT,
239
+ "[RNSARWorkletRuntime] host object jsiHostObjectPtr was NULL; "
240
+ "skipping host-worklet fan-out for this frame.");
241
+ return;
242
+ }
243
+
244
+ if (_ctx == nullptr) {
245
+ // installIfNeeded wasn't called. This shouldn't happen
246
+ // because RNSARSession.start calls installIfNeeded before
247
+ // any frames arrive, but guard defensively.
248
+ os_log_error(OS_LOG_DEFAULT,
249
+ "[RNSARWorkletRuntime] _ctx is nullptr in dispatchFrame; "
250
+ "did installIfNeeded run? Skipping host-worklet fan-out.");
251
+ // Leaked: hostObjPtr (boxed shared_ptr). Reclaim it here so
252
+ // we don't leak even on the defensive path.
253
+ delete static_cast<std::shared_ptr<facebook::jsi::HostObject>*>(hostObjPtr);
254
+ return;
255
+ }
256
+
257
+ _ctx->invokeOnWorkletThread(
258
+ [invokers, hostObjPtr, hostObj](
259
+ RNWorklet::JsiWorkletContext* /*ctx*/,
260
+ facebook::jsi::Runtime& rt) {
261
+ // Reclaim the boxed shared_ptr. After this scope the
262
+ // unique_ptr automatically deletes the heap allocation
263
+ // even if the JSI call below throws.
264
+ std::unique_ptr<std::shared_ptr<facebook::jsi::HostObject>> spBox(
265
+ static_cast<std::shared_ptr<facebook::jsi::HostObject>*>(
266
+ hostObjPtr));
267
+
268
+ facebook::jsi::Object frameJsi =
269
+ facebook::jsi::Object::createFromHostObject(rt, *spBox);
270
+ // Pass the host object as a single argument. The
271
+ // worklet's signature is `(frame: StitcherFrame) =>
272
+ // void` — matches.
273
+ //
274
+ // Construct the argument value as a copy of the
275
+ // Object (jsi::Value(rt, obj) makes a fresh Value
276
+ // wrapping the same host object — refcounted by JSI).
277
+ facebook::jsi::Value frameVal(rt, frameJsi);
278
+
279
+ for (const auto& entry : invokers) {
280
+ if (!entry.invoker) continue;
281
+ try {
282
+ entry.invoker->call(rt, facebook::jsi::Value::undefined(),
283
+ &frameVal, 1);
284
+ } catch (const facebook::jsi::JSError& jsErr) {
285
+ // Per-worklet failure isolation: one host
286
+ // worklet throwing must NOT stop the lib's own
287
+ // path or other host worklets. Log + continue.
288
+ os_log_error(OS_LOG_DEFAULT,
289
+ "[RNSARWorkletRuntime] host worklet '%{public}s' "
290
+ "threw JS error: %{public}s",
291
+ entry.id.c_str(), jsErr.what());
292
+ } catch (const std::exception& e) {
293
+ os_log_error(OS_LOG_DEFAULT,
294
+ "[RNSARWorkletRuntime] host worklet '%{public}s' "
295
+ "threw native exception: %{public}s",
296
+ entry.id.c_str(), e.what());
297
+ } catch (...) {
298
+ os_log_error(OS_LOG_DEFAULT,
299
+ "[RNSARWorkletRuntime] host worklet '%{public}s' "
300
+ "threw unknown exception", entry.id.c_str());
301
+ }
302
+ }
303
+
304
+ // Drop the JSI references BEFORE invalidating the host
305
+ // object — `frameJsi` / `frameVal` go out of scope at
306
+ // end of lambda anyway, but be explicit. Then
307
+ // invalidate the Obj-C facade which releases the
308
+ // CFBridgingRetain'd ARFrame so ARKit's pool can recycle.
309
+ [hostObj invalidate];
310
+ });
311
+ }
312
+
313
+ @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.2",
3
+ "version": "0.18.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",