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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
package io.imagestitcher.rn
|
|
3
|
+
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
6
|
+
import com.facebook.react.bridge.WritableMap
|
|
7
|
+
import java.util.concurrent.CopyOnWriteArrayList
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 0.19.0 — process-wide registry of [ARFramePlugin]s (iOS twin:
|
|
11
|
+
* `RNISARPluginRegistry`).
|
|
12
|
+
*
|
|
13
|
+
* The host app registers native plugins HERE at startup (typically from its
|
|
14
|
+
* `MainApplication.onCreate`); the SDK's AR per-frame path
|
|
15
|
+
* ([RNSARCameraView.forwardToIncremental]) reads [plugins] each frame and,
|
|
16
|
+
* when the list is non-empty, builds one [ARFrameContext] and calls every
|
|
17
|
+
* plugin's [ARFramePlugin.process].
|
|
18
|
+
*
|
|
19
|
+
* Doubles as the **async result channel**: a plugin that returned `null`
|
|
20
|
+
* from `process` (deferring heavy work to its own queue) calls [emit] later
|
|
21
|
+
* to deliver its result to JS over the `RNImageStitcherARPluginResult`
|
|
22
|
+
* device event.
|
|
23
|
+
*
|
|
24
|
+
* ## Threading
|
|
25
|
+
*
|
|
26
|
+
* Backed by a [CopyOnWriteArrayList], so [register] / [unregister] (usually
|
|
27
|
+
* on the main thread at startup) never race the AR thread's [plugins] read.
|
|
28
|
+
* [emit] is safe from any thread — it routes through the singleton
|
|
29
|
+
* [RNSARSession]'s React context, which guards a torn-down Catalyst
|
|
30
|
+
* instance internally.
|
|
31
|
+
*/
|
|
32
|
+
object RNSARPluginRegistry {
|
|
33
|
+
|
|
34
|
+
private const val TAG = "RNSARPluginRegistry"
|
|
35
|
+
|
|
36
|
+
/// Event name carrying an ASYNC plugin result to JS. MUST match the
|
|
37
|
+
/// iOS `supportedEvents` entry + the TS `NativeEventEmitter`
|
|
38
|
+
/// subscription string exactly.
|
|
39
|
+
const val AR_PLUGIN_RESULT_EVENT = "RNImageStitcherARPluginResult"
|
|
40
|
+
|
|
41
|
+
private val registered = CopyOnWriteArrayList<ARFramePlugin>()
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register a plugin. Idempotent by [ARFramePlugin.name]: registering a
|
|
45
|
+
* plugin whose name matches an existing one REPLACES the old instance
|
|
46
|
+
* (so a host re-registering on a JS reload doesn't accumulate
|
|
47
|
+
* duplicates). Safe to call from any thread.
|
|
48
|
+
*/
|
|
49
|
+
@JvmStatic
|
|
50
|
+
fun register(plugin: ARFramePlugin) {
|
|
51
|
+
val name = plugin.name()
|
|
52
|
+
// Drop any prior plugin with the same name, then add the new one.
|
|
53
|
+
registered.removeAll { it.name() == name }
|
|
54
|
+
registered.add(plugin)
|
|
55
|
+
Log.i(TAG, "register: '$name' (now ${registered.size} plugin(s))")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Unregister the plugin with the given [name] (no-op if none match).
|
|
60
|
+
* Safe to call from any thread.
|
|
61
|
+
*/
|
|
62
|
+
@JvmStatic
|
|
63
|
+
fun unregister(name: String) {
|
|
64
|
+
val removed = registered.removeAll { it.name() == name }
|
|
65
|
+
if (removed) Log.i(TAG, "unregister: '$name' (now ${registered.size} plugin(s))")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Snapshot of the currently-registered plugins. Read once per AR frame
|
|
70
|
+
* by the SDK; the [CopyOnWriteArrayList] makes iteration race-free
|
|
71
|
+
* against concurrent [register] / [unregister].
|
|
72
|
+
*/
|
|
73
|
+
@JvmStatic
|
|
74
|
+
fun plugins(): List<ARFramePlugin> = registered
|
|
75
|
+
|
|
76
|
+
/** Cheap fast-path read for the AR thread: "do we have any plugins?". */
|
|
77
|
+
@JvmStatic
|
|
78
|
+
fun isEmpty(): Boolean = registered.isEmpty()
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Emit an ASYNC plugin result to JS over the
|
|
82
|
+
* `RNImageStitcherARPluginResult` device event.
|
|
83
|
+
*
|
|
84
|
+
* Event body: `{ plugin: <pluginName>, result: <result> }` — the same
|
|
85
|
+
* shape the TS `onArPluginResult` prop consumes. Routes through the
|
|
86
|
+
* singleton [RNSARSession]'s `DeviceEventManagerModule` emitter (the
|
|
87
|
+
* SAME channel `onArFrame` uses), so RN drops the event when no JS
|
|
88
|
+
* listener is attached and a torn-down Catalyst instance is swallowed
|
|
89
|
+
* silently.
|
|
90
|
+
*
|
|
91
|
+
* Safe from any thread (the plugin's own work queue, typically).
|
|
92
|
+
*
|
|
93
|
+
* @param pluginName the emitting plugin's [ARFramePlugin.name].
|
|
94
|
+
* @param result the plugin's result map (consumed by JS verbatim).
|
|
95
|
+
*/
|
|
96
|
+
@JvmStatic
|
|
97
|
+
fun emit(pluginName: String, result: WritableMap) {
|
|
98
|
+
val session = RNSARSession.instance
|
|
99
|
+
if (session == null) {
|
|
100
|
+
Log.d(TAG, "emit('$pluginName'): no RNSARSession instance yet — dropping")
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
val body: WritableMap = Arguments.createMap().apply {
|
|
104
|
+
putString("plugin", pluginName)
|
|
105
|
+
putMap("result", result)
|
|
106
|
+
}
|
|
107
|
+
session.emitArPluginResult(body)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -120,6 +120,72 @@ class RNSARSession(reactContext: ReactApplicationContext)
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Android side of the JS `<Camera planeDetection=...>` prop. Sets
|
|
125
|
+
* which plane orientations are EMITTED into `arAnchors`
|
|
126
|
+
* (`"vertical"` | `"horizontal"` | `"both"`); an unrecognised value
|
|
127
|
+
* falls back to `"vertical"`.
|
|
128
|
+
*
|
|
129
|
+
* Unlike iOS — where the prop narrows ARKit's `planeDetection`
|
|
130
|
+
* option set — we deliberately do NOT narrow ARCore's
|
|
131
|
+
* `planeFindingMode`: it stays `HORIZONTAL_AND_VERTICAL` so ARCore
|
|
132
|
+
* keeps bootstrapping tracking from horizontal planes (a plain
|
|
133
|
+
* vertical wall alone leaves ARCore unable to establish a
|
|
134
|
+
* gravity-aligned world; see the start()/startForView() config
|
|
135
|
+
* comments). We only FILTER which plane orientations are surfaced
|
|
136
|
+
* into `arAnchors` (in [RNSARCameraView.collectTrackingAnchors]).
|
|
137
|
+
*
|
|
138
|
+
* Void (no Promise): a fire-and-forget setter mirroring the other
|
|
139
|
+
* config-prop bridge calls.
|
|
140
|
+
*/
|
|
141
|
+
@ReactMethod
|
|
142
|
+
fun setPlaneDetection(mode: String) {
|
|
143
|
+
planeDetectionMode = when (mode) {
|
|
144
|
+
"vertical", "horizontal", "both" -> mode
|
|
145
|
+
else -> "vertical"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── onArFrame — LIGHT AR metadata channel (v0.18.0) ───────────────
|
|
150
|
+
//
|
|
151
|
+
// Android side of the shared `onArFrame` contract. Delivers light
|
|
152
|
+
// AR metadata (tracking state, pose, intrinsics, depth dims, anchor
|
|
153
|
+
// descriptors, mesh counts) to JS on the main thread as a normal
|
|
154
|
+
// RCTDeviceEventEmitter event — NO worklets, NO zero-copy buffers.
|
|
155
|
+
//
|
|
156
|
+
// This is the deliberate counterpart to the `arFrameProcessor`
|
|
157
|
+
// host-worklet path: worklets-core's closure-wrap crashes when an
|
|
158
|
+
// AR worklet captures host objects, so `onArFrame` bypasses worklets
|
|
159
|
+
// entirely and uses the bridge event channel the rest of this module
|
|
160
|
+
// already uses (see `IncrementalStitcher.emitState`).
|
|
161
|
+
//
|
|
162
|
+
// TS sets the gate via `setArFrameMetaEnabled(true, intervalMs)` when
|
|
163
|
+
// a host passes the `onArFrame` prop, and `setArFrameMetaEnabled(false, _)`
|
|
164
|
+
// on unmount / prop removal. The per-frame build + emit happens in
|
|
165
|
+
// `RNSARCameraView.onDrawFrame` (the only thread guaranteed to run
|
|
166
|
+
// once the AR session is live), gated + throttled by the companion
|
|
167
|
+
// state set here.
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* JS-facing gate for the `onArFrame` metadata channel.
|
|
171
|
+
*
|
|
172
|
+
* - `enabled` — true while a host supplies the `onArFrame` prop.
|
|
173
|
+
* - `intervalMs` — throttle floor in milliseconds (default contract
|
|
174
|
+
* value 100 ≈ 10 Hz). Clamped to ≥ 0; a 0 interval
|
|
175
|
+
* means "emit every render frame" (no throttle).
|
|
176
|
+
*
|
|
177
|
+
* Fire-and-forget (no Promise) — mirrors the other config-prop bridge
|
|
178
|
+
* setters (`setPlaneDetection`, `lockPortrait`).
|
|
179
|
+
*/
|
|
180
|
+
@ReactMethod
|
|
181
|
+
fun setArFrameMetaEnabled(enabled: Boolean, intervalMs: Double) {
|
|
182
|
+
arFrameMetaEnabled = enabled
|
|
183
|
+
arFrameMetaIntervalMs = if (intervalMs.isNaN()) 100L else intervalMs.toLong().coerceAtLeast(0L)
|
|
184
|
+
// Reset the throttle clock so the first frame after enabling emits
|
|
185
|
+
// immediately rather than waiting out a stale interval window.
|
|
186
|
+
arFrameMetaLastEmitNs = 0L
|
|
187
|
+
}
|
|
188
|
+
|
|
123
189
|
@ReactMethod
|
|
124
190
|
fun isSupported(promise: Promise) {
|
|
125
191
|
// `checkAvailability` can return UNKNOWN_CHECKING if the
|
|
@@ -832,6 +898,70 @@ class RNSARSession(reactContext: ReactApplicationContext)
|
|
|
832
898
|
if (attachedView === view) attachedView = null
|
|
833
899
|
}
|
|
834
900
|
|
|
901
|
+
/**
|
|
902
|
+
* Emit a pre-built [ARFrameMeta] WritableMap to JS over the shared
|
|
903
|
+
* `RNImageStitcherARFrame` device event. Called from
|
|
904
|
+
* [RNSARCameraView.maybeEmitArFrameMeta] on the GL render thread.
|
|
905
|
+
*
|
|
906
|
+
* Uses the same `DeviceEventManagerModule.RCTDeviceEventEmitter`
|
|
907
|
+
* channel as [IncrementalStitcher.emitState] — RN drops the event
|
|
908
|
+
* when no JS listener is attached, so no extra gating is needed
|
|
909
|
+
* beyond the enabled flag the caller already checked. The
|
|
910
|
+
* TS-required `addListener`/`removeListeners` no-op pair already
|
|
911
|
+
* exists on the `IncrementalStitcher` module; the `NativeEventEmitter`
|
|
912
|
+
* the TS layer constructs over `RNSARSession` needs the same pair, so
|
|
913
|
+
* they're declared below.
|
|
914
|
+
*/
|
|
915
|
+
internal fun emitArFrameMeta(meta: com.facebook.react.bridge.WritableMap) {
|
|
916
|
+
try {
|
|
917
|
+
reactApplicationContext
|
|
918
|
+
.getJSModule(
|
|
919
|
+
com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java,
|
|
920
|
+
)
|
|
921
|
+
.emit(AR_FRAME_META_EVENT, meta)
|
|
922
|
+
} catch (t: Throwable) {
|
|
923
|
+
// Catalyst instance torn down mid-emit (reload / unmount race),
|
|
924
|
+
// or no JS context yet — drop the frame silently. AR metadata
|
|
925
|
+
// is best-effort and re-emitted every interval, so a dropped
|
|
926
|
+
// frame is harmless.
|
|
927
|
+
Log.d(TAG, "emitArFrameMeta: emit failed (ignoring): ${t.message}")
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Emit a pre-built ASYNC plugin-result body to JS over the
|
|
933
|
+
* `RNImageStitcherARPluginResult` device event (0.19.0). Called from
|
|
934
|
+
* [RNSARPluginRegistry.emit] — the body is `{ plugin, result }`.
|
|
935
|
+
*
|
|
936
|
+
* Reuses the SAME `DeviceEventManagerModule.RCTDeviceEventEmitter`
|
|
937
|
+
* channel as [emitArFrameMeta]; RN drops the event when no JS listener
|
|
938
|
+
* is attached, and a torn-down Catalyst instance is swallowed silently
|
|
939
|
+
* (plugin results are best-effort).
|
|
940
|
+
*/
|
|
941
|
+
internal fun emitArPluginResult(body: com.facebook.react.bridge.WritableMap) {
|
|
942
|
+
try {
|
|
943
|
+
reactApplicationContext
|
|
944
|
+
.getJSModule(
|
|
945
|
+
com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java,
|
|
946
|
+
)
|
|
947
|
+
.emit(AR_PLUGIN_RESULT_EVENT, body)
|
|
948
|
+
} catch (t: Throwable) {
|
|
949
|
+
Log.d(TAG, "emitArPluginResult: emit failed (ignoring): ${t.message}")
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/// Required by RN's `NativeEventEmitter` contract — the TS
|
|
954
|
+
/// `onArFrame` wiring constructs a `NativeEventEmitter` over this
|
|
955
|
+
/// module, which calls `addListener`/`removeListeners` on subscribe /
|
|
956
|
+
/// unsubscribe. No-op on Android: `DeviceEventManagerModule` does its
|
|
957
|
+
/// own listener tracking and drops events when none are attached
|
|
958
|
+
/// (same rationale as `IncrementalStitcher.addListener`).
|
|
959
|
+
@ReactMethod
|
|
960
|
+
fun addListener(eventName: String) { /* no-op — see KDoc */ }
|
|
961
|
+
|
|
962
|
+
@ReactMethod
|
|
963
|
+
fun removeListeners(count: Int) { /* no-op — see KDoc */ }
|
|
964
|
+
|
|
835
965
|
private fun clearPoseLogInternal() {
|
|
836
966
|
poseLogLock.write { poseLog.clear() }
|
|
837
967
|
}
|
|
@@ -915,6 +1045,60 @@ class RNSARSession(reactContext: ReactApplicationContext)
|
|
|
915
1045
|
@Volatile
|
|
916
1046
|
var instance: RNSARSession? = null
|
|
917
1047
|
private set
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Which plane orientations reach `arAnchors`:
|
|
1051
|
+
* `"vertical"` | `"horizontal"` | `"both"`.
|
|
1052
|
+
*
|
|
1053
|
+
* Default `"vertical"` preserves the legacy plane-projected
|
|
1054
|
+
* stitch path (the shutter-gate / evaluatePlanesForFrame logic
|
|
1055
|
+
* only ever cared about vertical planes), so existing hosts see
|
|
1056
|
+
* no change unless they opt into a wider filter via the JS
|
|
1057
|
+
* `<Camera planeDetection=...>` prop (→ [setPlaneDetection]).
|
|
1058
|
+
*
|
|
1059
|
+
* Read on the GL render thread in
|
|
1060
|
+
* [RNSARCameraView.collectTrackingAnchors]; written from the JS
|
|
1061
|
+
* thread via [setPlaneDetection] — hence `@Volatile`.
|
|
1062
|
+
*/
|
|
1063
|
+
@JvmStatic
|
|
1064
|
+
@Volatile
|
|
1065
|
+
var planeDetectionMode: String = "vertical"
|
|
1066
|
+
|
|
1067
|
+
// ── onArFrame gate + throttle (v0.18.0) ──────────────────────
|
|
1068
|
+
//
|
|
1069
|
+
// Written from the JS thread via [setArFrameMetaEnabled];
|
|
1070
|
+
// read on the GL render thread in
|
|
1071
|
+
// [RNSARCameraView.maybeEmitArFrameMeta] — hence `@Volatile`.
|
|
1072
|
+
//
|
|
1073
|
+
// - `arFrameMetaEnabled` — gate: only build+emit when true.
|
|
1074
|
+
// - `arFrameMetaIntervalMs` — throttle floor (ms); contract
|
|
1075
|
+
// default 100 (≈10 Hz).
|
|
1076
|
+
// - `arFrameMetaLastEmitNs` — monotonic clock of the last emit
|
|
1077
|
+
// (System.nanoTime()); 0 = "never",
|
|
1078
|
+
// reset on every enable so the first
|
|
1079
|
+
// post-enable frame emits at once.
|
|
1080
|
+
@JvmStatic
|
|
1081
|
+
@Volatile
|
|
1082
|
+
var arFrameMetaEnabled: Boolean = false
|
|
1083
|
+
|
|
1084
|
+
@JvmStatic
|
|
1085
|
+
@Volatile
|
|
1086
|
+
var arFrameMetaIntervalMs: Long = 100L
|
|
1087
|
+
|
|
1088
|
+
@JvmStatic
|
|
1089
|
+
@Volatile
|
|
1090
|
+
var arFrameMetaLastEmitNs: Long = 0L
|
|
1091
|
+
|
|
1092
|
+
/// Event name carrying the [ARFrameMeta] payload to JS. MUST
|
|
1093
|
+
/// match the shared contract + the iOS `supportedEvents` entry +
|
|
1094
|
+
/// the TS `NativeEventEmitter` subscription string exactly.
|
|
1095
|
+
const val AR_FRAME_META_EVENT = "RNImageStitcherARFrame"
|
|
1096
|
+
|
|
1097
|
+
/// Event name carrying an ASYNC plugin result to JS (0.19.0).
|
|
1098
|
+
/// MUST match [RNSARPluginRegistry.AR_PLUGIN_RESULT_EVENT], the
|
|
1099
|
+
/// iOS `supportedEvents` entry, and the TS subscription string.
|
|
1100
|
+
const val AR_PLUGIN_RESULT_EVENT =
|
|
1101
|
+
RNSARPluginRegistry.AR_PLUGIN_RESULT_EVENT
|
|
918
1102
|
}
|
|
919
1103
|
|
|
920
1104
|
init {
|
|
@@ -40,7 +40,7 @@ import com.facebook.react.bridge.ReactMethod
|
|
|
40
40
|
* `retailens::StitcherWorkletRegistry`. Per-frame fan-out from
|
|
41
41
|
* Android's `StitcherWorkletRuntime` is a separate piece of work
|
|
42
42
|
* (Phase 4b.ii follow-up) — needs the Kotlin↔JNI bridge that
|
|
43
|
-
* constructs a `
|
|
43
|
+
* constructs a `CameraFrameJsiHostObject` from an `ArImage` +
|
|
44
44
|
* pose and posts it through a worklet runtime. Until that lands,
|
|
45
45
|
* Android-registered worklets behave exactly like iOS-registered
|
|
46
46
|
* worklets BEFORE Phase 4b.i: they exist in the registry but
|
|
@@ -18,7 +18,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|
|
18
18
|
* the GLSurfaceView GL render thread (audit caveat #4 from the
|
|
19
19
|
* Phase-0 audit).
|
|
20
20
|
* - `dispatchFrame()` stub — Phase 3c will fill in:
|
|
21
|
-
* 1. Build `
|
|
21
|
+
* 1. Build `CameraFrameHostObject` from ARCore Frame + pose
|
|
22
22
|
* (via the shared C++ JSI host object now linked into
|
|
23
23
|
* `libimage_stitcher.so` post-Phase-3a).
|
|
24
24
|
* 2. Run first-party stitching synchronously on the caller
|
|
@@ -165,7 +165,7 @@ object StitcherWorkletRuntime {
|
|
|
165
165
|
/// **When host worklets ARE registered:** the JNI layer copies
|
|
166
166
|
/// the NV21 byte array into an owned C++ `std::vector` (so the
|
|
167
167
|
/// async dispatch can outlive ARCore's `Image.close()` scope),
|
|
168
|
-
/// builds a `
|
|
168
|
+
/// builds a `CameraFrameJsiHostObject`, and posts a lambda
|
|
169
169
|
/// onto worklets-core's default `JsiWorkletContext`'s worklet
|
|
170
170
|
/// thread. The lambda iterates the registry's
|
|
171
171
|
/// `WorkletInvoker`s, calls each with the JSI host object as
|
|
@@ -196,6 +196,26 @@ object StitcherWorkletRuntime {
|
|
|
196
196
|
/// @param trackingState One of "" / "notAvailable" / "limited"
|
|
197
197
|
/// / "normal". Empty string ⇒ JS-side
|
|
198
198
|
/// `arTrackingState` is `undefined`.
|
|
199
|
+
/// @param depthBytes Raw ARCore DEPTH16 bytes, dense row-packed
|
|
200
|
+
/// (`depthWidth*depthHeight*2` bytes, uint16/px,
|
|
201
|
+
/// low 13 bits = mm, high 3 bits = confidence).
|
|
202
|
+
/// `null` when depth is unavailable this frame —
|
|
203
|
+
/// the JNI then leaves `data.arDepth == nullopt`.
|
|
204
|
+
/// @param depthWidth Depth-map width (px); 0 when no depth.
|
|
205
|
+
/// @param depthHeight Depth-map height (px); 0 when no depth.
|
|
206
|
+
/// @param anchorIds Parallel arrays describing every TRACKING
|
|
207
|
+
/// @param anchorTypes anchor: stable id, coarse type
|
|
208
|
+
/// @param anchorTransforms ("plane"/"image"/"point"/"mesh"), and a
|
|
209
|
+
/// 16-element ROW-MAJOR (anchor->world) transform
|
|
210
|
+
/// (identity for the depth-derived "mesh" anchor —
|
|
211
|
+
/// its vertices are camera-local). Empty when no
|
|
212
|
+
/// anchors/mesh were collected.
|
|
213
|
+
/// @param anchorMeshVertices Parallel per-anchor mesh byte arrays: a
|
|
214
|
+
/// @param anchorMeshFaces Float32-xyz vertex buffer + a Uint32 index
|
|
215
|
+
/// buffer for the depth-derived mesh anchor, `null`
|
|
216
|
+
/// for every non-mesh anchor. Carried verbatim to
|
|
217
|
+
/// the JNI which sets `ArAnchor.hasMesh` + the
|
|
218
|
+
/// mesh vectors when both are non-null.
|
|
199
219
|
@JvmStatic
|
|
200
220
|
fun dispatchToHostWorklets(
|
|
201
221
|
nv21Bytes: ByteArray,
|
|
@@ -205,6 +225,18 @@ object StitcherWorkletRuntime {
|
|
|
205
225
|
tx: Double, ty: Double, tz: Double,
|
|
206
226
|
timestampNs: Double,
|
|
207
227
|
trackingState: String,
|
|
228
|
+
depthBytes: ByteArray?,
|
|
229
|
+
depthWidth: Int,
|
|
230
|
+
depthHeight: Int,
|
|
231
|
+
anchorIds: Array<String>,
|
|
232
|
+
anchorTypes: Array<String>,
|
|
233
|
+
anchorTransforms: Array<DoubleArray>,
|
|
234
|
+
anchorMeshVertices: Array<ByteArray?>,
|
|
235
|
+
anchorMeshFaces: Array<ByteArray?>,
|
|
236
|
+
fx: Double, fy: Double, cx: Double, cy: Double,
|
|
237
|
+
intrinsicsImageWidth: Int, intrinsicsImageHeight: Int,
|
|
238
|
+
anchorAlignments: Array<String>,
|
|
239
|
+
anchorExtents: Array<DoubleArray?>,
|
|
208
240
|
) {
|
|
209
241
|
if (!installed.get()) return
|
|
210
242
|
nativeDispatchToHostWorklets(
|
|
@@ -212,6 +244,12 @@ object StitcherWorkletRuntime {
|
|
|
212
244
|
qx, qy, qz, qw,
|
|
213
245
|
tx, ty, tz,
|
|
214
246
|
timestampNs, trackingState,
|
|
247
|
+
depthBytes, depthWidth, depthHeight,
|
|
248
|
+
anchorIds, anchorTypes, anchorTransforms,
|
|
249
|
+
anchorMeshVertices, anchorMeshFaces,
|
|
250
|
+
fx, fy, cx, cy,
|
|
251
|
+
intrinsicsImageWidth, intrinsicsImageHeight,
|
|
252
|
+
anchorAlignments, anchorExtents,
|
|
215
253
|
)
|
|
216
254
|
}
|
|
217
255
|
|
|
@@ -228,9 +266,41 @@ object StitcherWorkletRuntime {
|
|
|
228
266
|
return nativeRegistryCount() > 0
|
|
229
267
|
}
|
|
230
268
|
|
|
269
|
+
/// Per-frame AR-metadata extraction toggles (the JS-driven
|
|
270
|
+
/// enableDepth/enableAnchors/enableMesh `<Camera>` props, written via
|
|
271
|
+
/// `__stitcherProxy.setExtractionConfig`). Read once per frame in
|
|
272
|
+
/// `RNSARCameraView.forwardToIncremental` to GATE the costly ARCore
|
|
273
|
+
/// depth-acquire / anchor-collect / mesh-build work — all default OFF,
|
|
274
|
+
/// so a host pays zero AR-metadata cost until it opts in.
|
|
275
|
+
///
|
|
276
|
+
/// Returns all-false (no extraction) before `installIfNeeded()` runs.
|
|
277
|
+
data class ExtractionFlags(
|
|
278
|
+
val depth: Boolean,
|
|
279
|
+
val anchors: Boolean,
|
|
280
|
+
val mesh: Boolean,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
@JvmStatic
|
|
284
|
+
fun extractionFlags(): ExtractionFlags {
|
|
285
|
+
if (!installed.get()) return ExtractionFlags(false, false, false)
|
|
286
|
+
val bits = nativeExtractionFlags()
|
|
287
|
+
return ExtractionFlags(
|
|
288
|
+
depth = (bits and 0x1) != 0,
|
|
289
|
+
anchors = (bits and 0x2) != 0,
|
|
290
|
+
mesh = (bits and 0x4) != 0,
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
231
294
|
@JvmStatic
|
|
232
295
|
private external fun nativeRegistryCount(): Int
|
|
233
296
|
|
|
297
|
+
/// JNI binding: `nativeExtractionFlags` in
|
|
298
|
+
/// `android/src/main/cpp/stitcher_jsi_install_jni.cpp`. Packs
|
|
299
|
+
/// `retailens::getExtractionConfig()` into a bitmask
|
|
300
|
+
/// (bit0=depth, bit1=anchors, bit2=mesh).
|
|
301
|
+
@JvmStatic
|
|
302
|
+
private external fun nativeExtractionFlags(): Int
|
|
303
|
+
|
|
234
304
|
/// JNI binding: `android/src/main/cpp/stitcher_jsi_install_jni.cpp`'s
|
|
235
305
|
/// `nativeDispatchToHostWorklets`. Fast-path early-exit lives
|
|
236
306
|
/// inside the native function — see its docstring.
|
|
@@ -243,6 +313,18 @@ object StitcherWorkletRuntime {
|
|
|
243
313
|
tx: Double, ty: Double, tz: Double,
|
|
244
314
|
timestampNs: Double,
|
|
245
315
|
trackingState: String,
|
|
316
|
+
depthBytes: ByteArray?,
|
|
317
|
+
depthWidth: Int,
|
|
318
|
+
depthHeight: Int,
|
|
319
|
+
anchorIds: Array<String>,
|
|
320
|
+
anchorTypes: Array<String>,
|
|
321
|
+
anchorTransforms: Array<DoubleArray>,
|
|
322
|
+
anchorMeshVertices: Array<ByteArray?>,
|
|
323
|
+
anchorMeshFaces: Array<ByteArray?>,
|
|
324
|
+
fx: Double, fy: Double, cx: Double, cy: Double,
|
|
325
|
+
intrinsicsImageWidth: Int, intrinsicsImageHeight: Int,
|
|
326
|
+
anchorAlignments: Array<String>,
|
|
327
|
+
anchorExtents: Array<DoubleArray?>,
|
|
246
328
|
)
|
|
247
329
|
|
|
248
330
|
init {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// camera_frame_data.hpp — platform-agnostic backing data for the
|
|
4
4
|
// v0.8.0 `StitcherFrame` JSI host object.
|
|
5
5
|
//
|
|
6
6
|
// ## Why this lives here
|
|
@@ -16,21 +16,24 @@
|
|
|
16
16
|
// ## Memory model
|
|
17
17
|
//
|
|
18
18
|
// `PixelBufferReader` is an opaque interface; platform code (iOS
|
|
19
|
-
// `
|
|
19
|
+
// `CameraFrameHostObject.mm`; Android `stitcher_frame_jni.cpp`)
|
|
20
20
|
// implements it by wrapping the underlying `CVPixelBufferRef` /
|
|
21
21
|
// `ArImage*`. Lifetime: the reader holds a strong ref to its
|
|
22
22
|
// source for the entire host-object lifetime; releases on
|
|
23
23
|
// destruction (deterministic, RAII).
|
|
24
24
|
//
|
|
25
|
-
// `
|
|
25
|
+
// `CameraFrameData` is value-typed (cheap to copy; ~100 bytes).
|
|
26
26
|
// Construct on the worklet runtime's thread before each dispatch.
|
|
27
27
|
|
|
28
28
|
#pragma once
|
|
29
29
|
|
|
30
|
+
#include <array>
|
|
30
31
|
#include <cstddef>
|
|
31
32
|
#include <cstdint>
|
|
32
33
|
#include <memory>
|
|
34
|
+
#include <optional>
|
|
33
35
|
#include <string>
|
|
36
|
+
#include <vector>
|
|
34
37
|
|
|
35
38
|
namespace retailens {
|
|
36
39
|
|
|
@@ -68,10 +71,75 @@ public:
|
|
|
68
71
|
virtual std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) = 0;
|
|
69
72
|
};
|
|
70
73
|
|
|
74
|
+
/// One detected AR anchor (ARKit `ARAnchor` / ARCore `Anchor`).
|
|
75
|
+
/// Mirrors the JS `ARAnchor`: a stable id, a coarse type, and a
|
|
76
|
+
/// 4x4 transform.
|
|
77
|
+
struct ArAnchor {
|
|
78
|
+
/// Stable per-session identifier (ARKit UUID / ARCore anchor id).
|
|
79
|
+
std::string id;
|
|
80
|
+
/// Coarse class: `"plane"`, `"image"`, `"point"`, or `"mesh"`.
|
|
81
|
+
std::string type;
|
|
82
|
+
/// 4x4 anchor->world transform, ROW-MAJOR (16 elements). Platform
|
|
83
|
+
/// code is responsible for emitting row-major regardless of the
|
|
84
|
+
/// native matrix's storage order.
|
|
85
|
+
std::array<double, 16> transform{};
|
|
86
|
+
|
|
87
|
+
/// Plane alignment: "" (n/a), "horizontal", or "vertical". Set on
|
|
88
|
+
/// plane anchors; empty -> JS `alignment === undefined`.
|
|
89
|
+
std::string alignment;
|
|
90
|
+
/// Plane extent in metres along local x/z ([extentX, extentZ]); valid
|
|
91
|
+
/// only when hasExtent (plane anchors).
|
|
92
|
+
bool hasExtent = false;
|
|
93
|
+
double extentX = 0.0;
|
|
94
|
+
double extentZ = 0.0;
|
|
95
|
+
/// ARKit semantic class ("wall","floor","table",… or "none"); empty
|
|
96
|
+
/// string -> JS `classification === undefined` (iOS only).
|
|
97
|
+
std::string classification;
|
|
98
|
+
|
|
99
|
+
// ── Scene-reconstruction geometry (only when type == "mesh") ──
|
|
100
|
+
/// True when this anchor carries a mesh (gates JSI emission of
|
|
101
|
+
/// `meshGeometry`). Raw bytes, emitted as ArrayBuffers verbatim
|
|
102
|
+
/// (no conversion) by the JSI layer:
|
|
103
|
+
/// - meshVertices: Float32 xyz triplets, anchor-local.
|
|
104
|
+
/// - meshFaces: Uint32 triangle indices into the vertices.
|
|
105
|
+
/// - meshClassifications: optional Uint8 per-face class (iOS
|
|
106
|
+
/// ARMeshAnchor; empty on Android — depth-derived meshes have
|
|
107
|
+
/// no semantics).
|
|
108
|
+
bool hasMesh = false;
|
|
109
|
+
std::vector<uint8_t> meshVertices;
|
|
110
|
+
std::vector<uint8_t> meshFaces;
|
|
111
|
+
std::vector<uint8_t> meshClassifications;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/// AR depth map for one frame. The platforms encode depth differently,
|
|
115
|
+
/// so we carry the raw bytes plus a `format` tag and NORMALISE to a
|
|
116
|
+
/// single JS shape (Float32 metres + Uint8 confidence 0..2) in the JSI
|
|
117
|
+
/// layer (`camera_frame_jsi.cpp`):
|
|
118
|
+
/// - iOS (ARKit `ARDepthData`): `depthBytes` = Float32 metres
|
|
119
|
+
/// (row-packed); `confidenceBytes` = Uint8 `ARConfidenceLevel`
|
|
120
|
+
/// (0=low,1=medium,2=high). `format = "f32m"`.
|
|
121
|
+
/// - Android (ARCore DEPTH16): `depthBytes` = uint16 packed (low 13
|
|
122
|
+
/// bits = millimetres, high 3 bits = confidence 0..7);
|
|
123
|
+
/// `confidenceBytes` empty. `format = "u16packed"`.
|
|
124
|
+
/// Depth maps are small (~256x192 iOS, ~160x120 Android) so the bytes
|
|
125
|
+
/// are eager-copied at extraction time (the ARCore Image is closed
|
|
126
|
+
/// in-scope; iOS copies for the same uniform contract).
|
|
127
|
+
struct ArDepth {
|
|
128
|
+
int32_t width = 0;
|
|
129
|
+
int32_t height = 0;
|
|
130
|
+
/// Encoding of `depthBytes`: `"f32m"` or `"u16packed"`.
|
|
131
|
+
std::string format;
|
|
132
|
+
/// Raw depth bytes, interpreted per `format`.
|
|
133
|
+
std::vector<uint8_t> depthBytes;
|
|
134
|
+
/// Per-pixel confidence (Uint8 0..2). Populated on iOS; empty on
|
|
135
|
+
/// Android (confidence is packed into `depthBytes`).
|
|
136
|
+
std::vector<uint8_t> confidenceBytes;
|
|
137
|
+
};
|
|
138
|
+
|
|
71
139
|
/// Plain-old-data payload for one `StitcherFrame`. Fully extracted
|
|
72
140
|
/// at construction time (cheap fields) plus an opaque reader for
|
|
73
141
|
/// the lazy pixel access.
|
|
74
|
-
struct
|
|
142
|
+
struct CameraFrameData {
|
|
75
143
|
/// Discriminator. `"ar"` for AR-mode frames, `"vc"` for
|
|
76
144
|
/// vision-camera frames. Used by worklets to gate on AR-only
|
|
77
145
|
/// field access (translation, depth, anchors, tracking state).
|
|
@@ -127,15 +195,30 @@ struct StitcherFrameData {
|
|
|
127
195
|
/// docstring for lifetime contract.
|
|
128
196
|
std::shared_ptr<PixelBufferReader> pixelReader;
|
|
129
197
|
|
|
130
|
-
// ── AR-only optional fields
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
///
|
|
198
|
+
// ── AR-only optional fields ──────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/// AR depth map (ARKit sceneDepth / ARCore Depth API). `nullopt`
|
|
201
|
+
/// when the device/session can't provide depth; the JSI host object
|
|
202
|
+
/// then exposes `arDepth === undefined`. Normalised to a single JS
|
|
203
|
+
/// shape in the JSI layer regardless of the native `format`.
|
|
204
|
+
std::optional<ArDepth> arDepth;
|
|
205
|
+
|
|
206
|
+
/// Tracked AR anchors visible this frame. Empty when none (or in
|
|
207
|
+
/// non-AR mode); the JSI host object exposes `arAnchors === undefined`
|
|
208
|
+
/// only when empty AND source != "ar" (an AR frame with no anchors
|
|
209
|
+
/// returns an empty array, per the JS contract).
|
|
210
|
+
std::vector<ArAnchor> arAnchors;
|
|
211
|
+
|
|
212
|
+
/// Per-frame camera intrinsics (fx,fy,cx,cy in pixels) + the capture
|
|
213
|
+
/// resolution they're expressed at. Valid only when hasIntrinsics
|
|
214
|
+
/// (AR frames); the JSI exposes `intrinsics === undefined` otherwise.
|
|
215
|
+
bool hasIntrinsics = false;
|
|
216
|
+
double fx = 0.0;
|
|
217
|
+
double fy = 0.0;
|
|
218
|
+
double cx = 0.0;
|
|
219
|
+
double cy = 0.0;
|
|
220
|
+
int32_t intrinsicsImageWidth = 0;
|
|
221
|
+
int32_t intrinsicsImageHeight = 0;
|
|
139
222
|
};
|
|
140
223
|
|
|
141
224
|
} // namespace retailens
|