react-native-image-stitcher 0.14.2 → 0.15.1
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 +164 -0
- package/README.md +35 -0
- package/RNImageStitcher.podspec +8 -7
- package/android/build.gradle +0 -16
- package/android/src/main/cpp/CMakeLists.txt +2 -63
- package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +129 -71
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +49 -0
- package/cpp/keyframe_gate.cpp +82 -23
- package/cpp/keyframe_gate.hpp +31 -2
- package/cpp/stitcher.cpp +208 -28
- package/cpp/tests/CMakeLists.txt +18 -12
- package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
- package/cpp/tests/warp_guard_test.cpp +48 -0
- package/cpp/warp_guard.hpp +41 -0
- package/dist/camera/Camera.d.ts +31 -16
- package/dist/camera/Camera.js +11 -3
- package/dist/camera/CameraView.js +93 -3
- package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
- package/dist/camera/CaptureStitchStatsToast.js +27 -7
- package/dist/camera/PanoramaSettings.d.ts +10 -223
- package/dist/camera/PanoramaSettings.js +6 -28
- package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
- package/dist/camera/PanoramaSettingsBridge.js +3 -102
- package/dist/camera/PanoramaSettingsModal.js +7 -1
- package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
- package/dist/camera/buildPanoramaInitialSettings.js +4 -0
- package/dist/camera/cameraErrorMessages.d.ts +32 -0
- package/dist/camera/cameraErrorMessages.js +53 -0
- package/dist/camera/selectCaptureDevice.d.ts +5 -1
- package/dist/camera/selectCaptureDevice.js +22 -2
- package/dist/camera/useCapture.js +38 -0
- package/dist/index.d.ts +5 -8
- package/dist/index.js +11 -34
- package/dist/stitching/incremental.d.ts +1 -117
- package/dist/stitching/stitchVideo.d.ts +0 -35
- package/dist/types.d.ts +0 -87
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +82 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
- package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
- package/package.json +3 -2
- package/src/camera/Camera.tsx +44 -23
- package/src/camera/CameraView.tsx +113 -4
- package/src/camera/CaptureStitchStatsToast.tsx +58 -14
- package/src/camera/PanoramaSettings.ts +16 -289
- package/src/camera/PanoramaSettingsBridge.ts +3 -114
- package/src/camera/PanoramaSettingsModal.tsx +14 -1
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
- package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
- package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
- package/src/camera/buildPanoramaInitialSettings.ts +17 -0
- package/src/camera/cameraErrorMessages.ts +84 -0
- package/src/camera/selectCaptureDevice.ts +28 -3
- package/src/camera/useCapture.ts +44 -1
- package/src/index.ts +11 -40
- package/src/stitching/incremental.ts +3 -140
- package/src/stitching/stitchVideo.ts +0 -26
- package/src/types.ts +0 -95
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
- package/cpp/stitcher_frame_jsi.cpp +0 -214
- package/cpp/stitcher_frame_jsi.hpp +0 -108
- package/cpp/stitcher_proxy_jsi.cpp +0 -109
- package/cpp/stitcher_proxy_jsi.hpp +0 -46
- package/cpp/stitcher_worklet_dispatch.cpp +0 -103
- package/cpp/stitcher_worklet_dispatch.hpp +0 -71
- package/cpp/stitcher_worklet_registry.cpp +0 -91
- package/cpp/stitcher_worklet_registry.hpp +0 -146
- package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
- package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
- package/dist/stitching/IncrementalStitcherView.js +0 -157
- package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
- package/dist/stitching/StitcherWorkletRegistry.js +0 -78
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
- package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
- package/dist/stitching/useFrameProcessor.d.ts +0 -119
- package/dist/stitching/useFrameProcessor.js +0 -196
- package/dist/stitching/useFrameStream.d.ts +0 -34
- package/dist/stitching/useFrameStream.js +0 -234
- package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
- package/dist/stitching/useThrottledFrameProcessor.js +0 -132
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
- package/src/stitching/IncrementalStitcherView.tsx +0 -198
- package/src/stitching/StitcherWorkletRegistry.ts +0 -156
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
- package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
- package/src/stitching/useFrameProcessor.ts +0 -226
- package/src/stitching/useFrameStream.ts +0 -271
- package/src/stitching/useThrottledFrameProcessor.ts +0 -145
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
|
|
3
|
-
import type { StitcherFrameProcessor } from './StitcherFrame';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* v0.8.0 Phase 4a — process-scope registry of host-supplied worklets
|
|
7
|
-
* that the v0.8.0 `useFrameProcessor` hook registers into.
|
|
8
|
-
*
|
|
9
|
-
* ## What this is (Phase 4a)
|
|
10
|
-
*
|
|
11
|
-
* A plain JS singleton holding an ordered list of registered
|
|
12
|
-
* worklets. Hosts mount the `useFrameProcessor` hook (in this
|
|
13
|
-
* directory); the hook registers its worklet into this singleton
|
|
14
|
-
* on mount and unregisters on unmount. Each entry carries:
|
|
15
|
-
*
|
|
16
|
-
* - `id`: stable identifier issued by `register`; passed to
|
|
17
|
-
* `unregister`.
|
|
18
|
-
* - `worklet`: the host's `StitcherFrameProcessor` function.
|
|
19
|
-
* MUST be `'worklet'`-prefixed at the call site (TS can't
|
|
20
|
-
* enforce that — convention).
|
|
21
|
-
* - `isFirstParty`: `false` for host-supplied worklets;
|
|
22
|
-
* reserved for the lib's own first-party stitching path which
|
|
23
|
-
* today is wired natively (not through this registry).
|
|
24
|
-
*
|
|
25
|
-
* Order is stable: first-party entries (none in Phase 4a) come
|
|
26
|
-
* first, then host entries by registration order. Re-registration
|
|
27
|
-
* of the same worklet by identity yields a new entry — hosts that
|
|
28
|
-
* re-render and call `register` again ARE responsible for calling
|
|
29
|
-
* `unregister` first. The `useFrameProcessor` hook handles this
|
|
30
|
-
* via its `deps` dependency array.
|
|
31
|
-
*
|
|
32
|
-
* ## What this is NOT (Phase 4b)
|
|
33
|
-
*
|
|
34
|
-
* **The native AR worklet runtime does NOT yet read this registry.**
|
|
35
|
-
* Worklets registered here for AR-mode captures will not fire
|
|
36
|
-
* until Phase 4b lands the cross-runtime handoff (a
|
|
37
|
-
* worklets-core `SharedValue` mirror that `RNSARWorkletRuntime`
|
|
38
|
-
* reads on each `dispatchFrame:pose:` call; the runtime then
|
|
39
|
-
* constructs a `StitcherFrameHostObject` + invokes each
|
|
40
|
-
* registered worklet via `RNWorklet::WorkletInvoker::call`).
|
|
41
|
-
*
|
|
42
|
-
* In non-AR mode the host-supplied worklet IS invoked, but via
|
|
43
|
-
* vision-camera's Frame Processor runtime directly (the
|
|
44
|
-
* `useFrameProcessor` hook returns vc's processor object which
|
|
45
|
-
* `<Camera>` passes to vision-camera). So Phase 4a's public API
|
|
46
|
-
* is fully functional for non-AR; AR is API-stable but
|
|
47
|
-
* runtime-deferred.
|
|
48
|
-
*
|
|
49
|
-
* ## Singleton lifetime
|
|
50
|
-
*
|
|
51
|
-
* The registry is a module-level instance. It lives for the
|
|
52
|
-
* lifetime of the JS runtime (= until app reload). Entries
|
|
53
|
-
* accumulate only via `register` and shed only via `unregister`
|
|
54
|
-
* — no GC / weak-ref logic. Hosts that mount `useFrameProcessor`
|
|
55
|
-
* inside React components MUST rely on the hook's effect cleanup
|
|
56
|
-
* to unregister on unmount, or they'll leak entries until
|
|
57
|
-
* reload. The hook handles this correctly today.
|
|
58
|
-
*
|
|
59
|
-
* ## Why a singleton (vs context provider)
|
|
60
|
-
*
|
|
61
|
-
* The native AR worklet runtime is itself a process-scope
|
|
62
|
-
* singleton (`RNSARWorkletRuntime`, `StitcherWorkletRuntime`).
|
|
63
|
-
* The Phase 4b handoff between TS and native is necessarily
|
|
64
|
-
* process-scope. Wrapping the registry in a React context
|
|
65
|
-
* would force every consumer to be in the same provider tree
|
|
66
|
-
* which is friction for layer-2 hosts that compose
|
|
67
|
-
* `<ARCameraView>` / `useIncrementalStitcher` themselves. The
|
|
68
|
-
* singleton is the right shape; the React-level ergonomics are
|
|
69
|
-
* provided by the `useFrameProcessor` hook.
|
|
70
|
-
*/
|
|
71
|
-
export interface StitcherWorkletEntry {
|
|
72
|
-
readonly id: string;
|
|
73
|
-
readonly worklet: StitcherFrameProcessor;
|
|
74
|
-
readonly isFirstParty: boolean;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
class Registry {
|
|
78
|
-
private entries: StitcherWorkletEntry[] = [];
|
|
79
|
-
private nextHostCounter = 0;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Register a worklet. Returns a stable ID for `unregister`.
|
|
83
|
-
*
|
|
84
|
-
* Entries are appended in registration order; first-party
|
|
85
|
-
* entries (if any are added in future) sort to the front.
|
|
86
|
-
*/
|
|
87
|
-
register(opts: {
|
|
88
|
-
worklet: StitcherFrameProcessor;
|
|
89
|
-
isFirstParty?: boolean;
|
|
90
|
-
}): string {
|
|
91
|
-
const isFirstParty = opts.isFirstParty ?? false;
|
|
92
|
-
const id = isFirstParty
|
|
93
|
-
? `fp-${this.nextHostCounter++}`
|
|
94
|
-
: `host-${this.nextHostCounter++}`;
|
|
95
|
-
const entry: StitcherWorkletEntry = {
|
|
96
|
-
id,
|
|
97
|
-
worklet: opts.worklet,
|
|
98
|
-
isFirstParty,
|
|
99
|
-
};
|
|
100
|
-
this.entries.push(entry);
|
|
101
|
-
// Re-sort so first-party always runs before host entries.
|
|
102
|
-
// Stable sort: registration order is preserved within each
|
|
103
|
-
// partition. Single-pass O(n log n) is fine — registration
|
|
104
|
-
// is rare (per-`<Camera>`-mount, not per-frame).
|
|
105
|
-
this.entries.sort((a, b) => {
|
|
106
|
-
if (a.isFirstParty !== b.isFirstParty) {
|
|
107
|
-
return a.isFirstParty ? -1 : 1;
|
|
108
|
-
}
|
|
109
|
-
return 0;
|
|
110
|
-
});
|
|
111
|
-
return id;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Remove a previously-registered worklet by ID. No-op if the ID
|
|
116
|
-
* isn't found. Hosts call this in their effect's cleanup.
|
|
117
|
-
*/
|
|
118
|
-
unregister(id: string): void {
|
|
119
|
-
this.entries = this.entries.filter((e) => e.id !== id);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Snapshot the current entries. Returned array is a copy —
|
|
124
|
-
* mutations don't affect the registry. Phase 4b's native
|
|
125
|
-
* handoff will read a `SharedValue` mirror of this list so the
|
|
126
|
-
* AR runtime doesn't need a JS-thread hop on the hot per-frame
|
|
127
|
-
* path; for Phase 4a this method is the JS-side accessor.
|
|
128
|
-
*/
|
|
129
|
-
getEntries(): readonly StitcherWorkletEntry[] {
|
|
130
|
-
return [...this.entries];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Total number of registered worklets (first-party + host).
|
|
135
|
-
* Useful for diagnostics + tests.
|
|
136
|
-
*/
|
|
137
|
-
get count(): number {
|
|
138
|
-
return this.entries.length;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Test-only — clear all entries. NOT exported from
|
|
143
|
-
* `src/index.ts`. Used in unit tests to reset state between
|
|
144
|
-
* cases.
|
|
145
|
-
*/
|
|
146
|
-
_resetForTests(): void {
|
|
147
|
-
this.entries = [];
|
|
148
|
-
this.nextHostCounter = 0;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Process-scope singleton. Imported by `useFrameProcessor` (in
|
|
154
|
-
* this directory) + by the Phase 4b native-handoff code (TBD).
|
|
155
|
-
*/
|
|
156
|
-
export const StitcherWorkletRegistry = new Registry();
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the v0.8.0 Phase 4a `StitcherWorkletRegistry` singleton.
|
|
4
|
-
*
|
|
5
|
-
* The registry is the JS-side staging area for host worklets — the
|
|
6
|
-
* Phase 4b native handoff will read from it to fan out invocations on
|
|
7
|
-
* the AR runtime. These tests pin the invariants the native handoff
|
|
8
|
-
* is going to rely on:
|
|
9
|
-
*
|
|
10
|
-
* - Stable, unique IDs out of `register`.
|
|
11
|
-
* - Registration order preserved within the host-entry partition.
|
|
12
|
-
* - First-party entries always sort to the front of `getEntries`.
|
|
13
|
-
* - `unregister` is a no-op for unknown IDs (no throw — the native
|
|
14
|
-
* handoff may race a JS-side unregister with a frame in flight).
|
|
15
|
-
* - `getEntries` returns a snapshot — mutating the returned array
|
|
16
|
-
* can't corrupt registry state.
|
|
17
|
-
* - `_resetForTests` returns the registry to a pristine state
|
|
18
|
-
* (used by these tests; documented as test-only in the source).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { StitcherWorkletRegistry } from '../StitcherWorkletRegistry';
|
|
22
|
-
import type { StitcherFrameProcessor } from '../StitcherFrame';
|
|
23
|
-
|
|
24
|
-
// Fresh no-op worklet stubs. These are NOT real worklets — they have
|
|
25
|
-
// no `'worklet'` directive — but the registry doesn't care about
|
|
26
|
-
// invocation, only about identity + ordering.
|
|
27
|
-
const makeWorklet = (label: string): StitcherFrameProcessor => {
|
|
28
|
-
const fn = (_frame: unknown) => {
|
|
29
|
-
void label;
|
|
30
|
-
};
|
|
31
|
-
return fn as unknown as StitcherFrameProcessor;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
describe('StitcherWorkletRegistry', () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
StitcherWorkletRegistry._resetForTests();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('register', () => {
|
|
40
|
-
it('returns a non-empty string ID', () => {
|
|
41
|
-
const id = StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
42
|
-
expect(typeof id).toBe('string');
|
|
43
|
-
expect(id.length).toBeGreaterThan(0);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('issues distinct IDs across calls', () => {
|
|
47
|
-
const id1 = StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
48
|
-
const id2 = StitcherWorkletRegistry.register({ worklet: makeWorklet('b') });
|
|
49
|
-
expect(id1).not.toBe(id2);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('issues distinct IDs even for the same worklet identity', () => {
|
|
53
|
-
// Hosts that re-render and re-register without unregistering get
|
|
54
|
-
// a fresh slot — the hook itself handles cleanup via deps, but
|
|
55
|
-
// the registry treats each `register` as independent.
|
|
56
|
-
const w = makeWorklet('shared');
|
|
57
|
-
const id1 = StitcherWorkletRegistry.register({ worklet: w });
|
|
58
|
-
const id2 = StitcherWorkletRegistry.register({ worklet: w });
|
|
59
|
-
expect(id1).not.toBe(id2);
|
|
60
|
-
expect(StitcherWorkletRegistry.count).toBe(2);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('host entries default to isFirstParty=false', () => {
|
|
64
|
-
StitcherWorkletRegistry.register({ worklet: makeWorklet('host') });
|
|
65
|
-
const [entry] = StitcherWorkletRegistry.getEntries();
|
|
66
|
-
expect(entry.isFirstParty).toBe(false);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('first-party flag passes through to the entry', () => {
|
|
70
|
-
StitcherWorkletRegistry.register({
|
|
71
|
-
worklet: makeWorklet('fp'),
|
|
72
|
-
isFirstParty: true,
|
|
73
|
-
});
|
|
74
|
-
const [entry] = StitcherWorkletRegistry.getEntries();
|
|
75
|
-
expect(entry.isFirstParty).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('getEntries ordering', () => {
|
|
80
|
-
it('preserves host registration order when no first-party entries', () => {
|
|
81
|
-
const wa = makeWorklet('a');
|
|
82
|
-
const wb = makeWorklet('b');
|
|
83
|
-
const wc = makeWorklet('c');
|
|
84
|
-
StitcherWorkletRegistry.register({ worklet: wa });
|
|
85
|
-
StitcherWorkletRegistry.register({ worklet: wb });
|
|
86
|
-
StitcherWorkletRegistry.register({ worklet: wc });
|
|
87
|
-
const entries = StitcherWorkletRegistry.getEntries();
|
|
88
|
-
expect(entries.map((e) => e.worklet)).toEqual([wa, wb, wc]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('sorts first-party entries before host entries regardless of registration order', () => {
|
|
92
|
-
const host1 = makeWorklet('host1');
|
|
93
|
-
const fp1 = makeWorklet('fp1');
|
|
94
|
-
const host2 = makeWorklet('host2');
|
|
95
|
-
const fp2 = makeWorklet('fp2');
|
|
96
|
-
// Interleave registrations.
|
|
97
|
-
StitcherWorkletRegistry.register({ worklet: host1 });
|
|
98
|
-
StitcherWorkletRegistry.register({ worklet: fp1, isFirstParty: true });
|
|
99
|
-
StitcherWorkletRegistry.register({ worklet: host2 });
|
|
100
|
-
StitcherWorkletRegistry.register({ worklet: fp2, isFirstParty: true });
|
|
101
|
-
const entries = StitcherWorkletRegistry.getEntries();
|
|
102
|
-
// First-party block first (in registration order),
|
|
103
|
-
// then host block (in registration order).
|
|
104
|
-
expect(entries.map((e) => e.worklet)).toEqual([fp1, fp2, host1, host2]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('returns a snapshot — mutating the returned array does not affect the registry', () => {
|
|
108
|
-
StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
109
|
-
const entries = StitcherWorkletRegistry.getEntries();
|
|
110
|
-
// Cast away readonly so we can attempt a mutation.
|
|
111
|
-
(entries as unknown as unknown[]).push({} as never);
|
|
112
|
-
// Registry's own count is unchanged.
|
|
113
|
-
expect(StitcherWorkletRegistry.count).toBe(1);
|
|
114
|
-
expect(StitcherWorkletRegistry.getEntries()).toHaveLength(1);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('unregister', () => {
|
|
119
|
-
it('removes a previously-registered entry by ID', () => {
|
|
120
|
-
const id = StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
121
|
-
expect(StitcherWorkletRegistry.count).toBe(1);
|
|
122
|
-
StitcherWorkletRegistry.unregister(id);
|
|
123
|
-
expect(StitcherWorkletRegistry.count).toBe(0);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('is a no-op for an unknown ID (no throw)', () => {
|
|
127
|
-
StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
128
|
-
expect(() => StitcherWorkletRegistry.unregister('host-9999')).not.toThrow();
|
|
129
|
-
expect(StitcherWorkletRegistry.count).toBe(1);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('removes the right entry when multiple are registered', () => {
|
|
133
|
-
const id1 = StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
134
|
-
const id2 = StitcherWorkletRegistry.register({ worklet: makeWorklet('b') });
|
|
135
|
-
const id3 = StitcherWorkletRegistry.register({ worklet: makeWorklet('c') });
|
|
136
|
-
StitcherWorkletRegistry.unregister(id2);
|
|
137
|
-
const remainingIds = StitcherWorkletRegistry.getEntries().map((e) => e.id);
|
|
138
|
-
expect(remainingIds).toEqual([id1, id3]);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('survives double-unregister of the same ID', () => {
|
|
142
|
-
const id = StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
143
|
-
StitcherWorkletRegistry.unregister(id);
|
|
144
|
-
expect(() => StitcherWorkletRegistry.unregister(id)).not.toThrow();
|
|
145
|
-
expect(StitcherWorkletRegistry.count).toBe(0);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe('count', () => {
|
|
150
|
-
it('starts at 0 after reset', () => {
|
|
151
|
-
expect(StitcherWorkletRegistry.count).toBe(0);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('reflects register/unregister deltas', () => {
|
|
155
|
-
expect(StitcherWorkletRegistry.count).toBe(0);
|
|
156
|
-
const id1 = StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
157
|
-
expect(StitcherWorkletRegistry.count).toBe(1);
|
|
158
|
-
StitcherWorkletRegistry.register({ worklet: makeWorklet('b') });
|
|
159
|
-
expect(StitcherWorkletRegistry.count).toBe(2);
|
|
160
|
-
StitcherWorkletRegistry.unregister(id1);
|
|
161
|
-
expect(StitcherWorkletRegistry.count).toBe(1);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('_resetForTests', () => {
|
|
166
|
-
it('clears all entries', () => {
|
|
167
|
-
StitcherWorkletRegistry.register({ worklet: makeWorklet('a') });
|
|
168
|
-
StitcherWorkletRegistry.register({ worklet: makeWorklet('b'), isFirstParty: true });
|
|
169
|
-
StitcherWorkletRegistry.register({ worklet: makeWorklet('c') });
|
|
170
|
-
expect(StitcherWorkletRegistry.count).toBe(3);
|
|
171
|
-
StitcherWorkletRegistry._resetForTests();
|
|
172
|
-
expect(StitcherWorkletRegistry.count).toBe(0);
|
|
173
|
-
expect(StitcherWorkletRegistry.getEntries()).toEqual([]);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the v0.8.0 Phase 4b `ensureStitcherProxyInstalled`
|
|
4
|
-
* helper. The native install path can't be exercised in jest (no
|
|
5
|
-
* JSI runtime), so these tests cover only the JS-side branches:
|
|
6
|
-
*
|
|
7
|
-
* - Idempotency (second call short-circuits).
|
|
8
|
-
* - "module missing" path returns false + warns once.
|
|
9
|
-
* - "install returns false" path returns false + warns once.
|
|
10
|
-
* - "install throws" path returns false + warns once.
|
|
11
|
-
* - "install succeeds" path returns true + caches.
|
|
12
|
-
*
|
|
13
|
-
* NativeModules is stubbed via the jest mock at
|
|
14
|
-
* `jest.mocks/react-native.js`; per-test customization swaps in a
|
|
15
|
-
* scenario-specific module.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { NativeModules } from 'react-native';
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
_resetStitcherProxyInstallStateForTests,
|
|
22
|
-
ensureStitcherProxyInstalled,
|
|
23
|
-
} from '../ensureStitcherProxyInstalled';
|
|
24
|
-
|
|
25
|
-
type MutableGlobal = Record<string, unknown>;
|
|
26
|
-
|
|
27
|
-
describe('ensureStitcherProxyInstalled', () => {
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
_resetStitcherProxyInstallStateForTests();
|
|
30
|
-
// Clear any previous __stitcherProxy from prior test cases so the
|
|
31
|
-
// module's "already installed" fast-path doesn't bypass our
|
|
32
|
-
// scenario-specific NativeModules stub.
|
|
33
|
-
delete (globalThis as MutableGlobal).__stitcherProxy;
|
|
34
|
-
// Wipe the NativeModules.StitcherJsiInstaller entry so each test
|
|
35
|
-
// starts with a clean slate (the mock module is a shared object
|
|
36
|
-
// across the whole test file).
|
|
37
|
-
(NativeModules as MutableGlobal).StitcherJsiInstaller = undefined;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('returns false when the native module is missing', () => {
|
|
41
|
-
expect(ensureStitcherProxyInstalled()).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('returns true when install() returns true', () => {
|
|
45
|
-
const install = jest.fn(() => true);
|
|
46
|
-
(NativeModules as MutableGlobal).StitcherJsiInstaller = { install };
|
|
47
|
-
expect(ensureStitcherProxyInstalled()).toBe(true);
|
|
48
|
-
expect(install).toHaveBeenCalledTimes(1);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns false when install() returns false', () => {
|
|
52
|
-
const install = jest.fn(() => false);
|
|
53
|
-
(NativeModules as MutableGlobal).StitcherJsiInstaller = { install };
|
|
54
|
-
expect(ensureStitcherProxyInstalled()).toBe(false);
|
|
55
|
-
expect(install).toHaveBeenCalledTimes(1);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('returns false when install() throws', () => {
|
|
59
|
-
const install = jest.fn(() => {
|
|
60
|
-
throw new Error('boom');
|
|
61
|
-
});
|
|
62
|
-
(NativeModules as MutableGlobal).StitcherJsiInstaller = { install };
|
|
63
|
-
expect(ensureStitcherProxyInstalled()).toBe(false);
|
|
64
|
-
expect(install).toHaveBeenCalledTimes(1);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('is idempotent — second call short-circuits without re-invoking install()', () => {
|
|
68
|
-
const install = jest.fn(() => true);
|
|
69
|
-
(NativeModules as MutableGlobal).StitcherJsiInstaller = { install };
|
|
70
|
-
expect(ensureStitcherProxyInstalled()).toBe(true);
|
|
71
|
-
expect(ensureStitcherProxyInstalled()).toBe(true);
|
|
72
|
-
expect(install).toHaveBeenCalledTimes(1);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('short-circuits if __stitcherProxy is already on globalThis (e.g., other consumer installed it first)', () => {
|
|
76
|
-
// Simulate a different SDK instance having installed the proxy.
|
|
77
|
-
(globalThis as MutableGlobal).__stitcherProxy = {
|
|
78
|
-
install: jest.fn(),
|
|
79
|
-
uninstall: jest.fn(),
|
|
80
|
-
count: jest.fn(() => 0),
|
|
81
|
-
};
|
|
82
|
-
const install = jest.fn();
|
|
83
|
-
(NativeModules as MutableGlobal).StitcherJsiInstaller = { install };
|
|
84
|
-
expect(ensureStitcherProxyInstalled()).toBe(true);
|
|
85
|
-
// We did NOT call our own native install — we accepted the
|
|
86
|
-
// already-installed proxy.
|
|
87
|
-
expect(install).not.toHaveBeenCalled();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('treats `install: undefined` as a missing module (not a crash)', () => {
|
|
91
|
-
(NativeModules as MutableGlobal).StitcherJsiInstaller = {};
|
|
92
|
-
expect(ensureStitcherProxyInstalled()).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the v0.9.0 Layer 2 `useThrottledFrameProcessor` hook.
|
|
4
|
-
*
|
|
5
|
-
* The worklet runtime can't run in jest (no JSI, no worklets-core).
|
|
6
|
-
* What we CAN test:
|
|
7
|
-
*
|
|
8
|
-
* - The `sampleHz` clamping (`[0.5, 30]`)
|
|
9
|
-
* - `minIntervalMs` math (1000 / sampleHz)
|
|
10
|
-
* - The deps propagation (host's deps → useFrameProcessor's deps)
|
|
11
|
-
* - The throttle gate logic (extracted as a pure function for
|
|
12
|
-
* isolated verification — see `_throttleGateForTests`).
|
|
13
|
-
*
|
|
14
|
-
* The hook itself is tested via a thin React-renderer-free harness:
|
|
15
|
-
* we mock `useFrameProcessor` + `useSharedValue` so we can verify
|
|
16
|
-
* the call shape without booting the worklet runtime.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { useThrottledFrameProcessor } from '../useThrottledFrameProcessor';
|
|
20
|
-
|
|
21
|
-
// ─── Mock vision-camera + worklets-core ─────────────────────────────
|
|
22
|
-
// These are minimal-shim mocks — enough surface for the hook to call
|
|
23
|
-
// `useFrameProcessor(workletBody, deps)` and `useSharedValue(0)`.
|
|
24
|
-
|
|
25
|
-
const useFrameProcessorMock = jest.fn();
|
|
26
|
-
const useSharedValueMock = jest.fn();
|
|
27
|
-
|
|
28
|
-
jest.mock('../useFrameProcessor', () => ({
|
|
29
|
-
useFrameProcessor: (...args: unknown[]) => useFrameProcessorMock(...args),
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
jest.mock('react-native-worklets-core', () => ({
|
|
33
|
-
useSharedValue: (initial: number) => useSharedValueMock(initial),
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
describe('useThrottledFrameProcessor', () => {
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
useFrameProcessorMock.mockReset();
|
|
39
|
-
useSharedValueMock.mockReset();
|
|
40
|
-
// Default behaviour for useSharedValue: return an object with a
|
|
41
|
-
// mutable `.value` field (mirrors worklets-core's API).
|
|
42
|
-
useSharedValueMock.mockImplementation((initial: number) => ({
|
|
43
|
-
value: initial,
|
|
44
|
-
}));
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('sampleHz clamping', () => {
|
|
48
|
-
it('clamps below 0.5 to 0.5', () => {
|
|
49
|
-
const noop = (() => {}) as unknown as Parameters<typeof useThrottledFrameProcessor>[0];
|
|
50
|
-
useThrottledFrameProcessor(noop, { sampleHz: 0.1 }, []);
|
|
51
|
-
// useFrameProcessor receives the wrapped worklet; the deps
|
|
52
|
-
// array's first entry is `minIntervalMs`. For sampleHz=0.5,
|
|
53
|
-
// minIntervalMs = 2000.
|
|
54
|
-
const [, deps] = useFrameProcessorMock.mock.calls[0]!;
|
|
55
|
-
expect(deps[0]).toBeCloseTo(2000);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('clamps above 30 to 30', () => {
|
|
59
|
-
const noop = (() => {}) as unknown as Parameters<typeof useThrottledFrameProcessor>[0];
|
|
60
|
-
useThrottledFrameProcessor(noop, { sampleHz: 999 }, []);
|
|
61
|
-
const [, deps] = useFrameProcessorMock.mock.calls[0]!;
|
|
62
|
-
// sampleHz=30 → minIntervalMs = 33.333...
|
|
63
|
-
expect(deps[0]).toBeCloseTo(1000 / 30);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('passes through in-range sampleHz unchanged', () => {
|
|
67
|
-
const noop = (() => {}) as unknown as Parameters<typeof useThrottledFrameProcessor>[0];
|
|
68
|
-
useThrottledFrameProcessor(noop, { sampleHz: 2 }, []);
|
|
69
|
-
const [, deps] = useFrameProcessorMock.mock.calls[0]!;
|
|
70
|
-
expect(deps[0]).toBeCloseTo(500);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('accepts boundary values exactly', () => {
|
|
74
|
-
const noop = (() => {}) as unknown as Parameters<typeof useThrottledFrameProcessor>[0];
|
|
75
|
-
useThrottledFrameProcessor(noop, { sampleHz: 0.5 }, []);
|
|
76
|
-
let deps = useFrameProcessorMock.mock.calls[0]![1];
|
|
77
|
-
expect(deps[0]).toBeCloseTo(2000);
|
|
78
|
-
|
|
79
|
-
useFrameProcessorMock.mockClear();
|
|
80
|
-
useThrottledFrameProcessor(noop, { sampleHz: 30 }, []);
|
|
81
|
-
deps = useFrameProcessorMock.mock.calls[0]![1];
|
|
82
|
-
expect(deps[0]).toBeCloseTo(1000 / 30);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('deps propagation', () => {
|
|
87
|
-
it('appends host deps after the internal interval + worklet deps', () => {
|
|
88
|
-
const noop = (() => {}) as unknown as Parameters<typeof useThrottledFrameProcessor>[0];
|
|
89
|
-
const hostDep1 = { id: 'a' };
|
|
90
|
-
const hostDep2 = 42;
|
|
91
|
-
useThrottledFrameProcessor(noop, { sampleHz: 2 }, [hostDep1, hostDep2]);
|
|
92
|
-
const [, deps] = useFrameProcessorMock.mock.calls[0]!;
|
|
93
|
-
// Expected shape: [minIntervalMs, worklet, ...hostDeps]
|
|
94
|
-
expect(deps).toHaveLength(4);
|
|
95
|
-
expect(deps[0]).toBeCloseTo(500);
|
|
96
|
-
expect(deps[1]).toBe(noop);
|
|
97
|
-
expect(deps[2]).toBe(hostDep1);
|
|
98
|
-
expect(deps[3]).toBe(hostDep2);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('with empty host deps: deps = [minIntervalMs, worklet]', () => {
|
|
102
|
-
const noop = (() => {}) as unknown as Parameters<typeof useThrottledFrameProcessor>[0];
|
|
103
|
-
useThrottledFrameProcessor(noop, { sampleHz: 2 }, []);
|
|
104
|
-
const [, deps] = useFrameProcessorMock.mock.calls[0]!;
|
|
105
|
-
expect(deps).toHaveLength(2);
|
|
106
|
-
expect(deps[0]).toBeCloseTo(500);
|
|
107
|
-
expect(deps[1]).toBe(noop);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('throttle gate', () => {
|
|
112
|
-
// The throttle logic lives INSIDE the wrapped worklet body, which
|
|
113
|
-
// jest can't execute directly (it's a `'worklet'`-prefixed
|
|
114
|
-
// function). But the wrapped function IS just a plain JS
|
|
115
|
-
// function until the worklets-core babel plugin transforms it,
|
|
116
|
-
// so we can call it manually with mock frames + a mock
|
|
117
|
-
// shared-value gate.
|
|
118
|
-
//
|
|
119
|
-
// The body's logic:
|
|
120
|
-
// if (frame.timestamp - lastSampleMs.value < minIntervalMs) return;
|
|
121
|
-
// lastSampleMs.value = frame.timestamp;
|
|
122
|
-
// worklet(frame);
|
|
123
|
-
|
|
124
|
-
it('fires the worklet on the first frame regardless of timestamp', () => {
|
|
125
|
-
const hostWorklet = jest.fn();
|
|
126
|
-
useThrottledFrameProcessor(hostWorklet, { sampleHz: 2 }, []);
|
|
127
|
-
const [wrappedBody] = useFrameProcessorMock.mock.calls[0]!;
|
|
128
|
-
|
|
129
|
-
const frame = { timestamp: 12345 } as Parameters<typeof hostWorklet>[0];
|
|
130
|
-
wrappedBody(frame);
|
|
131
|
-
|
|
132
|
-
expect(hostWorklet).toHaveBeenCalledTimes(1);
|
|
133
|
-
expect(hostWorklet).toHaveBeenCalledWith(frame);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('skips a frame too close to the previous sample', () => {
|
|
137
|
-
const hostWorklet = jest.fn();
|
|
138
|
-
useThrottledFrameProcessor(hostWorklet, { sampleHz: 2 }, []); // 500ms interval
|
|
139
|
-
const [wrappedBody] = useFrameProcessorMock.mock.calls[0]!;
|
|
140
|
-
|
|
141
|
-
wrappedBody({ timestamp: 1000 } as never);
|
|
142
|
-
wrappedBody({ timestamp: 1100 } as never); // 100ms after — too soon
|
|
143
|
-
wrappedBody({ timestamp: 1200 } as never); // 200ms after — too soon
|
|
144
|
-
|
|
145
|
-
expect(hostWorklet).toHaveBeenCalledTimes(1);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('fires again exactly at the interval boundary', () => {
|
|
149
|
-
const hostWorklet = jest.fn();
|
|
150
|
-
useThrottledFrameProcessor(hostWorklet, { sampleHz: 2 }, []); // 500ms
|
|
151
|
-
const [wrappedBody] = useFrameProcessorMock.mock.calls[0]!;
|
|
152
|
-
|
|
153
|
-
wrappedBody({ timestamp: 1000 } as never);
|
|
154
|
-
wrappedBody({ timestamp: 1500 } as never); // exactly at boundary
|
|
155
|
-
|
|
156
|
-
expect(hostWorklet).toHaveBeenCalledTimes(2);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('fires again past the interval boundary', () => {
|
|
160
|
-
const hostWorklet = jest.fn();
|
|
161
|
-
useThrottledFrameProcessor(hostWorklet, { sampleHz: 2 }, []); // 500ms
|
|
162
|
-
const [wrappedBody] = useFrameProcessorMock.mock.calls[0]!;
|
|
163
|
-
|
|
164
|
-
wrappedBody({ timestamp: 1000 } as never);
|
|
165
|
-
wrappedBody({ timestamp: 1600 } as never); // 600ms after
|
|
166
|
-
|
|
167
|
-
expect(hostWorklet).toHaveBeenCalledTimes(2);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe('shared value lifecycle', () => {
|
|
172
|
-
it('initializes lastSampleMs to 0', () => {
|
|
173
|
-
const noop = (() => {}) as unknown as Parameters<typeof useThrottledFrameProcessor>[0];
|
|
174
|
-
useThrottledFrameProcessor(noop, { sampleHz: 2 }, []);
|
|
175
|
-
expect(useSharedValueMock).toHaveBeenCalledWith(0);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
});
|