react-native-image-stitcher 0.9.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.
@@ -58,8 +58,7 @@
58
58
  // passed to `<Camera frameProcessor={...}>`. The hook returns
59
59
  // the processor object so hosts can wire it up either way.
60
60
 
61
- import { useCallback, useEffect, useMemo, useRef } from 'react';
62
- import { Platform } from 'react-native';
61
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
63
62
  import {
64
63
  VisionCameraProxy,
65
64
  type Frame,
@@ -73,6 +72,7 @@ import type {
73
72
  FrameStreamOptions,
74
73
  SampledFrame,
75
74
  } from '../types';
75
+ import { getDefaultCaptureDir } from '../utils/files';
76
76
 
77
77
  /**
78
78
  * `useFrameStream` — Layer 3. See module docstring for the full
@@ -111,37 +111,39 @@ export function useFrameStream(
111
111
  const sampleHz = Math.max(0.5, Math.min(10, options.sampleHz));
112
112
  const quality = options.quality ?? 75;
113
113
 
114
- // Default output dir: a per-app cache subdirectory. Hosts that
115
- // want a known path supply their own via `options.outputDir`.
116
- // `Platform.OS`-specific cache paths are read once at hook mount.
117
- const outputDir = useMemo(() => {
118
- if (options.outputDir != null) return options.outputDir;
119
- // Both platforms expose a cache directory at a predictable path
120
- // via React Native APIs; we use a small inline computation to
121
- // avoid pulling `react-native-fs` as a hard dep. The lib's
122
- // existing JPEG encode targets the app's data dir via similar
123
- // logic in `RNSARCameraView.kt` / `IncrementalStitcher.swift`.
124
- //
125
- // We just generate a relative-ish path under /tmp/ for cross-
126
- // platform simplicity; the native plugin writes wherever it's
127
- // told to (absolute path), so as long as the directory exists
128
- // the encode succeeds. Hosts that care about file lifecycle
129
- // should supply `outputDir` explicitly.
130
- return Platform.OS === 'ios'
131
- ? '/tmp/rnis-frame-stream'
132
- : '/data/local/tmp/rnis-frame-stream';
114
+ // Default output dir: the lib's canonical capture dir resolved
115
+ // via `FileBridge.defaultCaptureDir()`. Same dir the lib uses
116
+ // for panorama JPEGs / keyframe JPEGs guaranteed writable on
117
+ // both platforms (iOS NSCachesDirectory + Android Context.cacheDir),
118
+ // created if missing. Resolved async on first mount; until
119
+ // resolution completes the worklet's `outputDir` is empty and
120
+ // the plugin call no-ops silently (a few frames missed at most;
121
+ // typical resolution time is <50ms).
122
+ //
123
+ // Hosts that want a specific path supply `options.outputDir`
124
+ // and skip the resolution entirely.
125
+ const [resolvedDefaultDir, setResolvedDefaultDir] = useState<string>('');
126
+ useEffect(() => {
127
+ if (options.outputDir != null) return;
128
+ let cancelled = false;
129
+ getDefaultCaptureDir()
130
+ .then((dir) => {
131
+ if (!cancelled) setResolvedDefaultDir(dir);
132
+ })
133
+ .catch((err) => {
134
+ // eslint-disable-next-line no-console
135
+ console.warn(
136
+ '[useFrameStream] FileBridge.defaultCaptureDir() failed; ' +
137
+ 'samples will not fire until `options.outputDir` is supplied. ' +
138
+ String(err),
139
+ );
140
+ });
141
+ return () => {
142
+ cancelled = true;
143
+ };
133
144
  }, [options.outputDir]);
134
145
 
135
- // Ensure outputDir exists on the native side. We could use
136
- // react-native-fs but to keep the dep surface minimal, we just
137
- // attempt to create via a tiny native call — or, simpler, accept
138
- // that the plugin's write call will fail if the dir doesn't
139
- // exist + log a clear error. For v0.9.0 baseline we defer
140
- // mkdir to the host (document it in the option's JSDoc) OR fall
141
- // back to the platform's tmpdir which already exists.
142
- //
143
- // The tmpdir defaults above always exist on iOS + Android, so
144
- // the common case "host doesn't supply outputDir" Just Works.
146
+ const outputDir = options.outputDir ?? resolvedDefaultDir;
145
147
 
146
148
  // Stable JS-side handler reference for `runOnJS`. The hook re-
147
149
  // captures `handler` on every render but the ref keeps the
@@ -174,20 +176,37 @@ export function useFrameStream(
174
176
  // registry hasn't initialised yet (rare race on app start). We
175
177
  // retry every 16ms (one display frame) until success — matches
176
178
  // the pattern in `useFrameProcessorDriver`.
177
- const pluginRef = useRef<FrameProcessorPlugin | null>(null);
179
+ //
180
+ // Use `useState` (not `useRef`) so the eventual non-null value
181
+ // triggers a re-render — the worklet closure below captures
182
+ // `plugin` by value at render time, so without state we'd
183
+ // capture `null` forever.
184
+ const [plugin, setPlugin] = useState<FrameProcessorPlugin | null>(null);
178
185
  useEffect(() => {
179
186
  let cancelled = false;
180
187
  let timerId: ReturnType<typeof setTimeout> | null = null;
188
+ let attempts = 0;
181
189
  const tryAcquire = () => {
182
190
  if (cancelled) return;
191
+ attempts += 1;
183
192
  const p = VisionCameraProxy.initFrameProcessorPlugin(
184
193
  'save_frame_as_jpeg',
185
194
  {},
186
195
  );
187
196
  if (p != null) {
188
- pluginRef.current = p;
197
+ setPlugin(p);
189
198
  return;
190
199
  }
200
+ // After ~1s of failed retries, warn once — the plugin should
201
+ // be registered by then; persistent failure means the host's
202
+ // native bundle doesn't include `save_frame_as_jpeg`.
203
+ if (attempts === 60) {
204
+ // eslint-disable-next-line no-console
205
+ console.warn(
206
+ '[useFrameStream] save_frame_as_jpeg plugin not found after 1s of retries. ' +
207
+ 'Verify react-native-image-stitcher native module is installed in your host app.',
208
+ );
209
+ }
191
210
  timerId = setTimeout(tryAcquire, 16);
192
211
  };
193
212
  tryAcquire();
@@ -197,16 +216,13 @@ export function useFrameStream(
197
216
  };
198
217
  }, []);
199
218
 
200
- // The worklet body — fires at sampleHz, calls the JPEG plugin,
201
- // bridges the result to JS. Note we read `pluginRef.current`
202
- // inside the worklet via the captured `plugin` value below;
203
- // worklets-core handles the JS↔worklet reference.
204
- const plugin = pluginRef.current;
205
-
206
219
  return useThrottledFrameProcessor(
207
220
  (frame: StitcherFrame) => {
208
221
  'worklet';
209
222
  if (plugin == null) return;
223
+ // Async outputDir resolution may not have completed yet on
224
+ // the first few frames after mount — bail until it does.
225
+ if (outputDir === '') return;
210
226
 
211
227
  // Slot rotation: compute slot from frame timestamp. At
212
228
  // sampleHz=2 (500ms interval), the slot index changes every