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,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.
|
|
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",
|
package/src/camera/Camera.tsx
CHANGED
|
@@ -294,21 +294,71 @@ export interface CameraProps {
|
|
|
294
294
|
onError?: (err: CameraError) => void;
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
* Optional vision-camera frame processor.
|
|
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
|
-
*
|
|
303
|
-
* `docs/f8-frame-processor-plan.md`.
|
|
299
|
+
* ## When to set this prop
|
|
304
300
|
*
|
|
305
|
-
*
|
|
306
|
-
* `
|
|
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
|
|
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
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
|
|
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
|
-
&& !
|
|
865
|
+
&& !hostFrameProcessorAcceptedWarnedRef.current
|
|
805
866
|
) {
|
|
806
|
-
|
|
867
|
+
hostFrameProcessorAcceptedWarnedRef.current = true;
|
|
807
868
|
// eslint-disable-next-line no-console
|
|
808
|
-
console.
|
|
809
|
-
'[react-native-image-stitcher]
|
|
810
|
-
+ '
|
|
811
|
-
+ '
|
|
812
|
-
+ '
|
|
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
|
-
|
|
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).
|