react-native-image-stitcher 0.7.1 → 0.9.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 +241 -0
- package/android/build.gradle +35 -1
- package/android/src/main/cpp/CMakeLists.txt +64 -2
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +30 -11
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +21 -3
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
- package/android/src/main/java/io/imagestitcher/rn/SaveFrameAsJpegPlugin.kt +162 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
- package/cpp/stitcher_frame_data.hpp +141 -0
- package/cpp/stitcher_frame_jsi.cpp +214 -0
- package/cpp/stitcher_frame_jsi.hpp +108 -0
- package/cpp/stitcher_proxy_jsi.cpp +109 -0
- package/cpp/stitcher_proxy_jsi.hpp +46 -0
- package/cpp/stitcher_worklet_dispatch.cpp +103 -0
- package/cpp/stitcher_worklet_dispatch.hpp +71 -0
- package/cpp/stitcher_worklet_registry.cpp +81 -0
- package/cpp/stitcher_worklet_registry.hpp +136 -0
- package/dist/camera/Camera.d.ts +62 -12
- package/dist/camera/Camera.js +30 -15
- package/dist/index.d.ts +6 -0
- package/dist/index.js +30 -1
- package/dist/stitching/StitcherFrame.d.ts +170 -0
- package/dist/stitching/StitcherFrame.js +4 -0
- package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
- package/dist/stitching/StitcherWorkletRegistry.js +78 -0
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/dist/stitching/useFrameProcessor.d.ts +119 -0
- package/dist/stitching/useFrameProcessor.js +196 -0
- package/dist/stitching/useFrameStream.d.ts +34 -0
- package/dist/stitching/useFrameStream.js +219 -0
- package/dist/stitching/useThrottledFrameProcessor.d.ts +33 -0
- package/dist/stitching/useThrottledFrameProcessor.js +132 -0
- package/dist/types.d.ts +87 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -10
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +185 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
- package/package.json +1 -1
- package/src/camera/Camera.tsx +93 -28
- package/src/index.ts +35 -0
- package/src/stitching/StitcherFrame.ts +197 -0
- package/src/stitching/StitcherWorkletRegistry.ts +156 -0
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +178 -0
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- package/src/stitching/useFrameProcessor.ts +226 -0
- package/src/stitching/useFrameStream.ts +255 -0
- package/src/stitching/useThrottledFrameProcessor.ts +145 -0
- package/src/types.ts +95 -0
|
@@ -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
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// stitcher_worklet_dispatch.cpp — implementation of the v0.8.0
|
|
4
|
+
// Phase 4b.iii per-frame fan-out helper. See header for contract.
|
|
5
|
+
|
|
6
|
+
#include "stitcher_worklet_dispatch.hpp"
|
|
7
|
+
|
|
8
|
+
#include "stitcher_frame_jsi.hpp"
|
|
9
|
+
#include "stitcher_worklet_registry.hpp"
|
|
10
|
+
|
|
11
|
+
#include <react-native-worklets-core/WKTJsiWorklet.h>
|
|
12
|
+
#include <react-native-worklets-core/WKTJsiWorkletContext.h>
|
|
13
|
+
|
|
14
|
+
#include <exception>
|
|
15
|
+
#include <memory>
|
|
16
|
+
#include <utility>
|
|
17
|
+
#include <vector>
|
|
18
|
+
|
|
19
|
+
#if defined(__ANDROID__)
|
|
20
|
+
#include <android/log.h>
|
|
21
|
+
#define DISPATCH_LOG_ERROR(...) \
|
|
22
|
+
__android_log_print(ANDROID_LOG_ERROR, "StitcherWorkletDispatch", __VA_ARGS__)
|
|
23
|
+
#else
|
|
24
|
+
// iOS path uses `os_log` from its caller (RNSARWorkletRuntime.mm);
|
|
25
|
+
// this shared helper isn't invoked from iOS in v0.8.0. Fall back
|
|
26
|
+
// to fprintf for any non-Android build that picks this up.
|
|
27
|
+
#include <cstdio>
|
|
28
|
+
#define DISPATCH_LOG_ERROR(...) std::fprintf(stderr, __VA_ARGS__)
|
|
29
|
+
#endif
|
|
30
|
+
|
|
31
|
+
namespace retailens {
|
|
32
|
+
|
|
33
|
+
void dispatchToHostWorklets(RNWorklet::JsiWorkletContext* context,
|
|
34
|
+
StitcherFrameData data) {
|
|
35
|
+
// Fast-path early-exit when no host worklets are registered.
|
|
36
|
+
// The Android caller (`StitcherWorkletRuntime.dispatchToHostWorklets`)
|
|
37
|
+
// already runs in a hot per-frame loop; saving the host-object
|
|
38
|
+
// alloc + dispatch hop on every frame is meaningful — typical
|
|
39
|
+
// first-party-only deployments will hit this path.
|
|
40
|
+
auto invokers = StitcherWorkletRegistry::shared().snapshot();
|
|
41
|
+
if (invokers.empty()) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (context == nullptr) {
|
|
46
|
+
DISPATCH_LOG_ERROR(
|
|
47
|
+
"dispatchToHostWorklets: context is null; "
|
|
48
|
+
"did Worklets.install() run on the JS side?");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build the JSI host object on the worklet thread (inside the
|
|
53
|
+
// lambda) so JSI access happens on the target runtime.
|
|
54
|
+
// `StitcherFrameJsiHostObject::create` uses the `make_shared`-via-
|
|
55
|
+
// factory pattern (required by `shared_from_this()` inside the
|
|
56
|
+
// `toArrayBuffer` lambda); see its header.
|
|
57
|
+
//
|
|
58
|
+
// Capture `data` by-move so the StitcherFrameData (including the
|
|
59
|
+
// pixel reader's shared_ptr) lives until the lambda runs.
|
|
60
|
+
// Capture `invokers` by-move as well.
|
|
61
|
+
context->invokeOnWorkletThread(
|
|
62
|
+
[invokers = std::move(invokers), data = std::move(data)](
|
|
63
|
+
RNWorklet::JsiWorkletContext* /*ctx*/,
|
|
64
|
+
facebook::jsi::Runtime& rt) mutable {
|
|
65
|
+
auto hostObj = StitcherFrameJsiHostObject::create(std::move(data));
|
|
66
|
+
facebook::jsi::Object frameJsi =
|
|
67
|
+
facebook::jsi::Object::createFromHostObject(rt, hostObj);
|
|
68
|
+
facebook::jsi::Value frameVal(rt, frameJsi);
|
|
69
|
+
|
|
70
|
+
for (const auto& entry : invokers) {
|
|
71
|
+
if (!entry.invoker) continue;
|
|
72
|
+
try {
|
|
73
|
+
entry.invoker->call(rt, facebook::jsi::Value::undefined(),
|
|
74
|
+
&frameVal, 1);
|
|
75
|
+
} catch (const facebook::jsi::JSError& jsErr) {
|
|
76
|
+
// Per-worklet failure isolation: one host worklet
|
|
77
|
+
// throwing must NOT stop the lib's own path or other
|
|
78
|
+
// host worklets. Log + continue. Same three-level
|
|
79
|
+
// catch hierarchy iOS' `RNSARWorkletRuntime` uses.
|
|
80
|
+
DISPATCH_LOG_ERROR(
|
|
81
|
+
"host worklet '%s' threw JS error: %s",
|
|
82
|
+
entry.id.c_str(), jsErr.what());
|
|
83
|
+
} catch (const std::exception& e) {
|
|
84
|
+
DISPATCH_LOG_ERROR(
|
|
85
|
+
"host worklet '%s' threw native exception: %s",
|
|
86
|
+
entry.id.c_str(), e.what());
|
|
87
|
+
} catch (...) {
|
|
88
|
+
DISPATCH_LOG_ERROR(
|
|
89
|
+
"host worklet '%s' threw unknown exception",
|
|
90
|
+
entry.id.c_str());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Invalidate after all worklets finish. Releases the
|
|
95
|
+
// PixelBufferReader's shared_ptr, which (when its refcount
|
|
96
|
+
// drops to 0) drops the underlying buffer — for Android
|
|
97
|
+
// that's the std::vector of copied NV21 bytes; for iOS it
|
|
98
|
+
// would be the CFBridgingRetain'd ARFrame.
|
|
99
|
+
hostObj->invalidate();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
} // namespace retailens
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// stitcher_worklet_dispatch.hpp — shared C++ helper that fans out a
|
|
4
|
+
// `StitcherFrameData` to every host worklet registered in the
|
|
5
|
+
// process-scope `retailens::StitcherWorkletRegistry`.
|
|
6
|
+
//
|
|
7
|
+
// v0.8.0 Phase 4b.iii — used by Android's per-frame fan-out path
|
|
8
|
+
// (`StitcherWorkletRuntime.dispatchToHostWorklets` → JNI binding →
|
|
9
|
+
// this function). Designed to be platform-neutral so iOS' inline
|
|
10
|
+
// dispatch in `RNSARWorkletRuntime.mm` could refactor onto this
|
|
11
|
+
// helper in a later cleanup pass.
|
|
12
|
+
//
|
|
13
|
+
// ## Threading
|
|
14
|
+
//
|
|
15
|
+
// `dispatchToHostWorklets` is safe to call from ANY thread. It
|
|
16
|
+
// posts a lambda onto `context`'s worklet thread via
|
|
17
|
+
// `JsiWorkletContext::invokeOnWorkletThread`. The caller's thread
|
|
18
|
+
// returns immediately (async); the lambda runs later on the
|
|
19
|
+
// worklet thread.
|
|
20
|
+
//
|
|
21
|
+
// `data` is moved into the lambda — including the
|
|
22
|
+
// `std::shared_ptr<PixelBufferReader>` which owns the pixel bytes.
|
|
23
|
+
// The reader (and any underlying buffer it holds) lives until the
|
|
24
|
+
// dispatch lambda completes + the resulting jsi::Object is GC'd by
|
|
25
|
+
// the worklet runtime.
|
|
26
|
+
|
|
27
|
+
#pragma once
|
|
28
|
+
|
|
29
|
+
#include "stitcher_frame_data.hpp"
|
|
30
|
+
|
|
31
|
+
#include <jsi/jsi.h>
|
|
32
|
+
|
|
33
|
+
namespace RNWorklet {
|
|
34
|
+
class JsiWorkletContext;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
namespace retailens {
|
|
38
|
+
|
|
39
|
+
/// Fan out a `StitcherFrameData` to every registered host worklet.
|
|
40
|
+
///
|
|
41
|
+
/// Behaviour:
|
|
42
|
+
///
|
|
43
|
+
/// 1. Fast-path early-exit when `StitcherWorkletRegistry::shared()`
|
|
44
|
+
/// is empty. No host object is constructed; the caller's
|
|
45
|
+
/// thread returns immediately.
|
|
46
|
+
/// 2. Otherwise, the function snapshots the registry, constructs
|
|
47
|
+
/// a `StitcherFrameJsiHostObject` (deferred until inside the
|
|
48
|
+
/// worklet-thread lambda so JSI access happens on the
|
|
49
|
+
/// target runtime), and dispatches via
|
|
50
|
+
/// `context->invokeOnWorkletThread(...)`.
|
|
51
|
+
/// 3. Each registered `RNWorklet::WorkletInvoker` is called with
|
|
52
|
+
/// the host object as its single argument. Per-worklet failure
|
|
53
|
+
/// isolation: exceptions thrown by one invoker do NOT stop
|
|
54
|
+
/// the next invoker (each call is try/catch'd).
|
|
55
|
+
/// 4. After all invokers return, the host object is invalidated;
|
|
56
|
+
/// its underlying `PixelBufferReader` is released so the
|
|
57
|
+
/// caller-provided buffer (NV21 bytes / CVPixelBuffer / etc.)
|
|
58
|
+
/// can be reclaimed.
|
|
59
|
+
///
|
|
60
|
+
/// @param context Worklet runtime to dispatch on. On Android this
|
|
61
|
+
/// is typically `RNWorklet::JsiWorkletContext::
|
|
62
|
+
/// getDefaultInstance()` (worklets-core's default,
|
|
63
|
+
/// set up by `Worklets.install()`). iOS uses its
|
|
64
|
+
/// own context (`RNSARWorkletRuntime::_ctx`).
|
|
65
|
+
/// MUST be non-null and initialized.
|
|
66
|
+
/// @param data Frame data + pixel reader. Moved into the
|
|
67
|
+
/// worklet-thread lambda.
|
|
68
|
+
void dispatchToHostWorklets(RNWorklet::JsiWorkletContext* context,
|
|
69
|
+
StitcherFrameData data);
|
|
70
|
+
|
|
71
|
+
} // namespace retailens
|