react-native-image-stitcher 0.7.1 → 0.9.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/android/build.gradle +35 -1
  3. package/android/src/main/cpp/CMakeLists.txt +64 -2
  4. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
  5. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +30 -11
  6. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +21 -3
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
  8. package/android/src/main/java/io/imagestitcher/rn/SaveFrameAsJpegPlugin.kt +162 -0
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
  11. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
  12. package/cpp/stitcher_frame_data.hpp +141 -0
  13. package/cpp/stitcher_frame_jsi.cpp +214 -0
  14. package/cpp/stitcher_frame_jsi.hpp +108 -0
  15. package/cpp/stitcher_proxy_jsi.cpp +109 -0
  16. package/cpp/stitcher_proxy_jsi.hpp +46 -0
  17. package/cpp/stitcher_worklet_dispatch.cpp +103 -0
  18. package/cpp/stitcher_worklet_dispatch.hpp +71 -0
  19. package/cpp/stitcher_worklet_registry.cpp +81 -0
  20. package/cpp/stitcher_worklet_registry.hpp +136 -0
  21. package/dist/camera/Camera.d.ts +62 -12
  22. package/dist/camera/Camera.js +30 -15
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.js +30 -1
  25. package/dist/stitching/StitcherFrame.d.ts +170 -0
  26. package/dist/stitching/StitcherFrame.js +4 -0
  27. package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
  28. package/dist/stitching/StitcherWorkletRegistry.js +78 -0
  29. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  30. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  31. package/dist/stitching/useFrameProcessor.d.ts +119 -0
  32. package/dist/stitching/useFrameProcessor.js +196 -0
  33. package/dist/stitching/useFrameStream.d.ts +34 -0
  34. package/dist/stitching/useFrameStream.js +219 -0
  35. package/dist/stitching/useThrottledFrameProcessor.d.ts +33 -0
  36. package/dist/stitching/useThrottledFrameProcessor.js +132 -0
  37. package/dist/types.d.ts +87 -0
  38. package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -10
  39. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  40. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  41. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +185 -0
  42. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
  43. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
  44. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  45. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
  46. package/package.json +1 -1
  47. package/src/camera/Camera.tsx +93 -28
  48. package/src/index.ts +35 -0
  49. package/src/stitching/StitcherFrame.ts +197 -0
  50. package/src/stitching/StitcherWorkletRegistry.ts +156 -0
  51. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
  52. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
  53. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +178 -0
  54. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
  55. package/src/stitching/useFrameProcessor.ts +226 -0
  56. package/src/stitching/useFrameStream.ts +255 -0
  57. package/src/stitching/useThrottledFrameProcessor.ts +145 -0
  58. package/src/types.ts +95 -0
