react-native-image-stitcher 0.14.2 → 0.15.1

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/README.md +35 -0
  3. package/RNImageStitcher.podspec +8 -7
  4. package/android/build.gradle +0 -16
  5. package/android/src/main/cpp/CMakeLists.txt +2 -63
  6. package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
  7. package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
  8. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
  9. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
  10. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
  11. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
  12. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +129 -71
  13. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +49 -0
  14. package/cpp/keyframe_gate.cpp +82 -23
  15. package/cpp/keyframe_gate.hpp +31 -2
  16. package/cpp/stitcher.cpp +208 -28
  17. package/cpp/tests/CMakeLists.txt +18 -12
  18. package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
  19. package/cpp/tests/warp_guard_test.cpp +48 -0
  20. package/cpp/warp_guard.hpp +41 -0
  21. package/dist/camera/Camera.d.ts +31 -16
  22. package/dist/camera/Camera.js +11 -3
  23. package/dist/camera/CameraView.js +93 -3
  24. package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
  25. package/dist/camera/CaptureStitchStatsToast.js +27 -7
  26. package/dist/camera/PanoramaSettings.d.ts +10 -223
  27. package/dist/camera/PanoramaSettings.js +6 -28
  28. package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
  29. package/dist/camera/PanoramaSettingsBridge.js +3 -102
  30. package/dist/camera/PanoramaSettingsModal.js +7 -1
  31. package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
  32. package/dist/camera/buildPanoramaInitialSettings.js +4 -0
  33. package/dist/camera/cameraErrorMessages.d.ts +32 -0
  34. package/dist/camera/cameraErrorMessages.js +53 -0
  35. package/dist/camera/selectCaptureDevice.d.ts +5 -1
  36. package/dist/camera/selectCaptureDevice.js +22 -2
  37. package/dist/camera/useCapture.js +38 -0
  38. package/dist/index.d.ts +5 -8
  39. package/dist/index.js +11 -34
  40. package/dist/stitching/incremental.d.ts +1 -117
  41. package/dist/stitching/stitchVideo.d.ts +0 -35
  42. package/dist/types.d.ts +0 -87
  43. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
  44. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
  45. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
  46. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
  47. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
  48. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
  49. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
  50. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
  51. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
  52. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +82 -7
  53. package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
  54. package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
  55. package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
  56. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
  57. package/package.json +3 -2
  58. package/src/camera/Camera.tsx +44 -23
  59. package/src/camera/CameraView.tsx +113 -4
  60. package/src/camera/CaptureStitchStatsToast.tsx +58 -14
  61. package/src/camera/PanoramaSettings.ts +16 -289
  62. package/src/camera/PanoramaSettingsBridge.ts +3 -114
  63. package/src/camera/PanoramaSettingsModal.tsx +14 -1
  64. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
  65. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
  66. package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
  67. package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
  68. package/src/camera/buildPanoramaInitialSettings.ts +17 -0
  69. package/src/camera/cameraErrorMessages.ts +84 -0
  70. package/src/camera/selectCaptureDevice.ts +28 -3
  71. package/src/camera/useCapture.ts +44 -1
  72. package/src/index.ts +11 -40
  73. package/src/stitching/incremental.ts +3 -140
  74. package/src/stitching/stitchVideo.ts +0 -26
  75. package/src/types.ts +0 -95
  76. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
  77. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
  78. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
  79. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
  80. package/cpp/stitcher_frame_jsi.cpp +0 -214
  81. package/cpp/stitcher_frame_jsi.hpp +0 -108
  82. package/cpp/stitcher_proxy_jsi.cpp +0 -109
  83. package/cpp/stitcher_proxy_jsi.hpp +0 -46
  84. package/cpp/stitcher_worklet_dispatch.cpp +0 -103
  85. package/cpp/stitcher_worklet_dispatch.hpp +0 -71
  86. package/cpp/stitcher_worklet_registry.cpp +0 -91
  87. package/cpp/stitcher_worklet_registry.hpp +0 -146
  88. package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
  89. package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
  90. package/dist/stitching/IncrementalStitcherView.js +0 -157
  91. package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
  92. package/dist/stitching/StitcherWorkletRegistry.js +0 -78
  93. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
  94. package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
  95. package/dist/stitching/useFrameProcessor.d.ts +0 -119
  96. package/dist/stitching/useFrameProcessor.js +0 -196
  97. package/dist/stitching/useFrameStream.d.ts +0 -34
  98. package/dist/stitching/useFrameStream.js +0 -234
  99. package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
  100. package/dist/stitching/useThrottledFrameProcessor.js +0 -132
  101. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
  102. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
  105. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
  106. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
  107. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
  108. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  109. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
  110. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
  111. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
  112. package/src/stitching/IncrementalStitcherView.tsx +0 -198
  113. package/src/stitching/StitcherWorkletRegistry.ts +0 -156
  114. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
  115. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
  116. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
  117. package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
  118. package/src/stitching/useFrameProcessor.ts +0 -226
  119. package/src/stitching/useFrameStream.ts +0 -271
  120. package/src/stitching/useThrottledFrameProcessor.ts +0 -145
