react-native-image-stitcher 0.7.1 → 0.8.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 +122 -0
- package/android/build.gradle +35 -1
- package/android/src/main/cpp/CMakeLists.txt +64 -2
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +30 -11
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +4 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
- package/cpp/stitcher_frame_data.hpp +141 -0
- package/cpp/stitcher_frame_jsi.cpp +214 -0
- package/cpp/stitcher_frame_jsi.hpp +108 -0
- package/cpp/stitcher_proxy_jsi.cpp +109 -0
- package/cpp/stitcher_proxy_jsi.hpp +46 -0
- package/cpp/stitcher_worklet_dispatch.cpp +103 -0
- package/cpp/stitcher_worklet_dispatch.hpp +71 -0
- package/cpp/stitcher_worklet_registry.cpp +81 -0
- package/cpp/stitcher_worklet_registry.hpp +136 -0
- package/dist/camera/Camera.d.ts +62 -12
- package/dist/camera/Camera.js +30 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -1
- package/dist/stitching/StitcherFrame.d.ts +170 -0
- package/dist/stitching/StitcherFrame.js +4 -0
- package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
- package/dist/stitching/StitcherWorkletRegistry.js +78 -0
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/dist/stitching/useFrameProcessor.d.ts +119 -0
- package/dist/stitching/useFrameProcessor.js +196 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -10
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
- package/package.json +1 -1
- package/src/camera/Camera.tsx +93 -28
- package/src/index.ts +16 -0
- package/src/stitching/StitcherFrame.ts +197 -0
- package/src/stitching/StitcherWorkletRegistry.ts +156 -0
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- package/src/stitching/useFrameProcessor.ts +226 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.useFrameProcessor = useFrameProcessor;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
7
|
+
const ensureStitcherProxyInstalled_1 = require("./ensureStitcherProxyInstalled");
|
|
8
|
+
const StitcherWorkletRegistry_1 = require("./StitcherWorkletRegistry");
|
|
9
|
+
/**
|
|
10
|
+
* v0.8.0 Phase 4a — public hook for hosts to attach a per-frame
|
|
11
|
+
* worklet that runs in BOTH AR and non-AR capture modes.
|
|
12
|
+
*
|
|
13
|
+
* ## Quick start
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* import { useFrameProcessor, type StitcherFrame } from 'react-native-image-stitcher';
|
|
17
|
+
*
|
|
18
|
+
* function MyOcrOverlay() {
|
|
19
|
+
* const processor = useFrameProcessor((frame: StitcherFrame) => {
|
|
20
|
+
* 'worklet';
|
|
21
|
+
* // Pixel data is in `frame.toArrayBuffer()`.
|
|
22
|
+
* // AR-only fields: `frame.arDepth`, `frame.arAnchors`, `frame.arTrackingState`.
|
|
23
|
+
* // Discriminate via `frame.source === 'ar'` / `'vc'`.
|
|
24
|
+
* }, []);
|
|
25
|
+
* return <Camera frameProcessor={processor} ... />;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ## Two behaviours, depending on mode
|
|
30
|
+
*
|
|
31
|
+
* **Non-AR mode (today, fully working):** the worklet runs on
|
|
32
|
+
* vision-camera's Frame Processor runtime. Same thread + same
|
|
33
|
+
* cost envelope as a plain `useFrameProcessor` from
|
|
34
|
+
* `react-native-vision-camera`. The lib's own first-party
|
|
35
|
+
* stitching plugin runs alongside on the same producer-thread
|
|
36
|
+
* runtime (composition is handled by vision-camera's own dispatch
|
|
37
|
+
* order).
|
|
38
|
+
*
|
|
39
|
+
* Your worklet receives whatever vision-camera delivers — vc's raw
|
|
40
|
+
* `Frame`. This is a structural subset of `StitcherFrame`: the
|
|
41
|
+
* vc-shaped fields (`width`, `height`, `pixelFormat`, `orientation`,
|
|
42
|
+
* `timestamp`, `toArrayBuffer`) are guaranteed; the
|
|
43
|
+
* `StitcherFrame`-only fields (`source`, `pose`, `arDepth`,
|
|
44
|
+
* `arAnchors`, `arTrackingState`) are **undefined** at runtime
|
|
45
|
+
* because the lib does NOT wrap or augment vc's `Frame` in Phase 4a
|
|
46
|
+
* (cross-worklet-boundary field injection is Phase 4b work).
|
|
47
|
+
* Worklets that need to read `source` / `pose` MUST guard for
|
|
48
|
+
* `undefined`:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* if (frame.source === 'ar') { ... } // false in non-AR mode
|
|
52
|
+
* if (frame.pose) { ... } // skipped in non-AR mode
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* **AR mode — iOS Phase 4b.i (this release):** the worklet is
|
|
56
|
+
* installed into the native registry via
|
|
57
|
+
* `globalThis.__stitcherProxy.install(workletFn)`, where
|
|
58
|
+
* `__stitcherProxy` is a JSI host object installed at lib
|
|
59
|
+
* bootstrap by the native `StitcherJsiInstaller` module. The
|
|
60
|
+
* AR worklet runtime (`RNSARWorkletRuntime`) reads from the
|
|
61
|
+
* native registry on each `dispatchFrame:pose:` call and fans
|
|
62
|
+
* out invocations — your worklet fires alongside the lib's
|
|
63
|
+
* first-party stitching path.
|
|
64
|
+
*
|
|
65
|
+
* **AR mode — Android Phase 4b.ii (deferred):** the native
|
|
66
|
+
* installer + JNI bridge from `StitcherWorkletRuntime.kt`'s
|
|
67
|
+
* `runFirstParty {...}` path to a parallel C++ registry land in
|
|
68
|
+
* a follow-up release. Until then, on Android the hook falls
|
|
69
|
+
* back to the JS-side `StitcherWorkletRegistry`; AR-mode host
|
|
70
|
+
* worklets register but do not invoke. No regression vs.
|
|
71
|
+
* Phase 4a; iOS gets the API first.
|
|
72
|
+
*
|
|
73
|
+
* ### When Phase 4b.ii lands (Android)
|
|
74
|
+
*
|
|
75
|
+
* The hook's call signature does NOT change. Android hosts that
|
|
76
|
+
* write code today against this API will see their worklets
|
|
77
|
+
* start firing in AR mode automatically when Phase 4b.ii is
|
|
78
|
+
* merged. No migration required.
|
|
79
|
+
*
|
|
80
|
+
* ## Frame contract
|
|
81
|
+
*
|
|
82
|
+
* The worklet receives a {@link StitcherFrame} (see
|
|
83
|
+
* `src/stitching/StitcherFrame.ts` for the full contract +
|
|
84
|
+
* lifecycle). Highlights:
|
|
85
|
+
*
|
|
86
|
+
* - **`source`** discriminator: `'vc'` or `'ar'`. Branch on this
|
|
87
|
+
* before reading `arDepth` / `arAnchors` / `arTrackingState`
|
|
88
|
+
* so non-AR captures don't break.
|
|
89
|
+
* - **`pose`** always present. `pose.translation` is `undefined`
|
|
90
|
+
* in non-AR mode (gyro provides only rotation; no spatial
|
|
91
|
+
* anchor).
|
|
92
|
+
* - **Buffer lifetime**: pixel data is valid only for the
|
|
93
|
+
* duration of the worklet call. Worklets that need to retain
|
|
94
|
+
* data must `toArrayBuffer()` synchronously inside the
|
|
95
|
+
* worklet body — returning a reference and reading it later
|
|
96
|
+
* reads freed memory.
|
|
97
|
+
*
|
|
98
|
+
* ## Threading
|
|
99
|
+
*
|
|
100
|
+
* The worklet runs on the producer thread (vision-camera's
|
|
101
|
+
* runtime in non-AR mode; the AR-session callback thread under
|
|
102
|
+
* Phase 4b). Worklets MUST NOT block the producer thread for
|
|
103
|
+
* more than a few ms — the next frame's processing is gated on
|
|
104
|
+
* the previous frame returning. Long work belongs on a queue
|
|
105
|
+
* crossed via Reanimated / worklets-core's `runOnJS`.
|
|
106
|
+
*
|
|
107
|
+
* @param worklet The host's frame processor function. Must be
|
|
108
|
+
* `'worklet'`-prefixed at the call site. TS
|
|
109
|
+
* cannot enforce the prefix; the runtime will
|
|
110
|
+
* throw at attempt to invoke a non-worklet
|
|
111
|
+
* function.
|
|
112
|
+
* @param deps Standard React deps array. When `deps` change,
|
|
113
|
+
* the previous registration is removed and the
|
|
114
|
+
* new worklet is registered. Same semantics as
|
|
115
|
+
* vision-camera's `useFrameProcessor`.
|
|
116
|
+
*
|
|
117
|
+
* @returns A vision-camera frame-processor object that
|
|
118
|
+
* `<Camera frameProcessor={...}>` accepts. In non-AR
|
|
119
|
+
* mode this is what drives the per-frame worklet
|
|
120
|
+
* invocation; in AR mode it's currently a no-op (vc
|
|
121
|
+
* isn't mounted in AR mode anyway).
|
|
122
|
+
*/
|
|
123
|
+
function useFrameProcessor(worklet, deps) {
|
|
124
|
+
// Non-AR path: delegate to vision-camera's hook. The returned
|
|
125
|
+
// processor object is what `<Camera>` hands to vc. Worklet
|
|
126
|
+
// fires on vc's producer-thread runtime.
|
|
127
|
+
//
|
|
128
|
+
// Cast rationale: vc's hook expects `(frame: Frame) => void`.
|
|
129
|
+
// Our worklet is typed `(frame: StitcherFrame) => void`.
|
|
130
|
+
// `StitcherFrame` is a structural superset of `Frame` (it adds
|
|
131
|
+
// required `source` + `pose` and the optional AR fields), so
|
|
132
|
+
// assigning a function that consumes `StitcherFrame` to a
|
|
133
|
+
// `Frame`-consuming slot is unsound at the type level — TS is
|
|
134
|
+
// right to reject the direct assignment. At RUNTIME the worklet
|
|
135
|
+
// will see vc's raw `Frame`; the `source` / `pose` / AR fields
|
|
136
|
+
// are undefined (the hook's docstring above documents this and
|
|
137
|
+
// tells hosts to guard). We double-cast through `unknown` to
|
|
138
|
+
// suppress, accepting the explicit type-system gap as the price
|
|
139
|
+
// of Phase 4a's pre-Phase-4b deferral on cross-runtime frame
|
|
140
|
+
// wrapping.
|
|
141
|
+
const vcProcessor = (0, react_native_vision_camera_1.useFrameProcessor)(worklet, deps);
|
|
142
|
+
// AR path: install into the native registry if available (iOS
|
|
143
|
+
// Phase 4b.i — and Android Phase 4b.ii once it lands). Falls
|
|
144
|
+
// back to the JS-side `StitcherWorkletRegistry` when the native
|
|
145
|
+
// installer isn't present (Android in 4b.i; remote debug mode;
|
|
146
|
+
// unit tests). The fallback path matches Phase 4a's
|
|
147
|
+
// register-but-not-invoke semantics.
|
|
148
|
+
//
|
|
149
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
|
+
(0, react_1.useEffect)(() => {
|
|
151
|
+
const nativeReady = (0, ensureStitcherProxyInstalled_1.ensureStitcherProxyInstalled)();
|
|
152
|
+
if (nativeReady &&
|
|
153
|
+
typeof globalThis.__stitcherProxy !== 'undefined') {
|
|
154
|
+
// Native path — install through the JSI proxy. Errors here
|
|
155
|
+
// most commonly mean the worklet doesn't have the
|
|
156
|
+
// `'worklet'` directive at the call site (the worklets-core
|
|
157
|
+
// babel plugin didn't transform it). Surface them via the
|
|
158
|
+
// proxy's own throw with a host-side log so the failure is
|
|
159
|
+
// obvious.
|
|
160
|
+
let id;
|
|
161
|
+
try {
|
|
162
|
+
id = globalThis.__stitcherProxy.install(worklet);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// Guard `__DEV__` read so the hook works in any environment
|
|
166
|
+
// that imports it without defining the flag (jest, SSR,
|
|
167
|
+
// custom tooling).
|
|
168
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
169
|
+
console.error('[react-native-image-stitcher] __stitcherProxy.install ' +
|
|
170
|
+
'threw — is the worklet function decorated with ' +
|
|
171
|
+
"`'worklet';` and processed by react-native-worklets-core's " +
|
|
172
|
+
'babel plugin? Original error: ' +
|
|
173
|
+
String(err));
|
|
174
|
+
}
|
|
175
|
+
return; // No cleanup needed — nothing was installed.
|
|
176
|
+
}
|
|
177
|
+
return () => {
|
|
178
|
+
try {
|
|
179
|
+
globalThis.__stitcherProxy.uninstall(id);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Uninstall is best-effort; an exception here means the
|
|
183
|
+
// proxy was already gone (e.g., app reload mid-cleanup).
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Fallback — JS-side registry. Same as Phase 4a.
|
|
188
|
+
const jsId = StitcherWorkletRegistry_1.StitcherWorkletRegistry.register({
|
|
189
|
+
worklet,
|
|
190
|
+
isFirstParty: false,
|
|
191
|
+
});
|
|
192
|
+
return () => StitcherWorkletRegistry_1.StitcherWorkletRegistry.unregister(jsId);
|
|
193
|
+
}, deps);
|
|
194
|
+
return vcProcessor;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=useFrameProcessor.js.map
|
|
@@ -484,10 +484,41 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
484
484
|
Int32(config.videoFormat.framesPerSecond))
|
|
485
485
|
isRunning = true
|
|
486
486
|
currentTrackingState = .initialising
|
|
487
|
+
|
|
488
|
+
// v0.8.0 Phase 3c — install the worklet runtime + register
|
|
489
|
+
// the first-party stitching callback. The delegate's
|
|
490
|
+
// per-frame ingest now routes through
|
|
491
|
+
// `RNSARWorkletRuntime.dispatchFrame` (see
|
|
492
|
+
// `session(_:didUpdate:)` below) which invokes this
|
|
493
|
+
// callback synchronously. Net behavior is byte-identical
|
|
494
|
+
// to the pre-Phase-3c direct `consumer.consumeFrame(...)`
|
|
495
|
+
// call. The indirection sets up the seam where Phase 4
|
|
496
|
+
// will fan out to host worklets without touching this
|
|
497
|
+
// first-party path.
|
|
498
|
+
RNSARWorkletRuntime.shared().installIfNeeded()
|
|
499
|
+
RNSARWorkletRuntime.shared().setFirstPartyCallback {
|
|
500
|
+
[weak self] arFrame, pose in
|
|
501
|
+
// ARKit pool reuse contract: must consume the pixel
|
|
502
|
+
// buffer before returning. The consumer's
|
|
503
|
+
// `consumeFrame` does that synchronously inside the
|
|
504
|
+
// call (NV12 → cv::Mat sync, then heavy work on its
|
|
505
|
+
// own queue). We're on the same thread as the
|
|
506
|
+
// delegate (ARSession.delegateQueue), so the contract
|
|
507
|
+
// holds end-to-end.
|
|
508
|
+
guard let self = self else { return }
|
|
509
|
+
guard let consumer = self.incrementalConsumer else { return }
|
|
510
|
+
consumer.consumeFrame(pixelBuffer: arFrame.capturedImage,
|
|
511
|
+
pose: pose)
|
|
512
|
+
}
|
|
487
513
|
}
|
|
488
514
|
|
|
489
515
|
@objc public func stop() {
|
|
490
516
|
guard isRunning else { return }
|
|
517
|
+
// v0.8.0 Phase 3c — drop the first-party callback so the
|
|
518
|
+
// closure's `[weak self]` reference can be released
|
|
519
|
+
// immediately + no in-flight delegate frame re-enters the
|
|
520
|
+
// engine after stop. Idempotent.
|
|
521
|
+
RNSARWorkletRuntime.shared().setFirstPartyCallback(nil)
|
|
491
522
|
arSession.pause()
|
|
492
523
|
isRunning = false
|
|
493
524
|
currentTrackingState = .notAvailable
|
|
@@ -561,16 +592,21 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
561
592
|
}
|
|
562
593
|
}
|
|
563
594
|
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
// path
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
595
|
+
// v0.8.0 Phase 3c — route the per-frame ingest through the
|
|
596
|
+
// worklet runtime instead of calling the consumer directly.
|
|
597
|
+
// The first-party callback (installed in `start()` above)
|
|
598
|
+
// wraps the same `consumer.consumeFrame(pixelBuffer:pose:)`
|
|
599
|
+
// call path, so net behavior is byte-identical to v0.7.x.
|
|
600
|
+
// The indirection sets up the seam where Phase 4 will fan
|
|
601
|
+
// out to host worklets (registered via the v0.8.0
|
|
602
|
+
// `useFrameProcessor` TS hook + a JSI plugin entry point)
|
|
603
|
+
// without changing this first-party path.
|
|
604
|
+
//
|
|
605
|
+
// ARKit pool reuse contract: still satisfied — the runtime
|
|
606
|
+
// invokes the callback synchronously on the delegate
|
|
607
|
+
// thread, and the callback's `consumer.consumeFrame(...)`
|
|
608
|
+
// does the same NV12 → cv::Mat sync conversion as before.
|
|
609
|
+
RNSARWorkletRuntime.shared().dispatchFrame(frame, pose: pose)
|
|
574
610
|
|
|
575
611
|
// If recording is in flight, append this frame to the
|
|
576
612
|
// asset writer DIRECTLY — no queue hop.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// RNSARWorkletRuntime.h — Obj-C facade for the v0.8.0 AR-mode
|
|
4
|
+
// worklet runtime. Wraps `react-native-worklets-core`'s
|
|
5
|
+
// `RNWorklet::JsiWorkletContext` (the same primitive vision-camera
|
|
6
|
+
// uses for its Frame Processor runtime) so the lib can dispatch
|
|
7
|
+
// per-ARFrame worklets on a thread we own — rather than ARKit's
|
|
8
|
+
// delegate queue, where doing significant work would block the
|
|
9
|
+
// AR session's update loop.
|
|
10
|
+
//
|
|
11
|
+
// ## Phase 3b scope (this commit)
|
|
12
|
+
//
|
|
13
|
+
// Owns:
|
|
14
|
+
// - The dispatch queue the worklet runtime pins to.
|
|
15
|
+
// - The underlying `JsiWorkletContext` (constructed lazily on
|
|
16
|
+
// `installIfNeeded`, lives for the singleton's lifetime).
|
|
17
|
+
//
|
|
18
|
+
// Exposes:
|
|
19
|
+
// - `+ shared` singleton accessor.
|
|
20
|
+
// - `- installIfNeeded` (idempotent runtime construction).
|
|
21
|
+
// - `- isInstalled` for diagnostics + tests.
|
|
22
|
+
// - `- dispatchFrame:pose:` — currently a no-op stub; Phase 3c
|
|
23
|
+
// fills in the actual host-object construction + worklet
|
|
24
|
+
// invocation + first-party stitching dispatch.
|
|
25
|
+
//
|
|
26
|
+
// Host-worklet registry is intentionally NOT in Phase 3b — Phase 4
|
|
27
|
+
// lands the JSI plugin + TS-side hook that defines the storage
|
|
28
|
+
// shape (NSMutableArray of boxed shared_ptrs vs a C++ vector ivar
|
|
29
|
+
// vs something else). Pre-committing the storage type here would
|
|
30
|
+
// risk rework. See
|
|
31
|
+
// `docs/plans/handoff/2026-05-26-v0.8.0-phases-2-5-implementation-guide.md`
|
|
32
|
+
// Phase 4 section for the planned API.
|
|
33
|
+
//
|
|
34
|
+
// ## Why Obj-C facade with `.mm` implementation
|
|
35
|
+
//
|
|
36
|
+
// The implementation needs to hold `std::shared_ptr<JsiWorkletContext>`
|
|
37
|
+
// + run JSI value construction, which can't live in pure Swift. Same
|
|
38
|
+
// pattern as `KeyframeGateBridge.{h,mm}` + `StitcherFrameHostObject.{h,mm}`:
|
|
39
|
+
// keep the header umbrella-safe (no JSI imports), put the C++ glue in
|
|
40
|
+
// the .mm.
|
|
41
|
+
//
|
|
42
|
+
// ## Header umbrella safety
|
|
43
|
+
//
|
|
44
|
+
// This .h imports only Foundation + ARKit (both system frameworks).
|
|
45
|
+
// Worklets-core types are confined to the .mm.
|
|
46
|
+
|
|
47
|
+
#pragma once
|
|
48
|
+
|
|
49
|
+
#import <Foundation/Foundation.h>
|
|
50
|
+
#import <ARKit/ARKit.h>
|
|
51
|
+
|
|
52
|
+
@class RNSARFramePose;
|
|
53
|
+
|
|
54
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
55
|
+
|
|
56
|
+
NS_SWIFT_NAME(RNSARWorkletRuntime)
|
|
57
|
+
@interface RNSARWorkletRuntime : NSObject
|
|
58
|
+
|
|
59
|
+
/// Singleton accessor. One AR worklet runtime per process; multiple
|
|
60
|
+
/// `<Camera>` mounts share it. Construction is cheap (just an Obj-C
|
|
61
|
+
/// alloc + an `NSMutableArray`); the heavy JSI work happens in
|
|
62
|
+
/// `-installIfNeeded`.
|
|
63
|
+
+ (instancetype)shared;
|
|
64
|
+
|
|
65
|
+
/// Construct the underlying `JsiWorkletContext` if not yet
|
|
66
|
+
/// installed. Idempotent — repeated calls are no-ops. Called from
|
|
67
|
+
/// `RNSARSession` at AR-mode start time (Phase 3c will wire this
|
|
68
|
+
/// up; Phase 3b ships the method but no one calls it yet).
|
|
69
|
+
///
|
|
70
|
+
/// Threading: safe to call from any thread; internally serialised.
|
|
71
|
+
/// The runtime's own dispatch queue starts running once installed.
|
|
72
|
+
- (void)installIfNeeded;
|
|
73
|
+
|
|
74
|
+
/// Diagnostics + tests. Returns `YES` after a successful
|
|
75
|
+
/// `-installIfNeeded`.
|
|
76
|
+
- (BOOL)isInstalled;
|
|
77
|
+
|
|
78
|
+
/// Phase 3c — type of the first-party stitching callback. Invoked
|
|
79
|
+
/// synchronously on the caller thread (`ARSession.delegateQueue` —
|
|
80
|
+
/// typically main queue today) per AR frame. Block must consume
|
|
81
|
+
/// the pixel buffer before returning (ARKit pool reuse contract).
|
|
82
|
+
typedef void (^RNSARFirstPartyCallback)(ARFrame *arFrame,
|
|
83
|
+
RNSARFramePose *pose);
|
|
84
|
+
|
|
85
|
+
/// Phase 3c — install the closure that takes ownership of the
|
|
86
|
+
/// per-frame first-party stitching dispatch. Called from
|
|
87
|
+
/// `RNSARSession.start()` after the incremental consumer is set;
|
|
88
|
+
/// the block then routes `dispatchFrame:pose:` calls through to
|
|
89
|
+
/// the existing `incrementalConsumer.consumeFrame(...)` path.
|
|
90
|
+
///
|
|
91
|
+
/// Pre-Phase-3c the delegate called the consumer directly. After
|
|
92
|
+
/// Phase 3c the delegate calls `dispatchFrame:pose:` (this class)
|
|
93
|
+
/// which invokes the callback. Net behavior is byte-identical;
|
|
94
|
+
/// the indirection sets up the seam where Phase 4 will fan out to
|
|
95
|
+
/// host worklets without changing the first-party path.
|
|
96
|
+
///
|
|
97
|
+
/// Pass `nil` to clear (e.g. on `RNSARSession.stop()`). Idempotent.
|
|
98
|
+
- (void)setFirstPartyCallback:(nullable RNSARFirstPartyCallback)callback;
|
|
99
|
+
|
|
100
|
+
/// Dispatch one AR frame through the registered worklets. Called
|
|
101
|
+
/// per `ARFrame` by `RNSARSession.delegate` once Phase 3c lands the
|
|
102
|
+
/// migration (Phase 3b ships this method as a no-op stub so the
|
|
103
|
+
/// runtime can be built + linked + the API surface fixed).
|
|
104
|
+
///
|
|
105
|
+
/// The Phase 3c implementation will:
|
|
106
|
+
/// 1. Build a `StitcherFrameHostObject` from `arFrame` + `pose`.
|
|
107
|
+
/// 2. Run the first-party stitching synchronously on the caller
|
|
108
|
+
/// thread (preserves today's `ingestFromARCameraView` cost
|
|
109
|
+
/// envelope at the producer site).
|
|
110
|
+
/// 3. If any host worklets are registered, dispatch the host
|
|
111
|
+
/// object onto the worklet runtime's thread + invoke each
|
|
112
|
+
/// worklet via `RNWorklet::WorkletInvoker::call`.
|
|
113
|
+
/// 4. Invalidate the host object after all worklets return.
|
|
114
|
+
///
|
|
115
|
+
/// Phase 3c gate: install/idempotence tests + this method's
|
|
116
|
+
/// integration test required before merge. See
|
|
117
|
+
/// `docs/plans/handoff/2026-05-26-v0.8.0-phases-2-5-implementation-guide.md`
|
|
118
|
+
/// Phase 3c gate criteria.
|
|
119
|
+
///
|
|
120
|
+
/// Threading: typically called from `ARSession.delegateQueue` (main
|
|
121
|
+
/// queue by default; Phase 3c will pin it explicitly to a
|
|
122
|
+
/// dedicated queue).
|
|
123
|
+
- (void)dispatchFrame:(ARFrame *)arFrame pose:(RNSARFramePose *)pose
|
|
124
|
+
NS_SWIFT_NAME(dispatchFrame(_:pose:));
|
|
125
|
+
|
|
126
|
+
@end
|
|
127
|
+
|
|
128
|
+
NS_ASSUME_NONNULL_END
|