react-native-image-stitcher 0.9.0 → 0.10.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 +150 -0
- package/android/build.gradle +10 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +115 -10
- package/cpp/stitcher_worklet_registry.cpp +10 -0
- package/cpp/stitcher_worklet_registry.hpp +10 -0
- package/cpp/tests/CMakeLists.txt +98 -0
- package/cpp/tests/README.md +86 -0
- package/cpp/tests/pose_test.cpp +74 -0
- package/cpp/tests/stitcher_frame_data_test.cpp +132 -0
- package/cpp/tests/stitcher_worklet_registry_test.cpp +195 -0
- package/cpp/tests/stubs/jsi/jsi.h +33 -0
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +34 -0
- package/dist/camera/useCapture.d.ts +1 -1
- package/dist/camera/useCapture.js +1 -1
- package/dist/stitching/incremental.d.ts +41 -0
- package/dist/stitching/useFrameStream.js +52 -37
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +138 -9
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +50 -14
- package/package.json +1 -1
- package/src/camera/useCapture.ts +1 -1
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +276 -0
- package/src/stitching/incremental.ts +42 -0
- package/src/stitching/useFrameStream.ts +55 -39
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// stitcher_frame_data_test.cpp — v0.10.0 audit #9A
|
|
4
|
+
//
|
|
5
|
+
// Sanity coverage for the `StitcherFrameData` POD payload + the
|
|
6
|
+
// `PixelBufferReader` interface contract. The shared C++
|
|
7
|
+
// `StitcherFrameData` is constructed by both the iOS Obj-C++ side
|
|
8
|
+
// (`StitcherFrameHostObject.mm`) and the Android JNI side
|
|
9
|
+
// (`stitcher_frame_jni.cpp`); these tests pin the default-construction
|
|
10
|
+
// invariants both sides depend on (e.g. `hasTranslation=false`,
|
|
11
|
+
// `qw=1.0`).
|
|
12
|
+
//
|
|
13
|
+
// The `PixelBufferReader` tests use a fake-buffer implementation
|
|
14
|
+
// (`FakePixelBufferReader`) to validate the `copyTo` clipping
|
|
15
|
+
// behaviour the docstring promises.
|
|
16
|
+
|
|
17
|
+
#include "stitcher_frame_data.hpp"
|
|
18
|
+
|
|
19
|
+
#include <gtest/gtest.h>
|
|
20
|
+
|
|
21
|
+
#include <cstdint>
|
|
22
|
+
#include <cstring>
|
|
23
|
+
#include <memory>
|
|
24
|
+
#include <vector>
|
|
25
|
+
|
|
26
|
+
using retailens::PixelBufferReader;
|
|
27
|
+
using retailens::StitcherFrameData;
|
|
28
|
+
|
|
29
|
+
namespace {
|
|
30
|
+
|
|
31
|
+
/// Minimal fake reader — backs a fixed byte vector. Used by the
|
|
32
|
+
/// `PixelBufferReader` contract tests below to verify
|
|
33
|
+
/// `copyTo` clips at the smaller of (maxBytes, byteSize).
|
|
34
|
+
class FakePixelBufferReader : public PixelBufferReader {
|
|
35
|
+
public:
|
|
36
|
+
explicit FakePixelBufferReader(std::vector<uint8_t> bytes)
|
|
37
|
+
: _bytes(std::move(bytes)) {}
|
|
38
|
+
|
|
39
|
+
std::size_t byteSize() const override { return _bytes.size(); }
|
|
40
|
+
|
|
41
|
+
std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) override {
|
|
42
|
+
const std::size_t n =
|
|
43
|
+
maxBytes < _bytes.size() ? maxBytes : _bytes.size();
|
|
44
|
+
std::memcpy(dst, _bytes.data(), n);
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private:
|
|
49
|
+
std::vector<uint8_t> _bytes;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
} // namespace
|
|
53
|
+
|
|
54
|
+
// ─── StitcherFrameData default-construction invariants ─────────────
|
|
55
|
+
|
|
56
|
+
TEST(StitcherFrameDataTest, DefaultsAreSafeForJSIDispatch) {
|
|
57
|
+
// The JSI host object's `get()` dispatch keys off these defaults to
|
|
58
|
+
// expose `undefined` for unset fields. In particular:
|
|
59
|
+
// - `hasTranslation == false` → pose.translation === undefined
|
|
60
|
+
// - `arTrackingState.empty()` → arTrackingState === undefined
|
|
61
|
+
// - `qw == 1.0` with rest zero → identity rotation (safe default
|
|
62
|
+
// for non-AR mode where rotation is unknown)
|
|
63
|
+
StitcherFrameData d;
|
|
64
|
+
EXPECT_EQ(d.width, 0);
|
|
65
|
+
EXPECT_EQ(d.height, 0);
|
|
66
|
+
EXPECT_TRUE(d.source.empty());
|
|
67
|
+
EXPECT_TRUE(d.pixelFormat.empty());
|
|
68
|
+
EXPECT_TRUE(d.orientation.empty());
|
|
69
|
+
EXPECT_DOUBLE_EQ(d.timestampNs, 0.0);
|
|
70
|
+
EXPECT_DOUBLE_EQ(d.qx, 0.0);
|
|
71
|
+
EXPECT_DOUBLE_EQ(d.qy, 0.0);
|
|
72
|
+
EXPECT_DOUBLE_EQ(d.qz, 0.0);
|
|
73
|
+
EXPECT_DOUBLE_EQ(d.qw, 1.0); // identity rotation
|
|
74
|
+
EXPECT_DOUBLE_EQ(d.tx, 0.0);
|
|
75
|
+
EXPECT_DOUBLE_EQ(d.ty, 0.0);
|
|
76
|
+
EXPECT_DOUBLE_EQ(d.tz, 0.0);
|
|
77
|
+
EXPECT_FALSE(d.hasTranslation);
|
|
78
|
+
EXPECT_TRUE(d.arTrackingState.empty());
|
|
79
|
+
EXPECT_EQ(d.pixelReader, nullptr);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
TEST(StitcherFrameDataTest, IsCopyable) {
|
|
83
|
+
// `StitcherFrameData` is documented as "value-typed (cheap to copy;
|
|
84
|
+
// ~100 bytes)". Copy needs to deep-copy the strings + bump the
|
|
85
|
+
// pixelReader shared_ptr refcount.
|
|
86
|
+
StitcherFrameData a;
|
|
87
|
+
a.source = "ar";
|
|
88
|
+
a.width = 1920;
|
|
89
|
+
a.height = 1080;
|
|
90
|
+
a.pixelReader = std::make_shared<FakePixelBufferReader>(
|
|
91
|
+
std::vector<uint8_t>{1, 2, 3});
|
|
92
|
+
|
|
93
|
+
StitcherFrameData b = a;
|
|
94
|
+
EXPECT_EQ(b.source, "ar");
|
|
95
|
+
EXPECT_EQ(b.width, 1920);
|
|
96
|
+
EXPECT_EQ(b.height, 1080);
|
|
97
|
+
ASSERT_NE(b.pixelReader, nullptr);
|
|
98
|
+
EXPECT_EQ(b.pixelReader.use_count(), 2); // both a and b hold a ref
|
|
99
|
+
EXPECT_EQ(b.pixelReader->byteSize(), 3u);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── PixelBufferReader contract ────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
TEST(PixelBufferReaderTest, CopyToReturnsAllBytesWhenMaxBytesExceedsSize) {
|
|
105
|
+
FakePixelBufferReader reader({0x11, 0x22, 0x33});
|
|
106
|
+
uint8_t buf[8] = {0};
|
|
107
|
+
const std::size_t written = reader.copyTo(buf, sizeof(buf));
|
|
108
|
+
EXPECT_EQ(written, 3u);
|
|
109
|
+
EXPECT_EQ(buf[0], 0x11);
|
|
110
|
+
EXPECT_EQ(buf[1], 0x22);
|
|
111
|
+
EXPECT_EQ(buf[2], 0x33);
|
|
112
|
+
EXPECT_EQ(buf[3], 0u); // untouched tail
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
TEST(PixelBufferReaderTest, CopyToClipsWhenMaxBytesIsSmaller) {
|
|
116
|
+
// Contract per stitcher_frame_data.hpp: "Implementations MUST handle
|
|
117
|
+
// the case where maxBytes < byteSize() (clip silently)."
|
|
118
|
+
FakePixelBufferReader reader({0xAA, 0xBB, 0xCC, 0xDD});
|
|
119
|
+
uint8_t buf[2] = {0};
|
|
120
|
+
const std::size_t written = reader.copyTo(buf, sizeof(buf));
|
|
121
|
+
EXPECT_EQ(written, 2u);
|
|
122
|
+
EXPECT_EQ(buf[0], 0xAA);
|
|
123
|
+
EXPECT_EQ(buf[1], 0xBB);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
TEST(PixelBufferReaderTest, CopyToWithZeroMaxBytesReturnsZero) {
|
|
127
|
+
FakePixelBufferReader reader({0x01, 0x02, 0x03});
|
|
128
|
+
uint8_t dummy = 0xFF;
|
|
129
|
+
const std::size_t written = reader.copyTo(&dummy, 0);
|
|
130
|
+
EXPECT_EQ(written, 0u);
|
|
131
|
+
EXPECT_EQ(dummy, 0xFF); // dst untouched when maxBytes == 0
|
|
132
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// stitcher_worklet_registry_test.cpp — v0.10.0 audit #9A
|
|
4
|
+
//
|
|
5
|
+
// Lifecycle + concurrency coverage for the process-scope native
|
|
6
|
+
// `StitcherWorkletRegistry` introduced in v0.8.0 Phase 4b. See
|
|
7
|
+
// `cpp/stitcher_worklet_registry.hpp` for the public contract.
|
|
8
|
+
//
|
|
9
|
+
// The registry's production `install(runtime, workletValue)` path
|
|
10
|
+
// constructs an `RNWorklet::WorkletInvoker` from a JSI `Runtime` and
|
|
11
|
+
// `Value`. Standing up a real worklets-core runtime under gtest would
|
|
12
|
+
// pull in Hermes + JSI + the whole worklets-core library — too heavy
|
|
13
|
+
// for these scope-limited storage tests. Instead we exercise the
|
|
14
|
+
// equivalent `_installEntryForTests(invoker)` test seam (mirrors
|
|
15
|
+
// `_resetForTests` in pattern) with `nullptr` invokers. The registry
|
|
16
|
+
// never dereferences the pointer — it only stores the shared_ptr and
|
|
17
|
+
// hands it back via `snapshot` — so nullptr is safe.
|
|
18
|
+
//
|
|
19
|
+
// What this covers (lifting from the production `install`/`uninstall`/
|
|
20
|
+
// `snapshot`/`count` contract):
|
|
21
|
+
// - shared() returns the same instance across calls
|
|
22
|
+
// - count/snapshot start empty after _resetForTests
|
|
23
|
+
// - install assigns monotonically increasing host-N IDs
|
|
24
|
+
// - count tracks installs / uninstalls
|
|
25
|
+
// - uninstall of an unknown ID is a no-op (matches JS side)
|
|
26
|
+
// - snapshot returns an independent copy (mutations after snapshot
|
|
27
|
+
// don't affect the snapshot's view)
|
|
28
|
+
// - concurrent installs from many threads serialise correctly and
|
|
29
|
+
// yield unique IDs (no double-issue under contention)
|
|
30
|
+
|
|
31
|
+
#include "stitcher_worklet_registry.hpp"
|
|
32
|
+
#include <react-native-worklets-core/WKTJsiWorklet.h>
|
|
33
|
+
|
|
34
|
+
#include <gtest/gtest.h>
|
|
35
|
+
|
|
36
|
+
#include <algorithm>
|
|
37
|
+
#include <atomic>
|
|
38
|
+
#include <chrono>
|
|
39
|
+
#include <set>
|
|
40
|
+
#include <string>
|
|
41
|
+
#include <thread>
|
|
42
|
+
#include <vector>
|
|
43
|
+
|
|
44
|
+
using retailens::StitcherWorkletEntry;
|
|
45
|
+
using retailens::StitcherWorkletRegistry;
|
|
46
|
+
|
|
47
|
+
namespace {
|
|
48
|
+
|
|
49
|
+
/// Test fixture — resets the singleton before each test so cases
|
|
50
|
+
/// don't leak state into each other. Without this every test would
|
|
51
|
+
/// see the cumulative entries from prior tests in the run.
|
|
52
|
+
class StitcherWorkletRegistryTest : public ::testing::Test {
|
|
53
|
+
protected:
|
|
54
|
+
void SetUp() override {
|
|
55
|
+
StitcherWorkletRegistry::shared()._resetForTests();
|
|
56
|
+
}
|
|
57
|
+
void TearDown() override {
|
|
58
|
+
StitcherWorkletRegistry::shared()._resetForTests();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
} // namespace
|
|
63
|
+
|
|
64
|
+
TEST_F(StitcherWorkletRegistryTest, SharedReturnsSameInstance) {
|
|
65
|
+
// The singleton invariant is load-bearing for the JS-side mental
|
|
66
|
+
// model: `useFrameProcessor` mounts on one component and unmounts
|
|
67
|
+
// from another but the registry stays.
|
|
68
|
+
StitcherWorkletRegistry& a = StitcherWorkletRegistry::shared();
|
|
69
|
+
StitcherWorkletRegistry& b = StitcherWorkletRegistry::shared();
|
|
70
|
+
EXPECT_EQ(&a, &b);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
TEST_F(StitcherWorkletRegistryTest, StartsEmptyAfterReset) {
|
|
74
|
+
auto& r = StitcherWorkletRegistry::shared();
|
|
75
|
+
EXPECT_EQ(r.count(), 0u);
|
|
76
|
+
EXPECT_TRUE(r.snapshot().empty());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
TEST_F(StitcherWorkletRegistryTest, InstallAssignsHostPrefixedIncrementingIds) {
|
|
80
|
+
auto& r = StitcherWorkletRegistry::shared();
|
|
81
|
+
const std::string id0 = r._installEntryForTests(nullptr);
|
|
82
|
+
const std::string id1 = r._installEntryForTests(nullptr);
|
|
83
|
+
const std::string id2 = r._installEntryForTests(nullptr);
|
|
84
|
+
|
|
85
|
+
// IDs are "host-N" with N monotonically increasing from 0 after a
|
|
86
|
+
// reset. The format is part of the public contract — uninstall
|
|
87
|
+
// callers store these IDs verbatim.
|
|
88
|
+
EXPECT_EQ(id0, "host-0");
|
|
89
|
+
EXPECT_EQ(id1, "host-1");
|
|
90
|
+
EXPECT_EQ(id2, "host-2");
|
|
91
|
+
EXPECT_EQ(r.count(), 3u);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
TEST_F(StitcherWorkletRegistryTest, UninstallRemovesByIdAndUpdatesCount) {
|
|
95
|
+
auto& r = StitcherWorkletRegistry::shared();
|
|
96
|
+
const std::string id0 = r._installEntryForTests(nullptr);
|
|
97
|
+
const std::string id1 = r._installEntryForTests(nullptr);
|
|
98
|
+
const std::string id2 = r._installEntryForTests(nullptr);
|
|
99
|
+
EXPECT_EQ(r.count(), 3u);
|
|
100
|
+
|
|
101
|
+
r.uninstall(id1); // remove the middle entry
|
|
102
|
+
EXPECT_EQ(r.count(), 2u);
|
|
103
|
+
|
|
104
|
+
// Snapshot should contain only id0 and id2, in some order — the
|
|
105
|
+
// contract doesn't promise insertion order survives uninstall
|
|
106
|
+
// (the erase-remove pattern is stable in practice but we don't
|
|
107
|
+
// pin that publicly).
|
|
108
|
+
const auto snap = r.snapshot();
|
|
109
|
+
std::vector<std::string> remainingIds;
|
|
110
|
+
remainingIds.reserve(snap.size());
|
|
111
|
+
for (const auto& entry : snap) {
|
|
112
|
+
remainingIds.push_back(entry.id);
|
|
113
|
+
}
|
|
114
|
+
std::sort(remainingIds.begin(), remainingIds.end());
|
|
115
|
+
EXPECT_EQ(remainingIds, (std::vector<std::string>{id0, id2}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
TEST_F(StitcherWorkletRegistryTest, UninstallOfUnknownIdIsNoop) {
|
|
119
|
+
// Matches the JS-side `StitcherWorkletRegistry.uninstall` semantics
|
|
120
|
+
// (idempotent, no throw on unknown). Critical because the JS
|
|
121
|
+
// useEffect cleanup can fire after an unmount/remount race where the
|
|
122
|
+
// ID has already been removed.
|
|
123
|
+
auto& r = StitcherWorkletRegistry::shared();
|
|
124
|
+
r._installEntryForTests(nullptr);
|
|
125
|
+
EXPECT_EQ(r.count(), 1u);
|
|
126
|
+
EXPECT_NO_THROW(r.uninstall("host-does-not-exist"));
|
|
127
|
+
EXPECT_NO_THROW(r.uninstall(""));
|
|
128
|
+
EXPECT_EQ(r.count(), 1u); // existing entry untouched
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
TEST_F(StitcherWorkletRegistryTest, SnapshotIsIndependentOfFutureMutations) {
|
|
132
|
+
// Per header docstring: "mutations against the registry after
|
|
133
|
+
// `snapshot` returns do not affect the snapshot." This matters for
|
|
134
|
+
// the AR-session dispatch path, which snapshots and then iterates
|
|
135
|
+
// without holding the registry lock — concurrent uninstall on the
|
|
136
|
+
// JS thread must NOT invalidate the snapshot.
|
|
137
|
+
auto& r = StitcherWorkletRegistry::shared();
|
|
138
|
+
const std::string id0 = r._installEntryForTests(nullptr);
|
|
139
|
+
const std::string id1 = r._installEntryForTests(nullptr);
|
|
140
|
+
|
|
141
|
+
const auto snap = r.snapshot();
|
|
142
|
+
ASSERT_EQ(snap.size(), 2u);
|
|
143
|
+
|
|
144
|
+
r.uninstall(id0);
|
|
145
|
+
r.uninstall(id1);
|
|
146
|
+
EXPECT_EQ(r.count(), 0u);
|
|
147
|
+
|
|
148
|
+
// Snapshot still has both entries.
|
|
149
|
+
EXPECT_EQ(snap.size(), 2u);
|
|
150
|
+
EXPECT_EQ(snap[0].id, id0);
|
|
151
|
+
EXPECT_EQ(snap[1].id, id1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
TEST_F(StitcherWorkletRegistryTest, ConcurrentInstallsYieldUniqueIds) {
|
|
155
|
+
// Many threads racing `_installEntryForTests` simultaneously must
|
|
156
|
+
// not see duplicate IDs (would indicate the mutex around _nextId is
|
|
157
|
+
// missing or broken). This is the per-instance equivalent of
|
|
158
|
+
// TransferredNV21Test's "only one of N concurrent callers wins"
|
|
159
|
+
// pattern, adapted for "all N callers succeed but produce distinct
|
|
160
|
+
// IDs".
|
|
161
|
+
auto& r = StitcherWorkletRegistry::shared();
|
|
162
|
+
constexpr int kThreads = 16;
|
|
163
|
+
constexpr int kPerThread = 32; // 512 total installs
|
|
164
|
+
|
|
165
|
+
std::vector<std::thread> workers;
|
|
166
|
+
workers.reserve(kThreads);
|
|
167
|
+
std::vector<std::vector<std::string>> idsPerThread(kThreads);
|
|
168
|
+
|
|
169
|
+
std::atomic<bool> go{false};
|
|
170
|
+
for (int t = 0; t < kThreads; ++t) {
|
|
171
|
+
workers.emplace_back([&, t]() {
|
|
172
|
+
while (!go.load(std::memory_order_acquire)) {
|
|
173
|
+
std::this_thread::yield();
|
|
174
|
+
}
|
|
175
|
+
for (int i = 0; i < kPerThread; ++i) {
|
|
176
|
+
idsPerThread[t].push_back(r._installEntryForTests(nullptr));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
go.store(true, std::memory_order_release);
|
|
181
|
+
for (auto& w : workers) w.join();
|
|
182
|
+
|
|
183
|
+
// Aggregate every ID issued. Total count = kThreads * kPerThread;
|
|
184
|
+
// uniqueness = the set size matches.
|
|
185
|
+
std::set<std::string> allIds;
|
|
186
|
+
for (const auto& v : idsPerThread) {
|
|
187
|
+
for (const auto& id : v) {
|
|
188
|
+
allIds.insert(id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
EXPECT_EQ(allIds.size(),
|
|
192
|
+
static_cast<std::size_t>(kThreads * kPerThread));
|
|
193
|
+
EXPECT_EQ(r.count(),
|
|
194
|
+
static_cast<std::size_t>(kThreads * kPerThread));
|
|
195
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// jsi.h — TEST-ONLY stub of facebook::jsi types.
|
|
4
|
+
//
|
|
5
|
+
// The real jsi.h ships with React Native and pulls in a large surface
|
|
6
|
+
// area (Runtime, Value, Object, Function, HostObject, HostFunction,
|
|
7
|
+
// Array, ArrayBuffer, PropNameID, …) along with the build infra to
|
|
8
|
+
// link them. For pure-C++ unit tests that exercise data-structure
|
|
9
|
+
// invariants of code that REFERENCES jsi types but never CALLS into
|
|
10
|
+
// them (e.g. `StitcherWorkletRegistry` storing a `shared_ptr` and
|
|
11
|
+
// forwarding `Runtime&` to a constructor stub), we only need the
|
|
12
|
+
// types to be NAMED so headers compile.
|
|
13
|
+
//
|
|
14
|
+
// Pattern: this stub is placed first on the test target's include
|
|
15
|
+
// path so `#include <jsi/jsi.h>` resolves here instead of to RN's
|
|
16
|
+
// real header. Production builds NEVER see this file — it lives
|
|
17
|
+
// only under `cpp/tests/stubs/`, which is referenced exclusively by
|
|
18
|
+
// `cpp/tests/CMakeLists.txt`.
|
|
19
|
+
//
|
|
20
|
+
// Tests that need to actually CONSTRUCT or CALL into JSI types should
|
|
21
|
+
// not use this stub — they should run against a real JSI runtime (a
|
|
22
|
+
// future v0.11.0+ test target that links Hermes).
|
|
23
|
+
|
|
24
|
+
#pragma once
|
|
25
|
+
|
|
26
|
+
namespace facebook {
|
|
27
|
+
namespace jsi {
|
|
28
|
+
|
|
29
|
+
class Runtime {};
|
|
30
|
+
class Value {};
|
|
31
|
+
|
|
32
|
+
} // namespace jsi
|
|
33
|
+
} // namespace facebook
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// WKTJsiWorklet.h — TEST-ONLY stub of RNWorklet::WorkletInvoker.
|
|
4
|
+
//
|
|
5
|
+
// `cpp/stitcher_worklet_registry.cpp` constructs a
|
|
6
|
+
// `std::make_shared<RNWorklet::WorkletInvoker>(runtime, value)` inside
|
|
7
|
+
// `install`. The real WorkletInvoker (from react-native-worklets-core)
|
|
8
|
+
// captures the worklet's source / closure / runtime affinity and is
|
|
9
|
+
// non-trivial to stand up in a unit-test context.
|
|
10
|
+
//
|
|
11
|
+
// This stub provides JUST the symbols needed for the registry to
|
|
12
|
+
// compile and link. The constructor and destructor are no-ops; calling
|
|
13
|
+
// methods on a stub invoker is undefined behaviour, but the registry
|
|
14
|
+
// itself never does (it only stores the shared_ptr and hands it out
|
|
15
|
+
// via `snapshot`). Tests construct entries directly via
|
|
16
|
+
// `_installEntryForTests(nullptr)` to avoid even the trivial
|
|
17
|
+
// allocation.
|
|
18
|
+
//
|
|
19
|
+
// See cpp/tests/stubs/jsi/jsi.h for the parallel stub of facebook::jsi.
|
|
20
|
+
|
|
21
|
+
#pragma once
|
|
22
|
+
|
|
23
|
+
#include <jsi/jsi.h>
|
|
24
|
+
|
|
25
|
+
namespace RNWorklet {
|
|
26
|
+
|
|
27
|
+
class WorkletInvoker {
|
|
28
|
+
public:
|
|
29
|
+
WorkletInvoker(facebook::jsi::Runtime& /*runtime*/,
|
|
30
|
+
const facebook::jsi::Value& /*workletValue*/) {}
|
|
31
|
+
~WorkletInvoker() = default;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
} // namespace RNWorklet
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - This hook does NOT persist captures. Host apps hand the
|
|
18
18
|
* returned CaptureResult to their own storage layer (WatermelonDB
|
|
19
19
|
* insert, Redux dispatch, whatever).
|
|
20
|
-
* - Video recording lives in useVideoCapture
|
|
20
|
+
* - Video recording lives in useVideoCapture.
|
|
21
21
|
*
|
|
22
22
|
* The public API is designed to be minimal and replaceable: host apps
|
|
23
23
|
* that prefer the raw vision-camera API can opt out of this hook and
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - This hook does NOT persist captures. Host apps hand the
|
|
20
20
|
* returned CaptureResult to their own storage layer (WatermelonDB
|
|
21
21
|
* insert, Redux dispatch, whatever).
|
|
22
|
-
* - Video recording lives in useVideoCapture
|
|
22
|
+
* - Video recording lives in useVideoCapture.
|
|
23
23
|
*
|
|
24
24
|
* The public API is designed to be minimal and replaceable: host apps
|
|
25
25
|
* that prefer the raw vision-camera API can opt out of this hook and
|
|
@@ -249,6 +249,47 @@ export interface IncrementalState {
|
|
|
249
249
|
* keyframes on disk.
|
|
250
250
|
*/
|
|
251
251
|
refinedPanoramaPath?: string;
|
|
252
|
+
/**
|
|
253
|
+
* v0.10.0 (#15A) — current phase of an in-flight `refinePanorama`
|
|
254
|
+
* call. Fires from both the explicit `module.refinePanorama(...)`
|
|
255
|
+
* JS API path AND the hybrid-engine auto-refine path (which calls
|
|
256
|
+
* the same native refinePanorama internally).
|
|
257
|
+
*
|
|
258
|
+
* Lifecycle:
|
|
259
|
+
* - `"validating"` (fraction 0.05) — synchronous input checks
|
|
260
|
+
* - `"stitching"` (fraction 0.10) — OpenCV stitch in flight
|
|
261
|
+
* - `"writing"` (fraction 0.90) — stitch done, JPEG written
|
|
262
|
+
* - `"done"` (fraction 1.00) — success
|
|
263
|
+
* - `"error"` (fraction 1.00) — failure; `refineError` is set
|
|
264
|
+
*
|
|
265
|
+
* Coarse on purpose: OpenCV's Stitcher doesn't expose mid-pipeline
|
|
266
|
+
* progress, so the 0.10 → 0.90 jump is one opaque step. Use
|
|
267
|
+
* `refineStage` for a stage label; use `refineProgress` purely for
|
|
268
|
+
* spinner progress.
|
|
269
|
+
*
|
|
270
|
+
* Undefined when no refinement is in flight.
|
|
271
|
+
*/
|
|
272
|
+
refineStage?: 'validating' | 'stitching' | 'writing' | 'done' | 'error';
|
|
273
|
+
/**
|
|
274
|
+
* v0.10.0 (#15A) — coarse progress fraction in `[0, 1]` aligned
|
|
275
|
+
* with `refineStage`. See `refineStage` for the per-stage value
|
|
276
|
+
* mapping. Undefined when no refinement is in flight.
|
|
277
|
+
*/
|
|
278
|
+
refineProgress?: number;
|
|
279
|
+
/**
|
|
280
|
+
* v0.10.0 (#15A) — number of input frames the in-flight refine is
|
|
281
|
+
* processing. Useful for the UI label
|
|
282
|
+
* (`Stitching 6 frames…`). Mirrors the `framesRequested` field
|
|
283
|
+
* returned in the explicit refinePanorama resolution. Undefined
|
|
284
|
+
* when no refinement is in flight.
|
|
285
|
+
*/
|
|
286
|
+
refineFrames?: number;
|
|
287
|
+
/**
|
|
288
|
+
* v0.10.0 (#15A) — present only when `refineStage === 'error'`.
|
|
289
|
+
* Human-readable error message; the same text the rejected promise
|
|
290
|
+
* carries. Use to render a one-line failure pill.
|
|
291
|
+
*/
|
|
292
|
+
refineError?: string;
|
|
252
293
|
}
|
|
253
294
|
export interface IncrementalStartOptions {
|
|
254
295
|
/**
|
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
62
|
exports.useFrameStream = useFrameStream;
|
|
63
63
|
const react_1 = require("react");
|
|
64
|
-
const react_native_1 = require("react-native");
|
|
65
64
|
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
66
65
|
const react_native_worklets_core_1 = require("react-native-worklets-core");
|
|
67
66
|
const useThrottledFrameProcessor_1 = require("./useThrottledFrameProcessor");
|
|
67
|
+
const files_1 = require("../utils/files");
|
|
68
68
|
/**
|
|
69
69
|
* `useFrameStream` — Layer 3. See module docstring for the full
|
|
70
70
|
* design + use-case mapping. Quick start:
|
|
@@ -98,37 +98,38 @@ const useThrottledFrameProcessor_1 = require("./useThrottledFrameProcessor");
|
|
|
98
98
|
function useFrameStream(options, handler) {
|
|
99
99
|
const sampleHz = Math.max(0.5, Math.min(10, options.sampleHz));
|
|
100
100
|
const quality = options.quality ?? 75;
|
|
101
|
-
// Default output dir:
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
101
|
+
// Default output dir: the lib's canonical capture dir resolved
|
|
102
|
+
// via `FileBridge.defaultCaptureDir()`. Same dir the lib uses
|
|
103
|
+
// for panorama JPEGs / keyframe JPEGs — guaranteed writable on
|
|
104
|
+
// both platforms (iOS NSCachesDirectory + Android Context.cacheDir),
|
|
105
|
+
// created if missing. Resolved async on first mount; until
|
|
106
|
+
// resolution completes the worklet's `outputDir` is empty and
|
|
107
|
+
// the plugin call no-ops silently (a few frames missed at most;
|
|
108
|
+
// typical resolution time is <50ms).
|
|
109
|
+
//
|
|
110
|
+
// Hosts that want a specific path supply `options.outputDir`
|
|
111
|
+
// and skip the resolution entirely.
|
|
112
|
+
const [resolvedDefaultDir, setResolvedDefaultDir] = (0, react_1.useState)('');
|
|
113
|
+
(0, react_1.useEffect)(() => {
|
|
105
114
|
if (options.outputDir != null)
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
return;
|
|
116
|
+
let cancelled = false;
|
|
117
|
+
(0, files_1.getDefaultCaptureDir)()
|
|
118
|
+
.then((dir) => {
|
|
119
|
+
if (!cancelled)
|
|
120
|
+
setResolvedDefaultDir(dir);
|
|
121
|
+
})
|
|
122
|
+
.catch((err) => {
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.warn('[useFrameStream] FileBridge.defaultCaptureDir() failed; ' +
|
|
125
|
+
'samples will not fire until `options.outputDir` is supplied. ' +
|
|
126
|
+
String(err));
|
|
127
|
+
});
|
|
128
|
+
return () => {
|
|
129
|
+
cancelled = true;
|
|
130
|
+
};
|
|
121
131
|
}, [options.outputDir]);
|
|
122
|
-
|
|
123
|
-
// react-native-fs but to keep the dep surface minimal, we just
|
|
124
|
-
// attempt to create via a tiny native call — or, simpler, accept
|
|
125
|
-
// that the plugin's write call will fail if the dir doesn't
|
|
126
|
-
// exist + log a clear error. For v0.9.0 baseline we defer
|
|
127
|
-
// mkdir to the host (document it in the option's JSDoc) OR fall
|
|
128
|
-
// back to the platform's tmpdir which already exists.
|
|
129
|
-
//
|
|
130
|
-
// The tmpdir defaults above always exist on iOS + Android, so
|
|
131
|
-
// the common case "host doesn't supply outputDir" Just Works.
|
|
132
|
+
const outputDir = options.outputDir ?? resolvedDefaultDir;
|
|
132
133
|
// Stable JS-side handler reference for `runOnJS`. The hook re-
|
|
133
134
|
// captures `handler` on every render but the ref keeps the
|
|
134
135
|
// worklet closure pointing at the latest callback (avoid stale
|
|
@@ -152,18 +153,33 @@ function useFrameStream(options, handler) {
|
|
|
152
153
|
// registry hasn't initialised yet (rare race on app start). We
|
|
153
154
|
// retry every 16ms (one display frame) until success — matches
|
|
154
155
|
// the pattern in `useFrameProcessorDriver`.
|
|
155
|
-
|
|
156
|
+
//
|
|
157
|
+
// Use `useState` (not `useRef`) so the eventual non-null value
|
|
158
|
+
// triggers a re-render — the worklet closure below captures
|
|
159
|
+
// `plugin` by value at render time, so without state we'd
|
|
160
|
+
// capture `null` forever.
|
|
161
|
+
const [plugin, setPlugin] = (0, react_1.useState)(null);
|
|
156
162
|
(0, react_1.useEffect)(() => {
|
|
157
163
|
let cancelled = false;
|
|
158
164
|
let timerId = null;
|
|
165
|
+
let attempts = 0;
|
|
159
166
|
const tryAcquire = () => {
|
|
160
167
|
if (cancelled)
|
|
161
168
|
return;
|
|
169
|
+
attempts += 1;
|
|
162
170
|
const p = react_native_vision_camera_1.VisionCameraProxy.initFrameProcessorPlugin('save_frame_as_jpeg', {});
|
|
163
171
|
if (p != null) {
|
|
164
|
-
|
|
172
|
+
setPlugin(p);
|
|
165
173
|
return;
|
|
166
174
|
}
|
|
175
|
+
// After ~1s of failed retries, warn once — the plugin should
|
|
176
|
+
// be registered by then; persistent failure means the host's
|
|
177
|
+
// native bundle doesn't include `save_frame_as_jpeg`.
|
|
178
|
+
if (attempts === 60) {
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.warn('[useFrameStream] save_frame_as_jpeg plugin not found after 1s of retries. ' +
|
|
181
|
+
'Verify react-native-image-stitcher native module is installed in your host app.');
|
|
182
|
+
}
|
|
167
183
|
timerId = setTimeout(tryAcquire, 16);
|
|
168
184
|
};
|
|
169
185
|
tryAcquire();
|
|
@@ -173,15 +189,14 @@ function useFrameStream(options, handler) {
|
|
|
173
189
|
clearTimeout(timerId);
|
|
174
190
|
};
|
|
175
191
|
}, []);
|
|
176
|
-
// The worklet body — fires at sampleHz, calls the JPEG plugin,
|
|
177
|
-
// bridges the result to JS. Note we read `pluginRef.current`
|
|
178
|
-
// inside the worklet via the captured `plugin` value below;
|
|
179
|
-
// worklets-core handles the JS↔worklet reference.
|
|
180
|
-
const plugin = pluginRef.current;
|
|
181
192
|
return (0, useThrottledFrameProcessor_1.useThrottledFrameProcessor)((frame) => {
|
|
182
193
|
'worklet';
|
|
183
194
|
if (plugin == null)
|
|
184
195
|
return;
|
|
196
|
+
// Async outputDir resolution may not have completed yet on
|
|
197
|
+
// the first few frames after mount — bail until it does.
|
|
198
|
+
if (outputDir === '')
|
|
199
|
+
return;
|
|
185
200
|
// Slot rotation: compute slot from frame timestamp. At
|
|
186
201
|
// sampleHz=2 (500ms interval), the slot index changes every
|
|
187
202
|
// ~1s, giving each slot ~2 samples before being overwritten.
|