react-native-image-stitcher 0.17.0 → 0.19.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 +151 -0
- package/RNImageStitcher.podspec +1 -1
- package/android/src/main/cpp/CMakeLists.txt +4 -4
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +216 -7
- package/android/src/main/java/io/imagestitcher/rn/ARFrameContext.kt +89 -0
- package/android/src/main/java/io/imagestitcher/rn/ARFramePlugin.kt +57 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +831 -6
- package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +109 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +184 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +84 -2
- package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
- package/cpp/{stitcher_frame_jsi.cpp → camera_frame_jsi.cpp} +154 -11
- package/cpp/{stitcher_frame_jsi.hpp → camera_frame_jsi.hpp} +12 -12
- package/cpp/stitcher_proxy_jsi.cpp +31 -0
- package/cpp/stitcher_proxy_jsi.hpp +16 -0
- package/cpp/stitcher_worklet_dispatch.cpp +5 -5
- package/cpp/stitcher_worklet_dispatch.hpp +5 -5
- package/dist/camera/ARCameraView.d.ts +81 -3
- package/dist/camera/ARCameraView.js +103 -1
- package/dist/camera/Camera.d.ts +73 -7
- package/dist/camera/Camera.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/stitching/ARFrameMeta.d.ts +149 -0
- package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
- package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
- package/dist/stitching/CameraFrame.js +4 -0
- package/dist/stitching/useStitcherWorklet.d.ts +4 -4
- package/dist/stitching/useStitcherWorklet.js +4 -4
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +172 -2
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +108 -0
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +772 -0
- package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +247 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +418 -34
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +2 -2
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +4 -4
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +230 -5
- package/src/camera/Camera.tsx +91 -7
- package/src/index.ts +12 -3
- package/src/stitching/ARFrameMeta.ts +157 -0
- package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
- package/src/stitching/useStitcherWorklet.ts +9 -9
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// host object. See
|
|
3
|
+
// camera_frame_jsi.cpp — implementation of the shared C++ JSI
|
|
4
|
+
// host object. See camera_frame_jsi.hpp for class docs.
|
|
5
5
|
|
|
6
|
-
#include "
|
|
6
|
+
#include "camera_frame_jsi.hpp"
|
|
7
7
|
|
|
8
|
+
#include <cstring>
|
|
8
9
|
#include <string>
|
|
9
10
|
#include <utility>
|
|
11
|
+
#include <vector>
|
|
10
12
|
|
|
11
13
|
namespace retailens {
|
|
12
14
|
|
|
@@ -20,10 +22,10 @@ using facebook::jsi::Runtime;
|
|
|
20
22
|
using facebook::jsi::String;
|
|
21
23
|
using facebook::jsi::Value;
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
CameraFrameJsiHostObject::CameraFrameJsiHostObject(CameraFrameData data)
|
|
24
26
|
: _data(std::move(data)), _isValid(true) {}
|
|
25
27
|
|
|
26
|
-
void
|
|
28
|
+
void CameraFrameJsiHostObject::invalidate() {
|
|
27
29
|
_isValid = false;
|
|
28
30
|
// Release the pixel reader immediately so the underlying camera
|
|
29
31
|
// buffer can be reclaimed. ARKit's ARFrame uses a pooled
|
|
@@ -33,7 +35,7 @@ void StitcherFrameJsiHostObject::invalidate() {
|
|
|
33
35
|
_data.pixelReader.reset();
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
std::vector<PropNameID>
|
|
38
|
+
std::vector<PropNameID> CameraFrameJsiHostObject::getPropertyNames(
|
|
37
39
|
Runtime& rt) {
|
|
38
40
|
std::vector<PropNameID> names;
|
|
39
41
|
names.push_back(PropNameID::forUtf8(rt, "isValid"));
|
|
@@ -50,10 +52,21 @@ std::vector<PropNameID> StitcherFrameJsiHostObject::getPropertyNames(
|
|
|
50
52
|
if (!_data.arTrackingState.empty()) {
|
|
51
53
|
names.push_back(PropNameID::forUtf8(rt, "arTrackingState"));
|
|
52
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
|
+
}
|
|
53
66
|
return names;
|
|
54
67
|
}
|
|
55
68
|
|
|
56
|
-
Value
|
|
69
|
+
Value CameraFrameJsiHostObject::get(Runtime& rt,
|
|
57
70
|
const PropNameID& propName) {
|
|
58
71
|
const std::string name = propName.utf8(rt);
|
|
59
72
|
|
|
@@ -106,7 +119,7 @@ Value StitcherFrameJsiHostObject::get(Runtime& rt,
|
|
|
106
119
|
// object's lifetime beyond what the runtime intended. When the
|
|
107
120
|
// runtime releases its shared_ptr (after dispatch), the weak
|
|
108
121
|
// ref expires and toArrayBuffer() throws on next call.
|
|
109
|
-
auto weakSelf = std::weak_ptr<
|
|
122
|
+
auto weakSelf = std::weak_ptr<CameraFrameJsiHostObject>(shared_from_this());
|
|
110
123
|
HostFunctionType fn = [weakSelf](Runtime& runtime,
|
|
111
124
|
const Value& thisVal,
|
|
112
125
|
const Value* args,
|
|
@@ -205,9 +218,139 @@ Value StitcherFrameJsiHostObject::get(Runtime& rt,
|
|
|
205
218
|
PropNameID::forUtf8(rt, "toArrayBuffer"), 0, fn);
|
|
206
219
|
}
|
|
207
220
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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).
|
|
211
354
|
return Value::undefined();
|
|
212
355
|
}
|
|
213
356
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// camera_frame_jsi.hpp — shared C++ JSI host object for the v0.8.0
|
|
4
4
|
// `StitcherFrame` contract. Compiles on both iOS and Android; each
|
|
5
5
|
// platform provides only the PixelBufferReader implementation and
|
|
6
6
|
// the construction call site (Obj-C++ on iOS; JNI on Android).
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
#include <memory>
|
|
19
19
|
#include <vector>
|
|
20
20
|
|
|
21
|
-
#include "
|
|
21
|
+
#include "camera_frame_data.hpp"
|
|
22
22
|
|
|
23
23
|
namespace retailens {
|
|
24
24
|
|
|
@@ -59,26 +59,26 @@ class OwningPixelBuffer : public facebook::jsi::MutableBuffer {
|
|
|
59
59
|
/// worklet, then invalidate (typically immediately after dispatch
|
|
60
60
|
/// returns — the underlying pixel buffer's lifetime is bound to
|
|
61
61
|
/// the calling AR-session callback scope).
|
|
62
|
-
class
|
|
62
|
+
class CameraFrameJsiHostObject
|
|
63
63
|
: public facebook::jsi::HostObject,
|
|
64
|
-
public std::enable_shared_from_this<
|
|
64
|
+
public std::enable_shared_from_this<CameraFrameJsiHostObject> {
|
|
65
65
|
public:
|
|
66
66
|
/// Factory. ALWAYS use this — `shared_from_this()` (called inside
|
|
67
67
|
/// `get` for `toArrayBuffer`) requires the instance to be owned
|
|
68
68
|
/// by a `shared_ptr` from the moment of construction. A raw
|
|
69
|
-
/// `new
|
|
69
|
+
/// `new CameraFrameJsiHostObject(...)` would throw
|
|
70
70
|
/// `std::bad_weak_ptr` on the first `toArrayBuffer()` JSI call.
|
|
71
71
|
///
|
|
72
72
|
/// Private constructor + public factory enforces this at the
|
|
73
73
|
/// language level; callers can't accidentally construct without
|
|
74
74
|
/// `std::make_shared`.
|
|
75
|
-
static std::shared_ptr<
|
|
76
|
-
|
|
75
|
+
static std::shared_ptr<CameraFrameJsiHostObject> create(
|
|
76
|
+
CameraFrameData data) {
|
|
77
77
|
// `std::make_shared` would require a public ctor; route through
|
|
78
78
|
// a tagged-dispatch private constructor instead.
|
|
79
|
-
struct EnableMakeShared :
|
|
80
|
-
explicit EnableMakeShared(
|
|
81
|
-
:
|
|
79
|
+
struct EnableMakeShared : CameraFrameJsiHostObject {
|
|
80
|
+
explicit EnableMakeShared(CameraFrameData d)
|
|
81
|
+
: CameraFrameJsiHostObject(std::move(d)) {}
|
|
82
82
|
};
|
|
83
83
|
return std::make_shared<EnableMakeShared>(std::move(data));
|
|
84
84
|
}
|
|
@@ -99,9 +99,9 @@ class StitcherFrameJsiHostObject
|
|
|
99
99
|
bool isValid() const { return _isValid; }
|
|
100
100
|
|
|
101
101
|
private:
|
|
102
|
-
explicit
|
|
102
|
+
explicit CameraFrameJsiHostObject(CameraFrameData data);
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
CameraFrameData _data;
|
|
105
105
|
bool _isValid;
|
|
106
106
|
};
|
|
107
107
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
#include "stitcher_worklet_registry.hpp"
|
|
10
10
|
|
|
11
|
+
#include <atomic>
|
|
11
12
|
#include <memory>
|
|
12
13
|
#include <string>
|
|
13
14
|
#include <vector>
|
|
@@ -16,6 +17,13 @@ namespace retailens {
|
|
|
16
17
|
|
|
17
18
|
namespace {
|
|
18
19
|
|
|
20
|
+
// AR-metadata extraction toggles (see header). Atomic: written on the
|
|
21
|
+
// JS thread via __stitcherProxy.setExtractionConfig, read on the AR
|
|
22
|
+
// delegate / GL thread by the platform extraction. Default all false.
|
|
23
|
+
std::atomic<bool> g_extractDepth{false};
|
|
24
|
+
std::atomic<bool> g_extractAnchors{false};
|
|
25
|
+
std::atomic<bool> g_extractMesh{false};
|
|
26
|
+
|
|
19
27
|
class StitcherProxyHostObject : public facebook::jsi::HostObject {
|
|
20
28
|
public:
|
|
21
29
|
facebook::jsi::Value get(facebook::jsi::Runtime& rt,
|
|
@@ -84,6 +92,21 @@ class StitcherProxyHostObject : public facebook::jsi::HostObject {
|
|
|
84
92
|
rt, PropNameID::forUtf8(rt, "count"), 0, std::move(fn));
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
if (name == "setExtractionConfig") {
|
|
96
|
+
// setExtractionConfig(depth, anchors, mesh) — three booleans gating
|
|
97
|
+
// the platform AR extraction. Missing/non-bool args default false.
|
|
98
|
+
auto fn = [](facebook::jsi::Runtime& runtime, const Value& /*thisVal*/,
|
|
99
|
+
const Value* args, size_t count) -> Value {
|
|
100
|
+
g_extractDepth.store(count > 0 && args[0].isBool() && args[0].getBool());
|
|
101
|
+
g_extractAnchors.store(count > 1 && args[1].isBool() &&
|
|
102
|
+
args[1].getBool());
|
|
103
|
+
g_extractMesh.store(count > 2 && args[2].isBool() && args[2].getBool());
|
|
104
|
+
return Value::undefined();
|
|
105
|
+
};
|
|
106
|
+
return Function::createFromHostFunction(
|
|
107
|
+
rt, PropNameID::forUtf8(rt, "setExtractionConfig"), 3, std::move(fn));
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
return Value::undefined();
|
|
88
111
|
}
|
|
89
112
|
|
|
@@ -93,6 +116,8 @@ class StitcherProxyHostObject : public facebook::jsi::HostObject {
|
|
|
93
116
|
names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "install"));
|
|
94
117
|
names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "uninstall"));
|
|
95
118
|
names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "count"));
|
|
119
|
+
names.push_back(
|
|
120
|
+
facebook::jsi::PropNameID::forUtf8(rt, "setExtractionConfig"));
|
|
96
121
|
return names;
|
|
97
122
|
}
|
|
98
123
|
};
|
|
@@ -106,4 +131,10 @@ void installStitcherProxy(facebook::jsi::Runtime& runtime) {
|
|
|
106
131
|
facebook::jsi::Object::createFromHostObject(runtime, proxy));
|
|
107
132
|
}
|
|
108
133
|
|
|
134
|
+
ExtractionConfig getExtractionConfig() {
|
|
135
|
+
return ExtractionConfig{g_extractDepth.load(std::memory_order_relaxed),
|
|
136
|
+
g_extractAnchors.load(std::memory_order_relaxed),
|
|
137
|
+
g_extractMesh.load(std::memory_order_relaxed)};
|
|
138
|
+
}
|
|
139
|
+
|
|
109
140
|
} // namespace retailens
|
|
@@ -43,4 +43,20 @@ namespace retailens {
|
|
|
43
43
|
/// Android: `@ReactMethod(isBlockingSynchronousMethod = true)`).
|
|
44
44
|
void installStitcherProxy(facebook::jsi::Runtime& runtime);
|
|
45
45
|
|
|
46
|
+
/// Per-frame AR-metadata extraction toggles. Set from JS via
|
|
47
|
+
/// `__stitcherProxy.setExtractionConfig(depth, anchors, mesh)` (driven by
|
|
48
|
+
/// the `<Camera>` enableDepth/enableAnchors/enableMesh props); read by the
|
|
49
|
+
/// platform AR extraction to skip costly work when off. Defaults: all
|
|
50
|
+
/// false — zero arDepth/arAnchors/mesh cost until a host opts in (the
|
|
51
|
+
/// always-cheap pose/tracking/pixels are unaffected).
|
|
52
|
+
struct ExtractionConfig {
|
|
53
|
+
bool depth = false;
|
|
54
|
+
bool anchors = false;
|
|
55
|
+
bool mesh = false;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/// Thread-safe snapshot of the current extraction config. Written on the
|
|
59
|
+
/// JS thread (via the proxy), read on the AR delegate / GL thread.
|
|
60
|
+
ExtractionConfig getExtractionConfig();
|
|
61
|
+
|
|
46
62
|
} // namespace retailens
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
#include "stitcher_worklet_dispatch.hpp"
|
|
7
7
|
|
|
8
|
-
#include "
|
|
8
|
+
#include "camera_frame_jsi.hpp"
|
|
9
9
|
#include "stitcher_worklet_registry.hpp"
|
|
10
10
|
|
|
11
11
|
#include <react-native-worklets-core/WKTJsiWorklet.h>
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
namespace retailens {
|
|
32
32
|
|
|
33
33
|
void dispatchToHostWorklets(RNWorklet::JsiWorkletContext* context,
|
|
34
|
-
|
|
34
|
+
CameraFrameData data) {
|
|
35
35
|
// Fast-path early-exit when no host worklets are registered.
|
|
36
36
|
// The Android caller (`StitcherWorkletRuntime.dispatchToHostWorklets`)
|
|
37
37
|
// already runs in a hot per-frame loop; saving the host-object
|
|
@@ -51,18 +51,18 @@ void dispatchToHostWorklets(RNWorklet::JsiWorkletContext* context,
|
|
|
51
51
|
|
|
52
52
|
// Build the JSI host object on the worklet thread (inside the
|
|
53
53
|
// lambda) so JSI access happens on the target runtime.
|
|
54
|
-
// `
|
|
54
|
+
// `CameraFrameJsiHostObject::create` uses the `make_shared`-via-
|
|
55
55
|
// factory pattern (required by `shared_from_this()` inside the
|
|
56
56
|
// `toArrayBuffer` lambda); see its header.
|
|
57
57
|
//
|
|
58
|
-
// Capture `data` by-move so the
|
|
58
|
+
// Capture `data` by-move so the CameraFrameData (including the
|
|
59
59
|
// pixel reader's shared_ptr) lives until the lambda runs.
|
|
60
60
|
// Capture `invokers` by-move as well.
|
|
61
61
|
context->invokeOnWorkletThread(
|
|
62
62
|
[invokers = std::move(invokers), data = std::move(data)](
|
|
63
63
|
RNWorklet::JsiWorkletContext* /*ctx*/,
|
|
64
64
|
facebook::jsi::Runtime& rt) mutable {
|
|
65
|
-
auto hostObj =
|
|
65
|
+
auto hostObj = CameraFrameJsiHostObject::create(std::move(data));
|
|
66
66
|
facebook::jsi::Object frameJsi =
|
|
67
67
|
facebook::jsi::Object::createFromHostObject(rt, hostObj);
|
|
68
68
|
facebook::jsi::Value frameVal(rt, frameJsi);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
//
|
|
3
3
|
// stitcher_worklet_dispatch.hpp — shared C++ helper that fans out a
|
|
4
|
-
// `
|
|
4
|
+
// `CameraFrameData` to every host worklet registered in the
|
|
5
5
|
// process-scope `retailens::StitcherWorkletRegistry`.
|
|
6
6
|
//
|
|
7
7
|
// v0.8.0 Phase 4b.iii — used by Android's per-frame fan-out path
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
#pragma once
|
|
28
28
|
|
|
29
|
-
#include "
|
|
29
|
+
#include "camera_frame_data.hpp"
|
|
30
30
|
|
|
31
31
|
#include <jsi/jsi.h>
|
|
32
32
|
|
|
@@ -36,7 +36,7 @@ class JsiWorkletContext;
|
|
|
36
36
|
|
|
37
37
|
namespace retailens {
|
|
38
38
|
|
|
39
|
-
/// Fan out a `
|
|
39
|
+
/// Fan out a `CameraFrameData` to every registered host worklet.
|
|
40
40
|
///
|
|
41
41
|
/// Behaviour:
|
|
42
42
|
///
|
|
@@ -44,7 +44,7 @@ namespace retailens {
|
|
|
44
44
|
/// is empty. No host object is constructed; the caller's
|
|
45
45
|
/// thread returns immediately.
|
|
46
46
|
/// 2. Otherwise, the function snapshots the registry, constructs
|
|
47
|
-
/// a `
|
|
47
|
+
/// a `CameraFrameJsiHostObject` (deferred until inside the
|
|
48
48
|
/// worklet-thread lambda so JSI access happens on the
|
|
49
49
|
/// target runtime), and dispatches via
|
|
50
50
|
/// `context->invokeOnWorkletThread(...)`.
|
|
@@ -66,6 +66,6 @@ namespace retailens {
|
|
|
66
66
|
/// @param data Frame data + pixel reader. Moved into the
|
|
67
67
|
/// worklet-thread lambda.
|
|
68
68
|
void dispatchToHostWorklets(RNWorklet::JsiWorkletContext* context,
|
|
69
|
-
|
|
69
|
+
CameraFrameData data);
|
|
70
70
|
|
|
71
71
|
} // namespace retailens
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import React from 'react';
|
|
32
32
|
import { type ViewStyle } from 'react-native';
|
|
33
|
-
import type {
|
|
33
|
+
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
34
|
+
import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
|
|
34
35
|
export interface ARCameraViewProps {
|
|
35
36
|
/** Layout style, typically `StyleSheet.absoluteFill` or `flex: 1`. */
|
|
36
37
|
style?: ViewStyle;
|
|
@@ -43,7 +44,7 @@ export interface ARCameraViewProps {
|
|
|
43
44
|
/**
|
|
44
45
|
* Optional host worklet invoked once per AR frame, ALONGSIDE the
|
|
45
46
|
* lib's first-party stitching (composition, not replacement). The
|
|
46
|
-
* worklet receives a `
|
|
47
|
+
* worklet receives a `CameraFrame` enriched with AR metadata —
|
|
47
48
|
* `source: 'ar'`, world-space `pose` (rotation + translation),
|
|
48
49
|
* `arTrackingState`, and (when supported) `arDepth` / `arAnchors`.
|
|
49
50
|
*
|
|
@@ -58,7 +59,84 @@ export interface ARCameraViewProps {
|
|
|
58
59
|
* different runtimes with different frame shapes, hence the separate
|
|
59
60
|
* prop.
|
|
60
61
|
*/
|
|
61
|
-
arFrameProcessor?:
|
|
62
|
+
arFrameProcessor?: CameraFrameProcessor;
|
|
63
|
+
/**
|
|
64
|
+
* Opt in to per-frame AR depth extraction (`CameraFrame.arDepth`).
|
|
65
|
+
* Default `false` — depth is the costliest field (a per-frame buffer
|
|
66
|
+
* copy), so it stays off until a worklet needs it.
|
|
67
|
+
*/
|
|
68
|
+
enableDepth?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Opt in to per-frame AR anchor extraction (`CameraFrame.arAnchors` —
|
|
71
|
+
* detected planes / augmented images). Default `false`.
|
|
72
|
+
*/
|
|
73
|
+
enableAnchors?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` entries
|
|
76
|
+
* in `arAnchors`, carrying `meshGeometry`). Default `false`. iOS
|
|
77
|
+
* enables ARKit `sceneReconstruction` (LiDAR devices); Android
|
|
78
|
+
* reconstructs a rough mesh from the depth map. Expensive — only on
|
|
79
|
+
* when needed. Implies depth on Android.
|
|
80
|
+
*/
|
|
81
|
+
enableMesh?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Which plane orientations to surface in `arAnchors` (requires
|
|
84
|
+
* `enableAnchors`). Default `'vertical'` — the orientation the
|
|
85
|
+
* plane-projected stitch path has always used, so existing callers
|
|
86
|
+
* see no change.
|
|
87
|
+
*
|
|
88
|
+
* - `'vertical'` — walls / doors / fixtures (the default)
|
|
89
|
+
* - `'horizontal'` — floors / tables / seats
|
|
90
|
+
* - `'both'` — surface every detected plane
|
|
91
|
+
*
|
|
92
|
+
* Platform notes: iOS changes ARKit `planeDetection` to match (a
|
|
93
|
+
* live session reconfigure). Android always detects both planes
|
|
94
|
+
* (ARCore needs horizontal planes to bootstrap tracking) and simply
|
|
95
|
+
* FILTERS which orientations reach `arAnchors`, so the JS-observable
|
|
96
|
+
* set is identical on both platforms.
|
|
97
|
+
*/
|
|
98
|
+
planeDetection?: 'vertical' | 'horizontal' | 'both';
|
|
99
|
+
/**
|
|
100
|
+
* v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
|
|
101
|
+
* MAIN thread (NOT a worklet). When provided, the native AR session
|
|
102
|
+
* builds an {@link ARFrameMeta} per frame and emits it as a device
|
|
103
|
+
* event; this component subscribes and calls the handler. Worklet-free
|
|
104
|
+
* — this is the recommended way to read AR pose / tracking / anchor /
|
|
105
|
+
* intrinsics / depth-dims / mesh-counts data (the `arFrameProcessor`
|
|
106
|
+
* worklet can only safely surface a shared value; see `ARFrameMeta`).
|
|
107
|
+
*
|
|
108
|
+
* Costly fields are gated: `depth` only when `enableDepth`, `mesh` only
|
|
109
|
+
* when `enableMesh`, `anchors` only when `enableAnchors`;
|
|
110
|
+
* `intrinsics` / `pose` / `trackingState` are always present. Emission
|
|
111
|
+
* is throttled to {@link arFrameMetaInterval} ms.
|
|
112
|
+
*/
|
|
113
|
+
onArFrame?: (meta: ARFrameMeta) => void;
|
|
114
|
+
/**
|
|
115
|
+
* v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
|
|
116
|
+
* (≈ 10 Hz). No effect unless `onArFrame` is provided.
|
|
117
|
+
*/
|
|
118
|
+
arFrameMetaInterval?: number;
|
|
119
|
+
/**
|
|
120
|
+
* v0.19.0 — ASYNCHRONOUS AR-plugin result callback, invoked on the JS MAIN
|
|
121
|
+
* thread (NOT a worklet). Part of the AR plugin framework: host-registered
|
|
122
|
+
* native plugins (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) can
|
|
123
|
+
* offload heavy per-frame work to their own queue and later push a result
|
|
124
|
+
* via `registry.emit(name, result)`. The SDK routes that to JS as a
|
|
125
|
+
* `RNImageStitcherARPluginResult` device event; when this prop is provided,
|
|
126
|
+
* this component subscribes and invokes the handler with
|
|
127
|
+
* `{ plugin, result }`.
|
|
128
|
+
*
|
|
129
|
+
* SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
|
|
130
|
+
* the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins} —
|
|
131
|
+
* read them there. This callback is ONLY for the out-of-band async channel.
|
|
132
|
+
*
|
|
133
|
+
* The subscription is independent of {@link onArFrame}: a host can read
|
|
134
|
+
* sync results via `onArFrame` and async results via `onArPluginResult`,
|
|
135
|
+
* either, or both. Wiring mirrors `onArFrame` exactly (latest handler held
|
|
136
|
+
* in a ref so the subscription effect depends only on whether a handler is
|
|
137
|
+
* present; cleanup on unmount / when the handler is removed).
|
|
138
|
+
*/
|
|
139
|
+
onArPluginResult?: (e: ARPluginResult) => void;
|
|
62
140
|
}
|
|
63
141
|
/**
|
|
64
142
|
* Imperative handle exposed via the ref — shape mirrors the subset
|
|
@@ -77,7 +77,7 @@ const ensureStitcherProxyInstalled_1 = require("../stitching/ensureStitcherProxy
|
|
|
77
77
|
const NativeARCameraView = react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android'
|
|
78
78
|
? (0, react_native_1.requireNativeComponent)('RNSARCameraView')
|
|
79
79
|
: null;
|
|
80
|
-
exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor }, ref) {
|
|
80
|
+
exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, onArPluginResult, }, ref) {
|
|
81
81
|
// Held across the start→stop lifecycle so stopRecording's
|
|
82
82
|
// resolved VideoFile can be delivered via the same callback
|
|
83
83
|
// pair vision-camera uses.
|
|
@@ -103,6 +103,108 @@ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, gu
|
|
|
103
103
|
proxy.uninstall(id);
|
|
104
104
|
};
|
|
105
105
|
}, [arFrameProcessor]);
|
|
106
|
+
// Push the AR-metadata extraction config to native — gates the
|
|
107
|
+
// costly per-frame depth / anchor / mesh work (all off by default).
|
|
108
|
+
// Routed through `__stitcherProxy.setExtractionConfig`, read by the
|
|
109
|
+
// platform AR extraction. iOS ADDITIONALLY toggles ARKit
|
|
110
|
+
// `sceneReconstruction` for mesh (a session-config change, not a
|
|
111
|
+
// per-frame gate); Android reconstructs mesh from the depth map and
|
|
112
|
+
// needs no session change.
|
|
113
|
+
(0, react_1.useEffect)(() => {
|
|
114
|
+
const depth = enableDepth === true;
|
|
115
|
+
const anchors = enableAnchors === true;
|
|
116
|
+
const mesh = enableMesh === true;
|
|
117
|
+
if ((0, ensureStitcherProxyInstalled_1.ensureStitcherProxyInstalled)()) {
|
|
118
|
+
globalThis.__stitcherProxy?.setExtractionConfig?.(depth, anchors, mesh);
|
|
119
|
+
}
|
|
120
|
+
if (react_native_1.Platform.OS === 'ios') {
|
|
121
|
+
const session = react_native_1.NativeModules
|
|
122
|
+
.RNSARSession;
|
|
123
|
+
session?.setSceneReconstructionEnabled?.(mesh);
|
|
124
|
+
}
|
|
125
|
+
}, [enableDepth, enableAnchors, enableMesh]);
|
|
126
|
+
// Push the plane-detection mode to native. Unlike the extraction
|
|
127
|
+
// config above this is a SESSION setting, so it routes through the
|
|
128
|
+
// RNSARSession native module on BOTH platforms (iOS reconfigures
|
|
129
|
+
// ARKit `planeDetection`; Android stores an emission filter — see
|
|
130
|
+
// the prop docs). Defaults to `'vertical'` to preserve the
|
|
131
|
+
// plane-projected stitch path's long-standing behaviour.
|
|
132
|
+
(0, react_1.useEffect)(() => {
|
|
133
|
+
const mode = planeDetection ?? 'vertical';
|
|
134
|
+
const session = react_native_1.NativeModules
|
|
135
|
+
.RNSARSession;
|
|
136
|
+
session?.setPlaneDetection?.(mode);
|
|
137
|
+
}, [planeDetection]);
|
|
138
|
+
// v0.18.0 — onArFrame device-event wiring (worklet-free, main thread).
|
|
139
|
+
//
|
|
140
|
+
// The latest `onArFrame` is held in a ref so the subscription effect
|
|
141
|
+
// depends only on whether a handler is present + the interval — NOT on
|
|
142
|
+
// the handler's identity (which typically changes every render). This
|
|
143
|
+
// avoids tearing down + re-establishing the native event subscription
|
|
144
|
+
// (and the costly `setArFrameMetaEnabled(true)` extraction toggle) on
|
|
145
|
+
// every parent re-render.
|
|
146
|
+
const onArFrameRef = (0, react_1.useRef)(onArFrame);
|
|
147
|
+
(0, react_1.useEffect)(() => {
|
|
148
|
+
onArFrameRef.current = onArFrame;
|
|
149
|
+
}, [onArFrame]);
|
|
150
|
+
const arFrameEnabled = onArFrame != null;
|
|
151
|
+
(0, react_1.useEffect)(() => {
|
|
152
|
+
if (!arFrameEnabled) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const session = react_native_1.NativeModules
|
|
156
|
+
.RNSARSession;
|
|
157
|
+
if (session?.setArFrameMetaEnabled == null) {
|
|
158
|
+
// Native module / method unavailable (e.g. web, or a native build
|
|
159
|
+
// predating the event channel): no-op, no crash.
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const intervalMs = arFrameMetaInterval ?? 100;
|
|
163
|
+
session.setArFrameMetaEnabled(true, intervalMs);
|
|
164
|
+
const emitter = new react_native_1.NativeEventEmitter(react_native_1.NativeModules.RNSARSession);
|
|
165
|
+
const sub = emitter.addListener('RNImageStitcherARFrame', (meta) => {
|
|
166
|
+
onArFrameRef.current?.(meta);
|
|
167
|
+
});
|
|
168
|
+
return () => {
|
|
169
|
+
sub.remove();
|
|
170
|
+
session.setArFrameMetaEnabled?.(false, intervalMs);
|
|
171
|
+
};
|
|
172
|
+
}, [arFrameEnabled, arFrameMetaInterval]);
|
|
173
|
+
// v0.19.0 — onArPluginResult device-event wiring (worklet-free, main
|
|
174
|
+
// thread). Mirrors the onArFrame subscription above: the latest handler
|
|
175
|
+
// is held in a ref so the subscription effect depends only on WHETHER a
|
|
176
|
+
// handler is present, not its (per-render-changing) identity — so the
|
|
177
|
+
// native event subscription isn't torn down + re-established every render.
|
|
178
|
+
//
|
|
179
|
+
// This is a PURELY-JS subscription: unlike onArFrame there's no native
|
|
180
|
+
// "enable" toggle to flip. Native emits `RNImageStitcherARPluginResult`
|
|
181
|
+
// whenever a registered plugin calls `registry.emit(...)`; the registry is
|
|
182
|
+
// empty unless the host registered plugins, so an app with no plugins
|
|
183
|
+
// never sees an event even if this prop is wired.
|
|
184
|
+
const onArPluginResultRef = (0, react_1.useRef)(onArPluginResult);
|
|
185
|
+
(0, react_1.useEffect)(() => {
|
|
186
|
+
onArPluginResultRef.current = onArPluginResult;
|
|
187
|
+
}, [onArPluginResult]);
|
|
188
|
+
const arPluginResultEnabled = onArPluginResult != null;
|
|
189
|
+
(0, react_1.useEffect)(() => {
|
|
190
|
+
if (!arPluginResultEnabled) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const native = react_native_1.NativeModules
|
|
194
|
+
.RNSARSession;
|
|
195
|
+
if (native == null) {
|
|
196
|
+
// Native module unavailable (e.g. web, or a native build predating
|
|
197
|
+
// the plugin event channel): no-op, no crash.
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
const emitter = new react_native_1.NativeEventEmitter(native);
|
|
201
|
+
const sub = emitter.addListener('RNImageStitcherARPluginResult', (e) => {
|
|
202
|
+
onArPluginResultRef.current?.(e);
|
|
203
|
+
});
|
|
204
|
+
return () => {
|
|
205
|
+
sub.remove();
|
|
206
|
+
};
|
|
207
|
+
}, [arPluginResultEnabled]);
|
|
106
208
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
107
209
|
takePhoto: async (options = {}) => {
|
|
108
210
|
const native = react_native_1.NativeModules.RNSARSession;
|