react-native-image-stitcher 0.16.2 → 0.18.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 +154 -0
- package/RNImageStitcher.podspec +26 -1
- package/android/build.gradle +20 -0
- package/android/src/main/cpp/CMakeLists.txt +46 -3
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +436 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +711 -6
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +338 -0
- package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
- package/cpp/camera_frame_jsi.cpp +357 -0
- package/cpp/camera_frame_jsi.hpp +108 -0
- package/cpp/stitcher_proxy_jsi.cpp +140 -0
- package/cpp/stitcher_proxy_jsi.hpp +62 -0
- package/cpp/stitcher_worklet_dispatch.cpp +103 -0
- package/cpp/stitcher_worklet_dispatch.hpp +71 -0
- package/cpp/stitcher_worklet_registry.cpp +91 -0
- package/cpp/stitcher_worklet_registry.hpp +146 -0
- package/dist/camera/ARCameraView.d.ts +77 -0
- package/dist/camera/ARCameraView.js +90 -1
- package/dist/camera/Camera.d.ts +63 -4
- package/dist/camera/Camera.js +2 -2
- package/dist/camera/CaptureMemoryPill.d.ts +4 -3
- package/dist/camera/CaptureMemoryPill.js +4 -3
- package/dist/index.d.ts +2 -1
- package/dist/stitching/ARFrameMeta.d.ts +100 -0
- package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
- package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
- package/dist/stitching/CameraFrame.js +4 -0
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/dist/stitching/useStitcherWorklet.d.ts +4 -4
- package/dist/stitching/useStitcherWorklet.js +4 -4
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +137 -2
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +83 -0
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +336 -40
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +160 -0
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +211 -2
- package/src/camera/Camera.tsx +81 -4
- package/src/camera/CaptureMemoryPill.tsx +4 -3
- package/src/index.ts +7 -3
- package/src/stitching/ARFrameMeta.ts +107 -0
- package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- package/src/stitching/useStitcherWorklet.ts +9 -9
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
* developer verification.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
33
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
34
34
|
import {
|
|
35
|
+
NativeEventEmitter,
|
|
35
36
|
NativeModules,
|
|
36
37
|
Platform,
|
|
37
38
|
StyleSheet,
|
|
@@ -41,6 +42,10 @@ import {
|
|
|
41
42
|
type ViewStyle,
|
|
42
43
|
} from 'react-native';
|
|
43
44
|
|
|
45
|
+
import { ensureStitcherProxyInstalled } from '../stitching/ensureStitcherProxyInstalled';
|
|
46
|
+
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
47
|
+
import type { ARFrameMeta } from '../stitching/ARFrameMeta';
|
|
48
|
+
|
|
44
49
|
|
|
45
50
|
// React Native looks up the component by its NATIVE name.
|
|
46
51
|
// iOS: comes from `ARCameraViewManager.m`'s
|
|
@@ -63,6 +68,83 @@ export interface ARCameraViewProps {
|
|
|
63
68
|
* components without rewriting their guidance text plumbing.
|
|
64
69
|
*/
|
|
65
70
|
guidance?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Optional host worklet invoked once per AR frame, ALONGSIDE the
|
|
73
|
+
* lib's first-party stitching (composition, not replacement). The
|
|
74
|
+
* worklet receives a `CameraFrame` enriched with AR metadata —
|
|
75
|
+
* `source: 'ar'`, world-space `pose` (rotation + translation),
|
|
76
|
+
* `arTrackingState`, and (when supported) `arDepth` / `arAnchors`.
|
|
77
|
+
*
|
|
78
|
+
* Must be a `'worklet'`-prefixed function. Registration installs the
|
|
79
|
+
* native `__stitcherProxy` JSI host object on first use and fans the
|
|
80
|
+
* worklet out from the AR session's per-frame dispatch. If the
|
|
81
|
+
* native install is unavailable (e.g. remote debugging), the worklet
|
|
82
|
+
* silently never fires — no crash.
|
|
83
|
+
*
|
|
84
|
+
* The non-AR equivalent is vision-camera's own `useFrameProcessor`
|
|
85
|
+
* passed via `<Camera frameProcessor={...}>`; the two modes run on
|
|
86
|
+
* different runtimes with different frame shapes, hence the separate
|
|
87
|
+
* prop.
|
|
88
|
+
*/
|
|
89
|
+
arFrameProcessor?: CameraFrameProcessor;
|
|
90
|
+
/**
|
|
91
|
+
* Opt in to per-frame AR depth extraction (`CameraFrame.arDepth`).
|
|
92
|
+
* Default `false` — depth is the costliest field (a per-frame buffer
|
|
93
|
+
* copy), so it stays off until a worklet needs it.
|
|
94
|
+
*/
|
|
95
|
+
enableDepth?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Opt in to per-frame AR anchor extraction (`CameraFrame.arAnchors` —
|
|
98
|
+
* detected planes / augmented images). Default `false`.
|
|
99
|
+
*/
|
|
100
|
+
enableAnchors?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` entries
|
|
103
|
+
* in `arAnchors`, carrying `meshGeometry`). Default `false`. iOS
|
|
104
|
+
* enables ARKit `sceneReconstruction` (LiDAR devices); Android
|
|
105
|
+
* reconstructs a rough mesh from the depth map. Expensive — only on
|
|
106
|
+
* when needed. Implies depth on Android.
|
|
107
|
+
*/
|
|
108
|
+
enableMesh?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Which plane orientations to surface in `arAnchors` (requires
|
|
111
|
+
* `enableAnchors`). Default `'vertical'` — the orientation the
|
|
112
|
+
* plane-projected stitch path has always used, so existing callers
|
|
113
|
+
* see no change.
|
|
114
|
+
*
|
|
115
|
+
* - `'vertical'` — walls / doors / fixtures (the default)
|
|
116
|
+
* - `'horizontal'` — floors / tables / seats
|
|
117
|
+
* - `'both'` — surface every detected plane
|
|
118
|
+
*
|
|
119
|
+
* Platform notes: iOS changes ARKit `planeDetection` to match (a
|
|
120
|
+
* live session reconfigure). Android always detects both planes
|
|
121
|
+
* (ARCore needs horizontal planes to bootstrap tracking) and simply
|
|
122
|
+
* FILTERS which orientations reach `arAnchors`, so the JS-observable
|
|
123
|
+
* set is identical on both platforms.
|
|
124
|
+
*/
|
|
125
|
+
planeDetection?: 'vertical' | 'horizontal' | 'both';
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
|
|
129
|
+
* MAIN thread (NOT a worklet). When provided, the native AR session
|
|
130
|
+
* builds an {@link ARFrameMeta} per frame and emits it as a device
|
|
131
|
+
* event; this component subscribes and calls the handler. Worklet-free
|
|
132
|
+
* — this is the recommended way to read AR pose / tracking / anchor /
|
|
133
|
+
* intrinsics / depth-dims / mesh-counts data (the `arFrameProcessor`
|
|
134
|
+
* worklet can only safely surface a shared value; see `ARFrameMeta`).
|
|
135
|
+
*
|
|
136
|
+
* Costly fields are gated: `depth` only when `enableDepth`, `mesh` only
|
|
137
|
+
* when `enableMesh`, `anchors` only when `enableAnchors`;
|
|
138
|
+
* `intrinsics` / `pose` / `trackingState` are always present. Emission
|
|
139
|
+
* is throttled to {@link arFrameMetaInterval} ms.
|
|
140
|
+
*/
|
|
141
|
+
onArFrame?: (meta: ARFrameMeta) => void;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
|
|
145
|
+
* (≈ 10 Hz). No effect unless `onArFrame` is provided.
|
|
146
|
+
*/
|
|
147
|
+
arFrameMetaInterval?: number;
|
|
66
148
|
}
|
|
67
149
|
|
|
68
150
|
|
|
@@ -145,7 +227,17 @@ type RecordingCallbacks = {
|
|
|
145
227
|
|
|
146
228
|
export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
147
229
|
function ARCameraView(
|
|
148
|
-
{
|
|
230
|
+
{
|
|
231
|
+
style,
|
|
232
|
+
guidance,
|
|
233
|
+
arFrameProcessor,
|
|
234
|
+
enableDepth,
|
|
235
|
+
enableAnchors,
|
|
236
|
+
enableMesh,
|
|
237
|
+
planeDetection,
|
|
238
|
+
onArFrame,
|
|
239
|
+
arFrameMetaInterval,
|
|
240
|
+
},
|
|
149
241
|
ref,
|
|
150
242
|
): React.JSX.Element {
|
|
151
243
|
// Held across the start→stop lifecycle so stopRecording's
|
|
@@ -153,6 +245,123 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
|
153
245
|
// pair vision-camera uses.
|
|
154
246
|
const recordingCallbacksRef = useRef<RecordingCallbacks | null>(null);
|
|
155
247
|
|
|
248
|
+
// AR frame-processor registration. Installs the native
|
|
249
|
+
// `__stitcherProxy` (idempotent) and registers the host worklet so
|
|
250
|
+
// the AR session's per-frame fan-out invokes it; unregisters on
|
|
251
|
+
// unmount or when the worklet identity changes. No-op when no
|
|
252
|
+
// worklet is supplied or the native install is unavailable.
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (arFrameProcessor == null) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
if (!ensureStitcherProxyInstalled()) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const proxy = (globalThis as {
|
|
261
|
+
__stitcherProxy?: {
|
|
262
|
+
install(fn: CameraFrameProcessor): string;
|
|
263
|
+
uninstall(id: string): void;
|
|
264
|
+
};
|
|
265
|
+
}).__stitcherProxy;
|
|
266
|
+
if (proxy == null) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
const id = proxy.install(arFrameProcessor);
|
|
270
|
+
return () => {
|
|
271
|
+
proxy.uninstall(id);
|
|
272
|
+
};
|
|
273
|
+
}, [arFrameProcessor]);
|
|
274
|
+
|
|
275
|
+
// Push the AR-metadata extraction config to native — gates the
|
|
276
|
+
// costly per-frame depth / anchor / mesh work (all off by default).
|
|
277
|
+
// Routed through `__stitcherProxy.setExtractionConfig`, read by the
|
|
278
|
+
// platform AR extraction. iOS ADDITIONALLY toggles ARKit
|
|
279
|
+
// `sceneReconstruction` for mesh (a session-config change, not a
|
|
280
|
+
// per-frame gate); Android reconstructs mesh from the depth map and
|
|
281
|
+
// needs no session change.
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
const depth = enableDepth === true;
|
|
284
|
+
const anchors = enableAnchors === true;
|
|
285
|
+
const mesh = enableMesh === true;
|
|
286
|
+
if (ensureStitcherProxyInstalled()) {
|
|
287
|
+
(globalThis as {
|
|
288
|
+
__stitcherProxy?: {
|
|
289
|
+
setExtractionConfig?(d: boolean, a: boolean, m: boolean): void;
|
|
290
|
+
};
|
|
291
|
+
}).__stitcherProxy?.setExtractionConfig?.(depth, anchors, mesh);
|
|
292
|
+
}
|
|
293
|
+
if (Platform.OS === 'ios') {
|
|
294
|
+
const session = (NativeModules as Record<string, unknown>)
|
|
295
|
+
.RNSARSession as
|
|
296
|
+
| { setSceneReconstructionEnabled?(on: boolean): void }
|
|
297
|
+
| undefined;
|
|
298
|
+
session?.setSceneReconstructionEnabled?.(mesh);
|
|
299
|
+
}
|
|
300
|
+
}, [enableDepth, enableAnchors, enableMesh]);
|
|
301
|
+
|
|
302
|
+
// Push the plane-detection mode to native. Unlike the extraction
|
|
303
|
+
// config above this is a SESSION setting, so it routes through the
|
|
304
|
+
// RNSARSession native module on BOTH platforms (iOS reconfigures
|
|
305
|
+
// ARKit `planeDetection`; Android stores an emission filter — see
|
|
306
|
+
// the prop docs). Defaults to `'vertical'` to preserve the
|
|
307
|
+
// plane-projected stitch path's long-standing behaviour.
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
const mode = planeDetection ?? 'vertical';
|
|
310
|
+
const session = (NativeModules as Record<string, unknown>)
|
|
311
|
+
.RNSARSession as
|
|
312
|
+
| { setPlaneDetection?(mode: string): void }
|
|
313
|
+
| undefined;
|
|
314
|
+
session?.setPlaneDetection?.(mode);
|
|
315
|
+
}, [planeDetection]);
|
|
316
|
+
|
|
317
|
+
// v0.18.0 — onArFrame device-event wiring (worklet-free, main thread).
|
|
318
|
+
//
|
|
319
|
+
// The latest `onArFrame` is held in a ref so the subscription effect
|
|
320
|
+
// depends only on whether a handler is present + the interval — NOT on
|
|
321
|
+
// the handler's identity (which typically changes every render). This
|
|
322
|
+
// avoids tearing down + re-establishing the native event subscription
|
|
323
|
+
// (and the costly `setArFrameMetaEnabled(true)` extraction toggle) on
|
|
324
|
+
// every parent re-render.
|
|
325
|
+
const onArFrameRef = useRef<((meta: ARFrameMeta) => void) | undefined>(
|
|
326
|
+
onArFrame,
|
|
327
|
+
);
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
onArFrameRef.current = onArFrame;
|
|
330
|
+
}, [onArFrame]);
|
|
331
|
+
|
|
332
|
+
const arFrameEnabled = onArFrame != null;
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (!arFrameEnabled) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const session = (NativeModules as Record<string, unknown>)
|
|
338
|
+
.RNSARSession as
|
|
339
|
+
| {
|
|
340
|
+
setArFrameMetaEnabled?(enabled: boolean, intervalMs: number): void;
|
|
341
|
+
}
|
|
342
|
+
| undefined;
|
|
343
|
+
if (session?.setArFrameMetaEnabled == null) {
|
|
344
|
+
// Native module / method unavailable (e.g. web, or a native build
|
|
345
|
+
// predating the event channel): no-op, no crash.
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
const intervalMs = arFrameMetaInterval ?? 100;
|
|
349
|
+
session.setArFrameMetaEnabled(true, intervalMs);
|
|
350
|
+
const emitter = new NativeEventEmitter(
|
|
351
|
+
NativeModules.RNSARSession as never,
|
|
352
|
+
);
|
|
353
|
+
const sub = emitter.addListener(
|
|
354
|
+
'RNImageStitcherARFrame',
|
|
355
|
+
(meta: ARFrameMeta) => {
|
|
356
|
+
onArFrameRef.current?.(meta);
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
return () => {
|
|
360
|
+
sub.remove();
|
|
361
|
+
session.setArFrameMetaEnabled?.(false, intervalMs);
|
|
362
|
+
};
|
|
363
|
+
}, [arFrameEnabled, arFrameMetaInterval]);
|
|
364
|
+
|
|
156
365
|
useImperativeHandle(ref, () => ({
|
|
157
366
|
takePhoto: async (options = {}) => {
|
|
158
367
|
const native: any =
|
package/src/camera/Camera.tsx
CHANGED
|
@@ -66,6 +66,8 @@ import type {
|
|
|
66
66
|
} from 'react-native-vision-camera';
|
|
67
67
|
|
|
68
68
|
import { useARSession } from '../ar/useARSession';
|
|
69
|
+
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
70
|
+
import type { ARFrameMeta } from '../stitching/ARFrameMeta';
|
|
69
71
|
import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
|
|
70
72
|
import { CameraShutter } from './CameraShutter';
|
|
71
73
|
import { CameraView } from './CameraView';
|
|
@@ -679,11 +681,11 @@ export interface CameraProps {
|
|
|
679
681
|
* worklet to fire on vc's Frame Processor runtime.
|
|
680
682
|
*
|
|
681
683
|
* ```tsx
|
|
682
|
-
* import { Camera, useFrameProcessor, type
|
|
684
|
+
* import { Camera, useFrameProcessor, type CameraFrame }
|
|
683
685
|
* from 'react-native-image-stitcher';
|
|
684
686
|
*
|
|
685
687
|
* function MyScreen() {
|
|
686
|
-
* const fp = useFrameProcessor((frame:
|
|
688
|
+
* const fp = useFrameProcessor((frame: CameraFrame) => {
|
|
687
689
|
* 'worklet';
|
|
688
690
|
* // ...
|
|
689
691
|
* }, []);
|
|
@@ -704,12 +706,12 @@ export interface CameraProps {
|
|
|
704
706
|
* ```tsx
|
|
705
707
|
* import {
|
|
706
708
|
* Camera, useFrameProcessor, useStitcherWorklet,
|
|
707
|
-
* type
|
|
709
|
+
* type CameraFrame,
|
|
708
710
|
* } from 'react-native-image-stitcher';
|
|
709
711
|
*
|
|
710
712
|
* function MyScreen() {
|
|
711
713
|
* const stitcher = useStitcherWorklet();
|
|
712
|
-
* const fp = useFrameProcessor((frame:
|
|
714
|
+
* const fp = useFrameProcessor((frame: CameraFrame) => {
|
|
713
715
|
* 'worklet';
|
|
714
716
|
* hostPreLogic(frame);
|
|
715
717
|
* stitcher.call(frame); // ← first-party stitching
|
|
@@ -751,6 +753,67 @@ export interface CameraProps {
|
|
|
751
753
|
*/
|
|
752
754
|
frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
|
|
753
755
|
|
|
756
|
+
/**
|
|
757
|
+
* AR-mode host worklet, invoked once per ARKit / ARCore frame
|
|
758
|
+
* ALONGSIDE the lib's first-party stitching (composition, not
|
|
759
|
+
* replacement). Receives a `CameraFrame` tagged `source: 'ar'`
|
|
760
|
+
* with world-space `pose` + `arTrackingState`. Only fires in AR
|
|
761
|
+
* capture (`captureSource === 'ar'`); the non-AR equivalent is
|
|
762
|
+
* `frameProcessor` above (the two modes use different runtimes and
|
|
763
|
+
* frame shapes). Must be a `'worklet'`-prefixed function; if the
|
|
764
|
+
* native install is unavailable it silently never fires.
|
|
765
|
+
*/
|
|
766
|
+
arFrameProcessor?: CameraFrameProcessor;
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Opt in to per-frame AR depth on the `arFrameProcessor` frame
|
|
770
|
+
* (`CameraFrame.arDepth`). Default `false` — depth is the costliest
|
|
771
|
+
* field (a per-frame buffer copy), so it's off until you need it.
|
|
772
|
+
*/
|
|
773
|
+
enableDepth?: boolean;
|
|
774
|
+
/**
|
|
775
|
+
* Opt in to per-frame AR anchors (`CameraFrame.arAnchors` — detected
|
|
776
|
+
* planes / images). Default `false`.
|
|
777
|
+
*/
|
|
778
|
+
enableAnchors?: boolean;
|
|
779
|
+
/**
|
|
780
|
+
* Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` in
|
|
781
|
+
* `arAnchors`, with `meshGeometry`). Default `false`. iOS enables
|
|
782
|
+
* ARKit `sceneReconstruction` (LiDAR); Android reconstructs a rough
|
|
783
|
+
* mesh from the depth map. Expensive — only on when needed.
|
|
784
|
+
*/
|
|
785
|
+
enableMesh?: boolean;
|
|
786
|
+
/**
|
|
787
|
+
* Which plane orientations to surface in `CameraFrame.arAnchors`
|
|
788
|
+
* (requires `enableAnchors`; AR capture only). Default `'vertical'`
|
|
789
|
+
* — the orientation the plane-projected stitch path has always used.
|
|
790
|
+
* `'horizontal'` surfaces floors / tables; `'both'` surfaces every
|
|
791
|
+
* detected plane. See `ARCameraView` for the per-platform details.
|
|
792
|
+
*/
|
|
793
|
+
planeDetection?: 'vertical' | 'horizontal' | 'both';
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
|
|
797
|
+
* MAIN thread (NOT a worklet). Only fires in AR capture
|
|
798
|
+
* (`captureSource === 'ar'`). Receives an {@link ARFrameMeta} carrying
|
|
799
|
+
* pose, tracking state, intrinsics, and (when the matching `enable*`
|
|
800
|
+
* prop is on) depth dimensions, anchors, and mesh counts.
|
|
801
|
+
*
|
|
802
|
+
* This is the recommended way to read AR metadata: it sidesteps the
|
|
803
|
+
* worklet path entirely (the `arFrameProcessor` worklet can only safely
|
|
804
|
+
* surface a worklets-core shared value, because capturing a host
|
|
805
|
+
* callback crashes the worklet closure-wrap). Native builds the meta
|
|
806
|
+
* and emits a device event; `<Camera>` threads the handler through to
|
|
807
|
+
* `<ARCameraView>`, which subscribes and invokes it on the main thread.
|
|
808
|
+
*/
|
|
809
|
+
onArFrame?: (meta: ARFrameMeta) => void;
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
|
|
813
|
+
* (≈ 10 Hz). No effect unless `onArFrame` is provided.
|
|
814
|
+
*/
|
|
815
|
+
arFrameMetaInterval?: number;
|
|
816
|
+
|
|
754
817
|
// ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────────
|
|
755
818
|
/**
|
|
756
819
|
* Which device holds the non-AR panorama capture accepts.
|
|
@@ -1157,6 +1220,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1157
1220
|
capturePreviewActions,
|
|
1158
1221
|
onCapturePreviewClose,
|
|
1159
1222
|
frameProcessor: hostFrameProcessor,
|
|
1223
|
+
arFrameProcessor,
|
|
1224
|
+
enableDepth,
|
|
1225
|
+
enableAnchors,
|
|
1226
|
+
enableMesh,
|
|
1227
|
+
planeDetection,
|
|
1228
|
+
onArFrame,
|
|
1229
|
+
arFrameMetaInterval,
|
|
1160
1230
|
engine = 'batch-keyframe',
|
|
1161
1231
|
// ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
|
|
1162
1232
|
panMode = 'vertical',
|
|
@@ -2420,6 +2490,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
2420
2490
|
<ARCameraView
|
|
2421
2491
|
ref={arViewRef}
|
|
2422
2492
|
style={StyleSheet.absoluteFill}
|
|
2493
|
+
arFrameProcessor={arFrameProcessor}
|
|
2494
|
+
enableDepth={enableDepth}
|
|
2495
|
+
enableAnchors={enableAnchors}
|
|
2496
|
+
enableMesh={enableMesh}
|
|
2497
|
+
planeDetection={planeDetection}
|
|
2498
|
+
onArFrame={onArFrame}
|
|
2499
|
+
arFrameMetaInterval={arFrameMetaInterval}
|
|
2423
2500
|
/>
|
|
2424
2501
|
) : (
|
|
2425
2502
|
<CameraView
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* (false comfort exactly where OOM happens). Falls back to 1500/2200 if the
|
|
18
18
|
* RAM read is unavailable.
|
|
19
19
|
*
|
|
20
|
-
* Backed by the `getMemoryFootprintMB()` native module (iOS:
|
|
21
|
-
* `phys_footprint`; Android: `/proc/self/statm` RSS
|
|
22
|
-
*
|
|
20
|
+
* Backed by the `getMemoryFootprintMB()` native module (iOS:
|
|
21
|
+
* `task_info(TASK_VM_INFO)` `phys_footprint`; Android: `/proc/self/statm` RSS
|
|
22
|
+
* — resident pages, unthrottled — the SAME number the C++ `[memstat]` logs
|
|
23
|
+
* report). Returns -1 if the native call fails.
|
|
23
24
|
*
|
|
24
25
|
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
25
26
|
* polls native every 500 ms and is unwanted in production builds.
|
package/src/index.ts
CHANGED
|
@@ -259,10 +259,14 @@ export { useKeyframeStream } from './stitching/useKeyframeStream';
|
|
|
259
259
|
// v0.8.0 — unified frame contract for the worklet processor. Same
|
|
260
260
|
// JS-visible shape regardless of capture mode (AR vs non-AR).
|
|
261
261
|
export type {
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
CameraFrame,
|
|
263
|
+
CameraFrameProcessor,
|
|
264
264
|
ARAnchor,
|
|
265
|
-
} from './stitching/
|
|
265
|
+
} from './stitching/CameraFrame';
|
|
266
|
+
// v0.18.0 — LIGHT per-frame AR metadata delivered via the `onArFrame`
|
|
267
|
+
// callback (main-thread, worklet-free). See the type's docstring for why
|
|
268
|
+
// it bypasses the worklet path.
|
|
269
|
+
export type { ARFrameMeta } from './stitching/ARFrameMeta';
|
|
266
270
|
// NOTE: the host-worklet / frame-stream hooks `useFrameProcessor`,
|
|
267
271
|
// `useThrottledFrameProcessor` and `useFrameStream` (v0.8–v0.9) were
|
|
268
272
|
// archived in the batch-keyframe cleanup — they drove the third-party
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* v0.18.0 — LIGHT per-frame AR metadata delivered to JS on the MAIN
|
|
5
|
+
* thread via a normal callback (`onArFrame`), bypassing worklets entirely.
|
|
6
|
+
*
|
|
7
|
+
* ## Why a callback, NOT a worklet
|
|
8
|
+
*
|
|
9
|
+
* The AR worklet path (`arFrameProcessor` + the `__stitcherProxy` JSI
|
|
10
|
+
* registry) deep-copies the worklet's whole closure into the AR worklet
|
|
11
|
+
* runtime via react-native-worklets-core's `WorkletInvoker`. When the
|
|
12
|
+
* worklet captures a host object (e.g. a `createRunOnJS` callback) that
|
|
13
|
+
* closure-wrap recurses without termination → stack overflow → SIGBUS the
|
|
14
|
+
* instant AR mode mounts (verified on device). Worklets can therefore
|
|
15
|
+
* only safely capture a worklets-core *shared value* — an awkward,
|
|
16
|
+
* poll-from-JS pattern for getting structured data back.
|
|
17
|
+
*
|
|
18
|
+
* `onArFrame` sidesteps the whole problem: native builds the metadata and
|
|
19
|
+
* emits it as a plain `RNImageStitcherARFrame` device event carrying a
|
|
20
|
+
* JSON object; the JS side subscribes via `NativeEventEmitter` and invokes
|
|
21
|
+
* the host callback on the main thread. No worklet, no closure-wrap, no
|
|
22
|
+
* shared-value polling. This is the recommended way to read AR metadata.
|
|
23
|
+
*
|
|
24
|
+
* ## Cost / gating
|
|
25
|
+
*
|
|
26
|
+
* The metadata is intentionally LIGHT — no pixel / vertex / face byte
|
|
27
|
+
* marshaling. `depth` reports only the depth map's dimensions + whether a
|
|
28
|
+
* confidence channel exists (no buffer copy); `mesh` reports only anchor /
|
|
29
|
+
* vertex / face *counts*. Native gates each costly field on the matching
|
|
30
|
+
* extraction flag (`depth` ⇐ `enableDepth`, `mesh` ⇐ `enableMesh`,
|
|
31
|
+
* `anchors` ⇐ `enableAnchors`); `intrinsics` / `pose` / `trackingState` are
|
|
32
|
+
* always present. Emission is gated on a TS-set enabled flag (only true
|
|
33
|
+
* when `onArFrame` is provided) and throttled to `arFrameMetaInterval` ms
|
|
34
|
+
* (default 100 ≈ 10 Hz) on the native side.
|
|
35
|
+
*/
|
|
36
|
+
export interface ARFrameMeta {
|
|
37
|
+
/** Frame-capture timestamp in NANOSECONDS (AR-framework monotonic clock). */
|
|
38
|
+
timestamp: number;
|
|
39
|
+
|
|
40
|
+
/** AR tracking quality at this frame. */
|
|
41
|
+
trackingState: 'notAvailable' | 'limited' | 'normal';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Camera pose in world coordinates at frame-capture time.
|
|
45
|
+
*
|
|
46
|
+
* - `rotation` — quaternion `(x, y, z, w)`, matching the convention
|
|
47
|
+
* used throughout the engine + the `CameraFrame.pose` field.
|
|
48
|
+
* - `translation` — metres in world space `[x, y, z]`.
|
|
49
|
+
*/
|
|
50
|
+
pose: {
|
|
51
|
+
rotation: [number, number, number, number];
|
|
52
|
+
translation: [number, number, number];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Camera intrinsics for this frame — focal lengths (`fx`, `fy`) and
|
|
57
|
+
* principal point (`cx`, `cy`) in PIXELS at the `imageWidth × imageHeight`
|
|
58
|
+
* capture resolution. Always attempted (not gated); `null` only when the
|
|
59
|
+
* AR framework didn't provide them for this frame.
|
|
60
|
+
*/
|
|
61
|
+
intrinsics: {
|
|
62
|
+
fx: number;
|
|
63
|
+
fy: number;
|
|
64
|
+
cx: number;
|
|
65
|
+
cy: number;
|
|
66
|
+
imageWidth: number;
|
|
67
|
+
imageHeight: number;
|
|
68
|
+
} | null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Depth-map summary — dimensions + whether a per-pixel confidence channel
|
|
72
|
+
* is available. NO pixel buffer is copied (that's the costly part).
|
|
73
|
+
* Present only when the `enableDepth` prop is on AND the device produced a
|
|
74
|
+
* depth map this frame; `null` otherwise.
|
|
75
|
+
*/
|
|
76
|
+
depth: {
|
|
77
|
+
width: number;
|
|
78
|
+
height: number;
|
|
79
|
+
hasConfidence: boolean;
|
|
80
|
+
} | null;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Tracked AR anchors visible in this frame (planes / images / points /
|
|
84
|
+
* mesh). Empty array when `enableAnchors` is on but nothing is tracked;
|
|
85
|
+
* effectively empty when `enableAnchors` is off. `transform` is a 4×4
|
|
86
|
+
* row-major anchor→world matrix (16 numbers).
|
|
87
|
+
*/
|
|
88
|
+
anchors: Array<{
|
|
89
|
+
id: string;
|
|
90
|
+
type: 'plane' | 'image' | 'point' | 'mesh';
|
|
91
|
+
alignment?: 'horizontal' | 'vertical';
|
|
92
|
+
extent?: [number, number];
|
|
93
|
+
classification?: string;
|
|
94
|
+
transform: number[];
|
|
95
|
+
}>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Scene-reconstruction mesh summary — anchor / vertex / face COUNTS only
|
|
99
|
+
* (no vertex / face byte marshaling). Present only when `enableMesh` is
|
|
100
|
+
* on; `null` otherwise.
|
|
101
|
+
*/
|
|
102
|
+
mesh: {
|
|
103
|
+
anchorCount: number;
|
|
104
|
+
vertexCount: number;
|
|
105
|
+
faceCount: number;
|
|
106
|
+
} | null;
|
|
107
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* v0.8.0 — unified frame contract for the lib's worklet processor.
|
|
5
5
|
*
|
|
6
6
|
* Worklets registered via the v0.8.0 `useFrameProcessor` hook (also in
|
|
7
|
-
* this directory) receive a `
|
|
7
|
+
* this directory) receive a `CameraFrame` regardless of capture mode.
|
|
8
8
|
* The lib-owned worklet runtime guarantees the same JS-visible shape
|
|
9
9
|
* whether the underlying source is a vision-camera `Frame` (non-AR
|
|
10
10
|
* mode, sourced from the FP plugin) or an ARKit `ARFrame` / ARCore
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* (Phase-0 audit confirmed the iOS path). But vision-camera's
|
|
19
19
|
* **Android** `Frame` is `androidx.camera.core.ImageProxy`-coupled —
|
|
20
20
|
* ARCore does NOT produce `ImageProxy` instances. Forcing
|
|
21
|
-
* `
|
|
21
|
+
* `CameraFrame extends Frame` would either (a) require reverse-
|
|
22
22
|
* engineering ImageProxy on Android (intractable + fragile), or
|
|
23
23
|
* (b) make the type asymmetric per platform. Both are worse than
|
|
24
|
-
* making `
|
|
24
|
+
* making `CameraFrame` a structural sibling type that vc Frames
|
|
25
25
|
* happen to satisfy (because vc Frames carry the same width / height /
|
|
26
26
|
* orientation / pixelFormat / timestamp / toArrayBuffer surface).
|
|
27
27
|
*
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
* a JPEG-encode frame-processor plugin). Returning a reference and
|
|
39
39
|
* reading it later will read into freed memory.
|
|
40
40
|
*/
|
|
41
|
-
export interface
|
|
41
|
+
export interface CameraFrame {
|
|
42
42
|
// ── vision-camera-shaped fields (structural compat) ─────────────
|
|
43
43
|
// Worklets written against a vc `Frame` work unchanged against a
|
|
44
|
-
// `
|
|
44
|
+
// `CameraFrame` (the fields below are a strict subset of vc
|
|
45
45
|
// Frame's JS-visible surface).
|
|
46
46
|
|
|
47
47
|
/** Pixel width of the camera image. */
|
|
@@ -167,6 +167,28 @@ export interface StitcherFrame {
|
|
|
167
167
|
* tracking is degraded check this. Undefined in non-AR mode.
|
|
168
168
|
*/
|
|
169
169
|
arTrackingState?: 'notAvailable' | 'limited' | 'normal';
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Camera intrinsics for THIS frame — focal lengths (`fx`,`fy`) and
|
|
173
|
+
* principal point (`cx`,`cy`) in PIXELS at the `imageWidth × imageHeight`
|
|
174
|
+
* capture resolution. Needed to lift 2D image-space coordinates to 3D
|
|
175
|
+
* via pose + intrinsics (e.g. object-level reconstruction).
|
|
176
|
+
*
|
|
177
|
+
* Populated on **AR frames** (`source: 'ar'`) from ARKit
|
|
178
|
+
* `ARCamera.intrinsics` / ARCore `Camera` intrinsics. **Undefined for
|
|
179
|
+
* non-AR (vision-camera) frames** — they are raw vc `Frame`s without an
|
|
180
|
+
* intrinsics surface; read vc's own APIs there if needed. (The spec
|
|
181
|
+
* called this required; it's optional here because the non-AR frame
|
|
182
|
+
* shape genuinely can't carry it.)
|
|
183
|
+
*/
|
|
184
|
+
intrinsics?: {
|
|
185
|
+
fx: number;
|
|
186
|
+
fy: number;
|
|
187
|
+
cx: number;
|
|
188
|
+
cy: number;
|
|
189
|
+
imageWidth: number;
|
|
190
|
+
imageHeight: number;
|
|
191
|
+
};
|
|
170
192
|
}
|
|
171
193
|
|
|
172
194
|
/**
|
|
@@ -177,21 +199,67 @@ export interface StitcherFrame {
|
|
|
177
199
|
export interface ARAnchor {
|
|
178
200
|
/** Stable per-session anchor identifier. */
|
|
179
201
|
id: string;
|
|
180
|
-
/** Anchor kind. `'point'` is Android (ARCore) only. */
|
|
181
|
-
type: 'plane' | 'image' | 'point';
|
|
182
202
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
203
|
+
* Anchor kind. `'point'` is Android (ARCore) only; `'mesh'` is a
|
|
204
|
+
* scene-reconstruction mesh anchor, present only when the `enableMesh`
|
|
205
|
+
* `<Camera>` prop is on (and the device supports reconstruction).
|
|
206
|
+
*/
|
|
207
|
+
type: 'plane' | 'image' | 'point' | 'mesh';
|
|
208
|
+
/**
|
|
209
|
+
* 4×4 row-major transform from anchor space to world space (16
|
|
210
|
+
* numbers). For `'mesh'` anchors, the `meshGeometry.vertices` are in
|
|
211
|
+
* this anchor's LOCAL space — multiply by `transform` for world coords.
|
|
185
212
|
*/
|
|
186
213
|
transform: number[];
|
|
214
|
+
/**
|
|
215
|
+
* Plane orientation — `'horizontal'` (floor / table / seat) vs
|
|
216
|
+
* `'vertical'` (wall / door / window). Present on `'plane'` anchors;
|
|
217
|
+
* undefined for other anchor kinds. Lets a host distinguish a shelf
|
|
218
|
+
* surface from the wall behind it.
|
|
219
|
+
*/
|
|
220
|
+
alignment?: 'horizontal' | 'vertical';
|
|
221
|
+
/**
|
|
222
|
+
* Plane size in metres along its local x / z axes (`[x, z]`). Present
|
|
223
|
+
* on `'plane'` anchors only.
|
|
224
|
+
*/
|
|
225
|
+
extent?: [number, number];
|
|
226
|
+
/**
|
|
227
|
+
* ARKit semantic classification of the plane's surface, when the
|
|
228
|
+
* framework provides it (iOS; mostly horizontal planes). Undefined
|
|
229
|
+
* when unknown / unsupported (incl. Android, which has no equivalent).
|
|
230
|
+
*/
|
|
231
|
+
classification?:
|
|
232
|
+
| 'wall'
|
|
233
|
+
| 'floor'
|
|
234
|
+
| 'ceiling'
|
|
235
|
+
| 'table'
|
|
236
|
+
| 'seat'
|
|
237
|
+
| 'door'
|
|
238
|
+
| 'window'
|
|
239
|
+
| 'none';
|
|
240
|
+
/**
|
|
241
|
+
* Scene-reconstruction geometry — present only on `type: 'mesh'`
|
|
242
|
+
* anchors. Buffers (wrap in the noted typed-array view):
|
|
243
|
+
* - `vertices` → `Float32Array`, xyz triplets in anchor-local space.
|
|
244
|
+
* - `faces` → `Uint32Array`, triangle indices into `vertices`.
|
|
245
|
+
* - `classifications` → optional `Uint8Array`, one ARKit mesh class
|
|
246
|
+
* per face (0=none, 1=wall, 2=floor, 3=ceiling, …). **iOS only**
|
|
247
|
+
* (from `ARMeshAnchor`); absent on Android, where the mesh is
|
|
248
|
+
* reconstructed from the depth map and carries no semantics.
|
|
249
|
+
*/
|
|
250
|
+
meshGeometry?: {
|
|
251
|
+
vertices: ArrayBuffer;
|
|
252
|
+
faces: ArrayBuffer;
|
|
253
|
+
classifications?: ArrayBuffer;
|
|
254
|
+
};
|
|
187
255
|
}
|
|
188
256
|
|
|
189
257
|
/**
|
|
190
258
|
* v0.8.0 — worklet function signature for the unified frame processor.
|
|
191
259
|
*
|
|
192
260
|
* Must be a `'worklet'`-prefixed function (so it can run on the
|
|
193
|
-
* worklet runtime). Receives a `
|
|
261
|
+
* worklet runtime). Receives a `CameraFrame` per camera frame; the
|
|
194
262
|
* return value is ignored (use `runOnJS` / shared values to surface
|
|
195
263
|
* results back to the JS thread).
|
|
196
264
|
*/
|
|
197
|
-
export type
|
|
265
|
+
export type CameraFrameProcessor = (frame: CameraFrame) => void;
|