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,247 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// RNISARFramePlugin — v0.19.0 native AR plugin framework (iOS).
|
|
4
|
+
//
|
|
5
|
+
// The SDK owns the ARSession and drives one per-frame callback path
|
|
6
|
+
// (`RNSARSession.session(_:didUpdate:)`). Host apps that need to run
|
|
7
|
+
// heavier native per-frame analysis (OCR, barcode reading, ML
|
|
8
|
+
// inference, …) register a *native* plugin against this framework — the
|
|
9
|
+
// SDK ships ONLY the generic plumbing; no OCR or other concrete plugin.
|
|
10
|
+
//
|
|
11
|
+
// The ergonomics mirror vision-camera's FrameProcessorPlugin
|
|
12
|
+
// registration: the host conforms a class to `RNISARFramePlugin`,
|
|
13
|
+
// registers it once at startup via `RNISARPluginRegistry.shared`, and
|
|
14
|
+
// the SDK calls `process(_:)` on the AR thread for every ARFrame while
|
|
15
|
+
// the registry is non-empty.
|
|
16
|
+
//
|
|
17
|
+
// Two result channels:
|
|
18
|
+
// 1. SYNC — `process(_:)` returns a light `[String: Any]?`. Non-nil
|
|
19
|
+
// results are folded into the throttled `onArFrame` `ARFrameMeta`
|
|
20
|
+
// under `plugins: { [name]: result }`, riding the existing
|
|
21
|
+
// `RNImageStitcherARFrame` device event. Use for cheap, per-frame
|
|
22
|
+
// scalars (brightness, a quick blur score, …).
|
|
23
|
+
// 2. ASYNC — the plugin offloads heavy work to its own queue and later
|
|
24
|
+
// calls `RNISARPluginRegistry.shared.emit(name, result)`, which the
|
|
25
|
+
// SDK re-emits as the `RNImageStitcherARPluginResult` device event
|
|
26
|
+
// `{ plugin, result }`. Use for OCR / ML whose latency exceeds a
|
|
27
|
+
// frame interval.
|
|
28
|
+
//
|
|
29
|
+
// PERFORMANCE CONTRACT: the SDK only builds the `RNISARFrameContext` and
|
|
30
|
+
// calls plugins when the registry is NON-EMPTY, so a zero-plugin app
|
|
31
|
+
// pays nothing on the AR hot path. Plugins MUST self-throttle (the SDK
|
|
32
|
+
// calls `process(_:)` on every ARFrame) and MUST offload anything
|
|
33
|
+
// heavier than a few hundred microseconds.
|
|
34
|
+
|
|
35
|
+
import Foundation
|
|
36
|
+
import ARKit
|
|
37
|
+
import CoreVideo
|
|
38
|
+
|
|
39
|
+
// MARK: - Async result event channel
|
|
40
|
+
|
|
41
|
+
/// v0.19.0 — async AR-plugin result channel. `RNISARPluginRegistry.emit`
|
|
42
|
+
/// posts this notification (carrying `{ plugin, result }`); the
|
|
43
|
+
/// `RNSARSessionBridge` (an RCTEventEmitter) observes it and re-emits as
|
|
44
|
+
/// the JS `RNImageStitcherARPluginResult` device event. We route via
|
|
45
|
+
/// NotificationCenter — rather than the registry holding a bridge
|
|
46
|
+
/// reference — mirroring the `.retailensARFrameMeta` (`onArFrame`)
|
|
47
|
+
/// channel, so the framework-free engine pattern is preserved.
|
|
48
|
+
public extension Notification.Name {
|
|
49
|
+
static let retailensARPluginResult =
|
|
50
|
+
Notification.Name("RNImageStitcherARPluginResult")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// MARK: - Plugin protocol
|
|
55
|
+
|
|
56
|
+
/// A native AR frame plugin. Host apps conform a class to this and
|
|
57
|
+
/// register it once via `RNISARPluginRegistry.shared.register(_:)`.
|
|
58
|
+
///
|
|
59
|
+
/// The SDK calls `process(_:)` once per ARFrame on the AR (ARSession
|
|
60
|
+
/// delegate) thread while the registry is non-empty. Return a light
|
|
61
|
+
/// `[String: Any]?` for the SYNC channel, or `nil`; for heavy work,
|
|
62
|
+
/// offload to your own queue and later call
|
|
63
|
+
/// `RNISARPluginRegistry.shared.emit(name(), result)` for the ASYNC
|
|
64
|
+
/// channel.
|
|
65
|
+
@objc public protocol RNISARFramePlugin: AnyObject {
|
|
66
|
+
/// Stable identifier for this plugin. Used as the key in the
|
|
67
|
+
/// `onArFrame` meta's `plugins` map AND as the `plugin` field of the
|
|
68
|
+
/// async `RNImageStitcherARPluginResult` event. Keep it constant for
|
|
69
|
+
/// the plugin's lifetime; the registry stores plugins keyed by name
|
|
70
|
+
/// (a second `register` with the same name replaces the first).
|
|
71
|
+
func name() -> String
|
|
72
|
+
|
|
73
|
+
/// Called on the AR thread once per ARFrame while the registry is
|
|
74
|
+
/// non-empty. Return a light JSON-safe result for the SYNC channel
|
|
75
|
+
/// (NSNumber / NSString / NSArray / NSDictionary leaves) or `nil`.
|
|
76
|
+
///
|
|
77
|
+
/// LIFETIME: `context.pixelBuffer` (and `context.depthBuffer`) are the
|
|
78
|
+
/// live ARFrame buffers — VALID ONLY for the duration of this call.
|
|
79
|
+
/// ARKit recycles them once `process(_:)` returns. If you offload
|
|
80
|
+
/// work to another thread/queue, you MUST copy the bytes you need
|
|
81
|
+
/// BEFORE returning. Do NOT retain the CVPixelBuffer expecting the
|
|
82
|
+
/// pixels to survive — a CF retain does not protect against ARKit's
|
|
83
|
+
/// pool reuse.
|
|
84
|
+
func process(_ context: RNISARFrameContext) -> [String: Any]?
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
// MARK: - Per-frame context
|
|
89
|
+
|
|
90
|
+
/// Zero-copy native view of one ARFrame, handed to each plugin's
|
|
91
|
+
/// `process(_:)`. Exposes the live capture buffer + pose + intrinsics +
|
|
92
|
+
/// (opt-in) depth + (opt-in) anchors. Nothing here is copied for the
|
|
93
|
+
/// plugin's benefit; the buffers belong to ARKit and are valid only
|
|
94
|
+
/// during the synchronous `process(_:)` call (see the lifetime note on
|
|
95
|
+
/// `RNISARFramePlugin.process(_:)`).
|
|
96
|
+
@objc(RNISARFrameContext)
|
|
97
|
+
public final class RNISARFrameContext: NSObject {
|
|
98
|
+
|
|
99
|
+
/// The ARFrame's `capturedImage` (BGRA/YUV `CVPixelBuffer`). VALID
|
|
100
|
+
/// ONLY during `process(_:)` — copy before offloading (see the
|
|
101
|
+
/// lifetime note on `RNISARFramePlugin.process(_:)`).
|
|
102
|
+
@objc public let pixelBuffer: CVPixelBuffer
|
|
103
|
+
|
|
104
|
+
/// Frame timestamp in NANOSECONDS (AR-framework monotonic clock) —
|
|
105
|
+
/// matches `CameraFrame.timestampNs` and the `ARFrameMeta.timestamp`
|
|
106
|
+
/// contract.
|
|
107
|
+
@objc public let timestampNs: Double
|
|
108
|
+
|
|
109
|
+
/// Camera intrinsics (pixels). `imageWidth`/`imageHeight` are the
|
|
110
|
+
/// capture resolution the intrinsics are expressed against.
|
|
111
|
+
@objc public let fx: Double
|
|
112
|
+
@objc public let fy: Double
|
|
113
|
+
@objc public let cx: Double
|
|
114
|
+
@objc public let cy: Double
|
|
115
|
+
@objc public let imageWidth: Int
|
|
116
|
+
@objc public let imageHeight: Int
|
|
117
|
+
|
|
118
|
+
/// World-space camera pose: rotation as a unit quaternion
|
|
119
|
+
/// `[x, y, z, w]` and translation `[x, y, z]` in metres (ARKit's
|
|
120
|
+
/// right-handed, Y-up, -Z-forward world frame).
|
|
121
|
+
@objc public let poseRotation: [Double]
|
|
122
|
+
@objc public let poseTranslation: [Double]
|
|
123
|
+
|
|
124
|
+
/// Tracking quality: `"notAvailable"` / `"limited"` / `"normal"`.
|
|
125
|
+
@objc public let trackingState: String
|
|
126
|
+
|
|
127
|
+
/// The ARFrame's `sceneDepth` (or `smoothedSceneDepth`) depth map
|
|
128
|
+
/// `CVPixelBuffer` — `nil` unless the `<Camera enableDepth>` prop is
|
|
129
|
+
/// on AND the device produced depth this frame. VALID ONLY during
|
|
130
|
+
/// `process(_:)`; copy before offloading.
|
|
131
|
+
@objc public let depthBuffer: CVPixelBuffer?
|
|
132
|
+
|
|
133
|
+
/// Tracking anchors as the same light dicts the `onArFrame` meta
|
|
134
|
+
/// surfaces (`{ id, type, alignment?, extent?, classification?,
|
|
135
|
+
/// transform }`; mesh anchors excluded). EMPTY unless the
|
|
136
|
+
/// `<Camera enableAnchors>` prop is on.
|
|
137
|
+
@objc public let anchors: [[String: Any]]
|
|
138
|
+
|
|
139
|
+
@objc public init(
|
|
140
|
+
pixelBuffer: CVPixelBuffer,
|
|
141
|
+
timestampNs: Double,
|
|
142
|
+
fx: Double, fy: Double, cx: Double, cy: Double,
|
|
143
|
+
imageWidth: Int, imageHeight: Int,
|
|
144
|
+
poseRotation: [Double],
|
|
145
|
+
poseTranslation: [Double],
|
|
146
|
+
trackingState: String,
|
|
147
|
+
depthBuffer: CVPixelBuffer?,
|
|
148
|
+
anchors: [[String: Any]]
|
|
149
|
+
) {
|
|
150
|
+
self.pixelBuffer = pixelBuffer
|
|
151
|
+
self.timestampNs = timestampNs
|
|
152
|
+
self.fx = fx; self.fy = fy; self.cx = cx; self.cy = cy
|
|
153
|
+
self.imageWidth = imageWidth
|
|
154
|
+
self.imageHeight = imageHeight
|
|
155
|
+
self.poseRotation = poseRotation
|
|
156
|
+
self.poseTranslation = poseTranslation
|
|
157
|
+
self.trackingState = trackingState
|
|
158
|
+
self.depthBuffer = depthBuffer
|
|
159
|
+
self.anchors = anchors
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// MARK: - Registry
|
|
165
|
+
|
|
166
|
+
/// Process-wide registry of native AR plugins + the async result router.
|
|
167
|
+
///
|
|
168
|
+
/// The host registers plugins at startup (e.g. in the AppDelegate); the
|
|
169
|
+
/// SDK reads `plugins()` on the AR thread each frame. Also the entry
|
|
170
|
+
/// point for the ASYNC channel: a plugin calls `emit(name, result)` from
|
|
171
|
+
/// its own queue and the SDK re-emits a `RNImageStitcherARPluginResult`
|
|
172
|
+
/// JS event.
|
|
173
|
+
///
|
|
174
|
+
/// THREAD SAFETY: registration (host/startup thread) and reads (AR
|
|
175
|
+
/// thread) are serialised by an internal lock. `plugins()` returns a
|
|
176
|
+
/// snapshot array so the AR thread can iterate without holding the lock.
|
|
177
|
+
@objc(RNISARPluginRegistry)
|
|
178
|
+
public final class RNISARPluginRegistry: NSObject {
|
|
179
|
+
|
|
180
|
+
/// Shared instance — the only way hosts register plugins.
|
|
181
|
+
@objc public static let shared = RNISARPluginRegistry()
|
|
182
|
+
|
|
183
|
+
/// Registered plugins, keyed by `name()` for O(1) replace/unregister.
|
|
184
|
+
/// Insertion order is preserved for deterministic `process(_:)`
|
|
185
|
+
/// ordering via a parallel ordered key list.
|
|
186
|
+
private var pluginsByName: [String: RNISARFramePlugin] = [:]
|
|
187
|
+
private var order: [String] = []
|
|
188
|
+
private let lock = NSLock()
|
|
189
|
+
|
|
190
|
+
private override init() { super.init() }
|
|
191
|
+
|
|
192
|
+
/// Register (or replace) a plugin. Keyed by `plugin.name()`: a
|
|
193
|
+
/// second register with the same name replaces the first (and keeps
|
|
194
|
+
/// its position in the ordering). Idempotent for the same instance.
|
|
195
|
+
@objc public func register(_ plugin: RNISARFramePlugin) {
|
|
196
|
+
let key = plugin.name()
|
|
197
|
+
lock.lock()
|
|
198
|
+
defer { lock.unlock() }
|
|
199
|
+
if pluginsByName[key] == nil {
|
|
200
|
+
order.append(key)
|
|
201
|
+
}
|
|
202
|
+
pluginsByName[key] = plugin
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Remove the plugin registered under `name`. No-op if absent.
|
|
206
|
+
@objc public func unregister(_ name: String) {
|
|
207
|
+
lock.lock()
|
|
208
|
+
defer { lock.unlock() }
|
|
209
|
+
pluginsByName.removeValue(forKey: name)
|
|
210
|
+
order.removeAll { $0 == name }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Snapshot of registered plugins in registration order. Returns a
|
|
214
|
+
/// copy so the AR thread can iterate without holding the lock.
|
|
215
|
+
@objc public func plugins() -> [RNISARFramePlugin] {
|
|
216
|
+
lock.lock()
|
|
217
|
+
defer { lock.unlock() }
|
|
218
|
+
return order.compactMap { pluginsByName[$0] }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Whether any plugin is registered. Cheap gate the SDK checks per
|
|
222
|
+
/// ARFrame before building the (per-frame) `RNISARFrameContext`.
|
|
223
|
+
@objc public var isEmpty: Bool {
|
|
224
|
+
lock.lock()
|
|
225
|
+
defer { lock.unlock() }
|
|
226
|
+
return order.isEmpty
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// ASYNC channel — route a plugin's later-computed result to JS as the
|
|
230
|
+
/// `RNImageStitcherARPluginResult` device event `{ plugin, result }`.
|
|
231
|
+
/// Safe to call from any thread (the plugin's own queue); posts on
|
|
232
|
+
/// NotificationCenter, which the bridge observes + re-emits on the
|
|
233
|
+
/// main queue.
|
|
234
|
+
///
|
|
235
|
+
/// `pluginName` should match the plugin's `name()` so JS can correlate
|
|
236
|
+
/// the result with its source.
|
|
237
|
+
@objc public func emit(_ pluginName: String, _ result: [String: Any]) {
|
|
238
|
+
NotificationCenter.default.post(
|
|
239
|
+
name: .retailensARPluginResult,
|
|
240
|
+
object: nil,
|
|
241
|
+
userInfo: [
|
|
242
|
+
"plugin": pluginName,
|
|
243
|
+
"result": result,
|
|
244
|
+
]
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|