react-native-image-stitcher 0.9.0 → 0.11.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 +246 -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/Camera.d.ts +30 -14
- package/dist/camera/Camera.js +18 -18
- package/dist/camera/useCapture.d.ts +1 -1
- package/dist/camera/useCapture.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/stitching/incremental.d.ts +41 -0
- package/dist/stitching/useFrameProcessorDriver.d.ts +50 -95
- package/dist/stitching/useFrameProcessorDriver.js +76 -294
- package/dist/stitching/useFrameStream.js +52 -37
- package/dist/stitching/useStitcherWorklet.d.ts +185 -0
- package/dist/stitching/useStitcherWorklet.js +275 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +138 -9
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +50 -14
- package/package.json +1 -1
- package/src/camera/Camera.tsx +48 -32
- package/src/camera/useCapture.ts +1 -1
- package/src/index.ts +13 -0
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +276 -0
- package/src/stitching/incremental.ts +42 -0
- package/src/stitching/useFrameProcessorDriver.ts +79 -320
- package/src/stitching/useFrameStream.ts +55 -39
- package/src/stitching/useStitcherWorklet.ts +390 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// pose_test.cpp — v0.10.0 audit #9A
|
|
4
|
+
//
|
|
5
|
+
// Layout / size invariants for the cross-platform POD structs that
|
|
6
|
+
// marshal AR-frame pose data between Swift/Kotlin and shared C++.
|
|
7
|
+
// The Pose / PlaneTransform structs MUST stay binary-compatible
|
|
8
|
+
// across iOS (Swift → C++) and Android (Kotlin → JNI → C++) — any
|
|
9
|
+
// silent field reorder, padding shift, or size change would diverge
|
|
10
|
+
// gate decisions between platforms.
|
|
11
|
+
//
|
|
12
|
+
// These are pinned to the contract in `cpp/ar_frame_pose.h`'s
|
|
13
|
+
// docstring; if the struct shape evolves intentionally, update both
|
|
14
|
+
// the docstring and these tests in the same commit.
|
|
15
|
+
|
|
16
|
+
#include "ar_frame_pose.h"
|
|
17
|
+
|
|
18
|
+
#include <gtest/gtest.h>
|
|
19
|
+
|
|
20
|
+
#include <cstddef>
|
|
21
|
+
#include <type_traits>
|
|
22
|
+
|
|
23
|
+
using retailens::Pose;
|
|
24
|
+
using retailens::PlaneTransform;
|
|
25
|
+
|
|
26
|
+
TEST(PoseLayoutTest, IsStandardLayoutPod) {
|
|
27
|
+
// Required for `memcpy` marshalling and for the iOS Obj-C++ /
|
|
28
|
+
// Android JNI bridges to write into the struct directly.
|
|
29
|
+
EXPECT_TRUE(std::is_standard_layout<Pose>::value);
|
|
30
|
+
EXPECT_TRUE(std::is_trivially_copyable<Pose>::value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
TEST(PoseLayoutTest, SizeMatchesExpectedFields) {
|
|
34
|
+
// 11 floats (tx, ty, tz, qx, qy, qz, qw, fx, fy, cx, cy) + 2 int32_t
|
|
35
|
+
// (imageWidth, imageHeight) = 11*4 + 2*4 = 52 bytes. No padding
|
|
36
|
+
// expected: every field is 4-byte aligned and the struct contains
|
|
37
|
+
// only 4-byte primitives.
|
|
38
|
+
EXPECT_EQ(sizeof(Pose), static_cast<std::size_t>(11 * 4 + 2 * 4));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
TEST(PoseLayoutTest, FieldOrderMatchesContract) {
|
|
42
|
+
// Translation comes before rotation; rotation before intrinsics;
|
|
43
|
+
// intrinsics before image dimensions. Swift / Kotlin marshallers
|
|
44
|
+
// assume this order — flipping any pair silently breaks the
|
|
45
|
+
// memcpy-based bridge.
|
|
46
|
+
EXPECT_EQ(offsetof(Pose, tx), 0u);
|
|
47
|
+
EXPECT_EQ(offsetof(Pose, ty), sizeof(float) * 1);
|
|
48
|
+
EXPECT_EQ(offsetof(Pose, tz), sizeof(float) * 2);
|
|
49
|
+
EXPECT_EQ(offsetof(Pose, qx), sizeof(float) * 3);
|
|
50
|
+
EXPECT_EQ(offsetof(Pose, qy), sizeof(float) * 4);
|
|
51
|
+
EXPECT_EQ(offsetof(Pose, qz), sizeof(float) * 5);
|
|
52
|
+
EXPECT_EQ(offsetof(Pose, qw), sizeof(float) * 6);
|
|
53
|
+
EXPECT_EQ(offsetof(Pose, fx), sizeof(float) * 7);
|
|
54
|
+
EXPECT_EQ(offsetof(Pose, fy), sizeof(float) * 8);
|
|
55
|
+
EXPECT_EQ(offsetof(Pose, cx), sizeof(float) * 9);
|
|
56
|
+
EXPECT_EQ(offsetof(Pose, cy), sizeof(float) * 10);
|
|
57
|
+
EXPECT_EQ(offsetof(Pose, imageWidth), sizeof(float) * 11);
|
|
58
|
+
EXPECT_EQ(offsetof(Pose, imageHeight),
|
|
59
|
+
sizeof(float) * 11 + sizeof(int32_t));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
TEST(PlaneTransformLayoutTest, IsStandardLayoutPod) {
|
|
63
|
+
EXPECT_TRUE(std::is_standard_layout<PlaneTransform>::value);
|
|
64
|
+
EXPECT_TRUE(std::is_trivially_copyable<PlaneTransform>::value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
TEST(PlaneTransformLayoutTest, SixteenFloatsContiguous) {
|
|
68
|
+
// The `m[16]` array MUST be a contiguous 64-byte block — both
|
|
69
|
+
// bridges call `memcpy(planeTransform.m, source, 64)`. No leading
|
|
70
|
+
// padding, no field reorder (there's only one field, but pinning the
|
|
71
|
+
// size catches any accidental wrapper/struct change).
|
|
72
|
+
EXPECT_EQ(sizeof(PlaneTransform), static_cast<std::size_t>(16 * 4));
|
|
73
|
+
EXPECT_EQ(offsetof(PlaneTransform, m), 0u);
|
|
74
|
+
}
|
|
@@ -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
|
package/dist/camera/Camera.d.ts
CHANGED
|
@@ -221,26 +221,42 @@ export interface CameraProps {
|
|
|
221
221
|
* }
|
|
222
222
|
* ```
|
|
223
223
|
*
|
|
224
|
-
* ## Non-AR mode
|
|
224
|
+
* ## Non-AR mode composition (v0.11.0+)
|
|
225
225
|
*
|
|
226
226
|
* vision-camera's `<Camera>` accepts ONLY ONE frame processor.
|
|
227
227
|
* The lib's internal `useFrameProcessorDriver` produces the
|
|
228
228
|
* processor that drives first-party panorama stitching in non-AR
|
|
229
|
-
* mode. If you supply your own via this prop,
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
229
|
+
* mode. If you supply your own via this prop, the lib's
|
|
230
|
+
* default processor is REPLACED — but as of v0.11.0 you can
|
|
231
|
+
* COMPOSE first-party stitching back into your worklet body
|
|
232
|
+
* using `useStitcherWorklet`:
|
|
233
233
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
234
|
+
* ```tsx
|
|
235
|
+
* import {
|
|
236
|
+
* Camera, useFrameProcessor, useStitcherWorklet,
|
|
237
|
+
* type StitcherFrame,
|
|
238
|
+
* } from 'react-native-image-stitcher';
|
|
239
|
+
*
|
|
240
|
+
* function MyScreen() {
|
|
241
|
+
* const stitcher = useStitcherWorklet();
|
|
242
|
+
* const fp = useFrameProcessor((frame: StitcherFrame) => {
|
|
243
|
+
* 'worklet';
|
|
244
|
+
* hostPreLogic(frame);
|
|
245
|
+
* stitcher.call(frame); // ← first-party stitching
|
|
246
|
+
* hostPostLogic(frame);
|
|
247
|
+
* }, [stitcher.call]);
|
|
248
|
+
* return <Camera frameProcessor={fp} ... />;
|
|
249
|
+
* }
|
|
250
|
+
* ```
|
|
239
251
|
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
252
|
+
* Hosts that DON'T call `useStitcherWorklet` from their worklet
|
|
253
|
+
* body replace first-party stitching for non-AR captures (a
|
|
254
|
+
* one-shot console.info documents this when the prop is first
|
|
255
|
+
* supplied). AR mode is unaffected either way — the AR-mode
|
|
256
|
+
* dispatch path (v0.8.0 Phase 4b.i / 4b.iii) natively fans out
|
|
257
|
+
* to both the lib's first-party stitching AND every registered
|
|
258
|
+
* host worklet on every frame, with per-worklet failure
|
|
259
|
+
* isolation.
|
|
244
260
|
*
|
|
245
261
|
* ## AR mode behaviour
|
|
246
262
|
*
|
package/dist/camera/Camera.js
CHANGED
|
@@ -435,36 +435,36 @@ function Camera(props) {
|
|
|
435
435
|
// Safety: stop the driver if the component unmounts mid-recording.
|
|
436
436
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
437
437
|
(0, react_1.useEffect)(() => () => { fpDriver.stop(); }, []);
|
|
438
|
-
// v0.8.0 Phase 5 — frameProcessor prop semantics:
|
|
438
|
+
// v0.8.0 Phase 5 / v0.11.0 — frameProcessor prop semantics:
|
|
439
439
|
//
|
|
440
|
-
// - Host supplied? → use host's processor
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
440
|
+
// - Host supplied? → use host's processor. The host's worklet
|
|
441
|
+
// body controls whether first-party stitching also fires:
|
|
442
|
+
// call `stitcher.call(frame)` (from `useStitcherWorklet`)
|
|
443
|
+
// inside the body to compose; omit to replace. One-shot
|
|
444
|
+
// console.info documents the choice so the host can spot a
|
|
445
|
+
// missing `useStitcherWorklet` call before they go hunting
|
|
446
|
+
// for "why is non-AR panorama capture not producing output".
|
|
447
|
+
// AR-mode capture is unaffected either way — the AR-session
|
|
448
|
+
// dispatch path fans out to BOTH first-party stitching AND
|
|
449
|
+
// every host worklet independently.
|
|
447
450
|
//
|
|
448
451
|
// - No host processor? → use `fpDriver.frameProcessor` which is
|
|
449
452
|
// the lib's internal worklet driving first-party stitching
|
|
450
453
|
// via `useFrameProcessorDriver`. Default behaviour for the
|
|
451
454
|
// common "I just want panorama capture" case.
|
|
452
|
-
//
|
|
453
|
-
// The pre-v0.8.0 behaviour (host's prop silently ignored with
|
|
454
|
-
// a warning) is gone — Phase 5 plumbs the prop through. The
|
|
455
|
-
// tradeoff is honestly documented in the CameraProps docstring.
|
|
456
455
|
const hostFrameProcessorAcceptedWarnedRef = (0, react_1.useRef)(false);
|
|
457
456
|
if (hostFrameProcessor != null
|
|
458
457
|
&& !hostFrameProcessorAcceptedWarnedRef.current) {
|
|
459
458
|
hostFrameProcessorAcceptedWarnedRef.current = true;
|
|
460
459
|
// eslint-disable-next-line no-console
|
|
461
460
|
console.info('[react-native-image-stitcher] Host frameProcessor supplied — '
|
|
462
|
-
+ 'non-AR mode will run YOUR worklet
|
|
463
|
-
+ 'first-party stitching
|
|
464
|
-
+ '
|
|
465
|
-
+ '
|
|
466
|
-
+ '
|
|
467
|
-
+ '
|
|
461
|
+
+ 'non-AR mode will run YOUR composed worklet. If you want '
|
|
462
|
+
+ 'first-party panorama stitching alongside your own logic, '
|
|
463
|
+
+ 'call `useStitcherWorklet()` and invoke `stitcher.call(frame)` '
|
|
464
|
+
+ 'from your worklet body (see `<Camera>` `frameProcessor` '
|
|
465
|
+
+ 'JSDoc for the composition pattern). AR-mode capture is '
|
|
466
|
+
+ 'unaffected (AR-session dispatch fans out to both '
|
|
467
|
+
+ 'first-party and host worklets independently).');
|
|
468
468
|
}
|
|
469
469
|
// The Frame Processor worklet bound to vision-camera's Camera.
|
|
470
470
|
// Host's wins if supplied; lib's internal driver otherwise.
|
|
@@ -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
|
package/dist/index.d.ts
CHANGED
|
@@ -73,5 +73,7 @@ export { useFrameStream } from './stitching/useFrameStream';
|
|
|
73
73
|
export type { FrameStreamOptions, SampledFrame } from './types';
|
|
74
74
|
export { useFrameProcessorDriver } from './stitching/useFrameProcessorDriver';
|
|
75
75
|
export type { UseFrameProcessorDriverOptions, FrameProcessorDriverHandle, } from './stitching/useFrameProcessorDriver';
|
|
76
|
+
export { useStitcherWorklet } from './stitching/useStitcherWorklet';
|
|
77
|
+
export type { UseStitcherWorkletOptions, StitcherWorkletHandle, StitcherWorkletInput, } from './stitching/useStitcherWorklet';
|
|
76
78
|
export { stitchVideo } from './stitching/stitchVideo';
|
|
77
79
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* adds RetaiLens-specific features on top.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.stitchVideo = exports.useFrameProcessorDriver = exports.useFrameStream = exports.useThrottledFrameProcessor = exports.useFrameProcessor = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
25
|
+
exports.stitchVideo = exports.useStitcherWorklet = exports.useFrameProcessorDriver = exports.useFrameStream = exports.useThrottledFrameProcessor = exports.useFrameProcessor = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
26
26
|
// ─────────────────────────────────────────────────────────────────────
|
|
27
27
|
// Layer 1 — the high-level <Camera> component
|
|
28
28
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -179,6 +179,14 @@ Object.defineProperty(exports, "useFrameStream", { enumerable: true, get: functi
|
|
|
179
179
|
// `useIncrementalJSDriver` was removed; was deprecated in v0.5).
|
|
180
180
|
var useFrameProcessorDriver_1 = require("./stitching/useFrameProcessorDriver");
|
|
181
181
|
Object.defineProperty(exports, "useFrameProcessorDriver", { enumerable: true, get: function () { return useFrameProcessorDriver_1.useFrameProcessorDriver; } });
|
|
182
|
+
// v0.11.0 — composable first-party stitching as a worklet function.
|
|
183
|
+
// Hosts that want to COMPOSE their own per-frame logic with the
|
|
184
|
+
// lib's stitching (instead of REPLACING it via the <Camera>
|
|
185
|
+
// `frameProcessor` prop) call this hook + invoke `stitcher.call`
|
|
186
|
+
// inside their own `useFrameProcessor` body. See
|
|
187
|
+
// `docs/host-app-integration.md` § Tier 3 for the full pattern.
|
|
188
|
+
var useStitcherWorklet_1 = require("./stitching/useStitcherWorklet");
|
|
189
|
+
Object.defineProperty(exports, "useStitcherWorklet", { enumerable: true, get: function () { return useStitcherWorklet_1.useStitcherWorklet; } });
|
|
182
190
|
// ── Batch stitching ───────────────────────────────────────────────────
|
|
183
191
|
// Feed a video file straight to OpenCV's cv::Stitcher, bypassing the
|
|
184
192
|
// incremental pipeline. Useful when you have content captured
|