react-native-image-stitcher 0.10.0 → 0.11.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 +137 -0
- package/dist/camera/Camera.d.ts +30 -14
- package/dist/camera/Camera.js +18 -18
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/stitching/useFrameProcessorDriver.d.ts +50 -95
- package/dist/stitching/useFrameProcessorDriver.js +76 -294
- package/dist/stitching/useStitcherWorklet.d.ts +185 -0
- package/dist/stitching/useStitcherWorklet.js +300 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +58 -1
- package/package.json +2 -1
- package/src/camera/Camera.tsx +48 -32
- package/src/index.ts +13 -0
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +202 -0
- package/src/stitching/useFrameProcessorDriver.ts +79 -320
- package/src/stitching/useStitcherWorklet.ts +415 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* useStitcherWorklet — exposes the lib's first-party stitching as a
|
|
5
|
+
* callable worklet function for host-composed Frame Processors.
|
|
6
|
+
*
|
|
7
|
+
* v0.11.0 — closes the v0.8.0 Phase 5 either-or constraint by letting
|
|
8
|
+
* hosts COMPOSE: write ONE `useFrameProcessor` worklet body that calls
|
|
9
|
+
* BOTH your custom logic AND the lib's first-party stitching, instead
|
|
10
|
+
* of one displacing the other. See `docs/host-app-integration.md`
|
|
11
|
+
* § Tier 3 composition for the pattern.
|
|
12
|
+
*
|
|
13
|
+
* ## Why this is a separate hook
|
|
14
|
+
*
|
|
15
|
+
* vision-camera v4 lets a `<Camera>` mount accept exactly ONE frame
|
|
16
|
+
* processor. Pre-v0.11.0, hosts that passed a `frameProcessor` prop
|
|
17
|
+
* to the lib's `<Camera>` REPLACED the lib's first-party stitching
|
|
18
|
+
* processor in non-AR mode. Composing required hand-writing both
|
|
19
|
+
* worklet bodies in the host's processor. v0.11.0 extracts the
|
|
20
|
+
* lib's worklet body into this hook so hosts can compose with a
|
|
21
|
+
* single call:
|
|
22
|
+
*
|
|
23
|
+
* const stitcher = useStitcherWorklet();
|
|
24
|
+
* const fp = useFrameProcessor((frame) => {
|
|
25
|
+
* 'worklet';
|
|
26
|
+
* hostPreLogic(frame);
|
|
27
|
+
* stitcher.call(frame); // ← lib's first-party stitching
|
|
28
|
+
* hostPostLogic(frame);
|
|
29
|
+
* }, [stitcher.call]);
|
|
30
|
+
* return <Camera frameProcessor={fp} ... />;
|
|
31
|
+
*
|
|
32
|
+
* AR mode is unaffected — the AR-session dispatch path (v0.8.0 Phase
|
|
33
|
+
* 4b.i / 4b.iii) already composes natively.
|
|
34
|
+
*
|
|
35
|
+
* ## What this owns
|
|
36
|
+
*
|
|
37
|
+
* - vc Frame Processor plugin acquisition for
|
|
38
|
+
* `cv_flow_gate_process_frame` (the same plugin the legacy
|
|
39
|
+
* `useFrameProcessorDriver` used; reentrant by construction).
|
|
40
|
+
* - Shared values backing pose (yaw / pitch / roll), throttle
|
|
41
|
+
* counter, every-N gate, and FoV-derived intrinsics scalars.
|
|
42
|
+
* - Gyro subscription on the JS thread (always-on between mount
|
|
43
|
+
* and unmount; subscription cost is tiny).
|
|
44
|
+
* - The worklet body itself: throttle → pose synthesis →
|
|
45
|
+
* `plugin.call(frame, params)`.
|
|
46
|
+
*
|
|
47
|
+
* ## Lifecycle
|
|
48
|
+
*
|
|
49
|
+
* - Gyro auto-subscribes on mount, auto-unsubscribes on unmount.
|
|
50
|
+
* Composed hosts get pose tracking for free.
|
|
51
|
+
* - `reset()` zeros the accumulated yaw / pitch / roll between
|
|
52
|
+
* captures. `useFrameProcessorDriver` calls this on `start()` to
|
|
53
|
+
* preserve pre-v0.11.0 per-capture pose-reset behaviour;
|
|
54
|
+
* composed hosts should call it at the start of each capture too
|
|
55
|
+
* (otherwise pose drifts across captures).
|
|
56
|
+
*
|
|
57
|
+
* ## Behaviour delta from pre-v0.11.0
|
|
58
|
+
*
|
|
59
|
+
* Before: `useFrameProcessorDriver.start()` subscribed the gyro;
|
|
60
|
+
* `stop()` unsubscribed. The subscription was tied to the
|
|
61
|
+
* capture lifecycle.
|
|
62
|
+
*
|
|
63
|
+
* After: the gyro is subscribed for the lifetime of this hook
|
|
64
|
+
* (i.e., as long as the component using it is mounted). In the
|
|
65
|
+
* default `<Camera>` integration the hook mounts when the camera
|
|
66
|
+
* screen mounts, so the practical effect is the same; in
|
|
67
|
+
* custom-composed integrations the host controls mount/unmount
|
|
68
|
+
* by mounting/unmounting the component that calls
|
|
69
|
+
* `useStitcherWorklet`. The battery delta is small: gyroscope
|
|
70
|
+
* sampling at 33ms costs ≪1% CPU on every Android/iOS device
|
|
71
|
+
* the lib supports.
|
|
72
|
+
*
|
|
73
|
+
* `pose reset` semantics are preserved via the new explicit
|
|
74
|
+
* `reset()` method. Hosts that previously relied on `start()`
|
|
75
|
+
* to zero pose now call `stitcher.reset()` at the capture start.
|
|
76
|
+
*
|
|
77
|
+
* ## Pose synthesis (verbatim from `useFrameProcessorDriver`)
|
|
78
|
+
*
|
|
79
|
+
* Quaternion: q = q_yaw * q_pitch * q_roll (Tait-Bryan YPR, body
|
|
80
|
+
* frame). Expanded:
|
|
81
|
+
* qx = cy*sp*cr + sy*cp*sr
|
|
82
|
+
* qy = sy*cp*cr - cy*sp*sr
|
|
83
|
+
* qz = cy*cp*sr - sy*sp*cr
|
|
84
|
+
* qw = cy*cp*cr + sy*sp*sr
|
|
85
|
+
*
|
|
86
|
+
* When roll=0 this collapses to the legacy 2-axis form so captures
|
|
87
|
+
* held level produce bit-identical poses to the pre-v0.6 driver
|
|
88
|
+
* (and bit-identical to v0.10.x's `useFrameProcessorDriver`).
|
|
89
|
+
*
|
|
90
|
+
* ## Throttling (verbatim)
|
|
91
|
+
*
|
|
92
|
+
* `evalEveryNFrames` controls how often the worklet calls the
|
|
93
|
+
* plugin. Default 1. Independent of — and stacks on top of —
|
|
94
|
+
* the stitcher's own internal `flowEvalEveryNFrames` in
|
|
95
|
+
* `KeyframeGate.swift`; effective cadence is the product.
|
|
96
|
+
*
|
|
97
|
+
* ## Pairing with `IncrementalStitcher.start`
|
|
98
|
+
*
|
|
99
|
+
* The plugin's per-frame call into `consumeFrameFromPlugin` is
|
|
100
|
+
* gated by `IncrementalStitcher.frameProcessorIngestEnabled`,
|
|
101
|
+
* which is TRUE only when the stitcher was started with
|
|
102
|
+
* `frameSourceMode === 'frameProcessor'`. Hosts MUST call
|
|
103
|
+
* `incrementalStitcher.start({ frameSourceMode: 'frameProcessor',
|
|
104
|
+
* ... })` to actually get frames into the engine — otherwise the
|
|
105
|
+
* worklet runs to completion but the wrapper drops the call.
|
|
106
|
+
* `Camera.tsx` does this wiring automatically when the host opts
|
|
107
|
+
* into the lib's `useFrameProcessorDriver`. Hosts that compose
|
|
108
|
+
* their own worklet via this hook must do the wiring themselves.
|
|
109
|
+
*/
|
|
110
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
111
|
+
exports.useStitcherWorklet = useStitcherWorklet;
|
|
112
|
+
const react_1 = require("react");
|
|
113
|
+
const react_native_sensors_1 = require("react-native-sensors");
|
|
114
|
+
// Reanimated's `useSharedValue` is the documented vision-camera
|
|
115
|
+
// idiom, but it's a heavy peer dep. `react-native-worklets-core`
|
|
116
|
+
// (already a transitive dep via vision-camera v4 on RN 0.84) exposes
|
|
117
|
+
// the same API surface (a `value` getter/setter readable from
|
|
118
|
+
// worklets and the JS thread) and is sufficient for our use.
|
|
119
|
+
const react_native_worklets_core_1 = require("react-native-worklets-core");
|
|
120
|
+
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
121
|
+
function useStitcherWorklet(options = {}) {
|
|
122
|
+
const { gyroIntervalMs = 33, fovHorizDegrees = 65, fovVertDegrees = 50, evalEveryNFrames = 1, } = options;
|
|
123
|
+
// ── Plugin acquisition ──────────────────────────────────────────
|
|
124
|
+
//
|
|
125
|
+
// `initFrameProcessorPlugin` can return `undefined` if called
|
|
126
|
+
// before vision-camera's plugin registry has finished initialising
|
|
127
|
+
// (race observed in F8.1.a). Mount-once useEffect with a 16ms
|
|
128
|
+
// retry until success. Verbatim from `useFrameProcessorDriver`.
|
|
129
|
+
const [plugin, setPlugin] = (0, react_1.useState)(null);
|
|
130
|
+
(0, react_1.useEffect)(() => {
|
|
131
|
+
let cancelled = false;
|
|
132
|
+
let timerId = null;
|
|
133
|
+
const tryAcquire = () => {
|
|
134
|
+
if (cancelled)
|
|
135
|
+
return;
|
|
136
|
+
const p = react_native_vision_camera_1.VisionCameraProxy.initFrameProcessorPlugin('cv_flow_gate_process_frame', {});
|
|
137
|
+
if (p != null) {
|
|
138
|
+
setPlugin(p);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
timerId = setTimeout(tryAcquire, 16);
|
|
142
|
+
};
|
|
143
|
+
tryAcquire();
|
|
144
|
+
return () => {
|
|
145
|
+
cancelled = true;
|
|
146
|
+
if (timerId != null)
|
|
147
|
+
clearTimeout(timerId);
|
|
148
|
+
};
|
|
149
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
|
+
}, []);
|
|
151
|
+
// ── Shared values (worklet ↔ JS thread) ─────────────────────────
|
|
152
|
+
const sharedYaw = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
153
|
+
const sharedPitch = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
154
|
+
const sharedRoll = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
155
|
+
const sharedFrameCounter = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
156
|
+
const sharedEvalEveryN = (0, react_native_worklets_core_1.useSharedValue)(Math.max(1, evalEveryNFrames));
|
|
157
|
+
const sharedFxNumerator = (0, react_native_worklets_core_1.useSharedValue)(1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2)));
|
|
158
|
+
const sharedFyNumerator = (0, react_native_worklets_core_1.useSharedValue)(1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2)));
|
|
159
|
+
// Prop-derived shared values stay in sync via cheap effects.
|
|
160
|
+
(0, react_1.useEffect)(() => {
|
|
161
|
+
sharedEvalEveryN.value = Math.max(1, evalEveryNFrames);
|
|
162
|
+
}, [evalEveryNFrames, sharedEvalEveryN]);
|
|
163
|
+
(0, react_1.useEffect)(() => {
|
|
164
|
+
sharedFxNumerator.value =
|
|
165
|
+
1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2));
|
|
166
|
+
}, [fovHorizDegrees, sharedFxNumerator]);
|
|
167
|
+
(0, react_1.useEffect)(() => {
|
|
168
|
+
sharedFyNumerator.value =
|
|
169
|
+
1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2));
|
|
170
|
+
}, [fovVertDegrees, sharedFyNumerator]);
|
|
171
|
+
// ── Gyro subscription (always-on while mounted) ─────────────────
|
|
172
|
+
//
|
|
173
|
+
// v0.11.0 — moved here from `useFrameProcessorDriver.start()`.
|
|
174
|
+
// The composition pattern needs gyro running whenever
|
|
175
|
+
// `useStitcherWorklet` is in use; gating the subscription on a
|
|
176
|
+
// separate start/stop pair would force every composed host to
|
|
177
|
+
// wire its own lifecycle. Cost is tiny: ≪1% CPU at 33ms
|
|
178
|
+
// sampling. See module header "Behaviour delta from pre-v0.11.0".
|
|
179
|
+
(0, react_1.useEffect)(() => {
|
|
180
|
+
let lastGyroAt = null;
|
|
181
|
+
(0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, gyroIntervalMs);
|
|
182
|
+
const sub = react_native_sensors_1.gyroscope.subscribe({
|
|
183
|
+
next: ({ x, y, z }) => {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
if (lastGyroAt === null) {
|
|
186
|
+
lastGyroAt = now;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const dt = (now - lastGyroAt) / 1000.0;
|
|
190
|
+
lastGyroAt = now;
|
|
191
|
+
sharedYaw.value += y * dt;
|
|
192
|
+
sharedPitch.value += x * dt;
|
|
193
|
+
sharedRoll.value += z * dt;
|
|
194
|
+
},
|
|
195
|
+
error: (err) => {
|
|
196
|
+
// eslint-disable-next-line no-console
|
|
197
|
+
console.warn('[useStitcherWorklet] gyro error', err);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
return () => {
|
|
201
|
+
sub.unsubscribe();
|
|
202
|
+
};
|
|
203
|
+
}, [gyroIntervalMs, sharedYaw, sharedPitch, sharedRoll]);
|
|
204
|
+
// ── Explicit reset (for per-capture pose zero-ing) ──────────────
|
|
205
|
+
const reset = (0, react_1.useCallback)(() => {
|
|
206
|
+
sharedYaw.value = 0;
|
|
207
|
+
sharedPitch.value = 0;
|
|
208
|
+
sharedRoll.value = 0;
|
|
209
|
+
sharedFrameCounter.value = 0;
|
|
210
|
+
}, [sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
|
|
211
|
+
// ── Worklet body ────────────────────────────────────────────────
|
|
212
|
+
//
|
|
213
|
+
// Returned as `handle.call`. Re-created when `plugin` changes
|
|
214
|
+
// (which happens at most once at acquire time); deps array on the
|
|
215
|
+
// useCallback ensures consumers' `useFrameProcessor([handle.call])`
|
|
216
|
+
// re-binds when the worklet identity changes.
|
|
217
|
+
//
|
|
218
|
+
// The `'worklet'` directive marks this function for the
|
|
219
|
+
// worklets-core transformer so it can be serialised into the
|
|
220
|
+
// producer-thread runtime; that's the contract that lets a host
|
|
221
|
+
// `useFrameProcessor` worklet body call it without a thread hop.
|
|
222
|
+
const call = (0, react_1.useCallback)((frame) => {
|
|
223
|
+
'worklet';
|
|
224
|
+
if (plugin == null)
|
|
225
|
+
return;
|
|
226
|
+
// v0.11.1 — AR-source frames are stitched natively by the AR-
|
|
227
|
+
// side dispatcher (`RNSARSession.swift:510-511` → the first-
|
|
228
|
+
// party callback installed in `RNSARWorkletRuntime`). Calling
|
|
229
|
+
// the vc Frame Processor plugin here would throw
|
|
230
|
+
// `getPropertyAsObject: property '__frame' is undefined`
|
|
231
|
+
// because AR frames are `StitcherFrameHostObject` instances
|
|
232
|
+
// and don't carry the vc `Frame` proxy's JSI marker. The
|
|
233
|
+
// throw is caught silently by the per-worklet error handler
|
|
234
|
+
// (`RNSARWorkletRuntime.mm:284-301`) and bubbles up only to
|
|
235
|
+
// `os_log` — invisible to JS, which is why pre-v0.11.1
|
|
236
|
+
// composed hosts saw their post-`stitcher.call` lines
|
|
237
|
+
// (`fireFrameProcessorLog`, `runOnJS` callbacks) silently
|
|
238
|
+
// never execute in AR mode. Silent no-op here matches the
|
|
239
|
+
// module-header promise that AR mode is "unaffected" by this
|
|
240
|
+
// hook (the AR-side stitching path runs natively, independent
|
|
241
|
+
// of the composed worklet body).
|
|
242
|
+
//
|
|
243
|
+
// The `(frame as StitcherFrame).source` cast is safe: vc
|
|
244
|
+
// `Frame` doesn't carry a `source` property so the check
|
|
245
|
+
// returns `undefined !== 'ar'` → `true`, and the worklet
|
|
246
|
+
// proceeds normally. Only frames that explicitly tag
|
|
247
|
+
// themselves as AR-source (which our native AR dispatcher
|
|
248
|
+
// does — see `StitcherFrameHostObject.mm`) get short-circuited.
|
|
249
|
+
if (frame.source === 'ar')
|
|
250
|
+
return;
|
|
251
|
+
// Throttle (verbatim from useFrameProcessorDriver).
|
|
252
|
+
sharedFrameCounter.value += 1;
|
|
253
|
+
const N = sharedEvalEveryN.value;
|
|
254
|
+
if (N > 1 && (sharedFrameCounter.value % N) !== 0)
|
|
255
|
+
return;
|
|
256
|
+
// Pose synthesis (verbatim from useFrameProcessorDriver).
|
|
257
|
+
const halfYaw = sharedYaw.value / 2;
|
|
258
|
+
const halfPitch = sharedPitch.value / 2;
|
|
259
|
+
const halfRoll = sharedRoll.value / 2;
|
|
260
|
+
const cy_ = Math.cos(halfYaw);
|
|
261
|
+
const sy_ = Math.sin(halfYaw);
|
|
262
|
+
const cp = Math.cos(halfPitch);
|
|
263
|
+
const sp = Math.sin(halfPitch);
|
|
264
|
+
const cr = Math.cos(halfRoll);
|
|
265
|
+
const sr = Math.sin(halfRoll);
|
|
266
|
+
const qx = cy_ * sp * cr + sy_ * cp * sr;
|
|
267
|
+
const qy = sy_ * cp * cr - cy_ * sp * sr;
|
|
268
|
+
const qz = cy_ * cp * sr - sy_ * sp * cr;
|
|
269
|
+
const qw = cy_ * cp * cr + sy_ * sp * sr;
|
|
270
|
+
// Intrinsics from FoV + actual frame dims.
|
|
271
|
+
const w = frame.width;
|
|
272
|
+
const h = frame.height;
|
|
273
|
+
const fx = w * sharedFxNumerator.value;
|
|
274
|
+
const fy = h * sharedFyNumerator.value;
|
|
275
|
+
// vc's `plugin.call` is typed against vc's `Frame`. The worklet
|
|
276
|
+
// accepts the union (`Frame | StitcherFrame`); cast through
|
|
277
|
+
// `unknown` because the union doesn't satisfy vc's interface
|
|
278
|
+
// even though structurally both members do.
|
|
279
|
+
plugin.call(frame, {
|
|
280
|
+
tx: 0, ty: 0, tz: 0,
|
|
281
|
+
qx, qy, qz, qw,
|
|
282
|
+
fx, fy,
|
|
283
|
+
cx: w / 2, cy: h / 2,
|
|
284
|
+
imageWidth: w, imageHeight: h,
|
|
285
|
+
timestampMs: 0,
|
|
286
|
+
trackingStateRaw: 2, // RNSARTrackingState.tracking (no AR signal in non-AR mode)
|
|
287
|
+
});
|
|
288
|
+
}, [
|
|
289
|
+
plugin,
|
|
290
|
+
sharedFrameCounter,
|
|
291
|
+
sharedEvalEveryN,
|
|
292
|
+
sharedYaw,
|
|
293
|
+
sharedPitch,
|
|
294
|
+
sharedRoll,
|
|
295
|
+
sharedFxNumerator,
|
|
296
|
+
sharedFyNumerator,
|
|
297
|
+
]);
|
|
298
|
+
return { call, reset, isReady: plugin != null };
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=useStitcherWorklet.js.map
|
|
@@ -22,11 +22,37 @@
|
|
|
22
22
|
#import <React/RCTBridge.h>
|
|
23
23
|
#import <React/RCTBridge+Private.h>
|
|
24
24
|
#import <React/RCTUtils.h>
|
|
25
|
+
#import <ReactCommon/CallInvoker.h>
|
|
26
|
+
// `RCTCxxBridge` (and its bridgeless-mode `RCTBridgeProxy` forwarder)
|
|
27
|
+
// exposes `-jsCallInvoker` returning `std::shared_ptr<CallInvoker>`,
|
|
28
|
+
// but the property declaration lives in `<ReactCommon/RCTTurboModule.h>`
|
|
29
|
+
// which isn't on our pod's HEADER_SEARCH_PATHS (worklets-core gets it
|
|
30
|
+
// via its own ReactCommon dep). Rather than enlarging our pod's
|
|
31
|
+
// dependency surface, forward-declare the property in an anonymous
|
|
32
|
+
// category — the runtime dispatches to RN's actual implementation.
|
|
33
|
+
// Pattern matches `WKTJsiWorkletContext.cpp`'s approach to keep the
|
|
34
|
+
// pod self-contained.
|
|
35
|
+
@interface RCTCxxBridge ()
|
|
36
|
+
@property (nonatomic, readonly) std::shared_ptr<facebook::react::CallInvoker> jsCallInvoker;
|
|
37
|
+
@end
|
|
25
38
|
#import <os/log.h>
|
|
26
39
|
|
|
27
40
|
#include <jsi/jsi.h>
|
|
28
41
|
|
|
29
42
|
#include "stitcher_proxy_jsi.hpp"
|
|
43
|
+
// v0.11.1 — worklets-core JsiWorkletContext. We initialize the
|
|
44
|
+
// SINGLETON default instance here so that other contexts in this
|
|
45
|
+
// library that use the 2-arg `JsiWorkletContext(name, workletInvoker)`
|
|
46
|
+
// constructor inherit a working `_jsCallInvoker` (and thus their
|
|
47
|
+
// `runOnJS` / `Worklets.createRunOnJS` callbacks actually route back
|
|
48
|
+
// to the main JS thread). Specifically: `RNSARWorkletRuntime`'s AR-
|
|
49
|
+
// side worklet context (see `RNSARWorkletRuntime.mm:155`) uses the
|
|
50
|
+
// 2-arg ctor; pre-v0.11.1 that left its inherited `_jsCallInvoker`
|
|
51
|
+
// nullptr, and `invokeOnJsThread` silently no-op'd (see
|
|
52
|
+
// `WKTJsiWorkletContext.cpp:124-131`). Test 2 of the v0.11.0
|
|
53
|
+
// manual-verification checklist surfaced this as "AR-mode host
|
|
54
|
+
// worklets register but their runOnJS callbacks never fire."
|
|
55
|
+
#include "WKTJsiWorkletContext.h"
|
|
30
56
|
|
|
31
57
|
using namespace facebook;
|
|
32
58
|
|
|
@@ -94,9 +120,40 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
|
|
|
94
120
|
jsi::Runtime& runtime = *(jsi::Runtime*)cxxBridge.runtime;
|
|
95
121
|
retailens::installStitcherProxy(runtime);
|
|
96
122
|
|
|
123
|
+
// v0.11.1 — initialize the singleton default JsiWorkletContext so
|
|
124
|
+
// that downstream 2-arg ctors (RNSARWorkletRuntime) inherit a
|
|
125
|
+
// working `_jsCallInvoker`. Without this, AR-mode host worklets'
|
|
126
|
+
// `runOnJS` / `Worklets.createRunOnJS` callbacks silently no-op
|
|
127
|
+
// (`WKTJsiWorkletContext.cpp:124-131` early-returns when
|
|
128
|
+
// `_jsCallInvoker == nullptr`). See file-top comment for the full
|
|
129
|
+
// diagnosis (Test 2 of v0.11.0 manual-verification checklist).
|
|
130
|
+
//
|
|
131
|
+
// Idempotent at the worklets-core level: re-initialization is
|
|
132
|
+
// tolerated; the default instance is a process-scope singleton
|
|
133
|
+
// and we're called once per JS-runtime bootstrap. In bridgeless
|
|
134
|
+
// mode `cxxBridge.jsCallInvoker` is forwarded via RCTBridgeProxy
|
|
135
|
+
// to the underlying RCTHost's `CallInvoker` (same forwarding
|
|
136
|
+
// pattern as `cxxBridge.runtime` above).
|
|
137
|
+
auto jsCallInvoker = cxxBridge.jsCallInvoker;
|
|
138
|
+
if (jsCallInvoker == nullptr) {
|
|
139
|
+
os_log_error(OS_LOG_DEFAULT,
|
|
140
|
+
"[StitcherJsiInstaller] cxxBridge.jsCallInvoker is nullptr; "
|
|
141
|
+
"AR-mode host worklets' runOnJS will not fire. Proxy installed "
|
|
142
|
+
"but worklet-bridging is impaired.");
|
|
143
|
+
// Proxy is still installed; only the runOnJS path is impaired.
|
|
144
|
+
// Return @YES so JS callers don't fall back to the JS-side registry.
|
|
145
|
+
return @YES;
|
|
146
|
+
}
|
|
147
|
+
auto jsInvokerAdapter =
|
|
148
|
+
[jsCallInvoker](std::function<void()>&& fp) {
|
|
149
|
+
jsCallInvoker->invokeAsync(std::move(fp));
|
|
150
|
+
};
|
|
151
|
+
RNWorklet::JsiWorkletContext::getDefaultInstance()->initialize(
|
|
152
|
+
"stitcher.default", &runtime, jsInvokerAdapter);
|
|
153
|
+
|
|
97
154
|
os_log_info(OS_LOG_DEFAULT,
|
|
98
155
|
"[StitcherJsiInstaller] installed globalThis.__stitcherProxy "
|
|
99
|
-
"on main JS runtime.");
|
|
156
|
+
"AND initialized default JsiWorkletContext on main JS runtime.");
|
|
100
157
|
return @YES;
|
|
101
158
|
}
|
|
102
159
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-image-stitcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"homepage": "https://github.com/bhargavkanda/react-native-image-stitcher#readme",
|
|
55
55
|
"devDependencies": {
|
|
56
|
+
"@react-native-community/cli": "20.1.0",
|
|
56
57
|
"@types/jest": "^29.5.0",
|
|
57
58
|
"@types/react": "^19.0.0",
|
|
58
59
|
"jest": "^29.7.0",
|
package/src/camera/Camera.tsx
CHANGED
|
@@ -323,26 +323,42 @@ export interface CameraProps {
|
|
|
323
323
|
* }
|
|
324
324
|
* ```
|
|
325
325
|
*
|
|
326
|
-
* ## Non-AR mode
|
|
326
|
+
* ## Non-AR mode composition (v0.11.0+)
|
|
327
327
|
*
|
|
328
328
|
* vision-camera's `<Camera>` accepts ONLY ONE frame processor.
|
|
329
329
|
* The lib's internal `useFrameProcessorDriver` produces the
|
|
330
330
|
* processor that drives first-party panorama stitching in non-AR
|
|
331
|
-
* mode. If you supply your own via this prop,
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
331
|
+
* mode. If you supply your own via this prop, the lib's
|
|
332
|
+
* default processor is REPLACED — but as of v0.11.0 you can
|
|
333
|
+
* COMPOSE first-party stitching back into your worklet body
|
|
334
|
+
* using `useStitcherWorklet`:
|
|
335
335
|
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
336
|
+
* ```tsx
|
|
337
|
+
* import {
|
|
338
|
+
* Camera, useFrameProcessor, useStitcherWorklet,
|
|
339
|
+
* type StitcherFrame,
|
|
340
|
+
* } from 'react-native-image-stitcher';
|
|
341
341
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
342
|
+
* function MyScreen() {
|
|
343
|
+
* const stitcher = useStitcherWorklet();
|
|
344
|
+
* const fp = useFrameProcessor((frame: StitcherFrame) => {
|
|
345
|
+
* 'worklet';
|
|
346
|
+
* hostPreLogic(frame);
|
|
347
|
+
* stitcher.call(frame); // ← first-party stitching
|
|
348
|
+
* hostPostLogic(frame);
|
|
349
|
+
* }, [stitcher.call]);
|
|
350
|
+
* return <Camera frameProcessor={fp} ... />;
|
|
351
|
+
* }
|
|
352
|
+
* ```
|
|
353
|
+
*
|
|
354
|
+
* Hosts that DON'T call `useStitcherWorklet` from their worklet
|
|
355
|
+
* body replace first-party stitching for non-AR captures (a
|
|
356
|
+
* one-shot console.info documents this when the prop is first
|
|
357
|
+
* supplied). AR mode is unaffected either way — the AR-mode
|
|
358
|
+
* dispatch path (v0.8.0 Phase 4b.i / 4b.iii) natively fans out
|
|
359
|
+
* to both the lib's first-party stitching AND every registered
|
|
360
|
+
* host worklet on every frame, with per-worklet failure
|
|
361
|
+
* isolation.
|
|
346
362
|
*
|
|
347
363
|
* ## AR mode behaviour
|
|
348
364
|
*
|
|
@@ -841,24 +857,23 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
841
857
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
842
858
|
useEffect(() => () => { fpDriver.stop(); }, []);
|
|
843
859
|
|
|
844
|
-
// v0.8.0 Phase 5 — frameProcessor prop semantics:
|
|
860
|
+
// v0.8.0 Phase 5 / v0.11.0 — frameProcessor prop semantics:
|
|
845
861
|
//
|
|
846
|
-
// - Host supplied? → use host's processor
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
//
|
|
862
|
+
// - Host supplied? → use host's processor. The host's worklet
|
|
863
|
+
// body controls whether first-party stitching also fires:
|
|
864
|
+
// call `stitcher.call(frame)` (from `useStitcherWorklet`)
|
|
865
|
+
// inside the body to compose; omit to replace. One-shot
|
|
866
|
+
// console.info documents the choice so the host can spot a
|
|
867
|
+
// missing `useStitcherWorklet` call before they go hunting
|
|
868
|
+
// for "why is non-AR panorama capture not producing output".
|
|
869
|
+
// AR-mode capture is unaffected either way — the AR-session
|
|
870
|
+
// dispatch path fans out to BOTH first-party stitching AND
|
|
871
|
+
// every host worklet independently.
|
|
853
872
|
//
|
|
854
873
|
// - No host processor? → use `fpDriver.frameProcessor` which is
|
|
855
874
|
// the lib's internal worklet driving first-party stitching
|
|
856
875
|
// via `useFrameProcessorDriver`. Default behaviour for the
|
|
857
876
|
// common "I just want panorama capture" case.
|
|
858
|
-
//
|
|
859
|
-
// The pre-v0.8.0 behaviour (host's prop silently ignored with
|
|
860
|
-
// a warning) is gone — Phase 5 plumbs the prop through. The
|
|
861
|
-
// tradeoff is honestly documented in the CameraProps docstring.
|
|
862
877
|
const hostFrameProcessorAcceptedWarnedRef = useRef(false);
|
|
863
878
|
if (
|
|
864
879
|
hostFrameProcessor != null
|
|
@@ -868,12 +883,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
868
883
|
// eslint-disable-next-line no-console
|
|
869
884
|
console.info(
|
|
870
885
|
'[react-native-image-stitcher] Host frameProcessor supplied — '
|
|
871
|
-
+ 'non-AR mode will run YOUR worklet
|
|
872
|
-
+ 'first-party stitching
|
|
873
|
-
+ '
|
|
874
|
-
+ '
|
|
875
|
-
+ '
|
|
876
|
-
+ '
|
|
886
|
+
+ 'non-AR mode will run YOUR composed worklet. If you want '
|
|
887
|
+
+ 'first-party panorama stitching alongside your own logic, '
|
|
888
|
+
+ 'call `useStitcherWorklet()` and invoke `stitcher.call(frame)` '
|
|
889
|
+
+ 'from your worklet body (see `<Camera>` `frameProcessor` '
|
|
890
|
+
+ 'JSDoc for the composition pattern). AR-mode capture is '
|
|
891
|
+
+ 'unaffected (AR-session dispatch fans out to both '
|
|
892
|
+
+ 'first-party and host worklets independently).',
|
|
877
893
|
);
|
|
878
894
|
}
|
|
879
895
|
// The Frame Processor worklet bound to vision-camera's Camera.
|
package/src/index.ts
CHANGED
|
@@ -226,6 +226,19 @@ export type {
|
|
|
226
226
|
FrameProcessorDriverHandle,
|
|
227
227
|
} from './stitching/useFrameProcessorDriver';
|
|
228
228
|
|
|
229
|
+
// v0.11.0 — composable first-party stitching as a worklet function.
|
|
230
|
+
// Hosts that want to COMPOSE their own per-frame logic with the
|
|
231
|
+
// lib's stitching (instead of REPLACING it via the <Camera>
|
|
232
|
+
// `frameProcessor` prop) call this hook + invoke `stitcher.call`
|
|
233
|
+
// inside their own `useFrameProcessor` body. See
|
|
234
|
+
// `docs/host-app-integration.md` § Tier 3 for the full pattern.
|
|
235
|
+
export { useStitcherWorklet } from './stitching/useStitcherWorklet';
|
|
236
|
+
export type {
|
|
237
|
+
UseStitcherWorkletOptions,
|
|
238
|
+
StitcherWorkletHandle,
|
|
239
|
+
StitcherWorkletInput,
|
|
240
|
+
} from './stitching/useStitcherWorklet';
|
|
241
|
+
|
|
229
242
|
// ── Batch stitching ───────────────────────────────────────────────────
|
|
230
243
|
// Feed a video file straight to OpenCV's cv::Stitcher, bypassing the
|
|
231
244
|
// incremental pipeline. Useful when you have content captured
|