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.
- package/CHANGELOG.md +199 -1
- package/android/src/main/java/io/imagestitcher/rn/CvFlowGateFrameProcessor.kt +2 -2
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +2 -30
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +90 -368
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +6 -3
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +17 -30
- package/dist/camera/Camera.d.ts +11 -27
- package/dist/camera/Camera.js +46 -78
- package/dist/index.d.ts +2 -3
- package/dist/index.js +10 -6
- package/dist/stitching/incremental.d.ts +79 -11
- package/dist/stitching/useFrameProcessorDriver.d.ts +7 -6
- package/dist/stitching/useFrameProcessorDriver.js +12 -11
- package/dist/stitching/useKeyframeStream.d.ts +69 -0
- package/dist/stitching/useKeyframeStream.js +120 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +48 -208
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +0 -8
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +6 -126
- package/ios/Sources/RNImageStitcher/KeyframeGateFrameProcessor.mm +6 -6
- package/package.json +1 -1
- package/src/camera/Camera.tsx +57 -106
- package/src/index.ts +9 -9
- package/src/stitching/incremental.ts +84 -11
- package/src/stitching/useFrameProcessorDriver.ts +12 -11
- package/src/stitching/useKeyframeStream.ts +127 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +0 -74
- package/dist/stitching/useIncrementalJSDriver.js +0 -220
- package/src/stitching/useIncrementalJSDriver.ts +0 -297
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
import Foundation
|
|
21
21
|
import React
|
|
22
22
|
import os.log
|
|
23
|
-
import ImageIO // CGImageSource + kCGImagePropertyOrientation for EXIF read in processFrameAtPath
|
|
24
23
|
|
|
25
24
|
@objc(IncrementalStitcherBridge)
|
|
26
25
|
public final class IncrementalStitcherBridge: RCTEventEmitter {
|
|
@@ -75,10 +74,12 @@ public final class IncrementalStitcherBridge: RCTEventEmitter {
|
|
|
75
74
|
/// Resolves with `{ ok: true }`. Rejects when `frameSourceMode`
|
|
76
75
|
/// (options dict) is 'arSession' (the default) AND the AR session
|
|
77
76
|
/// isn't running — that path needs ARKit to deliver frames.
|
|
78
|
-
/// When `frameSourceMode` is '
|
|
79
|
-
/// skipped and the engine expects
|
|
80
|
-
/// `
|
|
81
|
-
///
|
|
77
|
+
/// When `frameSourceMode` is 'frameProcessor' the AR-session check
|
|
78
|
+
/// is skipped and the engine expects the vision-camera Frame
|
|
79
|
+
/// Processor plugin (`CvFlowGateFrameProcessor`) to feed frames
|
|
80
|
+
/// via `consumeFrameFromPlugin`. The pre-v0.6 'jsDriver' mode
|
|
81
|
+
/// (push frames in from JS via `processFrameAtPath`) has been
|
|
82
|
+
/// removed.
|
|
82
83
|
@objc(start:resolver:rejecter:)
|
|
83
84
|
public func start(
|
|
84
85
|
options: NSDictionary,
|
|
@@ -251,127 +252,6 @@ public final class IncrementalStitcherBridge: RCTEventEmitter {
|
|
|
251
252
|
resolver(["ok": true])
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
/// 2026-05-18 (Issue #2 v2) — JS-driven frame ingestion for iOS
|
|
255
|
-
/// non-AR mode. Mirrors Android's `processFrameAtPath` exactly:
|
|
256
|
-
/// the JPEG at `path` is already saved on disk by vision-camera
|
|
257
|
-
/// in its native EXIF-correct orientation. We DO NOT decode the
|
|
258
|
-
/// image here. Instead:
|
|
259
|
-
///
|
|
260
|
-
/// - Build a synthetic `RNSARFramePose` from the
|
|
261
|
-
/// JS-supplied quaternion + intrinsics (no translation;
|
|
262
|
-
/// non-AR captures don't have it).
|
|
263
|
-
/// - Hand the path + pose to
|
|
264
|
-
/// `IncrementalStitcher.addBatchKeyframePath`, which
|
|
265
|
-
/// evaluates the shared-C++ KeyframeGate and (if accepted)
|
|
266
|
-
/// records the path in the finalize-time keyframe list +
|
|
267
|
-
/// emits the same state event the AR-delegate path emits.
|
|
268
|
-
/// - `cv::imread` at finalize handles EXIF orientation
|
|
269
|
-
/// natively, so the output panorama reads upright with no
|
|
270
|
-
/// iOS-specific orientation handling needed in this bridge.
|
|
271
|
-
///
|
|
272
|
-
/// History: Issue #2 v1 (commit 0e40f17) tried to decode the
|
|
273
|
-
/// JPEG into a CVPixelBuffer and reuse the existing AR
|
|
274
|
-
/// `consumeFrame(pixelBuffer:pose:)` path. That introduced two
|
|
275
|
-
/// orientation bugs (CGContext Y-flip + UIImage.size vs
|
|
276
|
-
/// cgImage.width dim swap) → upside-down output AND canvas-
|
|
277
|
-
/// dimension overflow → OOM crashes (user-reported 2026-05-18).
|
|
278
|
-
/// Architecturally Android never decoded the image either, so
|
|
279
|
-
/// the right fix was to mirror that.
|
|
280
|
-
///
|
|
281
|
-
/// `options` keys:
|
|
282
|
-
/// - path (NSString, required) — local file path (no file://)
|
|
283
|
-
/// - qx, qy, qz, qw (Double, required) — quaternion, JS-side
|
|
284
|
-
/// gyro-integrated
|
|
285
|
-
/// - fx, fy, cx, cy (Double, required) — intrinsics in sensor px
|
|
286
|
-
/// - imageWidth, imageHeight (Int, required)
|
|
287
|
-
/// - trackingPoor (Bool, optional, default false)
|
|
288
|
-
/// - timestampMs (Double, optional, default = now)
|
|
289
|
-
///
|
|
290
|
-
/// Only batch-keyframe captures are supported on this path right
|
|
291
|
-
/// now — other engines (hybrid / firstwins) need real pixel data
|
|
292
|
-
/// during the live phase, which isn't trivially derivable from a
|
|
293
|
-
/// JPEG path. Reject with `E_NOT_BATCH_KEYFRAME` so the JS host
|
|
294
|
-
/// can fall back to the legacy stitchVideo path if needed.
|
|
295
|
-
@objc(processFrameAtPath:resolver:rejecter:)
|
|
296
|
-
public func processFrameAtPath(
|
|
297
|
-
options: NSDictionary,
|
|
298
|
-
resolver: @escaping RCTPromiseResolveBlock,
|
|
299
|
-
rejecter: @escaping RCTPromiseRejectBlock
|
|
300
|
-
) {
|
|
301
|
-
guard let pathRaw = options["path"] as? String, !pathRaw.isEmpty else {
|
|
302
|
-
rejecter("E_NO_PATH", "processFrameAtPath: missing 'path'", nil)
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
// Strip optional file:// prefix — JS callers sometimes send
|
|
306
|
-
// file URIs, native APIs want filesystem paths.
|
|
307
|
-
let cleanPath = pathRaw.hasPrefix("file://")
|
|
308
|
-
? String(pathRaw.dropFirst("file://".count))
|
|
309
|
-
: pathRaw
|
|
310
|
-
|
|
311
|
-
let engine = IncrementalStitcher.shared
|
|
312
|
-
guard engine.isBatchKeyframeMode else {
|
|
313
|
-
rejecter("E_NOT_BATCH_KEYFRAME",
|
|
314
|
-
"processFrameAtPath only supports batch-keyframe "
|
|
315
|
-
+ "engine mode on iOS. Configure "
|
|
316
|
-
+ "incrementalEngine='batch-keyframe' in start() "
|
|
317
|
-
+ "options, or fall back to the stitchVideo path.",
|
|
318
|
-
nil)
|
|
319
|
-
return
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
let qx = (options["qx"] as? Double) ?? 0
|
|
323
|
-
let qy = (options["qy"] as? Double) ?? 0
|
|
324
|
-
let qz = (options["qz"] as? Double) ?? 0
|
|
325
|
-
let qw = (options["qw"] as? Double) ?? 1 // identity quat default
|
|
326
|
-
let fx = (options["fx"] as? Double) ?? 1000.0
|
|
327
|
-
let fy = (options["fy"] as? Double) ?? 1000.0
|
|
328
|
-
let cx = (options["cx"] as? Double) ?? 540.0
|
|
329
|
-
let cy = (options["cy"] as? Double) ?? 960.0
|
|
330
|
-
let imageWidth = (options["imageWidth"] as? Int) ?? 1080
|
|
331
|
-
let imageHeight = (options["imageHeight"] as? Int) ?? 1920
|
|
332
|
-
let trackingPoor = (options["trackingPoor"] as? Bool) ?? false
|
|
333
|
-
let timestampMs = (options["timestampMs"] as? Double)
|
|
334
|
-
?? (Date().timeIntervalSince1970 * 1000.0)
|
|
335
|
-
let trackingState: RNSARTrackingState =
|
|
336
|
-
trackingPoor ? .limited : .tracking
|
|
337
|
-
|
|
338
|
-
let pose = RNSARFramePose(
|
|
339
|
-
tx: 0, ty: 0, tz: 0, // no translation in non-AR
|
|
340
|
-
qx: qx, qy: qy, qz: qz, qw: qw,
|
|
341
|
-
fx: fx, fy: fy, cx: cx, cy: cy,
|
|
342
|
-
imageWidth: imageWidth, imageHeight: imageHeight,
|
|
343
|
-
timestampMs: timestampMs,
|
|
344
|
-
trackingState: trackingState
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
// 2026-05-18 (Iss #1 diag) — read EXIF Orientation tag from the
|
|
348
|
-
// keyframe JPEG before handing it to the engine. vision-camera
|
|
349
|
-
// writes a JPEG with an EXIF tag matching the physical capture
|
|
350
|
-
// orientation (1=no rotation, 3=180°, 6=90°CW, 8=90°CCW). The
|
|
351
|
-
// bake-rotation table in cpp/stitcher.cpp assumes the post-imread
|
|
352
|
-
// Mat is in user-view orientation (post-EXIF apply). If the EXIF
|
|
353
|
-
// tag isn't what we expect for a given physical orientation, the
|
|
354
|
-
// input Mat to cv::Stitcher will be a different shape than the AR
|
|
355
|
-
// path produces (AR keyframes hardcode EXIF=6, commit 7b828f1) —
|
|
356
|
-
// which would explain why iOS non-AR landscape captures stitch
|
|
357
|
-
// but bake the wrong way. CGImageSource is cheap (metadata-only;
|
|
358
|
-
// no decode).
|
|
359
|
-
var exifOrientation: Int = -1
|
|
360
|
-
if let src = CGImageSourceCreateWithURL(
|
|
361
|
-
URL(fileURLWithPath: cleanPath) as CFURL, nil
|
|
362
|
-
),
|
|
363
|
-
let props = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as? [CFString: Any],
|
|
364
|
-
let o = props[kCGImagePropertyOrientation] as? Int {
|
|
365
|
-
exifOrientation = o
|
|
366
|
-
}
|
|
367
|
-
os_log(.fault, log: OSLog(subsystem: "com.tiger.retailens",
|
|
368
|
-
category: "stitcher.diag"),
|
|
369
|
-
"[V16-batch-keyframe.js] processFrameAtPath EXIF=%d imageW=%d imageH=%d path=%{public}@",
|
|
370
|
-
Int32(exifOrientation), Int32(imageWidth), Int32(imageHeight), cleanPath)
|
|
371
|
-
|
|
372
|
-
let accepted = engine.addBatchKeyframePath(path: cleanPath, pose: pose)
|
|
373
|
-
resolver(["ok": true, "accepted": accepted])
|
|
374
|
-
}
|
|
375
255
|
|
|
376
256
|
/// 2026-05-18 (Iss 3) — bridge for `cleanupKeyframes`. See the
|
|
377
257
|
/// Swift method's docstring for behaviour. Options dict keys:
|
|
@@ -140,13 +140,13 @@ static NSInteger kg_argInt(NSDictionary* args, NSString* key, NSInteger defaultV
|
|
|
140
140
|
// Pose from worklet args. Defaults are safe non-AR values:
|
|
141
141
|
// * tx/ty/tz = 0 (no translation in non-AR; gyro only gives rot)
|
|
142
142
|
// * qw = 1 (identity quaternion if JS hasn't supplied rotation)
|
|
143
|
-
// * fx/fy/cx/cy = 0 →
|
|
144
|
-
// engine derives FoV from intrinsics; 0 would
|
|
145
|
-
// We default the principal point to image
|
|
146
|
-
// fallback if only fx/fy are missing.
|
|
143
|
+
// * fx/fy/cx/cy = 0 → the Frame Processor worklet caller MUST
|
|
144
|
+
// supply these (the engine derives FoV from intrinsics; 0 would
|
|
145
|
+
// yield NaN FoV). We default the principal point to image
|
|
146
|
+
// centre as a safer fallback if only fx/fy are missing.
|
|
147
147
|
// * trackingStateRaw = 2 → `.tracking` (non-AR captures don't
|
|
148
|
-
// have a real tracking-quality signal;
|
|
149
|
-
//
|
|
148
|
+
// have a real tracking-quality signal; reporting `.tracking`
|
|
149
|
+
// keeps the engine's `trackingPoor` path inactive).
|
|
150
150
|
double tx = kg_argDouble(arguments, @"tx", 0.0);
|
|
151
151
|
double ty = kg_argDouble(arguments, @"ty", 0.0);
|
|
152
152
|
double tz = kg_argDouble(arguments, @"tz", 0.0);
|
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/camera/Camera.tsx
CHANGED
|
@@ -89,7 +89,6 @@ import {
|
|
|
89
89
|
subscribeIncrementalState,
|
|
90
90
|
type IncrementalState,
|
|
91
91
|
} from '../stitching/incremental';
|
|
92
|
-
import { useIncrementalJSDriver } from '../stitching/useIncrementalJSDriver';
|
|
93
92
|
import { useFrameProcessorDriver } from '../stitching/useFrameProcessorDriver';
|
|
94
93
|
import { useIncrementalStitcher } from '../stitching/useIncrementalStitcher';
|
|
95
94
|
import { useIMUTranslationGate } from '../sensors/useIMUTranslationGate';
|
|
@@ -303,37 +302,20 @@ export interface CameraProps {
|
|
|
303
302
|
* Introduced for F8 (FrameProcessor port) — see
|
|
304
303
|
* `docs/f8-frame-processor-plan.md`.
|
|
305
304
|
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* one-time console.warn — supplying your own worklet would race
|
|
312
|
-
* with the SDK's pixel-buffer feed.
|
|
305
|
+
* The SDK installs its own frame processor via
|
|
306
|
+
* `useFrameProcessorDriver`. Setting this prop is ignored with
|
|
307
|
+
* a one-time `console.warn` — supplying a host worklet would
|
|
308
|
+
* race with the SDK's pixel-buffer feed. Either remove the prop
|
|
309
|
+
* or fork the SDK if you genuinely need a custom worklet.
|
|
313
310
|
*
|
|
314
|
-
*
|
|
315
|
-
* * Default (modern non-AR): SDK owns the worklet, this prop
|
|
316
|
-
* is ignored.
|
|
317
|
-
* * `legacyDriver={true}`: SDK uses the old `useIncrementalJSDriver`
|
|
318
|
-
* (takeSnapshot path). Honoured for diagnostics or as an
|
|
319
|
-
* escape hatch.
|
|
320
|
-
* * AR mode: vision-camera Camera isn't mounted, this prop is
|
|
321
|
-
* irrelevant.
|
|
322
|
-
*/
|
|
323
|
-
frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Opt back into the legacy `useIncrementalJSDriver` for non-AR
|
|
327
|
-
* captures (the v0.4 path: `takeSnapshot` → JPEG → cache file →
|
|
328
|
-
* `IncrementalStitcher.processFrameAtPath`).
|
|
311
|
+
* AR mode is irrelevant: vision-camera's Camera isn't mounted.
|
|
329
312
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* specific issue with the new driver and need to ship a fix.
|
|
313
|
+
* (v0.5 had a `legacyDriver` escape hatch that routed back to
|
|
314
|
+
* `useIncrementalJSDriver`. That hook + prop were removed in
|
|
315
|
+
* v0.6 per the deprecation timeline announced in the v0.5.0
|
|
316
|
+
* CHANGELOG.)
|
|
335
317
|
*/
|
|
336
|
-
|
|
318
|
+
frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
|
|
337
319
|
}
|
|
338
320
|
|
|
339
321
|
|
|
@@ -609,7 +591,6 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
609
591
|
onFramesDropped,
|
|
610
592
|
onError,
|
|
611
593
|
frameProcessor: hostFrameProcessor,
|
|
612
|
-
legacyDriver = false,
|
|
613
594
|
engine = 'batch-keyframe',
|
|
614
595
|
} = props;
|
|
615
596
|
|
|
@@ -790,63 +771,49 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
790
771
|
},
|
|
791
772
|
});
|
|
792
773
|
|
|
793
|
-
//
|
|
794
|
-
// engine consumes frames from the ARSession stream
|
|
795
|
-
// hook stays idle.
|
|
774
|
+
// Frame Processor driver for non-AR captures (iOS + Android).
|
|
775
|
+
// In AR mode the engine consumes frames from the ARSession stream
|
|
776
|
+
// natively, so this hook stays idle.
|
|
796
777
|
//
|
|
797
778
|
// IMPORTANT: start()/stop() are called imperatively from the hold
|
|
798
779
|
// handlers below — NOT from a useEffect driven by statusPhase. The
|
|
799
780
|
// hook returns a fresh object identity on every render, and during
|
|
800
781
|
// a recording the engine emits IncrementalStateUpdate events that
|
|
801
|
-
// cause re-renders multiple times per second. An effect with
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
// imperative pattern (start on hold-start, stop on hold-end) avoids
|
|
809
|
-
// the re-render churn entirely.
|
|
810
|
-
const jsDriver = useIncrementalJSDriver();
|
|
811
|
-
// F8.3 — vision-camera Frame Processor variant. Always
|
|
812
|
-
// instantiated so we don't have conditional hook calls; only one
|
|
813
|
-
// of the two drivers actually .start()s per capture. Stop() on
|
|
814
|
-
// an idle driver is a no-op.
|
|
782
|
+
// cause re-renders multiple times per second. An effect with the
|
|
783
|
+
// driver in its deps would teardown + restart on every event,
|
|
784
|
+
// resetting the gyro accumulator (yaw/pitch) to zero each cycle.
|
|
785
|
+
// User-visible symptom: "only the first keyframe is accepted, every
|
|
786
|
+
// subsequent ingest sees pose=(0,0) and is rejected as a duplicate".
|
|
787
|
+
// The imperative pattern (start on hold-start, stop on hold-end)
|
|
788
|
+
// avoids the re-render churn entirely.
|
|
815
789
|
const fpDriver = useFrameProcessorDriver();
|
|
816
|
-
// Safety:
|
|
817
|
-
// mid-recording. Empty deps so this only fires on unmount.
|
|
790
|
+
// Safety: stop the driver if the component unmounts mid-recording.
|
|
818
791
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
819
|
-
useEffect(() => () => {
|
|
820
|
-
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
792
|
+
useEffect(() => () => { fpDriver.stop(); }, []);
|
|
793
|
+
|
|
794
|
+
// One-shot deprecation warning when the host supplies their own
|
|
795
|
+
// `frameProcessor` prop. Two worklets racing on the same
|
|
796
|
+
// producer thread would corrupt the engine's workQueue ordering,
|
|
797
|
+
// so the SDK's own worklet wins and the host's is silently
|
|
798
|
+
// ignored. (v0.5 had a `legacyDriver` opt-out for hosts that
|
|
799
|
+
// wanted to route around the SDK driver; that was removed in
|
|
800
|
+
// v0.6 along with `useIncrementalJSDriver`.)
|
|
828
801
|
const hostFrameProcessorIgnoredWarnedRef = useRef(false);
|
|
829
802
|
if (
|
|
830
803
|
hostFrameProcessor != null
|
|
831
|
-
&& !legacyDriver
|
|
832
804
|
&& !hostFrameProcessorIgnoredWarnedRef.current
|
|
833
805
|
) {
|
|
834
806
|
hostFrameProcessorIgnoredWarnedRef.current = true;
|
|
835
807
|
// eslint-disable-next-line no-console
|
|
836
808
|
console.warn(
|
|
837
809
|
'[react-native-image-stitcher] The `frameProcessor` prop on '
|
|
838
|
-
+ '<Camera> is ignored
|
|
839
|
-
+ '
|
|
840
|
-
+ '
|
|
810
|
+
+ '<Camera> is ignored — the SDK installs its own worklet '
|
|
811
|
+
+ 'via useFrameProcessorDriver. Remove the prop, or fork '
|
|
812
|
+
+ 'the SDK if you genuinely need a custom worklet.',
|
|
841
813
|
);
|
|
842
814
|
}
|
|
843
|
-
// The Frame Processor worklet
|
|
844
|
-
|
|
845
|
-
// 1. Legacy mode: honor the host's prop (or null).
|
|
846
|
-
// 2. Modern mode: SDK driver's worklet, regardless of host's prop.
|
|
847
|
-
const effectiveFrameProcessor = legacyDriver
|
|
848
|
-
? (hostFrameProcessor ?? null)
|
|
849
|
-
: fpDriver.frameProcessor;
|
|
815
|
+
// The Frame Processor worklet bound to vision-camera's Camera.
|
|
816
|
+
const effectiveFrameProcessor = fpDriver.frameProcessor;
|
|
850
817
|
|
|
851
818
|
// ── Subscribe to engine state for live keyframe thumbs ──────────
|
|
852
819
|
useEffect(() => {
|
|
@@ -903,17 +870,17 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
903
870
|
const accepted = incrementalState?.acceptedCount ?? 0;
|
|
904
871
|
if (accepted > lastAcceptedCountRef.current) {
|
|
905
872
|
lastAcceptedCountRef.current = accepted;
|
|
906
|
-
// F8.3 review-of-review (M3 revert):
|
|
907
|
-
// `legacyDriver` because the Frame
|
|
908
|
-
// consult `imuGate` for its own pose
|
|
909
|
-
// load-bearing side effect:
|
|
910
|
-
// IIR-integrator drift
|
|
911
|
-
// `imuGate.getTotalAbsMetres()` is read
|
|
912
|
-
//
|
|
873
|
+
// F8.3 review-of-review (M3 revert): an earlier draft gated
|
|
874
|
+
// this on the pre-v0.6 `legacyDriver` prop because the Frame
|
|
875
|
+
// Processor driver doesn't consult `imuGate` for its own pose
|
|
876
|
+
// synthesis. That ignored a load-bearing side effect:
|
|
877
|
+
// `imuGate.resetAnchor()` bounds the IIR-integrator drift
|
|
878
|
+
// window per-accept, and `imuGate.getTotalAbsMetres()` is read
|
|
879
|
+
// at finalize time as `imuTranslationMetres` into the native
|
|
913
880
|
// stitchMode auto-resolver (PANORAMA vs SCANS). Without the
|
|
914
881
|
// per-accept reset, long FP-driver captures let IIR drift
|
|
915
|
-
// compound → inflated metres → biased toward SCANS.
|
|
916
|
-
//
|
|
882
|
+
// compound → inflated metres → biased toward SCANS. Now fires
|
|
883
|
+
// for ALL non-AR captures (the only non-AR driver post-v0.6).
|
|
917
884
|
if (isNonAR) {
|
|
918
885
|
imuGate.resetAnchor();
|
|
919
886
|
}
|
|
@@ -1044,13 +1011,11 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1044
1011
|
snapshotEveryNAccepts: 1,
|
|
1045
1012
|
frameRotationDegrees: orientationRotation,
|
|
1046
1013
|
captureOrientation: deviceOrientation,
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
// ARSession-driven path.
|
|
1051
|
-
frameSourceMode: isNonAR
|
|
1052
|
-
? (legacyDriver ? 'jsDriver' : 'frameProcessor')
|
|
1053
|
-
: 'arSession',
|
|
1014
|
+
// Non-AR captures use the Frame Processor driver
|
|
1015
|
+
// (vision-camera producer-thread worklet → cv_flow_gate
|
|
1016
|
+
// plugin → IncrementalStitcher.consumeFrame). AR captures
|
|
1017
|
+
// use the ARSession-driven path.
|
|
1018
|
+
frameSourceMode: isNonAR ? 'frameProcessor' : 'arSession',
|
|
1054
1019
|
composeWidth: 1920,
|
|
1055
1020
|
composeHeight: 1080,
|
|
1056
1021
|
canvasWidth: 5000,
|
|
@@ -1066,21 +1031,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1066
1031
|
// matching comment on the per-accept reset useEffect above).
|
|
1067
1032
|
// Keep firing it on every capture start, not just legacy mode.
|
|
1068
1033
|
imuGate.resetAnchor();
|
|
1069
|
-
// Start the non-AR
|
|
1070
|
-
// ARSession so
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
// * Legacy: JS driver — `takeSnapshot` + `processFrameAtPath`
|
|
1075
|
-
// via the cameraRef.
|
|
1076
|
-
// Imperative-pattern rationale: see the useIncrementalJSDriver
|
|
1077
|
-
// comment above re. why this isn't a useEffect.
|
|
1034
|
+
// Start the Frame Processor driver for non-AR captures. AR
|
|
1035
|
+
// mode feeds natively from ARSession so the driver stays idle.
|
|
1036
|
+
// Imperative pattern (vs useEffect) because the driver's start
|
|
1037
|
+
// resets pose accumulators that should only fire at the
|
|
1038
|
+
// hold-start moment, not on every re-render.
|
|
1078
1039
|
if (isNonAR) {
|
|
1079
|
-
|
|
1080
|
-
jsDriver.start(visionCameraRef);
|
|
1081
|
-
} else {
|
|
1082
|
-
fpDriver.start();
|
|
1083
|
-
}
|
|
1040
|
+
fpDriver.start();
|
|
1084
1041
|
}
|
|
1085
1042
|
} catch (err) {
|
|
1086
1043
|
setStatusPhase('idle');
|
|
@@ -1100,9 +1057,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1100
1057
|
settings,
|
|
1101
1058
|
effectiveCaptureSource,
|
|
1102
1059
|
imuGate,
|
|
1103
|
-
jsDriver,
|
|
1104
1060
|
fpDriver,
|
|
1105
|
-
legacyDriver,
|
|
1106
1061
|
engine,
|
|
1107
1062
|
onError,
|
|
1108
1063
|
]);
|
|
@@ -1112,10 +1067,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1112
1067
|
setStatusPhase('stitching');
|
|
1113
1068
|
// Stop pumping new frames before finalizing so the engine isn't
|
|
1114
1069
|
// racing the final cv::Stitcher pass against late-arriving
|
|
1115
|
-
// keyframes.
|
|
1116
|
-
// corresponding driver wasn't started (AR mode, or the inactive
|
|
1117
|
-
// driver in non-AR mode).
|
|
1118
|
-
jsDriver.stop();
|
|
1070
|
+
// keyframes. No-op in AR mode (the driver was never started).
|
|
1119
1071
|
fpDriver.stop();
|
|
1120
1072
|
try {
|
|
1121
1073
|
// Compose the panorama output path: host-controlled if
|
|
@@ -1193,7 +1145,6 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1193
1145
|
onFramesDropped,
|
|
1194
1146
|
onError,
|
|
1195
1147
|
recordingStartedAt,
|
|
1196
|
-
jsDriver,
|
|
1197
1148
|
fpDriver,
|
|
1198
1149
|
// F10 Phase 2 review N1 — these four were missing pre-fix. The
|
|
1199
1150
|
// callback reads `settings.debug` (to gate the stitchToast),
|
package/src/index.ts
CHANGED
|
@@ -175,16 +175,16 @@ 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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} from './stitching/
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
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';
|
|
185
|
+
// vision-camera Frame Processor driver for non-AR captures. As
|
|
186
|
+
// of v0.6 the only non-AR driver exported (the legacy
|
|
187
|
+
// `useIncrementalJSDriver` was removed; was deprecated in v0.5).
|
|
188
188
|
export { useFrameProcessorDriver } from './stitching/useFrameProcessorDriver';
|
|
189
189
|
export type {
|
|
190
190
|
UseFrameProcessorDriverOptions,
|
|
@@ -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
|
|
@@ -200,23 +274,22 @@ export interface IncrementalStartOptions {
|
|
|
200
274
|
* bridge.start() requires `RNSARSession.start()` to
|
|
201
275
|
* have already been called.
|
|
202
276
|
*
|
|
203
|
-
* - 'jsDriver' — engine skips AR-session registration; JS
|
|
204
|
-
* feeds frames via `processFrameAtPath`. Use in iOS non-AR
|
|
205
|
-
* captures (vision-camera + gyro). No AR session required.
|
|
206
|
-
* LEGACY; deprecated in v0.5, removed in v0.6.
|
|
207
|
-
*
|
|
208
277
|
* - 'frameProcessor' (F8.3 iOS / F8.4 Android, v0.5+) — engine
|
|
209
278
|
* flips on `frameProcessorIngestEnabled` so the vision-camera
|
|
210
279
|
* Frame Processor plugin (`cv_flow_gate_process_frame`) can
|
|
211
280
|
* feed pixel data directly into the engine's gate path. iOS
|
|
212
281
|
* passes the `CVPixelBuffer` straight to `consumeFrame`;
|
|
213
|
-
* Android extracts the Y plane to a ByteArray and
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* captures driven by `useFrameProcessorDriver`. Pairs
|
|
217
|
-
* `Camera`'s default driver mode.
|
|
282
|
+
* Android extracts the Y plane to a ByteArray and (since
|
|
283
|
+
* F8.6, v0.5.1) routes live-engine ingest through
|
|
284
|
+
* `addFramePixelData` without a JPEG round-trip. Use in
|
|
285
|
+
* non-AR captures driven by `useFrameProcessorDriver`. Pairs
|
|
286
|
+
* with `Camera`'s default driver mode.
|
|
287
|
+
*
|
|
288
|
+
* `'jsDriver'` was removed in v0.6 (deprecated in v0.5). Hosts
|
|
289
|
+
* that used it should switch to `useFrameProcessorDriver` (or
|
|
290
|
+
* just let `<Camera>` use its default).
|
|
218
291
|
*/
|
|
219
|
-
frameSourceMode?: 'arSession' | '
|
|
292
|
+
frameSourceMode?: 'arSession' | 'frameProcessor';
|
|
220
293
|
/** Compose-resolution width in pixels (default 720 for portrait, 960 for landscape). */
|
|
221
294
|
composeWidth?: number;
|
|
222
295
|
/** Compose-resolution height in pixels (default 960 for portrait, 720 for landscape). */
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
3
|
* useFrameProcessorDriver — vision-camera Frame Processor + gyro
|
|
4
|
-
* driver for the incremental panorama engine.
|
|
5
|
-
* `useIncrementalJSDriver`
|
|
4
|
+
* driver for the incremental panorama engine. Sole non-AR driver
|
|
5
|
+
* from v0.6 onward (replaced the deprecated `useIncrementalJSDriver`
|
|
6
|
+
* hook, which was removed in v0.6).
|
|
6
7
|
*
|
|
7
|
-
* Why this exists (vs the JS-driver predecessor)
|
|
8
|
+
* Why this exists (vs the pre-v0.6 JS-driver predecessor)
|
|
8
9
|
*
|
|
9
|
-
* The JS driver
|
|
10
|
-
* path to `IncrementalStitcher.processFrameAtPath
|
|
11
|
-
*
|
|
10
|
+
* The old JS driver took a JPEG snapshot every ~250 ms and fed the
|
|
11
|
+
* path to `IncrementalStitcher.processFrameAtPath` (both removed in
|
|
12
|
+
* v0.6). That path had three costs:
|
|
12
13
|
*
|
|
13
14
|
* 1. JPEG encode (`takeSnapshot` ≈ 30–80 ms on iPhone 16 Pro)
|
|
14
15
|
* 2. Disk write of the JPEG
|
|
@@ -303,9 +304,9 @@ export function useFrameProcessorDriver(
|
|
|
303
304
|
// y = horizontal pan (yaw, about world-Y)
|
|
304
305
|
// x = vertical tilt (pitch, about world-X)
|
|
305
306
|
// z = wrist-twist roll (about world-Z, normal to the screen)
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
307
|
+
// Right-hand-rule convention throughout — same signs the pre-v0.6
|
|
308
|
+
// `useIncrementalJSDriver` produced. If field captures show
|
|
309
|
+
// inverted roll, flip the sign on `z * dt` below.
|
|
309
310
|
setUpdateIntervalForType(SensorTypes.gyroscope, gyroIntervalMs);
|
|
310
311
|
gyroSubRef.current = gyroscope.subscribe({
|
|
311
312
|
next: ({ x, y, z }) => {
|
|
@@ -382,8 +383,8 @@ export function useFrameProcessorDriver(
|
|
|
382
383
|
imageWidth: w, imageHeight: h,
|
|
383
384
|
timestampMs: 0,
|
|
384
385
|
// 2 == RNSARTrackingState.tracking — we always claim "good
|
|
385
|
-
// tracking" because there's no ARKit signal to differentiate
|
|
386
|
-
// (
|
|
386
|
+
// tracking" because there's no ARKit signal to differentiate.
|
|
387
|
+
// (Same contract as the pre-v0.6 useIncrementalJSDriver.)
|
|
387
388
|
trackingStateRaw: 2,
|
|
388
389
|
});
|
|
389
390
|
// Deps array intentionally minimal: only `plugin` actually
|