react-native-image-stitcher 0.5.1 → 0.7.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 (28) hide show
  1. package/CHANGELOG.md +199 -1
  2. package/android/src/main/java/io/imagestitcher/rn/CvFlowGateFrameProcessor.kt +2 -2
  3. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +2 -30
  4. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +90 -368
  5. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +6 -3
  6. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +17 -30
  7. package/dist/camera/Camera.d.ts +11 -27
  8. package/dist/camera/Camera.js +46 -78
  9. package/dist/index.d.ts +2 -3
  10. package/dist/index.js +10 -6
  11. package/dist/stitching/incremental.d.ts +79 -11
  12. package/dist/stitching/useFrameProcessorDriver.d.ts +7 -6
  13. package/dist/stitching/useFrameProcessorDriver.js +12 -11
  14. package/dist/stitching/useKeyframeStream.d.ts +69 -0
  15. package/dist/stitching/useKeyframeStream.js +120 -0
  16. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +48 -208
  17. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +0 -8
  18. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +6 -126
  19. package/ios/Sources/RNImageStitcher/KeyframeGateFrameProcessor.mm +6 -6
  20. package/package.json +1 -1
  21. package/src/camera/Camera.tsx +57 -106
  22. package/src/index.ts +9 -9
  23. package/src/stitching/incremental.ts +84 -11
  24. package/src/stitching/useFrameProcessorDriver.ts +12 -11
  25. package/src/stitching/useKeyframeStream.ts +127 -0
  26. package/dist/stitching/useIncrementalJSDriver.d.ts +0 -74
  27. package/dist/stitching/useIncrementalJSDriver.js +0 -220
  28. package/src/stitching/useIncrementalJSDriver.ts +0 -297
