react-native-image-stitcher 0.11.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
CHANGED
|
@@ -16,6 +16,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.11.1] — 2026-05-28
|
|
20
|
+
|
|
21
|
+
### Fixed — AR-mode composed worklets silently throw
|
|
22
|
+
|
|
23
|
+
`useStitcherWorklet`'s `call(frame)` was invoking the vision-camera
|
|
24
|
+
Frame Processor plugin on every frame regardless of mode. In AR
|
|
25
|
+
mode the frame is a `StitcherFrameHostObject` (no `__frame` JSI
|
|
26
|
+
marker), so the vc plugin threw `getPropertyAsObject: property
|
|
27
|
+
'__frame' is undefined`. The throw was caught silently by
|
|
28
|
+
`RNSARWorkletRuntime`'s per-worklet error isolation (logged to
|
|
29
|
+
`os_log`, not surfaced to JS), causing any host code AFTER
|
|
30
|
+
`stitcher.call(frame)` in the composed worklet body —
|
|
31
|
+
`runOnJS` callbacks, `Worklets.createRunOnJS` dispatches, further
|
|
32
|
+
host worklet logic — to silently never execute in AR mode.
|
|
33
|
+
|
|
34
|
+
The hook's module docstring already promised AR mode would no-op
|
|
35
|
+
("AR mode is unaffected — the AR-session dispatch path already
|
|
36
|
+
composes natively"), but the code didn't enforce it. v0.11.1 adds
|
|
37
|
+
an early-return on `frame.source === 'ar'` in `useStitcherWorklet`'s
|
|
38
|
+
worklet body. AR stitching continues to run natively via
|
|
39
|
+
`RNSARSession.swift`'s first-party callback path
|
|
40
|
+
(`consumer.consumeFrame(arFrame, pose)` at line 510-511), which is
|
|
41
|
+
the architectural contract for AR-mode stitching since v0.8.0.
|
|
42
|
+
|
|
43
|
+
This bug was latent in v0.11.0 — surfaced by Test 2 of
|
|
44
|
+
`docs/v0.11.0-manual-verification-checklist.md` on Ram's iPhone.
|
|
45
|
+
|
|
46
|
+
Also added: `StitcherJsiInstaller::install` now eagerly initializes
|
|
47
|
+
the worklets-core default `JsiWorkletContext` singleton during JSI
|
|
48
|
+
bootstrap. This is defense-in-depth — worklets-core's own `Worklets`
|
|
49
|
+
module also initializes the default, but eager init from our
|
|
50
|
+
installer makes `runOnJS` from AR-mode worklets robust to host-app
|
|
51
|
+
import order (no dependency on worklets-core's `Worklets` module
|
|
52
|
+
loading before our AR runtime constructs its context).
|
|
53
|
+
|
|
54
|
+
### Added — Jest test for AR-source short-circuit
|
|
55
|
+
|
|
56
|
+
New test file `src/stitching/__tests__/useStitcherWorklet.test.ts`
|
|
57
|
+
pins the AR no-op contract. 5 new tests; full suite now 74/74 pass
|
|
58
|
+
(was 69/69 in v0.11.0).
|
|
59
|
+
|
|
19
60
|
## [0.11.0] — 2026-05-28
|
|
20
61
|
|
|
21
62
|
### Added — `useStitcherWorklet` for non-AR composition
|
|
@@ -223,6 +223,31 @@ function useStitcherWorklet(options = {}) {
|
|
|
223
223
|
'worklet';
|
|
224
224
|
if (plugin == null)
|
|
225
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;
|
|
226
251
|
// Throttle (verbatim from useFrameProcessorDriver).
|
|
227
252
|
sharedFrameCounter.value += 1;
|
|
228
253
|
const N = sharedEvalEveryN.value;
|
|
@@ -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.11.
|
|
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",
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for `useStitcherWorklet`.
|
|
4
|
+
*
|
|
5
|
+
* Coverage focus (v0.11.1):
|
|
6
|
+
*
|
|
7
|
+
* - **AR-source short-circuit.** The hook's docstring promises
|
|
8
|
+
* that AR-mode hosts can call `stitcher.call(frame)` from a
|
|
9
|
+
* single composed worklet body without per-mode branching; AR
|
|
10
|
+
* stitching runs natively via the AR-side dispatcher. Pre-
|
|
11
|
+
* v0.11.1 the code didn't enforce that — `stitcher.call` would
|
|
12
|
+
* invoke the vc Frame Processor plugin even on AR-source
|
|
13
|
+
* frames, which throws `getPropertyAsObject: property '__frame'
|
|
14
|
+
* is undefined` because AR frames are `StitcherFrameHostObject`
|
|
15
|
+
* instances and don't carry vc's JSI `Frame` proxy marker. The
|
|
16
|
+
* throw was caught silently by the per-worklet error handler in
|
|
17
|
+
* `RNSARWorkletRuntime.mm`, surfacing only as an `os_log` entry
|
|
18
|
+
* — invisible to JS, which is why composed hosts saw their
|
|
19
|
+
* post-`stitcher.call` lines (`fireFrameProcessorLog`,
|
|
20
|
+
* `runOnJS` callbacks) silently never execute in AR mode. Test
|
|
21
|
+
* 2 of `docs/v0.11.0-manual-verification-checklist.md`
|
|
22
|
+
* reproduced this on Ram's iPhone. This test pins the fix.
|
|
23
|
+
*
|
|
24
|
+
* - **vc-source happy path.** vc-source frames (and frames whose
|
|
25
|
+
* `source` is `undefined` — which is what vc's raw `Frame`
|
|
26
|
+
* looks like; the lib doesn't wrap vc frames in Phase 4a) MUST
|
|
27
|
+
* still invoke the plugin.
|
|
28
|
+
*
|
|
29
|
+
* ## Why mock React's hooks directly
|
|
30
|
+
*
|
|
31
|
+
* The hook owns state via `useState` (the JSI plugin handle) and
|
|
32
|
+
* side effects via `useEffect` (plugin acquisition retry loop + gyro
|
|
33
|
+
* subscription). The existing test pattern in this directory (see
|
|
34
|
+
* `useThrottledFrameProcessor.test.ts`) doesn't use a React renderer
|
|
35
|
+
* — instead it mocks the hooks the SUT calls so the SUT can be
|
|
36
|
+
* executed as a plain function. Same approach here: we mock
|
|
37
|
+
* `useState` to return a pre-resolved plugin, `useCallback` to
|
|
38
|
+
* return the function as-is, `useEffect` as a no-op (we don't need
|
|
39
|
+
* the plugin-acquisition retry or gyro for the call-routing test).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { StitcherFrame } from '../StitcherFrame';
|
|
43
|
+
|
|
44
|
+
// ─── Mock vision-camera ──────────────────────────────────────────
|
|
45
|
+
const pluginCallSpy = jest.fn();
|
|
46
|
+
const fakePlugin = { call: pluginCallSpy } as unknown as object;
|
|
47
|
+
|
|
48
|
+
jest.mock('react-native-vision-camera', () => ({
|
|
49
|
+
VisionCameraProxy: {
|
|
50
|
+
initFrameProcessorPlugin: jest.fn(() => fakePlugin),
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// ─── Mock react-native-worklets-core ─────────────────────────────
|
|
55
|
+
jest.mock('react-native-worklets-core', () => ({
|
|
56
|
+
useSharedValue: (initial: number) => ({ value: initial }),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
// ─── Mock react-native-sensors ───────────────────────────────────
|
|
60
|
+
jest.mock('react-native-sensors', () => ({
|
|
61
|
+
gyroscope: { subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })) },
|
|
62
|
+
setUpdateIntervalForType: jest.fn(),
|
|
63
|
+
SensorTypes: { gyroscope: 'gyroscope' },
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// ─── Mock React's hooks so the SUT runs as a plain function ──────
|
|
67
|
+
//
|
|
68
|
+
// `useState` returns the plugin pre-resolved. `useCallback` returns
|
|
69
|
+
// the function identity (deps array ignored — we're not testing
|
|
70
|
+
// re-render semantics). `useEffect` is a no-op (no plugin retry,
|
|
71
|
+
// no gyro subscription). This lets us call the hook synchronously
|
|
72
|
+
// and exercise the worklet body via the returned `call` function.
|
|
73
|
+
jest.mock('react', () => {
|
|
74
|
+
const actual = jest.requireActual('react');
|
|
75
|
+
return {
|
|
76
|
+
...actual,
|
|
77
|
+
useState: <T,>(initial: T): [T, (next: T) => void] => {
|
|
78
|
+
// For the [plugin, setPlugin] tuple: return the fake plugin
|
|
79
|
+
// immediately rather than starting at `null`. This skips the
|
|
80
|
+
// plugin-acquisition retry path and lets `call` actually
|
|
81
|
+
// invoke `plugin.call(...)`.
|
|
82
|
+
const resolved = (initial === null ? fakePlugin : initial) as T;
|
|
83
|
+
return [resolved, () => {}];
|
|
84
|
+
},
|
|
85
|
+
useEffect: () => {},
|
|
86
|
+
useCallback: <T,>(fn: T): T => fn,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// SUT — imported AFTER mocks so the hook sees them.
|
|
91
|
+
// eslint-disable-next-line import/first
|
|
92
|
+
import { useStitcherWorklet } from '../useStitcherWorklet';
|
|
93
|
+
|
|
94
|
+
describe('useStitcherWorklet', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
pluginCallSpy.mockReset();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('AR-source short-circuit (v0.11.1 fix)', () => {
|
|
100
|
+
it('does NOT invoke the vc plugin for AR-source frames', () => {
|
|
101
|
+
const { call } = useStitcherWorklet();
|
|
102
|
+
const arFrame: StitcherFrame = {
|
|
103
|
+
width: 1920,
|
|
104
|
+
height: 1080,
|
|
105
|
+
pixelFormat: 'yuv',
|
|
106
|
+
orientation: 'landscape-right',
|
|
107
|
+
timestamp: 0,
|
|
108
|
+
toArrayBuffer: () => new ArrayBuffer(0),
|
|
109
|
+
source: 'ar',
|
|
110
|
+
pose: { rotation: [0, 0, 0, 1] },
|
|
111
|
+
};
|
|
112
|
+
call(arFrame);
|
|
113
|
+
expect(pluginCallSpy).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does NOT invoke the vc plugin for AR-source frames even when called repeatedly', () => {
|
|
117
|
+
const { call } = useStitcherWorklet();
|
|
118
|
+
const arFrame: StitcherFrame = {
|
|
119
|
+
width: 1920,
|
|
120
|
+
height: 1080,
|
|
121
|
+
pixelFormat: 'yuv',
|
|
122
|
+
orientation: 'landscape-right',
|
|
123
|
+
timestamp: 0,
|
|
124
|
+
toArrayBuffer: () => new ArrayBuffer(0),
|
|
125
|
+
source: 'ar',
|
|
126
|
+
pose: { rotation: [0, 0, 0, 1] },
|
|
127
|
+
};
|
|
128
|
+
for (let i = 0; i < 30; i++) call(arFrame);
|
|
129
|
+
expect(pluginCallSpy).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('vc-source happy path', () => {
|
|
134
|
+
it('invokes the vc plugin for vc-source frames', () => {
|
|
135
|
+
const { call } = useStitcherWorklet();
|
|
136
|
+
const vcFrame: StitcherFrame = {
|
|
137
|
+
width: 1920,
|
|
138
|
+
height: 1080,
|
|
139
|
+
pixelFormat: 'yuv',
|
|
140
|
+
orientation: 'landscape-right',
|
|
141
|
+
timestamp: 0,
|
|
142
|
+
toArrayBuffer: () => new ArrayBuffer(0),
|
|
143
|
+
source: 'vc',
|
|
144
|
+
pose: { rotation: [0, 0, 0, 1] },
|
|
145
|
+
};
|
|
146
|
+
call(vcFrame);
|
|
147
|
+
expect(pluginCallSpy).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('invokes the vc plugin for frames with undefined source (raw vc Frame)', () => {
|
|
151
|
+
// vc's raw `Frame` doesn't carry the `source` field — the lib's
|
|
152
|
+
// Phase 4a deferral means we don't wrap vc frames into
|
|
153
|
+
// `StitcherFrame`. The AR-source check must treat undefined
|
|
154
|
+
// as "not AR" to preserve the non-AR worklet path.
|
|
155
|
+
const { call } = useStitcherWorklet();
|
|
156
|
+
const rawVcFrame = {
|
|
157
|
+
width: 1920,
|
|
158
|
+
height: 1080,
|
|
159
|
+
pixelFormat: 'yuv',
|
|
160
|
+
orientation: 'landscape-right',
|
|
161
|
+
timestamp: 0,
|
|
162
|
+
toArrayBuffer: () => new ArrayBuffer(0),
|
|
163
|
+
// `source` intentionally absent
|
|
164
|
+
} as unknown as StitcherFrame;
|
|
165
|
+
call(rawVcFrame);
|
|
166
|
+
expect(pluginCallSpy).toHaveBeenCalledTimes(1);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('plugin.call payload shape', () => {
|
|
171
|
+
it('passes the frame + a numeric-intrinsics params object', () => {
|
|
172
|
+
const { call } = useStitcherWorklet();
|
|
173
|
+
const vcFrame: StitcherFrame = {
|
|
174
|
+
width: 1920,
|
|
175
|
+
height: 1080,
|
|
176
|
+
pixelFormat: 'yuv',
|
|
177
|
+
orientation: 'landscape-right',
|
|
178
|
+
timestamp: 0,
|
|
179
|
+
toArrayBuffer: () => new ArrayBuffer(0),
|
|
180
|
+
source: 'vc',
|
|
181
|
+
pose: { rotation: [0, 0, 0, 1] },
|
|
182
|
+
};
|
|
183
|
+
call(vcFrame);
|
|
184
|
+
expect(pluginCallSpy).toHaveBeenCalledWith(
|
|
185
|
+
vcFrame,
|
|
186
|
+
expect.objectContaining({
|
|
187
|
+
tx: 0, ty: 0, tz: 0,
|
|
188
|
+
qx: expect.any(Number),
|
|
189
|
+
qy: expect.any(Number),
|
|
190
|
+
qz: expect.any(Number),
|
|
191
|
+
qw: expect.any(Number),
|
|
192
|
+
fx: expect.any(Number),
|
|
193
|
+
fy: expect.any(Number),
|
|
194
|
+
cx: 960,
|
|
195
|
+
cy: 540,
|
|
196
|
+
imageWidth: 1920,
|
|
197
|
+
imageHeight: 1080,
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -336,6 +336,31 @@ export function useStitcherWorklet(
|
|
|
336
336
|
'worklet';
|
|
337
337
|
if (plugin == null) return;
|
|
338
338
|
|
|
339
|
+
// v0.11.1 — AR-source frames are stitched natively by the AR-
|
|
340
|
+
// side dispatcher (`RNSARSession.swift:510-511` → the first-
|
|
341
|
+
// party callback installed in `RNSARWorkletRuntime`). Calling
|
|
342
|
+
// the vc Frame Processor plugin here would throw
|
|
343
|
+
// `getPropertyAsObject: property '__frame' is undefined`
|
|
344
|
+
// because AR frames are `StitcherFrameHostObject` instances
|
|
345
|
+
// and don't carry the vc `Frame` proxy's JSI marker. The
|
|
346
|
+
// throw is caught silently by the per-worklet error handler
|
|
347
|
+
// (`RNSARWorkletRuntime.mm:284-301`) and bubbles up only to
|
|
348
|
+
// `os_log` — invisible to JS, which is why pre-v0.11.1
|
|
349
|
+
// composed hosts saw their post-`stitcher.call` lines
|
|
350
|
+
// (`fireFrameProcessorLog`, `runOnJS` callbacks) silently
|
|
351
|
+
// never execute in AR mode. Silent no-op here matches the
|
|
352
|
+
// module-header promise that AR mode is "unaffected" by this
|
|
353
|
+
// hook (the AR-side stitching path runs natively, independent
|
|
354
|
+
// of the composed worklet body).
|
|
355
|
+
//
|
|
356
|
+
// The `(frame as StitcherFrame).source` cast is safe: vc
|
|
357
|
+
// `Frame` doesn't carry a `source` property so the check
|
|
358
|
+
// returns `undefined !== 'ar'` → `true`, and the worklet
|
|
359
|
+
// proceeds normally. Only frames that explicitly tag
|
|
360
|
+
// themselves as AR-source (which our native AR dispatcher
|
|
361
|
+
// does — see `StitcherFrameHostObject.mm`) get short-circuited.
|
|
362
|
+
if ((frame as StitcherFrame).source === 'ar') return;
|
|
363
|
+
|
|
339
364
|
// Throttle (verbatim from useFrameProcessorDriver).
|
|
340
365
|
sharedFrameCounter.value += 1;
|
|
341
366
|
const N = sharedEvalEveryN.value;
|