react-native-image-stitcher 0.4.1 → 0.5.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 +83 -0
- package/README.md +1 -0
- package/android/build.gradle +33 -0
- package/android/src/main/java/io/imagestitcher/rn/CvFlowGateFrameProcessor.kt +163 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +214 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +65 -7
- package/dist/camera/Camera.d.ts +50 -1
- package/dist/camera/Camera.js +100 -15
- package/dist/camera/CameraView.d.ts +17 -5
- package/dist/camera/CameraView.js +28 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -1
- package/dist/stitching/incremental.d.ts +13 -4
- package/dist/stitching/useFrameProcessorDriver.d.ts +148 -0
- package/dist/stitching/useFrameProcessorDriver.js +321 -0
- package/dist/stitching/useIncrementalJSDriver.js +21 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +128 -8
- package/ios/Sources/RNImageStitcher/KeyframeGateFrameProcessor.mm +196 -0
- package/package.json +3 -1
- package/src/camera/Camera.tsx +164 -14
- package/src/camera/CameraView.tsx +50 -0
- package/src/index.ts +12 -0
- package/src/stitching/incremental.ts +12 -3
- package/src/stitching/useFrameProcessorDriver.ts +407 -0
- package/src/stitching/useIncrementalJSDriver.ts +24 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* useFrameProcessorDriver — vision-camera Frame Processor + gyro
|
|
5
|
+
* driver for the incremental panorama engine. Replaces
|
|
6
|
+
* `useIncrementalJSDriver` in non-AR captures.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists (vs the JS-driver predecessor)
|
|
9
|
+
*
|
|
10
|
+
* The JS driver takes a JPEG snapshot every ~250 ms and feeds the
|
|
11
|
+
* path to `IncrementalStitcher.processFrameAtPath`. That path
|
|
12
|
+
* has three costs:
|
|
13
|
+
*
|
|
14
|
+
* 1. JPEG encode (`takeSnapshot` ≈ 30–80 ms on iPhone 16 Pro)
|
|
15
|
+
* 2. Disk write of the JPEG
|
|
16
|
+
* 3. JPEG decode + cv::Mat alloc inside the engine
|
|
17
|
+
*
|
|
18
|
+
* Per-frame round-trip ~80 ms means ~4 Hz max throughput, and
|
|
19
|
+
* ~80 ms latency between "this is the moment to accept" and "this
|
|
20
|
+
* frame is in the engine". Both numbers caused operator-felt lag
|
|
21
|
+
* on long shelf pans.
|
|
22
|
+
*
|
|
23
|
+
* This hook uses vision-camera's Frame Processor instead. The
|
|
24
|
+
* worklet runs on the camera producer thread at the native frame
|
|
25
|
+
* rate (30 fps on iOS). Each frame goes through a JSI plugin
|
|
26
|
+
* (`cv_flow_gate_process_frame`) directly into
|
|
27
|
+
* `IncrementalStitcher.consumeFrame` — the SAME entry point AR
|
|
28
|
+
* mode uses, with the engine's existing KeyframeGate making the
|
|
29
|
+
* accept/reject decision. Rejected frames cost ~3–8 ms; accepted
|
|
30
|
+
* frames take the same deep-copy + workQueue path AR mode takes.
|
|
31
|
+
*
|
|
32
|
+
* Net wins: no JPEG round-trip on rejected frames, no disk thrash
|
|
33
|
+
* during recording, lower latency to accept, full 30 fps gate
|
|
34
|
+
* evaluation budget.
|
|
35
|
+
*
|
|
36
|
+
* Pose synthesis
|
|
37
|
+
*
|
|
38
|
+
* Non-AR mode has no ARKit pose. We integrate the gyroscope on
|
|
39
|
+
* the JS thread (`react-native-sensors`), accumulate yaw + pitch,
|
|
40
|
+
* and publish them via Reanimated `useSharedValue` so the worklet
|
|
41
|
+
* can read them WITHOUT a thread hop. Translation is reported as
|
|
42
|
+
* zero (no IMU translation; this is a known limitation we share
|
|
43
|
+
* with the legacy driver — drift ~1–2°/min over a 30 s capture is
|
|
44
|
+
* below the gate's overlap threshold and rarely matters).
|
|
45
|
+
*
|
|
46
|
+
* Quaternion synthesis (q = q_yaw * q_pitch * q_roll, Tait-Bryan
|
|
47
|
+
* YPR order to match the legacy driver's body-frame intent):
|
|
48
|
+
* q_yaw = (0, sin(yaw/2), 0, cos(yaw/2))
|
|
49
|
+
* q_pitch = (sin(pitch/2), 0, 0, cos(pitch/2))
|
|
50
|
+
* q_roll = (0, 0, sin(roll/2), cos(roll/2))
|
|
51
|
+
*
|
|
52
|
+
* Expanded (cy, sy = cos/sin(yaw/2); analogous for cp/sp, cr/sr):
|
|
53
|
+
* qx = cy*sp*cr + sy*cp*sr
|
|
54
|
+
* qy = sy*cp*cr - cy*sp*sr
|
|
55
|
+
* qz = cy*cp*sr - sy*sp*cr
|
|
56
|
+
* qw = cy*cp*cr + sy*sp*sr
|
|
57
|
+
*
|
|
58
|
+
* When roll=0 this collapses to the 2-axis form
|
|
59
|
+
* `(cy*sp, sy*cp, -sy*sp, cy*cp)` the legacy driver used, so
|
|
60
|
+
* captures held perfectly level produce identical poses to the
|
|
61
|
+
* pre-roll behaviour.
|
|
62
|
+
*
|
|
63
|
+
* Intrinsics are synthesised from the actual frame dimensions
|
|
64
|
+
* (`frame.width`, `frame.height`) plus the host-provided
|
|
65
|
+
* horizontal/vertical FoV defaults. The stitcher derives its FoV-
|
|
66
|
+
* overlap window from these, so the assumed FoV matters for the
|
|
67
|
+
* gate's overlap math but not for the panorama itself (the
|
|
68
|
+
* stitcher feature-matches + RANSACs the final alignment).
|
|
69
|
+
*
|
|
70
|
+
* Throttling
|
|
71
|
+
*
|
|
72
|
+
* `evalEveryNFrames` controls how often the worklet calls the
|
|
73
|
+
* plugin. Default 1 (every frame). Set higher to amortise the
|
|
74
|
+
* plugin call + consumeFrame's gate evaluation across multiple
|
|
75
|
+
* producer-thread frames on lower-end devices. Independent of —
|
|
76
|
+
* and stacks on top of — the stitcher's own internal
|
|
77
|
+
* `flowEvalEveryNFrames` (see `KeyframeGate.swift`); both
|
|
78
|
+
* throttles can be active simultaneously and the effective cadence
|
|
79
|
+
* is `evalEveryNFrames * flowEvalEveryNFrames`.
|
|
80
|
+
*
|
|
81
|
+
* Lifecycle
|
|
82
|
+
*
|
|
83
|
+
* `start()` subscribes to the gyro and resets pose accumulators.
|
|
84
|
+
* `stop()` unsubscribes and resets. The returned `frameProcessor`
|
|
85
|
+
* is meant to be passed to `<Camera frameProcessor={...} />` —
|
|
86
|
+
* it's stable as long as the plugin reference and the FoV props
|
|
87
|
+
* haven't changed. Returns `null` when the plugin isn't loaded
|
|
88
|
+
* yet; pass `null`-or-fallback to the Camera in that case.
|
|
89
|
+
*
|
|
90
|
+
* Pairing with `IncrementalStitcher.start({frameSourceMode})`
|
|
91
|
+
*
|
|
92
|
+
* The plugin's per-frame call into `consumeFrameFromPlugin` is
|
|
93
|
+
* gated by `IncrementalStitcher.frameProcessorIngestEnabled`,
|
|
94
|
+
* which is TRUE only when the stitcher was started with
|
|
95
|
+
* `frameSourceMode === 'frameProcessor'`. Hosts MUST call
|
|
96
|
+
* `incrementalStitcher.start({ frameSourceMode: 'frameProcessor',
|
|
97
|
+
* ... })` to actually get frames into the engine — otherwise the
|
|
98
|
+
* worklet runs to completion but the wrapper drops the call.
|
|
99
|
+
* `Camera.tsx` does this wiring automatically when the host opts
|
|
100
|
+
* into this driver.
|
|
101
|
+
*/
|
|
102
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
103
|
+
exports.useFrameProcessorDriver = useFrameProcessorDriver;
|
|
104
|
+
const react_1 = require("react");
|
|
105
|
+
const react_native_sensors_1 = require("react-native-sensors");
|
|
106
|
+
// Reanimated's `useSharedValue` is the documented vision-camera
|
|
107
|
+
// idiom, but it's a heavy peer dep. `react-native-worklets-core`
|
|
108
|
+
// (already a transitive dep via vision-camera v4 on RN 0.84) exposes
|
|
109
|
+
// the same API surface (a `value` getter/setter readable from
|
|
110
|
+
// worklets and the JS thread) and is sufficient for our use.
|
|
111
|
+
const react_native_worklets_core_1 = require("react-native-worklets-core");
|
|
112
|
+
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
113
|
+
function useFrameProcessorDriver(options = {}) {
|
|
114
|
+
const { gyroIntervalMs = 33, fovHorizDegrees = 65, fovVertDegrees = 50, evalEveryNFrames = 1, } = options;
|
|
115
|
+
// ── Plugin acquisition ──────────────────────────────────────────
|
|
116
|
+
//
|
|
117
|
+
// `initFrameProcessorPlugin` can return `undefined` if called
|
|
118
|
+
// before vision-camera's plugin registry has finished initialising
|
|
119
|
+
// (race observed in F8.1.a). We retry on a fixed timer instead of
|
|
120
|
+
// firing on every render — the earlier render-driven pattern
|
|
121
|
+
// (adversarial-review H3) re-invoked `initFrameProcessorPlugin`
|
|
122
|
+
// 60+ times per second during recording, and the vision-camera
|
|
123
|
+
// contract for repeated lookups is undocumented.
|
|
124
|
+
//
|
|
125
|
+
// Pattern: mount-once useEffect, try synchronously, fall back to a
|
|
126
|
+
// 16-ms retry timer until success or unmount.
|
|
127
|
+
const [plugin, setPlugin] = (0, react_1.useState)(null);
|
|
128
|
+
(0, react_1.useEffect)(() => {
|
|
129
|
+
let cancelled = false;
|
|
130
|
+
let timerId = null;
|
|
131
|
+
const tryAcquire = () => {
|
|
132
|
+
if (cancelled)
|
|
133
|
+
return;
|
|
134
|
+
const p = react_native_vision_camera_1.VisionCameraProxy.initFrameProcessorPlugin('cv_flow_gate_process_frame', {});
|
|
135
|
+
if (p != null) {
|
|
136
|
+
setPlugin(p);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// ~one display-frame retry — matches the F8.1.a observation
|
|
140
|
+
// that the registry becomes ready by the next render tick.
|
|
141
|
+
timerId = setTimeout(tryAcquire, 16);
|
|
142
|
+
};
|
|
143
|
+
tryAcquire();
|
|
144
|
+
return () => {
|
|
145
|
+
cancelled = true;
|
|
146
|
+
if (timerId != null)
|
|
147
|
+
clearTimeout(timerId);
|
|
148
|
+
};
|
|
149
|
+
// Empty deps on purpose — runs ONCE on mount. Re-acquiring on
|
|
150
|
+
// re-render would race with worklet binding.
|
|
151
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
152
|
+
}, []);
|
|
153
|
+
// ── Shared values (worklet ↔ JS thread) ─────────────────────────
|
|
154
|
+
//
|
|
155
|
+
// Reanimated guarantees coherent reads from the producer thread.
|
|
156
|
+
// We write yaw/pitch on the JS thread (gyro callbacks); the worklet
|
|
157
|
+
// reads them every frame. No round-trip cost — these are mapped
|
|
158
|
+
// into the worklet's runtime by the Reanimated bridge.
|
|
159
|
+
//
|
|
160
|
+
// FoV-derived values (the "half-angle tangent reciprocal"
|
|
161
|
+
// f-numerators) are pre-computed on the JS thread + published via
|
|
162
|
+
// shared values so the worklet's dependency array shrinks to just
|
|
163
|
+
// `[plugin]`. Earlier draft baked `fovHorizDegrees` /
|
|
164
|
+
// `fovVertDegrees` into the closure → worklet re-serialised on
|
|
165
|
+
// every host re-render that changed the prop refs (adversarial-
|
|
166
|
+
// review M1).
|
|
167
|
+
const sharedYaw = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
168
|
+
const sharedPitch = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
169
|
+
// F8.3-followup-roll — integrate gyroscope Z (out-of-screen for a
|
|
170
|
+
// portrait device) to track wrist-twist roll. Field captures with
|
|
171
|
+
// casual hand-hold rarely stay perfectly level; without this the
|
|
172
|
+
// pose stream lies and the cv::Stitcher's intrinsic estimator may
|
|
173
|
+
// pick a worse projection mode.
|
|
174
|
+
const sharedRoll = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
175
|
+
const sharedFrameCounter = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
176
|
+
const sharedEvalEveryN = (0, react_native_worklets_core_1.useSharedValue)(Math.max(1, evalEveryNFrames));
|
|
177
|
+
const sharedFxNumerator = (0, react_native_worklets_core_1.useSharedValue)(1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2)));
|
|
178
|
+
const sharedFyNumerator = (0, react_native_worklets_core_1.useSharedValue)(1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2)));
|
|
179
|
+
// Keep prop-derived shared values in sync. Cheap re-renders;
|
|
180
|
+
// these don't trigger worklet rebuild.
|
|
181
|
+
(0, react_1.useEffect)(() => {
|
|
182
|
+
sharedEvalEveryN.value = Math.max(1, evalEveryNFrames);
|
|
183
|
+
}, [evalEveryNFrames, sharedEvalEveryN]);
|
|
184
|
+
(0, react_1.useEffect)(() => {
|
|
185
|
+
sharedFxNumerator.value =
|
|
186
|
+
1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2));
|
|
187
|
+
}, [fovHorizDegrees, sharedFxNumerator]);
|
|
188
|
+
(0, react_1.useEffect)(() => {
|
|
189
|
+
sharedFyNumerator.value =
|
|
190
|
+
1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2));
|
|
191
|
+
}, [fovVertDegrees, sharedFyNumerator]);
|
|
192
|
+
// ── Lifecycle state (JS thread only) ────────────────────────────
|
|
193
|
+
const gyroSubRef = (0, react_1.useRef)(null);
|
|
194
|
+
const lastGyroAtRef = (0, react_1.useRef)(null);
|
|
195
|
+
const isRunningRef = (0, react_1.useRef)(false);
|
|
196
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
197
|
+
if (gyroSubRef.current) {
|
|
198
|
+
gyroSubRef.current.unsubscribe();
|
|
199
|
+
gyroSubRef.current = null;
|
|
200
|
+
}
|
|
201
|
+
isRunningRef.current = false;
|
|
202
|
+
sharedYaw.value = 0;
|
|
203
|
+
sharedPitch.value = 0;
|
|
204
|
+
sharedRoll.value = 0;
|
|
205
|
+
sharedFrameCounter.value = 0;
|
|
206
|
+
lastGyroAtRef.current = null;
|
|
207
|
+
}, [sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
|
|
208
|
+
const start = (0, react_1.useCallback)(() => {
|
|
209
|
+
if (isRunningRef.current)
|
|
210
|
+
return;
|
|
211
|
+
sharedYaw.value = 0;
|
|
212
|
+
sharedPitch.value = 0;
|
|
213
|
+
sharedRoll.value = 0;
|
|
214
|
+
sharedFrameCounter.value = 0;
|
|
215
|
+
lastGyroAtRef.current = null;
|
|
216
|
+
isRunningRef.current = true;
|
|
217
|
+
// Gyro integration. Each sample carries angular velocity in
|
|
218
|
+
// rad/s; multiply by dt to accumulate displacement. Axes for a
|
|
219
|
+
// device held portrait:
|
|
220
|
+
// y = horizontal pan (yaw, about world-Y)
|
|
221
|
+
// x = vertical tilt (pitch, about world-X)
|
|
222
|
+
// z = wrist-twist roll (about world-Z, normal to the screen)
|
|
223
|
+
// Signs match the legacy `useIncrementalJSDriver` for x/y; z
|
|
224
|
+
// follows the same right-hand-rule convention. If field
|
|
225
|
+
// captures show inverted roll, flip the sign on `z * dt` below.
|
|
226
|
+
(0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, gyroIntervalMs);
|
|
227
|
+
gyroSubRef.current = react_native_sensors_1.gyroscope.subscribe({
|
|
228
|
+
next: ({ x, y, z }) => {
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
if (lastGyroAtRef.current === null) {
|
|
231
|
+
lastGyroAtRef.current = now;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const dt = (now - lastGyroAtRef.current) / 1000.0;
|
|
235
|
+
lastGyroAtRef.current = now;
|
|
236
|
+
sharedYaw.value += y * dt;
|
|
237
|
+
sharedPitch.value += x * dt;
|
|
238
|
+
sharedRoll.value += z * dt;
|
|
239
|
+
},
|
|
240
|
+
error: (err) => {
|
|
241
|
+
// eslint-disable-next-line no-console
|
|
242
|
+
console.warn('[useFrameProcessorDriver] gyro error', err);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}, [gyroIntervalMs, sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
|
|
246
|
+
// ── Worklet ─────────────────────────────────────────────────────
|
|
247
|
+
//
|
|
248
|
+
// Memoised: rebuilt only when the plugin acquires (null → defined)
|
|
249
|
+
// or when the FoV props change (cheap math but they're in the
|
|
250
|
+
// closure so they must be in the deps). Shared values are NOT in
|
|
251
|
+
// the deps — Reanimated wires their .value reads through the
|
|
252
|
+
// worklet's frozen runtime independently of React's render cycle.
|
|
253
|
+
const frameProcessor = (0, react_native_vision_camera_1.useFrameProcessor)((frame) => {
|
|
254
|
+
'worklet';
|
|
255
|
+
if (plugin == null)
|
|
256
|
+
return;
|
|
257
|
+
// Throttle: only every Nth frame. Counter increments first so
|
|
258
|
+
// frame #0 is "due" (N>=1 always divides 0). Cheaper than
|
|
259
|
+
// calling the plugin on rejected frames; saves the ~1 µs
|
|
260
|
+
// marshalling cost per skip.
|
|
261
|
+
sharedFrameCounter.value += 1;
|
|
262
|
+
const N = sharedEvalEveryN.value;
|
|
263
|
+
if (N > 1 && (sharedFrameCounter.value % N) !== 0)
|
|
264
|
+
return;
|
|
265
|
+
// Synthesise quaternion from accumulated yaw + pitch + roll.
|
|
266
|
+
// YPR Tait-Bryan order: q = q_yaw * q_pitch * q_roll. When
|
|
267
|
+
// roll=0 this reduces to the legacy 2-axis form (cy*sp, sy*cp,
|
|
268
|
+
// -sy*sp, cy*cp), so captures held level produce identical
|
|
269
|
+
// poses to the pre-F8.3-followup-roll behaviour. See the
|
|
270
|
+
// expanded math in the file header doc-comment.
|
|
271
|
+
const halfYaw = sharedYaw.value / 2;
|
|
272
|
+
const halfPitch = sharedPitch.value / 2;
|
|
273
|
+
const halfRoll = sharedRoll.value / 2;
|
|
274
|
+
const cy_ = Math.cos(halfYaw);
|
|
275
|
+
const sy_ = Math.sin(halfYaw);
|
|
276
|
+
const cp = Math.cos(halfPitch);
|
|
277
|
+
const sp = Math.sin(halfPitch);
|
|
278
|
+
const cr = Math.cos(halfRoll);
|
|
279
|
+
const sr = Math.sin(halfRoll);
|
|
280
|
+
const qx = cy_ * sp * cr + sy_ * cp * sr;
|
|
281
|
+
const qy = sy_ * cp * cr - cy_ * sp * sr;
|
|
282
|
+
const qz = cy_ * cp * sr - sy_ * sp * cr;
|
|
283
|
+
const qw = cy_ * cp * cr + sy_ * sp * sr;
|
|
284
|
+
// Intrinsics from FoV + actual frame dims.
|
|
285
|
+
// fx = w * (1 / (2 * tan(fovH/2))) (the parenthesised half
|
|
286
|
+
// is the precomputed `sharedFxNumerator` — see M1 fix).
|
|
287
|
+
const w = frame.width;
|
|
288
|
+
const h = frame.height;
|
|
289
|
+
const fx = w * sharedFxNumerator.value;
|
|
290
|
+
const fy = h * sharedFyNumerator.value;
|
|
291
|
+
plugin.call(frame, {
|
|
292
|
+
tx: 0, ty: 0, tz: 0,
|
|
293
|
+
qx, qy, qz, qw,
|
|
294
|
+
fx, fy,
|
|
295
|
+
cx: w / 2, cy: h / 2,
|
|
296
|
+
imageWidth: w, imageHeight: h,
|
|
297
|
+
timestampMs: 0,
|
|
298
|
+
// 2 == RNSARTrackingState.tracking — we always claim "good
|
|
299
|
+
// tracking" because there's no ARKit signal to differentiate
|
|
300
|
+
// (matches legacy useIncrementalJSDriver semantics).
|
|
301
|
+
trackingStateRaw: 2,
|
|
302
|
+
});
|
|
303
|
+
// Deps array intentionally minimal: only `plugin` actually
|
|
304
|
+
// requires worklet rebuild. All FoV / pose / counter / cadence
|
|
305
|
+
// values flow through stable shared-value refs that Reanimated
|
|
306
|
+
// wires through the producer-thread runtime independently of
|
|
307
|
+
// React's render cycle. (Adversarial-review M1.)
|
|
308
|
+
}, [plugin]);
|
|
309
|
+
// ── Return handle ───────────────────────────────────────────────
|
|
310
|
+
//
|
|
311
|
+
// Returns a getter for `isRunning` so callers always see the live
|
|
312
|
+
// state (the hook itself doesn't re-render on start/stop — that's
|
|
313
|
+
// intentional, avoids stale-Camera-prop churn).
|
|
314
|
+
return (0, react_1.useMemo)(() => ({
|
|
315
|
+
start,
|
|
316
|
+
stop,
|
|
317
|
+
frameProcessor: plugin != null ? frameProcessor : null,
|
|
318
|
+
get isRunning() { return isRunningRef.current; },
|
|
319
|
+
}), [start, stop, plugin, frameProcessor]);
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=useFrameProcessorDriver.js.map
|
|
@@ -44,6 +44,11 @@ exports.useIncrementalJSDriver = useIncrementalJSDriver;
|
|
|
44
44
|
const react_1 = require("react");
|
|
45
45
|
const react_native_1 = require("react-native");
|
|
46
46
|
const react_native_sensors_1 = require("react-native-sensors");
|
|
47
|
+
// One-shot deprecation flag — module-scoped so multiple host
|
|
48
|
+
// instances of the hook all share the same gate and we only emit
|
|
49
|
+
// the warning the first time anyone calls .start() in this
|
|
50
|
+
// process.
|
|
51
|
+
let deprecationWarningEmitted = false;
|
|
47
52
|
function getNativeIncremental() {
|
|
48
53
|
const m = react_native_1.NativeModules['IncrementalStitcher'];
|
|
49
54
|
if (!m || typeof m !== 'object')
|
|
@@ -88,6 +93,22 @@ function useIncrementalJSDriver(options = {}) {
|
|
|
88
93
|
// non-AR mode.
|
|
89
94
|
if (isRunningRef.current)
|
|
90
95
|
return;
|
|
96
|
+
// F8.5 — one-shot deprecation warning. v0.5.0 introduced
|
|
97
|
+
// `useFrameProcessorDriver` (vision-camera producer-thread
|
|
98
|
+
// path, native frame rate, no JPEG round-trip). The legacy
|
|
99
|
+
// takeSnapshot path stays available for one minor cycle to
|
|
100
|
+
// give hosts time to migrate, then is removed in v0.6.
|
|
101
|
+
if (!deprecationWarningEmitted) {
|
|
102
|
+
deprecationWarningEmitted = true;
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.warn('[react-native-image-stitcher] `useIncrementalJSDriver` '
|
|
105
|
+
+ 'is DEPRECATED as of v0.5.0 and will be REMOVED in '
|
|
106
|
+
+ 'v0.6.0. Migrate to `useFrameProcessorDriver` (or '
|
|
107
|
+
+ 'simply let `<Camera>` use its default driver — no host '
|
|
108
|
+
+ 'code change needed). Opt-out via the `legacyDriver` '
|
|
109
|
+
+ 'prop on `<Camera>` if you need to stay on the legacy '
|
|
110
|
+
+ 'path temporarily.');
|
|
111
|
+
}
|
|
91
112
|
const native = getNativeIncremental();
|
|
92
113
|
if (!native)
|
|
93
114
|
return;
|
|
@@ -362,6 +362,29 @@ public final class IncrementalStitcher: NSObject {
|
|
|
362
362
|
private var hasFirstFrameTranslation: Bool = false
|
|
363
363
|
private var consumeFrameCounter: Int = 0
|
|
364
364
|
|
|
365
|
+
/// F8.3 — gate for `consumeFrameFromPlugin` (the vision-camera
|
|
366
|
+
/// Frame Processor producer-thread entry point). TRUE only when
|
|
367
|
+
/// the current capture was started with
|
|
368
|
+
/// `frameSourceMode == "frameProcessor"`. In any other mode
|
|
369
|
+
/// (especially the legacy `"jsDriver"` path which feeds via
|
|
370
|
+
/// `processFrameAtPath`), the plugin would double-feed the
|
|
371
|
+
/// engine — pixel buffers from the producer thread + JPEG paths
|
|
372
|
+
/// from the JS interval, racing on the same workQueue — so we
|
|
373
|
+
/// drop the producer-thread call.
|
|
374
|
+
///
|
|
375
|
+
/// Set under `stateLock` in `start()`, cleared under `stateLock`
|
|
376
|
+
/// in `cancel()` and `finalize()`, ALSO read under `stateLock`
|
|
377
|
+
/// from `consumeFrameFromPlugin`. The lock-protected read is
|
|
378
|
+
/// the simplest correctness story under Swift's
|
|
379
|
+
/// implementation-defined memory model — an earlier draft did an
|
|
380
|
+
/// unlocked read on the assumption "Bool loads are atomic on
|
|
381
|
+
/// arm64", but that's only true for the *instruction*, not for
|
|
382
|
+
/// compiler reordering / CSE if the property dispatch ever
|
|
383
|
+
/// changes from `@objc` (Obj-C dynamic, opaque to the optimiser)
|
|
384
|
+
/// to a Swift-only call (where the load could be hoisted).
|
|
385
|
+
/// Adversarial-review H1.
|
|
386
|
+
@objc public private(set) var frameProcessorIngestEnabled: Bool = false
|
|
387
|
+
|
|
365
388
|
/// V16 — pose-driven keyframe gate. When `enabled` (set from the
|
|
366
389
|
/// JS `frameSelectionMode = "pose-based"` config), each ARFrame is
|
|
367
390
|
/// projected onto the latched ARKit plane and accepted only when
|
|
@@ -916,6 +939,11 @@ public final class IncrementalStitcher: NSObject {
|
|
|
916
939
|
self.batchKeyframeMode = false
|
|
917
940
|
}
|
|
918
941
|
self.isRunning = true
|
|
942
|
+
// F8.3 — enable the Frame Processor plugin's producer-thread
|
|
943
|
+
// ingest only for the new "frameProcessor" mode. Any other
|
|
944
|
+
// mode (arSession, jsDriver) keeps it OFF; see the ivar's
|
|
945
|
+
// declaration comment for why.
|
|
946
|
+
self.frameProcessorIngestEnabled = (frameSourceMode == "frameProcessor")
|
|
919
947
|
self.snapshotJpegQuality = max(1, min(100, snapshotJpegQuality))
|
|
920
948
|
self.snapshotEveryNAccepts = max(1, snapshotEveryNAccepts)
|
|
921
949
|
self.acceptsSinceSnapshot = 0
|
|
@@ -1044,14 +1072,30 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1044
1072
|
|
|
1045
1073
|
stateLock.unlock()
|
|
1046
1074
|
|
|
1047
|
-
// Register with the AR session
|
|
1048
|
-
// AR
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
|
|
1075
|
+
// Register with the AR session's consumer registry — ONLY
|
|
1076
|
+
// for AR mode. Other modes don't need it:
|
|
1077
|
+
//
|
|
1078
|
+
// * `arSession` — REGISTER. ARKit's frame delegate
|
|
1079
|
+
// (RNSARSession.swift:572) calls
|
|
1080
|
+
// `consumer.consumeFrame(...)`.
|
|
1081
|
+
// * `frameProcessor` — DO NOT register. The vision-
|
|
1082
|
+
// camera plugin calls us directly via
|
|
1083
|
+
// `consumeFrameFromPlugin`; we own
|
|
1084
|
+
// the camera, ARKit is intentionally
|
|
1085
|
+
// stopped. Registering here would
|
|
1086
|
+
// let any sibling code that briefly
|
|
1087
|
+
// starts an `ARSession` mid-capture
|
|
1088
|
+
// (analytics SDK, future "AR preview"
|
|
1089
|
+
// toggle, etc.) silently feed frames
|
|
1090
|
+
// in parallel with our producer-
|
|
1091
|
+
// thread plugin, racing on
|
|
1092
|
+
// `stateLock.try()` and corrupting
|
|
1093
|
+
// the gate's novelty math.
|
|
1094
|
+
// (Adversarial-review C1.)
|
|
1095
|
+
// * `jsDriver` — DO NOT register. Legacy path uses
|
|
1096
|
+
// `processFrameAtPath`; bypasses
|
|
1097
|
+
// consumeFrame entirely.
|
|
1098
|
+
if frameSourceMode == "arSession" {
|
|
1055
1099
|
RNSARSession.shared.incrementalConsumer = self
|
|
1056
1100
|
}
|
|
1057
1101
|
}
|
|
@@ -1259,6 +1303,13 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1259
1303
|
self.keyframePaths = []
|
|
1260
1304
|
self.keyframePoses = []
|
|
1261
1305
|
self.isRunning = false
|
|
1306
|
+
// F8.3 — disable the Frame Processor plugin's producer-thread
|
|
1307
|
+
// ingest at the SAME lock-protected moment we flip isRunning,
|
|
1308
|
+
// so any in-flight producer-thread frame either sees both
|
|
1309
|
+
// (and proceeds with a now-doomed call that consumeFrame
|
|
1310
|
+
// drops via its own !isRunning guard) or sees neither (and
|
|
1311
|
+
// skips entirely).
|
|
1312
|
+
self.frameProcessorIngestEnabled = false
|
|
1262
1313
|
let drops = self.droppedBackpressure
|
|
1263
1314
|
stateLock.unlock()
|
|
1264
1315
|
|
|
@@ -2048,6 +2099,9 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2048
2099
|
self.keyframePaths = []
|
|
2049
2100
|
self.keyframePoses = []
|
|
2050
2101
|
self.isRunning = false
|
|
2102
|
+
// F8.3 — mirror the finalize() flip: cut producer-thread
|
|
2103
|
+
// ingest the moment we go !isRunning.
|
|
2104
|
+
self.frameProcessorIngestEnabled = false
|
|
2051
2105
|
self.lastState = nil
|
|
2052
2106
|
// V16 — reset the keyframe gate so the next start() begins
|
|
2053
2107
|
// with a clean polygon state and counter. Safe to do under
|
|
@@ -3039,3 +3093,69 @@ public final class IncrementalStitcher: NSObject {
|
|
|
3039
3093
|
}
|
|
3040
3094
|
|
|
3041
3095
|
extension IncrementalStitcher: ARFrameConsumer {}
|
|
3096
|
+
|
|
3097
|
+
// MARK: - F8.3 — Frame Processor entry point
|
|
3098
|
+
//
|
|
3099
|
+
// `consumeFrameFromPlugin` is a thin @objc-compatible wrapper around
|
|
3100
|
+
// `consumeFrame(pixelBuffer:pose:)` that takes primitive args instead
|
|
3101
|
+
// of a `RNSARFramePose` instance. It exists so the
|
|
3102
|
+
// `KeyframeGateFrameProcessor.mm` plugin (ObjC++ producer-thread code)
|
|
3103
|
+
// can submit a frame without needing to construct a Swift class
|
|
3104
|
+
// across the bridging header.
|
|
3105
|
+
//
|
|
3106
|
+
// Threading: the worklet runs on vision-camera's producer thread
|
|
3107
|
+
// (NOT ARKit's delegate queue). Both threads ultimately serialise on
|
|
3108
|
+
// `consumeFrame`'s `stateLock.try()`, which is the documented
|
|
3109
|
+
// reentrancy boundary.
|
|
3110
|
+
//
|
|
3111
|
+
// In non-AR (Frame Processor) mode the caller supplies:
|
|
3112
|
+
// * `pixelBuffer` from `frame.buffer` (vision-camera YUV biplanar)
|
|
3113
|
+
// * `tx`/`ty`/`tz` = 0 (no AR translation; gyro only gives rotation)
|
|
3114
|
+
// * `qx,qy,qz,qw` from JS-thread gyro-integrated yaw+pitch (synthesised
|
|
3115
|
+
// as `q = q_yaw * q_pitch` — same convention as
|
|
3116
|
+
// `useIncrementalJSDriver`'s pose synthesis)
|
|
3117
|
+
// * `fx`/`fy` from frame dims + assumed FoV
|
|
3118
|
+
// * `cx`/`cy` at image centre
|
|
3119
|
+
// * `trackingStateRaw = 2` (= `.tracking`) — non-AR captures don't have
|
|
3120
|
+
// a real ARKit tracking-quality signal; reporting `.tracking` keeps
|
|
3121
|
+
// the engine's `trackingPoor` path inactive, matching the legacy
|
|
3122
|
+
// `useIncrementalJSDriver` contract.
|
|
3123
|
+
extension IncrementalStitcher {
|
|
3124
|
+
@objc public func consumeFrameFromPlugin(
|
|
3125
|
+
pixelBuffer: CVPixelBuffer,
|
|
3126
|
+
tx: Double, ty: Double, tz: Double,
|
|
3127
|
+
qx: Double, qy: Double, qz: Double, qw: Double,
|
|
3128
|
+
fx: Double, fy: Double, cx: Double, cy: Double,
|
|
3129
|
+
imageWidth: Int, imageHeight: Int,
|
|
3130
|
+
timestampMs: Double,
|
|
3131
|
+
trackingStateRaw: Int
|
|
3132
|
+
) {
|
|
3133
|
+
// F8.3 — drop the call unless this capture was started in
|
|
3134
|
+
// frameProcessor mode. Read under stateLock so the producer
|
|
3135
|
+
// thread can't observe a stale TRUE during a cancel/finalize
|
|
3136
|
+
// teardown (adversarial-review H1). The lock-protected
|
|
3137
|
+
// read costs ~1 µs at producer-thread rate; negligible vs
|
|
3138
|
+
// the deep-copy that follows on accepts.
|
|
3139
|
+
stateLock.lock()
|
|
3140
|
+
let enabled = self.frameProcessorIngestEnabled
|
|
3141
|
+
stateLock.unlock()
|
|
3142
|
+
guard enabled else { return }
|
|
3143
|
+
|
|
3144
|
+
// Map the raw enum integer. Unknown values fall back to
|
|
3145
|
+
// `.notAvailable` so the engine's existing tracking-poor
|
|
3146
|
+
// branches catch them — failing CLOSED is safer than
|
|
3147
|
+
// silently claiming healthy tracking when the JS side sent
|
|
3148
|
+
// garbage (adversarial-review C2).
|
|
3149
|
+
let trackingState =
|
|
3150
|
+
RNSARTrackingState(rawValue: trackingStateRaw) ?? .notAvailable
|
|
3151
|
+
let pose = RNSARFramePose(
|
|
3152
|
+
tx: tx, ty: ty, tz: tz,
|
|
3153
|
+
qx: qx, qy: qy, qz: qz, qw: qw,
|
|
3154
|
+
fx: fx, fy: fy, cx: cx, cy: cy,
|
|
3155
|
+
imageWidth: imageWidth, imageHeight: imageHeight,
|
|
3156
|
+
timestampMs: timestampMs,
|
|
3157
|
+
trackingState: trackingState
|
|
3158
|
+
)
|
|
3159
|
+
consumeFrame(pixelBuffer: pixelBuffer, pose: pose)
|
|
3160
|
+
}
|
|
3161
|
+
}
|