@@ -1,141 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
-
3
- import { NativeModules } from 'react-native';
4
-
5
- /**
6
- * v0.8.0 Phase 4b — one-shot installer that asks the native side
7
- * to install `globalThis.__stitcherProxy` on the main JS runtime.
8
- *
9
- * ## When this runs
10
- *
11
- * The first call to `useFrameProcessor` triggers this. Idempotent:
12
- * once the global is installed, subsequent calls short-circuit.
13
- *
14
- * ## What it does
15
- *
16
- * Calls into the platform-native `StitcherJsiInstaller` RN module
17
- * which is registered with a `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install)`
18
- * on iOS (see `ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm`)
19
- * and — Phase 4b.ii — an analogous Kotlin TurboModule on Android.
20
- *
21
- * The native module reaches into the main JS runtime via
22
- * `RCTCxxBridge.runtime` (iOS) / the equivalent Android JSI access
23
- * pattern and installs a host object on `globalThis.__stitcherProxy`
24
- * exposing `install(workletFn)` / `uninstall(id)` / `count()`.
25
- *
26
- * ## Failure modes (and what happens then)
27
- *
28
- * 1. **Module not registered** (Android in Phase 4b.i; old iOS
29
- * builds without the new pod files). `NativeModules
30
- * .StitcherJsiInstaller` is `undefined`. This function returns
31
- * `false` and the hook falls back to the JS-side
32
- * `StitcherWorkletRegistry` — host worklets are registered
33
- * on the JS side but never fan out to AR mode. No crash, no
34
- * regression vs. Phase 4a.
35
- *
36
- * 2. **JSI runtime unreachable** (e.g., remote debug mode). The
37
- * sync method returns `false`. Same JS-side-registry fallback.
38
- *
39
- * 3. **Native install succeeds but global not yet visible.**
40
- * The native call is SYNCHRONOUS (`BLOCKING_SYNCHRONOUS_METHOD`),
41
- * so by the time the function returns the global is installed.
42
- * No race here.
43
- *
44
- * ## Why a separate module
45
- *
46
- * The install method is a one-time runtime bootstrap, not a
47
- * per-call API. Putting it on its own RN module (vs. on the
48
- * existing `StitcherBridge` / `IncrementalStitcherBridge`) keeps
49
- * the responsibility surface narrow and the failure mode easy
50
- * to diagnose ("`__stitcherProxy` not installed" → check
51
- * `StitcherJsiInstaller` module registration first).
52
- */
53
-
54
- interface StitcherJsiInstallerModule {
55
- install(): boolean;
56
- }
57
-
58
- /**
59
- * `__DEV__` is RN's global dev-flag. Guard the read with `typeof`
60
- * so the helper works in any environment that imports it without
61
- * defining __DEV__ (jest, SSR, custom tooling). Same pattern RN's
62
- * own debug code uses.
63
- */
64
- function isDev(): boolean {
65
- return typeof __DEV__ !== 'undefined' && __DEV__;
66
- }
67
-
68
- let installed = false;
69
-
70
- export function ensureStitcherProxyInstalled(): boolean {
71
- if (installed) return true;
72
- // Already installed by an earlier hook mount. Cheap fast-path.
73
- if (typeof (globalThis as { __stitcherProxy?: unknown }).__stitcherProxy !== 'undefined') {
74
- installed = true;
75
- return true;
76
- }
77
-
78
- const mod = (NativeModules as { StitcherJsiInstaller?: StitcherJsiInstallerModule })
79
- .StitcherJsiInstaller;
80
- if (mod == null || typeof mod.install !== 'function') {
81
- // Module not present — Android until Phase 4b.ii lands, or
82
- // an old iOS build. Surface this once at debug-info level so
83
- // the host can see "your worklets are JS-registered only" in
84
- // logcat / Console.app without a noisy per-frame warning.
85
- if (isDev() && !warnedAboutMissingModule) {
86
- warnedAboutMissingModule = true;
87
- console.info(
88
- '[react-native-image-stitcher] StitcherJsiInstaller native ' +
89
- 'module not found; host worklets registered in JS-side ' +
90
- 'registry only. AR-mode dispatch requires the native install ' +
91
- '(iOS Phase 4b.i — included in v0.8.0; Android Phase 4b.ii ' +
92
- '— follow-up release).',
93
- );
94
- }
95
- return false;
96
- }
97
-
98
- try {
99
- const ok = mod.install();
100
- if (!ok) {
101
- // Native module ran but couldn't install (JSI runtime
102
- // unreachable). Same fallback as the missing-module case.
103
- if (isDev() && !warnedAboutFailedInstall) {
104
- warnedAboutFailedInstall = true;
105
- console.info(
106
- '[react-native-image-stitcher] StitcherJsiInstaller.install() ' +
107
- 'returned false (JSI runtime unreachable — remote debug ' +
108
- 'mode?). Falling back to JS-side host worklet registry.',
109
- );
110
- }
111
- return false;
112
- }
113
- installed = true;
114
- return true;
115
- } catch (err) {
116
- if (isDev() && !warnedAboutFailedInstall) {
117
- warnedAboutFailedInstall = true;
118
- console.info(
119
- '[react-native-image-stitcher] StitcherJsiInstaller.install() ' +
120
- 'threw: ' +
121
- String(err) +
122
- '. Falling back to JS-side host worklet registry.',
123
- );
124
- }
125
- return false;
126
- }
127
- }
128
-
129
- let warnedAboutMissingModule = false;
130
- let warnedAboutFailedInstall = false;
131
-
132
- /**
133
- * Test-only — reset module-internal state. Used by jest to allow
134
- * multiple test cases to re-trigger the install path independently.
135
- * NOT exported from `src/index.ts`.
136
- */
137
- export function _resetStitcherProxyInstallStateForTests(): void {
138
- installed = false;
139
- warnedAboutMissingModule = false;
140
- warnedAboutFailedInstall = false;
141
- }
@@ -1,226 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
-
3
- import { useEffect, type DependencyList } from 'react';
4
- import {
5
- useFrameProcessor as visionCameraUseFrameProcessor,
6
- type DrawableFrameProcessor,
7
- type Frame,
8
- type ReadonlyFrameProcessor,
9
- } from 'react-native-vision-camera';
10
-
11
- import { ensureStitcherProxyInstalled } from './ensureStitcherProxyInstalled';
12
- import { StitcherWorkletRegistry } from './StitcherWorkletRegistry';
13
- import type { StitcherFrameProcessor } from './StitcherFrame';
14
-
15
- /**
16
- * Shape of the native-installed `globalThis.__stitcherProxy` host
17
- * object (iOS Phase 4b.i; Android Phase 4b.ii). When present, the
18
- * hook prefers the native registry over the JS-side mirror — the
19
- * native AR worklet runtime reads from the native side directly.
20
- */
21
- interface StitcherProxy {
22
- install(workletFn: StitcherFrameProcessor): string;
23
- uninstall(id: string): void;
24
- count(): number;
25
- }
26
-
27
- /**
28
- * v0.8.0 Phase 4a — public hook for hosts to attach a per-frame
29
- * worklet that runs in BOTH AR and non-AR capture modes.
30
- *
31
- * ## Quick start
32
- *
33
- * ```tsx
34
- * import { useFrameProcessor, type StitcherFrame } from 'react-native-image-stitcher';
35
- *
36
- * function MyOcrOverlay() {
37
- * const processor = useFrameProcessor((frame: StitcherFrame) => {
38
- * 'worklet';
39
- * // Pixel data is in `frame.toArrayBuffer()`.
40
- * // AR-only fields: `frame.arDepth`, `frame.arAnchors`, `frame.arTrackingState`.
41
- * // Discriminate via `frame.source === 'ar'` / `'vc'`.
42
- * }, []);
43
- * return <Camera frameProcessor={processor} ... />;
44
- * }
45
- * ```
46
- *
47
- * ## Two behaviours, depending on mode
48
- *
49
- * **Non-AR mode (today, fully working):** the worklet runs on
50
- * vision-camera's Frame Processor runtime. Same thread + same
51
- * cost envelope as a plain `useFrameProcessor` from
52
- * `react-native-vision-camera`. The lib's own first-party
53
- * stitching plugin runs alongside on the same producer-thread
54
- * runtime (composition is handled by vision-camera's own dispatch
55
- * order).
56
- *
57
- * Your worklet receives whatever vision-camera delivers — vc's raw
58
- * `Frame`. This is a structural subset of `StitcherFrame`: the
59
- * vc-shaped fields (`width`, `height`, `pixelFormat`, `orientation`,
60
- * `timestamp`, `toArrayBuffer`) are guaranteed; the
61
- * `StitcherFrame`-only fields (`source`, `pose`, `arDepth`,
62
- * `arAnchors`, `arTrackingState`) are **undefined** at runtime
63
- * because the lib does NOT wrap or augment vc's `Frame` in Phase 4a
64
- * (cross-worklet-boundary field injection is Phase 4b work).
65
- * Worklets that need to read `source` / `pose` MUST guard for
66
- * `undefined`:
67
- *
68
- * ```ts
69
- * if (frame.source === 'ar') { ... } // false in non-AR mode
70
- * if (frame.pose) { ... } // skipped in non-AR mode
71
- * ```
72
- *
73
- * **AR mode — iOS Phase 4b.i (this release):** the worklet is
74
- * installed into the native registry via
75
- * `globalThis.__stitcherProxy.install(workletFn)`, where
76
- * `__stitcherProxy` is a JSI host object installed at lib
77
- * bootstrap by the native `StitcherJsiInstaller` module. The
78
- * AR worklet runtime (`RNSARWorkletRuntime`) reads from the
79
- * native registry on each `dispatchFrame:pose:` call and fans
80
- * out invocations — your worklet fires alongside the lib's
81
- * first-party stitching path.
82
- *
83
- * **AR mode — Android Phase 4b.ii (deferred):** the native
84
- * installer + JNI bridge from `StitcherWorkletRuntime.kt`'s
85
- * `runFirstParty {...}` path to a parallel C++ registry land in
86
- * a follow-up release. Until then, on Android the hook falls
87
- * back to the JS-side `StitcherWorkletRegistry`; AR-mode host
88
- * worklets register but do not invoke. No regression vs.
89
- * Phase 4a; iOS gets the API first.
90
- *
91
- * ### When Phase 4b.ii lands (Android)
92
- *
93
- * The hook's call signature does NOT change. Android hosts that
94
- * write code today against this API will see their worklets
95
- * start firing in AR mode automatically when Phase 4b.ii is
96
- * merged. No migration required.
97
- *
98
- * ## Frame contract
99
- *
100
- * The worklet receives a {@link StitcherFrame} (see
101
- * `src/stitching/StitcherFrame.ts` for the full contract +
102
- * lifecycle). Highlights:
103
- *
104
- * - **`source`** discriminator: `'vc'` or `'ar'`. Branch on this
105
- * before reading `arDepth` / `arAnchors` / `arTrackingState`
106
- * so non-AR captures don't break.
107
- * - **`pose`** always present. `pose.translation` is `undefined`
108
- * in non-AR mode (gyro provides only rotation; no spatial
109
- * anchor).
110
- * - **Buffer lifetime**: pixel data is valid only for the
111
- * duration of the worklet call. Worklets that need to retain
112
- * data must `toArrayBuffer()` synchronously inside the
113
- * worklet body — returning a reference and reading it later
114
- * reads freed memory.
115
- *
116
- * ## Threading
117
- *
118
- * The worklet runs on the producer thread (vision-camera's
119
- * runtime in non-AR mode; the AR-session callback thread under
120
- * Phase 4b). Worklets MUST NOT block the producer thread for
121
- * more than a few ms — the next frame's processing is gated on
122
- * the previous frame returning. Long work belongs on a queue
123
- * crossed via Reanimated / worklets-core's `runOnJS`.
124
- *
125
- * @param worklet The host's frame processor function. Must be
126
- * `'worklet'`-prefixed at the call site. TS
127
- * cannot enforce the prefix; the runtime will
128
- * throw at attempt to invoke a non-worklet
129
- * function.
130
- * @param deps Standard React deps array. When `deps` change,
131
- * the previous registration is removed and the
132
- * new worklet is registered. Same semantics as
133
- * vision-camera's `useFrameProcessor`.
134
- *
135
- * @returns A vision-camera frame-processor object that
136
- * `<Camera frameProcessor={...}>` accepts. In non-AR
137
- * mode this is what drives the per-frame worklet
138
- * invocation; in AR mode it's currently a no-op (vc
139
- * isn't mounted in AR mode anyway).
140
- */
141
- export function useFrameProcessor(
142
- worklet: StitcherFrameProcessor,
143
- deps: DependencyList,
144
- ): ReadonlyFrameProcessor | DrawableFrameProcessor {
145
- // Non-AR path: delegate to vision-camera's hook. The returned
146
- // processor object is what `<Camera>` hands to vc. Worklet
147
- // fires on vc's producer-thread runtime.
148
- //
149
- // Cast rationale: vc's hook expects `(frame: Frame) => void`.
150
- // Our worklet is typed `(frame: StitcherFrame) => void`.
151
- // `StitcherFrame` is a structural superset of `Frame` (it adds
152
- // required `source` + `pose` and the optional AR fields), so
153
- // assigning a function that consumes `StitcherFrame` to a
154
- // `Frame`-consuming slot is unsound at the type level — TS is
155
- // right to reject the direct assignment. At RUNTIME the worklet
156
- // will see vc's raw `Frame`; the `source` / `pose` / AR fields
157
- // are undefined (the hook's docstring above documents this and
158
- // tells hosts to guard). We double-cast through `unknown` to
159
- // suppress, accepting the explicit type-system gap as the price
160
- // of Phase 4a's pre-Phase-4b deferral on cross-runtime frame
161
- // wrapping.
162
- const vcProcessor = visionCameraUseFrameProcessor(
163
- worklet as unknown as (frame: Frame) => void,
164
- deps,
165
- );
166
-
167
- // AR path: install into the native registry if available (iOS
168
- // Phase 4b.i — and Android Phase 4b.ii once it lands). Falls
169
- // back to the JS-side `StitcherWorkletRegistry` when the native
170
- // installer isn't present (Android in 4b.i; remote debug mode;
171
- // unit tests). The fallback path matches Phase 4a's
172
- // register-but-not-invoke semantics.
173
- //
174
- // eslint-disable-next-line react-hooks/exhaustive-deps
175
- useEffect(() => {
176
- const nativeReady = ensureStitcherProxyInstalled();
177
- if (
178
- nativeReady &&
179
- typeof (globalThis as { __stitcherProxy?: StitcherProxy }).__stitcherProxy !== 'undefined'
180
- ) {
181
- // Native path — install through the JSI proxy. Errors here
182
- // most commonly mean the worklet doesn't have the
183
- // `'worklet'` directive at the call site (the worklets-core
184
- // babel plugin didn't transform it). Surface them via the
185
- // proxy's own throw with a host-side log so the failure is
186
- // obvious.
187
- let id: string | undefined;
188
- try {
189
- id = (globalThis as unknown as { __stitcherProxy: StitcherProxy }).__stitcherProxy.install(
190
- worklet,
191
- );
192
- } catch (err) {
193
- // Guard `__DEV__` read so the hook works in any environment
194
- // that imports it without defining the flag (jest, SSR,
195
- // custom tooling).
196
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
197
- console.error(
198
- '[react-native-image-stitcher] __stitcherProxy.install ' +
199
- 'threw — is the worklet function decorated with ' +
200
- "`'worklet';` and processed by react-native-worklets-core's " +
201
- 'babel plugin? Original error: ' +
202
- String(err),
203
- );
204
- }
205
- return; // No cleanup needed — nothing was installed.
206
- }
207
- return () => {
208
- try {
209
- (globalThis as unknown as { __stitcherProxy: StitcherProxy }).__stitcherProxy.uninstall(id!);
210
- } catch {
211
- // Uninstall is best-effort; an exception here means the
212
- // proxy was already gone (e.g., app reload mid-cleanup).
213
- }
214
- };
215
- }
216
-
217
- // Fallback — JS-side registry. Same as Phase 4a.
218
- const jsId = StitcherWorkletRegistry.register({
219
- worklet,
220
- isFirstParty: false,
221
- });
222
- return () => StitcherWorkletRegistry.unregister(jsId);
223
- }, deps);
224
-
225
- return vcProcessor;
226
- }
@@ -1,271 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- //
3
- // v0.9.0 Layer 3 — JS-thread sampled-frame stream over Layer 1 +
4
- // Layer 2.
5
- //
6
- // ## What this is
7
- //
8
- // A hook that:
9
- // 1. Throttles a worklet via `useThrottledFrameProcessor` (Layer 2)
10
- // to fire at `sampleHz` Hz.
11
- // 2. Inside the worklet, calls the `save_frame_as_jpeg` vc Frame
12
- // Processor plugin (Layer 1) to JPEG-encode the frame to a
13
- // bounded-rotation slot on disk.
14
- // 3. Bridges the resulting `SampledFrame` (file path + pose +
15
- // dims) to a JS-thread callback via `runOnJS`.
16
- //
17
- // The host gets a per-sample callback on the JS thread with a file
18
- // path they can pass to `<Image>`, an OCR RN module, a cloud-upload
19
- // library, etc. Zero worklet boilerplate.
20
- //
21
- // ## When to use this (vs alternatives)
22
- //
23
- // - **`useFrameStream`** (this hook) — JS-thread consumers. File-
24
- // path OCR libraries, cloud upload, thumbnail UI, sampled
25
- // server-side analysis.
26
- // - **`useThrottledFrameProcessor`** (Layer 2) — worklet-native
27
- // consumers. Native OCR (Vision.framework / ML Kit) wrapped as
28
- // vc plugins, TFLite ML inference, LiDAR depth processing.
29
- // Lower latency; no JPEG roundtrip.
30
- // - **`useFrameProcessor`** — every camera frame; full control.
31
- //
32
- // ## Slot reuse / disk usage
33
- //
34
- // JPEG files are written to `<outputDir>/stream-<N>.jpg` where N
35
- // cycles 0..3 based on `frame.timestamp / 1000`. At most 4 stale
36
- // JPEGs ever exist on disk; the same file is rewritten on each
37
- // rotation, so disk usage is bounded.
38
- //
39
- // Hosts that need long-term retention (e.g., archive each sample
40
- // for later upload) MUST copy the file synchronously inside the
41
- // handler — the slot may be overwritten by the next sample.
42
- //
43
- // ## Backpressure
44
- //
45
- // If the JS handler returns slower than `1/sampleHz`, subsequent
46
- // ticks DO still fire (the throttle is time-based, not handler-
47
- // completion-based). This means multiple handler invocations can
48
- // be in flight simultaneously. For most use cases that's fine
49
- // (the handlers are pure or commute). Hosts that need serialised
50
- // handling should track in-flight state themselves and early-return.
51
- //
52
- // ## AR vs non-AR
53
- //
54
- // Works in both modes because it composes over
55
- // `useThrottledFrameProcessor` → `useFrameProcessor`. In AR mode
56
- // the worklet auto-registers via `__stitcherProxy` (v0.8.0 Phase
57
- // 4b.i/iii); in non-AR mode the returned processor object is
58
- // passed to `<Camera frameProcessor={...}>`. The hook returns
59
- // the processor object so hosts can wire it up either way.
60
-
61
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
62
- import {
63
- VisionCameraProxy,
64
- type Frame,
65
- type FrameProcessorPlugin,
66
- } from 'react-native-vision-camera';
67
- import { Worklets } from 'react-native-worklets-core';
68
-
69
- import { useThrottledFrameProcessor } from './useThrottledFrameProcessor';
70
- import type { StitcherFrame } from './StitcherFrame';
71
- import type {
72
- FrameStreamOptions,
73
- SampledFrame,
74
- } from '../types';
75
- import { getDefaultCaptureDir } from '../utils/files';
76
-
77
- /**
78
- * `useFrameStream` — Layer 3. See module docstring for the full
79
- * design + use-case mapping. Quick start:
80
- *
81
- * ```tsx
82
- * import { Camera, useFrameStream } from 'react-native-image-stitcher';
83
- *
84
- * function MyScreen() {
85
- * const fp = useFrameStream(
86
- * { sampleHz: 2, quality: 75 },
87
- * (sample) => {
88
- * setThumbnail(sample.jpegPath);
89
- * },
90
- * );
91
- * return <Camera frameProcessor={fp} ... />;
92
- * }
93
- * ```
94
- *
95
- * @param options `{ sampleHz, quality?, outputDir? }`. `sampleHz`
96
- * clamped to `[0.5, 10]`.
97
- * @param handler JS-thread callback fired per sample. Receives a
98
- * `SampledFrame`. May return a Promise; rejections
99
- * are caught + logged (not re-thrown) so one
100
- * misbehaving handler doesn't break the stream.
101
- *
102
- * @returns A `useFrameProcessor`-shaped processor object — pass to
103
- * `<Camera frameProcessor={...}>` for non-AR mode wiring.
104
- * (AR mode auto-registration via `__stitcherProxy` is
105
- * handled inside `useFrameProcessor`.)
106
- */
107
- export function useFrameStream(
108
- options: FrameStreamOptions,
109
- handler: (sample: SampledFrame) => void | Promise<void>,
110
- ): ReturnType<typeof useThrottledFrameProcessor> {
111
- const sampleHz = Math.max(0.5, Math.min(10, options.sampleHz));
112
- const quality = options.quality ?? 75;
113
-
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
- };
144
- }, [options.outputDir]);
145
-
146
- const outputDir = options.outputDir ?? resolvedDefaultDir;
147
-
148
- // Stable JS-side handler reference for `runOnJS`. The hook re-
149
- // captures `handler` on every render but the ref keeps the
150
- // worklet closure pointing at the latest callback (avoid stale
151
- // captures).
152
- const handlerRef = useRef(handler);
153
- handlerRef.current = handler;
154
-
155
- const onSampleJS = useCallback((sample: SampledFrame) => {
156
- const result = handlerRef.current(sample);
157
- if (
158
- result != null &&
159
- typeof (result as Promise<void>).catch === 'function'
160
- ) {
161
- (result as Promise<void>).catch((err) => {
162
- // eslint-disable-next-line no-console
163
- console.error('[useFrameStream] handler threw:', err);
164
- });
165
- }
166
- }, []);
167
-
168
- const onSampleOnJS = useMemo(
169
- () => Worklets.createRunOnJS(onSampleJS),
170
- [onSampleJS],
171
- );
172
-
173
- // ── Plugin acquisition (Layer 1) ─────────────────────────────────
174
- //
175
- // `initFrameProcessorPlugin` can return `undefined` if the native
176
- // registry hasn't initialised yet (rare race on app start). We
177
- // retry every 16ms (one display frame) until success — matches
178
- // the pattern in `useFrameProcessorDriver`.
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);
185
- useEffect(() => {
186
- let cancelled = false;
187
- let timerId: ReturnType<typeof setTimeout> | null = null;
188
- let attempts = 0;
189
- const tryAcquire = () => {
190
- if (cancelled) return;
191
- attempts += 1;
192
- const p = VisionCameraProxy.initFrameProcessorPlugin(
193
- 'save_frame_as_jpeg',
194
- {},
195
- );
196
- if (p != null) {
197
- setPlugin(p);
198
- return;
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
- }
210
- timerId = setTimeout(tryAcquire, 16);
211
- };
212
- tryAcquire();
213
- return () => {
214
- cancelled = true;
215
- if (timerId != null) clearTimeout(timerId);
216
- };
217
- }, []);
218
-
219
- return useThrottledFrameProcessor(
220
- (frame: StitcherFrame) => {
221
- 'worklet';
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;
226
-
227
- // Slot rotation: compute slot from frame timestamp. At
228
- // sampleHz=2 (500ms interval), the slot index changes every
229
- // ~1s, giving each slot ~2 samples before being overwritten.
230
- // That's overkill for the "stream-of-samples" use case but
231
- // matches the docstring's "at most 4 stale JPEGs" guarantee.
232
- const slot = Math.floor(frame.timestamp / 1000) % 4;
233
- const path = `${outputDir}/stream-${slot}.jpg`;
234
-
235
- // vc's `FrameProcessorPlugin.call` expects vc's `Frame` type.
236
- // `StitcherFrame` is structurally a superset (it adds `source`,
237
- // `pose`, AR-only fields). Cast through `unknown` — same
238
- // pattern v0.8.0's `useFrameProcessor` uses when handing a
239
- // StitcherFrame-typed worklet to vc.
240
- const result = plugin.call(frame as unknown as Frame, {
241
- path,
242
- quality,
243
- });
244
- if (
245
- result == null ||
246
- (result as { ok?: boolean }).ok !== true
247
- ) {
248
- // Native side reported an error (path not writable, format
249
- // wrong, etc.). Silently skip this sample — the next tick
250
- // will retry. The plugin already logs the specific reason
251
- // on the native side.
252
- return;
253
- }
254
- const r = result as {
255
- path: string;
256
- width: number;
257
- height: number;
258
- };
259
-
260
- onSampleOnJS({
261
- jpegPath: r.path,
262
- pose: frame.pose,
263
- timestamp: frame.timestamp,
264
- width: r.width,
265
- height: r.height,
266
- });
267
- },
268
- { sampleHz },
269
- [plugin, outputDir, quality, onSampleOnJS],
270
- );
271
- }