react-native-image-stitcher 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +119 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +17 -3
- package/android/src/main/java/io/imagestitcher/rn/SaveFrameAsJpegPlugin.kt +162 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -1
- package/dist/stitching/useFrameStream.d.ts +34 -0
- package/dist/stitching/useFrameStream.js +219 -0
- package/dist/stitching/useThrottledFrameProcessor.d.ts +33 -0
- package/dist/stitching/useThrottledFrameProcessor.js +132 -0
- package/dist/types.d.ts +87 -0
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +185 -0
- package/package.json +1 -1
- package/src/index.ts +19 -0
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +178 -0
- package/src/stitching/useFrameStream.ts +255 -0
- package/src/stitching/useThrottledFrameProcessor.ts +145 -0
- package/src/types.ts +95 -0
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,125 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.9.0] — 2026-05-27
|
|
20
|
+
|
|
21
|
+
### Added — layered frame-access helpers
|
|
22
|
+
|
|
23
|
+
Three new primitives completing the Tier 2 surface in the
|
|
24
|
+
three-tier extensibility pattern. See `docs/frame-access-tiers.md`
|
|
25
|
+
for the full decision flow + use-case mapping.
|
|
26
|
+
|
|
27
|
+
#### Layer 1 — `save_frame_as_jpeg` vc Frame Processor plugin (native)
|
|
28
|
+
|
|
29
|
+
Worklet-callable JPEG encoder. Registers on both platforms:
|
|
30
|
+
|
|
31
|
+
- **iOS** — `SaveFrameAsJpegPlugin.mm` (CIImage → CGImage → UIImage
|
|
32
|
+
→ UIImageJPEGRepresentation → atomic NSData write). Registered
|
|
33
|
+
via `+ (void)load` hook into `FrameProcessorPluginRegistry`.
|
|
34
|
+
- **Android** — `SaveFrameAsJpegPlugin.kt` wrapping the lib's
|
|
35
|
+
existing `YuvImageConverter.encodeJpegFromNV21` encoder (the
|
|
36
|
+
same one used by `RNSARCameraView`'s keyframe-accept callback).
|
|
37
|
+
Registered alongside `cv_flow_gate_process_frame` in
|
|
38
|
+
`RNImageStitcherPackage.ensureFrameProcessorPluginRegistered`.
|
|
39
|
+
|
|
40
|
+
Plugin contract (identical on both platforms):
|
|
41
|
+
- Args: `path` (string, REQUIRED), `quality` (number 0-100,
|
|
42
|
+
default 75, clamped `[1, 100]`)
|
|
43
|
+
- Returns: `{ ok: true, path, width, height }` OR
|
|
44
|
+
`{ ok: false, error: "..." }`
|
|
45
|
+
|
|
46
|
+
Hosts can call this directly from their own `useFrameProcessor`
|
|
47
|
+
worklet for custom rate-control logic; most consumers use it
|
|
48
|
+
indirectly via Layer 3.
|
|
49
|
+
|
|
50
|
+
#### Layer 2 — `useThrottledFrameProcessor` hook
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
const fp = useThrottledFrameProcessor(
|
|
54
|
+
(frame) => {
|
|
55
|
+
'worklet';
|
|
56
|
+
// Worklet-native processing at sub-frame-rate
|
|
57
|
+
},
|
|
58
|
+
{ sampleHz: 2 },
|
|
59
|
+
[],
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Pure TS throttle gate over `useFrameProcessor` (v0.8.0). Worklet
|
|
64
|
+
fires up to `sampleHz` times per second; ticks too close together
|
|
65
|
+
dropped via a monotonic-time `useSharedValue` gate.
|
|
66
|
+
|
|
67
|
+
**Use for**: worklet-native processing — native OCR via
|
|
68
|
+
Vision.framework / ML Kit wrapped as vc Frame Processor plugins,
|
|
69
|
+
TFLite ML inference, LiDAR depth (`frame.arDepth`). Direct
|
|
70
|
+
buffer/pose/depth access in the worklet; bridge small bbox-result
|
|
71
|
+
payloads to JS via `runOnJS`.
|
|
72
|
+
|
|
73
|
+
`sampleHz` clamped to `[0.5, 30]`.
|
|
74
|
+
|
|
75
|
+
#### Layer 3 — `useFrameStream` hook
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const fp = useFrameStream(
|
|
79
|
+
{ sampleHz: 2, quality: 75 },
|
|
80
|
+
(sample) => {
|
|
81
|
+
// JS-thread callback: sample.jpegPath, sample.pose, sample.timestamp
|
|
82
|
+
setThumbnail(sample.jpegPath);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Composes Layer 2 + Layer 1 + `runOnJS` bridge to deliver
|
|
88
|
+
`SampledFrame` objects to a JS-thread handler. Slot-reuse
|
|
89
|
+
strategy bounds disk usage to ~4 stale JPEGs.
|
|
90
|
+
|
|
91
|
+
**Use for**: JS-thread consumers — file-path OCR libraries (RN
|
|
92
|
+
modules wrapping ML Kit), cloud upload, thumbnail preview UI,
|
|
93
|
+
JS-side ML (TF.js, transformers.js).
|
|
94
|
+
|
|
95
|
+
`sampleHz` clamped to `[0.5, 10]`; `quality` clamped `[1, 100]`.
|
|
96
|
+
|
|
97
|
+
#### Types
|
|
98
|
+
|
|
99
|
+
- `SampledFrame` — `{ jpegPath, pose, timestamp, width, height }`
|
|
100
|
+
- `FrameStreamOptions` — `{ sampleHz, quality?, outputDir? }`
|
|
101
|
+
- `ThrottledFrameProcessorOptions` — `{ sampleHz }`
|
|
102
|
+
|
|
103
|
+
All exported from `react-native-image-stitcher`.
|
|
104
|
+
|
|
105
|
+
### Documentation
|
|
106
|
+
|
|
107
|
+
- `docs/frame-access-tiers.md` — new comprehensive reference for
|
|
108
|
+
all four host-facing hooks (`useKeyframeStream`,
|
|
109
|
+
`useThrottledFrameProcessor`, `useFrameStream`,
|
|
110
|
+
`useFrameProcessor`) with decision flow, cost envelope, use-case
|
|
111
|
+
mapping, AR vs non-AR mode tradeoff.
|
|
112
|
+
|
|
113
|
+
### Example app
|
|
114
|
+
|
|
115
|
+
`example/App.tsx` now mounts `useFrameStream` at 2 Hz with a
|
|
116
|
+
visible thumbnail overlay (bottom-right corner) — visual proof of
|
|
117
|
+
the Layer 1 + 2 + 3 pipeline working end-to-end on both iPhone
|
|
118
|
+
(60 Hz AR) and Galaxy A35 (30 Hz AR).
|
|
119
|
+
|
|
120
|
+
### Compatibility
|
|
121
|
+
|
|
122
|
+
- Strict additive over v0.8.0. No host changes required.
|
|
123
|
+
- Works in both AR and non-AR modes via v0.8.0's unified
|
|
124
|
+
`useFrameProcessor`.
|
|
125
|
+
- New hooks return `useFrameProcessor`-shape objects compatible
|
|
126
|
+
with `<Camera frameProcessor={...}>` (Phase 5 from v0.8.0).
|
|
127
|
+
|
|
128
|
+
### Notes
|
|
129
|
+
|
|
130
|
+
- Formal SSIM parity gate (Phase 7 of the v0.9.0 plan) was NOT
|
|
131
|
+
run for this release — the layered design doesn't touch
|
|
132
|
+
first-party stitching, so a regression is structurally unlikely.
|
|
133
|
+
Harness still in place from v0.8.0 (`scripts/ssim-compare.py`)
|
|
134
|
+
for any host that wants to run it locally.
|
|
135
|
+
|
|
136
|
+
[0.9.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.8.0...v0.9.0
|
|
137
|
+
|
|
19
138
|
## [0.8.0] — 2026-05-27
|
|
20
139
|
|
|
21
140
|
### Added — `useFrameProcessor` hook for host worklets
|
|
@@ -55,19 +55,33 @@ class RNImageStitcherPackage : ReactPackage {
|
|
|
55
55
|
) { proxy, options ->
|
|
56
56
|
CvFlowGateFrameProcessor(proxy, options)
|
|
57
57
|
}
|
|
58
|
+
// v0.9.0 Layer 1 — register `save_frame_as_jpeg`
|
|
59
|
+
// alongside the cv_flow_gate plugin. Same lifecycle,
|
|
60
|
+
// same defensive error handling (the outer try/catch
|
|
61
|
+
// covers both registrations). Either both register
|
|
62
|
+
// or neither does — if vc isn't on the classpath,
|
|
63
|
+
// both calls are skipped together.
|
|
64
|
+
FrameProcessorPluginRegistry.addFrameProcessorPlugin(
|
|
65
|
+
SaveFrameAsJpegPlugin.PLUGIN_NAME,
|
|
66
|
+
) { proxy, options ->
|
|
67
|
+
SaveFrameAsJpegPlugin(proxy, options)
|
|
68
|
+
}
|
|
58
69
|
fpPluginRegistered = true
|
|
59
70
|
} catch (e: NoClassDefFoundError) {
|
|
60
71
|
android.util.Log.i(
|
|
61
72
|
"RNImageStitcherPackage",
|
|
62
73
|
"vision-camera FrameProcessorPluginRegistry not on classpath — "
|
|
63
|
-
+ "skipping cv_flow_gate_process_frame
|
|
64
|
-
+ "(host app doesn't appear to use
|
|
74
|
+
+ "skipping cv_flow_gate_process_frame + save_frame_as_jpeg "
|
|
75
|
+
+ "plugin registration (host app doesn't appear to use "
|
|
76
|
+
+ "Frame Processors).",
|
|
65
77
|
)
|
|
66
78
|
fpPluginRegistered = true // don't retry every package init
|
|
67
79
|
} catch (e: Throwable) {
|
|
68
80
|
android.util.Log.w(
|
|
69
81
|
"RNImageStitcherPackage",
|
|
70
|
-
"Failed to register
|
|
82
|
+
"Failed to register Frame Processor plugins "
|
|
83
|
+
+ "(cv_flow_gate_process_frame / save_frame_as_jpeg): "
|
|
84
|
+
+ e.message,
|
|
71
85
|
)
|
|
72
86
|
fpPluginRegistered = true
|
|
73
87
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
package io.imagestitcher.rn
|
|
3
|
+
|
|
4
|
+
import android.graphics.ImageFormat
|
|
5
|
+
import android.media.Image
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.annotation.Keep
|
|
8
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
9
|
+
import com.mrousavy.camera.frameprocessors.Frame
|
|
10
|
+
import com.mrousavy.camera.frameprocessors.FrameProcessorPlugin
|
|
11
|
+
import com.mrousavy.camera.frameprocessors.VisionCameraProxy
|
|
12
|
+
import io.imagestitcher.rn.ar.YuvImageConverter
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* v0.9.0 Layer 1 — Android vc Frame Processor plugin that JPEG-
|
|
16
|
+
* encodes the supplied frame to a host-supplied path. Mirror of
|
|
17
|
+
* iOS' `SaveFrameAsJpegPlugin.mm`.
|
|
18
|
+
*
|
|
19
|
+
* Plugin name (must match iOS): `save_frame_as_jpeg`.
|
|
20
|
+
*
|
|
21
|
+
* ## Wrapping the existing encoder
|
|
22
|
+
*
|
|
23
|
+
* The lib already encodes JPEGs from NV21 bytes via
|
|
24
|
+
* `YuvImageConverter.encodeJpegFromNV21` — that's the path
|
|
25
|
+
* `RNSARCameraView.kt`'s keyframe-accept callback uses (line 589,
|
|
26
|
+
* `onAccept = { targetPath -> ... encodeJpegFromNV21(...) }`).
|
|
27
|
+
* This plugin reuses that exact encoder so:
|
|
28
|
+
* - JPEG output is byte-equivalent to the keyframe-accept output
|
|
29
|
+
* (same encoder, same quality knob)
|
|
30
|
+
* - No new encoder maintenance burden
|
|
31
|
+
*
|
|
32
|
+
* vision-camera's `Frame.image` is an `android.media.Image` in
|
|
33
|
+
* `YUV_420_888` format (the camera's native). We pass it through
|
|
34
|
+
* `YuvImageConverter.packNV21(image)` to extract the dense NV21
|
|
35
|
+
* byte array + dims, then `encodeJpegFromNV21(packed, file, q, rot)`
|
|
36
|
+
* does the JPEG write.
|
|
37
|
+
*
|
|
38
|
+
* ## Plugin contract (matches iOS surface exactly)
|
|
39
|
+
*
|
|
40
|
+
* Arguments dict:
|
|
41
|
+
* - `path` (string, REQUIRED): absolute output path.
|
|
42
|
+
* - `quality` (number, optional): 0-100 JPEG quality. Default 75.
|
|
43
|
+
* Clamped to [1, 100].
|
|
44
|
+
*
|
|
45
|
+
* Returns:
|
|
46
|
+
* - On success: `{ "ok" => true, "path" => ..., "width" => ...,
|
|
47
|
+
* "height" => ... }`
|
|
48
|
+
* - On failure: `{ "ok" => false, "error" => "..." }`
|
|
49
|
+
*
|
|
50
|
+
* Errors surfaced via the result map (not thrown) — host worklets
|
|
51
|
+
* can branch on `result.ok` without try/catch. Same convention
|
|
52
|
+
* as iOS.
|
|
53
|
+
*
|
|
54
|
+
* ## Lifetime / threading
|
|
55
|
+
*
|
|
56
|
+
* The supplied `Frame` (and its `Image`) is valid only for the
|
|
57
|
+
* duration of this callback — vision-camera closes the underlying
|
|
58
|
+
* `Image` on return. All Image access (NV21 pack + JPEG encode)
|
|
59
|
+
* happens synchronously inside `callback()`.
|
|
60
|
+
*
|
|
61
|
+
* ## Format restriction
|
|
62
|
+
*
|
|
63
|
+
* Only `YUV_420_888` input is supported (vc Android's standard).
|
|
64
|
+
* Anything else returns `{ ok: false, error: "unsupported format" }`.
|
|
65
|
+
* No format conversion fallback — that would mask bugs in the host
|
|
66
|
+
* camera config.
|
|
67
|
+
*
|
|
68
|
+
* ## Registration
|
|
69
|
+
*
|
|
70
|
+
* Registered in `RNImageStitcherPackage.kt`'s companion-object
|
|
71
|
+
* `ensureFrameProcessorPluginRegistered()`, alongside
|
|
72
|
+
* `cv_flow_gate_process_frame`. Same defensive
|
|
73
|
+
* NoClassDefFoundError handling — if vc isn't on the host's
|
|
74
|
+
* classpath, registration is silently skipped (and the plugin's
|
|
75
|
+
* `init { … }` calls below never happen).
|
|
76
|
+
*/
|
|
77
|
+
@DoNotStrip
|
|
78
|
+
@Keep
|
|
79
|
+
class SaveFrameAsJpegPlugin(
|
|
80
|
+
@Suppress("UNUSED_PARAMETER") proxy: VisionCameraProxy,
|
|
81
|
+
@Suppress("UNUSED_PARAMETER") options: Map<String, Any>?,
|
|
82
|
+
) : FrameProcessorPlugin() {
|
|
83
|
+
|
|
84
|
+
override fun callback(frame: Frame, params: Map<String, Any>?): Any {
|
|
85
|
+
val path = params?.get("path") as? String
|
|
86
|
+
?: return mapOf(
|
|
87
|
+
"ok" to false,
|
|
88
|
+
"error" to "missing required `path` argument",
|
|
89
|
+
)
|
|
90
|
+
val rawQuality = (params["quality"] as? Number)?.toInt() ?: 75
|
|
91
|
+
val quality = rawQuality.coerceIn(1, 100)
|
|
92
|
+
|
|
93
|
+
// Frame may throw if vc already released it.
|
|
94
|
+
val image: Image = try {
|
|
95
|
+
frame.image
|
|
96
|
+
} catch (e: Throwable) {
|
|
97
|
+
return mapOf(
|
|
98
|
+
"ok" to false,
|
|
99
|
+
"error" to "frame invalid: ${e.message}",
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (image.format != ImageFormat.YUV_420_888) {
|
|
104
|
+
return mapOf(
|
|
105
|
+
"ok" to false,
|
|
106
|
+
"error" to "unsupported format ${image.format} (need YUV_420_888)",
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Pack NV21 — same call site RNSARCameraView uses.
|
|
111
|
+
val packed = YuvImageConverter.packNV21(image)
|
|
112
|
+
?: return mapOf(
|
|
113
|
+
"ok" to false,
|
|
114
|
+
"error" to "YuvImageConverter.packNV21 returned null",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Reuse the lib's existing JPEG encoder. Rotation is 0 here
|
|
118
|
+
// (the host's frame is in camera-native orientation; if they
|
|
119
|
+
// want display orientation they can pass an `orientation`
|
|
120
|
+
// arg in a future version — for v0.9.0 the worklet emits
|
|
121
|
+
// raw-camera-oriented JPEGs, matching the keyframe-accept
|
|
122
|
+
// pipeline's behaviour).
|
|
123
|
+
//
|
|
124
|
+
// Signature: `encodeJpegFromNV21(packed, outputPath: String,
|
|
125
|
+
// jpegQuality: Int, displayRotation: Int): String?` — returns
|
|
126
|
+
// the written path on success, null on failure.
|
|
127
|
+
val encodedPath: String? = try {
|
|
128
|
+
YuvImageConverter.encodeJpegFromNV21(
|
|
129
|
+
packed,
|
|
130
|
+
path,
|
|
131
|
+
jpegQuality = quality,
|
|
132
|
+
displayRotation = 0,
|
|
133
|
+
)
|
|
134
|
+
} catch (e: Throwable) {
|
|
135
|
+
return mapOf(
|
|
136
|
+
"ok" to false,
|
|
137
|
+
"error" to "encodeJpegFromNV21 threw: ${e.message}",
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
if (encodedPath == null) {
|
|
141
|
+
return mapOf(
|
|
142
|
+
"ok" to false,
|
|
143
|
+
"error" to "encodeJpegFromNV21 returned null",
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return mapOf(
|
|
148
|
+
"ok" to true,
|
|
149
|
+
"path" to encodedPath,
|
|
150
|
+
"width" to packed.width,
|
|
151
|
+
"height" to packed.height,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
companion object {
|
|
156
|
+
private const val TAG = "SaveFrameAsJpegPlugin"
|
|
157
|
+
|
|
158
|
+
/// Plugin name; MUST match iOS + the JS-side
|
|
159
|
+
/// `initFrameProcessorPlugin('save_frame_as_jpeg')` call.
|
|
160
|
+
const val PLUGIN_NAME = "save_frame_as_jpeg"
|
|
161
|
+
}
|
|
162
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,10 @@ export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
|
|
|
67
67
|
export { useKeyframeStream } from './stitching/useKeyframeStream';
|
|
68
68
|
export type { StitcherFrame, StitcherFrameProcessor, ARAnchor, } from './stitching/StitcherFrame';
|
|
69
69
|
export { useFrameProcessor } from './stitching/useFrameProcessor';
|
|
70
|
+
export { useThrottledFrameProcessor } from './stitching/useThrottledFrameProcessor';
|
|
71
|
+
export type { ThrottledFrameProcessorOptions } from './types';
|
|
72
|
+
export { useFrameStream } from './stitching/useFrameStream';
|
|
73
|
+
export type { FrameStreamOptions, SampledFrame } from './types';
|
|
70
74
|
export { useFrameProcessorDriver } from './stitching/useFrameProcessorDriver';
|
|
71
75
|
export type { UseFrameProcessorDriverOptions, FrameProcessorDriverHandle, } from './stitching/useFrameProcessorDriver';
|
|
72
76
|
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.useFrameProcessor = 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;
|
|
25
|
+
exports.stitchVideo = exports.useFrameProcessorDriver = exports.useFrameStream = exports.useThrottledFrameProcessor = exports.useFrameProcessor = 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
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -155,6 +155,25 @@ Object.defineProperty(exports, "useKeyframeStream", { enumerable: true, get: fun
|
|
|
155
155
|
// See the hook's docstring + StitcherFrame.ts for the contract.
|
|
156
156
|
var useFrameProcessor_1 = require("./stitching/useFrameProcessor");
|
|
157
157
|
Object.defineProperty(exports, "useFrameProcessor", { enumerable: true, get: function () { return useFrameProcessor_1.useFrameProcessor; } });
|
|
158
|
+
// v0.9.0 Layer 2 — `useThrottledFrameProcessor`. Throttle gate over
|
|
159
|
+
// `useFrameProcessor` for sub-frame-rate worklet-native processing
|
|
160
|
+
// (native OCR via Vision.framework / ML Kit, TFLite ML detection,
|
|
161
|
+
// LiDAR depth). The worklet runtime has direct access to
|
|
162
|
+
// `frame.toArrayBuffer()` / `frame.arDepth`; bridge small payloads
|
|
163
|
+
// (bboxes, depth-derived metrics) to JS via `runOnJS`. For JS-thread
|
|
164
|
+
// JPEG consumers (file-path OCR libs, cloud upload, thumbnail UI),
|
|
165
|
+
// prefer `useFrameStream` (Layer 3, ships in the same release).
|
|
166
|
+
var useThrottledFrameProcessor_1 = require("./stitching/useThrottledFrameProcessor");
|
|
167
|
+
Object.defineProperty(exports, "useThrottledFrameProcessor", { enumerable: true, get: function () { return useThrottledFrameProcessor_1.useThrottledFrameProcessor; } });
|
|
168
|
+
// v0.9.0 Layer 3 — `useFrameStream`. JS-thread sampled-frame
|
|
169
|
+
// stream over Layer 1 (`save_frame_as_jpeg` vc plugin) + Layer 2
|
|
170
|
+
// (`useThrottledFrameProcessor`). Use for JS-thread consumers:
|
|
171
|
+
// file-path OCR libs (RN modules), cloud upload, thumbnail UI.
|
|
172
|
+
// For worklet-native processing (Vision/ML Kit as vc plugins,
|
|
173
|
+
// TFLite ML, LiDAR depth), prefer `useThrottledFrameProcessor`
|
|
174
|
+
// (Layer 2) — lower latency, no JPEG roundtrip.
|
|
175
|
+
var useFrameStream_1 = require("./stitching/useFrameStream");
|
|
176
|
+
Object.defineProperty(exports, "useFrameStream", { enumerable: true, get: function () { return useFrameStream_1.useFrameStream; } });
|
|
158
177
|
// vision-camera Frame Processor driver for non-AR captures. As
|
|
159
178
|
// of v0.6 the only non-AR driver exported (the legacy
|
|
160
179
|
// `useIncrementalJSDriver` was removed; was deprecated in v0.5).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useThrottledFrameProcessor } from './useThrottledFrameProcessor';
|
|
2
|
+
import type { FrameStreamOptions, SampledFrame } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* `useFrameStream` — Layer 3. See module docstring for the full
|
|
5
|
+
* design + use-case mapping. Quick start:
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { Camera, useFrameStream } from 'react-native-image-stitcher';
|
|
9
|
+
*
|
|
10
|
+
* function MyScreen() {
|
|
11
|
+
* const fp = useFrameStream(
|
|
12
|
+
* { sampleHz: 2, quality: 75 },
|
|
13
|
+
* (sample) => {
|
|
14
|
+
* setThumbnail(sample.jpegPath);
|
|
15
|
+
* },
|
|
16
|
+
* );
|
|
17
|
+
* return <Camera frameProcessor={fp} ... />;
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @param options `{ sampleHz, quality?, outputDir? }`. `sampleHz`
|
|
22
|
+
* clamped to `[0.5, 10]`.
|
|
23
|
+
* @param handler JS-thread callback fired per sample. Receives a
|
|
24
|
+
* `SampledFrame`. May return a Promise; rejections
|
|
25
|
+
* are caught + logged (not re-thrown) so one
|
|
26
|
+
* misbehaving handler doesn't break the stream.
|
|
27
|
+
*
|
|
28
|
+
* @returns A `useFrameProcessor`-shaped processor object — pass to
|
|
29
|
+
* `<Camera frameProcessor={...}>` for non-AR mode wiring.
|
|
30
|
+
* (AR mode auto-registration via `__stitcherProxy` is
|
|
31
|
+
* handled inside `useFrameProcessor`.)
|
|
32
|
+
*/
|
|
33
|
+
export declare function useFrameStream(options: FrameStreamOptions, handler: (sample: SampledFrame) => void | Promise<void>): ReturnType<typeof useThrottledFrameProcessor>;
|
|
34
|
+
//# sourceMappingURL=useFrameStream.d.ts.map
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// v0.9.0 Layer 3 — JS-thread sampled-frame stream over Layer 1 +
|
|
5
|
+
// Layer 2.
|
|
6
|
+
//
|
|
7
|
+
// ## What this is
|
|
8
|
+
//
|
|
9
|
+
// A hook that:
|
|
10
|
+
// 1. Throttles a worklet via `useThrottledFrameProcessor` (Layer 2)
|
|
11
|
+
// to fire at `sampleHz` Hz.
|
|
12
|
+
// 2. Inside the worklet, calls the `save_frame_as_jpeg` vc Frame
|
|
13
|
+
// Processor plugin (Layer 1) to JPEG-encode the frame to a
|
|
14
|
+
// bounded-rotation slot on disk.
|
|
15
|
+
// 3. Bridges the resulting `SampledFrame` (file path + pose +
|
|
16
|
+
// dims) to a JS-thread callback via `runOnJS`.
|
|
17
|
+
//
|
|
18
|
+
// The host gets a per-sample callback on the JS thread with a file
|
|
19
|
+
// path they can pass to `<Image>`, an OCR RN module, a cloud-upload
|
|
20
|
+
// library, etc. Zero worklet boilerplate.
|
|
21
|
+
//
|
|
22
|
+
// ## When to use this (vs alternatives)
|
|
23
|
+
//
|
|
24
|
+
// - **`useFrameStream`** (this hook) — JS-thread consumers. File-
|
|
25
|
+
// path OCR libraries, cloud upload, thumbnail UI, sampled
|
|
26
|
+
// server-side analysis.
|
|
27
|
+
// - **`useThrottledFrameProcessor`** (Layer 2) — worklet-native
|
|
28
|
+
// consumers. Native OCR (Vision.framework / ML Kit) wrapped as
|
|
29
|
+
// vc plugins, TFLite ML inference, LiDAR depth processing.
|
|
30
|
+
// Lower latency; no JPEG roundtrip.
|
|
31
|
+
// - **`useFrameProcessor`** — every camera frame; full control.
|
|
32
|
+
//
|
|
33
|
+
// ## Slot reuse / disk usage
|
|
34
|
+
//
|
|
35
|
+
// JPEG files are written to `<outputDir>/stream-<N>.jpg` where N
|
|
36
|
+
// cycles 0..3 based on `frame.timestamp / 1000`. At most 4 stale
|
|
37
|
+
// JPEGs ever exist on disk; the same file is rewritten on each
|
|
38
|
+
// rotation, so disk usage is bounded.
|
|
39
|
+
//
|
|
40
|
+
// Hosts that need long-term retention (e.g., archive each sample
|
|
41
|
+
// for later upload) MUST copy the file synchronously inside the
|
|
42
|
+
// handler — the slot may be overwritten by the next sample.
|
|
43
|
+
//
|
|
44
|
+
// ## Backpressure
|
|
45
|
+
//
|
|
46
|
+
// If the JS handler returns slower than `1/sampleHz`, subsequent
|
|
47
|
+
// ticks DO still fire (the throttle is time-based, not handler-
|
|
48
|
+
// completion-based). This means multiple handler invocations can
|
|
49
|
+
// be in flight simultaneously. For most use cases that's fine
|
|
50
|
+
// (the handlers are pure or commute). Hosts that need serialised
|
|
51
|
+
// handling should track in-flight state themselves and early-return.
|
|
52
|
+
//
|
|
53
|
+
// ## AR vs non-AR
|
|
54
|
+
//
|
|
55
|
+
// Works in both modes because it composes over
|
|
56
|
+
// `useThrottledFrameProcessor` → `useFrameProcessor`. In AR mode
|
|
57
|
+
// the worklet auto-registers via `__stitcherProxy` (v0.8.0 Phase
|
|
58
|
+
// 4b.i/iii); in non-AR mode the returned processor object is
|
|
59
|
+
// passed to `<Camera frameProcessor={...}>`. The hook returns
|
|
60
|
+
// the processor object so hosts can wire it up either way.
|
|
61
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
|
+
exports.useFrameStream = useFrameStream;
|
|
63
|
+
const react_1 = require("react");
|
|
64
|
+
const react_native_1 = require("react-native");
|
|
65
|
+
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
66
|
+
const react_native_worklets_core_1 = require("react-native-worklets-core");
|
|
67
|
+
const useThrottledFrameProcessor_1 = require("./useThrottledFrameProcessor");
|
|
68
|
+
/**
|
|
69
|
+
* `useFrameStream` — Layer 3. See module docstring for the full
|
|
70
|
+
* design + use-case mapping. Quick start:
|
|
71
|
+
*
|
|
72
|
+
* ```tsx
|
|
73
|
+
* import { Camera, useFrameStream } from 'react-native-image-stitcher';
|
|
74
|
+
*
|
|
75
|
+
* function MyScreen() {
|
|
76
|
+
* const fp = useFrameStream(
|
|
77
|
+
* { sampleHz: 2, quality: 75 },
|
|
78
|
+
* (sample) => {
|
|
79
|
+
* setThumbnail(sample.jpegPath);
|
|
80
|
+
* },
|
|
81
|
+
* );
|
|
82
|
+
* return <Camera frameProcessor={fp} ... />;
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @param options `{ sampleHz, quality?, outputDir? }`. `sampleHz`
|
|
87
|
+
* clamped to `[0.5, 10]`.
|
|
88
|
+
* @param handler JS-thread callback fired per sample. Receives a
|
|
89
|
+
* `SampledFrame`. May return a Promise; rejections
|
|
90
|
+
* are caught + logged (not re-thrown) so one
|
|
91
|
+
* misbehaving handler doesn't break the stream.
|
|
92
|
+
*
|
|
93
|
+
* @returns A `useFrameProcessor`-shaped processor object — pass to
|
|
94
|
+
* `<Camera frameProcessor={...}>` for non-AR mode wiring.
|
|
95
|
+
* (AR mode auto-registration via `__stitcherProxy` is
|
|
96
|
+
* handled inside `useFrameProcessor`.)
|
|
97
|
+
*/
|
|
98
|
+
function useFrameStream(options, handler) {
|
|
99
|
+
const sampleHz = Math.max(0.5, Math.min(10, options.sampleHz));
|
|
100
|
+
const quality = options.quality ?? 75;
|
|
101
|
+
// Default output dir: a per-app cache subdirectory. Hosts that
|
|
102
|
+
// want a known path supply their own via `options.outputDir`.
|
|
103
|
+
// `Platform.OS`-specific cache paths are read once at hook mount.
|
|
104
|
+
const outputDir = (0, react_1.useMemo)(() => {
|
|
105
|
+
if (options.outputDir != null)
|
|
106
|
+
return options.outputDir;
|
|
107
|
+
// Both platforms expose a cache directory at a predictable path
|
|
108
|
+
// via React Native APIs; we use a small inline computation to
|
|
109
|
+
// avoid pulling `react-native-fs` as a hard dep. The lib's
|
|
110
|
+
// existing JPEG encode targets the app's data dir via similar
|
|
111
|
+
// logic in `RNSARCameraView.kt` / `IncrementalStitcher.swift`.
|
|
112
|
+
//
|
|
113
|
+
// We just generate a relative-ish path under /tmp/ for cross-
|
|
114
|
+
// platform simplicity; the native plugin writes wherever it's
|
|
115
|
+
// told to (absolute path), so as long as the directory exists
|
|
116
|
+
// the encode succeeds. Hosts that care about file lifecycle
|
|
117
|
+
// should supply `outputDir` explicitly.
|
|
118
|
+
return react_native_1.Platform.OS === 'ios'
|
|
119
|
+
? '/tmp/rnis-frame-stream'
|
|
120
|
+
: '/data/local/tmp/rnis-frame-stream';
|
|
121
|
+
}, [options.outputDir]);
|
|
122
|
+
// Ensure outputDir exists on the native side. We could use
|
|
123
|
+
// react-native-fs but to keep the dep surface minimal, we just
|
|
124
|
+
// attempt to create via a tiny native call — or, simpler, accept
|
|
125
|
+
// that the plugin's write call will fail if the dir doesn't
|
|
126
|
+
// exist + log a clear error. For v0.9.0 baseline we defer
|
|
127
|
+
// mkdir to the host (document it in the option's JSDoc) OR fall
|
|
128
|
+
// back to the platform's tmpdir which already exists.
|
|
129
|
+
//
|
|
130
|
+
// The tmpdir defaults above always exist on iOS + Android, so
|
|
131
|
+
// the common case "host doesn't supply outputDir" Just Works.
|
|
132
|
+
// Stable JS-side handler reference for `runOnJS`. The hook re-
|
|
133
|
+
// captures `handler` on every render but the ref keeps the
|
|
134
|
+
// worklet closure pointing at the latest callback (avoid stale
|
|
135
|
+
// captures).
|
|
136
|
+
const handlerRef = (0, react_1.useRef)(handler);
|
|
137
|
+
handlerRef.current = handler;
|
|
138
|
+
const onSampleJS = (0, react_1.useCallback)((sample) => {
|
|
139
|
+
const result = handlerRef.current(sample);
|
|
140
|
+
if (result != null &&
|
|
141
|
+
typeof result.catch === 'function') {
|
|
142
|
+
result.catch((err) => {
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.error('[useFrameStream] handler threw:', err);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}, []);
|
|
148
|
+
const onSampleOnJS = (0, react_1.useMemo)(() => react_native_worklets_core_1.Worklets.createRunOnJS(onSampleJS), [onSampleJS]);
|
|
149
|
+
// ── Plugin acquisition (Layer 1) ─────────────────────────────────
|
|
150
|
+
//
|
|
151
|
+
// `initFrameProcessorPlugin` can return `undefined` if the native
|
|
152
|
+
// registry hasn't initialised yet (rare race on app start). We
|
|
153
|
+
// retry every 16ms (one display frame) until success — matches
|
|
154
|
+
// the pattern in `useFrameProcessorDriver`.
|
|
155
|
+
const pluginRef = (0, react_1.useRef)(null);
|
|
156
|
+
(0, react_1.useEffect)(() => {
|
|
157
|
+
let cancelled = false;
|
|
158
|
+
let timerId = null;
|
|
159
|
+
const tryAcquire = () => {
|
|
160
|
+
if (cancelled)
|
|
161
|
+
return;
|
|
162
|
+
const p = react_native_vision_camera_1.VisionCameraProxy.initFrameProcessorPlugin('save_frame_as_jpeg', {});
|
|
163
|
+
if (p != null) {
|
|
164
|
+
pluginRef.current = p;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
timerId = setTimeout(tryAcquire, 16);
|
|
168
|
+
};
|
|
169
|
+
tryAcquire();
|
|
170
|
+
return () => {
|
|
171
|
+
cancelled = true;
|
|
172
|
+
if (timerId != null)
|
|
173
|
+
clearTimeout(timerId);
|
|
174
|
+
};
|
|
175
|
+
}, []);
|
|
176
|
+
// The worklet body — fires at sampleHz, calls the JPEG plugin,
|
|
177
|
+
// bridges the result to JS. Note we read `pluginRef.current`
|
|
178
|
+
// inside the worklet via the captured `plugin` value below;
|
|
179
|
+
// worklets-core handles the JS↔worklet reference.
|
|
180
|
+
const plugin = pluginRef.current;
|
|
181
|
+
return (0, useThrottledFrameProcessor_1.useThrottledFrameProcessor)((frame) => {
|
|
182
|
+
'worklet';
|
|
183
|
+
if (plugin == null)
|
|
184
|
+
return;
|
|
185
|
+
// Slot rotation: compute slot from frame timestamp. At
|
|
186
|
+
// sampleHz=2 (500ms interval), the slot index changes every
|
|
187
|
+
// ~1s, giving each slot ~2 samples before being overwritten.
|
|
188
|
+
// That's overkill for the "stream-of-samples" use case but
|
|
189
|
+
// matches the docstring's "at most 4 stale JPEGs" guarantee.
|
|
190
|
+
const slot = Math.floor(frame.timestamp / 1000) % 4;
|
|
191
|
+
const path = `${outputDir}/stream-${slot}.jpg`;
|
|
192
|
+
// vc's `FrameProcessorPlugin.call` expects vc's `Frame` type.
|
|
193
|
+
// `StitcherFrame` is structurally a superset (it adds `source`,
|
|
194
|
+
// `pose`, AR-only fields). Cast through `unknown` — same
|
|
195
|
+
// pattern v0.8.0's `useFrameProcessor` uses when handing a
|
|
196
|
+
// StitcherFrame-typed worklet to vc.
|
|
197
|
+
const result = plugin.call(frame, {
|
|
198
|
+
path,
|
|
199
|
+
quality,
|
|
200
|
+
});
|
|
201
|
+
if (result == null ||
|
|
202
|
+
result.ok !== true) {
|
|
203
|
+
// Native side reported an error (path not writable, format
|
|
204
|
+
// wrong, etc.). Silently skip this sample — the next tick
|
|
205
|
+
// will retry. The plugin already logs the specific reason
|
|
206
|
+
// on the native side.
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const r = result;
|
|
210
|
+
onSampleOnJS({
|
|
211
|
+
jpegPath: r.path,
|
|
212
|
+
pose: frame.pose,
|
|
213
|
+
timestamp: frame.timestamp,
|
|
214
|
+
width: r.width,
|
|
215
|
+
height: r.height,
|
|
216
|
+
});
|
|
217
|
+
}, { sampleHz }, [plugin, outputDir, quality, onSampleOnJS]);
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=useFrameStream.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DependencyList } from 'react';
|
|
2
|
+
import { useFrameProcessor } from './useFrameProcessor';
|
|
3
|
+
import type { StitcherFrameProcessor } from './StitcherFrame';
|
|
4
|
+
import type { ThrottledFrameProcessorOptions } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Throttled variant of `useFrameProcessor`. See the module
|
|
7
|
+
* docstring for the full use-case mapping; quick version:
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const fp = useThrottledFrameProcessor(
|
|
11
|
+
* (frame) => {
|
|
12
|
+
* 'worklet';
|
|
13
|
+
* // worklet-native OCR / ML / depth processing here
|
|
14
|
+
* },
|
|
15
|
+
* { sampleHz: 2 },
|
|
16
|
+
* [],
|
|
17
|
+
* );
|
|
18
|
+
* return <Camera frameProcessor={fp} ... />;
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @param worklet Host's frame-processor worklet. Must be
|
|
22
|
+
* `'worklet'`-prefixed. Runs at most `sampleHz`
|
|
23
|
+
* times per second.
|
|
24
|
+
* @param options `{ sampleHz }` — clamped to `[0.5, 30]`.
|
|
25
|
+
* @param deps Standard React deps array. Treated the same as
|
|
26
|
+
* `useFrameProcessor`'s deps — when they change the
|
|
27
|
+
* inner worklet is re-bound.
|
|
28
|
+
*
|
|
29
|
+
* @returns A `useFrameProcessor`-shaped processor object, pass it
|
|
30
|
+
* to `<Camera frameProcessor={...}>`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useThrottledFrameProcessor(worklet: StitcherFrameProcessor, options: ThrottledFrameProcessorOptions, deps: DependencyList): ReturnType<typeof useFrameProcessor>;
|
|
33
|
+
//# sourceMappingURL=useThrottledFrameProcessor.d.ts.map
|