react-native-image-stitcher 0.16.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/RNImageStitcher.podspec +26 -1
  3. package/android/build.gradle +20 -0
  4. package/android/src/main/cpp/CMakeLists.txt +46 -3
  5. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +436 -0
  6. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +711 -6
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +338 -0
  11. package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
  12. package/cpp/camera_frame_jsi.cpp +357 -0
  13. package/cpp/camera_frame_jsi.hpp +108 -0
  14. package/cpp/stitcher_proxy_jsi.cpp +140 -0
  15. package/cpp/stitcher_proxy_jsi.hpp +62 -0
  16. package/cpp/stitcher_worklet_dispatch.cpp +103 -0
  17. package/cpp/stitcher_worklet_dispatch.hpp +71 -0
  18. package/cpp/stitcher_worklet_registry.cpp +91 -0
  19. package/cpp/stitcher_worklet_registry.hpp +146 -0
  20. package/dist/camera/ARCameraView.d.ts +77 -0
  21. package/dist/camera/ARCameraView.js +90 -1
  22. package/dist/camera/Camera.d.ts +63 -4
  23. package/dist/camera/Camera.js +2 -2
  24. package/dist/camera/CaptureMemoryPill.d.ts +4 -3
  25. package/dist/camera/CaptureMemoryPill.js +4 -3
  26. package/dist/index.d.ts +2 -1
  27. package/dist/stitching/ARFrameMeta.d.ts +100 -0
  28. package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
  29. package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
  30. package/dist/stitching/CameraFrame.js +4 -0
  31. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  32. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  33. package/dist/stitching/useStitcherWorklet.d.ts +4 -4
  34. package/dist/stitching/useStitcherWorklet.js +4 -4
  35. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
  36. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +137 -2
  37. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +83 -0
  38. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
  39. package/ios/Sources/RNImageStitcher/RNSARSession.swift +336 -40
  40. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  41. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  42. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  43. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +160 -0
  44. package/package.json +1 -1
  45. package/src/camera/ARCameraView.tsx +211 -2
  46. package/src/camera/Camera.tsx +81 -4
  47. package/src/camera/CaptureMemoryPill.tsx +4 -3
  48. package/src/index.ts +7 -3
  49. package/src/stitching/ARFrameMeta.ts +107 -0
  50. package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
  51. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
  52. package/src/stitching/useStitcherWorklet.ts +9 -9
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  //
3
- // stitcher_frame_data.hpp — platform-agnostic backing data for the
3
+ // camera_frame_data.hpp — platform-agnostic backing data for the
4
4
  // v0.8.0 `StitcherFrame` JSI host object.
5
5
  //
6
6
  // ## Why this lives here
@@ -16,21 +16,24 @@
16
16
  // ## Memory model
17
17
  //
18
18
  // `PixelBufferReader` is an opaque interface; platform code (iOS
19
- // `StitcherFrameHostObject.mm`; Android `stitcher_frame_jni.cpp`)
19
+ // `CameraFrameHostObject.mm`; Android `stitcher_frame_jni.cpp`)
20
20
  // implements it by wrapping the underlying `CVPixelBufferRef` /
21
21
  // `ArImage*`. Lifetime: the reader holds a strong ref to its
22
22
  // source for the entire host-object lifetime; releases on
23
23
  // destruction (deterministic, RAII).
24
24
  //
25
- // `StitcherFrameData` is value-typed (cheap to copy; ~100 bytes).
25
+ // `CameraFrameData` is value-typed (cheap to copy; ~100 bytes).
26
26
  // Construct on the worklet runtime's thread before each dispatch.
27
27
 
28
28
  #pragma once
29
29
 
30
+ #include <array>
30
31
  #include <cstddef>
31
32
  #include <cstdint>
32
33
  #include <memory>
34
+ #include <optional>
33
35
  #include <string>
36
+ #include <vector>
34
37
 
35
38
  namespace retailens {
36
39
 
@@ -68,10 +71,75 @@ public:
68
71
  virtual std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) = 0;
69
72
  };
70
73
 
