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
|
@@ -58,8 +58,7 @@
|
|
|
58
58
|
// passed to `<Camera frameProcessor={...}>`. The hook returns
|
|
59
59
|
// the processor object so hosts can wire it up either way.
|
|
60
60
|
|
|
61
|
-
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
62
|
-
import { Platform } from 'react-native';
|
|
61
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
63
62
|
import {
|
|
64
63
|
VisionCameraProxy,
|
|
65
64
|
type Frame,
|
|
@@ -73,6 +72,7 @@ import type {
|
|
|
73
72
|
FrameStreamOptions,
|
|
74
73
|
SampledFrame,
|
|
75
74
|
} from '../types';
|
|
75
|
+
import { getDefaultCaptureDir } from '../utils/files';
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* `useFrameStream` — Layer 3. See module docstring for the full
|
|
@@ -111,37 +111,39 @@ export function useFrameStream(
|
|
|
111
111
|
const sampleHz = Math.max(0.5, Math.min(10, options.sampleHz));
|
|
112
112
|
const quality = options.quality ?? 75;
|
|
113
113
|
|
|
114
|
-
// Default output dir:
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
114
|
+
// Default output dir: the lib's canonical capture dir resolved
|
|
115
|
+
// via `FileBridge.defaultCaptureDir()`. Same dir the lib uses
|
|
116
|
+
// for panorama JPEGs / keyframe JPEGs — guaranteed writable on
|
|
117
|
+
// both platforms (iOS NSCachesDirectory + Android Context.cacheDir),
|
|
118
|
+
// created if missing. Resolved async on first mount; until
|
|
119
|
+
// resolution completes the worklet's `outputDir` is empty and
|
|
120
|
+
// the plugin call no-ops silently (a few frames missed at most;
|
|
121
|
+
// typical resolution time is <50ms).
|
|
122
|
+
//
|
|
123
|
+
// Hosts that want a specific path supply `options.outputDir`
|
|
124
|
+
// and skip the resolution entirely.
|
|
125
|
+
const [resolvedDefaultDir, setResolvedDefaultDir] = useState<string>('');
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (options.outputDir != null) return;
|
|
128
|
+
let cancelled = false;
|
|
129
|
+
getDefaultCaptureDir()
|
|
130
|
+
.then((dir) => {
|
|
131
|
+
if (!cancelled) setResolvedDefaultDir(dir);
|
|
132
|
+
})
|
|
133
|
+
.catch((err) => {
|
|
134
|
+
// eslint-disable-next-line no-console
|
|
135
|
+
console.warn(
|
|
136
|
+
'[useFrameStream] FileBridge.defaultCaptureDir() failed; ' +
|
|
137
|
+
'samples will not fire until `options.outputDir` is supplied. ' +
|
|
138
|
+
String(err),
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
return () => {
|
|
142
|
+
cancelled = true;
|
|
143
|
+
};
|
|
133
144
|
}, [options.outputDir]);
|
|
134
145
|
|
|
135
|
-
|
|
136
|
-
// react-native-fs but to keep the dep surface minimal, we just
|
|
137
|
-
// attempt to create via a tiny native call — or, simpler, accept
|
|
138
|
-
// that the plugin's write call will fail if the dir doesn't
|
|
139
|
-
// exist + log a clear error. For v0.9.0 baseline we defer
|
|
140
|
-
// mkdir to the host (document it in the option's JSDoc) OR fall
|
|
141
|
-
// back to the platform's tmpdir which already exists.
|
|
142
|
-
//
|
|
143
|
-
// The tmpdir defaults above always exist on iOS + Android, so
|
|
144
|
-
// the common case "host doesn't supply outputDir" Just Works.
|
|
146
|
+
const outputDir = options.outputDir ?? resolvedDefaultDir;
|
|
145
147
|
|
|
146
148
|
// Stable JS-side handler reference for `runOnJS`. The hook re-
|
|
147
149
|
// captures `handler` on every render but the ref keeps the
|
|
@@ -174,20 +176,37 @@ export function useFrameStream(
|
|
|
174
176
|
// registry hasn't initialised yet (rare race on app start). We
|
|
175
177
|
// retry every 16ms (one display frame) until success — matches
|
|
176
178
|
// the pattern in `useFrameProcessorDriver`.
|
|
177
|
-
|
|
179
|
+
//
|
|
180
|
+
// Use `useState` (not `useRef`) so the eventual non-null value
|
|
181
|
+
// triggers a re-render — the worklet closure below captures
|
|
182
|
+
// `plugin` by value at render time, so without state we'd
|
|
183
|
+
// capture `null` forever.
|
|
184
|
+
const [plugin, setPlugin] = useState<FrameProcessorPlugin | null>(null);
|
|
178
185
|
useEffect(() => {
|
|
179
186
|
let cancelled = false;
|
|
180
187
|
let timerId: ReturnType<typeof setTimeout> | null = null;
|
|
188
|
+
let attempts = 0;
|
|
181
189
|
const tryAcquire = () => {
|
|
182
190
|
if (cancelled) return;
|
|
191
|
+
attempts += 1;
|
|
183
192
|
const p = VisionCameraProxy.initFrameProcessorPlugin(
|
|
184
193
|
'save_frame_as_jpeg',
|
|
185
194
|
{},
|
|
186
195
|
);
|
|
187
196
|
if (p != null) {
|
|
188
|
-
|
|
197
|
+
setPlugin(p);
|
|
189
198
|
return;
|
|
190
199
|
}
|
|
200
|
+
// After ~1s of failed retries, warn once — the plugin should
|
|
201
|
+
// be registered by then; persistent failure means the host's
|
|
202
|
+
// native bundle doesn't include `save_frame_as_jpeg`.
|
|
203
|
+
if (attempts === 60) {
|
|
204
|
+
// eslint-disable-next-line no-console
|
|
205
|
+
console.warn(
|
|
206
|
+
'[useFrameStream] save_frame_as_jpeg plugin not found after 1s of retries. ' +
|
|
207
|
+
'Verify react-native-image-stitcher native module is installed in your host app.',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
191
210
|
timerId = setTimeout(tryAcquire, 16);
|
|
192
211
|
};
|
|
193
212
|
tryAcquire();
|
|
@@ -197,16 +216,13 @@ export function useFrameStream(
|
|
|
197
216
|
};
|
|
198
217
|
}, []);
|
|
199
218
|
|
|
200
|
-
// The worklet body — fires at sampleHz, calls the JPEG plugin,
|
|
201
|
-
// bridges the result to JS. Note we read `pluginRef.current`
|
|
202
|
-
// inside the worklet via the captured `plugin` value below;
|
|
203
|
-
// worklets-core handles the JS↔worklet reference.
|
|
204
|
-
const plugin = pluginRef.current;
|
|
205
|
-
|
|
206
219
|
return useThrottledFrameProcessor(
|
|
207
220
|
(frame: StitcherFrame) => {
|
|
208
221
|
'worklet';
|
|
209
222
|
if (plugin == null) return;
|
|
223
|
+
// Async outputDir resolution may not have completed yet on
|
|
224
|
+
// the first few frames after mount — bail until it does.
|
|
225
|
+
if (outputDir === '') return;
|
|
210
226
|
|
|
211
227
|
// Slot rotation: compute slot from frame timestamp. At
|
|
212
228
|
// sampleHz=2 (500ms interval), the slot index changes every
|