@@ -0,0 +1,256 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ package io.imagestitcher.rn
3
+
4
+ import android.os.HandlerThread
5
+ import android.util.Log
6
+ import java.util.concurrent.atomic.AtomicBoolean
7
+
8
+ /**
9
+ * v0.8.0 Phase 3b — Android twin of iOS' `RNSARWorkletRuntime`.
10
+ * Owns the per-AR-frame worklet runtime + the thread it dispatches
11
+ * on. Symmetric API shape to the iOS class so the cross-platform
12
+ * dispatch story (Phase 3c) lands in lockstep.
13
+ *
14
+ * ## Phase 3b scope (this commit)
15
+ *
16
+ * - Singleton accessor + lifecycle (installIfNeeded / isInstalled).
17
+ * - Dedicated `HandlerThread` for worklet dispatch — keeps work off
18
+ * the GLSurfaceView GL render thread (audit caveat #4 from the
19
+ * Phase-0 audit).
20
+ * - `dispatchFrame()` stub — Phase 3c will fill in:
21
+ * 1. Build `StitcherFrameHostObject` from ARCore Frame + pose
22
+ * (via the shared C++ JSI host object now linked into
23
+ * `libimage_stitcher.so` post-Phase-3a).
24
+ * 2. Run first-party stitching synchronously on the caller
25
+ * thread (`onDrawFrame`'s GL thread, today).
26
+ * 3. If host worklets are registered, dispatch the host object
27
+ * onto this runtime's `HandlerThread` + invoke each worklet
28
+ * via JNI → `RNWorklet::WorkletInvoker::call`.
29
+ * 4. Invalidate the host object after all worklets return.
30
+ *
31
+ * ## Worklet-runtime construction model
32
+ *
33
+ * Unlike iOS (where the lib's `.mm` directly `std::make_shared`s
34
+ * a `RNWorklet::JsiWorkletContext`), Android can't construct the
35
+ * context purely from native C++ without JNI plumbing. Phase 3c
36
+ * will choose between two paths:
37
+ *
38
+ * - **Option A:** JS-side code calls
39
+ * `Worklets.createContext("stitcher.ar")` at AR-mode start;
40
+ * hands the resulting context pointer to this Kotlin class via
41
+ * a small JSI plugin. Minimal new JNI. **Phase 3b's
42
+ * HandlerThread becomes dead code** under this option — the
43
+ * JS-side `Worklets.createContext` picks its own thread. We'd
44
+ * need to remove the HandlerThread + change `installIfNeeded`
45
+ * into a no-op until a `setContextHandle(Long)` setter lands.
46
+ * - **Option B:** Direct JNI binding to worklets-core's C++
47
+ * constructor. More native code but no JS dependency at runtime.
48
+ * **Phase 3b's HandlerThread is exactly the right scaffold**
49
+ * under this option — its looper becomes the JsiWorkletContext's
50
+ * `workletCallInvoker` target.
51
+ *
52
+ * **Phase 3b assumption: Option B is the more likely path.** The
53
+ * scaffolding below (HandlerThread + serial dispatch) fits Option
54
+ * B; if Phase 3c picks Option A instead, the HandlerThread becomes
55
+ * unused and Phase 3c will refactor accordingly.
56
+ *
57
+ * @see [RNSARWorkletRuntime] iOS equivalent
58
+ * @see docs/plans/handoff/2026-05-26-v0.8.0-phase-0-audit.md
59
+ * worklets-core API rationale (Audit 2: `JsiWorkletContext`).
60
+ * @see docs/plans/handoff/2026-05-26-v0.8.0-phases-2-5-implementation-guide.md
61
+ * Phase 3c implementation plan.
62
+ */
63
+ object StitcherWorkletRuntime {
64
+ private const val TAG = "StitcherWorkletRuntime"
65
+
66
+ /// Single-flight install guard. `compareAndSet` makes the
67
+ /// runtime construction race-safe across concurrent first-mount
68
+ /// calls from multiple `<Camera>` instances.
69
+ private val installed = AtomicBoolean(false)
70
+
71
+ /// Dedicated dispatch thread. Constructed eagerly so we can
72
+ /// validate the thread starts cleanly during `installIfNeeded`.
73
+ /// Off the GLSurfaceView GL render thread (audit caveat #4)
74
+ /// + off the main thread. Phase 3c will configure the worklet
75
+ /// context's `workletCallInvoker` to post onto this thread's
76
+ /// looper.
77
+ private val dispatchThread: HandlerThread by lazy {
78
+ HandlerThread("io.imagestitcher.ar-worklet-runtime").apply { start() }
79
+ }
80
+
81
+ /// Construct the underlying worklet context if not yet installed.
82
+ /// Idempotent — repeated calls are no-ops.
83
+ ///
84
+ /// Phase 3b: starts the dispatch thread; no JsiWorkletContext
85
+ /// construction yet (deferred to Phase 3c).
86
+ /// Phase 3c: also wires the JsiWorkletContext + binds it to the
87
+ /// dispatch thread's looper.
88
+ @JvmStatic
89
+ fun installIfNeeded() {
90
+ if (!installed.compareAndSet(false, true)) {
91
+ return
92
+ }
93
+ // Force the lazy `dispatchThread` to initialise. If the
94
+ // OS denies thread creation (extreme memory pressure on a
95
+ // budget device), `HandlerThread.start()` won't throw but
96
+ // the looper won't be available — the Phase 3c dispatch
97
+ // logic will need to defend against that. For Phase 3b
98
+ // we only care that this method returns without throwing.
99
+ //
100
+ // Log `Thread.id` (Java-side monotonic, always non-zero) —
101
+ // NOT `HandlerThread.threadId` (Linux tid set after first
102
+ // Looper-prepared message; reading from this caller thread
103
+ // immediately after .start() returns -1 until scheduled).
104
+ val javaThreadId = dispatchThread.id
105
+ Log.i(TAG, "installed runtime; dispatch java-thread id=$javaThreadId")
106
+ }
107
+
108
+ /// Diagnostics + tests. Returns `true` after a successful
109
+ /// `installIfNeeded()`.
110
+ @JvmStatic
111
+ fun isInstalled(): Boolean = installed.get()
112
+
113
+ /// v0.8.0 Phase 3c — first-party stitching dispatch. Invokes
114
+ /// the supplied block synchronously on the caller thread
115
+ /// (`onDrawFrame`'s GL render thread today).
116
+ ///
117
+ /// Phase 3c minimum-viable: this is the closure-based equivalent
118
+ /// of iOS' first-party callback. The block is the original
119
+ /// `module.ingestFromARCameraView(...)` call site moved
120
+ /// verbatim into a lambda — no behaviour change, just an
121
+ /// indirection so Phase 4 can interpose host-worklet fanout
122
+ /// without touching the engine ingest path.
123
+ ///
124
+ /// **Why synchronous + on the caller thread:** the engine's
125
+ /// `ingestFromARCameraView` takes ownership of the ARCore
126
+ /// `Image`-derived NV21 buffer (via the v0.10.0 `TransferredNV21`
127
+ /// wrapper). ARCore's `Image.close()` happens after this call
128
+ /// returns, so the consumer must finish reading the bytes before
129
+ /// we return — exactly what synchronous block invocation
130
+ /// provides. Phase 4 will copy the buffer for off-thread
131
+ /// access in host worklets; Phase 3c keeps the sync contract.
132
+ ///
133
+ /// If `installIfNeeded()` hasn't been called yet, the block
134
+ /// still runs (no-op on the registry side). Defensive — the
135
+ /// caller may call this method before `installIfNeeded` is
136
+ /// wired up.
137
+ @JvmStatic
138
+ fun runFirstParty(block: () -> Unit) {
139
+ // Synchronous invocation — Phase 4 will extend this to also
140
+ // post the registered host worklets onto `dispatchThread`.
141
+ // Not `inline`: Phase 4 will need to read `dispatchThread`
142
+ // (private) from inside this function body, and Kotlin's
143
+ // inline functions can't access private members from call
144
+ // sites outside the declaring class. Per-frame lambda
145
+ // alloc is ~ns and the alternative (callers passing a
146
+ // method reference) doesn't materially change cost.
147
+ block()
148
+ }
149
+
150
+ /// v0.8.0 Phase 4b.iii — fan out one AR frame to every host
151
+ /// worklet registered in the shared C++ `StitcherWorkletRegistry`
152
+ /// (populated from JS via `__stitcherProxy.install(workletFn)`).
153
+ ///
154
+ /// Called from `RNSARCameraView.onDrawFrame` immediately after
155
+ /// `runFirstParty { ... }` returns, with the already-extracted
156
+ /// AR frame data (pose + NV21 bytes + dimensions + tracking
157
+ /// state).
158
+ ///
159
+ /// **Fast-path:** the native side queries the registry's count
160
+ /// FIRST and returns before copying any bytes when no host
161
+ /// worklets are registered. In the common first-party-only
162
+ /// deployment, this method costs one JNI call + one C++ atomic
163
+ /// read per frame — negligible.
164
+ ///
165
+ /// **When host worklets ARE registered:** the JNI layer copies
166
+ /// the NV21 byte array into an owned C++ `std::vector` (so the
167
+ /// async dispatch can outlive ARCore's `Image.close()` scope),
168
+ /// builds a `StitcherFrameJsiHostObject`, and posts a lambda
169
+ /// onto worklets-core's default `JsiWorkletContext`'s worklet
170
+ /// thread. The lambda iterates the registry's
171
+ /// `WorkletInvoker`s, calls each with the JSI host object as
172
+ /// its argument, and invalidates the host object after the
173
+ /// last invoker returns. Per-worklet failure isolation: one
174
+ /// host worklet throwing does NOT stop the lib's stitching or
175
+ /// the other host worklets.
176
+ ///
177
+ /// **Threading:** this method returns synchronously on the
178
+ /// caller's thread. The actual worklet invocations happen
179
+ /// asynchronously on the worklets-core thread; the caller does
180
+ /// NOT block on them.
181
+ ///
182
+ /// **Caller-thread contract:** the caller (`RNSARCameraView`'s
183
+ /// `onDrawFrame`) MUST have already invoked `runFirstParty`
184
+ /// before calling this method. The first-party stitching
185
+ /// path holds the synchronous ARCore Image consumption
186
+ /// contract; the host-worklet dispatch does not.
187
+ ///
188
+ /// @param nv21Bytes Pre-packed NV21 byte array. COPIED
189
+ /// into a native owned buffer; caller can
190
+ /// release the reference after return.
191
+ /// @param width Camera image width (pixels).
192
+ /// @param height Camera image height (pixels).
193
+ /// @param qx,qy,qz,qw Pose rotation quaternion (unit length).
194
+ /// @param tx,ty,tz Pose translation (metres, world coords).
195
+ /// @param timestampNs Frame timestamp in nanoseconds.
196
+ /// @param trackingState One of "" / "notAvailable" / "limited"
197
+ /// / "normal". Empty string ⇒ JS-side
198
+ /// `arTrackingState` is `undefined`.
199
+ @JvmStatic
200
+ fun dispatchToHostWorklets(
201
+ nv21Bytes: ByteArray,
202
+ width: Int,
203
+ height: Int,
204
+ qx: Double, qy: Double, qz: Double, qw: Double,
205
+ tx: Double, ty: Double, tz: Double,
206
+ timestampNs: Double,
207
+ trackingState: String,
208
+ ) {
209
+ if (!installed.get()) return
210
+ nativeDispatchToHostWorklets(
211
+ nv21Bytes, width, height,
212
+ qx, qy, qz, qw,
213
+ tx, ty, tz,
214
+ timestampNs, trackingState,
215
+ )
216
+ }
217
+
218
+ /// v0.8.0 Phase 4b.iii — number of registered host worklets.
219
+ /// Cheap (microsecond) call into the native registry. Used by
220
+ /// `RNSARCameraView.onDrawFrame` to gate the per-frame
221
+ /// NV21-pack + dispatch path: when no worklets are registered
222
+ /// AND no capture is active, the entire `forwardToIncremental`
223
+ /// branch can be skipped, saving the ~3-5ms NV21 pack cost per
224
+ /// idle preview frame.
225
+ @JvmStatic
226
+ fun hasHostWorklets(): Boolean {
227
+ if (!installed.get()) return false
228
+ return nativeRegistryCount() > 0
229
+ }
230
+
231
+ @JvmStatic
232
+ private external fun nativeRegistryCount(): Int
233
+
234
+ /// JNI binding: `android/src/main/cpp/stitcher_jsi_install_jni.cpp`'s
235
+ /// `nativeDispatchToHostWorklets`. Fast-path early-exit lives
236
+ /// inside the native function — see its docstring.
237
+ @JvmStatic
238
+ private external fun nativeDispatchToHostWorklets(
239
+ nv21Bytes: ByteArray,
240
+ width: Int,
241
+ height: Int,
242
+ qx: Double, qy: Double, qz: Double, qw: Double,
243
+ tx: Double, ty: Double, tz: Double,
244
+ timestampNs: Double,
245
+ trackingState: String,
246
+ )
247
+
248
+ init {
249
+ // The JSI install module (`StitcherJsiInstallerModule`)
250
+ // already loads `libimage_stitcher` at class load. We
251
+ // load it again here defensively in case
252
+ // `StitcherWorkletRuntime` is referenced before the install
253
+ // module — `System.loadLibrary` is idempotent.
254
+ System.loadLibrary("image_stitcher")
255
+ }
256
+ }
@@ -0,0 +1,100 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ package io.imagestitcher.rn
3
+
4
+ /**
5
+ * v0.10.0 (audit #4A) — single-use NV21 byte-array handle that
6
+ * enforces the engine's pixel-data ownership contract at runtime.
7
+ *
8
+ * ## Why this exists
9
+ *
10
+ * `IncrementalStitcher.ingestFromARCameraView` accepts an
11
+ * `nv21PixelData` parameter that the engine retains for ~50 ms
12
+ * after the producer thread returns (until the `workScope`
13
+ * coroutine consumes it). The documented contract is
14
+ * "callers MUST treat the array as transferred — do not mutate it
15
+ * or return it to a buffer pool after calling this method."
16
+ *
17
+ * The v0.10.0 audit (`docs/plans/handoff/2026-05-26-autonomous-run-handoff.md`
18
+ * finding #4A) noted this is by-convention only. The current AR
19
+ * caller (`RNSARCameraView`) passes the same `packed.nv21` array
20
+ * as BOTH `grayData` (consumed synchronously inside the gate)
21
+ * AND `nv21PixelData` (consumed asynchronously). Today no race
22
+ * because the sync read finishes before the async coroutine reads,
23
+ * but a future refactor that reorders consumption would silently
24
+ * corrupt frames.
25
+ *
26
+ * Wrapping the bytes in `TransferredNV21` turns the documentation
27
+ * contract into a runtime contract: callers can only extract the
28
+ * bytes once via `takeOnce()`; the second call throws. The
29
+ * misuse is caught at the call site, not at the engine.
30
+ *
31
+ * ## Cost
32
+ *
33
+ * Construction: tens of ns (one heap allocation for the wrapper +
34
+ * one volatile write of the bytes reference). `takeOnce()`: tens
35
+ * of ns (one synchronized read + one null-out). Negligible vs the
36
+ * underlying NV21 array's KB-scale memory footprint and the
37
+ * ms-scale frame-processing cost — but not a free pointer hop.
38
+ *
39
+ * ## Thread-safety
40
+ *
41
+ * `takeOnce()` and `available` are `synchronized` on the wrapper
42
+ * itself. Producers should still extract on a single thread (the
43
+ * frame producer); the synchronization defends against the
44
+ * pathological case where two threads race to extract.
45
+ */
46
+ class TransferredNV21(bytes: ByteArray) {
47
+ init {
48
+ // Empty arrays would propagate as "0 bytes of pixel data with
49
+ // a non-zero width/height" downstream and crash inside the
50
+ // C++ ingest with a far less actionable error. Catch at
51
+ // construction. Critic-finding [MAJOR][B].
52
+ require(bytes.isNotEmpty()) {
53
+ "TransferredNV21 requires a non-empty byte array " +
54
+ "(received zero-length)"
55
+ }
56
+ }
57
+
58
+ @Volatile
59
+ private var bytes: ByteArray? = bytes
60
+
61
+ /**
62
+ * Take the wrapped bytes. Throws on second call.
63
+ *
64
+ * Consumers should call this exactly once — typically once per
65
+ * frame, on the producer thread, immediately before handing
66
+ * the bytes to the async work queue:
67
+ *
68
+ * ```kotlin
69
+ * val pixelBytes: ByteArray? = if (hasPixelData) nv21PixelData!!.takeOnce() else null
70
+ * workScope.launch {
71
+ * // pixelBytes is captured by value; no race.
72
+ * engine.addFramePixelData(nv21 = pixelBytes!!, ...)
73
+ * }
74
+ * ```
75
+ *
76
+ * Concurrency note: `@Volatile` on the bytes field plus the
77
+ * `synchronized(this)` block here together guarantee both
78
+ * visibility AND atomicity across threads. The `@Volatile` is
79
+ * defensive for any future non-synchronized read; today every
80
+ * accessor goes through the synchronized block.
81
+ */
82
+ fun takeOnce(): ByteArray = synchronized(this) {
83
+ val b = bytes ?: error(
84
+ "TransferredNV21.takeOnce() called twice — bytes already transferred. " +
85
+ "Check that you're not passing the same TransferredNV21 instance to " +
86
+ "two consumers (e.g., a sync gate-eval call AND an async workScope.launch)."
87
+ )
88
+ bytes = null
89
+ b
90
+ }
91
+
92
+ // Note: an `available` property was considered and removed in
93
+ // pre-merge review (critic-finding [MAJOR][B]). Any
94
+ // `if (handle.available) handle.takeOnce()` pattern is
95
+ // inherently TOCTOU-racy — another thread could win the
96
+ // takeOnce() between the check and the use. Consumers should
97
+ // call `takeOnce()` directly and catch the `IllegalStateException`
98
+ // if they need recovery semantics. No internal caller used
99
+ // `available`; YAGNI removed it.
100
+ }
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // stitcher_frame_data.hpp — platform-agnostic backing data for the
4
+ // v0.8.0 `StitcherFrame` JSI host object.
5
+ //
6
+ // ## Why this lives here
7
+ //
8
+ // The JSI host object's `get()` dispatch logic is platform-specific
9
+ // (Obj-C++ on iOS includes `<jsi/jsi.h>` from React Native's
10
+ // CocoaPod; Android needs a more elaborate CMake setup to link
11
+ // against React Native's JSI library). But the *data* the host
12
+ // object exposes — pose, dimensions, the pixel-buffer reader
13
+ // indirection — is identical on both platforms. That data lives
14
+ // here so iOS / Android JSI dispatch code references one source.
15
+ //
16
+ // ## Memory model
17
+ //
18
+ // `PixelBufferReader` is an opaque interface; platform code (iOS
19
+ // `StitcherFrameHostObject.mm`; Android `stitcher_frame_jni.cpp`)
20
+ // implements it by wrapping the underlying `CVPixelBufferRef` /
21
+ // `ArImage*`. Lifetime: the reader holds a strong ref to its
22
+ // source for the entire host-object lifetime; releases on
23
+ // destruction (deterministic, RAII).
24
+ //
25
+ // `StitcherFrameData` is value-typed (cheap to copy; ~100 bytes).
26
+ // Construct on the worklet runtime's thread before each dispatch.
27
+
28
+ #pragma once
29
+
30
+ #include <cstddef>
31
+ #include <cstdint>
32
+ #include <memory>
33
+ #include <string>
34
+
35
+ namespace retailens {
36
+
37
+ /// Opaque interface for reading the underlying camera pixel data.
38
+ /// Platform code provides an implementation:
39
+ /// - iOS: wraps a `CVPixelBufferRef` (locks/unlocks base address
40
+ /// across copyTo, defers release until destruction).
41
+ /// - Android: wraps an ARCore `ArImage*` (handles plane access via
42
+ /// `ArImage_getPlaneData`, calls `ArImage_release` on destruct).
43
+ ///
44
+ /// **Thread-affinity contract:** implementations need not be
45
+ /// reentrant. An instance MAY be constructed on thread A
46
+ /// (typically the ARSession delegate queue) and used on thread B
47
+ /// (the worklet-runtime thread), provided the construction-thread
48
+ /// releases its `shared_ptr` reference before thread B uses the
49
+ /// reader. The `shared_ptr`'s atomic refcount serves as the
50
+ /// happens-before barrier — fields set in the constructor are
51
+ /// visible on the worklet thread once the construction-thread
52
+ /// drops its ref. Concurrent access from two threads simultaneously
53
+ /// is NOT supported.
54
+ class PixelBufferReader {
55
+ public:
56
+ virtual ~PixelBufferReader() = default;
57
+
58
+ /// Total byte size of the buffer the reader exposes. For Y-plane
59
+ /// access (the v0.8.0 default), this is `width * height`.
60
+ virtual std::size_t byteSize() const = 0;
61
+
62
+ /// Copy up to `maxBytes` of the underlying buffer into `dst`.
63
+ /// Returns bytes written. Returns 0 if reader is invalidated.
64
+ ///
65
+ /// Implementations MUST handle the case where `maxBytes < byteSize()`
66
+ /// (clip silently). This matches JS `ArrayBuffer.slice` semantics
67
+ /// even though the host object always allocates exactly `byteSize()`.
68
+ virtual std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) = 0;
69
+ };
70
+
71
+ /// Plain-old-data payload for one `StitcherFrame`. Fully extracted
72
+ /// at construction time (cheap fields) plus an opaque reader for
73
+ /// the lazy pixel access.
74
+ struct StitcherFrameData {
75
+ /// Discriminator. `"ar"` for AR-mode frames, `"vc"` for
76
+ /// vision-camera frames. Used by worklets to gate on AR-only
77
+ /// field access (translation, depth, anchors, tracking state).
78
+ /// Mirrored to the JS `source` field (standard discriminated-
79
+ /// union pattern).
80
+ std::string source;
81
+
82
+ /// Width / height of the camera image in pixels.
83
+ int32_t width = 0;
84
+ int32_t height = 0;
85
+
86
+ /// String pixel-format identifier; matches the JS
87
+ /// `StitcherFrame.pixelFormat` union: `"yuv"` / `"rgb"` /
88
+ /// `"unknown"`. Today's emitters always populate `"yuv"`
89
+ /// (NV12 on iOS, NV21 on Android).
90
+ std::string pixelFormat;
91
+
92
+ /// String orientation identifier; matches vision-camera's
93
+ /// `Frame.orientation`: `"portrait"`, `"portrait-upside-down"`,
94
+ /// `"landscape-left"`, `"landscape-right"`.
95
+ std::string orientation;
96
+
97
+ /// Monotonic timestamp in nanoseconds. AR mode: from
98
+ /// `ARFrame.timestamp` (CFAbsoluteTime, converted to ns).
99
+ /// Non-AR mode: from `vision-camera Frame.timestamp` (already ns).
100
+ double timestampNs = 0.0;
101
+
102
+ /// Pose rotation as quaternion `(x, y, z, w)`. Matches the
103
+ /// `q = q_yaw * q_pitch * q_roll` convention used elsewhere in
104
+ /// the lib (KeyframeGate, RNSARFramePose, AcceptedKeyframe).
105
+ double qx = 0.0;
106
+ double qy = 0.0;
107
+ double qz = 0.0;
108
+ double qw = 1.0;
109
+
110
+ /// Pose translation in metres (world coords). AR mode: from
111
+ /// `ARFrame.camera.transform`. Non-AR mode: undefined — the
112
+ /// `hasTranslation` flag is `false` and JS receives
113
+ /// `pose.translation === undefined`.
114
+ double tx = 0.0;
115
+ double ty = 0.0;
116
+ double tz = 0.0;
117
+ bool hasTranslation = false;
118
+
119
+ /// AR tracking state. Empty string (`""`) means "not
120
+ /// applicable" (the JS host object exposes `arTrackingState ===
121
+ /// undefined` in that case). Otherwise one of `"notAvailable"`,
122
+ /// `"limited"`, `"normal"`.
123
+ std::string arTrackingState;
124
+
125
+ /// Pixel data accessor. Always present (even for AR mode where
126
+ /// arDepth might be the more interesting buffer). See class
127
+ /// docstring for lifetime contract.
128
+ std::shared_ptr<PixelBufferReader> pixelReader;
129
+
130
+ // ── AR-only optional fields (not populated in v0.8.0; stubs) ──
131
+ // These are deferred to v0.8.1+ because the host worklets that
132
+ // would consume them aren't shipping in v0.8.0 either. Adding
133
+ // them here as plain data fields keeps the JSI host object code
134
+ // simple when they DO arrive.
135
+
136
+ /// arDepth, arAnchors stubs intentionally omitted — they're
137
+ /// fields the JSI dispatch will return `undefined` for in v0.8.0.
138
+ /// v0.8.1+ adds them here as `std::optional<ArDepth>` etc.
139
+ };
140
+
141
+ } // namespace retailens