74
+ /// One detected AR anchor (ARKit `ARAnchor` / ARCore `Anchor`).
75
+ /// Mirrors the JS `ARAnchor`: a stable id, a coarse type, and a
76
+ /// 4x4 transform.
77
+ struct ArAnchor {
78
+ /// Stable per-session identifier (ARKit UUID / ARCore anchor id).
79
+ std::string id;
80
+ /// Coarse class: `"plane"`, `"image"`, `"point"`, or `"mesh"`.
81
+ std::string type;
82
+ /// 4x4 anchor->world transform, ROW-MAJOR (16 elements). Platform
83
+ /// code is responsible for emitting row-major regardless of the
84
+ /// native matrix's storage order.
85
+ std::array<double, 16> transform{};
86
+
87
+ /// Plane alignment: "" (n/a), "horizontal", or "vertical". Set on
88
+ /// plane anchors; empty -> JS `alignment === undefined`.
89
+ std::string alignment;
90
+ /// Plane extent in metres along local x/z ([extentX, extentZ]); valid
91
+ /// only when hasExtent (plane anchors).
92
+ bool hasExtent = false;
93
+ double extentX = 0.0;
94
+ double extentZ = 0.0;
95
+ /// ARKit semantic class ("wall","floor","table",… or "none"); empty
96
+ /// string -> JS `classification === undefined` (iOS only).
97
+ std::string classification;
98
+
99
+ // ── Scene-reconstruction geometry (only when type == "mesh") ──
100
+ /// True when this anchor carries a mesh (gates JSI emission of
101
+ /// `meshGeometry`). Raw bytes, emitted as ArrayBuffers verbatim
102
+ /// (no conversion) by the JSI layer:
103
+ /// - meshVertices: Float32 xyz triplets, anchor-local.
104
+ /// - meshFaces: Uint32 triangle indices into the vertices.
105
+ /// - meshClassifications: optional Uint8 per-face class (iOS
106
+ /// ARMeshAnchor; empty on Android — depth-derived meshes have
107
+ /// no semantics).
108
+ bool hasMesh = false;
109
+ std::vector<uint8_t> meshVertices;
110
+ std::vector<uint8_t> meshFaces;
111
+ std::vector<uint8_t> meshClassifications;
112
+ };
113
+
114
+ /// AR depth map for one frame. The platforms encode depth differently,
115
+ /// so we carry the raw bytes plus a `format` tag and NORMALISE to a
116
+ /// single JS shape (Float32 metres + Uint8 confidence 0..2) in the JSI
117
+ /// layer (`camera_frame_jsi.cpp`):
118
+ /// - iOS (ARKit `ARDepthData`): `depthBytes` = Float32 metres
119
+ /// (row-packed); `confidenceBytes` = Uint8 `ARConfidenceLevel`
120
+ /// (0=low,1=medium,2=high). `format = "f32m"`.
121
+ /// - Android (ARCore DEPTH16): `depthBytes` = uint16 packed (low 13
122
+ /// bits = millimetres, high 3 bits = confidence 0..7);
123
+ /// `confidenceBytes` empty. `format = "u16packed"`.
124
+ /// Depth maps are small (~256x192 iOS, ~160x120 Android) so the bytes
125
+ /// are eager-copied at extraction time (the ARCore Image is closed
126
+ /// in-scope; iOS copies for the same uniform contract).
127
+ struct ArDepth {
128
+ int32_t width = 0;
129
+ int32_t height = 0;
130
+ /// Encoding of `depthBytes`: `"f32m"` or `"u16packed"`.
131
+ std::string format;
132
+ /// Raw depth bytes, interpreted per `format`.
133
+ std::vector<uint8_t> depthBytes;
134
+ /// Per-pixel confidence (Uint8 0..2). Populated on iOS; empty on
135
+ /// Android (confidence is packed into `depthBytes`).
136
+ std::vector<uint8_t> confidenceBytes;
137
+ };
138
+
71
139
  /// Plain-old-data payload for one `StitcherFrame`. Fully extracted
72
140
  /// at construction time (cheap fields) plus an opaque reader for
73
141
  /// the lazy pixel access.
