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,141 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // stitcher_frame_data.hpp — platform-agnostic backing data for the
4
+ // v0.8.0 `StitcherFrame` JSI host object.
5
+ //
6
+ // ## Why this lives here
7
+ //
8
+ // The JSI host object's `get()` dispatch logic is platform-specific
9
+ // (Obj-C++ on iOS includes `<jsi/jsi.h>` from React Native's
10
+ // CocoaPod; Android needs a more elaborate CMake setup to link
11
+ // against React Native's JSI library). But the *data* the host
12
+ // object exposes — pose, dimensions, the pixel-buffer reader
13
+ // indirection — is identical on both platforms. That data lives
14
+ // here so iOS / Android JSI dispatch code references one source.
15
+ //
16
+ // ## Memory model
17
+ //
18
+ // `PixelBufferReader` is an opaque interface; platform code (iOS
19
+ // `StitcherFrameHostObject.mm`; Android `stitcher_frame_jni.cpp`)
20
+ // implements it by wrapping the underlying `CVPixelBufferRef` /
21
+ // `ArImage*`. Lifetime: the reader holds a strong ref to its
22
+ // source for the entire host-object lifetime; releases on
23
+ // destruction (deterministic, RAII).
24
+ //
25
+ // `StitcherFrameData` is value-typed (cheap to copy; ~100 bytes).
26
+ // Construct on the worklet runtime's thread before each dispatch.
27
+
28
+ #pragma once
29
+
30
+ #include <cstddef>
31
+ #include <cstdint>
32
+ #include <memory>
33
+ #include <string>
34
+
35
+ namespace retailens {
36
+
37
+ /// Opaque interface for reading the underlying camera pixel data.
38
+ /// Platform code provides an implementation:
39
+ /// - iOS: wraps a `CVPixelBufferRef` (locks/unlocks base address
40
+ /// across copyTo, defers release until destruction).
41
+ /// - Android: wraps an ARCore `ArImage*` (handles plane access via
42
+ /// `ArImage_getPlaneData`, calls `ArImage_release` on destruct).
43
+ ///
44
+ /// **Thread-affinity contract:** implementations need not be
45
+ /// reentrant. An instance MAY be constructed on thread A
46
+ /// (typically the ARSession delegate queue) and used on thread B
47
+ /// (the worklet-runtime thread), provided the construction-thread
48
+ /// releases its `shared_ptr` reference before thread B uses the
49
+ /// reader. The `shared_ptr`'s atomic refcount serves as the
50
+ /// happens-before barrier — fields set in the constructor are
51
+ /// visible on the worklet thread once the construction-thread
52
+ /// drops its ref. Concurrent access from two threads simultaneously
53
+ /// is NOT supported.
54
+ class PixelBufferReader {
55
+ public:
56
+ virtual ~PixelBufferReader() = default;
57
+
58
+ /// Total byte size of the buffer the reader exposes. For Y-plane
59
+ /// access (the v0.8.0 default), this is `width * height`.
60
+ virtual std::size_t byteSize() const = 0;
61
+
62
+ /// Copy up to `maxBytes` of the underlying buffer into `dst`.
63
+ /// Returns bytes written. Returns 0 if reader is invalidated.
64
+ ///
65
+ /// Implementations MUST handle the case where `maxBytes < byteSize()`
66
+ /// (clip silently). This matches JS `ArrayBuffer.slice` semantics
67
+ /// even though the host object always allocates exactly `byteSize()`.
68
+ virtual std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) = 0;
69
+ };
70
+
71
+ /// Plain-old-data payload for one `StitcherFrame`. Fully extracted
72
+ /// at construction time (cheap fields) plus an opaque reader for
73
+ /// the lazy pixel access.
74
+ struct StitcherFrameData {
75
+ /// Discriminator. `"ar"` for AR-mode frames, `"vc"` for
76
+ /// vision-camera frames. Used by worklets to gate on AR-only
77
+ /// field access (translation, depth, anchors, tracking state).
78
+ /// Mirrored to the JS `source` field (standard discriminated-
79
+ /// union pattern).
80
+ std::string source;
81
+
82
+ /// Width / height of the camera image in pixels.
83
+ int32_t width = 0;
84
+ int32_t height = 0;
85
+
86
+ /// String pixel-format identifier; matches the JS
87
+ /// `StitcherFrame.pixelFormat` union: `"yuv"` / `"rgb"` /
88
+ /// `"unknown"`. Today's emitters always populate `"yuv"`
89
+ /// (NV12 on iOS, NV21 on Android).
90
+ std::string pixelFormat;
91
+
92
+ /// String orientation identifier; matches vision-camera's
93
+ /// `Frame.orientation`: `"portrait"`, `"portrait-upside-down"`,
94
+ /// `"landscape-left"`, `"landscape-right"`.
95
+ std::string orientation;
96
+
97
+ /// Monotonic timestamp in nanoseconds. AR mode: from
98
+ /// `ARFrame.timestamp` (CFAbsoluteTime, converted to ns).
99
+ /// Non-AR mode: from `vision-camera Frame.timestamp` (already ns).
100
+ double timestampNs = 0.0;
101
+
102
+ /// Pose rotation as quaternion `(x, y, z, w)`. Matches the
103
+ /// `q = q_yaw * q_pitch * q_roll` convention used elsewhere in
104
+ /// the lib (KeyframeGate, RNSARFramePose, AcceptedKeyframe).
105
+ double qx = 0.0;
106
+ double qy = 0.0;
107
+ double qz = 0.0;
108
+ double qw = 1.0;
109
+
110
+ /// Pose translation in metres (world coords). AR mode: from
111
+ /// `ARFrame.camera.transform`. Non-AR mode: undefined — the
112
+ /// `hasTranslation` flag is `false` and JS receives
113
+ /// `pose.translation === undefined`.
114
+ double tx = 0.0;
115
+ double ty = 0.0;
116
+ double tz = 0.0;
117
+ bool hasTranslation = false;
118
+
119
+ /// AR tracking state. Empty string (`""`) means "not
120
+ /// applicable" (the JS host object exposes `arTrackingState ===
121
+ /// undefined` in that case). Otherwise one of `"notAvailable"`,
122
+ /// `"limited"`, `"normal"`.
123
+ std::string arTrackingState;
124
+
125
+ /// Pixel data accessor. Always present (even for AR mode where
126
+ /// arDepth might be the more interesting buffer). See class
127
+ /// docstring for lifetime contract.
128
+ std::shared_ptr<PixelBufferReader> pixelReader;
129
+
130
+ // ── AR-only optional fields (not populated in v0.8.0; stubs) ──
131
+ // These are deferred to v0.8.1+ because the host worklets that
132
+ // would consume them aren't shipping in v0.8.0 either. Adding
133
+ // them here as plain data fields keeps the JSI host object code
134
+ // simple when they DO arrive.
135
+
136
+ /// arDepth, arAnchors stubs intentionally omitted — they're
137
+ /// fields the JSI dispatch will return `undefined` for in v0.8.0.
138
+ /// v0.8.1+ adds them here as `std::optional<ArDepth>` etc.
139
+ };
140
+
141
+ } // namespace retailens
@@ -0,0 +1,214 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // stitcher_frame_jsi.cpp — implementation of the shared C++ JSI
4
+ // host object. See stitcher_frame_jsi.hpp for class docs.
5
+
6
+ #include "stitcher_frame_jsi.hpp"
7
+
8
+ #include <string>
9
+ #include <utility>
10
+
11
+ namespace retailens {
12
+
13
+ using facebook::jsi::Array;
14
+ using facebook::jsi::Function;
15
+ using facebook::jsi::HostFunctionType;
16
+ using facebook::jsi::JSError;
17
+ using facebook::jsi::Object;
18
+ using facebook::jsi::PropNameID;
19
+ using facebook::jsi::Runtime;
20
+ using facebook::jsi::String;
21
+ using facebook::jsi::Value;
22
+
23
+ StitcherFrameJsiHostObject::StitcherFrameJsiHostObject(StitcherFrameData data)
24
+ : _data(std::move(data)), _isValid(true) {}
25
+
26
+ void StitcherFrameJsiHostObject::invalidate() {
27
+ _isValid = false;
28
+ // Release the pixel reader immediately so the underlying camera
29
+ // buffer can be reclaimed. ARKit's ARFrame uses a pooled
30
+ // CVPixelBuffer; holding past the dispatch scope causes
31
+ // back-pressure. ARCore's ArImage must be explicitly released
32
+ // for the next frame's acquire to succeed.
33
+ _data.pixelReader.reset();
34
+ }
35
+
36
+ std::vector<PropNameID> StitcherFrameJsiHostObject::getPropertyNames(
37
+ Runtime& rt) {
38
+ std::vector<PropNameID> names;
39
+ names.push_back(PropNameID::forUtf8(rt, "isValid"));
40
+ if (!_isValid) return names;
41
+
42
+ names.push_back(PropNameID::forUtf8(rt, "width"));
43
+ names.push_back(PropNameID::forUtf8(rt, "height"));
44
+ names.push_back(PropNameID::forUtf8(rt, "pixelFormat"));
45
+ names.push_back(PropNameID::forUtf8(rt, "orientation"));
46
+ names.push_back(PropNameID::forUtf8(rt, "timestamp"));
47
+ names.push_back(PropNameID::forUtf8(rt, "pose"));
48
+ names.push_back(PropNameID::forUtf8(rt, "source"));
49
+ names.push_back(PropNameID::forUtf8(rt, "toArrayBuffer"));
50
+ if (!_data.arTrackingState.empty()) {
51
+ names.push_back(PropNameID::forUtf8(rt, "arTrackingState"));
52
+ }
53
+ return names;
54
+ }
55
+
56
+ Value StitcherFrameJsiHostObject::get(Runtime& rt,
57
+ const PropNameID& propName) {
58
+ const std::string name = propName.utf8(rt);
59
+
60
+ if (name == "isValid") {
61
+ return Value(_isValid);
62
+ }
63
+ // Invalidated host objects expose only `isValid` (returns false).
64
+ // Every other access throws — matches vc FrameHostObject's contract.
65
+ // Lets worklets that incorrectly retain a frame across dispatch
66
+ // boundaries fail loudly rather than read garbage.
67
+ if (!_isValid) {
68
+ throw JSError(rt,
69
+ "[StitcherFrame] cannot access property '" + name +
70
+ "' after host object was invalidated. "
71
+ "Frame data is only valid for the duration of the worklet call.");
72
+ }
73
+
74
+ if (name == "width") return Value(static_cast<double>(_data.width));
75
+ if (name == "height") return Value(static_cast<double>(_data.height));
76
+ if (name == "pixelFormat") return String::createFromUtf8(rt, _data.pixelFormat);
77
+ if (name == "orientation") return String::createFromUtf8(rt, _data.orientation);
78
+ if (name == "timestamp") return Value(_data.timestampNs);
79
+ if (name == "source") return String::createFromUtf8(rt, _data.source);
80
+
81
+ if (name == "pose") {
82
+ Object pose(rt);
83
+ Array rotation(rt, 4);
84
+ rotation.setValueAtIndex(rt, 0, Value(_data.qx));
85
+ rotation.setValueAtIndex(rt, 1, Value(_data.qy));
86
+ rotation.setValueAtIndex(rt, 2, Value(_data.qz));
87
+ rotation.setValueAtIndex(rt, 3, Value(_data.qw));
88
+ pose.setProperty(rt, "rotation", rotation);
89
+ if (_data.hasTranslation) {
90
+ Array translation(rt, 3);
91
+ translation.setValueAtIndex(rt, 0, Value(_data.tx));
92
+ translation.setValueAtIndex(rt, 1, Value(_data.ty));
93
+ translation.setValueAtIndex(rt, 2, Value(_data.tz));
94
+ pose.setProperty(rt, "translation", translation);
95
+ }
96
+ return pose;
97
+ }
98
+
99
+ if (name == "arTrackingState") {
100
+ if (_data.arTrackingState.empty()) return Value::undefined();
101
+ return String::createFromUtf8(rt, _data.arTrackingState);
102
+ }
103
+
104
+ if (name == "toArrayBuffer") {
105
+ // Capture a weak self so the lambda doesn't extend the host
106
+ // object's lifetime beyond what the runtime intended. When the
107
+ // runtime releases its shared_ptr (after dispatch), the weak
108
+ // ref expires and toArrayBuffer() throws on next call.
109
+ auto weakSelf = std::weak_ptr<StitcherFrameJsiHostObject>(shared_from_this());
110
+ HostFunctionType fn = [weakSelf](Runtime& runtime,
111
+ const Value& thisVal,
112
+ const Value* args,
113
+ size_t count) -> Value {
114
+ auto self = weakSelf.lock();
115
+ if (!self || !self->_isValid || !self->_data.pixelReader) {
116
+ throw JSError(runtime,
117
+ "[StitcherFrame] toArrayBuffer() called on invalidated frame "
118
+ "(host object was released after the worklet dispatch returned)");
119
+ }
120
+ const std::size_t bufSize = self->_data.pixelReader->byteSize();
121
+
122
+ // Per-runtime ArrayBuffer cache. Pattern from vision-camera's
123
+ // FrameHostObject.mm:124-149. Without this, every per-frame
124
+ // worklet call to toArrayBuffer() allocates a fresh ~2MB
125
+ // vector (1920x1080 NV12 Y-plane) — ~60 MB/s of GC churn at
126
+ // 30 fps that defeats the point of having a worklet at all.
127
+ // Caching on `runtime.global()` is safe because (a) each
128
+ // worklet runtime has its own global, and (b) every call
129
+ // overwrites the cached buffer before returning, so there's
130
+ // no time-window for cross-worklet data leaks.
131
+ static constexpr const char* kCacheKey =
132
+ "__stitcherFrameArrayBufferCache";
133
+ auto global = runtime.global();
134
+ std::shared_ptr<OwningPixelBuffer> owning;
135
+
136
+ bool needsAlloc = true;
137
+ if (global.hasProperty(runtime, kCacheKey)) {
138
+ auto cached = global.getPropertyAsObject(runtime, kCacheKey);
139
+ if (cached.isArrayBuffer(runtime)) {
140
+ auto cachedBuffer = cached.getArrayBuffer(runtime);
141
+ // Hermes JSI exposes the underlying MutableBuffer via the
142
+ // shared_ptr the ArrayBuffer was constructed with — but
143
+ // there's no public getter once handed to JSI. We retain
144
+ // a parallel shared_ptr below via a hidden global slot.
145
+ if (cachedBuffer.size(runtime) == bufSize) {
146
+ // Size matches — reuse. Pull the parallel
147
+ // OwningPixelBuffer ref out of its hidden slot.
148
+ static constexpr const char* kRefKey =
149
+ "__stitcherFrameArrayBufferCacheRef";
150
+ if (global.hasProperty(runtime, kRefKey)) {
151
+ // The hidden ref is stored as a HostObject wrapping
152
+ // the shared_ptr; pull it back. See alloc path below.
153
+ auto refObj = global.getPropertyAsObject(runtime, kRefKey);
154
+ if (refObj.isHostObject(runtime)) {
155
+ struct RefHolder : facebook::jsi::HostObject {
156
+ std::shared_ptr<OwningPixelBuffer> buf;
157
+ explicit RefHolder(std::shared_ptr<OwningPixelBuffer> b)
158
+ : buf(std::move(b)) {}
159
+ };
160
+ auto holder =
161
+ refObj.getHostObject<RefHolder>(runtime);
162
+ if (holder && holder->buf) {
163
+ owning = holder->buf;
164
+ needsAlloc = false;
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ if (needsAlloc) {
173
+ owning = std::make_shared<OwningPixelBuffer>(bufSize);
174
+ // Store the ArrayBuffer + a parallel ref-holder on global.
175
+ // The ArrayBuffer's MutableBuffer is the same `owning`; the
176
+ // ref-holder lets us pull `owning` back out on cache hits.
177
+ global.setProperty(runtime, kCacheKey,
178
+ facebook::jsi::ArrayBuffer(runtime, owning));
179
+ struct RefHolder : facebook::jsi::HostObject {
180
+ std::shared_ptr<OwningPixelBuffer> buf;
181
+ explicit RefHolder(std::shared_ptr<OwningPixelBuffer> b)
182
+ : buf(std::move(b)) {}
183
+ };
184
+ global.setProperty(runtime, "__stitcherFrameArrayBufferCacheRef",
185
+ facebook::jsi::Object::createFromHostObject(runtime,
186
+ std::make_shared<RefHolder>(owning)));
187
+ }
188
+
189
+ std::size_t written =
190
+ self->_data.pixelReader->copyTo(owning->bytes(), bufSize);
191
+ if (written == 0 && bufSize > 0) {
192
+ throw JSError(runtime,
193
+ "[StitcherFrame] toArrayBuffer() pixel copy failed "
194
+ "(reader returned 0 bytes — likely the underlying "
195
+ "camera buffer was NULL or unreadable; see native log)");
196
+ }
197
+
198
+ // Re-fetch the cached ArrayBuffer to return. Cheap (just a
199
+ // property lookup); avoids constructing a new jsi::ArrayBuffer
200
+ // that wraps the same MutableBuffer (which would be wasteful).
201
+ return global.getPropertyAsObject(runtime, kCacheKey)
202
+ .getArrayBuffer(runtime);
203
+ };
204
+ return Function::createFromHostFunction(rt,
205
+ PropNameID::forUtf8(rt, "toArrayBuffer"), 0, fn);
206
+ }
207
+
208
+ // Unknown property — return undefined (matches JS object
209
+ // semantics). Worklets accessing arDepth / arAnchors hit this
210
+ // path in v0.8.0 (stubbed to undefined; populated in v0.8.1+).
211
+ return Value::undefined();
212
+ }
213
+
214
+ } // namespace retailens
@@ -0,0 +1,108 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // stitcher_frame_jsi.hpp — shared C++ JSI host object for the v0.8.0
4
+ // `StitcherFrame` contract. Compiles on both iOS and Android; each
5
+ // platform provides only the PixelBufferReader implementation and
6
+ // the construction call site (Obj-C++ on iOS; JNI on Android).
7
+ //
8
+ // The JSI dispatch logic (`get` / `getPropertyNames`) is identical
9
+ // across platforms — the host object exposes the same JS-visible
10
+ // surface regardless of frame source, by design of the
11
+ // `StitcherFrame` contract.
12
+
13
+ #pragma once
14
+
15
+ #include <jsi/jsi.h>
16
+
17
+ #include <cstdint>
18
+ #include <memory>
19
+ #include <vector>
20
+
21
+ #include "stitcher_frame_data.hpp"
22
+
23
+ namespace retailens {
24
+
25
+ /// Owning byte buffer that satisfies the `jsi::MutableBuffer`
26
+ /// contract. Backs the `ArrayBuffer` returned by
27
+ /// `StitcherFrame.toArrayBuffer()`.
28
+ ///
29
+ /// **Lifetime:** tied to the JSI ArrayBuffer's GC root. The buffer
30
+ /// persists until Hermes / JSC garbage-collects the ArrayBuffer
31
+ /// (not deterministic with frame timing). To avoid per-frame
32
+ /// allocation churn (30 fps × 2 MB = ~60 MB/s in the AR-mode pan
33
+ /// case), `toArrayBuffer()` caches a single instance per JSI
34
+ /// runtime on `runtime.global()` and reuses it across frames —
35
+ /// reallocating only when the requested size changes. Pattern
36
+ /// adopted from vision-camera's `FrameHostObject.mm:124-149`.
37
+ class OwningPixelBuffer : public facebook::jsi::MutableBuffer {
38
+ public:
39
+ explicit OwningPixelBuffer(std::size_t sizeBytes)
40
+ : _storage(sizeBytes, 0) {}
41
+
42
+ // jsi::MutableBuffer interface
43
+ uint8_t* data() override { return _storage.data(); }
44
+ size_t size() const override { return _storage.size(); }
45
+
46
+ /// Direct accessor for the native side to memcpy into before
47
+ /// handing the buffer to JSI. Not part of jsi::MutableBuffer.
48
+ uint8_t* bytes() { return _storage.data(); }
49
+
50
+ private:
51
+ std::vector<uint8_t> _storage;
52
+ };
53
+
54
+ /// v0.8.0 — JSI host object representing one `StitcherFrame`. See
55
+ /// `src/stitching/StitcherFrame.ts` for the JS-visible contract.
56
+ ///
57
+ /// Construct on the worklet runtime's thread, hand to
58
+ /// `jsi::Object::createFromHostObject`, dispatch to a registered
59
+ /// worklet, then invalidate (typically immediately after dispatch
60
+ /// returns — the underlying pixel buffer's lifetime is bound to
61
+ /// the calling AR-session callback scope).
62
+ class StitcherFrameJsiHostObject
63
+ : public facebook::jsi::HostObject,
64
+ public std::enable_shared_from_this<StitcherFrameJsiHostObject> {
65
+ public:
66
+ /// Factory. ALWAYS use this — `shared_from_this()` (called inside
67
+ /// `get` for `toArrayBuffer`) requires the instance to be owned
68
+ /// by a `shared_ptr` from the moment of construction. A raw
69
+ /// `new StitcherFrameJsiHostObject(...)` would throw
70
+ /// `std::bad_weak_ptr` on the first `toArrayBuffer()` JSI call.
71
+ ///
72
+ /// Private constructor + public factory enforces this at the
73
+ /// language level; callers can't accidentally construct without
74
+ /// `std::make_shared`.
75
+ static std::shared_ptr<StitcherFrameJsiHostObject> create(
76
+ StitcherFrameData data) {
77
+ // `std::make_shared` would require a public ctor; route through
78
+ // a tagged-dispatch private constructor instead.
79
+ struct EnableMakeShared : StitcherFrameJsiHostObject {
80
+ explicit EnableMakeShared(StitcherFrameData d)
81
+ : StitcherFrameJsiHostObject(std::move(d)) {}
82
+ };
83
+ return std::make_shared<EnableMakeShared>(std::move(data));
84
+ }
85
+
86
+ // jsi::HostObject interface
87
+ facebook::jsi::Value get(
88
+ facebook::jsi::Runtime& rt,
89
+ const facebook::jsi::PropNameID& name) override;
90
+ std::vector<facebook::jsi::PropNameID> getPropertyNames(
91
+ facebook::jsi::Runtime& rt) override;
92
+
93
+ /// Mark the host object's backing data as no longer accessible.
94
+ /// Subsequent JSI reads of valid-required properties throw.
95
+ /// Releases the pixel reader (and its underlying ARFrame /
96
+ /// ArImage retain) immediately. Idempotent.
97
+ void invalidate();
98
+
99
+ bool isValid() const { return _isValid; }
100
+
101
+ private:
102
+ explicit StitcherFrameJsiHostObject(StitcherFrameData data);
103
+
104
+ StitcherFrameData _data;
105
+ bool _isValid;
106
+ };
107
+
108
+ } // namespace retailens
@@ -0,0 +1,109 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // stitcher_proxy_jsi.cpp — shared C++ JSI host object that exposes
4
+ // the v0.8.0 `__stitcherProxy` API to JS. See header for the
5
+ // surface + threading rules.
6
+
7
+ #include "stitcher_proxy_jsi.hpp"
8
+
9
+ #include "stitcher_worklet_registry.hpp"
10
+
11
+ #include <memory>
12
+ #include <string>
13
+ #include <vector>
14
+
15
+ namespace retailens {
16
+
17
+ namespace {
18
+
19
+ class StitcherProxyHostObject : public facebook::jsi::HostObject {
20
+ public:
21
+ facebook::jsi::Value get(facebook::jsi::Runtime& rt,
22
+ const facebook::jsi::PropNameID& propName) override {
23
+ using facebook::jsi::Function;
24
+ using facebook::jsi::JSError;
25
+ using facebook::jsi::PropNameID;
26
+ using facebook::jsi::String;
27
+ using facebook::jsi::Value;
28
+
29
+ const std::string name = propName.utf8(rt);
30
+
31
+ if (name == "install") {
32
+ // install(workletFn) → string ID. The host function captures
33
+ // nothing; the registry is a process-scope singleton.
34
+ auto fn = [](facebook::jsi::Runtime& runtime,
35
+ const Value& /*thisVal*/, const Value* args,
36
+ size_t count) -> Value {
37
+ if (count < 1) {
38
+ throw JSError(runtime,
39
+ "[StitcherProxy] install() requires 1 argument (worklet "
40
+ "function); got 0");
41
+ }
42
+ if (!args[0].isObject() ||
43
+ !args[0].getObject(runtime).isFunction(runtime)) {
44
+ throw JSError(runtime,
45
+ "[StitcherProxy] install() argument must be a function "
46
+ "decorated with 'worklet'");
47
+ }
48
+ // The WorkletInvoker ctor extracts the worklet metadata
49
+ // (`__workletHash` etc.) and throws if absent. Propagate
50
+ // to JS so misuse fails loudly.
51
+ std::string id =
52
+ StitcherWorkletRegistry::shared().install(runtime, args[0]);
53
+ return String::createFromUtf8(runtime, id);
54
+ };
55
+ return Function::createFromHostFunction(
56
+ rt, PropNameID::forUtf8(rt, "install"), 1, std::move(fn));
57
+ }
58
+
59
+ if (name == "uninstall") {
60
+ auto fn = [](facebook::jsi::Runtime& runtime,
61
+ const Value& /*thisVal*/, const Value* args,
62
+ size_t count) -> Value {
63
+ if (count < 1 || !args[0].isString()) {
64
+ // No throw — match the JS-side registry's permissive
65
+ // uninstall semantics; missing/bad ID is a no-op.
66
+ return Value::undefined();
67
+ }
68
+ std::string id = args[0].getString(runtime).utf8(runtime);
69
+ StitcherWorkletRegistry::shared().uninstall(id);
70
+ return Value::undefined();
71
+ };
72
+ return Function::createFromHostFunction(
73
+ rt, PropNameID::forUtf8(rt, "uninstall"), 1, std::move(fn));
74
+ }
75
+
76
+ if (name == "count") {
77
+ auto fn = [](facebook::jsi::Runtime& runtime,
78
+ const Value& /*thisVal*/, const Value* /*args*/,
79
+ size_t /*count*/) -> Value {
80
+ return Value(static_cast<double>(
81
+ StitcherWorkletRegistry::shared().count()));
82
+ };
83
+ return Function::createFromHostFunction(
84
+ rt, PropNameID::forUtf8(rt, "count"), 0, std::move(fn));
85
+ }
86
+
87
+ return Value::undefined();
88
+ }
89
+
90
+ std::vector<facebook::jsi::PropNameID> getPropertyNames(
91
+ facebook::jsi::Runtime& rt) override {
92
+ std::vector<facebook::jsi::PropNameID> names;
93
+ names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "install"));
94
+ names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "uninstall"));
95
+ names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "count"));
96
+ return names;
97
+ }
98
+ };
99
+
100
+ } // namespace
101
+
102
+ void installStitcherProxy(facebook::jsi::Runtime& runtime) {
103
+ auto proxy = std::make_shared<StitcherProxyHostObject>();
104
+ runtime.global().setProperty(
105
+ runtime, "__stitcherProxy",
106
+ facebook::jsi::Object::createFromHostObject(runtime, proxy));
107
+ }
108
+
109
+ } // namespace retailens
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // stitcher_proxy_jsi.hpp — shared C++ JSI host object that's exposed
4
+ // as `globalThis.__stitcherProxy` on the main JS runtime.
5
+ //
6
+ // Originally inlined as an anonymous-namespace class in iOS'
7
+ // `StitcherJsiInstaller.mm` (v0.8.0 Phase 4b.i). Phase 4b.ii lifts
8
+ // it into shared C++ so the Android JNI installer reuses the same
9
+ // `install` / `uninstall` / `count` host functions verbatim — the
10
+ // JSI dispatch is identical across platforms (matches the
11
+ // `StitcherFrame` host object's design).
12
+ //
13
+ // Platform-specific code (Obj-C++ on iOS, JNI on Android) only
14
+ // owns the bootstrap: get a handle to the main JS runtime, then
15
+ // call `retailens::installStitcherProxy(runtime)`.
16
+ //
17
+ // ## Surface
18
+ //
19
+ // __stitcherProxy.install(workletFn) → string ID
20
+ // __stitcherProxy.uninstall(id) → undefined
21
+ // __stitcherProxy.count() → number (diagnostic)
22
+ //
23
+ // `install` wraps the worklet into a `RNWorklet::WorkletInvoker`
24
+ // and stores it in the process-scope C++
25
+ // `retailens::StitcherWorkletRegistry`. The AR worklet runtime
26
+ // (iOS' `RNSARWorkletRuntime`, Android's `StitcherWorkletRuntime`)
27
+ // reads from that registry to fan out per-frame invocations.
28
+
29
+ #pragma once
30
+
31
+ #include <jsi/jsi.h>
32
+
33
+ namespace retailens {
34
+
35
+ /// Install `globalThis.__stitcherProxy` on the supplied runtime.
36
+ /// Idempotent — re-installing overwrites the existing global with
37
+ /// a fresh host object; the underlying `StitcherWorkletRegistry`
38
+ /// state is unaffected.
39
+ ///
40
+ /// Thread: must be called from a thread that owns `runtime`.
41
+ /// Typically called once at lib bootstrap from a synchronous JS
42
+ /// bridge method (iOS: `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD`;
43
+ /// Android: `@ReactMethod(isBlockingSynchronousMethod = true)`).
44
+ void installStitcherProxy(facebook::jsi::Runtime& runtime);
45
+
46
+ } // namespace retailens