react-native-image-stitcher 0.6.0 → 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.
- package/CHANGELOG.md +89 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +35 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/dist/stitching/incremental.d.ts +69 -0
- package/dist/stitching/useKeyframeStream.d.ts +69 -0
- package/dist/stitching/useKeyframeStream.js +120 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +23 -2
- package/package.json +1 -1
- package/src/index.ts +6 -1
- package/src/stitching/incremental.ts +74 -0
- package/src/stitching/useKeyframeStream.ts +127 -0
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,93 @@ 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
|
+
|
|
19
106
|
## [0.6.0] — 2026-05-25
|
|
20
107
|
|
|
21
108
|
> [!WARNING]
|
|
@@ -1161,7 +1248,8 @@ Native module names also changed:
|
|
|
1161
1248
|
- iOS pod: `RetaiLensCaptureSDK` → `RNImageStitcher`
|
|
1162
1249
|
- iOS xcframework: shipped as `opencv2.xcframework` (linked from `RNImageStitcher.podspec`)
|
|
1163
1250
|
|
|
1164
|
-
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.
|
|
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
|
|
1165
1253
|
[0.6.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.5.1...v0.6.0
|
|
1166
1254
|
[0.5.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.5.0...v0.5.1
|
|
1167
1255
|
[0.5.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.4.1...v0.5.0
|
|
@@ -1174,6 +1174,15 @@ class IncrementalStitcher(
|
|
|
1174
1174
|
keyframeMax = keyframeGate.maxCount,
|
|
1175
1175
|
isLandscape = imageWidth >= imageHeight,
|
|
1176
1176
|
newContentFraction = decision.newContentFraction,
|
|
1177
|
+
// v0.7.0 — Tier 1 hook: pose snapshot + accept timestamp
|
|
1178
|
+
// threaded through to JS via the existing state-update
|
|
1179
|
+
// channel. `tx,ty,tz,qx,qy,qz,qw` are parameters of
|
|
1180
|
+
// `ingestFromARCameraView`; in AR mode they're real
|
|
1181
|
+
// ARCore pose components, in non-AR mode they're
|
|
1182
|
+
// gyro-synthesised (translation ≈ 0).
|
|
1183
|
+
poseQx = qx, poseQy = qy, poseQz = qz, poseQw = qw,
|
|
1184
|
+
poseTx = tx, poseTy = ty, poseTz = tz,
|
|
1185
|
+
acceptedAtMs = System.currentTimeMillis(),
|
|
1177
1186
|
)
|
|
1178
1187
|
return
|
|
1179
1188
|
}
|
|
@@ -2034,6 +2043,13 @@ class IncrementalStitcher(
|
|
|
2034
2043
|
// "unknown" behaviour for call sites that don't have a
|
|
2035
2044
|
// decision in hand.
|
|
2036
2045
|
newContentFraction: Double,
|
|
2046
|
+
// v0.7.0 — Tier 1 hook fields. Pose is the AR pose at the
|
|
2047
|
+
// accept moment (gyro-synthesised in non-AR mode — translation
|
|
2048
|
+
// reads as ~zeros). `acceptedAtMs` is wall-clock ms since
|
|
2049
|
+
// Unix epoch; matches `Date.now()` on the JS side.
|
|
2050
|
+
poseQx: Double, poseQy: Double, poseQz: Double, poseQw: Double,
|
|
2051
|
+
poseTx: Double, poseTy: Double, poseTz: Double,
|
|
2052
|
+
acceptedAtMs: Long,
|
|
2037
2053
|
) {
|
|
2038
2054
|
val state = Arguments.createMap()
|
|
2039
2055
|
state.putNull("panoramaPath")
|
|
@@ -2063,6 +2079,25 @@ class IncrementalStitcher(
|
|
|
2063
2079
|
// emitter).
|
|
2064
2080
|
state.putString("batchKeyframeThumbnailPath", thumbnailPath)
|
|
2065
2081
|
state.putInt("batchKeyframeIndex", keyframeIndex)
|
|
2082
|
+
// v0.7.0 — Tier 1 hook (useKeyframeStream) reads these. See
|
|
2083
|
+
// `AcceptedKeyframe` in src/stitching/incremental.ts. Translation
|
|
2084
|
+
// is always emitted; AR mode populates it from the camera
|
|
2085
|
+
// transform, non-AR mode reads ~zeros (gyro-only, no spatial
|
|
2086
|
+
// anchor).
|
|
2087
|
+
val pose = Arguments.createMap()
|
|
2088
|
+
val rotation = Arguments.createArray()
|
|
2089
|
+
rotation.pushDouble(poseQx)
|
|
2090
|
+
rotation.pushDouble(poseQy)
|
|
2091
|
+
rotation.pushDouble(poseQz)
|
|
2092
|
+
rotation.pushDouble(poseQw)
|
|
2093
|
+
pose.putArray("rotation", rotation)
|
|
2094
|
+
val translation = Arguments.createArray()
|
|
2095
|
+
translation.pushDouble(poseTx)
|
|
2096
|
+
translation.pushDouble(poseTy)
|
|
2097
|
+
translation.pushDouble(poseTz)
|
|
2098
|
+
pose.putArray("translation", translation)
|
|
2099
|
+
state.putMap("batchKeyframePose", pose)
|
|
2100
|
+
state.putDouble("batchKeyframeAcceptedAtMs", acceptedAtMs.toDouble())
|
|
2066
2101
|
emitState(state)
|
|
2067
2102
|
}
|
|
2068
2103
|
|
package/dist/index.d.ts
CHANGED
|
@@ -62,8 +62,9 @@ export type { TakePhotoCallOptions } from './camera/useCapture';
|
|
|
62
62
|
export { useVideoCapture } from './camera/useVideoCapture';
|
|
63
63
|
export { useDeviceOrientation } from './camera/useDeviceOrientation';
|
|
64
64
|
export { IncrementalOutcome, incrementalStitcherIsAvailable, subscribeIncrementalState, getIncrementalNativeModule, cleanupOldKeyframes, } from './stitching/incremental';
|
|
65
|
-
export type { IncrementalState } from './stitching/incremental';
|
|
65
|
+
export type { IncrementalState, AcceptedKeyframe } from './stitching/incremental';
|
|
66
66
|
export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
|
|
67
|
+
export { useKeyframeStream } from './stitching/useKeyframeStream';
|
|
67
68
|
export { useFrameProcessorDriver } from './stitching/useFrameProcessorDriver';
|
|
68
69
|
export type { UseFrameProcessorDriverOptions, FrameProcessorDriverHandle, } from './stitching/useFrameProcessorDriver';
|
|
69
70
|
export { stitchVideo } from './stitching/stitchVideo';
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* adds RetaiLens-specific features on top.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.stitchVideo = exports.useFrameProcessorDriver = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
25
|
+
exports.stitchVideo = exports.useFrameProcessorDriver = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
26
26
|
// ─────────────────────────────────────────────────────────────────────
|
|
27
27
|
// Layer 1 — the high-level <Camera> component
|
|
28
28
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -139,6 +139,12 @@ Object.defineProperty(exports, "getIncrementalNativeModule", { enumerable: true,
|
|
|
139
139
|
Object.defineProperty(exports, "cleanupOldKeyframes", { enumerable: true, get: function () { return incremental_1.cleanupOldKeyframes; } });
|
|
140
140
|
var useIncrementalStitcher_1 = require("./stitching/useIncrementalStitcher");
|
|
141
141
|
Object.defineProperty(exports, "useIncrementalStitcher", { enumerable: true, get: function () { return useIncrementalStitcher_1.useIncrementalStitcher; } });
|
|
142
|
+
// v0.7.0 — Tier 1 subscriber API. Fires on each accepted keyframe
|
|
143
|
+
// in batch-keyframe captures (see hook's docstring for engine-mode
|
|
144
|
+
// caveat). Foundation for plugin-pattern host features (OCR per
|
|
145
|
+
// keyframe, packet detection, server-side analysis, etc.).
|
|
146
|
+
var useKeyframeStream_1 = require("./stitching/useKeyframeStream");
|
|
147
|
+
Object.defineProperty(exports, "useKeyframeStream", { enumerable: true, get: function () { return useKeyframeStream_1.useKeyframeStream; } });
|
|
142
148
|
// vision-camera Frame Processor driver for non-AR captures. As
|
|
143
149
|
// of v0.6 the only non-AR driver exported (the legacy
|
|
144
150
|
// `useIncrementalJSDriver` was removed; was deprecated in v0.5).
|
|
@@ -54,6 +54,48 @@ export declare enum IncrementalOutcome {
|
|
|
54
54
|
*/
|
|
55
55
|
SkippedKeyframeMaxReached = 9
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* v0.7.0 (Tier 1) — public payload type for an accepted keyframe.
|
|
59
|
+
* Delivered to subscribers of the `useKeyframeStream` hook.
|
|
60
|
+
*
|
|
61
|
+
* Emits once per keyframe accepted by the stitching engine — typically
|
|
62
|
+
* 4-6 times per panorama, not per camera frame. Use for low-frequency
|
|
63
|
+
* per-keyframe host work (OCR on the saved JPEG, packet detection,
|
|
64
|
+
* server-side analysis, analytics, etc.).
|
|
65
|
+
*
|
|
66
|
+
* Caveat: only the `batch-keyframe` engine emits these events as of
|
|
67
|
+
* v0.7.0. Live engines (`firstwins-rectilinear`, `hybrid`,
|
|
68
|
+
* `slitscan-*`) paint into a live canvas instead of saving per-accept
|
|
69
|
+
* JPEGs and do not currently surface accept events through this
|
|
70
|
+
* channel; the hook silently does not fire there. A v0.7.1 follow-up
|
|
71
|
+
* may add live-engine accept emit if a real consumer needs it.
|
|
72
|
+
*
|
|
73
|
+
* The JPEG at `jpegPath` is the engine's own copy under the active
|
|
74
|
+
* capture's session directory. It persists for the lifetime of the
|
|
75
|
+
* panorama and is cleaned up automatically when the panorama finalises
|
|
76
|
+
* or is abandoned (or via explicit `cleanupKeyframes`). Host code
|
|
77
|
+
* wanting to retain it long-term must copy synchronously inside the
|
|
78
|
+
* handler.
|
|
79
|
+
*/
|
|
80
|
+
export interface AcceptedKeyframe {
|
|
81
|
+
/** Absolute filesystem path to the keyframe JPEG. No `file://` prefix. */
|
|
82
|
+
jpegPath: string;
|
|
83
|
+
/**
|
|
84
|
+
* Pose snapshot at the moment of acceptance. Quaternion
|
|
85
|
+
* convention: `(x, y, z, w)`; lib uses
|
|
86
|
+
* `q = q_yaw * q_pitch * q_roll`. Translation in metres (world
|
|
87
|
+
* coords) is present in AR mode and undefined in non-AR mode (no
|
|
88
|
+
* spatial anchor — only gyro-derived rotation is available).
|
|
89
|
+
*/
|
|
90
|
+
pose: {
|
|
91
|
+
rotation: [number, number, number, number];
|
|
92
|
+
translation?: [number, number, number];
|
|
93
|
+
};
|
|
94
|
+
/** Milliseconds since the Unix epoch when the engine accepted this keyframe. */
|
|
95
|
+
timestamp: number;
|
|
96
|
+
/** Zero-based index of this keyframe within the in-progress panorama. */
|
|
97
|
+
index: number;
|
|
98
|
+
}
|
|
57
99
|
export interface IncrementalState {
|
|
58
100
|
/**
|
|
59
101
|
* Path to the latest panorama snapshot JPEG (file path, no
|
|
@@ -145,6 +187,33 @@ export interface IncrementalState {
|
|
|
145
187
|
* for the thumbnail strip.
|
|
146
188
|
*/
|
|
147
189
|
batchKeyframeIndex?: number;
|
|
190
|
+
/**
|
|
191
|
+
* v0.7.0 (Tier 1) — pose snapshot at the moment the engine
|
|
192
|
+
* accepted this keyframe. Populated alongside
|
|
193
|
+
* `batchKeyframeThumbnailPath` + `batchKeyframeIndex` on the
|
|
194
|
+
* keyframe-accepted state emit from the `batch-keyframe` engine.
|
|
195
|
+
* Undefined for other engines and for non-accept events.
|
|
196
|
+
*
|
|
197
|
+
* Quaternion convention: `(x, y, z, w)`; lib uses
|
|
198
|
+
* `q = q_yaw * q_pitch * q_roll`. AR mode populates `translation`
|
|
199
|
+
* from the AR camera transform (metres, world coords). Non-AR
|
|
200
|
+
* mode omits `translation` (no spatial anchor — only gyro-derived
|
|
201
|
+
* rotation is available).
|
|
202
|
+
*
|
|
203
|
+
* Foundation for the `useKeyframeStream` Tier 1 host hook.
|
|
204
|
+
*/
|
|
205
|
+
batchKeyframePose?: {
|
|
206
|
+
rotation: [number, number, number, number];
|
|
207
|
+
translation?: [number, number, number];
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* v0.7.0 (Tier 1) — monotonic timestamp (milliseconds since the
|
|
211
|
+
* Unix epoch) when the engine accepted this keyframe. Populated
|
|
212
|
+
* alongside the other `batchKeyframe*` fields on the
|
|
213
|
+
* keyframe-accepted emit. Undefined for other engines and for
|
|
214
|
+
* non-accept events.
|
|
215
|
+
*/
|
|
216
|
+
batchKeyframeAcceptedAtMs?: number;
|
|
148
217
|
/**
|
|
149
218
|
* 2026-05-16 — realtime+batch fusion (Option A "Replace on
|
|
150
219
|
* completion"). True between the moment a hybrid-engine
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type AcceptedKeyframe } from './incremental';
|
|
2
|
+
/**
|
|
3
|
+
* v0.7.0 — Tier 1: subscribe to accepted-keyframe events while a
|
|
4
|
+
* panorama is in progress.
|
|
5
|
+
*
|
|
6
|
+
* Fires once per keyframe accepted by the stitching engine — typically
|
|
7
|
+
* 4-6 times per panorama, NOT per camera frame. Use for low-frequency
|
|
8
|
+
* per-keyframe host work such as OCR on the saved JPEG, packet
|
|
9
|
+
* detection, server-side analysis, or analytics.
|
|
10
|
+
*
|
|
11
|
+
* For mid-frequency frame access (sampled stream), see `useFrameStream`
|
|
12
|
+
* (v0.9.0+). For per-frame worklet access (~30 Hz), see
|
|
13
|
+
* `useFrameProcessor` (v0.8.0+).
|
|
14
|
+
*
|
|
15
|
+
* ## Engine-mode caveat (v0.7.0)
|
|
16
|
+
*
|
|
17
|
+
* Only the `batch-keyframe` engine emits these events. Live engines
|
|
18
|
+
* (`firstwins-rectilinear`, `hybrid`, `slitscan-*`) paint into a live
|
|
19
|
+
* canvas instead of saving per-accept JPEGs, and do not surface accept
|
|
20
|
+
* events through this channel — the hook silently does not fire when
|
|
21
|
+
* such an engine is active. A v0.7.1 follow-up may add live-engine
|
|
22
|
+
* accept emit if a real consumer needs it.
|
|
23
|
+
*
|
|
24
|
+
* ## Payload
|
|
25
|
+
*
|
|
26
|
+
* The handler receives an {@link AcceptedKeyframe}:
|
|
27
|
+
*
|
|
28
|
+
* - `jpegPath`: absolute filesystem path, no `file://` prefix. The
|
|
29
|
+
* JPEG is the engine's own copy under the active capture's session
|
|
30
|
+
* directory. It persists for the lifetime of the panorama and is
|
|
31
|
+
* cleaned up automatically when the panorama finalises or is
|
|
32
|
+
* abandoned (or via explicit `cleanupOldKeyframes`). Copy
|
|
33
|
+
* synchronously inside the handler if long-term retention is
|
|
34
|
+
* needed.
|
|
35
|
+
* - `pose`: rotation quaternion (always present) + optional
|
|
36
|
+
* translation vector (populated in AR mode; undefined in non-AR).
|
|
37
|
+
* - `timestamp`: milliseconds since the Unix epoch.
|
|
38
|
+
* - `index`: zero-based keyframe position in the current panorama.
|
|
39
|
+
*
|
|
40
|
+
* ## Lifecycle
|
|
41
|
+
*
|
|
42
|
+
* Re-subscribes on `handler` identity changes. Wrap the handler in
|
|
43
|
+
* `useCallback` if it closes over state or props you don't want to
|
|
44
|
+
* trigger re-subscription on every render.
|
|
45
|
+
*
|
|
46
|
+
* Async handlers are fire-and-forget. Rejected promises are caught
|
|
47
|
+
* and logged via `console.error`; no backpressure on the native side.
|
|
48
|
+
* Host code wanting to serialise work across keyframes should manage
|
|
49
|
+
* that itself (e.g., push into a queue + worker).
|
|
50
|
+
*
|
|
51
|
+
* ## Example
|
|
52
|
+
*
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import { useCallback } from 'react';
|
|
55
|
+
* import { useKeyframeStream } from 'react-native-image-stitcher';
|
|
56
|
+
*
|
|
57
|
+
* function OcrPlugin() {
|
|
58
|
+
* useKeyframeStream(
|
|
59
|
+
* useCallback(async (kf) => {
|
|
60
|
+
* const text = await runOCR(kf.jpegPath);
|
|
61
|
+
* console.log(`Keyframe ${kf.index} pose=${kf.pose.rotation}:`, text);
|
|
62
|
+
* }, []),
|
|
63
|
+
* );
|
|
64
|
+
* return null;
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function useKeyframeStream(handler: (keyframe: AcceptedKeyframe) => void | Promise<void>): void;
|
|
69
|
+
//# sourceMappingURL=useKeyframeStream.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.useKeyframeStream = useKeyframeStream;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const incremental_1 = require("./incremental");
|
|
7
|
+
/**
|
|
8
|
+
* v0.7.0 — Tier 1: subscribe to accepted-keyframe events while a
|
|
9
|
+
* panorama is in progress.
|
|
10
|
+
*
|
|
11
|
+
* Fires once per keyframe accepted by the stitching engine — typically
|
|
12
|
+
* 4-6 times per panorama, NOT per camera frame. Use for low-frequency
|
|
13
|
+
* per-keyframe host work such as OCR on the saved JPEG, packet
|
|
14
|
+
* detection, server-side analysis, or analytics.
|
|
15
|
+
*
|
|
16
|
+
* For mid-frequency frame access (sampled stream), see `useFrameStream`
|
|
17
|
+
* (v0.9.0+). For per-frame worklet access (~30 Hz), see
|
|
18
|
+
* `useFrameProcessor` (v0.8.0+).
|
|
19
|
+
*
|
|
20
|
+
* ## Engine-mode caveat (v0.7.0)
|
|
21
|
+
*
|
|
22
|
+
* Only the `batch-keyframe` engine emits these events. Live engines
|
|
23
|
+
* (`firstwins-rectilinear`, `hybrid`, `slitscan-*`) paint into a live
|
|
24
|
+
* canvas instead of saving per-accept JPEGs, and do not surface accept
|
|
25
|
+
* events through this channel — the hook silently does not fire when
|
|
26
|
+
* such an engine is active. A v0.7.1 follow-up may add live-engine
|
|
27
|
+
* accept emit if a real consumer needs it.
|
|
28
|
+
*
|
|
29
|
+
* ## Payload
|
|
30
|
+
*
|
|
31
|
+
* The handler receives an {@link AcceptedKeyframe}:
|
|
32
|
+
*
|
|
33
|
+
* - `jpegPath`: absolute filesystem path, no `file://` prefix. The
|
|
34
|
+
* JPEG is the engine's own copy under the active capture's session
|
|
35
|
+
* directory. It persists for the lifetime of the panorama and is
|
|
36
|
+
* cleaned up automatically when the panorama finalises or is
|
|
37
|
+
* abandoned (or via explicit `cleanupOldKeyframes`). Copy
|
|
38
|
+
* synchronously inside the handler if long-term retention is
|
|
39
|
+
* needed.
|
|
40
|
+
* - `pose`: rotation quaternion (always present) + optional
|
|
41
|
+
* translation vector (populated in AR mode; undefined in non-AR).
|
|
42
|
+
* - `timestamp`: milliseconds since the Unix epoch.
|
|
43
|
+
* - `index`: zero-based keyframe position in the current panorama.
|
|
44
|
+
*
|
|
45
|
+
* ## Lifecycle
|
|
46
|
+
*
|
|
47
|
+
* Re-subscribes on `handler` identity changes. Wrap the handler in
|
|
48
|
+
* `useCallback` if it closes over state or props you don't want to
|
|
49
|
+
* trigger re-subscription on every render.
|
|
50
|
+
*
|
|
51
|
+
* Async handlers are fire-and-forget. Rejected promises are caught
|
|
52
|
+
* and logged via `console.error`; no backpressure on the native side.
|
|
53
|
+
* Host code wanting to serialise work across keyframes should manage
|
|
54
|
+
* that itself (e.g., push into a queue + worker).
|
|
55
|
+
*
|
|
56
|
+
* ## Example
|
|
57
|
+
*
|
|
58
|
+
* ```tsx
|
|
59
|
+
* import { useCallback } from 'react';
|
|
60
|
+
* import { useKeyframeStream } from 'react-native-image-stitcher';
|
|
61
|
+
*
|
|
62
|
+
* function OcrPlugin() {
|
|
63
|
+
* useKeyframeStream(
|
|
64
|
+
* useCallback(async (kf) => {
|
|
65
|
+
* const text = await runOCR(kf.jpegPath);
|
|
66
|
+
* console.log(`Keyframe ${kf.index} pose=${kf.pose.rotation}:`, text);
|
|
67
|
+
* }, []),
|
|
68
|
+
* );
|
|
69
|
+
* return null;
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
function useKeyframeStream(handler) {
|
|
74
|
+
(0, react_1.useEffect)(() => {
|
|
75
|
+
const sub = (0, incremental_1.subscribeIncrementalState)((state) => {
|
|
76
|
+
// The `batch-keyframe` engine emits four optional fields together
|
|
77
|
+
// on accept events. Non-accept emits (snapshot updates,
|
|
78
|
+
// refinement progress, live-engine state ticks, etc.) leave
|
|
79
|
+
// `batchKeyframeThumbnailPath` undefined — that's our
|
|
80
|
+
// accept-event sentinel.
|
|
81
|
+
const jpegPath = state.batchKeyframeThumbnailPath;
|
|
82
|
+
const index = state.batchKeyframeIndex;
|
|
83
|
+
if (jpegPath === undefined || index === undefined) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// `batchKeyframePose` + `batchKeyframeAcceptedAtMs` are
|
|
87
|
+
// populated alongside the path + index by the post-v0.7.0
|
|
88
|
+
// native emit. Defensive defaults guard against a host
|
|
89
|
+
// running on a slightly-older native binary (e.g., during a
|
|
90
|
+
// partial upgrade) — identity quaternion + `Date.now()`.
|
|
91
|
+
// Published v0.7.0 native always populates both.
|
|
92
|
+
const pose = state.batchKeyframePose ?? {
|
|
93
|
+
rotation: [0, 0, 0, 1],
|
|
94
|
+
};
|
|
95
|
+
const timestamp = state.batchKeyframeAcceptedAtMs ?? Date.now();
|
|
96
|
+
const keyframe = {
|
|
97
|
+
jpegPath,
|
|
98
|
+
pose,
|
|
99
|
+
timestamp,
|
|
100
|
+
index,
|
|
101
|
+
};
|
|
102
|
+
// Fire-and-forget. Async handler rejections are surfaced via
|
|
103
|
+
// console.error so they don't disappear into the void.
|
|
104
|
+
const result = handler(keyframe);
|
|
105
|
+
if (result && typeof result.catch === 'function') {
|
|
106
|
+
result.catch((err) => {
|
|
107
|
+
// eslint-disable-next-line no-console
|
|
108
|
+
console.error('[useKeyframeStream] handler threw:', err);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// `subscribeIncrementalState` returns null when the native module
|
|
113
|
+
// isn't linked (Expo Go, unit tests without the bridge, etc.).
|
|
114
|
+
// In that case we have nothing to clean up.
|
|
115
|
+
if (sub === null)
|
|
116
|
+
return;
|
|
117
|
+
return () => sub.remove();
|
|
118
|
+
}, [handler]);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=useKeyframeStream.js.map
|
|
@@ -2160,7 +2160,13 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2160
2160
|
keyframeIndex: Int,
|
|
2161
2161
|
keyframeCount: Int,
|
|
2162
2162
|
keyframeMax: Int,
|
|
2163
|
-
isLandscape: Bool
|
|
2163
|
+
isLandscape: Bool,
|
|
2164
|
+
// v0.7.0 — Tier 1 hook fields. `pose` is the AR pose at the
|
|
2165
|
+
// accept moment (gyro-synthesised in non-AR mode — translation
|
|
2166
|
+
// will read as ~zeros). `acceptedAtMs` is wall-clock ms since
|
|
2167
|
+
// Unix epoch; matches `Date.now()` on the JS side.
|
|
2168
|
+
pose: RNSARFramePose,
|
|
2169
|
+
acceptedAtMs: Double
|
|
2164
2170
|
) {
|
|
2165
2171
|
let state = IncrementalStateObject(
|
|
2166
2172
|
panoramaPath: nil,
|
|
@@ -2184,6 +2190,16 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2184
2190
|
// carry — JS reads these directly from the userInfo blob.
|
|
2185
2191
|
dict["batchKeyframeThumbnailPath"] = thumbnailPath
|
|
2186
2192
|
dict["batchKeyframeIndex"] = keyframeIndex
|
|
2193
|
+
// v0.7.0 — Tier 1 hook (useKeyframeStream) reads these. See
|
|
2194
|
+
// `AcceptedKeyframe` in src/stitching/incremental.ts. Translation
|
|
2195
|
+
// is always emitted; AR mode populates it from the camera
|
|
2196
|
+
// transform, non-AR mode reads ~zeros (gyro-only, no spatial
|
|
2197
|
+
// anchor).
|
|
2198
|
+
dict["batchKeyframePose"] = [
|
|
2199
|
+
"rotation": [pose.qx, pose.qy, pose.qz, pose.qw],
|
|
2200
|
+
"translation": [pose.tx, pose.ty, pose.tz],
|
|
2201
|
+
] as [String: Any]
|
|
2202
|
+
dict["batchKeyframeAcceptedAtMs"] = acceptedAtMs
|
|
2187
2203
|
NotificationCenter.default.post(
|
|
2188
2204
|
name: .retailensIncrementalStateUpdate,
|
|
2189
2205
|
object: nil,
|
|
@@ -2637,7 +2653,12 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2637
2653
|
keyframeIndex: Int(record.index),
|
|
2638
2654
|
keyframeCount: count,
|
|
2639
2655
|
keyframeMax: self.keyframeGate.maxCount,
|
|
2640
|
-
isLandscape: pose.imageWidth >= pose.imageHeight
|
|
2656
|
+
isLandscape: pose.imageWidth >= pose.imageHeight,
|
|
2657
|
+
// v0.7.0 — Tier 1 hook: pose snapshot + accept
|
|
2658
|
+
// timestamp threaded through to JS via the
|
|
2659
|
+
// existing state-update channel.
|
|
2660
|
+
pose: pose,
|
|
2661
|
+
acceptedAtMs: Date().timeIntervalSince1970 * 1000
|
|
2641
2662
|
)
|
|
2642
2663
|
} catch let err as NSError {
|
|
2643
2664
|
os_log(.fault, log: Self.diagLog,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-image-stitcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -175,8 +175,13 @@ export {
|
|
|
175
175
|
getIncrementalNativeModule,
|
|
176
176
|
cleanupOldKeyframes,
|
|
177
177
|
} from './stitching/incremental';
|
|
178
|
-
export type { IncrementalState } from './stitching/incremental';
|
|
178
|
+
export type { IncrementalState, AcceptedKeyframe } from './stitching/incremental';
|
|
179
179
|
export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
|
|
180
|
+
// v0.7.0 — Tier 1 subscriber API. Fires on each accepted keyframe
|
|
181
|
+
// in batch-keyframe captures (see hook's docstring for engine-mode
|
|
182
|
+
// caveat). Foundation for plugin-pattern host features (OCR per
|
|
183
|
+
// keyframe, packet detection, server-side analysis, etc.).
|
|
184
|
+
export { useKeyframeStream } from './stitching/useKeyframeStream';
|
|
180
185
|
// vision-camera Frame Processor driver for non-AR captures. As
|
|
181
186
|
// of v0.6 the only non-AR driver exported (the legacy
|
|
182
187
|
// `useIncrementalJSDriver` was removed; was deprecated in v0.5).
|
|
@@ -61,6 +61,53 @@ export enum IncrementalOutcome {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* v0.7.0 (Tier 1) — public payload type for an accepted keyframe.
|
|
66
|
+
* Delivered to subscribers of the `useKeyframeStream` hook.
|
|
67
|
+
*
|
|
68
|
+
* Emits once per keyframe accepted by the stitching engine — typically
|
|
69
|
+
* 4-6 times per panorama, not per camera frame. Use for low-frequency
|
|
70
|
+
* per-keyframe host work (OCR on the saved JPEG, packet detection,
|
|
71
|
+
* server-side analysis, analytics, etc.).
|
|
72
|
+
*
|
|
73
|
+
* Caveat: only the `batch-keyframe` engine emits these events as of
|
|
74
|
+
* v0.7.0. Live engines (`firstwins-rectilinear`, `hybrid`,
|
|
75
|
+
* `slitscan-*`) paint into a live canvas instead of saving per-accept
|
|
76
|
+
* JPEGs and do not currently surface accept events through this
|
|
77
|
+
* channel; the hook silently does not fire there. A v0.7.1 follow-up
|
|
78
|
+
* may add live-engine accept emit if a real consumer needs it.
|
|
79
|
+
*
|
|
80
|
+
* The JPEG at `jpegPath` is the engine's own copy under the active
|
|
81
|
+
* capture's session directory. It persists for the lifetime of the
|
|
82
|
+
* panorama and is cleaned up automatically when the panorama finalises
|
|
83
|
+
* or is abandoned (or via explicit `cleanupKeyframes`). Host code
|
|
84
|
+
* wanting to retain it long-term must copy synchronously inside the
|
|
85
|
+
* handler.
|
|
86
|
+
*/
|
|
87
|
+
export interface AcceptedKeyframe {
|
|
88
|
+
/** Absolute filesystem path to the keyframe JPEG. No `file://` prefix. */
|
|
89
|
+
jpegPath: string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Pose snapshot at the moment of acceptance. Quaternion
|
|
93
|
+
* convention: `(x, y, z, w)`; lib uses
|
|
94
|
+
* `q = q_yaw * q_pitch * q_roll`. Translation in metres (world
|
|
95
|
+
* coords) is present in AR mode and undefined in non-AR mode (no
|
|
96
|
+
* spatial anchor — only gyro-derived rotation is available).
|
|
97
|
+
*/
|
|
98
|
+
pose: {
|
|
99
|
+
rotation: [number, number, number, number];
|
|
100
|
+
translation?: [number, number, number];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Milliseconds since the Unix epoch when the engine accepted this keyframe. */
|
|
104
|
+
timestamp: number;
|
|
105
|
+
|
|
106
|
+
/** Zero-based index of this keyframe within the in-progress panorama. */
|
|
107
|
+
index: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
64
111
|
export interface IncrementalState {
|
|
65
112
|
/**
|
|
66
113
|
* Path to the latest panorama snapshot JPEG (file path, no
|
|
@@ -152,6 +199,33 @@ export interface IncrementalState {
|
|
|
152
199
|
* for the thumbnail strip.
|
|
153
200
|
*/
|
|
154
201
|
batchKeyframeIndex?: number;
|
|
202
|
+
/**
|
|
203
|
+
* v0.7.0 (Tier 1) — pose snapshot at the moment the engine
|
|
204
|
+
* accepted this keyframe. Populated alongside
|
|
205
|
+
* `batchKeyframeThumbnailPath` + `batchKeyframeIndex` on the
|
|
206
|
+
* keyframe-accepted state emit from the `batch-keyframe` engine.
|
|
207
|
+
* Undefined for other engines and for non-accept events.
|
|
208
|
+
*
|
|
209
|
+
* Quaternion convention: `(x, y, z, w)`; lib uses
|
|
210
|
+
* `q = q_yaw * q_pitch * q_roll`. AR mode populates `translation`
|
|
211
|
+
* from the AR camera transform (metres, world coords). Non-AR
|
|
212
|
+
* mode omits `translation` (no spatial anchor — only gyro-derived
|
|
213
|
+
* rotation is available).
|
|
214
|
+
*
|
|
215
|
+
* Foundation for the `useKeyframeStream` Tier 1 host hook.
|
|
216
|
+
*/
|
|
217
|
+
batchKeyframePose?: {
|
|
218
|
+
rotation: [number, number, number, number];
|
|
219
|
+
translation?: [number, number, number];
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* v0.7.0 (Tier 1) — monotonic timestamp (milliseconds since the
|
|
223
|
+
* Unix epoch) when the engine accepted this keyframe. Populated
|
|
224
|
+
* alongside the other `batchKeyframe*` fields on the
|
|
225
|
+
* keyframe-accepted emit. Undefined for other engines and for
|
|
226
|
+
* non-accept events.
|
|
227
|
+
*/
|
|
228
|
+
batchKeyframeAcceptedAtMs?: number;
|
|
155
229
|
/**
|
|
156
230
|
* 2026-05-16 — realtime+batch fusion (Option A "Replace on
|
|
157
231
|
* completion"). True between the moment a hybrid-engine
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
subscribeIncrementalState,
|
|
7
|
+
type AcceptedKeyframe,
|
|
8
|
+
type IncrementalState,
|
|
9
|
+
} from './incremental';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* v0.7.0 — Tier 1: subscribe to accepted-keyframe events while a
|
|
13
|
+
* panorama is in progress.
|
|
14
|
+
*
|
|
15
|
+
* Fires once per keyframe accepted by the stitching engine — typically
|
|
16
|
+
* 4-6 times per panorama, NOT per camera frame. Use for low-frequency
|
|
17
|
+
* per-keyframe host work such as OCR on the saved JPEG, packet
|
|
18
|
+
* detection, server-side analysis, or analytics.
|
|
19
|
+
*
|
|
20
|
+
* For mid-frequency frame access (sampled stream), see `useFrameStream`
|
|
21
|
+
* (v0.9.0+). For per-frame worklet access (~30 Hz), see
|
|
22
|
+
* `useFrameProcessor` (v0.8.0+).
|
|
23
|
+
*
|
|
24
|
+
* ## Engine-mode caveat (v0.7.0)
|
|
25
|
+
*
|
|
26
|
+
* Only the `batch-keyframe` engine emits these events. Live engines
|
|
27
|
+
* (`firstwins-rectilinear`, `hybrid`, `slitscan-*`) paint into a live
|
|
28
|
+
* canvas instead of saving per-accept JPEGs, and do not surface accept
|
|
29
|
+
* events through this channel — the hook silently does not fire when
|
|
30
|
+
* such an engine is active. A v0.7.1 follow-up may add live-engine
|
|
31
|
+
* accept emit if a real consumer needs it.
|
|
32
|
+
*
|
|
33
|
+
* ## Payload
|
|
34
|
+
*
|
|
35
|
+
* The handler receives an {@link AcceptedKeyframe}:
|
|
36
|
+
*
|
|
37
|
+
* - `jpegPath`: absolute filesystem path, no `file://` prefix. The
|
|
38
|
+
* JPEG is the engine's own copy under the active capture's session
|
|
39
|
+
* directory. It persists for the lifetime of the panorama and is
|
|
40
|
+
* cleaned up automatically when the panorama finalises or is
|
|
41
|
+
* abandoned (or via explicit `cleanupOldKeyframes`). Copy
|
|
42
|
+
* synchronously inside the handler if long-term retention is
|
|
43
|
+
* needed.
|
|
44
|
+
* - `pose`: rotation quaternion (always present) + optional
|
|
45
|
+
* translation vector (populated in AR mode; undefined in non-AR).
|
|
46
|
+
* - `timestamp`: milliseconds since the Unix epoch.
|
|
47
|
+
* - `index`: zero-based keyframe position in the current panorama.
|
|
48
|
+
*
|
|
49
|
+
* ## Lifecycle
|
|
50
|
+
*
|
|
51
|
+
* Re-subscribes on `handler` identity changes. Wrap the handler in
|
|
52
|
+
* `useCallback` if it closes over state or props you don't want to
|
|
53
|
+
* trigger re-subscription on every render.
|
|
54
|
+
*
|
|
55
|
+
* Async handlers are fire-and-forget. Rejected promises are caught
|
|
56
|
+
* and logged via `console.error`; no backpressure on the native side.
|
|
57
|
+
* Host code wanting to serialise work across keyframes should manage
|
|
58
|
+
* that itself (e.g., push into a queue + worker).
|
|
59
|
+
*
|
|
60
|
+
* ## Example
|
|
61
|
+
*
|
|
62
|
+
* ```tsx
|
|
63
|
+
* import { useCallback } from 'react';
|
|
64
|
+
* import { useKeyframeStream } from 'react-native-image-stitcher';
|
|
65
|
+
*
|
|
66
|
+
* function OcrPlugin() {
|
|
67
|
+
* useKeyframeStream(
|
|
68
|
+
* useCallback(async (kf) => {
|
|
69
|
+
* const text = await runOCR(kf.jpegPath);
|
|
70
|
+
* console.log(`Keyframe ${kf.index} pose=${kf.pose.rotation}:`, text);
|
|
71
|
+
* }, []),
|
|
72
|
+
* );
|
|
73
|
+
* return null;
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function useKeyframeStream(
|
|
78
|
+
handler: (keyframe: AcceptedKeyframe) => void | Promise<void>,
|
|
79
|
+
): void {
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const sub = subscribeIncrementalState((state: IncrementalState) => {
|
|
82
|
+
// The `batch-keyframe` engine emits four optional fields together
|
|
83
|
+
// on accept events. Non-accept emits (snapshot updates,
|
|
84
|
+
// refinement progress, live-engine state ticks, etc.) leave
|
|
85
|
+
// `batchKeyframeThumbnailPath` undefined — that's our
|
|
86
|
+
// accept-event sentinel.
|
|
87
|
+
const jpegPath = state.batchKeyframeThumbnailPath;
|
|
88
|
+
const index = state.batchKeyframeIndex;
|
|
89
|
+
if (jpegPath === undefined || index === undefined) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// `batchKeyframePose` + `batchKeyframeAcceptedAtMs` are
|
|
94
|
+
// populated alongside the path + index by the post-v0.7.0
|
|
95
|
+
// native emit. Defensive defaults guard against a host
|
|
96
|
+
// running on a slightly-older native binary (e.g., during a
|
|
97
|
+
// partial upgrade) — identity quaternion + `Date.now()`.
|
|
98
|
+
// Published v0.7.0 native always populates both.
|
|
99
|
+
const pose = state.batchKeyframePose ?? {
|
|
100
|
+
rotation: [0, 0, 0, 1] as [number, number, number, number],
|
|
101
|
+
};
|
|
102
|
+
const timestamp = state.batchKeyframeAcceptedAtMs ?? Date.now();
|
|
103
|
+
|
|
104
|
+
const keyframe: AcceptedKeyframe = {
|
|
105
|
+
jpegPath,
|
|
106
|
+
pose,
|
|
107
|
+
timestamp,
|
|
108
|
+
index,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Fire-and-forget. Async handler rejections are surfaced via
|
|
112
|
+
// console.error so they don't disappear into the void.
|
|
113
|
+
const result = handler(keyframe);
|
|
114
|
+
if (result && typeof (result as Promise<void>).catch === 'function') {
|
|
115
|
+
(result as Promise<void>).catch((err) => {
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.error('[useKeyframeStream] handler threw:', err);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// `subscribeIncrementalState` returns null when the native module
|
|
122
|
+
// isn't linked (Expo Go, unit tests without the bridge, etc.).
|
|
123
|
+
// In that case we have nothing to clean up.
|
|
124
|
+
if (sub === null) return;
|
|
125
|
+
return () => sub.remove();
|
|
126
|
+
}, [handler]);
|
|
127
|
+
}
|