74
- struct StitcherFrameData {
142
+ struct CameraFrameData {
75
143
  /// Discriminator. `"ar"` for AR-mode frames, `"vc"` for
76
144
  /// vision-camera frames. Used by worklets to gate on AR-only
77
145
  /// field access (translation, depth, anchors, tracking state).
@@ -127,15 +195,30 @@ struct StitcherFrameData {
127
195
  /// docstring for lifetime contract.
128
196
  std::shared_ptr<PixelBufferReader> pixelReader;
129
197
 
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.
198
+ // ── AR-only optional fields ──────────────────────────────────────
199
+
200
+ /// AR depth map (ARKit sceneDepth / ARCore Depth API). `nullopt`
201
+ /// when the device/session can't provide depth; the JSI host object
202
+ /// then exposes `arDepth === undefined`. Normalised to a single JS
203
+ /// shape in the JSI layer regardless of the native `format`.
204
+ std::optional<ArDepth> arDepth;
205
+
206
+ /// Tracked AR anchors visible this frame. Empty when none (or in
207
+ /// non-AR mode); the JSI host object exposes `arAnchors === undefined`
208
+ /// only when empty AND source != "ar" (an AR frame with no anchors
209
+ /// returns an empty array, per the JS contract).
210
+ std::vector<ArAnchor> arAnchors;
211
+
212
+ /// Per-frame camera intrinsics (fx,fy,cx,cy in pixels) + the capture
213
+ /// resolution they're expressed at. Valid only when hasIntrinsics
214
+ /// (AR frames); the JSI exposes `intrinsics === undefined` otherwise.
215
+ bool hasIntrinsics = false;
216
+ double fx = 0.0;
217
+ double fy = 0.0;
218
+ double cx = 0.0;
219
+ double cy = 0.0;
220
+ int32_t intrinsicsImageWidth = 0;
221
+ int32_t intrinsicsImageHeight = 0;
139
222
  };
140
223
 
141
224
  } // namespace retailens
@@ -0,0 +1,357 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // camera_frame_jsi.cpp — implementation of the shared C++ JSI
4
+ // host object. See camera_frame_jsi.hpp for class docs.
5
+
6
+ #include "camera_frame_jsi.hpp"
7
+
8
+ #include <cstring>
9
+ #include <string>
10
+ #include <utility>
11
+ #include <vector>
12
+
13
+ namespace retailens {
14
+
15
+ using facebook::jsi::Array;
16
+ using facebook::jsi::Function;
17
+ using facebook::jsi::HostFunctionType;
18
+ using facebook::jsi::JSError;
19
+ using facebook::jsi::Object;
20
+ using facebook::jsi::PropNameID;
21
+ using facebook::jsi::Runtime;
22
+ using facebook::jsi::String;
23
+ using facebook::jsi::Value;
24
+
25
+ CameraFrameJsiHostObject::CameraFrameJsiHostObject(CameraFrameData data)
26
+ : _data(std::move(data)), _isValid(true) {}
27
+
28
+ void CameraFrameJsiHostObject::invalidate() {
29
+ _isValid = false;
30
+ // Release the pixel reader immediately so the underlying camera
31
+ // buffer can be reclaimed. ARKit's ARFrame uses a pooled
32
+ // CVPixelBuffer; holding past the dispatch scope causes
33
+ // back-pressure. ARCore's ArImage must be explicitly released
34
+ // for the next frame's acquire to succeed.
35
+ _data.pixelReader.reset();
36
+ }
37
+
38
+ std::vector<PropNameID> CameraFrameJsiHostObject::getPropertyNames(
39
+ Runtime& rt) {
40
+ std::vector<PropNameID> names;
41
+ names.push_back(PropNameID::forUtf8(rt, "isValid"));
42
+ if (!_isValid) return names;
43
+
44
+ names.push_back(PropNameID::forUtf8(rt, "width"));
45
+ names.push_back(PropNameID::forUtf8(rt, "height"));
46
+ names.push_back(PropNameID::forUtf8(rt, "pixelFormat"));
47
+ names.push_back(PropNameID::forUtf8(rt, "orientation"));
48
+ names.push_back(PropNameID::forUtf8(rt, "timestamp"));
49
+ names.push_back(PropNameID::forUtf8(rt, "pose"));
50
+ names.push_back(PropNameID::forUtf8(rt, "source"));
51
+ names.push_back(PropNameID::forUtf8(rt, "toArrayBuffer"));
52
+ if (!_data.arTrackingState.empty()) {
53
+ names.push_back(PropNameID::forUtf8(rt, "arTrackingState"));
54
+ }
55
+ if (_data.arDepth.has_value()) {
56
+ names.push_back(PropNameID::forUtf8(rt, "arDepth"));
57
+ }
58
+ // AR frames expose `arAnchors` (possibly an empty array); non-AR
59
+ // frames omit it (JS sees `undefined`).
60
+ if (_data.source == "ar") {
61
+ names.push_back(PropNameID::forUtf8(rt, "arAnchors"));
62
+ }
63
+ if (_data.hasIntrinsics) {
64
+ names.push_back(PropNameID::forUtf8(rt, "intrinsics"));
65
+ }
66
+ return names;
67
+ }
68
+
69
+ Value CameraFrameJsiHostObject::get(Runtime& rt,
70
+ const PropNameID& propName) {
71
+ const std::string name = propName.utf8(rt);
72
+
73
+ if (name == "isValid") {
74
+ return Value(_isValid);
75
+ }
76
+ // Invalidated host objects expose only `isValid` (returns false).
77
+ // Every other access throws — matches vc FrameHostObject's contract.
78
+ // Lets worklets that incorrectly retain a frame across dispatch
79
+ // boundaries fail loudly rather than read garbage.
80
+ if (!_isValid) {
81
+ throw JSError(rt,
82
+ "[StitcherFrame] cannot access property '" + name +
83
+ "' after host object was invalidated. "
84
+ "Frame data is only valid for the duration of the worklet call.");
85
+ }
86
+
87
+ if (name == "width") return Value(static_cast<double>(_data.width));
88
+ if (name == "height") return Value(static_cast<double>(_data.height));
89
+ if (name == "pixelFormat") return String::createFromUtf8(rt, _data.pixelFormat);
90
+ if (name == "orientation") return String::createFromUtf8(rt, _data.orientation);
91
+ if (name == "timestamp") return Value(_data.timestampNs);
92
+ if (name == "source") return String::createFromUtf8(rt, _data.source);
93
+
94
+ if (name == "pose") {
95
+ Object pose(rt);
96
+ Array rotation(rt, 4);
97
+ rotation.setValueAtIndex(rt, 0, Value(_data.qx));
98
+ rotation.setValueAtIndex(rt, 1, Value(_data.qy));
99
+ rotation.setValueAtIndex(rt, 2, Value(_data.qz));
100
+ rotation.setValueAtIndex(rt, 3, Value(_data.qw));
101
+ pose.setProperty(rt, "rotation", rotation);
102
+ if (_data.hasTranslation) {
103
+ Array translation(rt, 3);
104
+ translation.setValueAtIndex(rt, 0, Value(_data.tx));
105
+ translation.setValueAtIndex(rt, 1, Value(_data.ty));
106
+ translation.setValueAtIndex(rt, 2, Value(_data.tz));
107
+ pose.setProperty(rt, "translation", translation);
108
+ }
109
+ return pose;
110
+ }
111
+
112
+ if (name == "arTrackingState") {
113
+ if (_data.arTrackingState.empty()) return Value::undefined();
114
+ return String::createFromUtf8(rt, _data.arTrackingState);
115
+ }
116
+
117
+ if (name == "toArrayBuffer") {
118
+ // Capture a weak self so the lambda doesn't extend the host
119
+ // object's lifetime beyond what the runtime intended. When the
120
+ // runtime releases its shared_ptr (after dispatch), the weak
121
+ // ref expires and toArrayBuffer() throws on next call.
122
+ auto weakSelf = std::weak_ptr<CameraFrameJsiHostObject>(shared_from_this());
123
+ HostFunctionType fn = [weakSelf](Runtime& runtime,
124
+ const Value& thisVal,
125
+ const Value* args,
126
+ size_t count) -> Value {
127
+ auto self = weakSelf.lock();
128
+ if (!self || !self->_isValid || !self->_data.pixelReader) {
129
+ throw JSError(runtime,
130
+ "[StitcherFrame] toArrayBuffer() called on invalidated frame "
131
+ "(host object was released after the worklet dispatch returned)");
132
+ }
133
+ const std::size_t bufSize = self->_data.pixelReader->byteSize();
134
+
135
+ // Per-runtime ArrayBuffer cache. Pattern from vision-camera's
136
+ // FrameHostObject.mm:124-149. Without this, every per-frame
137
+ // worklet call to toArrayBuffer() allocates a fresh ~2MB
138
+ // vector (1920x1080 NV12 Y-plane) — ~60 MB/s of GC churn at
139
+ // 30 fps that defeats the point of having a worklet at all.
140
+ // Caching on `runtime.global()` is safe because (a) each
141
+ // worklet runtime has its own global, and (b) every call
142
+ // overwrites the cached buffer before returning, so there's
143
+ // no time-window for cross-worklet data leaks.
144
+ static constexpr const char* kCacheKey =
145
+ "__stitcherFrameArrayBufferCache";
146
+ auto global = runtime.global();
147
+ std::shared_ptr<OwningPixelBuffer> owning;
148
+
149
+ bool needsAlloc = true;
150
+ if (global.hasProperty(runtime, kCacheKey)) {
151
+ auto cached = global.getPropertyAsObject(runtime, kCacheKey);
152
+ if (cached.isArrayBuffer(runtime)) {
153
+ auto cachedBuffer = cached.getArrayBuffer(runtime);
154
+ // Hermes JSI exposes the underlying MutableBuffer via the
155
+ // shared_ptr the ArrayBuffer was constructed with — but
156
+ // there's no public getter once handed to JSI. We retain
157
+ // a parallel shared_ptr below via a hidden global slot.
158
+ if (cachedBuffer.size(runtime) == bufSize) {
159
+ // Size matches — reuse. Pull the parallel
160
+ // OwningPixelBuffer ref out of its hidden slot.
161
+ static constexpr const char* kRefKey =
162
+ "__stitcherFrameArrayBufferCacheRef";
163
+ if (global.hasProperty(runtime, kRefKey)) {
164
+ // The hidden ref is stored as a HostObject wrapping
165
+ // the shared_ptr; pull it back. See alloc path below.
166
+ auto refObj = global.getPropertyAsObject(runtime, kRefKey);
167
+ if (refObj.isHostObject(runtime)) {
168
+ struct RefHolder : facebook::jsi::HostObject {
169
+ std::shared_ptr<OwningPixelBuffer> buf;
170
+ explicit RefHolder(std::shared_ptr<OwningPixelBuffer> b)
171
+ : buf(std::move(b)) {}
172
+ };
173
+ auto holder =
174
+ refObj.getHostObject<RefHolder>(runtime);
175
+ if (holder && holder->buf) {
176
+ owning = holder->buf;
177
+ needsAlloc = false;
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ if (needsAlloc) {
186
+ owning = std::make_shared<OwningPixelBuffer>(bufSize);
187
+ // Store the ArrayBuffer + a parallel ref-holder on global.
188
+ // The ArrayBuffer's MutableBuffer is the same `owning`; the
189
+ // ref-holder lets us pull `owning` back out on cache hits.
190
+ global.setProperty(runtime, kCacheKey,
191
+ facebook::jsi::ArrayBuffer(runtime, owning));
192
+ struct RefHolder : facebook::jsi::HostObject {
193
+ std::shared_ptr<OwningPixelBuffer> buf;
194
+ explicit RefHolder(std::shared_ptr<OwningPixelBuffer> b)
195
+ : buf(std::move(b)) {}
196
+ };
197
+ global.setProperty(runtime, "__stitcherFrameArrayBufferCacheRef",
198
+ facebook::jsi::Object::createFromHostObject(runtime,
199
+ std::make_shared<RefHolder>(owning)));
200
+ }
201
+
202
+ std::size_t written =
203
+ self->_data.pixelReader->copyTo(owning->bytes(), bufSize);
204
+ if (written == 0 && bufSize > 0) {
205
+ throw JSError(runtime,
206
+ "[StitcherFrame] toArrayBuffer() pixel copy failed "
207
+ "(reader returned 0 bytes — likely the underlying "
208
+ "camera buffer was NULL or unreadable; see native log)");
209
+ }
210
+
211
+ // Re-fetch the cached ArrayBuffer to return. Cheap (just a
212
+ // property lookup); avoids constructing a new jsi::ArrayBuffer
213
+ // that wraps the same MutableBuffer (which would be wasteful).
214
+ return global.getPropertyAsObject(runtime, kCacheKey)
215
+ .getArrayBuffer(runtime);
216
+ };
217
+ return Function::createFromHostFunction(rt,
218
+ PropNameID::forUtf8(rt, "toArrayBuffer"), 0, fn);
219
+ }
220
+
221
+ if (name == "arDepth") {
222
+ // Normalise both platforms to ONE JS shape:
223
+ // { width, height, depthMap: Float32 metres, confidenceMap?: Uint8 0..2 }
224
+ if (!_data.arDepth.has_value()) return Value::undefined();
225
+ const ArDepth& d = *_data.arDepth;
226
+ const std::size_t px =
227
+ static_cast<std::size_t>(d.width) * static_cast<std::size_t>(d.height);
228
+ if (px == 0) return Value::undefined();
229
+
230
+ Object depth(rt);
231
+ depth.setProperty(rt, "width", Value(static_cast<double>(d.width)));
232
+ depth.setProperty(rt, "height", Value(static_cast<double>(d.height)));
233
+
234
+ // depthMap — always emitted as Float32 metres (px * 4 bytes).
235
+ auto depthBuf = std::make_shared<OwningPixelBuffer>(px * sizeof(float));
236
+ auto* out = reinterpret_cast<float*>(depthBuf->bytes());
237
+ std::vector<uint8_t> conf; // Uint8 0..2 (empty => no confidenceMap)
238
+
239
+ if (d.format == "f32m") {
240
+ // iOS ARKit: depthBytes already Float32 metres; confidence is a
241
+ // separate Uint8 (ARConfidenceLevel 0..2) — pass both through.
242
+ if (d.depthBytes.size() >= px * sizeof(float)) {
243
+ std::memcpy(out, d.depthBytes.data(), px * sizeof(float));
244
+ }
245
+ if (d.confidenceBytes.size() >= px) {
246
+ conf.assign(d.confidenceBytes.begin(), d.confidenceBytes.begin() + px);
247
+ }
248
+ } else if (d.format == "u16packed") {
249
+ // Android ARCore DEPTH16: low 13 bits = millimetres, high 3 bits
250
+ // = confidence 0..7. Convert mm->metres and map confidence 0..7
251
+ // -> 0..2 so JS sees the same scale as iOS.
252
+ const auto* src = reinterpret_cast<const uint16_t*>(d.depthBytes.data());
253
+ const std::size_t srcCount = d.depthBytes.size() / sizeof(uint16_t);
254
+ conf.resize(px, 0);
255
+ for (std::size_t i = 0; i < px; ++i) {
256
+ const uint16_t raw = (i < srcCount) ? src[i] : 0;
257
+ out[i] = static_cast<float>(raw & 0x1FFF) / 1000.0f;
258
+ const uint8_t c7 = static_cast<uint8_t>((raw >> 13) & 0x7);
259
+ conf[i] = (c7 <= 2) ? 0 : (c7 <= 5 ? 1 : 2);
260
+ }
261
+ } else {
262
+ return Value::undefined();
263
+ }
264
+
265
+ depth.setProperty(rt, "depthMap", facebook::jsi::ArrayBuffer(rt, depthBuf));
266
+ if (!conf.empty()) {
267
+ auto confBuf = std::make_shared<OwningPixelBuffer>(px);
268
+ std::memcpy(confBuf->bytes(), conf.data(), px);
269
+ depth.setProperty(rt, "confidenceMap",
270
+ facebook::jsi::ArrayBuffer(rt, confBuf));
271
+ }
272
+ return depth;
273
+ }
274
+
275
+ if (name == "arAnchors") {
276
+ // AR frames return an array (possibly empty); non-AR returns
277
+ // undefined (matches the JS `arAnchors?: ARAnchor[]` contract).
278
+ if (_data.source != "ar") return Value::undefined();
279
+ Array anchors(rt, _data.arAnchors.size());
280
+ for (std::size_t i = 0; i < _data.arAnchors.size(); ++i) {
281
+ const ArAnchor& a = _data.arAnchors[i];
282
+ Object obj(rt);
283
+ obj.setProperty(rt, "id", String::createFromUtf8(rt, a.id));
284
+ obj.setProperty(rt, "type", String::createFromUtf8(rt, a.type));
285
+ Array transform(rt, 16);
286
+ for (std::size_t j = 0; j < 16; ++j) {
287
+ transform.setValueAtIndex(rt, j, Value(a.transform[j]));
288
+ }
289
+ obj.setProperty(rt, "transform", transform);
290
+ if (!a.alignment.empty()) {
291
+ obj.setProperty(rt, "alignment",
292
+ String::createFromUtf8(rt, a.alignment));
293
+ }
294
+ if (a.hasExtent) {
295
+ Array extent(rt, 2);
296
+ extent.setValueAtIndex(rt, 0, Value(a.extentX));
297
+ extent.setValueAtIndex(rt, 1, Value(a.extentZ));
298
+ obj.setProperty(rt, "extent", extent);
299
+ }
300
+ if (!a.classification.empty()) {
301
+ obj.setProperty(rt, "classification",
302
+ String::createFromUtf8(rt, a.classification));
303
+ }
304
+ if (a.hasMesh) {
305
+ // Scene-reconstruction geometry — bytes emitted verbatim as
306
+ // ArrayBuffers (vertices=Float32, faces=Uint32, classifications=Uint8).
307
+ Object mesh(rt);
308
+ auto vbuf = std::make_shared<OwningPixelBuffer>(a.meshVertices.size());
309
+ if (!a.meshVertices.empty()) {
310
+ std::memcpy(vbuf->bytes(), a.meshVertices.data(), a.meshVertices.size());
311
+ }
312
+ mesh.setProperty(rt, "vertices", facebook::jsi::ArrayBuffer(rt, vbuf));
313
+ auto fbuf = std::make_shared<OwningPixelBuffer>(a.meshFaces.size());
314
+ if (!a.meshFaces.empty()) {
315
+ std::memcpy(fbuf->bytes(), a.meshFaces.data(), a.meshFaces.size());
316
+ }
317
+ mesh.setProperty(rt, "faces", facebook::jsi::ArrayBuffer(rt, fbuf));
318
+ if (!a.meshClassifications.empty()) {
319
+ auto cbuf =
320
+ std::make_shared<OwningPixelBuffer>(a.meshClassifications.size());
321
+ std::memcpy(cbuf->bytes(), a.meshClassifications.data(),
322
+ a.meshClassifications.size());
323
+ mesh.setProperty(rt, "classifications",
324
+ facebook::jsi::ArrayBuffer(rt, cbuf));
325
+ }
326
+ obj.setProperty(rt, "meshGeometry", mesh);
327
+ }
328
+ anchors.setValueAtIndex(rt, i, obj);
329
+ }
330
+ return anchors;
331
+ }
332
+
333
+ // Per-frame camera intrinsics (AR frames only). `intrinsics ===
334
+ // undefined` when not populated (non-AR frames). Shape mirrors the
335
+ // JS `CameraFrame.intrinsics`: fx/fy/cx/cy in pixels + the capture
336
+ // resolution they're expressed at.
337
+ if (name == "intrinsics") {
338
+ if (!_data.hasIntrinsics) return Value::undefined();
339
+ Object intrinsics(rt);
340
+ intrinsics.setProperty(rt, "fx", Value(_data.fx));
341
+ intrinsics.setProperty(rt, "fy", Value(_data.fy));
342
+ intrinsics.setProperty(rt, "cx", Value(_data.cx));
343
+ intrinsics.setProperty(rt, "cy", Value(_data.cy));
344
+ intrinsics.setProperty(
345
+ rt, "imageWidth",
346
+ Value(static_cast<double>(_data.intrinsicsImageWidth)));
347
+ intrinsics.setProperty(
348
+ rt, "imageHeight",
349
+ Value(static_cast<double>(_data.intrinsicsImageHeight)));
350
+ return intrinsics;
351
+ }
352
+
353
+ // Unknown property — return undefined (matches JS object semantics).
354
+ return Value::undefined();
355
+ }
356
+
357
+ } // namespace retailens
@@ -0,0 +1,108 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // camera_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 "camera_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 CameraFrameJsiHostObject
63
+ : public facebook::jsi::HostObject,
64
+ public std::enable_shared_from_this<CameraFrameJsiHostObject> {
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 CameraFrameJsiHostObject(...)` 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<CameraFrameJsiHostObject> create(
76
+ CameraFrameData data) {
77
+ // `std::make_shared` would require a public ctor; route through
78
+ // a tagged-dispatch private constructor instead.
79
+ struct EnableMakeShared : CameraFrameJsiHostObject {
80
+ explicit EnableMakeShared(CameraFrameData d)
81
+ : CameraFrameJsiHostObject(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 CameraFrameJsiHostObject(CameraFrameData data);
103
+
104
+ CameraFrameData _data;
105
+ bool _isValid;
106
+ };
107
+
108
+ } // namespace retailens