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.
Files changed (58) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/android/build.gradle +35 -1
  3. package/android/src/main/cpp/CMakeLists.txt +64 -2
  4. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +227 -0
  5. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +30 -11
  6. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +21 -3
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
  8. package/android/src/main/java/io/imagestitcher/rn/SaveFrameAsJpegPlugin.kt +162 -0
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
  11. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
  12. package/cpp/stitcher_frame_data.hpp +141 -0
  13. package/cpp/stitcher_frame_jsi.cpp +214 -0
  14. package/cpp/stitcher_frame_jsi.hpp +108 -0
  15. package/cpp/stitcher_proxy_jsi.cpp +109 -0
  16. package/cpp/stitcher_proxy_jsi.hpp +46 -0
  17. package/cpp/stitcher_worklet_dispatch.cpp +103 -0
  18. package/cpp/stitcher_worklet_dispatch.hpp +71 -0
  19. package/cpp/stitcher_worklet_registry.cpp +81 -0
  20. package/cpp/stitcher_worklet_registry.hpp +136 -0
  21. package/dist/camera/Camera.d.ts +62 -12
  22. package/dist/camera/Camera.js +30 -15
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.js +30 -1
  25. package/dist/stitching/StitcherFrame.d.ts +170 -0
  26. package/dist/stitching/StitcherFrame.js +4 -0
  27. package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
  28. package/dist/stitching/StitcherWorkletRegistry.js +78 -0
  29. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  30. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  31. package/dist/stitching/useFrameProcessor.d.ts +119 -0
  32. package/dist/stitching/useFrameProcessor.js +196 -0
  33. package/dist/stitching/useFrameStream.d.ts +34 -0
  34. package/dist/stitching/useFrameStream.js +219 -0
  35. package/dist/stitching/useThrottledFrameProcessor.d.ts +33 -0
  36. package/dist/stitching/useThrottledFrameProcessor.js +132 -0
  37. package/dist/types.d.ts +87 -0
  38. package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -10
  39. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  40. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  41. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +185 -0
  42. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
  43. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
  44. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  45. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
  46. package/package.json +1 -1
  47. package/src/camera/Camera.tsx +93 -28
  48. package/src/index.ts +35 -0
  49. package/src/stitching/StitcherFrame.ts +197 -0
  50. package/src/stitching/StitcherWorkletRegistry.ts +156 -0
  51. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
  52. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
  53. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +178 -0
  54. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
  55. package/src/stitching/useFrameProcessor.ts +226 -0
  56. package/src/stitching/useFrameStream.ts +255 -0
  57. package/src/stitching/useThrottledFrameProcessor.ts +145 -0
  58. package/src/types.ts +95 -0
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // StitcherFrameHostObject.h — Obj-C facade for the v0.8.0
4
+ // `StitcherFrame` JSI host object. Header is intentionally
5
+ // Obj-C-only (no `<jsi/jsi.h>` import) so this can land in the
6
+ // public CocoaPods umbrella without breaking `use_frameworks!` hosts
7
+ // (same rationale as `KeyframeGateBridge.h`).
8
+ //
9
+ // The C++ JSI host object class lives in the .mm; this facade
10
+ // exposes only what cross-module callers need:
11
+ //
12
+ // - Factory `+ fromARFrame:pose:` that the AR worklet runtime
13
+ // calls per ARFrame to construct a host object backed by the
14
+ // current AR session's frame.
15
+ // - Opaque accessor `- (void *)jsiHostObjectPtr` returning the
16
+ // `std::shared_ptr<facebook::jsi::HostObject> *` (boxed) that
17
+ // the worklet runtime hands to `jsi::Object::createFromHostObject`.
18
+ //
19
+ // Lifetime: the Obj-C wrapper holds the C++ shared_ptr; ARC frees
20
+ // the wrapper when nothing references it. Worklet runtime
21
+ // invalidates the underlying ARFrame retain when the dispatch
22
+ // returns; after invalidation, JSI access throws.
23
+
24
+ #pragma once
25
+
26
+ #import <Foundation/Foundation.h>
27
+ #import <ARKit/ARKit.h>
28
+
29
+ @class RNSARFramePose;
30
+
31
+ NS_ASSUME_NONNULL_BEGIN
32
+
33
+ NS_SWIFT_NAME(StitcherFrameHostObject)
34
+ @interface StitcherFrameHostObject : NSObject
35
+
36
+ /// Construct a host object backed by the supplied ARFrame + pose.
37
+ /// Retains the ARFrame for the host object's lifetime — caller can
38
+ /// safely release their reference.
39
+ ///
40
+ /// Thread: safe to call from the ARSession delegate queue; the
41
+ /// resulting host object's JSI access must happen on the worklet
42
+ /// runtime's thread (separate queue).
43
+ + (instancetype)fromARFrame:(ARFrame *)arFrame pose:(RNSARFramePose *)pose;
44
+
45
+ /// Mark the host object's underlying ARFrame as no longer accessible.
46
+ /// Subsequent JSI property reads return `undefined` or throw,
47
+ /// depending on the property. Idempotent.
48
+ - (void)invalidate;
49
+
50
+ /// Opaque pointer to a `std::shared_ptr<facebook::jsi::HostObject>`.
51
+ /// The worklet runtime (Obj-C++ context with JSI available) casts
52
+ /// this back via `*reinterpret_cast<std::shared_ptr<facebook::jsi::HostObject>*>(ptr)`
53
+ /// to hand to `jsi::Object::createFromHostObject`.
54
+ ///
55
+ /// Returns `NULL` if the host object has been invalidated.
56
+ - (nullable void *)jsiHostObjectPtr;
57
+
58
+ @end
59
+
60
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,214 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // StitcherFrameHostObject.mm — iOS-specific wrapper for the shared
4
+ // `retailens::StitcherFrameJsiHostObject` (defined in
5
+ // `cpp/stitcher_frame_jsi.{hpp,cpp}`).
6
+ //
7
+ // Owns:
8
+ // - The Obj-C facade callable from Swift / other Obj-C / .mm files.
9
+ // - The iOS-specific `PixelBufferReader` impl (wraps a
10
+ // `CVPixelBufferRef` from `ARFrame.capturedImage`; lock / memcpy
11
+ // / unlock pattern).
12
+ // - The Obj-C → C++ extraction logic that builds a
13
+ // `retailens::StitcherFrameData` from an `ARFrame` + the lib's
14
+ // `RNSARFramePose`.
15
+ //
16
+ // Does NOT own:
17
+ // - The JSI `get` / `getPropertyNames` dispatch. That lives in
18
+ // `cpp/stitcher_frame_jsi.cpp` and is identical to the Android
19
+ // implementation (DRY across platforms).
20
+
21
+ #import "StitcherFrameHostObject.h"
22
+
23
+ #import <Foundation/Foundation.h>
24
+ #import <CoreVideo/CVPixelBuffer.h>
25
+ #import <CoreMedia/CoreMedia.h>
26
+ #import <os/log.h>
27
+
28
+ #include <jsi/jsi.h>
29
+
30
+ #include <algorithm>
31
+ #include <cstring>
32
+ #include <memory>
33
+ #include <string>
34
+ #include <utility>
35
+
36
+ #include "stitcher_frame_data.hpp"
37
+ #include "stitcher_frame_jsi.hpp"
38
+
39
+ using namespace facebook;
40
+
41
+ // Forward-declare the Swift `RNSARFramePose` Obj-C surface we need.
42
+ // This matches the pattern in `KeyframeGateFrameProcessor.mm`
43
+ // (forward-declaring `IncrementalStitcher`) — avoids depending on
44
+ // the autogenerated `RNImageStitcher-Swift.h`, which is created at
45
+ // build time and not always available to .mm files in this pod.
46
+ //
47
+ // MUST stay in sync with `RNSARSession.swift::RNSARFramePose` —
48
+ // adding a new field there means adding it here too.
49
+ @class RNSARFramePose;
50
+ @interface RNSARFramePose : NSObject
51
+ @property (nonatomic, readonly) double tx;
52
+ @property (nonatomic, readonly) double ty;
53
+ @property (nonatomic, readonly) double tz;
54
+ @property (nonatomic, readonly) double qx;
55
+ @property (nonatomic, readonly) double qy;
56
+ @property (nonatomic, readonly) double qz;
57
+ @property (nonatomic, readonly) double qw;
58
+ @property (nonatomic, readonly) NSInteger imageWidth;
59
+ @property (nonatomic, readonly) NSInteger imageHeight;
60
+ @property (nonatomic, readonly) double timestampMs;
61
+ @end
62
+
63
+ #pragma mark - iOS PixelBufferReader
64
+
65
+ namespace {
66
+
67
+ /// iOS-specific `retailens::PixelBufferReader` impl. See the base
68
+ /// class docstring for the general contract (thread-affinity,
69
+ /// invalidation semantics, Y-plane-only constraint). This subclass
70
+ /// adds:
71
+ /// - `CVPixelBuffer` lock/memcpy/unlock per copyTo
72
+ /// - `CFBridgingRetain` of the parent `ARFrame` so ARKit's
73
+ /// pool can't reclaim the underlying buffer mid-read
74
+ class IOSPixelBufferReader : public retailens::PixelBufferReader {
75
+ public:
76
+ explicit IOSPixelBufferReader(ARFrame* arFrame) {
77
+ // Retain the ARFrame for our lifetime. CFBridgingRetain hands
78
+ // ARC ownership to our void*. Released in destructor.
79
+ _retainedFrame = (void*)CFBridgingRetain(arFrame);
80
+ CVPixelBufferRef pixelBuffer = arFrame.capturedImage;
81
+ if (pixelBuffer != NULL) {
82
+ _bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
83
+ _height = CVPixelBufferGetHeight(pixelBuffer);
84
+ }
85
+ }
86
+
87
+ ~IOSPixelBufferReader() override {
88
+ // Transfer ownership back to ARC, which then releases.
89
+ if (_retainedFrame != nullptr) {
90
+ ARFrame* frame = CFBridgingRelease(_retainedFrame);
91
+ (void)frame;
92
+ _retainedFrame = nullptr;
93
+ }
94
+ }
95
+
96
+ std::size_t byteSize() const override {
97
+ return _bytesPerRow * _height;
98
+ }
99
+
100
+ std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) override {
101
+ if (_retainedFrame == nullptr) return 0;
102
+ ARFrame* frame = (__bridge ARFrame*)_retainedFrame;
103
+ CVPixelBufferRef pixelBuffer = frame.capturedImage;
104
+ if (pixelBuffer == NULL) return 0;
105
+
106
+ CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
107
+ const uint8_t* src = (const uint8_t*)CVPixelBufferGetBaseAddress(pixelBuffer);
108
+ std::size_t toCopy = std::min<std::size_t>(byteSize(), maxBytes);
109
+ if (src != nullptr && toCopy > 0) {
110
+ std::memcpy(dst, src, toCopy);
111
+ } else {
112
+ toCopy = 0;
113
+ }
114
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
115
+ return toCopy;
116
+ }
117
+
118
+ private:
119
+ void* _retainedFrame = nullptr; // CFBridgingRetain'd ARFrame
120
+ std::size_t _bytesPerRow = 0;
121
+ std::size_t _height = 0;
122
+ };
123
+
124
+ } // anonymous namespace
125
+
126
+ #pragma mark - Obj-C facade
127
+
128
+ @implementation StitcherFrameHostObject {
129
+ std::shared_ptr<retailens::StitcherFrameJsiHostObject> _hostObject;
130
+ }
131
+
132
+ + (instancetype)fromARFrame:(ARFrame*)arFrame pose:(RNSARFramePose*)pose {
133
+ StitcherFrameHostObject* obj = [[self alloc] init];
134
+
135
+ retailens::StitcherFrameData data;
136
+ data.source = "ar";
137
+ data.width = static_cast<int32_t>(pose.imageWidth);
138
+ data.height = static_cast<int32_t>(pose.imageHeight);
139
+ // ARKit's `kCVPixelFormatType_420YpCbCr8BiPlanarFullRange` (NV12)
140
+ // is reported as "yuv". Other formats (rare in ARKit; possible if
141
+ // ARWorldTrackingConfiguration.videoFormat is overridden to BGRA)
142
+ // → "unknown" + os_log warning so worklets that gate on
143
+ // `pixelFormat === 'yuv'` can be debugged without a screen recording.
144
+ OSType pf = CVPixelBufferGetPixelFormatType(arFrame.capturedImage);
145
+ if (pf == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ||
146
+ pf == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
147
+ data.pixelFormat = "yuv";
148
+ } else {
149
+ data.pixelFormat = "unknown";
150
+ os_log_error(OS_LOG_DEFAULT,
151
+ "[StitcherFrame] unexpected ARKit pixel format 0x%x; "
152
+ "worklet receives pixelFormat='unknown' and toArrayBuffer() "
153
+ "bytes are first-plane only (layout undefined for unknown "
154
+ "formats). See StitcherFrame.ts docstring.", (unsigned int)pf);
155
+ }
156
+ // ARKit doesn't have a `Frame.orientation` per se; pose carries
157
+ // the imageWidth >= imageHeight discriminator the lib uses
158
+ // elsewhere (`isLandscape`). v0.8.0 ships a coarse mapping;
159
+ // worklets that need exact UI orientation can read it from
160
+ // device-orientation sensors.
161
+ data.orientation =
162
+ (pose.imageWidth >= pose.imageHeight) ? "landscape-right" : "portrait";
163
+ // `ARFrame.timestamp` is CFAbsoluteTime (seconds since epoch).
164
+ // Convert to ns to match vc Frame.timestamp.
165
+ data.timestampNs = arFrame.timestamp * 1e9;
166
+
167
+ data.qx = pose.qx;
168
+ data.qy = pose.qy;
169
+ data.qz = pose.qz;
170
+ data.qw = pose.qw;
171
+ data.tx = pose.tx;
172
+ data.ty = pose.ty;
173
+ data.tz = pose.tz;
174
+ data.hasTranslation = true; // AR mode always has translation
175
+
176
+ switch (arFrame.camera.trackingState) {
177
+ case ARTrackingStateNotAvailable:
178
+ data.arTrackingState = "notAvailable";
179
+ break;
180
+ case ARTrackingStateLimited:
181
+ data.arTrackingState = "limited";
182
+ break;
183
+ case ARTrackingStateNormal:
184
+ data.arTrackingState = "normal";
185
+ break;
186
+ }
187
+
188
+ data.pixelReader = std::make_shared<IOSPixelBufferReader>(arFrame);
189
+
190
+ // Use the static factory (private ctor enforces shared_ptr
191
+ // ownership — required for `shared_from_this()` inside the JSI
192
+ // `toArrayBuffer` lambda).
193
+ obj->_hostObject =
194
+ retailens::StitcherFrameJsiHostObject::create(std::move(data));
195
+ return obj;
196
+ }
197
+
198
+ - (void)invalidate {
199
+ if (_hostObject) {
200
+ _hostObject->invalidate();
201
+ }
202
+ }
203
+
204
+ - (void*)jsiHostObjectPtr {
205
+ if (!_hostObject) return NULL;
206
+ // Box a heap-allocated copy of the shared_ptr to the abstract
207
+ // `jsi::HostObject` base. Caller (worklet runtime) does:
208
+ // auto sp = static_cast<std::shared_ptr<jsi::HostObject>*>(ptr);
209
+ // auto jsObj = jsi::Object::createFromHostObject(rt, *sp);
210
+ // delete sp;
211
+ return new std::shared_ptr<jsi::HostObject>(_hostObject);
212
+ }
213
+
214
+ @end
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // StitcherJsiInstaller.h — RN module that installs the
4
+ // `globalThis.__stitcherProxy` JSI host object on the main JS
5
+ // runtime. Called once at lib boot from the TS layer
6
+ // (`src/index.ts` or the `useFrameProcessor` hook) via a
7
+ // synchronous JS bridge call.
8
+ //
9
+ // The proxy exposes two host functions:
10
+ //
11
+ // __stitcherProxy.install(workletFn) → string ID
12
+ // __stitcherProxy.uninstall(id) → undefined
13
+ //
14
+ // `install` wraps the worklet function into a
15
+ // `RNWorklet::WorkletInvoker` and stores it in the C++
16
+ // `retailens::StitcherWorkletRegistry` singleton (in
17
+ // `cpp/stitcher_worklet_registry.{hpp,cpp}`). The AR worklet
18
+ // runtime's per-frame dispatch reads from that registry to fan
19
+ // out invocations.
20
+ //
21
+ // Why a RN module (not a vanilla NSObject installable):
22
+ // - Hosts can't reliably reach into the JSI runtime from JS
23
+ // without a native sync method to broker the install.
24
+ // - `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD` is the documented
25
+ // pattern for "JS calls a native method synchronously to
26
+ // install JSI bindings on the main runtime". vision-camera
27
+ // uses the same pattern (`VisionCameraInstaller.mm`).
28
+ // - In RN's bridgeless mode the legacy `RCTCxxBridge.runtime`
29
+ // accessor still works (vc has a comment to migrate but it
30
+ // hasn't been needed yet — same applies to us).
31
+
32
+ #pragma once
33
+
34
+ #import <Foundation/Foundation.h>
35
+ #import <React/RCTBridgeModule.h>
36
+
37
+ NS_ASSUME_NONNULL_BEGIN
38
+
39
+ @interface StitcherJsiInstaller : NSObject <RCTBridgeModule>
40
+ @end
41
+
42
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // StitcherJsiInstaller.mm — implementation. Installs
4
+ // `globalThis.__stitcherProxy` on the main JS runtime.
5
+ //
6
+ // ## Why a host object rather than two globalThis functions
7
+ //
8
+ // We could install `__stitcherProxy_install` + `__stitcherProxy_uninstall`
9
+ // directly on `globalThis`. Wrapping them in a host object is
10
+ // slightly more code but:
11
+ // - Namespaces the proxy under a single global property
12
+ // (easier to feature-detect; one `if (globalThis.__stitcherProxy)`
13
+ // instead of two).
14
+ // - Matches vc's pattern (`global.VisionCameraProxy`), so future
15
+ // readers recognise the shape.
16
+ // - Keeps room to grow (e.g., add `__stitcherProxy.snapshot()` for
17
+ // diagnostics) without polluting globalThis further.
18
+
19
+ #import "StitcherJsiInstaller.h"
20
+
21
+ #import <Foundation/Foundation.h>
22
+ #import <React/RCTBridge.h>
23
+ #import <React/RCTBridge+Private.h>
24
+ #import <React/RCTUtils.h>
25
+ #import <os/log.h>
26
+
27
+ #include <jsi/jsi.h>
28
+
29
+ #include "stitcher_proxy_jsi.hpp"
30
+
31
+ using namespace facebook;
32
+
33
+ // The host object class + install logic moved to shared C++ in
34
+ // `cpp/stitcher_proxy_jsi.{hpp,cpp}` (v0.8.0 Phase 4b.ii). The
35
+ // Android JNI installer reuses the same `install` / `uninstall` /
36
+ // `count` host functions verbatim — the JSI dispatch is identical
37
+ // across platforms (matches the StitcherFrame host object's design).
38
+
39
+ #pragma mark - RN module
40
+
41
+ @implementation StitcherJsiInstaller
42
+
43
+ // RN injects `_bridge` at module init (legacy bridge → RCTBridge*;
44
+ // bridgeless / new arch → RCTBridgeProxy*, which forwards `runtime`
45
+ // access via NSProxy `forwardInvocation:`). Using the injected
46
+ // `_bridge` instead of `[RCTBridge currentBridge]` is the
47
+ // bridgeless-compatible idiom — `currentBridge` is nil under new
48
+ // arch. Pattern lifted from `react-native-worklets-core/ios/Worklets.mm`.
49
+ @synthesize bridge = _bridge;
50
+
51
+ RCT_EXPORT_MODULE()
52
+
53
+ + (BOOL)requiresMainQueueSetup {
54
+ return YES;
55
+ }
56
+
57
+ - (void)setBridge:(RCTBridge*)bridge {
58
+ _bridge = bridge;
59
+ }
60
+
61
+ // Synchronous install method. JS calls this once at lib bootstrap
62
+ // to install the global proxy on the main JS runtime. Returns
63
+ // `@YES` on success or `@NO` if the JSI runtime wasn't reachable
64
+ // (remote debug mode pre-Hermes; bridge not yet ready; etc.).
65
+ //
66
+ // `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD` is the documented
67
+ // pattern for "run native code synchronously on the JS thread to
68
+ // install JSI bindings." Same pattern worklets-core + vision-camera
69
+ // use for their installs.
70
+ //
71
+ // **Bridgeless mode:** `_bridge` is an `RCTBridgeProxy` (NSProxy
72
+ // subclass) that forwards `-runtime` / `-jsCallInvoker` invocations
73
+ // to the underlying RCTHost-backed runtime. The `(RCTCxxBridge*)`
74
+ // cast is a no-op at runtime (NSProxy ignores static type) but
75
+ // keeps the Obj-C compiler happy about property access.
76
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
77
+ if (_bridge == nil) {
78
+ os_log_error(OS_LOG_DEFAULT,
79
+ "[StitcherJsiInstaller] _bridge is nil; the module was "
80
+ "instantiated without bridge injection. Cannot install "
81
+ "__stitcherProxy.");
82
+ return @NO;
83
+ }
84
+
85
+ RCTCxxBridge* cxxBridge = (RCTCxxBridge*)_bridge;
86
+ if (cxxBridge.runtime == nullptr) {
87
+ os_log_error(OS_LOG_DEFAULT,
88
+ "[StitcherJsiInstaller] _bridge.runtime is nullptr; the JS "
89
+ "runtime hasn't been initialized yet OR remote debugger is "
90
+ "attached. Cannot install __stitcherProxy.");
91
+ return @NO;
92
+ }
93
+
94
+ jsi::Runtime& runtime = *(jsi::Runtime*)cxxBridge.runtime;
95
+ retailens::installStitcherProxy(runtime);
96
+
97
+ os_log_info(OS_LOG_DEFAULT,
98
+ "[StitcherJsiInstaller] installed globalThis.__stitcherProxy "
99
+ "on main JS runtime.");
100
+ return @YES;
101
+ }
102
+
103
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-image-stitcher",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -294,21 +294,71 @@ export interface CameraProps {
294
294
  onError?: (err: CameraError) => void;
