react-native-image-stitcher 0.8.0 → 0.10.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 +269 -0
- package/android/build.gradle +10 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +115 -10
- 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/cpp/stitcher_worklet_registry.cpp +10 -0
- package/cpp/stitcher_worklet_registry.hpp +10 -0
- package/cpp/tests/CMakeLists.txt +98 -0
- package/cpp/tests/README.md +86 -0
- package/cpp/tests/pose_test.cpp +74 -0
- package/cpp/tests/stitcher_frame_data_test.cpp +132 -0
- package/cpp/tests/stitcher_worklet_registry_test.cpp +195 -0
- package/cpp/tests/stubs/jsi/jsi.h +33 -0
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +34 -0
- package/dist/camera/useCapture.d.ts +1 -1
- package/dist/camera/useCapture.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -1
- package/dist/stitching/incremental.d.ts +41 -0
- package/dist/stitching/useFrameStream.d.ts +34 -0
- package/dist/stitching/useFrameStream.js +234 -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/IncrementalStitcher.swift +138 -9
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +50 -14
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +185 -0
- package/package.json +1 -1
- package/src/camera/useCapture.ts +1 -1
- package/src/index.ts +19 -0
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +276 -0
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +178 -0
- package/src/stitching/incremental.ts +42 -0
- package/src/stitching/useFrameStream.ts +271 -0
- package/src/stitching/useThrottledFrameProcessor.ts +145 -0
- package/src/types.ts +95 -0
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - This hook does NOT persist captures. Host apps hand the
|
|
20
20
|
* returned CaptureResult to their own storage layer (WatermelonDB
|
|
21
21
|
* insert, Redux dispatch, whatever).
|
|
22
|
-
* - Video recording lives in useVideoCapture
|
|
22
|
+
* - Video recording lives in useVideoCapture.
|
|
23
23
|
*
|
|
24
24
|
* The public API is designed to be minimal and replaceable: host apps
|
|
25
25
|
* that prefer the raw vision-camera API can opt out of this hook and
|
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).
|
|
@@ -249,6 +249,47 @@ export interface IncrementalState {
|
|
|
249
249
|
* keyframes on disk.
|
|
250
250
|
*/
|
|
251
251
|
refinedPanoramaPath?: string;
|
|
252
|
+
/**
|
|
253
|
+
* v0.10.0 (#15A) — current phase of an in-flight `refinePanorama`
|
|
254
|
+
* call. Fires from both the explicit `module.refinePanorama(...)`
|
|
255
|
+
* JS API path AND the hybrid-engine auto-refine path (which calls
|
|
256
|
+
* the same native refinePanorama internally).
|
|
257
|
+
*
|
|
258
|
+
* Lifecycle:
|
|
259
|
+
* - `"validating"` (fraction 0.05) — synchronous input checks
|
|
260
|
+
* - `"stitching"` (fraction 0.10) — OpenCV stitch in flight
|
|
261
|
+
* - `"writing"` (fraction 0.90) — stitch done, JPEG written
|
|
262
|
+
* - `"done"` (fraction 1.00) — success
|
|
263
|
+
* - `"error"` (fraction 1.00) — failure; `refineError` is set
|
|
264
|
+
*
|
|
265
|
+
* Coarse on purpose: OpenCV's Stitcher doesn't expose mid-pipeline
|
|
266
|
+
* progress, so the 0.10 → 0.90 jump is one opaque step. Use
|
|
267
|
+
* `refineStage` for a stage label; use `refineProgress` purely for
|
|
268
|
+
* spinner progress.
|
|
269
|
+
*
|
|
270
|
+
* Undefined when no refinement is in flight.
|
|
271
|
+
*/
|
|
272
|
+
refineStage?: 'validating' | 'stitching' | 'writing' | 'done' | 'error';
|
|
273
|
+
/**
|
|
274
|
+
* v0.10.0 (#15A) — coarse progress fraction in `[0, 1]` aligned
|
|
275
|
+
* with `refineStage`. See `refineStage` for the per-stage value
|
|
276
|
+
* mapping. Undefined when no refinement is in flight.
|
|
277
|
+
*/
|
|
278
|
+
refineProgress?: number;
|
|
279
|
+
/**
|
|
280
|
+
* v0.10.0 (#15A) — number of input frames the in-flight refine is
|
|
281
|
+
* processing. Useful for the UI label
|
|
282
|
+
* (`Stitching 6 frames…`). Mirrors the `framesRequested` field
|
|
283
|
+
* returned in the explicit refinePanorama resolution. Undefined
|
|
284
|
+
* when no refinement is in flight.
|
|
285
|
+
*/
|
|
286
|
+
refineFrames?: number;
|
|
287
|
+
/**
|
|
288
|
+
* v0.10.0 (#15A) — present only when `refineStage === 'error'`.
|
|
289
|
+
* Human-readable error message; the same text the rejected promise
|
|
290
|
+
* carries. Use to render a one-line failure pill.
|
|
291
|
+
*/
|
|
292
|
+
refineError?: string;
|
|
252
293
|
}
|
|
253
294
|
export interface IncrementalStartOptions {
|
|
254
295
|
/**
|
|
@@ -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,234 @@
|
|
|
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_vision_camera_1 = require("react-native-vision-camera");
|
|
65
|
+
const react_native_worklets_core_1 = require("react-native-worklets-core");
|
|
66
|
+
const useThrottledFrameProcessor_1 = require("./useThrottledFrameProcessor");
|
|
67
|
+
const files_1 = require("../utils/files");
|
|
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: the lib's canonical capture dir resolved
|
|
102
|
+
// via `FileBridge.defaultCaptureDir()`. Same dir the lib uses
|
|
103
|
+
// for panorama JPEGs / keyframe JPEGs — guaranteed writable on
|
|
104
|
+
// both platforms (iOS NSCachesDirectory + Android Context.cacheDir),
|
|
105
|
+
// created if missing. Resolved async on first mount; until
|
|
106
|
+
// resolution completes the worklet's `outputDir` is empty and
|
|
107
|
+
// the plugin call no-ops silently (a few frames missed at most;
|
|
108
|
+
// typical resolution time is <50ms).
|
|
109
|
+
//
|
|
110
|
+
// Hosts that want a specific path supply `options.outputDir`
|
|
111
|
+
// and skip the resolution entirely.
|
|
112
|
+
const [resolvedDefaultDir, setResolvedDefaultDir] = (0, react_1.useState)('');
|
|
113
|
+
(0, react_1.useEffect)(() => {
|
|
114
|
+
if (options.outputDir != null)
|
|
115
|
+
return;
|
|
116
|
+
let cancelled = false;
|
|
117
|
+
(0, files_1.getDefaultCaptureDir)()
|
|
118
|
+
.then((dir) => {
|
|
119
|
+
if (!cancelled)
|
|
120
|
+
setResolvedDefaultDir(dir);
|
|
121
|
+
})
|
|
122
|
+
.catch((err) => {
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.warn('[useFrameStream] FileBridge.defaultCaptureDir() failed; ' +
|
|
125
|
+
'samples will not fire until `options.outputDir` is supplied. ' +
|
|
126
|
+
String(err));
|
|
127
|
+
});
|
|
128
|
+
return () => {
|
|
129
|
+
cancelled = true;
|
|
130
|
+
};
|
|
131
|
+
}, [options.outputDir]);
|
|
132
|
+
const outputDir = options.outputDir ?? resolvedDefaultDir;
|
|
133
|
+
// Stable JS-side handler reference for `runOnJS`. The hook re-
|
|
134
|
+
// captures `handler` on every render but the ref keeps the
|
|
135
|
+
// worklet closure pointing at the latest callback (avoid stale
|
|
136
|
+
// captures).
|
|
137
|
+
const handlerRef = (0, react_1.useRef)(handler);
|
|
138
|
+
handlerRef.current = handler;
|
|
139
|
+
const onSampleJS = (0, react_1.useCallback)((sample) => {
|
|
140
|
+
const result = handlerRef.current(sample);
|
|
141
|
+
if (result != null &&
|
|
142
|
+
typeof result.catch === 'function') {
|
|
143
|
+
result.catch((err) => {
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.error('[useFrameStream] handler threw:', err);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}, []);
|
|
149
|
+
const onSampleOnJS = (0, react_1.useMemo)(() => react_native_worklets_core_1.Worklets.createRunOnJS(onSampleJS), [onSampleJS]);
|
|
150
|
+
// ── Plugin acquisition (Layer 1) ─────────────────────────────────
|
|
151
|
+
//
|
|
152
|
+
// `initFrameProcessorPlugin` can return `undefined` if the native
|
|
153
|
+
// registry hasn't initialised yet (rare race on app start). We
|
|
154
|
+
// retry every 16ms (one display frame) until success — matches
|
|
155
|
+
// the pattern in `useFrameProcessorDriver`.
|
|
156
|
+
//
|
|
157
|
+
// Use `useState` (not `useRef`) so the eventual non-null value
|
|
158
|
+
// triggers a re-render — the worklet closure below captures
|
|
159
|
+
// `plugin` by value at render time, so without state we'd
|
|
160
|
+
// capture `null` forever.
|
|
161
|
+
const [plugin, setPlugin] = (0, react_1.useState)(null);
|
|
162
|
+
(0, react_1.useEffect)(() => {
|
|
163
|
+
let cancelled = false;
|
|
164
|
+
let timerId = null;
|
|
165
|
+
let attempts = 0;
|
|
166
|
+
const tryAcquire = () => {
|
|
167
|
+
if (cancelled)
|
|
168
|
+
return;
|
|
169
|
+
attempts += 1;
|
|
170
|
+
const p = react_native_vision_camera_1.VisionCameraProxy.initFrameProcessorPlugin('save_frame_as_jpeg', {});
|
|
171
|
+
if (p != null) {
|
|
172
|
+
setPlugin(p);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// After ~1s of failed retries, warn once — the plugin should
|
|
176
|
+
// be registered by then; persistent failure means the host's
|
|
177
|
+
// native bundle doesn't include `save_frame_as_jpeg`.
|
|
178
|
+
if (attempts === 60) {
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.warn('[useFrameStream] save_frame_as_jpeg plugin not found after 1s of retries. ' +
|
|
181
|
+
'Verify react-native-image-stitcher native module is installed in your host app.');
|
|
182
|
+
}
|
|
183
|
+
timerId = setTimeout(tryAcquire, 16);
|
|
184
|
+
};
|
|
185
|
+
tryAcquire();
|
|
186
|
+
return () => {
|
|
187
|
+
cancelled = true;
|
|
188
|
+
if (timerId != null)
|
|
189
|
+
clearTimeout(timerId);
|
|
190
|
+
};
|
|
191
|
+
}, []);
|
|
192
|
+
return (0, useThrottledFrameProcessor_1.useThrottledFrameProcessor)((frame) => {
|
|
193
|
+
'worklet';
|
|
194
|
+
if (plugin == null)
|
|
195
|
+
return;
|
|
196
|
+
// Async outputDir resolution may not have completed yet on
|
|
197
|
+
// the first few frames after mount — bail until it does.
|
|
198
|
+
if (outputDir === '')
|
|
199
|
+
return;
|
|
200
|
+
// Slot rotation: compute slot from frame timestamp. At
|
|
201
|
+
// sampleHz=2 (500ms interval), the slot index changes every
|
|
202
|
+
// ~1s, giving each slot ~2 samples before being overwritten.
|
|
203
|
+
// That's overkill for the "stream-of-samples" use case but
|
|
204
|
+
// matches the docstring's "at most 4 stale JPEGs" guarantee.
|
|
205
|
+
const slot = Math.floor(frame.timestamp / 1000) % 4;
|
|
206
|
+
const path = `${outputDir}/stream-${slot}.jpg`;
|
|
207
|
+
// vc's `FrameProcessorPlugin.call` expects vc's `Frame` type.
|
|
208
|
+
// `StitcherFrame` is structurally a superset (it adds `source`,
|
|
209
|
+
// `pose`, AR-only fields). Cast through `unknown` — same
|
|
210
|
+
// pattern v0.8.0's `useFrameProcessor` uses when handing a
|
|
211
|
+
// StitcherFrame-typed worklet to vc.
|
|
212
|
+
const result = plugin.call(frame, {
|
|
213
|
+
path,
|
|
214
|
+
quality,
|
|
215
|
+
});
|
|
216
|
+
if (result == null ||
|
|
217
|
+
result.ok !== true) {
|
|
218
|
+
// Native side reported an error (path not writable, format
|
|
219
|
+
// wrong, etc.). Silently skip this sample — the next tick
|
|
220
|
+
// will retry. The plugin already logs the specific reason
|
|
221
|
+
// on the native side.
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const r = result;
|
|
225
|
+
onSampleOnJS({
|
|
226
|
+
jpegPath: r.path,
|
|
227
|
+
pose: frame.pose,
|
|
228
|
+
timestamp: frame.timestamp,
|
|
229
|
+
width: r.width,
|
|
230
|
+
height: r.height,
|
|
231
|
+
});
|
|
232
|
+
}, { sampleHz }, [plugin, outputDir, quality, onSampleOnJS]);
|
|
233
|
+
}
|
|
234
|
+
//# 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
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// v0.9.0 Layer 2 — throttle gate over v0.8.0's `useFrameProcessor`.
|
|
5
|
+
//
|
|
6
|
+
// ## What this is
|
|
7
|
+
//
|
|
8
|
+
// A thin wrapper around `useFrameProcessor` that enforces a maximum
|
|
9
|
+
// invocation rate (`sampleHz`) at the worklet layer. The host's
|
|
10
|
+
// worklet fires up to `sampleHz` times per second; ticks too close
|
|
11
|
+
// together are dropped via a `useSharedValue<number>` monotonic-time
|
|
12
|
+
// gate inside the worklet body.
|
|
13
|
+
//
|
|
14
|
+
// ## When to use this (vs alternatives)
|
|
15
|
+
//
|
|
16
|
+
// - **`useFrameProcessor` directly** — every camera frame (~30-60 Hz).
|
|
17
|
+
// Use for true-realtime processing that wants to see every frame.
|
|
18
|
+
// - **`useThrottledFrameProcessor`** (this hook) — sub-frame-rate
|
|
19
|
+
// worklet-native processing. The worklet runtime has direct
|
|
20
|
+
// access to `frame.toArrayBuffer()`, `frame.arDepth`,
|
|
21
|
+
// `frame.arAnchors`, and can call other vc Frame Processor plugins
|
|
22
|
+
// (native OCR libraries, TFLite ML inference, etc.). Results
|
|
23
|
+
// bridged to JS via `runOnJS`.
|
|
24
|
+
// - **`useFrameStream`** (Layer 3, also in this directory) —
|
|
25
|
+
// sub-frame-rate JS-thread consumer. The lib JPEG-encodes each
|
|
26
|
+
// sample on the producer thread and delivers a `SampledFrame`
|
|
27
|
+
// (file path + pose + dims) to a JS-thread callback. Use for
|
|
28
|
+
// file-path OCR libraries (RN modules wrapping ML Kit etc.),
|
|
29
|
+
// cloud upload, thumbnail UI.
|
|
30
|
+
//
|
|
31
|
+
// ## Use-case mapping (canonical)
|
|
32
|
+
//
|
|
33
|
+
// | Use case | Layer | Why |
|
|
34
|
+
// |---------------------------------------|-------|----------------------------------|
|
|
35
|
+
// | OCR via Vision.framework / ML Kit | **2** | native libs, bbox in frame coords|
|
|
36
|
+
// | TFLite ML detection (via vc plugin) | **2** | same shape as OCR |
|
|
37
|
+
// | LiDAR depth → 3D reconstruction | **2** | depth too large to bridge |
|
|
38
|
+
// | Pose-only telemetry | **2** | tiny payload, no encoding needed |
|
|
39
|
+
// | File-path OCR (RN module) | 3 | host wants a JPEG, not pixels |
|
|
40
|
+
// | Cloud upload (sampled JPEG feed) | 3 | JPEG IS the payload |
|
|
41
|
+
// | Live thumbnail preview UI | 3 | `<Image source={{uri: ...}}>` |
|
|
42
|
+
//
|
|
43
|
+
// See `docs/host-app-integration.md` § "Tier 2 + 3" for recipes.
|
|
44
|
+
//
|
|
45
|
+
// ## Threading
|
|
46
|
+
//
|
|
47
|
+
// The wrapped worklet fires on whatever runtime `useFrameProcessor`
|
|
48
|
+
// dispatches on:
|
|
49
|
+
// - **Non-AR mode**: vision-camera's Frame Processor runtime
|
|
50
|
+
// (producer thread).
|
|
51
|
+
// - **AR mode**: the lib's `RNSARWorkletRuntime` (iOS) /
|
|
52
|
+
// worklets-core default context (Android) — fired by the AR
|
|
53
|
+
// session's per-frame dispatch. See v0.8.0 Phase 4b.i / 4b.iii.
|
|
54
|
+
//
|
|
55
|
+
// Either way, the worklet MUST NOT block — the next frame's
|
|
56
|
+
// processing is gated on this one returning. Long work belongs
|
|
57
|
+
// behind `runOnJS` / a separate worklet runtime.
|
|
58
|
+
//
|
|
59
|
+
// ## Behaviour at the throttle boundary
|
|
60
|
+
//
|
|
61
|
+
// The hook tracks a monotonic-time shared value of "last sample time".
|
|
62
|
+
// Each tick checks if `frame.timestamp - lastSampleMs.value >=
|
|
63
|
+
// (1000 / sampleHz)`. If yes, the worklet body runs and the value
|
|
64
|
+
// updates; if no, the worklet returns silently.
|
|
65
|
+
//
|
|
66
|
+
// Edge cases:
|
|
67
|
+
// - First-ever tick: `lastSampleMs.value` starts at 0; first frame's
|
|
68
|
+
// timestamp will be >> 0 → first tick always fires. Subsequent
|
|
69
|
+
// ticks throttle as expected.
|
|
70
|
+
// - vc v4 timestamp semantics: per the project's worklet-throttle
|
|
71
|
+
// gotcha note, `frame.timestamp` is NOT reliably nanoseconds in
|
|
72
|
+
// vc v4. The hook treats `frame.timestamp` as ALREADY in
|
|
73
|
+
// milliseconds (which is what vc v4 actually delivers; the
|
|
74
|
+
// v0.8.0 StitcherFrame contract documents this). If a future
|
|
75
|
+
// vc version changes the unit, the throttle math here needs
|
|
76
|
+
// re-checking.
|
|
77
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
78
|
+
exports.useThrottledFrameProcessor = useThrottledFrameProcessor;
|
|
79
|
+
const react_native_worklets_core_1 = require("react-native-worklets-core");
|
|
80
|
+
const useFrameProcessor_1 = require("./useFrameProcessor");
|
|
81
|
+
/**
|
|
82
|
+
* Throttled variant of `useFrameProcessor`. See the module
|
|
83
|
+
* docstring for the full use-case mapping; quick version:
|
|
84
|
+
*
|
|
85
|
+
* ```tsx
|
|
86
|
+
* const fp = useThrottledFrameProcessor(
|
|
87
|
+
* (frame) => {
|
|
88
|
+
* 'worklet';
|
|
89
|
+
* // worklet-native OCR / ML / depth processing here
|
|
90
|
+
* },
|
|
91
|
+
* { sampleHz: 2 },
|
|
92
|
+
* [],
|
|
93
|
+
* );
|
|
94
|
+
* return <Camera frameProcessor={fp} ... />;
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @param worklet Host's frame-processor worklet. Must be
|
|
98
|
+
* `'worklet'`-prefixed. Runs at most `sampleHz`
|
|
99
|
+
* times per second.
|
|
100
|
+
* @param options `{ sampleHz }` — clamped to `[0.5, 30]`.
|
|
101
|
+
* @param deps Standard React deps array. Treated the same as
|
|
102
|
+
* `useFrameProcessor`'s deps — when they change the
|
|
103
|
+
* inner worklet is re-bound.
|
|
104
|
+
*
|
|
105
|
+
* @returns A `useFrameProcessor`-shaped processor object, pass it
|
|
106
|
+
* to `<Camera frameProcessor={...}>`.
|
|
107
|
+
*/
|
|
108
|
+
function useThrottledFrameProcessor(worklet, options, deps) {
|
|
109
|
+
// Clamp + derive interval. Done outside the worklet so the
|
|
110
|
+
// useSharedValue / useFrameProcessor hooks see stable values.
|
|
111
|
+
const sampleHz = Math.max(0.5, Math.min(30, options.sampleHz));
|
|
112
|
+
const minIntervalMs = 1000 / sampleHz;
|
|
113
|
+
// Monotonic-time gate. Initialised to 0 → first tick always
|
|
114
|
+
// fires (frame.timestamp >> 0).
|
|
115
|
+
const lastSampleMs = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
116
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
117
|
+
return (0, useFrameProcessor_1.useFrameProcessor)((frame) => {
|
|
118
|
+
'worklet';
|
|
119
|
+
const now = frame.timestamp;
|
|
120
|
+
if (now - lastSampleMs.value < minIntervalMs) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
lastSampleMs.value = now;
|
|
124
|
+
worklet(frame);
|
|
125
|
+
},
|
|
126
|
+
// The throttle interval is captured in the worklet closure; if
|
|
127
|
+
// it changes we need to re-bind the worklet so the new
|
|
128
|
+
// `minIntervalMs` takes effect. Same for the host's worklet
|
|
129
|
+
// identity (so deps changes on the host side re-bind too).
|
|
130
|
+
[minIntervalMs, worklet, ...deps]);
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=useThrottledFrameProcessor.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -35,6 +35,93 @@ export interface DeviceMetadata {
|
|
|
35
35
|
cameraId: string;
|
|
36
36
|
flashEnabled: boolean;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* v0.9.0 Layer 3 — one sampled frame delivered by `useFrameStream`
|
|
40
|
+
* to the JS-thread handler.
|
|
41
|
+
*
|
|
42
|
+
* The JPEG file at `jpegPath` is the stream's own copy. Hosts that
|
|
43
|
+
* need long-term retention MUST copy the file synchronously inside
|
|
44
|
+
* the handler — the same path may be overwritten by a subsequent
|
|
45
|
+
* sample (slot reuse — see the hook's docstring for the rotation
|
|
46
|
+
* policy).
|
|
47
|
+
*/
|
|
48
|
+
export interface SampledFrame {
|
|
49
|
+
/** Absolute filesystem path to the JPEG. No `file://` prefix. */
|
|
50
|
+
jpegPath: string;
|
|
51
|
+
/**
|
|
52
|
+
* Pose at sample time. `translation` is `undefined` in non-AR
|
|
53
|
+
* mode (gyro provides rotation only; no spatial anchor).
|
|
54
|
+
*/
|
|
55
|
+
pose: {
|
|
56
|
+
rotation: [number, number, number, number];
|
|
57
|
+
translation?: [number, number, number];
|
|
58
|
+
};
|
|
59
|
+
/** Frame timestamp (ms; per the v0.8.0 StitcherFrame contract). */
|
|
60
|
+
timestamp: number;
|
|
61
|
+
/** JPEG width / height in pixels. */
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* v0.9.0 Layer 3 — options for `useFrameStream`.
|
|
67
|
+
*
|
|
68
|
+
* For worklet-native processing without JPEG roundtrip (OCR via
|
|
69
|
+
* Vision/ML Kit, TFLite ML, LiDAR depth), use
|
|
70
|
+
* `useThrottledFrameProcessor` (Layer 2) instead.
|
|
71
|
+
*/
|
|
72
|
+
export interface FrameStreamOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Target sampling rate in Hertz. Clamped to `[0.5, 10]`. The
|
|
75
|
+
* Layer 2 throttle gate enforces the rate inside the worklet;
|
|
76
|
+
* ticks too close together are dropped silently.
|
|
77
|
+
*
|
|
78
|
+
* Clamp upper bound (10 Hz) is intentionally lower than Layer 2's
|
|
79
|
+
* (30 Hz) — beyond 10 Hz the per-frame JPEG encode + JS-bridge
|
|
80
|
+
* cost dominates the wall-clock budget. Hosts that need higher
|
|
81
|
+
* rates should be on Layer 2 with their own JPEG encoder call
|
|
82
|
+
* (or no JPEG at all).
|
|
83
|
+
*/
|
|
84
|
+
sampleHz: number;
|
|
85
|
+
/**
|
|
86
|
+
* JPEG quality (0-100). Default 75. Clamped silently to
|
|
87
|
+
* `[1, 100]` by the underlying `save_frame_as_jpeg` native plugin.
|
|
88
|
+
*/
|
|
89
|
+
quality?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Directory to write JPEG files into. Defaults to a per-app
|
|
92
|
+
* `<cache>/rnis-frame-stream/` subdirectory. The directory is
|
|
93
|
+
* `mkdir -p`'d on first use; hosts that supply an existing
|
|
94
|
+
* absolute path are responsible for its lifecycle.
|
|
95
|
+
*/
|
|
96
|
+
outputDir?: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* v0.9.0 Layer 2 — options for `useThrottledFrameProcessor`.
|
|
100
|
+
*
|
|
101
|
+
* Wraps v0.8.0's `useFrameProcessor` with a monotonic-time throttle
|
|
102
|
+
* gate so the supplied worklet fires at most `sampleHz` times per
|
|
103
|
+
* second. Use for sub-frame-rate worklet-native processing — native
|
|
104
|
+
* OCR (Vision.framework / ML Kit), TFLite ML detection, LiDAR depth
|
|
105
|
+
* processing — where the bbox / depth payloads are small enough to
|
|
106
|
+
* bridge to JS via `runOnJS`.
|
|
107
|
+
*
|
|
108
|
+
* For JS-thread JPEG consumers (file-path OCR libraries, cloud
|
|
109
|
+
* upload, thumbnail UI), use `useFrameStream` (Layer 3) instead.
|
|
110
|
+
*/
|
|
111
|
+
export interface ThrottledFrameProcessorOptions {
|
|
112
|
+
/**
|
|
113
|
+
* Target sampling rate in Hertz. Clamped to `[0.5, 30]`. Inside
|
|
114
|
+
* the worklet a monotonic-time gate enforces the rate; ticks too
|
|
115
|
+
* close together are silently dropped.
|
|
116
|
+
*
|
|
117
|
+
* The clamp upper bound (30 Hz) sits at typical AR rates on
|
|
118
|
+
* mid-range Android devices — beyond that, the host should just
|
|
119
|
+
* use `useFrameProcessor` directly (no throttle). The clamp
|
|
120
|
+
* lower bound (0.5 Hz) prevents accidentally-zero-divide values
|
|
121
|
+
* + matches `useFrameStream`'s convention.
|
|
122
|
+
*/
|
|
123
|
+
sampleHz: number;
|
|
124
|
+
}
|
|
38
125
|
export interface CaptureResult {
|
|
39
126
|
/** Unique device-generated UUID */
|
|
40
127
|
deviceUuid: string;
|