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.
- package/CHANGELOG.md +154 -0
- package/RNImageStitcher.podspec +26 -1
- package/android/build.gradle +20 -0
- package/android/src/main/cpp/CMakeLists.txt +46 -3
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +436 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +711 -6
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +338 -0
- package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
- package/cpp/camera_frame_jsi.cpp +357 -0
- package/cpp/camera_frame_jsi.hpp +108 -0
- package/cpp/stitcher_proxy_jsi.cpp +140 -0
- package/cpp/stitcher_proxy_jsi.hpp +62 -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 +77 -0
- package/dist/camera/ARCameraView.js +90 -1
- package/dist/camera/Camera.d.ts +63 -4
- package/dist/camera/Camera.js +2 -2
- package/dist/camera/CaptureMemoryPill.d.ts +4 -3
- package/dist/camera/CaptureMemoryPill.js +4 -3
- package/dist/index.d.ts +2 -1
- package/dist/stitching/ARFrameMeta.d.ts +100 -0
- package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
- package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
- package/dist/stitching/CameraFrame.js +4 -0
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/dist/stitching/useStitcherWorklet.d.ts +4 -4
- package/dist/stitching/useStitcherWorklet.js +4 -4
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +137 -2
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +83 -0
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +336 -40
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -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 +211 -2
- package/src/camera/Camera.tsx +81 -4
- package/src/camera/CaptureMemoryPill.tsx +4 -3
- package/src/index.ts +7 -3
- package/src/stitching/ARFrameMeta.ts +107 -0
- package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- 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.
|
|
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",
|