react-native-image-stitcher 0.17.0 → 0.19.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 +151 -0
- package/RNImageStitcher.podspec +1 -1
- package/android/src/main/cpp/CMakeLists.txt +4 -4
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +216 -7
- package/android/src/main/java/io/imagestitcher/rn/ARFrameContext.kt +89 -0
- package/android/src/main/java/io/imagestitcher/rn/ARFramePlugin.kt +57 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +831 -6
- package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +109 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +184 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +84 -2
- package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
- package/cpp/{stitcher_frame_jsi.cpp → camera_frame_jsi.cpp} +154 -11
- package/cpp/{stitcher_frame_jsi.hpp → camera_frame_jsi.hpp} +12 -12
- package/cpp/stitcher_proxy_jsi.cpp +31 -0
- package/cpp/stitcher_proxy_jsi.hpp +16 -0
- package/cpp/stitcher_worklet_dispatch.cpp +5 -5
- package/cpp/stitcher_worklet_dispatch.hpp +5 -5
- package/dist/camera/ARCameraView.d.ts +81 -3
- package/dist/camera/ARCameraView.js +103 -1
- package/dist/camera/Camera.d.ts +73 -7
- package/dist/camera/Camera.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/stitching/ARFrameMeta.d.ts +149 -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/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 +172 -2
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +108 -0
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +772 -0
- package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +247 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +418 -34
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +2 -2
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +4 -4
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +230 -5
- package/src/camera/Camera.tsx +91 -7
- package/src/index.ts +12 -3
- package/src/stitching/ARFrameMeta.ts +157 -0
- package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
- package/src/stitching/useStitcherWorklet.ts +9 -9
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
34
34
|
import {
|
|
35
|
+
NativeEventEmitter,
|
|
35
36
|
NativeModules,
|
|
36
37
|
Platform,
|
|
37
38
|
StyleSheet,
|
|
@@ -42,7 +43,8 @@ import {
|
|
|
42
43
|
} from 'react-native';
|
|
43
44
|
|
|
44
45
|
import { ensureStitcherProxyInstalled } from '../stitching/ensureStitcherProxyInstalled';
|
|
45
|
-
import type {
|
|
46
|
+
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
47
|
+
import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
// React Native looks up the component by its NATIVE name.
|
|
@@ -69,7 +71,7 @@ export interface ARCameraViewProps {
|
|
|
69
71
|
/**
|
|
70
72
|
* Optional host worklet invoked once per AR frame, ALONGSIDE the
|
|
71
73
|
* lib's first-party stitching (composition, not replacement). The
|
|
72
|
-
* worklet receives a `
|
|
74
|
+
* worklet receives a `CameraFrame` enriched with AR metadata —
|
|
73
75
|
* `source: 'ar'`, world-space `pose` (rotation + translation),
|
|
74
76
|
* `arTrackingState`, and (when supported) `arDepth` / `arAnchors`.
|
|
75
77
|
*
|
|
@@ -84,7 +86,87 @@ export interface ARCameraViewProps {
|
|
|
84
86
|
* different runtimes with different frame shapes, hence the separate
|
|
85
87
|
* prop.
|
|
86
88
|
*/
|
|
87
|
-
arFrameProcessor?:
|
|
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;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* v0.19.0 — ASYNCHRONOUS AR-plugin result callback, invoked on the JS MAIN
|
|
151
|
+
* thread (NOT a worklet). Part of the AR plugin framework: host-registered
|
|
152
|
+
* native plugins (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) can
|
|
153
|
+
* offload heavy per-frame work to their own queue and later push a result
|
|
154
|
+
* via `registry.emit(name, result)`. The SDK routes that to JS as a
|
|
155
|
+
* `RNImageStitcherARPluginResult` device event; when this prop is provided,
|
|
156
|
+
* this component subscribes and invokes the handler with
|
|
157
|
+
* `{ plugin, result }`.
|
|
158
|
+
*
|
|
159
|
+
* SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
|
|
160
|
+
* the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins} —
|
|
161
|
+
* read them there. This callback is ONLY for the out-of-band async channel.
|
|
162
|
+
*
|
|
163
|
+
* The subscription is independent of {@link onArFrame}: a host can read
|
|
164
|
+
* sync results via `onArFrame` and async results via `onArPluginResult`,
|
|
165
|
+
* either, or both. Wiring mirrors `onArFrame` exactly (latest handler held
|
|
166
|
+
* in a ref so the subscription effect depends only on whether a handler is
|
|
167
|
+
* present; cleanup on unmount / when the handler is removed).
|
|
168
|
+
*/
|
|
169
|
+
onArPluginResult?: (e: ARPluginResult) => void;
|
|
88
170
|
}
|
|
89
171
|
|
|
90
172
|
|
|
@@ -167,7 +249,18 @@ type RecordingCallbacks = {
|
|
|
167
249
|
|
|
168
250
|
export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
169
251
|
function ARCameraView(
|
|
170
|
-
{
|
|
252
|
+
{
|
|
253
|
+
style,
|
|
254
|
+
guidance,
|
|
255
|
+
arFrameProcessor,
|
|
256
|
+
enableDepth,
|
|
257
|
+
enableAnchors,
|
|
258
|
+
enableMesh,
|
|
259
|
+
planeDetection,
|
|
260
|
+
onArFrame,
|
|
261
|
+
arFrameMetaInterval,
|
|
262
|
+
onArPluginResult,
|
|
263
|
+
},
|
|
171
264
|
ref,
|
|
172
265
|
): React.JSX.Element {
|
|
173
266
|
// Held across the start→stop lifecycle so stopRecording's
|
|
@@ -189,7 +282,7 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
|
189
282
|
}
|
|
190
283
|
const proxy = (globalThis as {
|
|
191
284
|
__stitcherProxy?: {
|
|
192
|
-
install(fn:
|
|
285
|
+
install(fn: CameraFrameProcessor): string;
|
|
193
286
|
uninstall(id: string): void;
|
|
194
287
|
};
|
|
195
288
|
}).__stitcherProxy;
|
|
@@ -202,6 +295,138 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
|
202
295
|
};
|
|
203
296
|
}, [arFrameProcessor]);
|
|
204
297
|
|
|
298
|
+
// Push the AR-metadata extraction config to native — gates the
|
|
299
|
+
// costly per-frame depth / anchor / mesh work (all off by default).
|
|
300
|
+
// Routed through `__stitcherProxy.setExtractionConfig`, read by the
|
|
301
|
+
// platform AR extraction. iOS ADDITIONALLY toggles ARKit
|
|
302
|
+
// `sceneReconstruction` for mesh (a session-config change, not a
|
|
303
|
+
// per-frame gate); Android reconstructs mesh from the depth map and
|
|
304
|
+
// needs no session change.
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const depth = enableDepth === true;
|
|
307
|
+
const anchors = enableAnchors === true;
|
|
308
|
+
const mesh = enableMesh === true;
|
|
309
|
+
if (ensureStitcherProxyInstalled()) {
|
|
310
|
+
(globalThis as {
|
|
311
|
+
__stitcherProxy?: {
|
|
312
|
+
setExtractionConfig?(d: boolean, a: boolean, m: boolean): void;
|
|
313
|
+
};
|
|
314
|
+
}).__stitcherProxy?.setExtractionConfig?.(depth, anchors, mesh);
|
|
315
|
+
}
|
|
316
|
+
if (Platform.OS === 'ios') {
|
|
317
|
+
const session = (NativeModules as Record<string, unknown>)
|
|
318
|
+
.RNSARSession as
|
|
319
|
+
| { setSceneReconstructionEnabled?(on: boolean): void }
|
|
320
|
+
| undefined;
|
|
321
|
+
session?.setSceneReconstructionEnabled?.(mesh);
|
|
322
|
+
}
|
|
323
|
+
}, [enableDepth, enableAnchors, enableMesh]);
|
|
324
|
+
|
|
325
|
+
// Push the plane-detection mode to native. Unlike the extraction
|
|
326
|
+
// config above this is a SESSION setting, so it routes through the
|
|
327
|
+
// RNSARSession native module on BOTH platforms (iOS reconfigures
|
|
328
|
+
// ARKit `planeDetection`; Android stores an emission filter — see
|
|
329
|
+
// the prop docs). Defaults to `'vertical'` to preserve the
|
|
330
|
+
// plane-projected stitch path's long-standing behaviour.
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const mode = planeDetection ?? 'vertical';
|
|
333
|
+
const session = (NativeModules as Record<string, unknown>)
|
|
334
|
+
.RNSARSession as
|
|
335
|
+
| { setPlaneDetection?(mode: string): void }
|
|
336
|
+
| undefined;
|
|
337
|
+
session?.setPlaneDetection?.(mode);
|
|
338
|
+
}, [planeDetection]);
|
|
339
|
+
|
|
340
|
+
// v0.18.0 — onArFrame device-event wiring (worklet-free, main thread).
|
|
341
|
+
//
|
|
342
|
+
// The latest `onArFrame` is held in a ref so the subscription effect
|
|
343
|
+
// depends only on whether a handler is present + the interval — NOT on
|
|
344
|
+
// the handler's identity (which typically changes every render). This
|
|
345
|
+
// avoids tearing down + re-establishing the native event subscription
|
|
346
|
+
// (and the costly `setArFrameMetaEnabled(true)` extraction toggle) on
|
|
347
|
+
// every parent re-render.
|
|
348
|
+
const onArFrameRef = useRef<((meta: ARFrameMeta) => void) | undefined>(
|
|
349
|
+
onArFrame,
|
|
350
|
+
);
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
onArFrameRef.current = onArFrame;
|
|
353
|
+
}, [onArFrame]);
|
|
354
|
+
|
|
355
|
+
const arFrameEnabled = onArFrame != null;
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
if (!arFrameEnabled) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
const session = (NativeModules as Record<string, unknown>)
|
|
361
|
+
.RNSARSession as
|
|
362
|
+
| {
|
|
363
|
+
setArFrameMetaEnabled?(enabled: boolean, intervalMs: number): void;
|
|
364
|
+
}
|
|
365
|
+
| undefined;
|
|
366
|
+
if (session?.setArFrameMetaEnabled == null) {
|
|
367
|
+
// Native module / method unavailable (e.g. web, or a native build
|
|
368
|
+
// predating the event channel): no-op, no crash.
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
const intervalMs = arFrameMetaInterval ?? 100;
|
|
372
|
+
session.setArFrameMetaEnabled(true, intervalMs);
|
|
373
|
+
const emitter = new NativeEventEmitter(
|
|
374
|
+
NativeModules.RNSARSession as never,
|
|
375
|
+
);
|
|
376
|
+
const sub = emitter.addListener(
|
|
377
|
+
'RNImageStitcherARFrame',
|
|
378
|
+
(meta: ARFrameMeta) => {
|
|
379
|
+
onArFrameRef.current?.(meta);
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
return () => {
|
|
383
|
+
sub.remove();
|
|
384
|
+
session.setArFrameMetaEnabled?.(false, intervalMs);
|
|
385
|
+
};
|
|
386
|
+
}, [arFrameEnabled, arFrameMetaInterval]);
|
|
387
|
+
|
|
388
|
+
// v0.19.0 — onArPluginResult device-event wiring (worklet-free, main
|
|
389
|
+
// thread). Mirrors the onArFrame subscription above: the latest handler
|
|
390
|
+
// is held in a ref so the subscription effect depends only on WHETHER a
|
|
391
|
+
// handler is present, not its (per-render-changing) identity — so the
|
|
392
|
+
// native event subscription isn't torn down + re-established every render.
|
|
393
|
+
//
|
|
394
|
+
// This is a PURELY-JS subscription: unlike onArFrame there's no native
|
|
395
|
+
// "enable" toggle to flip. Native emits `RNImageStitcherARPluginResult`
|
|
396
|
+
// whenever a registered plugin calls `registry.emit(...)`; the registry is
|
|
397
|
+
// empty unless the host registered plugins, so an app with no plugins
|
|
398
|
+
// never sees an event even if this prop is wired.
|
|
399
|
+
const onArPluginResultRef = useRef<
|
|
400
|
+
((e: ARPluginResult) => void) | undefined
|
|
401
|
+
>(onArPluginResult);
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
onArPluginResultRef.current = onArPluginResult;
|
|
404
|
+
}, [onArPluginResult]);
|
|
405
|
+
|
|
406
|
+
const arPluginResultEnabled = onArPluginResult != null;
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
if (!arPluginResultEnabled) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
const native = (NativeModules as Record<string, unknown>)
|
|
412
|
+
.RNSARSession;
|
|
413
|
+
if (native == null) {
|
|
414
|
+
// Native module unavailable (e.g. web, or a native build predating
|
|
415
|
+
// the plugin event channel): no-op, no crash.
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
const emitter = new NativeEventEmitter(native as never);
|
|
419
|
+
const sub = emitter.addListener(
|
|
420
|
+
'RNImageStitcherARPluginResult',
|
|
421
|
+
(e: ARPluginResult) => {
|
|
422
|
+
onArPluginResultRef.current?.(e);
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
return () => {
|
|
426
|
+
sub.remove();
|
|
427
|
+
};
|
|
428
|
+
}, [arPluginResultEnabled]);
|
|
429
|
+
|
|
205
430
|
useImperativeHandle(ref, () => ({
|
|
206
431
|
takePhoto: async (options = {}) => {
|
|
207
432
|
const native: any =
|
package/src/camera/Camera.tsx
CHANGED
|
@@ -66,7 +66,8 @@ import type {
|
|
|
66
66
|
} from 'react-native-vision-camera';
|
|
67
67
|
|
|
68
68
|
import { useARSession } from '../ar/useARSession';
|
|
69
|
-
import type {
|
|
69
|
+
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
70
|
+
import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
|
|
70
71
|
import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
|
|
71
72
|
import { CameraShutter } from './CameraShutter';
|
|
72
73
|
import { CameraView } from './CameraView';
|
|
@@ -680,11 +681,11 @@ export interface CameraProps {
|
|
|
680
681
|
* worklet to fire on vc's Frame Processor runtime.
|
|
681
682
|
*
|
|
682
683
|
* ```tsx
|
|
683
|
-
* import { Camera, useFrameProcessor, type
|
|
684
|
+
* import { Camera, useFrameProcessor, type CameraFrame }
|
|
684
685
|
* from 'react-native-image-stitcher';
|
|
685
686
|
*
|
|
686
687
|
* function MyScreen() {
|
|
687
|
-
* const fp = useFrameProcessor((frame:
|
|
688
|
+
* const fp = useFrameProcessor((frame: CameraFrame) => {
|
|
688
689
|
* 'worklet';
|
|
689
690
|
* // ...
|
|
690
691
|
* }, []);
|
|
@@ -705,12 +706,12 @@ export interface CameraProps {
|
|
|
705
706
|
* ```tsx
|
|
706
707
|
* import {
|
|
707
708
|
* Camera, useFrameProcessor, useStitcherWorklet,
|
|
708
|
-
* type
|
|
709
|
+
* type CameraFrame,
|
|
709
710
|
* } from 'react-native-image-stitcher';
|
|
710
711
|
*
|
|
711
712
|
* function MyScreen() {
|
|
712
713
|
* const stitcher = useStitcherWorklet();
|
|
713
|
-
* const fp = useFrameProcessor((frame:
|
|
714
|
+
* const fp = useFrameProcessor((frame: CameraFrame) => {
|
|
714
715
|
* 'worklet';
|
|
715
716
|
* hostPreLogic(frame);
|
|
716
717
|
* stitcher.call(frame); // ← first-party stitching
|
|
@@ -755,14 +756,83 @@ export interface CameraProps {
|
|
|
755
756
|
/**
|
|
756
757
|
* AR-mode host worklet, invoked once per ARKit / ARCore frame
|
|
757
758
|
* ALONGSIDE the lib's first-party stitching (composition, not
|
|
758
|
-
* replacement). Receives a `
|
|
759
|
+
* replacement). Receives a `CameraFrame` tagged `source: 'ar'`
|
|
759
760
|
* with world-space `pose` + `arTrackingState`. Only fires in AR
|
|
760
761
|
* capture (`captureSource === 'ar'`); the non-AR equivalent is
|
|
761
762
|
* `frameProcessor` above (the two modes use different runtimes and
|
|
762
763
|
* frame shapes). Must be a `'worklet'`-prefixed function; if the
|
|
763
764
|
* native install is unavailable it silently never fires.
|
|
764
765
|
*/
|
|
765
|
-
arFrameProcessor?:
|
|
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
|
+
|
|
817
|
+
/**
|
|
818
|
+
* v0.19.0 — ASYNCHRONOUS AR-plugin result callback (the AR plugin
|
|
819
|
+
* framework), invoked on the JS MAIN thread (NOT a worklet). Only fires in
|
|
820
|
+
* AR capture (`captureSource === 'ar'`). Host-registered native plugins
|
|
821
|
+
* (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) that offload heavy
|
|
822
|
+
* per-frame work to their own queue push results via
|
|
823
|
+
* `registry.emit(name, result)`; `<Camera>` threads this handler to
|
|
824
|
+
* `<ARCameraView>`, which subscribes to the `RNImageStitcherARPluginResult`
|
|
825
|
+
* device event and invokes it with `{ plugin, result }`.
|
|
826
|
+
*
|
|
827
|
+
* SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
|
|
828
|
+
* the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins}.
|
|
829
|
+
* Use `onArFrame` for the in-band sync channel and `onArPluginResult` for
|
|
830
|
+
* the out-of-band async channel — a host can wire either or both.
|
|
831
|
+
*
|
|
832
|
+
* The SDK ships ONLY the generic plugin framework; there are no built-in
|
|
833
|
+
* plugins, so this never fires unless the host registers native plugins.
|
|
834
|
+
*/
|
|
835
|
+
onArPluginResult?: (e: ARPluginResult) => void;
|
|
766
836
|
|
|
767
837
|
// ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────────
|
|
768
838
|
/**
|
|
@@ -1171,6 +1241,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1171
1241
|
onCapturePreviewClose,
|
|
1172
1242
|
frameProcessor: hostFrameProcessor,
|
|
1173
1243
|
arFrameProcessor,
|
|
1244
|
+
enableDepth,
|
|
1245
|
+
enableAnchors,
|
|
1246
|
+
enableMesh,
|
|
1247
|
+
planeDetection,
|
|
1248
|
+
onArFrame,
|
|
1249
|
+
arFrameMetaInterval,
|
|
1250
|
+
onArPluginResult,
|
|
1174
1251
|
engine = 'batch-keyframe',
|
|
1175
1252
|
// ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
|
|
1176
1253
|
panMode = 'vertical',
|
|
@@ -2435,6 +2512,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
2435
2512
|
ref={arViewRef}
|
|
2436
2513
|
style={StyleSheet.absoluteFill}
|
|
2437
2514
|
arFrameProcessor={arFrameProcessor}
|
|
2515
|
+
enableDepth={enableDepth}
|
|
2516
|
+
enableAnchors={enableAnchors}
|
|
2517
|
+
enableMesh={enableMesh}
|
|
2518
|
+
planeDetection={planeDetection}
|
|
2519
|
+
onArFrame={onArFrame}
|
|
2520
|
+
arFrameMetaInterval={arFrameMetaInterval}
|
|
2521
|
+
onArPluginResult={onArPluginResult}
|
|
2438
2522
|
/>
|
|
2439
2523
|
) : (
|
|
2440
2524
|
<CameraView
|
package/src/index.ts
CHANGED
|
@@ -259,10 +259,19 @@ 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. v0.19.0 adds `plugins` (sync results from
|
|
269
|
+
// host-registered AR plugins ride this same throttled event).
|
|
270
|
+
export type { ARFrameMeta } from './stitching/ARFrameMeta';
|
|
271
|
+
// v0.19.0 — the AR plugin framework's ASYNC result type, delivered via the
|
|
272
|
+
// `onArPluginResult` callback (a plugin's out-of-band `registry.emit(...)`
|
|
273
|
+
// result). The SDK ships only the generic framework — no built-in plugins.
|
|
274
|
+
export type { ARPluginResult } from './stitching/ARFrameMeta';
|
|
266
275
|
// NOTE: the host-worklet / frame-stream hooks `useFrameProcessor`,
|
|
267
276
|
// `useThrottledFrameProcessor` and `useFrameStream` (v0.8–v0.9) were
|
|
268
277
|
// archived in the batch-keyframe cleanup — they drove the third-party
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
|
|
108
|
+
/**
|
|
109
|
+
* v0.19.0 — SYNCHRONOUS results from host-registered AR frame plugins
|
|
110
|
+
* (the AR plugin framework). Keyed by each plugin's `name()`; the value
|
|
111
|
+
* is the light JSON result the plugin's `process(ctx)` returned on the AR
|
|
112
|
+
* thread (`nil`/`null`-returning plugins are omitted). Only present when
|
|
113
|
+
* the native plugin registry is non-empty AND at least one plugin returned
|
|
114
|
+
* a sync result for this frame; otherwise omitted entirely (zero-plugin
|
|
115
|
+
* apps pay nothing — native skips building the context).
|
|
116
|
+
*
|
|
117
|
+
* The SDK ships ONLY the generic framework — there are no built-in
|
|
118
|
+
* plugins. Hosts register native plugins via `RNISARPluginRegistry`
|
|
119
|
+
* (iOS) / `RNSARPluginRegistry` (Android) at startup; each plugin is
|
|
120
|
+
* called once per AR frame while the registry is non-empty. Result
|
|
121
|
+
* values are `unknown` because each plugin defines its own shape — cast
|
|
122
|
+
* after reading the entry you care about (e.g.
|
|
123
|
+
* `meta.plugins?.brightness as number`).
|
|
124
|
+
*
|
|
125
|
+
* ## Sync vs async results
|
|
126
|
+
*
|
|
127
|
+
* This field carries only the LIGHT, in-band SYNC results (computed fast
|
|
128
|
+
* enough to ride the throttled `onArFrame` event). Plugins that offload
|
|
129
|
+
* heavy work to their own queue deliver results out-of-band via
|
|
130
|
+
* `registry.emit(name, result)`, which surfaces through the separate
|
|
131
|
+
* `onArPluginResult` callback (the `RNImageStitcherARPluginResult`
|
|
132
|
+
* event) — NOT here.
|
|
133
|
+
*/
|
|
134
|
+
plugins?: { [name: string]: unknown };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* v0.19.0 — an ASYNCHRONOUS result from a host-registered AR frame plugin,
|
|
140
|
+
* delivered via the `onArPluginResult` callback.
|
|
141
|
+
*
|
|
142
|
+
* Unlike the in-band SYNC results carried on {@link ARFrameMeta.plugins}
|
|
143
|
+
* (which ride the throttled `onArFrame` event), a plugin produces an async
|
|
144
|
+
* result by offloading heavy work to its own queue and later calling
|
|
145
|
+
* `registry.emit(name, result)` on the native side. The SDK routes that to
|
|
146
|
+
* JS as a `RNImageStitcherARPluginResult` device event; `<ARCameraView>`
|
|
147
|
+
* subscribes and invokes `onArPluginResult` on the JS MAIN thread.
|
|
148
|
+
*
|
|
149
|
+
* `result` is `unknown` because each plugin defines its own result shape —
|
|
150
|
+
* branch on `plugin` (the emitting plugin's `name()`) and cast accordingly.
|
|
151
|
+
*/
|
|
152
|
+
export interface ARPluginResult {
|
|
153
|
+
/** The `name()` of the plugin that emitted this result. */
|
|
154
|
+
plugin: string;
|
|
155
|
+
/** The plugin-defined result payload (cast after branching on `plugin`). */
|
|
156
|
+
result: unknown;
|
|
157
|
+
}
|