react-native-image-stitcher 0.4.1 → 0.5.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 +165 -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/IncrementalFirstwinsEngine.kt +148 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +431 -23
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +65 -7
- package/dist/camera/Camera.d.ts +68 -1
- package/dist/camera/Camera.js +102 -16
- 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/Package.swift +35 -21
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +188 -8
- package/ios/Sources/RNImageStitcher/KeyframeGateFrameProcessor.mm +196 -0
- package/package.json +3 -1
- package/src/camera/Camera.tsx +190 -15
- 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,407 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* useFrameProcessorDriver — vision-camera Frame Processor + gyro
|
|
4
|
+
* driver for the incremental panorama engine. Replaces
|
|
5
|
+
* `useIncrementalJSDriver` in non-AR captures.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists (vs the JS-driver predecessor)
|
|
8
|
+
*
|
|
9
|
+
* The JS driver takes a JPEG snapshot every ~250 ms and feeds the
|
|
10
|
+
* path to `IncrementalStitcher.processFrameAtPath`. That path
|
|
11
|
+
* has three costs:
|
|
12
|
+
*
|
|
13
|
+
* 1. JPEG encode (`takeSnapshot` ≈ 30–80 ms on iPhone 16 Pro)
|
|
14
|
+
* 2. Disk write of the JPEG
|
|
15
|
+
* 3. JPEG decode + cv::Mat alloc inside the engine
|
|
16
|
+
*
|
|
17
|
+
* Per-frame round-trip ~80 ms means ~4 Hz max throughput, and
|
|
18
|
+
* ~80 ms latency between "this is the moment to accept" and "this
|
|
19
|
+
* frame is in the engine". Both numbers caused operator-felt lag
|
|
20
|
+
* on long shelf pans.
|
|
21
|
+
*
|
|
22
|
+
* This hook uses vision-camera's Frame Processor instead. The
|
|
23
|
+
* worklet runs on the camera producer thread at the native frame
|
|
24
|
+
* rate (30 fps on iOS). Each frame goes through a JSI plugin
|
|
25
|
+
* (`cv_flow_gate_process_frame`) directly into
|
|
26
|
+
* `IncrementalStitcher.consumeFrame` — the SAME entry point AR
|
|
27
|
+
* mode uses, with the engine's existing KeyframeGate making the
|
|
28
|
+
* accept/reject decision. Rejected frames cost ~3–8 ms; accepted
|
|
29
|
+
* frames take the same deep-copy + workQueue path AR mode takes.
|
|
30
|
+
*
|
|
31
|
+
* Net wins: no JPEG round-trip on rejected frames, no disk thrash
|
|
32
|
+
* during recording, lower latency to accept, full 30 fps gate
|
|
33
|
+
* evaluation budget.
|
|
34
|
+
*
|
|
35
|
+
* Pose synthesis
|
|
36
|
+
*
|
|
37
|
+
* Non-AR mode has no ARKit pose. We integrate the gyroscope on
|
|
38
|
+
* the JS thread (`react-native-sensors`), accumulate yaw + pitch,
|
|
39
|
+
* and publish them via Reanimated `useSharedValue` so the worklet
|
|
40
|
+
* can read them WITHOUT a thread hop. Translation is reported as
|
|
41
|
+
* zero (no IMU translation; this is a known limitation we share
|
|
42
|
+
* with the legacy driver — drift ~1–2°/min over a 30 s capture is
|
|
43
|
+
* below the gate's overlap threshold and rarely matters).
|
|
44
|
+
*
|
|
45
|
+
* Quaternion synthesis (q = q_yaw * q_pitch * q_roll, Tait-Bryan
|
|
46
|
+
* YPR order to match the legacy driver's body-frame intent):
|
|
47
|
+
* q_yaw = (0, sin(yaw/2), 0, cos(yaw/2))
|
|
48
|
+
* q_pitch = (sin(pitch/2), 0, 0, cos(pitch/2))
|
|
49
|
+
* q_roll = (0, 0, sin(roll/2), cos(roll/2))
|
|
50
|
+
*
|
|
51
|
+
* Expanded (cy, sy = cos/sin(yaw/2); analogous for cp/sp, cr/sr):
|
|
52
|
+
* qx = cy*sp*cr + sy*cp*sr
|
|
53
|
+
* qy = sy*cp*cr - cy*sp*sr
|
|
54
|
+
* qz = cy*cp*sr - sy*sp*cr
|
|
55
|
+
* qw = cy*cp*cr + sy*sp*sr
|
|
56
|
+
*
|
|
57
|
+
* When roll=0 this collapses to the 2-axis form
|
|
58
|
+
* `(cy*sp, sy*cp, -sy*sp, cy*cp)` the legacy driver used, so
|
|
59
|
+
* captures held perfectly level produce identical poses to the
|
|
60
|
+
* pre-roll behaviour.
|
|
61
|
+
*
|
|
62
|
+
* Intrinsics are synthesised from the actual frame dimensions
|
|
63
|
+
* (`frame.width`, `frame.height`) plus the host-provided
|
|
64
|
+
* horizontal/vertical FoV defaults. The stitcher derives its FoV-
|
|
65
|
+
* overlap window from these, so the assumed FoV matters for the
|
|
66
|
+
* gate's overlap math but not for the panorama itself (the
|
|
67
|
+
* stitcher feature-matches + RANSACs the final alignment).
|
|
68
|
+
*
|
|
69
|
+
* Throttling
|
|
70
|
+
*
|
|
71
|
+
* `evalEveryNFrames` controls how often the worklet calls the
|
|
72
|
+
* plugin. Default 1 (every frame). Set higher to amortise the
|
|
73
|
+
* plugin call + consumeFrame's gate evaluation across multiple
|
|
74
|
+
* producer-thread frames on lower-end devices. Independent of —
|
|
75
|
+
* and stacks on top of — the stitcher's own internal
|
|
76
|
+
* `flowEvalEveryNFrames` (see `KeyframeGate.swift`); both
|
|
77
|
+
* throttles can be active simultaneously and the effective cadence
|
|
78
|
+
* is `evalEveryNFrames * flowEvalEveryNFrames`.
|
|
79
|
+
*
|
|
80
|
+
* Lifecycle
|
|
81
|
+
*
|
|
82
|
+
* `start()` subscribes to the gyro and resets pose accumulators.
|
|
83
|
+
* `stop()` unsubscribes and resets. The returned `frameProcessor`
|
|
84
|
+
* is meant to be passed to `<Camera frameProcessor={...} />` —
|
|
85
|
+
* it's stable as long as the plugin reference and the FoV props
|
|
86
|
+
* haven't changed. Returns `null` when the plugin isn't loaded
|
|
87
|
+
* yet; pass `null`-or-fallback to the Camera in that case.
|
|
88
|
+
*
|
|
89
|
+
* Pairing with `IncrementalStitcher.start({frameSourceMode})`
|
|
90
|
+
*
|
|
91
|
+
* The plugin's per-frame call into `consumeFrameFromPlugin` is
|
|
92
|
+
* gated by `IncrementalStitcher.frameProcessorIngestEnabled`,
|
|
93
|
+
* which is TRUE only when the stitcher was started with
|
|
94
|
+
* `frameSourceMode === 'frameProcessor'`. Hosts MUST call
|
|
95
|
+
* `incrementalStitcher.start({ frameSourceMode: 'frameProcessor',
|
|
96
|
+
* ... })` to actually get frames into the engine — otherwise the
|
|
97
|
+
* worklet runs to completion but the wrapper drops the call.
|
|
98
|
+
* `Camera.tsx` does this wiring automatically when the host opts
|
|
99
|
+
* into this driver.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
103
|
+
import {
|
|
104
|
+
gyroscope,
|
|
105
|
+
setUpdateIntervalForType,
|
|
106
|
+
SensorTypes,
|
|
107
|
+
} from 'react-native-sensors';
|
|
108
|
+
import type { Subscription } from 'rxjs';
|
|
109
|
+
// Reanimated's `useSharedValue` is the documented vision-camera
|
|
110
|
+
// idiom, but it's a heavy peer dep. `react-native-worklets-core`
|
|
111
|
+
// (already a transitive dep via vision-camera v4 on RN 0.84) exposes
|
|
112
|
+
// the same API surface (a `value` getter/setter readable from
|
|
113
|
+
// worklets and the JS thread) and is sufficient for our use.
|
|
114
|
+
import { useSharedValue } from 'react-native-worklets-core';
|
|
115
|
+
import {
|
|
116
|
+
useFrameProcessor,
|
|
117
|
+
VisionCameraProxy,
|
|
118
|
+
} from 'react-native-vision-camera';
|
|
119
|
+
import type {
|
|
120
|
+
FrameProcessorPlugin,
|
|
121
|
+
ReadonlyFrameProcessor,
|
|
122
|
+
} from 'react-native-vision-camera';
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
export interface UseFrameProcessorDriverOptions {
|
|
126
|
+
/**
|
|
127
|
+
* Gyro sample interval in ms (~30 Hz default). Drives the JS-
|
|
128
|
+
* thread pose integration loop; not the producer-thread plugin
|
|
129
|
+
* call rate (the plugin runs at vision-camera's frame rate,
|
|
130
|
+
* usually 30 fps).
|
|
131
|
+
*/
|
|
132
|
+
gyroIntervalMs?: number;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Approximate horizontal FoV of the device camera, used to
|
|
136
|
+
* synthesise `fx` from frame width. Default 65° matches a typical
|
|
137
|
+
* mid-tier smartphone main camera. Host apps that know the actual
|
|
138
|
+
* FoV (e.g. via `Camera.getCameraFormat`) should pass it here —
|
|
139
|
+
* the engine's overlap gate gets a slightly better estimate.
|
|
140
|
+
*/
|
|
141
|
+
fovHorizDegrees?: number;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Approximate vertical FoV of the device camera, used to
|
|
145
|
+
* synthesise `fy` from frame height. Default 50° matches a
|
|
146
|
+
* typical 4:3 phone camera in landscape; for 16:9 portrait you
|
|
147
|
+
* probably want ~75°.
|
|
148
|
+
*/
|
|
149
|
+
fovVertDegrees?: number;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Evaluate the plugin every Nth producer-thread frame. Default 1
|
|
153
|
+
* (every frame). Higher values reduce the producer-thread cost
|
|
154
|
+
* linearly at the price of acceptance latency — N=3 with 30 fps
|
|
155
|
+
* source = up to 100 ms before a key moment is evaluated.
|
|
156
|
+
*/
|
|
157
|
+
evalEveryNFrames?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
export interface FrameProcessorDriverHandle {
|
|
162
|
+
/** Subscribe to the gyro + reset pose accumulators. Idempotent. */
|
|
163
|
+
start: () => void;
|
|
164
|
+
/** Unsubscribe + reset pose. */
|
|
165
|
+
stop: () => void;
|
|
166
|
+
/**
|
|
167
|
+
* Pass this to `<Camera frameProcessor={...} />`. `null` until
|
|
168
|
+
* the JSI plugin is loaded (typically resolves within ~1 frame of
|
|
169
|
+
* mount); the consumer should fall back to undefined / a no-op
|
|
170
|
+
* processor in that window.
|
|
171
|
+
*/
|
|
172
|
+
frameProcessor: ReadonlyFrameProcessor | null;
|
|
173
|
+
/** Whether `start()` has been called and `stop()` hasn't. */
|
|
174
|
+
isRunning: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
export function useFrameProcessorDriver(
|
|
179
|
+
options: UseFrameProcessorDriverOptions = {},
|
|
180
|
+
): FrameProcessorDriverHandle {
|
|
181
|
+
const {
|
|
182
|
+
gyroIntervalMs = 33,
|
|
183
|
+
fovHorizDegrees = 65,
|
|
184
|
+
fovVertDegrees = 50,
|
|
185
|
+
evalEveryNFrames = 1,
|
|
186
|
+
} = options;
|
|
187
|
+
|
|
188
|
+
// ── Plugin acquisition ──────────────────────────────────────────
|
|
189
|
+
//
|
|
190
|
+
// `initFrameProcessorPlugin` can return `undefined` if called
|
|
191
|
+
// before vision-camera's plugin registry has finished initialising
|
|
192
|
+
// (race observed in F8.1.a). We retry on a fixed timer instead of
|
|
193
|
+
// firing on every render — the earlier render-driven pattern
|
|
194
|
+
// (adversarial-review H3) re-invoked `initFrameProcessorPlugin`
|
|
195
|
+
// 60+ times per second during recording, and the vision-camera
|
|
196
|
+
// contract for repeated lookups is undocumented.
|
|
197
|
+
//
|
|
198
|
+
// Pattern: mount-once useEffect, try synchronously, fall back to a
|
|
199
|
+
// 16-ms retry timer until success or unmount.
|
|
200
|
+
const [plugin, setPlugin] = useState<FrameProcessorPlugin | null>(null);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
let cancelled = false;
|
|
203
|
+
let timerId: ReturnType<typeof setTimeout> | null = null;
|
|
204
|
+
const tryAcquire = () => {
|
|
205
|
+
if (cancelled) return;
|
|
206
|
+
const p = VisionCameraProxy.initFrameProcessorPlugin(
|
|
207
|
+
'cv_flow_gate_process_frame',
|
|
208
|
+
{},
|
|
209
|
+
);
|
|
210
|
+
if (p != null) {
|
|
211
|
+
setPlugin(p);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// ~one display-frame retry — matches the F8.1.a observation
|
|
215
|
+
// that the registry becomes ready by the next render tick.
|
|
216
|
+
timerId = setTimeout(tryAcquire, 16);
|
|
217
|
+
};
|
|
218
|
+
tryAcquire();
|
|
219
|
+
return () => {
|
|
220
|
+
cancelled = true;
|
|
221
|
+
if (timerId != null) clearTimeout(timerId);
|
|
222
|
+
};
|
|
223
|
+
// Empty deps on purpose — runs ONCE on mount. Re-acquiring on
|
|
224
|
+
// re-render would race with worklet binding.
|
|
225
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
// ── Shared values (worklet ↔ JS thread) ─────────────────────────
|
|
229
|
+
//
|
|
230
|
+
// Reanimated guarantees coherent reads from the producer thread.
|
|
231
|
+
// We write yaw/pitch on the JS thread (gyro callbacks); the worklet
|
|
232
|
+
// reads them every frame. No round-trip cost — these are mapped
|
|
233
|
+
// into the worklet's runtime by the Reanimated bridge.
|
|
234
|
+
//
|
|
235
|
+
// FoV-derived values (the "half-angle tangent reciprocal"
|
|
236
|
+
// f-numerators) are pre-computed on the JS thread + published via
|
|
237
|
+
// shared values so the worklet's dependency array shrinks to just
|
|
238
|
+
// `[plugin]`. Earlier draft baked `fovHorizDegrees` /
|
|
239
|
+
// `fovVertDegrees` into the closure → worklet re-serialised on
|
|
240
|
+
// every host re-render that changed the prop refs (adversarial-
|
|
241
|
+
// review M1).
|
|
242
|
+
const sharedYaw = useSharedValue(0);
|
|
243
|
+
const sharedPitch = useSharedValue(0);
|
|
244
|
+
// F8.3-followup-roll — integrate gyroscope Z (out-of-screen for a
|
|
245
|
+
// portrait device) to track wrist-twist roll. Field captures with
|
|
246
|
+
// casual hand-hold rarely stay perfectly level; without this the
|
|
247
|
+
// pose stream lies and the cv::Stitcher's intrinsic estimator may
|
|
248
|
+
// pick a worse projection mode.
|
|
249
|
+
const sharedRoll = useSharedValue(0);
|
|
250
|
+
const sharedFrameCounter = useSharedValue(0);
|
|
251
|
+
const sharedEvalEveryN = useSharedValue(Math.max(1, evalEveryNFrames));
|
|
252
|
+
const sharedFxNumerator = useSharedValue(
|
|
253
|
+
1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2)),
|
|
254
|
+
);
|
|
255
|
+
const sharedFyNumerator = useSharedValue(
|
|
256
|
+
1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2)),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Keep prop-derived shared values in sync. Cheap re-renders;
|
|
260
|
+
// these don't trigger worklet rebuild.
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
sharedEvalEveryN.value = Math.max(1, evalEveryNFrames);
|
|
263
|
+
}, [evalEveryNFrames, sharedEvalEveryN]);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
sharedFxNumerator.value =
|
|
266
|
+
1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2));
|
|
267
|
+
}, [fovHorizDegrees, sharedFxNumerator]);
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
sharedFyNumerator.value =
|
|
270
|
+
1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2));
|
|
271
|
+
}, [fovVertDegrees, sharedFyNumerator]);
|
|
272
|
+
|
|
273
|
+
// ── Lifecycle state (JS thread only) ────────────────────────────
|
|
274
|
+
const gyroSubRef = useRef<Subscription | null>(null);
|
|
275
|
+
const lastGyroAtRef = useRef<number | null>(null);
|
|
276
|
+
const isRunningRef = useRef(false);
|
|
277
|
+
|
|
278
|
+
const stop = useCallback(() => {
|
|
279
|
+
if (gyroSubRef.current) {
|
|
280
|
+
gyroSubRef.current.unsubscribe();
|
|
281
|
+
gyroSubRef.current = null;
|
|
282
|
+
}
|
|
283
|
+
isRunningRef.current = false;
|
|
284
|
+
sharedYaw.value = 0;
|
|
285
|
+
sharedPitch.value = 0;
|
|
286
|
+
sharedRoll.value = 0;
|
|
287
|
+
sharedFrameCounter.value = 0;
|
|
288
|
+
lastGyroAtRef.current = null;
|
|
289
|
+
}, [sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
|
|
290
|
+
|
|
291
|
+
const start = useCallback(() => {
|
|
292
|
+
if (isRunningRef.current) return;
|
|
293
|
+
sharedYaw.value = 0;
|
|
294
|
+
sharedPitch.value = 0;
|
|
295
|
+
sharedRoll.value = 0;
|
|
296
|
+
sharedFrameCounter.value = 0;
|
|
297
|
+
lastGyroAtRef.current = null;
|
|
298
|
+
isRunningRef.current = true;
|
|
299
|
+
|
|
300
|
+
// Gyro integration. Each sample carries angular velocity in
|
|
301
|
+
// rad/s; multiply by dt to accumulate displacement. Axes for a
|
|
302
|
+
// device held portrait:
|
|
303
|
+
// y = horizontal pan (yaw, about world-Y)
|
|
304
|
+
// x = vertical tilt (pitch, about world-X)
|
|
305
|
+
// z = wrist-twist roll (about world-Z, normal to the screen)
|
|
306
|
+
// Signs match the legacy `useIncrementalJSDriver` for x/y; z
|
|
307
|
+
// follows the same right-hand-rule convention. If field
|
|
308
|
+
// captures show inverted roll, flip the sign on `z * dt` below.
|
|
309
|
+
setUpdateIntervalForType(SensorTypes.gyroscope, gyroIntervalMs);
|
|
310
|
+
gyroSubRef.current = gyroscope.subscribe({
|
|
311
|
+
next: ({ x, y, z }) => {
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
if (lastGyroAtRef.current === null) {
|
|
314
|
+
lastGyroAtRef.current = now;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const dt = (now - lastGyroAtRef.current) / 1000.0;
|
|
318
|
+
lastGyroAtRef.current = now;
|
|
319
|
+
sharedYaw.value += y * dt;
|
|
320
|
+
sharedPitch.value += x * dt;
|
|
321
|
+
sharedRoll.value += z * dt;
|
|
322
|
+
},
|
|
323
|
+
error: (err) => {
|
|
324
|
+
// eslint-disable-next-line no-console
|
|
325
|
+
console.warn('[useFrameProcessorDriver] gyro error', err);
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}, [gyroIntervalMs, sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
|
|
329
|
+
|
|
330
|
+
// ── Worklet ─────────────────────────────────────────────────────
|
|
331
|
+
//
|
|
332
|
+
// Memoised: rebuilt only when the plugin acquires (null → defined)
|
|
333
|
+
// or when the FoV props change (cheap math but they're in the
|
|
334
|
+
// closure so they must be in the deps). Shared values are NOT in
|
|
335
|
+
// the deps — Reanimated wires their .value reads through the
|
|
336
|
+
// worklet's frozen runtime independently of React's render cycle.
|
|
337
|
+
const frameProcessor = useFrameProcessor((frame) => {
|
|
338
|
+
'worklet';
|
|
339
|
+
if (plugin == null) return;
|
|
340
|
+
|
|
341
|
+
// Throttle: only every Nth frame. Counter increments first so
|
|
342
|
+
// frame #0 is "due" (N>=1 always divides 0). Cheaper than
|
|
343
|
+
// calling the plugin on rejected frames; saves the ~1 µs
|
|
344
|
+
// marshalling cost per skip.
|
|
345
|
+
sharedFrameCounter.value += 1;
|
|
346
|
+
const N = sharedEvalEveryN.value;
|
|
347
|
+
if (N > 1 && (sharedFrameCounter.value % N) !== 0) return;
|
|
348
|
+
|
|
349
|
+
// Synthesise quaternion from accumulated yaw + pitch + roll.
|
|
350
|
+
// YPR Tait-Bryan order: q = q_yaw * q_pitch * q_roll. When
|
|
351
|
+
// roll=0 this reduces to the legacy 2-axis form (cy*sp, sy*cp,
|
|
352
|
+
// -sy*sp, cy*cp), so captures held level produce identical
|
|
353
|
+
// poses to the pre-F8.3-followup-roll behaviour. See the
|
|
354
|
+
// expanded math in the file header doc-comment.
|
|
355
|
+
const halfYaw = sharedYaw.value / 2;
|
|
356
|
+
const halfPitch = sharedPitch.value / 2;
|
|
357
|
+
const halfRoll = sharedRoll.value / 2;
|
|
358
|
+
const cy_ = Math.cos(halfYaw);
|
|
359
|
+
const sy_ = Math.sin(halfYaw);
|
|
360
|
+
const cp = Math.cos(halfPitch);
|
|
361
|
+
const sp = Math.sin(halfPitch);
|
|
362
|
+
const cr = Math.cos(halfRoll);
|
|
363
|
+
const sr = Math.sin(halfRoll);
|
|
364
|
+
const qx = cy_ * sp * cr + sy_ * cp * sr;
|
|
365
|
+
const qy = sy_ * cp * cr - cy_ * sp * sr;
|
|
366
|
+
const qz = cy_ * cp * sr - sy_ * sp * cr;
|
|
367
|
+
const qw = cy_ * cp * cr + sy_ * sp * sr;
|
|
368
|
+
|
|
369
|
+
// Intrinsics from FoV + actual frame dims.
|
|
370
|
+
// fx = w * (1 / (2 * tan(fovH/2))) (the parenthesised half
|
|
371
|
+
// is the precomputed `sharedFxNumerator` — see M1 fix).
|
|
372
|
+
const w = frame.width;
|
|
373
|
+
const h = frame.height;
|
|
374
|
+
const fx = w * sharedFxNumerator.value;
|
|
375
|
+
const fy = h * sharedFyNumerator.value;
|
|
376
|
+
|
|
377
|
+
plugin.call(frame, {
|
|
378
|
+
tx: 0, ty: 0, tz: 0,
|
|
379
|
+
qx, qy, qz, qw,
|
|
380
|
+
fx, fy,
|
|
381
|
+
cx: w / 2, cy: h / 2,
|
|
382
|
+
imageWidth: w, imageHeight: h,
|
|
383
|
+
timestampMs: 0,
|
|
384
|
+
// 2 == RNSARTrackingState.tracking — we always claim "good
|
|
385
|
+
// tracking" because there's no ARKit signal to differentiate
|
|
386
|
+
// (matches legacy useIncrementalJSDriver semantics).
|
|
387
|
+
trackingStateRaw: 2,
|
|
388
|
+
});
|
|
389
|
+
// Deps array intentionally minimal: only `plugin` actually
|
|
390
|
+
// requires worklet rebuild. All FoV / pose / counter / cadence
|
|
391
|
+
// values flow through stable shared-value refs that Reanimated
|
|
392
|
+
// wires through the producer-thread runtime independently of
|
|
393
|
+
// React's render cycle. (Adversarial-review M1.)
|
|
394
|
+
}, [plugin]);
|
|
395
|
+
|
|
396
|
+
// ── Return handle ───────────────────────────────────────────────
|
|
397
|
+
//
|
|
398
|
+
// Returns a getter for `isRunning` so callers always see the live
|
|
399
|
+
// state (the hook itself doesn't re-render on start/stop — that's
|
|
400
|
+
// intentional, avoids stale-Camera-prop churn).
|
|
401
|
+
return useMemo<FrameProcessorDriverHandle>(() => ({
|
|
402
|
+
start,
|
|
403
|
+
stop,
|
|
404
|
+
frameProcessor: plugin != null ? frameProcessor : null,
|
|
405
|
+
get isRunning() { return isRunningRef.current; },
|
|
406
|
+
}), [start, stop, plugin, frameProcessor]);
|
|
407
|
+
}
|
|
@@ -49,6 +49,12 @@ import {
|
|
|
49
49
|
import type { Subscription } from 'rxjs';
|
|
50
50
|
import type { Camera } from 'react-native-vision-camera';
|
|
51
51
|
|
|
52
|
+
// One-shot deprecation flag — module-scoped so multiple host
|
|
53
|
+
// instances of the hook all share the same gate and we only emit
|
|
54
|
+
// the warning the first time anyone calls .start() in this
|
|
55
|
+
// process.
|
|
56
|
+
let deprecationWarningEmitted = false;
|
|
57
|
+
|
|
52
58
|
|
|
53
59
|
export interface UseIncrementalJSDriverOptions {
|
|
54
60
|
/**
|
|
@@ -159,6 +165,24 @@ export function useIncrementalJSDriver(
|
|
|
159
165
|
// Swift bridge), so the same driver feeds both platforms in
|
|
160
166
|
// non-AR mode.
|
|
161
167
|
if (isRunningRef.current) return;
|
|
168
|
+
// F8.5 — one-shot deprecation warning. v0.5.0 introduced
|
|
169
|
+
// `useFrameProcessorDriver` (vision-camera producer-thread
|
|
170
|
+
// path, native frame rate, no JPEG round-trip). The legacy
|
|
171
|
+
// takeSnapshot path stays available for one minor cycle to
|
|
172
|
+
// give hosts time to migrate, then is removed in v0.6.
|
|
173
|
+
if (!deprecationWarningEmitted) {
|
|
174
|
+
deprecationWarningEmitted = true;
|
|
175
|
+
// eslint-disable-next-line no-console
|
|
176
|
+
console.warn(
|
|
177
|
+
'[react-native-image-stitcher] `useIncrementalJSDriver` '
|
|
178
|
+
+ 'is DEPRECATED as of v0.5.0 and will be REMOVED in '
|
|
179
|
+
+ 'v0.6.0. Migrate to `useFrameProcessorDriver` (or '
|
|
180
|
+
+ 'simply let `<Camera>` use its default driver — no host '
|
|
181
|
+
+ 'code change needed). Opt-out via the `legacyDriver` '
|
|
182
|
+
+ 'prop on `<Camera>` if you need to stay on the legacy '
|
|
183
|
+
+ 'path temporarily.',
|
|
184
|
+
);
|
|
185
|
+
}
|
|
162
186
|
const native = getNativeIncremental();
|
|
163
187
|
if (!native) return;
|
|
164
188
|
|