295
295
 
296
296
  /**
297
- * Optional vision-camera frame processor. Only attached to the
298
- * non-AR preview (AR mode uses ARCameraView, which doesn't expose
299
- * a worklet seam). Build the worklet on the host side with
300
- * `useFrameProcessor` from `react-native-vision-camera`.
297
+ * Optional host-supplied vision-camera frame processor.
301
298
  *
302
- * Introduced for F8 (FrameProcessor port) — see
303
- * `docs/f8-frame-processor-plan.md`.
299
+ * ## When to set this prop
304
300
  *
305
- * The SDK installs its own frame processor via
306
- * `useFrameProcessorDriver`. Setting this prop is ignored with
307
- * a one-time `console.warn` — supplying a host worklet would
308
- * race with the SDK's pixel-buffer feed. Either remove the prop
309
- * or fork the SDK if you genuinely need a custom worklet.
301
+ * v0.8.0+ canonical answer: use the lib's own `useFrameProcessor`
302
+ * hook, NOT `react-native-vision-camera`'s. The lib's hook:
310
303
  *
311
- * AR mode is irrelevant: vision-camera's Camera isn't mounted.
304
+ * - **AR mode**: auto-registers the worklet in the native
305
+ * `__stitcherProxy` registry; the AR session's per-frame
306
+ * dispatch fans out to it alongside the lib's first-party
307
+ * stitching. No prop wiring needed — just mount the hook
308
+ * anywhere in the tree.
309
+ * - **Non-AR mode**: returns a vc processor object that this
310
+ * prop accepts. Wiring it through enables the host's
311
+ * worklet to fire on vc's Frame Processor runtime.
312
+ *
313
+ * ```tsx
314
+ * import { Camera, useFrameProcessor, type StitcherFrame }
315
+ * from 'react-native-image-stitcher';
316
+ *
317
+ * function MyScreen() {
318
+ * const fp = useFrameProcessor((frame: StitcherFrame) => {
319
+ * 'worklet';
320
+ * // ...
321
+ * }, []);
322
+ * return <Camera frameProcessor={fp} ... />;
323
+ * }
324
+ * ```
325
+ *
326
+ * ## Non-AR mode tradeoff (HONEST)
327
+ *
328
+ * vision-camera's `<Camera>` accepts ONLY ONE frame processor.
329
+ * The lib's internal `useFrameProcessorDriver` produces the
330
+ * processor that drives first-party panorama stitching in non-AR
331
+ * mode. If you supply your own via this prop, **the lib's
332
+ * first-party stitching is replaced** — panorama capture in
333
+ * non-AR mode will not produce stitched output until you remove
334
+ * the prop or fork the SDK to compose both worklets manually.
335
+ *
336
+ * For the common case (host wants worklet + lib wants stitching
337
+ * concurrently), prefer AR mode: the AR-mode path natively fans
338
+ * out to both the lib's first-party stitching AND every
339
+ * registered host worklet on every frame, with per-worklet
340
+ * failure isolation.
341
+ *
342
+ * Composition for non-AR mode (lib stitching + host worklet on
343
+ * the same vc processor) is tracked as a v0.9+ follow-up;
344
+ * needs the lib's first-party logic exposed as a vc Frame
345
+ * Processor plugin the host's worklet can call.
346
+ *
347
+ * ## AR mode behaviour
348
+ *
349
+ * In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
350
+ * vc's `<Camera>` isn't mounted; this prop has no effect.
351
+ * Host worklets registered via the lib's `useFrameProcessor`
352
+ * fire automatically through the AR-session dispatch path
353
+ * (iOS Phase 4b.i / Android Phase 4b.iii).
354
+ *
355
+ * ## Backwards compatibility
356
+ *
357
+ * The pre-v0.8.0 behaviour (warn + ignore) is preserved when the
358
+ * supplied processor is recognisably from
359
+ * `react-native-vision-camera`'s `useFrameProcessor` directly
360
+ * (no `__stitcherFrame` marker). Hosts should migrate to the
361
+ * lib's `useFrameProcessor` to benefit from AR-mode dispatch.
312
362
  *
313
363
  * (v0.5 had a `legacyDriver` escape hatch that routed back to
314
364
  * `useIncrementalJSDriver`. That hook + prop were removed in
@@ -791,29 +841,44 @@ export function Camera(props: CameraProps): React.JSX.Element {
791
841
  // eslint-disable-next-line react-hooks/exhaustive-deps
792
842
  useEffect(() => () => { fpDriver.stop(); }, []);
793
843
 
794
- // One-shot deprecation warning when the host supplies their own
795
- // `frameProcessor` prop. Two worklets racing on the same
796
- // producer thread would corrupt the engine's workQueue ordering,
797
- // so the SDK's own worklet wins and the host's is silently
798
- // ignored. (v0.5 had a `legacyDriver` opt-out for hosts that
799
- // wanted to route around the SDK driver; that was removed in
800
- // v0.6 along with `useIncrementalJSDriver`.)
801
- const hostFrameProcessorIgnoredWarnedRef = useRef(false);
844
+ // v0.8.0 Phase 5 frameProcessor prop semantics:
845
+ //
846
+ // - Host supplied? use host's processor; lib's first-party
847
+ // stitching is DISABLED in non-AR mode (vc accepts only one
848
+ // processor). One-shot console.info documents the tradeoff
849
+ // so the host isn't surprised by "panorama capture stopped
850
+ // producing output" in non-AR mode. AR-mode capture is
851
+ // unaffected the AR-session dispatch path fans out to BOTH
852
+ // first-party and host worklets independently.
853
+ //
854
+ // - No host processor? → use `fpDriver.frameProcessor` which is
855
+ // the lib's internal worklet driving first-party stitching
856
+ // via `useFrameProcessorDriver`. Default behaviour for the
857
+ // common "I just want panorama capture" case.
858
+ //
859
+ // The pre-v0.8.0 behaviour (host's prop silently ignored with
860
+ // a warning) is gone — Phase 5 plumbs the prop through. The
861
+ // tradeoff is honestly documented in the CameraProps docstring.
862
+ const hostFrameProcessorAcceptedWarnedRef = useRef(false);
802
863
  if (
803
864
  hostFrameProcessor != null
804
- && !hostFrameProcessorIgnoredWarnedRef.current
865
+ && !hostFrameProcessorAcceptedWarnedRef.current
805
866
  ) {
806
- hostFrameProcessorIgnoredWarnedRef.current = true;
867
+ hostFrameProcessorAcceptedWarnedRef.current = true;
807
868
  // eslint-disable-next-line no-console
808
- console.warn(
809
- '[react-native-image-stitcher] The `frameProcessor` prop on '
810
- + '<Camera> is ignored the SDK installs its own worklet '
811
- + 'via useFrameProcessorDriver. Remove the prop, or fork '
812
- + 'the SDK if you genuinely need a custom worklet.',
869
+ console.info(
870
+ '[react-native-image-stitcher] Host frameProcessor supplied '
871
+ + 'non-AR mode will run YOUR worklet instead of the lib\'s '
872
+ + 'first-party stitching plugin (vc accepts only one frame '
873
+ + 'processor). Non-AR panorama capture will not produce '
874
+ + 'stitched output until this prop is removed. AR-mode '
875
+ + 'capture is unaffected (AR-session dispatch fans out to '
876
+ + 'both first-party and host worklets independently).',
813
877
  );
814
878
  }
815
879
  // The Frame Processor worklet bound to vision-camera's Camera.
816
- const effectiveFrameProcessor = fpDriver.frameProcessor;
880
+ // Host's wins if supplied; lib's internal driver otherwise.
881
+ const effectiveFrameProcessor = hostFrameProcessor ?? fpDriver.frameProcessor;
817
882
 
818
883
  // ── Subscribe to engine state for live keyframe thumbs ──────────
819
884
  useEffect(() => {
package/src/index.ts CHANGED
@@ -182,6 +182,41 @@ export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
182
182
  // caveat). Foundation for plugin-pattern host features (OCR per
183
183
  // keyframe, packet detection, server-side analysis, etc.).
184
184
  export { useKeyframeStream } from './stitching/useKeyframeStream';
185
+ // v0.8.0 — unified frame contract for the worklet processor. Same
186
+ // JS-visible shape regardless of capture mode (AR vs non-AR).
187
+ export type {
188
+ StitcherFrame,
189
+ StitcherFrameProcessor,
190
+ ARAnchor,
191
+ } from './stitching/StitcherFrame';
192
+ // v0.8.0 Phase 4a — public host-worklet hook. Hosts that want a
193
+ // per-frame callback (OCR overlay, packet detection, ML inference)
194
+ // use this to attach a `'worklet'`-prefixed function that fires
195
+ // on the camera producer thread. Non-AR mode is fully wired
196
+ // today via vision-camera passthrough; AR-mode dispatch is
197
+ // API-stable but registration-only until Phase 4b lands the
198
+ // cross-runtime handoff (the AR runtime iterating the registry).
199
+ // See the hook's docstring + StitcherFrame.ts for the contract.
200
+ export { useFrameProcessor } from './stitching/useFrameProcessor';
201
+ // v0.9.0 Layer 2 — `useThrottledFrameProcessor`. Throttle gate over
202
+ // `useFrameProcessor` for sub-frame-rate worklet-native processing
203
+ // (native OCR via Vision.framework / ML Kit, TFLite ML detection,
204
+ // LiDAR depth). The worklet runtime has direct access to
205
+ // `frame.toArrayBuffer()` / `frame.arDepth`; bridge small payloads
206
+ // (bboxes, depth-derived metrics) to JS via `runOnJS`. For JS-thread
207
+ // JPEG consumers (file-path OCR libs, cloud upload, thumbnail UI),
208
+ // prefer `useFrameStream` (Layer 3, ships in the same release).
209
+ export { useThrottledFrameProcessor } from './stitching/useThrottledFrameProcessor';
210
+ export type { ThrottledFrameProcessorOptions } from './types';
211
+ // v0.9.0 Layer 3 — `useFrameStream`. JS-thread sampled-frame
212
+ // stream over Layer 1 (`save_frame_as_jpeg` vc plugin) + Layer 2
213
+ // (`useThrottledFrameProcessor`). Use for JS-thread consumers:
214
+ // file-path OCR libs (RN modules), cloud upload, thumbnail UI.
215
+ // For worklet-native processing (Vision/ML Kit as vc plugins,
216
+ // TFLite ML, LiDAR depth), prefer `useThrottledFrameProcessor`
217
+ // (Layer 2) — lower latency, no JPEG roundtrip.
218
+ export { useFrameStream } from './stitching/useFrameStream';
219
+ export type { FrameStreamOptions, SampledFrame } from './types';
185
220
  // vision-camera Frame Processor driver for non-AR captures. As
186
221
  // of v0.6 the only non-AR driver exported (the legacy
187
222
  // `useIncrementalJSDriver` was removed; was deprecated in v0.5).