react-native-image-stitcher 0.7.1 → 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 +241 -0
- package/android/build.gradle +35 -1
- package/android/src/main/cpp/CMakeLists.txt +64 -2
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +30 -11
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +21 -3
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
- package/android/src/main/java/io/imagestitcher/rn/SaveFrameAsJpegPlugin.kt +162 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
- package/cpp/stitcher_frame_data.hpp +141 -0
- package/cpp/stitcher_frame_jsi.cpp +214 -0
- package/cpp/stitcher_frame_jsi.hpp +108 -0
- package/cpp/stitcher_proxy_jsi.cpp +109 -0
- package/cpp/stitcher_proxy_jsi.hpp +46 -0
- package/cpp/stitcher_worklet_dispatch.cpp +103 -0
- package/cpp/stitcher_worklet_dispatch.hpp +71 -0
- package/cpp/stitcher_worklet_registry.cpp +81 -0
- package/cpp/stitcher_worklet_registry.hpp +136 -0
- package/dist/camera/Camera.d.ts +62 -12
- package/dist/camera/Camera.js +30 -15
- package/dist/index.d.ts +6 -0
- package/dist/index.js +30 -1
- package/dist/stitching/StitcherFrame.d.ts +170 -0
- package/dist/stitching/StitcherFrame.js +4 -0
- package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
- package/dist/stitching/StitcherWorkletRegistry.js +78 -0
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/dist/stitching/useFrameProcessor.d.ts +119 -0
- package/dist/stitching/useFrameProcessor.js +196 -0
- 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/RNSARSession.swift +46 -10
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +185 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
- package/package.json +1 -1
- package/src/camera/Camera.tsx +93 -28
- package/src/index.ts +35 -0
- package/src/stitching/StitcherFrame.ts +197 -0
- package/src/stitching/StitcherWorkletRegistry.ts +156 -0
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +178 -0
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- package/src/stitching/useFrameProcessor.ts +226 -0
- package/src/stitching/useFrameStream.ts +255 -0
- package/src/stitching/useThrottledFrameProcessor.ts +145 -0
- package/src/types.ts +95 -0
|
@@ -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
|
|
@@ -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;
|
|
@@ -484,10 +484,41 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
484
484
|
Int32(config.videoFormat.framesPerSecond))
|
|
485
485
|
isRunning = true
|
|
486
486
|
currentTrackingState = .initialising
|
|
487
|
+
|
|
488
|
+
// v0.8.0 Phase 3c — install the worklet runtime + register
|
|
489
|
+
// the first-party stitching callback. The delegate's
|
|
490
|
+
// per-frame ingest now routes through
|
|
491
|
+
// `RNSARWorkletRuntime.dispatchFrame` (see
|
|
492
|
+
// `session(_:didUpdate:)` below) which invokes this
|
|
493
|
+
// callback synchronously. Net behavior is byte-identical
|
|
494
|
+
// to the pre-Phase-3c direct `consumer.consumeFrame(...)`
|
|
495
|
+
// call. The indirection sets up the seam where Phase 4
|
|
496
|
+
// will fan out to host worklets without touching this
|
|
497
|
+
// first-party path.
|
|
498
|
+
RNSARWorkletRuntime.shared().installIfNeeded()
|
|
499
|
+
RNSARWorkletRuntime.shared().setFirstPartyCallback {
|
|
500
|
+
[weak self] arFrame, pose in
|
|
501
|
+
// ARKit pool reuse contract: must consume the pixel
|
|
502
|
+
// buffer before returning. The consumer's
|
|
503
|
+
// `consumeFrame` does that synchronously inside the
|
|
504
|
+
// call (NV12 → cv::Mat sync, then heavy work on its
|
|
505
|
+
// own queue). We're on the same thread as the
|
|
506
|
+
// delegate (ARSession.delegateQueue), so the contract
|
|
507
|
+
// holds end-to-end.
|
|
508
|
+
guard let self = self else { return }
|
|
509
|
+
guard let consumer = self.incrementalConsumer else { return }
|
|
510
|
+
consumer.consumeFrame(pixelBuffer: arFrame.capturedImage,
|
|
511
|
+
pose: pose)
|
|
512
|
+
}
|
|
487
513
|
}
|
|
488
514
|
|
|
489
515
|
@objc public func stop() {
|
|
490
516
|
guard isRunning else { return }
|
|
517
|
+
// v0.8.0 Phase 3c — drop the first-party callback so the
|
|
518
|
+
// closure's `[weak self]` reference can be released
|
|
519
|
+
// immediately + no in-flight delegate frame re-enters the
|
|
520
|
+
// engine after stop. Idempotent.
|
|
521
|
+
RNSARWorkletRuntime.shared().setFirstPartyCallback(nil)
|
|
491
522
|
arSession.pause()
|
|
492
523
|
isRunning = false
|
|
493
524
|
currentTrackingState = .notAvailable
|
|
@@ -561,16 +592,21 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
561
592
|
}
|
|
562
593
|
}
|
|
563
594
|
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
// path
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
595
|
+
// v0.8.0 Phase 3c — route the per-frame ingest through the
|
|
596
|
+
// worklet runtime instead of calling the consumer directly.
|
|
597
|
+
// The first-party callback (installed in `start()` above)
|
|
598
|
+
// wraps the same `consumer.consumeFrame(pixelBuffer:pose:)`
|
|
599
|
+
// call path, so net behavior is byte-identical to v0.7.x.
|
|
600
|
+
// The indirection sets up the seam where Phase 4 will fan
|
|
601
|
+
// out to host worklets (registered via the v0.8.0
|
|
602
|
+
// `useFrameProcessor` TS hook + a JSI plugin entry point)
|
|
603
|
+
// without changing this first-party path.
|
|
604
|
+
//
|
|
605
|
+
// ARKit pool reuse contract: still satisfied — the runtime
|
|
606
|
+
// invokes the callback synchronously on the delegate
|
|
607
|
+
// thread, and the callback's `consumer.consumeFrame(...)`
|
|
608
|
+
// does the same NV12 → cv::Mat sync conversion as before.
|
|
609
|
+
RNSARWorkletRuntime.shared().dispatchFrame(frame, pose: pose)
|
|
574
610
|
|
|
575
611
|
// If recording is in flight, append this frame to the
|
|
576
612
|
// asset writer DIRECTLY — no queue hop.
|