react-native-image-stitcher 0.7.1 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/android/build.gradle +35 -1
  3. package/android/src/main/cpp/CMakeLists.txt +64 -2
  4. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
  5. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +30 -11
  6. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +4 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
  8. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
  10. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
  11. package/cpp/stitcher_frame_data.hpp +141 -0
  12. package/cpp/stitcher_frame_jsi.cpp +214 -0
  13. package/cpp/stitcher_frame_jsi.hpp +108 -0
  14. package/cpp/stitcher_proxy_jsi.cpp +109 -0
  15. package/cpp/stitcher_proxy_jsi.hpp +46 -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 +81 -0
  19. package/cpp/stitcher_worklet_registry.hpp +136 -0
  20. package/dist/camera/Camera.d.ts +62 -12
  21. package/dist/camera/Camera.js +30 -15
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +11 -1
  24. package/dist/stitching/StitcherFrame.d.ts +170 -0
  25. package/dist/stitching/StitcherFrame.js +4 -0
  26. package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
  27. package/dist/stitching/StitcherWorkletRegistry.js +78 -0
  28. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  29. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  30. package/dist/stitching/useFrameProcessor.d.ts +119 -0
  31. package/dist/stitching/useFrameProcessor.js +196 -0
  32. package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -10
  33. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  34. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  35. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
  36. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
  37. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  38. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
  39. package/package.json +1 -1
  40. package/src/camera/Camera.tsx +93 -28
  41. package/src/index.ts +16 -0
  42. package/src/stitching/StitcherFrame.ts +197 -0
  43. package/src/stitching/StitcherWorkletRegistry.ts +156 -0
  44. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
  45. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
  46. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
  47. 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