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,6 +30,8 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import React from 'react';
|
|
32
32
|
import { type ViewStyle } from 'react-native';
|
|
33
|
+
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
34
|
+
import type { ARFrameMeta } from '../stitching/ARFrameMeta';
|
|
33
35
|
export interface ARCameraViewProps {
|
|
34
36
|
/** Layout style, typically `StyleSheet.absoluteFill` or `flex: 1`. */
|
|
35
37
|
style?: ViewStyle;
|
|
@@ -39,6 +41,81 @@ export interface ARCameraViewProps {
|
|
|
39
41
|
* components without rewriting their guidance text plumbing.
|
|
40
42
|
*/
|
|
41
43
|
guidance?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional host worklet invoked once per AR frame, ALONGSIDE the
|
|
46
|
+
* lib's first-party stitching (composition, not replacement). The
|
|
47
|
+
* worklet receives a `CameraFrame` enriched with AR metadata —
|
|
48
|
+
* `source: 'ar'`, world-space `pose` (rotation + translation),
|
|
49
|
+
* `arTrackingState`, and (when supported) `arDepth` / `arAnchors`.
|
|
50
|
+
*
|
|
51
|
+
* Must be a `'worklet'`-prefixed function. Registration installs the
|
|
52
|
+
* native `__stitcherProxy` JSI host object on first use and fans the
|
|
53
|
+
* worklet out from the AR session's per-frame dispatch. If the
|
|
54
|
+
* native install is unavailable (e.g. remote debugging), the worklet
|
|
55
|
+
* silently never fires — no crash.
|
|
56
|
+
*
|
|
57
|
+
* The non-AR equivalent is vision-camera's own `useFrameProcessor`
|
|
58
|
+
* passed via `<Camera frameProcessor={...}>`; the two modes run on
|
|
59
|
+
* different runtimes with different frame shapes, hence the separate
|
|
60
|
+
* prop.
|
|
61
|
+
*/
|
|
62
|
+
arFrameProcessor?: CameraFrameProcessor;
|
|
63
|
+
/**
|
|
64
|
+
* Opt in to per-frame AR depth extraction (`CameraFrame.arDepth`).
|
|
65
|
+
* Default `false` — depth is the costliest field (a per-frame buffer
|
|
66
|
+
* copy), so it stays off until a worklet needs it.
|
|
67
|
+
*/
|
|
68
|
+
enableDepth?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Opt in to per-frame AR anchor extraction (`CameraFrame.arAnchors` —
|
|
71
|
+
* detected planes / augmented images). Default `false`.
|
|
72
|
+
*/
|
|
73
|
+
enableAnchors?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` entries
|
|
76
|
+
* in `arAnchors`, carrying `meshGeometry`). Default `false`. iOS
|
|
77
|
+
* enables ARKit `sceneReconstruction` (LiDAR devices); Android
|
|
78
|
+
* reconstructs a rough mesh from the depth map. Expensive — only on
|
|
79
|
+
* when needed. Implies depth on Android.
|
|
80
|
+
*/
|
|
81
|
+
enableMesh?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Which plane orientations to surface in `arAnchors` (requires
|
|
84
|
+
* `enableAnchors`). Default `'vertical'` — the orientation the
|
|
85
|
+
* plane-projected stitch path has always used, so existing callers
|
|
86
|
+
* see no change.
|
|
87
|
+
*
|
|
88
|
+
* - `'vertical'` — walls / doors / fixtures (the default)
|
|
89
|
+
* - `'horizontal'` — floors / tables / seats
|
|
90
|
+
* - `'both'` — surface every detected plane
|
|
91
|
+
*
|
|
92
|
+
* Platform notes: iOS changes ARKit `planeDetection` to match (a
|
|
93
|
+
* live session reconfigure). Android always detects both planes
|
|
94
|
+
* (ARCore needs horizontal planes to bootstrap tracking) and simply
|
|
95
|
+
* FILTERS which orientations reach `arAnchors`, so the JS-observable
|
|
96
|
+
* set is identical on both platforms.
|
|
97
|
+
*/
|
|
98
|
+
planeDetection?: 'vertical' | 'horizontal' | 'both';
|
|
99
|
+
/**
|
|
100
|
+
* v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
|
|
101
|
+
* MAIN thread (NOT a worklet). When provided, the native AR session
|
|
102
|
+
* builds an {@link ARFrameMeta} per frame and emits it as a device
|
|
103
|
+
* event; this component subscribes and calls the handler. Worklet-free
|
|
104
|
+
* — this is the recommended way to read AR pose / tracking / anchor /
|
|
105
|
+
* intrinsics / depth-dims / mesh-counts data (the `arFrameProcessor`
|
|
106
|
+
* worklet can only safely surface a shared value; see `ARFrameMeta`).
|
|
107
|
+
*
|
|
108
|
+
* Costly fields are gated: `depth` only when `enableDepth`, `mesh` only
|
|
109
|
+
* when `enableMesh`, `anchors` only when `enableAnchors`;
|
|
110
|
+
* `intrinsics` / `pose` / `trackingState` are always present. Emission
|
|
111
|
+
* is throttled to {@link arFrameMetaInterval} ms.
|
|
112
|
+
*/
|
|
113
|
+
onArFrame?: (meta: ARFrameMeta) => void;
|
|
114
|
+
/**
|
|
115
|
+
* v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
|
|
116
|
+
* (≈ 10 Hz). No effect unless `onArFrame` is provided.
|
|
117
|
+
*/
|
|
118
|
+
arFrameMetaInterval?: number;
|
|
42
119
|
}
|
|
43
120
|
/**
|
|
44
121
|
* Imperative handle exposed via the ref — shape mirrors the subset
|
|
@@ -67,6 +67,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
67
67
|
exports.ARCameraView = void 0;
|
|
68
68
|
const react_1 = __importStar(require("react"));
|
|
69
69
|
const react_native_1 = require("react-native");
|
|
70
|
+
const ensureStitcherProxyInstalled_1 = require("../stitching/ensureStitcherProxyInstalled");
|
|
70
71
|
// React Native looks up the component by its NATIVE name.
|
|
71
72
|
// iOS: comes from `ARCameraViewManager.m`'s
|
|
72
73
|
// `RCT_EXTERN_MODULE(RNSARCameraViewManager, RCTViewManager)`.
|
|
@@ -76,11 +77,99 @@ const react_native_1 = require("react-native");
|
|
|
76
77
|
const NativeARCameraView = react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android'
|
|
77
78
|
? (0, react_native_1.requireNativeComponent)('RNSARCameraView')
|
|
78
79
|
: null;
|
|
79
|
-
exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance }, ref) {
|
|
80
|
+
exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, }, ref) {
|
|
80
81
|
// Held across the start→stop lifecycle so stopRecording's
|
|
81
82
|
// resolved VideoFile can be delivered via the same callback
|
|
82
83
|
// pair vision-camera uses.
|
|
83
84
|
const recordingCallbacksRef = (0, react_1.useRef)(null);
|
|
85
|
+
// AR frame-processor registration. Installs the native
|
|
86
|
+
// `__stitcherProxy` (idempotent) and registers the host worklet so
|
|
87
|
+
// the AR session's per-frame fan-out invokes it; unregisters on
|
|
88
|
+
// unmount or when the worklet identity changes. No-op when no
|
|
89
|
+
// worklet is supplied or the native install is unavailable.
|
|
90
|
+
(0, react_1.useEffect)(() => {
|
|
91
|
+
if (arFrameProcessor == null) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (!(0, ensureStitcherProxyInstalled_1.ensureStitcherProxyInstalled)()) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const proxy = globalThis.__stitcherProxy;
|
|
98
|
+
if (proxy == null) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const id = proxy.install(arFrameProcessor);
|
|
102
|
+
return () => {
|
|
103
|
+
proxy.uninstall(id);
|
|
104
|
+
};
|
|
105
|
+
}, [arFrameProcessor]);
|
|
106
|
+
// Push the AR-metadata extraction config to native — gates the
|
|
107
|
+
// costly per-frame depth / anchor / mesh work (all off by default).
|
|
108
|
+
// Routed through `__stitcherProxy.setExtractionConfig`, read by the
|
|
109
|
+
// platform AR extraction. iOS ADDITIONALLY toggles ARKit
|
|
110
|
+
// `sceneReconstruction` for mesh (a session-config change, not a
|
|
111
|
+
// per-frame gate); Android reconstructs mesh from the depth map and
|
|
112
|
+
// needs no session change.
|
|
113
|
+
(0, react_1.useEffect)(() => {
|
|
114
|
+
const depth = enableDepth === true;
|
|
115
|
+
const anchors = enableAnchors === true;
|
|
116
|
+
const mesh = enableMesh === true;
|
|
117
|
+
if ((0, ensureStitcherProxyInstalled_1.ensureStitcherProxyInstalled)()) {
|
|
118
|
+
globalThis.__stitcherProxy?.setExtractionConfig?.(depth, anchors, mesh);
|
|
119
|
+
}
|
|
120
|
+
if (react_native_1.Platform.OS === 'ios') {
|
|
121
|
+
const session = react_native_1.NativeModules
|
|
122
|
+
.RNSARSession;
|
|
123
|
+
session?.setSceneReconstructionEnabled?.(mesh);
|
|
124
|
+
}
|
|
125
|
+
}, [enableDepth, enableAnchors, enableMesh]);
|
|
126
|
+
// Push the plane-detection mode to native. Unlike the extraction
|
|
127
|
+
// config above this is a SESSION setting, so it routes through the
|
|
128
|
+
// RNSARSession native module on BOTH platforms (iOS reconfigures
|
|
129
|
+
// ARKit `planeDetection`; Android stores an emission filter — see
|
|
130
|
+
// the prop docs). Defaults to `'vertical'` to preserve the
|
|
131
|
+
// plane-projected stitch path's long-standing behaviour.
|
|
132
|
+
(0, react_1.useEffect)(() => {
|
|
133
|
+
const mode = planeDetection ?? 'vertical';
|
|
134
|
+
const session = react_native_1.NativeModules
|
|
135
|
+
.RNSARSession;
|
|
136
|
+
session?.setPlaneDetection?.(mode);
|
|
137
|
+
}, [planeDetection]);
|
|
138
|
+
// v0.18.0 — onArFrame device-event wiring (worklet-free, main thread).
|
|
139
|
+
//
|
|
140
|
+
// The latest `onArFrame` is held in a ref so the subscription effect
|
|
141
|
+
// depends only on whether a handler is present + the interval — NOT on
|
|
142
|
+
// the handler's identity (which typically changes every render). This
|
|
143
|
+
// avoids tearing down + re-establishing the native event subscription
|
|
144
|
+
// (and the costly `setArFrameMetaEnabled(true)` extraction toggle) on
|
|
145
|
+
// every parent re-render.
|
|
146
|
+
const onArFrameRef = (0, react_1.useRef)(onArFrame);
|
|
147
|
+
(0, react_1.useEffect)(() => {
|
|
148
|
+
onArFrameRef.current = onArFrame;
|
|
149
|
+
}, [onArFrame]);
|
|
150
|
+
const arFrameEnabled = onArFrame != null;
|
|
151
|
+
(0, react_1.useEffect)(() => {
|
|
152
|
+
if (!arFrameEnabled) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const session = react_native_1.NativeModules
|
|
156
|
+
.RNSARSession;
|
|
157
|
+
if (session?.setArFrameMetaEnabled == null) {
|
|
158
|
+
// Native module / method unavailable (e.g. web, or a native build
|
|
159
|
+
// predating the event channel): no-op, no crash.
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const intervalMs = arFrameMetaInterval ?? 100;
|
|
163
|
+
session.setArFrameMetaEnabled(true, intervalMs);
|
|
164
|
+
const emitter = new react_native_1.NativeEventEmitter(react_native_1.NativeModules.RNSARSession);
|
|
165
|
+
const sub = emitter.addListener('RNImageStitcherARFrame', (meta) => {
|
|
166
|
+
onArFrameRef.current?.(meta);
|
|
167
|
+
});
|
|
168
|
+
return () => {
|
|
169
|
+
sub.remove();
|
|
170
|
+
session.setArFrameMetaEnabled?.(false, intervalMs);
|
|
171
|
+
};
|
|
172
|
+
}, [arFrameEnabled, arFrameMetaInterval]);
|
|
84
173
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
85
174
|
takePhoto: async (options = {}) => {
|
|
86
175
|
const native = react_native_1.NativeModules.RNSARSession;
|
package/dist/camera/Camera.d.ts
CHANGED
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
import React from 'react';
|
|
42
42
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
43
43
|
import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera';
|
|
44
|
+
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
45
|
+
import type { ARFrameMeta } from '../stitching/ARFrameMeta';
|
|
44
46
|
import { type CaptureHeaderProps } from './CaptureHeader';
|
|
45
47
|
import { type CapturePreviewAction } from './CapturePreview';
|
|
46
48
|
import { type CaptureThumbnailItem } from './CaptureThumbnailStrip';
|
|
@@ -526,11 +528,11 @@ export interface CameraProps {
|
|
|
526
528
|
* worklet to fire on vc's Frame Processor runtime.
|
|
527
529
|
*
|
|
528
530
|
* ```tsx
|
|
529
|
-
* import { Camera, useFrameProcessor, type
|
|
531
|
+
* import { Camera, useFrameProcessor, type CameraFrame }
|
|
530
532
|
* from 'react-native-image-stitcher';
|
|
531
533
|
*
|
|
532
534
|
* function MyScreen() {
|
|
533
|
-
* const fp = useFrameProcessor((frame:
|
|
535
|
+
* const fp = useFrameProcessor((frame: CameraFrame) => {
|
|
534
536
|
* 'worklet';
|
|
535
537
|
* // ...
|
|
536
538
|
* }, []);
|
|
@@ -551,12 +553,12 @@ export interface CameraProps {
|
|
|
551
553
|
* ```tsx
|
|
552
554
|
* import {
|
|
553
555
|
* Camera, useFrameProcessor, useStitcherWorklet,
|
|
554
|
-
* type
|
|
556
|
+
* type CameraFrame,
|
|
555
557
|
* } from 'react-native-image-stitcher';
|
|
556
558
|
*
|
|
557
559
|
* function MyScreen() {
|
|
558
560
|
* const stitcher = useStitcherWorklet();
|
|
559
|
-
* const fp = useFrameProcessor((frame:
|
|
561
|
+
* const fp = useFrameProcessor((frame: CameraFrame) => {
|
|
560
562
|
* 'worklet';
|
|
561
563
|
* hostPreLogic(frame);
|
|
562
564
|
* stitcher.call(frame); // ← first-party stitching
|
|
@@ -597,6 +599,63 @@ export interface CameraProps {
|
|
|
597
599
|
* CHANGELOG.)
|
|
598
600
|
*/
|
|
599
601
|
frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
|
|
602
|
+
/**
|
|
603
|
+
* AR-mode host worklet, invoked once per ARKit / ARCore frame
|
|
604
|
+
* ALONGSIDE the lib's first-party stitching (composition, not
|
|
605
|
+
* replacement). Receives a `CameraFrame` tagged `source: 'ar'`
|
|
606
|
+
* with world-space `pose` + `arTrackingState`. Only fires in AR
|
|
607
|
+
* capture (`captureSource === 'ar'`); the non-AR equivalent is
|
|
608
|
+
* `frameProcessor` above (the two modes use different runtimes and
|
|
609
|
+
* frame shapes). Must be a `'worklet'`-prefixed function; if the
|
|
610
|
+
* native install is unavailable it silently never fires.
|
|
611
|
+
*/
|
|
612
|
+
arFrameProcessor?: CameraFrameProcessor;
|
|
613
|
+
/**
|
|
614
|
+
* Opt in to per-frame AR depth on the `arFrameProcessor` frame
|
|
615
|
+
* (`CameraFrame.arDepth`). Default `false` — depth is the costliest
|
|
616
|
+
* field (a per-frame buffer copy), so it's off until you need it.
|
|
617
|
+
*/
|
|
618
|
+
enableDepth?: boolean;
|
|
619
|
+
/**
|
|
620
|
+
* Opt in to per-frame AR anchors (`CameraFrame.arAnchors` — detected
|
|
621
|
+
* planes / images). Default `false`.
|
|
622
|
+
*/
|
|
623
|
+
enableAnchors?: boolean;
|
|
624
|
+
/**
|
|
625
|
+
* Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` in
|
|
626
|
+
* `arAnchors`, with `meshGeometry`). Default `false`. iOS enables
|
|
627
|
+
* ARKit `sceneReconstruction` (LiDAR); Android reconstructs a rough
|
|
628
|
+
* mesh from the depth map. Expensive — only on when needed.
|
|
629
|
+
*/
|
|
630
|
+
enableMesh?: boolean;
|
|
631
|
+
/**
|
|
632
|
+
* Which plane orientations to surface in `CameraFrame.arAnchors`
|
|
633
|
+
* (requires `enableAnchors`; AR capture only). Default `'vertical'`
|
|
634
|
+
* — the orientation the plane-projected stitch path has always used.
|
|
635
|
+
* `'horizontal'` surfaces floors / tables; `'both'` surfaces every
|
|
636
|
+
* detected plane. See `ARCameraView` for the per-platform details.
|
|
637
|
+
*/
|
|
638
|
+
planeDetection?: 'vertical' | 'horizontal' | 'both';
|
|
639
|
+
/**
|
|
640
|
+
* v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
|
|
641
|
+
* MAIN thread (NOT a worklet). Only fires in AR capture
|
|
642
|
+
* (`captureSource === 'ar'`). Receives an {@link ARFrameMeta} carrying
|
|
643
|
+
* pose, tracking state, intrinsics, and (when the matching `enable*`
|
|
644
|
+
* prop is on) depth dimensions, anchors, and mesh counts.
|
|
645
|
+
*
|
|
646
|
+
* This is the recommended way to read AR metadata: it sidesteps the
|
|
647
|
+
* worklet path entirely (the `arFrameProcessor` worklet can only safely
|
|
648
|
+
* surface a worklets-core shared value, because capturing a host
|
|
649
|
+
* callback crashes the worklet closure-wrap). Native builds the meta
|
|
650
|
+
* and emits a device event; `<Camera>` threads the handler through to
|
|
651
|
+
* `<ARCameraView>`, which subscribes and invokes it on the main thread.
|
|
652
|
+
*/
|
|
653
|
+
onArFrame?: (meta: ARFrameMeta) => void;
|
|
654
|
+
/**
|
|
655
|
+
* v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
|
|
656
|
+
* (≈ 10 Hz). No effect unless `onArFrame` is provided.
|
|
657
|
+
*/
|
|
658
|
+
arFrameMetaInterval?: number;
|
|
600
659
|
/**
|
|
601
660
|
* Which device holds the non-AR panorama capture accepts.
|
|
602
661
|
*
|
package/dist/camera/Camera.js
CHANGED
|
@@ -310,7 +310,7 @@ function extractPanoramaOverrides(props) {
|
|
|
310
310
|
* The public `<Camera>` component.
|
|
311
311
|
*/
|
|
312
312
|
function Camera(props) {
|
|
313
|
-
const { defaultCaptureSource = 'non-ar', defaultLens = '1x', captureSources = 'both', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe',
|
|
313
|
+
const { defaultCaptureSource = 'non-ar', defaultLens = '1x', captureSources = 'both', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, engine = 'batch-keyframe',
|
|
314
314
|
// ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
|
|
315
315
|
panMode = 'vertical', panGuidance = true, maxPanDurationMs = 0, panTooFastThreshold, lateralBudgetCm = 4, rectCrop = false, showPreview = false, guidanceCopy, } = props;
|
|
316
316
|
// Derived guidance state. The landscape-only gate decision itself is
|
|
@@ -1425,7 +1425,7 @@ function Camera(props) {
|
|
|
1425
1425
|
// (V12.14.8 OOM fix). The CaptureStatusOverlay renders the
|
|
1426
1426
|
// "Stitching…" state on top, so no placeholder label is needed
|
|
1427
1427
|
// in that case — only for the camera-switch transition.
|
|
1428
|
-
react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] }, statusPhase === 'stitching' ? null : (react_1.default.createElement(react_native_1.Text, { style: styles.transitionLabel }, "Switching camera\u2026")))) : isAR ? (react_1.default.createElement(ARCameraView_1.ARCameraView, { ref: arViewRef, style: react_native_1.StyleSheet.absoluteFill })) : (react_1.default.createElement(CameraView_1.CameraView, { ref: visionCameraRef, device: capture.device, isActive: true,
|
|
1428
|
+
react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] }, statusPhase === 'stitching' ? null : (react_1.default.createElement(react_native_1.Text, { style: styles.transitionLabel }, "Switching camera\u2026")))) : isAR ? (react_1.default.createElement(ARCameraView_1.ARCameraView, { ref: arViewRef, style: react_native_1.StyleSheet.absoluteFill, arFrameProcessor: arFrameProcessor, enableDepth: enableDepth, enableAnchors: enableAnchors, enableMesh: enableMesh, planeDetection: planeDetection, onArFrame: onArFrame, arFrameMetaInterval: arFrameMetaInterval })) : (react_1.default.createElement(CameraView_1.CameraView, { ref: visionCameraRef, device: capture.device, isActive: true,
|
|
1429
1429
|
// `video={true}` is REQUIRED for takeSnapshot to work on iOS.
|
|
1430
1430
|
// vision-camera v4's iOS implementation of takeSnapshot waits
|
|
1431
1431
|
// for a frame on the video pipeline; with video disabled, the
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
* (false comfort exactly where OOM happens). Falls back to 1500/2200 if the
|
|
17
17
|
* RAM read is unavailable.
|
|
18
18
|
*
|
|
19
|
-
* Backed by the `getMemoryFootprintMB()` native module (iOS:
|
|
20
|
-
* `phys_footprint`; Android: `/proc/self/statm` RSS
|
|
21
|
-
*
|
|
19
|
+
* Backed by the `getMemoryFootprintMB()` native module (iOS:
|
|
20
|
+
* `task_info(TASK_VM_INFO)` `phys_footprint`; Android: `/proc/self/statm` RSS
|
|
21
|
+
* — resident pages, unthrottled — the SAME number the C++ `[memstat]` logs
|
|
22
|
+
* report). Returns -1 if the native call fails.
|
|
22
23
|
*
|
|
23
24
|
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
24
25
|
* polls native every 500 ms and is unwanted in production builds.
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* (false comfort exactly where OOM happens). Falls back to 1500/2200 if the
|
|
19
19
|
* RAM read is unavailable.
|
|
20
20
|
*
|
|
21
|
-
* Backed by the `getMemoryFootprintMB()` native module (iOS:
|
|
22
|
-
* `phys_footprint`; Android: `/proc/self/statm` RSS
|
|
23
|
-
*
|
|
21
|
+
* Backed by the `getMemoryFootprintMB()` native module (iOS:
|
|
22
|
+
* `task_info(TASK_VM_INFO)` `phys_footprint`; Android: `/proc/self/statm` RSS
|
|
23
|
+
* — resident pages, unthrottled — the SAME number the C++ `[memstat]` logs
|
|
24
|
+
* report). Returns -1 if the native call fails.
|
|
24
25
|
*
|
|
25
26
|
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
26
27
|
* polls native every 500 ms and is unwanted in production builds.
|
package/dist/index.d.ts
CHANGED
|
@@ -91,7 +91,8 @@ export { IncrementalOutcome, incrementalStitcherIsAvailable, subscribeIncrementa
|
|
|
91
91
|
export type { IncrementalState, AcceptedKeyframe } from './stitching/incremental';
|
|
92
92
|
export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
|
|
93
93
|
export { useKeyframeStream } from './stitching/useKeyframeStream';
|
|
94
|
-
export type {
|
|
94
|
+
export type { CameraFrame, CameraFrameProcessor, ARAnchor, } from './stitching/CameraFrame';
|
|
95
|
+
export type { ARFrameMeta } from './stitching/ARFrameMeta';
|
|
95
96
|
export { useFrameProcessorDriver } from './stitching/useFrameProcessorDriver';
|
|
96
97
|
export type { UseFrameProcessorDriverOptions, FrameProcessorDriverHandle, } from './stitching/useFrameProcessorDriver';
|
|
97
98
|
export { useStitcherWorklet } from './stitching/useStitcherWorklet';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.18.0 — LIGHT per-frame AR metadata delivered to JS on the MAIN
|
|
3
|
+
* thread via a normal callback (`onArFrame`), bypassing worklets entirely.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a callback, NOT a worklet
|
|
6
|
+
*
|
|
7
|
+
* The AR worklet path (`arFrameProcessor` + the `__stitcherProxy` JSI
|
|
8
|
+
* registry) deep-copies the worklet's whole closure into the AR worklet
|
|
9
|
+
* runtime via react-native-worklets-core's `WorkletInvoker`. When the
|
|
10
|
+
* worklet captures a host object (e.g. a `createRunOnJS` callback) that
|
|
11
|
+
* closure-wrap recurses without termination → stack overflow → SIGBUS the
|
|
12
|
+
* instant AR mode mounts (verified on device). Worklets can therefore
|
|
13
|
+
* only safely capture a worklets-core *shared value* — an awkward,
|
|
14
|
+
* poll-from-JS pattern for getting structured data back.
|
|
15
|
+
*
|
|
16
|
+
* `onArFrame` sidesteps the whole problem: native builds the metadata and
|
|
17
|
+
* emits it as a plain `RNImageStitcherARFrame` device event carrying a
|
|
18
|
+
* JSON object; the JS side subscribes via `NativeEventEmitter` and invokes
|
|
19
|
+
* the host callback on the main thread. No worklet, no closure-wrap, no
|
|
20
|
+
* shared-value polling. This is the recommended way to read AR metadata.
|
|
21
|
+
*
|
|
22
|
+
* ## Cost / gating
|
|
23
|
+
*
|
|
24
|
+
* The metadata is intentionally LIGHT — no pixel / vertex / face byte
|
|
25
|
+
* marshaling. `depth` reports only the depth map's dimensions + whether a
|
|
26
|
+
* confidence channel exists (no buffer copy); `mesh` reports only anchor /
|
|
27
|
+
* vertex / face *counts*. Native gates each costly field on the matching
|
|
28
|
+
* extraction flag (`depth` ⇐ `enableDepth`, `mesh` ⇐ `enableMesh`,
|
|
29
|
+
* `anchors` ⇐ `enableAnchors`); `intrinsics` / `pose` / `trackingState` are
|
|
30
|
+
* always present. Emission is gated on a TS-set enabled flag (only true
|
|
31
|
+
* when `onArFrame` is provided) and throttled to `arFrameMetaInterval` ms
|
|
32
|
+
* (default 100 ≈ 10 Hz) on the native side.
|
|
33
|
+
*/
|
|
34
|
+
export interface ARFrameMeta {
|
|
35
|
+
/** Frame-capture timestamp in NANOSECONDS (AR-framework monotonic clock). */
|
|
36
|
+
timestamp: number;
|
|
37
|
+
/** AR tracking quality at this frame. */
|
|
38
|
+
trackingState: 'notAvailable' | 'limited' | 'normal';
|
|
39
|
+
/**
|
|
40
|
+
* Camera pose in world coordinates at frame-capture time.
|
|
41
|
+
*
|
|
42
|
+
* - `rotation` — quaternion `(x, y, z, w)`, matching the convention
|
|
43
|
+
* used throughout the engine + the `CameraFrame.pose` field.
|
|
44
|
+
* - `translation` — metres in world space `[x, y, z]`.
|
|
45
|
+
*/
|
|
46
|
+
pose: {
|
|
47
|
+
rotation: [number, number, number, number];
|
|
48
|
+
translation: [number, number, number];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Camera intrinsics for this frame — focal lengths (`fx`, `fy`) and
|
|
52
|
+
* principal point (`cx`, `cy`) in PIXELS at the `imageWidth × imageHeight`
|
|
53
|
+
* capture resolution. Always attempted (not gated); `null` only when the
|
|
54
|
+
* AR framework didn't provide them for this frame.
|
|
55
|
+
*/
|
|
56
|
+
intrinsics: {
|
|
57
|
+
fx: number;
|
|
58
|
+
fy: number;
|
|
59
|
+
cx: number;
|
|
60
|
+
cy: number;
|
|
61
|
+
imageWidth: number;
|
|
62
|
+
imageHeight: number;
|
|
63
|
+
} | null;
|
|
64
|
+
/**
|
|
65
|
+
* Depth-map summary — dimensions + whether a per-pixel confidence channel
|
|
66
|
+
* is available. NO pixel buffer is copied (that's the costly part).
|
|
67
|
+
* Present only when the `enableDepth` prop is on AND the device produced a
|
|
68
|
+
* depth map this frame; `null` otherwise.
|
|
69
|
+
*/
|
|
70
|
+
depth: {
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
hasConfidence: boolean;
|
|
74
|
+
} | null;
|
|
75
|
+
/**
|
|
76
|
+
* Tracked AR anchors visible in this frame (planes / images / points /
|
|
77
|
+
* mesh). Empty array when `enableAnchors` is on but nothing is tracked;
|
|
78
|
+
* effectively empty when `enableAnchors` is off. `transform` is a 4×4
|
|
79
|
+
* row-major anchor→world matrix (16 numbers).
|
|
80
|
+
*/
|
|
81
|
+
anchors: Array<{
|
|
82
|
+
id: string;
|
|
83
|
+
type: 'plane' | 'image' | 'point' | 'mesh';
|
|
84
|
+
alignment?: 'horizontal' | 'vertical';
|
|
85
|
+
extent?: [number, number];
|
|
86
|
+
classification?: string;
|
|
87
|
+
transform: number[];
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Scene-reconstruction mesh summary — anchor / vertex / face COUNTS only
|
|
91
|
+
* (no vertex / face byte marshaling). Present only when `enableMesh` is
|
|
92
|
+
* on; `null` otherwise.
|
|
93
|
+
*/
|
|
94
|
+
mesh: {
|
|
95
|
+
anchorCount: number;
|
|
96
|
+
vertexCount: number;
|
|
97
|
+
faceCount: number;
|
|
98
|
+
} | null;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=ARFrameMeta.d.ts.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* v0.8.0 — unified frame contract for the lib's worklet processor.
|
|
3
3
|
*
|
|
4
4
|
* Worklets registered via the v0.8.0 `useFrameProcessor` hook (also in
|
|
5
|
-
* this directory) receive a `
|
|
5
|
+
* this directory) receive a `CameraFrame` regardless of capture mode.
|
|
6
6
|
* The lib-owned worklet runtime guarantees the same JS-visible shape
|
|
7
7
|
* whether the underlying source is a vision-camera `Frame` (non-AR
|
|
8
8
|
* mode, sourced from the FP plugin) or an ARKit `ARFrame` / ARCore
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
* (Phase-0 audit confirmed the iOS path). But vision-camera's
|
|
17
17
|
* **Android** `Frame` is `androidx.camera.core.ImageProxy`-coupled —
|
|
18
18
|
* ARCore does NOT produce `ImageProxy` instances. Forcing
|
|
19
|
-
* `
|
|
19
|
+
* `CameraFrame extends Frame` would either (a) require reverse-
|
|
20
20
|
* engineering ImageProxy on Android (intractable + fragile), or
|
|
21
21
|
* (b) make the type asymmetric per platform. Both are worse than
|
|
22
|
-
* making `
|
|
22
|
+
* making `CameraFrame` a structural sibling type that vc Frames
|
|
23
23
|
* happen to satisfy (because vc Frames carry the same width / height /
|
|
24
24
|
* orientation / pixelFormat / timestamp / toArrayBuffer surface).
|
|
25
25
|
*
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* a JPEG-encode frame-processor plugin). Returning a reference and
|
|
37
37
|
* reading it later will read into freed memory.
|
|
38
38
|
*/
|
|
39
|
-
export interface
|
|
39
|
+
export interface CameraFrame {
|
|
40
40
|
/** Pixel width of the camera image. */
|
|
41
41
|
width: number;
|
|
42
42
|
/** Pixel height of the camera image. */
|
|
@@ -141,6 +141,27 @@ export interface StitcherFrame {
|
|
|
141
141
|
* tracking is degraded check this. Undefined in non-AR mode.
|
|
142
142
|
*/
|
|
143
143
|
arTrackingState?: 'notAvailable' | 'limited' | 'normal';
|
|
144
|
+
/**
|
|
145
|
+
* Camera intrinsics for THIS frame — focal lengths (`fx`,`fy`) and
|
|
146
|
+
* principal point (`cx`,`cy`) in PIXELS at the `imageWidth × imageHeight`
|
|
147
|
+
* capture resolution. Needed to lift 2D image-space coordinates to 3D
|
|
148
|
+
* via pose + intrinsics (e.g. object-level reconstruction).
|
|
149
|
+
*
|
|
150
|
+
* Populated on **AR frames** (`source: 'ar'`) from ARKit
|
|
151
|
+
* `ARCamera.intrinsics` / ARCore `Camera` intrinsics. **Undefined for
|
|
152
|
+
* non-AR (vision-camera) frames** — they are raw vc `Frame`s without an
|
|
153
|
+
* intrinsics surface; read vc's own APIs there if needed. (The spec
|
|
154
|
+
* called this required; it's optional here because the non-AR frame
|
|
155
|
+
* shape genuinely can't carry it.)
|
|
156
|
+
*/
|
|
157
|
+
intrinsics?: {
|
|
158
|
+
fx: number;
|
|
159
|
+
fy: number;
|
|
160
|
+
cx: number;
|
|
161
|
+
cy: number;
|
|
162
|
+
imageWidth: number;
|
|
163
|
+
imageHeight: number;
|
|
164
|
+
};
|
|
144
165
|
}
|
|
145
166
|
/**
|
|
146
167
|
* v0.8.0 — public AR anchor type. Subset of ARKit/ARCore anchor info
|
|
@@ -150,21 +171,59 @@ export interface StitcherFrame {
|
|
|
150
171
|
export interface ARAnchor {
|
|
151
172
|
/** Stable per-session anchor identifier. */
|
|
152
173
|
id: string;
|
|
153
|
-
/** Anchor kind. `'point'` is Android (ARCore) only. */
|
|
154
|
-
type: 'plane' | 'image' | 'point';
|
|
155
174
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
175
|
+
* Anchor kind. `'point'` is Android (ARCore) only; `'mesh'` is a
|
|
176
|
+
* scene-reconstruction mesh anchor, present only when the `enableMesh`
|
|
177
|
+
* `<Camera>` prop is on (and the device supports reconstruction).
|
|
178
|
+
*/
|
|
179
|
+
type: 'plane' | 'image' | 'point' | 'mesh';
|
|
180
|
+
/**
|
|
181
|
+
* 4×4 row-major transform from anchor space to world space (16
|
|
182
|
+
* numbers). For `'mesh'` anchors, the `meshGeometry.vertices` are in
|
|
183
|
+
* this anchor's LOCAL space — multiply by `transform` for world coords.
|
|
158
184
|
*/
|
|
159
185
|
transform: number[];
|
|
186
|
+
/**
|
|
187
|
+
* Plane orientation — `'horizontal'` (floor / table / seat) vs
|
|
188
|
+
* `'vertical'` (wall / door / window). Present on `'plane'` anchors;
|
|
189
|
+
* undefined for other anchor kinds. Lets a host distinguish a shelf
|
|
190
|
+
* surface from the wall behind it.
|
|
191
|
+
*/
|
|
192
|
+
alignment?: 'horizontal' | 'vertical';
|
|
193
|
+
/**
|
|
194
|
+
* Plane size in metres along its local x / z axes (`[x, z]`). Present
|
|
195
|
+
* on `'plane'` anchors only.
|
|
196
|
+
*/
|
|
197
|
+
extent?: [number, number];
|
|
198
|
+
/**
|
|
199
|
+
* ARKit semantic classification of the plane's surface, when the
|
|
200
|
+
* framework provides it (iOS; mostly horizontal planes). Undefined
|
|
201
|
+
* when unknown / unsupported (incl. Android, which has no equivalent).
|
|
202
|
+
*/
|
|
203
|
+
classification?: 'wall' | 'floor' | 'ceiling' | 'table' | 'seat' | 'door' | 'window' | 'none';
|
|
204
|
+
/**
|
|
205
|
+
* Scene-reconstruction geometry — present only on `type: 'mesh'`
|
|
206
|
+
* anchors. Buffers (wrap in the noted typed-array view):
|
|
207
|
+
* - `vertices` → `Float32Array`, xyz triplets in anchor-local space.
|
|
208
|
+
* - `faces` → `Uint32Array`, triangle indices into `vertices`.
|
|
209
|
+
* - `classifications` → optional `Uint8Array`, one ARKit mesh class
|
|
210
|
+
* per face (0=none, 1=wall, 2=floor, 3=ceiling, …). **iOS only**
|
|
211
|
+
* (from `ARMeshAnchor`); absent on Android, where the mesh is
|
|
212
|
+
* reconstructed from the depth map and carries no semantics.
|
|
213
|
+
*/
|
|
214
|
+
meshGeometry?: {
|
|
215
|
+
vertices: ArrayBuffer;
|
|
216
|
+
faces: ArrayBuffer;
|
|
217
|
+
classifications?: ArrayBuffer;
|
|
218
|
+
};
|
|
160
219
|
}
|
|
161
220
|
/**
|
|
162
221
|
* v0.8.0 — worklet function signature for the unified frame processor.
|
|
163
222
|
*
|
|
164
223
|
* Must be a `'worklet'`-prefixed function (so it can run on the
|
|
165
|
-
* worklet runtime). Receives a `
|
|
224
|
+
* worklet runtime). Receives a `CameraFrame` per camera frame; the
|
|
166
225
|
* return value is ignored (use `runOnJS` / shared values to surface
|
|
167
226
|
* results back to the JS thread).
|
|
168
227
|
*/
|
|
169
|
-
export type
|
|
170
|
-
//# sourceMappingURL=
|
|
228
|
+
export type CameraFrameProcessor = (frame: CameraFrame) => void;
|
|
229
|
+
//# sourceMappingURL=CameraFrame.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function ensureStitcherProxyInstalled(): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Test-only — reset module-internal state. Used by jest to allow
|
|
4
|
+
* multiple test cases to re-trigger the install path independently.
|
|
5
|
+
* NOT exported from `src/index.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function _resetStitcherProxyInstallStateForTests(): void;
|
|
8
|
+
//# sourceMappingURL=ensureStitcherProxyInstalled.d.ts.map
|