react-native-image-stitcher 0.7.0 → 0.8.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 +180 -1
- package/android/build.gradle +35 -1
- package/android/src/main/cpp/CMakeLists.txt +64 -2
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +30 -11
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +4 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
- package/cpp/stitcher_frame_data.hpp +141 -0
- package/cpp/stitcher_frame_jsi.cpp +214 -0
- package/cpp/stitcher_frame_jsi.hpp +108 -0
- package/cpp/stitcher_proxy_jsi.cpp +109 -0
- package/cpp/stitcher_proxy_jsi.hpp +46 -0
- package/cpp/stitcher_worklet_dispatch.cpp +103 -0
- package/cpp/stitcher_worklet_dispatch.hpp +71 -0
- package/cpp/stitcher_worklet_registry.cpp +81 -0
- package/cpp/stitcher_worklet_registry.hpp +136 -0
- package/dist/camera/Camera.d.ts +62 -12
- package/dist/camera/Camera.js +30 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -1
- package/dist/stitching/StitcherFrame.d.ts +170 -0
- package/dist/stitching/StitcherFrame.js +4 -0
- package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
- package/dist/stitching/StitcherWorkletRegistry.js +78 -0
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/dist/stitching/useFrameProcessor.d.ts +119 -0
- package/dist/stitching/useFrameProcessor.js +196 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -10
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
- package/package.json +1 -1
- package/src/camera/Camera.tsx +93 -28
- package/src/index.ts +16 -0
- package/src/stitching/StitcherFrame.ts +197 -0
- package/src/stitching/StitcherWorkletRegistry.ts +156 -0
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- package/src/stitching/useFrameProcessor.ts +226 -0
|
@@ -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 "StitcherFrameHostObject.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
|
+
// StitcherFrameHostObject.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
|
+
StitcherFrameHostObject *hostObj =
|
|
214
|
+
[StitcherFrameHostObject 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,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
|