package/CHANGELOG.md CHANGED
@@ -16,6 +16,198 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.7.0] — 2026-05-26
20
+
21
+ ### Added — Tier 1: `useKeyframeStream`
22
+
23
+ JS-thread subscription hook for **accepted-keyframe events** — the
24
+ small subset of camera frames the stitching engine actually chose to
25
+ include in the panorama. Foundation for plugin-pattern host features:
26
+ OCR on each saved keyframe, packet detection, server-side analysis,
27
+ analytics, etc.
28
+
29
+ Fires 4-6 times per panorama (once per accepted keyframe), NOT per
30
+ camera frame — the lowest-frequency, highest-value frame stream.
31
+
32
+ ```tsx
33
+ import { useKeyframeStream, type AcceptedKeyframe } from 'react-native-image-stitcher';
34
+
35
+ function OcrPlugin() {
36
+ useKeyframeStream(useCallback(async (kf: AcceptedKeyframe) => {
37
+ const text = await runOCR(kf.jpegPath);
38
+ console.log(`Keyframe ${kf.index} pose=${kf.pose.rotation}:`, text);
39
+ }, []));
40
+ return null;
41
+ }
42
+ ```
43
+
44
+ - **`useKeyframeStream(handler)`** exported from
45
+ `react-native-image-stitcher`. Subscribes to the existing
46
+ `IncrementalStateUpdate` event channel; surfaces accepted-keyframe
47
+ events through a typed callback. Re-subscribes on handler-identity
48
+ changes; async handler rejections are surfaced via `console.error`
49
+ rather than swallowed.
50
+ - **`AcceptedKeyframe` type** exported. Fields: `jpegPath` (absolute
51
+ path, no `file://` prefix); `pose` (rotation quaternion + optional
52
+ translation vector); `timestamp` (ms since epoch); `index`
53
+ (zero-based position in current panorama).
54
+ - **`IncrementalState.batchKeyframePose?`** + **`batchKeyframeAcceptedAtMs?`**
55
+ new optional fields. Populated by the native emit alongside the
56
+ existing `batchKeyframeThumbnailPath` + `batchKeyframeIndex` on
57
+ accept events. Direct readers of `IncrementalState` can consume
58
+ these without going through the new hook.
59
+
60
+ ### Changed (internal — externally invisible)
61
+
62
+ - **Native `emitBatchKeyframeAcceptedState` populates pose + timestamp.**
63
+ Both `IncrementalStitcher.swift::emitBatchKeyframeAcceptedState` and
64
+ `IncrementalStitcher.kt::emitBatchKeyframeAcceptedState` grew
65
+ parameters for the pose snapshot (quaternion + translation) and
66
+ accept-time wall-clock millis. The existing call sites in the
67
+ batch-keyframe accept path thread the pose they already have in
68
+ scope.
69
+
70
+ ### Engine-mode caveat
71
+
72
+ `useKeyframeStream` only fires under the `batch-keyframe` engine (the
73
+ `<Camera>` component's default). Live engines (`firstwins-rectilinear`,
74
+ `hybrid`, `slitscan-*`) paint into a live canvas instead of saving
75
+ per-accept JPEGs and do not surface accept events through this channel
76
+ — the hook silently does not fire in those modes. Live-engine accept
77
+ emit may land as a v0.7.1 follow-up if a real consumer needs it.
78
+
79
+ ### Translation semantics
80
+
81
+ `AcceptedKeyframe.pose.translation` is always populated by the native
82
+ emit. In AR mode it carries the real ARKit / ARCore camera transform
83
+ in metres (world coords). In non-AR (Frame Processor) mode the
84
+ translation reads as `[0, 0, 0]` because gyroscope provides only
85
+ rotation (no spatial anchor). Hosts that need to distinguish can
86
+ either check the active `frameSourceMode` or threshold the translation
87
+ magnitude.
88
+
89
+ ### Compatibility
90
+
91
+ Strict additive over v0.6.0. No host changes required. Existing
92
+ `subscribeIncrementalState` consumers see new optional fields but
93
+ their existing reads are unaffected.
94
+
95
+ ### Verification
96
+
97
+ - iPhone 17 Pro (real device, iOS 26.5): hold-and-release AR-mode
98
+ panorama produced four accepted-keyframe events with real pose
99
+ data (unit quaternion + non-zero translation in metres matching
100
+ the physical pan).
101
+ - Android (Galaxy A35): `compileDebugKotlin` BUILD SUCCESSFUL;
102
+ on-device runtime verification deferred for this release (the
103
+ Kotlin emit mirrors the iOS emit at the byte-for-byte payload
104
+ level — same field names, same types, same call-site pattern).
105
+
106
+ ## [0.6.0] — 2026-05-25
107
+
108
+ > [!WARNING]
109
+ > **Breaking changes.** v0.6.0 retires the deprecated JS-driver
110
+ > non-AR path that was marked for removal in v0.5.0's *Deprecated*
111
+ > section. Hosts using the default `<Camera>` flow (`legacyDriver`
112
+ > unset) are not affected — they were already on
113
+ > `useFrameProcessorDriver`. Hosts that opted into the legacy
114
+ > driver (`legacyDriver={true}` on `<Camera>`, or a direct
115
+ > `useIncrementalJSDriver()` consumer) MUST migrate to the Frame
116
+ > Processor driver — see *Migration from 0.5.x* below.
117
+
118
+ ### Removed (breaking)
119
+
120
+ - **`useIncrementalJSDriver` hook** + its `UseIncrementalJSDriverOptions`
121
+ / `IncrementalJSDriverHandle` types. Deprecated in v0.5.0; the
122
+ v0.5 deprecation warning has now been replaced by deletion.
123
+ - **`legacyDriver?: boolean` prop on `<Camera>`**. The escape hatch
124
+ back to the JS driver is gone. Hosts that set this prop will
125
+ get a TS-level error; at runtime the prop is silently ignored.
126
+ - **`frameSourceMode: 'jsDriver'`** enum value in
127
+ `IncrementalStartOptions`. The TS type is now narrowed to
128
+ `'arSession' | 'frameProcessor'`. Passing `'jsDriver'` is a
129
+ compile error; at the native bridge layer the value falls through
130
+ to the default (now `'arSession'`).
131
+ - **`IncrementalStitcher.processFrameAtPath` native method** on both
132
+ iOS and Android. The only JS caller was `useIncrementalJSDriver`,
133
+ also deleted. Hosts calling
134
+ `NativeModules.IncrementalStitcher.processFrameAtPath(...)` via
135
+ raw `NativeModules` access will get a runtime "method does not
136
+ exist" error. Use the Frame Processor driver instead.
137
+
138
+ ### Changed (breaking)
139
+
140
+ - **Android `frameSourceMode` default switched from `"jsDriver"` to
141
+ `"arSession"`** for parity with iOS. Raw `NativeModules` callers
142
+ that omitted `frameSourceMode` were previously getting an inert
143
+ capture (the "jsDriver" branch dropped all engine input on
144
+ Android since v0.5.0); they now get AR-mode behaviour, matching
145
+ iOS. The production `<Camera>` is unaffected — it always passes
146
+ `frameSourceMode: 'arSession'` explicitly for AR captures.
147
+
148
+ ### Changed (non-breaking)
149
+
150
+ - **`RNSARCameraView` (AR mode) no longer eager-encodes a JPEG per
151
+ ARCore frame.** Migrated to the pixel-data path introduced for
152
+ the Frame Processor in v0.5.1's F8.6 work. AR-mode captures now
153
+ pass `nv21PixelData` / `nv21PixelWidth` / `nv21PixelHeight`
154
+ through `ingestFromARCameraView`; `legacyJpegPath` is always null
155
+ on this path. Expected gain on Galaxy A35: ~30-50 ms per
156
+ accepted frame, with the dominant savings on rejected frames
157
+ (no JPEG encode → no imread round-trip). Closes the v0.5.0
158
+ follow-up.
159
+
160
+ ### Removed (internal cleanup; no external API impact)
161
+
162
+ - **F8.6 perf-diagnostic logs** (`F8.6-route`, `F8.6-perf`)
163
+ introduced in v0.5.1 stripped from `IncrementalStitcher` +
164
+ `IncrementalFirstwinsEngine` — F8.6 is now baked in for
165
+ production and the diagnostic spam is no longer informative.
166
+ - **Orphaned native helpers** dropped after `processFrameAtPath`
167
+ removal:
168
+ - iOS: `addBatchKeyframePath(path:pose:)`, `isBatchKeyframeMode`
169
+ getter, `decodeJpegToGrayscalePixelBuffer` (only callers were
170
+ `processFrameAtPath`).
171
+ - Android: `decodeJpegToGrayscale` + `GrayscaleFrame` data class,
172
+ `isBatchKeyframeMode` getter (only callers were
173
+ `processFrameAtPath` and the AR-mode eager-encode branch).
174
+ - **Stale comments** referencing removed code paths swept across
175
+ Kotlin/Swift/Obj-C/TS. Historical "removed in v0.6" markers
176
+ retained; comments that described live code in terms of the
177
+ removed names rewritten to describe current behaviour.
178
+
179
+ ### Migration from 0.5.x
180
+
181
+ **Default `<Camera>` hosts (no `legacyDriver` prop set):** no
182
+ action required. `<Camera>` already used `useFrameProcessorDriver`
183
+ in non-AR mode and `RNSARSession` in AR mode since v0.5.0.
184
+
185
+ **Hosts with `legacyDriver={true}` on `<Camera>`:** remove the
186
+ prop. `<Camera>` will use the Frame Processor driver, which has
187
+ been the default since v0.5.0 and the only path since this release.
188
+
189
+ ```tsx
190
+ // Before (v0.5.x)
191
+ <Camera legacyDriver={true} ... />
192
+
193
+ // After (v0.6.0)
194
+ <Camera ... />
195
+ ```
196
+
197
+ **Hosts directly using `useIncrementalJSDriver`:** migrate to
198
+ `useFrameProcessorDriver`. The handle shape (`{ start, stop,
199
+ frameProcessor, isRunning }`) is preserved, but the new hook is a
200
+ Frame Processor + gyro driver instead of a `takeSnapshot` + JS
201
+ interval driver. See
202
+ [`src/stitching/useFrameProcessorDriver.ts`](src/stitching/useFrameProcessorDriver.ts)
203
+ for the migration mapping; the gyro pose synthesis convention
204
+ (`q = q_yaw * q_pitch * q_roll`) is identical, so existing pose
205
+ math at call sites continues to work.
206
+
207
+ **Hosts passing `frameSourceMode: 'jsDriver'` to
208
+ `incremental.start(...)`:** change to `'frameProcessor'`. The
209
+ TypeScript type now rejects `'jsDriver'` at compile time.
210
+
19
211
  ## [0.5.1] — 2026-05-25
20
212
 
21
213
  ### Added — F8.6 Android pixel-buffer engine parity
@@ -1056,7 +1248,13 @@ Native module names also changed:
1056
1248
  - iOS pod: `RetaiLensCaptureSDK` → `RNImageStitcher`
1057
1249
  - iOS xcframework: shipped as `opencv2.xcframework` (linked from `RNImageStitcher.podspec`)
1058
1250
 
1059
- [Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.3.0...HEAD
1251
+ [Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.7.0...HEAD
1252
+ [0.7.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.6.0...v0.7.0
1253
+ [0.6.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.5.1...v0.6.0
1254
+ [0.5.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.5.0...v0.5.1
1255
+ [0.5.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.4.1...v0.5.0
1256
+ [0.4.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.4.0...v0.4.1
1257
+ [0.4.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.3.0...v0.4.0
1060
1258
  [0.3.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.2.1...v0.3.0
1061
1259
  [0.2.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.2.0...v0.2.1
1062
1260
  [0.2.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.1.3...v0.2.0
@@ -29,8 +29,8 @@ import com.mrousavy.camera.frameprocessors.VisionCameraProxy
29
29
  * 3. Call `IncrementalStitcher.consumeFrameFromPlugin(image, …)`
30
30
  * which:
31
31
  * - Drops the call if `frameSourceMode != "frameProcessor"`
32
- * (prevents double-feeding the engine alongside the legacy
33
- * `processFrameAtPath` path).
32
+ * (prevents double-feeding the engine alongside the
33
+ * AR-mode `ingestFromARCameraView` path).
34
34
  * - Otherwise: extracts the Y plane, evaluates the keyframe
35
35
  * gate via `KeyframeGate.evaluateWithFrame`, encodes the
36
36
  * accepted frame to JPEG synchronously, and hands the path
@@ -197,7 +197,7 @@ internal class IncrementalFirstwinsEngine(
197
197
  }
198
198
  val frameBGR = downsampleToCompose(srcRaw)
199
199
  if (frameBGR !== srcRaw) srcRaw.release()
200
- val tele = addFrameMat(
200
+ return addFrameMat(
201
201
  frameBGR,
202
202
  qx, qy, qz, qw,
203
203
  fx, fy, cx, cy,
@@ -206,8 +206,6 @@ internal class IncrementalFirstwinsEngine(
206
206
  fovHorizDegrees, fovVertDegrees,
207
207
  t0,
208
208
  )
209
- f8_6_logPerf("firstwins/jpeg", t0, tele.outcome)
210
- return tele
211
209
  }
212
210
 
213
211
  /**
@@ -283,7 +281,7 @@ internal class IncrementalFirstwinsEngine(
283
281
  }
284
282
  val frameBGR = downsampleToCompose(srcRaw)
285
283
  if (frameBGR !== srcRaw) srcRaw.release()
286
- val tele = addFrameMat(
284
+ return addFrameMat(
287
285
  frameBGR,
288
286
  qx, qy, qz, qw,
289
287
  fx, fy, cx, cy,
@@ -292,32 +290,6 @@ internal class IncrementalFirstwinsEngine(
292
290
  fovHorizDegrees, fovVertDegrees,
293
291
  t0,
294
292
  )
295
- f8_6_logPerf("firstwins/pixel", t0, tele.outcome)
296
- return tele
297
- }
298
-
299
- /**
300
- * F8.6 perf-diagnostic counter. Logs ingest timing every Nth
301
- * call so a single capture session yields enough samples to
302
- * eyeball the JPEG-path vs pixel-data-path delta. Remove this
303
- * once F8.6 is verified in production.
304
- */
305
- @Volatile private var f8_6_perfCallCounter: Long = 0L
306
- private fun f8_6_logPerf(
307
- path: String,
308
- t0Nanos: Long,
309
- outcome: FrameOutcome,
310
- ) {
311
- val n = ++f8_6_perfCallCounter
312
- // Every 5th call ≈ ~1 line/sec at 5 Hz live-engine rate;
313
- // first call always logs so we see something on capture
314
- // start without waiting.
315
- if (n == 1L || n % 5L == 0L) {
316
- Log.i(
317
- "F8.6-perf",
318
- "$path took ${msSince(t0Nanos)}ms outcome=$outcome (call #$n)",
319
- )
320
- }
321
293
  }
322
294
 
323
295
  /**