react-native-image-stitcher 0.7.0 → 0.8.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 (47) hide show
  1. package/CHANGELOG.md +180 -1
  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 +4 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +78 -3
  8. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +256 -0
  10. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +100 -0
  11. package/cpp/stitcher_frame_data.hpp +141 -0
  12. package/cpp/stitcher_frame_jsi.cpp +214 -0
  13. package/cpp/stitcher_frame_jsi.hpp +108 -0
  14. package/cpp/stitcher_proxy_jsi.cpp +109 -0
  15. package/cpp/stitcher_proxy_jsi.hpp +46 -0
  16. package/cpp/stitcher_worklet_dispatch.cpp +103 -0
  17. package/cpp/stitcher_worklet_dispatch.hpp +71 -0
  18. package/cpp/stitcher_worklet_registry.cpp +81 -0
  19. package/cpp/stitcher_worklet_registry.hpp +136 -0
  20. package/dist/camera/Camera.d.ts +62 -12
  21. package/dist/camera/Camera.js +30 -15
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +11 -1
  24. package/dist/stitching/StitcherFrame.d.ts +170 -0
  25. package/dist/stitching/StitcherFrame.js +4 -0
  26. package/dist/stitching/StitcherWorkletRegistry.d.ts +117 -0
  27. package/dist/stitching/StitcherWorkletRegistry.js +78 -0
  28. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  29. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  30. package/dist/stitching/useFrameProcessor.d.ts +119 -0
  31. package/dist/stitching/useFrameProcessor.js +196 -0
  32. package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -10
  33. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  34. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  35. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +60 -0
  36. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +214 -0
  37. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  38. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +103 -0
  39. package/package.json +1 -1
  40. package/src/camera/Camera.tsx +93 -28
  41. package/src/index.ts +16 -0
  42. package/src/stitching/StitcherFrame.ts +197 -0
  43. package/src/stitching/StitcherWorkletRegistry.ts +156 -0
  44. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +176 -0
  45. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +94 -0
  46. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
  47. package/src/stitching/useFrameProcessor.ts +226 -0
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.useFrameProcessor = useFrameProcessor;
5
+ const react_1 = require("react");
6
+ const react_native_vision_camera_1 = require("react-native-vision-camera");
7
+ const ensureStitcherProxyInstalled_1 = require("./ensureStitcherProxyInstalled");
8
+ const StitcherWorkletRegistry_1 = require("./StitcherWorkletRegistry");
9
+ /**
10
+ * v0.8.0 Phase 4a — public hook for hosts to attach a per-frame
11
+ * worklet that runs in BOTH AR and non-AR capture modes.
12
+ *
13
+ * ## Quick start
14
+ *
15
+ * ```tsx
16
+ * import { useFrameProcessor, type StitcherFrame } from 'react-native-image-stitcher';
17
+ *
18
+ * function MyOcrOverlay() {
19
+ * const processor = useFrameProcessor((frame: StitcherFrame) => {
20
+ * 'worklet';
21
+ * // Pixel data is in `frame.toArrayBuffer()`.
22
+ * // AR-only fields: `frame.arDepth`, `frame.arAnchors`, `frame.arTrackingState`.
23
+ * // Discriminate via `frame.source === 'ar'` / `'vc'`.
24
+ * }, []);
25
+ * return <Camera frameProcessor={processor} ... />;
26
+ * }
27
+ * ```
28
+ *
29
+ * ## Two behaviours, depending on mode
30
+ *
31
+ * **Non-AR mode (today, fully working):** the worklet runs on
32
+ * vision-camera's Frame Processor runtime. Same thread + same
33
+ * cost envelope as a plain `useFrameProcessor` from
34
+ * `react-native-vision-camera`. The lib's own first-party
35
+ * stitching plugin runs alongside on the same producer-thread
36
+ * runtime (composition is handled by vision-camera's own dispatch
37
+ * order).
38
+ *
39
+ * Your worklet receives whatever vision-camera delivers — vc's raw
40
+ * `Frame`. This is a structural subset of `StitcherFrame`: the
41
+ * vc-shaped fields (`width`, `height`, `pixelFormat`, `orientation`,
42
+ * `timestamp`, `toArrayBuffer`) are guaranteed; the
43
+ * `StitcherFrame`-only fields (`source`, `pose`, `arDepth`,
44
+ * `arAnchors`, `arTrackingState`) are **undefined** at runtime
45
+ * because the lib does NOT wrap or augment vc's `Frame` in Phase 4a
46
+ * (cross-worklet-boundary field injection is Phase 4b work).
47
+ * Worklets that need to read `source` / `pose` MUST guard for
48
+ * `undefined`:
49
+ *
50
+ * ```ts
51
+ * if (frame.source === 'ar') { ... } // false in non-AR mode
52
+ * if (frame.pose) { ... } // skipped in non-AR mode
53
+ * ```
54
+ *
55
+ * **AR mode — iOS Phase 4b.i (this release):** the worklet is
56
+ * installed into the native registry via
57
+ * `globalThis.__stitcherProxy.install(workletFn)`, where
58
+ * `__stitcherProxy` is a JSI host object installed at lib
59
+ * bootstrap by the native `StitcherJsiInstaller` module. The
60
+ * AR worklet runtime (`RNSARWorkletRuntime`) reads from the
61
+ * native registry on each `dispatchFrame:pose:` call and fans
62
+ * out invocations — your worklet fires alongside the lib's
63
+ * first-party stitching path.
64
+ *
65
+ * **AR mode — Android Phase 4b.ii (deferred):** the native
66
+ * installer + JNI bridge from `StitcherWorkletRuntime.kt`'s
67
+ * `runFirstParty {...}` path to a parallel C++ registry land in
68
+ * a follow-up release. Until then, on Android the hook falls
69
+ * back to the JS-side `StitcherWorkletRegistry`; AR-mode host
70
+ * worklets register but do not invoke. No regression vs.
71
+ * Phase 4a; iOS gets the API first.
72
+ *
73
+ * ### When Phase 4b.ii lands (Android)
74
+ *
75
+ * The hook's call signature does NOT change. Android hosts that
76
+ * write code today against this API will see their worklets
77
+ * start firing in AR mode automatically when Phase 4b.ii is
78
+ * merged. No migration required.
79
+ *
80
+ * ## Frame contract
81
+ *
82
+ * The worklet receives a {@link StitcherFrame} (see
83
+ * `src/stitching/StitcherFrame.ts` for the full contract +
84
+ * lifecycle). Highlights:
85
+ *
86
+ * - **`source`** discriminator: `'vc'` or `'ar'`. Branch on this
87
+ * before reading `arDepth` / `arAnchors` / `arTrackingState`
88
+ * so non-AR captures don't break.
89
+ * - **`pose`** always present. `pose.translation` is `undefined`
90
+ * in non-AR mode (gyro provides only rotation; no spatial
91
+ * anchor).
92
+ * - **Buffer lifetime**: pixel data is valid only for the
93
+ * duration of the worklet call. Worklets that need to retain
94
+ * data must `toArrayBuffer()` synchronously inside the
95
+ * worklet body — returning a reference and reading it later
96
+ * reads freed memory.
97
+ *
98
+ * ## Threading
99
+ *
100
+ * The worklet runs on the producer thread (vision-camera's
101
+ * runtime in non-AR mode; the AR-session callback thread under
102
+ * Phase 4b). Worklets MUST NOT block the producer thread for
103
+ * more than a few ms — the next frame's processing is gated on
104
+ * the previous frame returning. Long work belongs on a queue
105
+ * crossed via Reanimated / worklets-core's `runOnJS`.
106
+ *
107
+ * @param worklet The host's frame processor function. Must be
108
+ * `'worklet'`-prefixed at the call site. TS
109
+ * cannot enforce the prefix; the runtime will
110
+ * throw at attempt to invoke a non-worklet
111
+ * function.
112
+ * @param deps Standard React deps array. When `deps` change,
113
+ * the previous registration is removed and the
114
+ * new worklet is registered. Same semantics as
115
+ * vision-camera's `useFrameProcessor`.
116
+ *
117
+ * @returns A vision-camera frame-processor object that
118
+ * `<Camera frameProcessor={...}>` accepts. In non-AR
119
+ * mode this is what drives the per-frame worklet
120
+ * invocation; in AR mode it's currently a no-op (vc
121
+ * isn't mounted in AR mode anyway).
122
+ */
123
+ function useFrameProcessor(worklet, deps) {
124
+ // Non-AR path: delegate to vision-camera's hook. The returned
125
+ // processor object is what `<Camera>` hands to vc. Worklet
126
+ // fires on vc's producer-thread runtime.
127
+ //
128
+ // Cast rationale: vc's hook expects `(frame: Frame) => void`.
129
+ // Our worklet is typed `(frame: StitcherFrame) => void`.
130
+ // `StitcherFrame` is a structural superset of `Frame` (it adds
131
+ // required `source` + `pose` and the optional AR fields), so
132
+ // assigning a function that consumes `StitcherFrame` to a
133
+ // `Frame`-consuming slot is unsound at the type level — TS is
134
+ // right to reject the direct assignment. At RUNTIME the worklet
135
+ // will see vc's raw `Frame`; the `source` / `pose` / AR fields
136
+ // are undefined (the hook's docstring above documents this and
137
+ // tells hosts to guard). We double-cast through `unknown` to
138
+ // suppress, accepting the explicit type-system gap as the price
139
+ // of Phase 4a's pre-Phase-4b deferral on cross-runtime frame
140
+ // wrapping.
141
+ const vcProcessor = (0, react_native_vision_camera_1.useFrameProcessor)(worklet, deps);
142
+ // AR path: install into the native registry if available (iOS
143
+ // Phase 4b.i — and Android Phase 4b.ii once it lands). Falls
144
+ // back to the JS-side `StitcherWorkletRegistry` when the native
145
+ // installer isn't present (Android in 4b.i; remote debug mode;
146
+ // unit tests). The fallback path matches Phase 4a's
147
+ // register-but-not-invoke semantics.
148
+ //
149
+ // eslint-disable-next-line react-hooks/exhaustive-deps
150
+ (0, react_1.useEffect)(() => {
151
+ const nativeReady = (0, ensureStitcherProxyInstalled_1.ensureStitcherProxyInstalled)();
152
+ if (nativeReady &&
153
+ typeof globalThis.__stitcherProxy !== 'undefined') {
154
+ // Native path — install through the JSI proxy. Errors here
155
+ // most commonly mean the worklet doesn't have the
156
+ // `'worklet'` directive at the call site (the worklets-core
157
+ // babel plugin didn't transform it). Surface them via the
158
+ // proxy's own throw with a host-side log so the failure is
159
+ // obvious.
160
+ let id;
161
+ try {
162
+ id = globalThis.__stitcherProxy.install(worklet);
163
+ }
164
+ catch (err) {
165
+ // Guard `__DEV__` read so the hook works in any environment
166
+ // that imports it without defining the flag (jest, SSR,
167
+ // custom tooling).
168
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
169
+ console.error('[react-native-image-stitcher] __stitcherProxy.install ' +
170
+ 'threw — is the worklet function decorated with ' +
171
+ "`'worklet';` and processed by react-native-worklets-core's " +
172
+ 'babel plugin? Original error: ' +
173
+ String(err));
174
+ }
175
+ return; // No cleanup needed — nothing was installed.
176
+ }
177
+ return () => {
178
+ try {
179
+ globalThis.__stitcherProxy.uninstall(id);
180
+ }
181
+ catch {
182
+ // Uninstall is best-effort; an exception here means the
183
+ // proxy was already gone (e.g., app reload mid-cleanup).
184
+ }
185
+ };
186
+ }
187
+ // Fallback — JS-side registry. Same as Phase 4a.
188
+ const jsId = StitcherWorkletRegistry_1.StitcherWorkletRegistry.register({
189
+ worklet,
190
+ isFirstParty: false,
191
+ });
192
+ return () => StitcherWorkletRegistry_1.StitcherWorkletRegistry.unregister(jsId);
193
+ }, deps);
194
+ return vcProcessor;
195
+ }
196
+ //# sourceMappingURL=useFrameProcessor.js.map
@@ -484,10 +484,41 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
484
484
  Int32(config.videoFormat.framesPerSecond))
485
485
  isRunning = true
486
486
  currentTrackingState = .initialising
487
+
488
+ // v0.8.0 Phase 3c — install the worklet runtime + register
489
+ // the first-party stitching callback. The delegate's
490
+ // per-frame ingest now routes through
491
+ // `RNSARWorkletRuntime.dispatchFrame` (see
492
+ // `session(_:didUpdate:)` below) which invokes this
493
+ // callback synchronously. Net behavior is byte-identical
494
+ // to the pre-Phase-3c direct `consumer.consumeFrame(...)`
495
+ // call. The indirection sets up the seam where Phase 4
496
+ // will fan out to host worklets without touching this
497
+ // first-party path.
498
+ RNSARWorkletRuntime.shared().installIfNeeded()
499
+ RNSARWorkletRuntime.shared().setFirstPartyCallback {
500
+ [weak self] arFrame, pose in
501
+ // ARKit pool reuse contract: must consume the pixel
502
+ // buffer before returning. The consumer's
503
+ // `consumeFrame` does that synchronously inside the
504
+ // call (NV12 → cv::Mat sync, then heavy work on its
505
+ // own queue). We're on the same thread as the
506
+ // delegate (ARSession.delegateQueue), so the contract
507
+ // holds end-to-end.
508
+ guard let self = self else { return }
509
+ guard let consumer = self.incrementalConsumer else { return }
510
+ consumer.consumeFrame(pixelBuffer: arFrame.capturedImage,
511
+ pose: pose)
512
+ }
487
513
  }
488
514
 
489
515
  @objc public func stop() {
490
516
  guard isRunning else { return }
517
+ // v0.8.0 Phase 3c — drop the first-party callback so the
518
+ // closure's `[weak self]` reference can be released
519
+ // immediately + no in-flight delegate frame re-enters the
520
+ // engine after stop. Idempotent.
521
+ RNSARWorkletRuntime.shared().setFirstPartyCallback(nil)
491
522
  arSession.pause()
492
523
  isRunning = false
493
524
  currentTrackingState = .notAvailable
@@ -561,16 +592,21 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
561
592
  }
562
593
  }
563
594
 
564
- // Deliver this frame to the live incremental-stitching
565
- // consumer if one is registered. The consumer MUST consume
566
- // the pixel buffer before returning (Apple's ARKit pool
567
- // reuse contract same constraint as the recording-append
568
- // path below) `IncrementalStitcher` does this by
569
- // converting NV12 cv::Mat synchronously inside the call,
570
- // then doing the heavy work on its own queue.
571
- if let consumer = self.incrementalConsumer {
572
- consumer.consumeFrame(pixelBuffer: frame.capturedImage, pose: pose)
573
- }
595
+ // v0.8.0 Phase 3c route the per-frame ingest through the
596
+ // worklet runtime instead of calling the consumer directly.
597
+ // The first-party callback (installed in `start()` above)
598
+ // wraps the same `consumer.consumeFrame(pixelBuffer:pose:)`
599
+ // call path, so net behavior is byte-identical to v0.7.x.
600
+ // The indirection sets up the seam where Phase 4 will fan
601
+ // out to host worklets (registered via the v0.8.0
602
+ // `useFrameProcessor` TS hook + a JSI plugin entry point)
603
+ // without changing this first-party path.
604
+ //
605
+ // ARKit pool reuse contract: still satisfied — the runtime
606
+ // invokes the callback synchronously on the delegate
607
+ // thread, and the callback's `consumer.consumeFrame(...)`
608
+ // does the same NV12 → cv::Mat sync conversion as before.
609
+ RNSARWorkletRuntime.shared().dispatchFrame(frame, pose: pose)
574
610
 
575
611
  // If recording is in flight, append this frame to the
576
612
  // asset writer DIRECTLY — no queue hop.
@@ -0,0 +1,128 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // RNSARWorkletRuntime.h — Obj-C facade for the v0.8.0 AR-mode
4
+ // worklet runtime. Wraps `react-native-worklets-core`'s
5
+ // `RNWorklet::JsiWorkletContext` (the same primitive vision-camera
6
+ // uses for its Frame Processor runtime) so the lib can dispatch
7
+ // per-ARFrame worklets on a thread we own — rather than ARKit's
8
+ // delegate queue, where doing significant work would block the
9
+ // AR session's update loop.
10
+ //
11
+ // ## Phase 3b scope (this commit)
12
+ //
13
+ // Owns:
14
+ // - The dispatch queue the worklet runtime pins to.
15
+ // - The underlying `JsiWorkletContext` (constructed lazily on
16
+ // `installIfNeeded`, lives for the singleton's lifetime).
17
+ //
18
+ // Exposes:
19
+ // - `+ shared` singleton accessor.
20
+ // - `- installIfNeeded` (idempotent runtime construction).
21
+ // - `- isInstalled` for diagnostics + tests.
22
+ // - `- dispatchFrame:pose:` — currently a no-op stub; Phase 3c
23
+ // fills in the actual host-object construction + worklet
24
+ // invocation + first-party stitching dispatch.
25
+ //
26
+ // Host-worklet registry is intentionally NOT in Phase 3b — Phase 4
27
+ // lands the JSI plugin + TS-side hook that defines the storage
28
+ // shape (NSMutableArray of boxed shared_ptrs vs a C++ vector ivar
29
+ // vs something else). Pre-committing the storage type here would
30
+ // risk rework. See
31
+ // `docs/plans/handoff/2026-05-26-v0.8.0-phases-2-5-implementation-guide.md`
32
+ // Phase 4 section for the planned API.
33
+ //
34
+ // ## Why Obj-C facade with `.mm` implementation
35
+ //
36
+ // The implementation needs to hold `std::shared_ptr<JsiWorkletContext>`
37
+ // + run JSI value construction, which can't live in pure Swift. Same
38
+ // pattern as `KeyframeGateBridge.{h,mm}` + `StitcherFrameHostObject.{h,mm}`:
39
+ // keep the header umbrella-safe (no JSI imports), put the C++ glue in
40
+ // the .mm.
41
+ //
42
+ // ## Header umbrella safety
43
+ //
44
+ // This .h imports only Foundation + ARKit (both system frameworks).
45
+ // Worklets-core types are confined to the .mm.
46
+
47
+ #pragma once
48
+
49
+ #import <Foundation/Foundation.h>
50
+ #import <ARKit/ARKit.h>
51
+
52
+ @class RNSARFramePose;
53
+
54
+ NS_ASSUME_NONNULL_BEGIN
55
+
56
+ NS_SWIFT_NAME(RNSARWorkletRuntime)
57
+ @interface RNSARWorkletRuntime : NSObject
58
+
59
+ /// Singleton accessor. One AR worklet runtime per process; multiple
60
+ /// `<Camera>` mounts share it. Construction is cheap (just an Obj-C
61
+ /// alloc + an `NSMutableArray`); the heavy JSI work happens in
62
+ /// `-installIfNeeded`.
63
+ + (instancetype)shared;
64
+
65
+ /// Construct the underlying `JsiWorkletContext` if not yet
66
+ /// installed. Idempotent — repeated calls are no-ops. Called from
67
+ /// `RNSARSession` at AR-mode start time (Phase 3c will wire this
68
+ /// up; Phase 3b ships the method but no one calls it yet).
69
+ ///
70
+ /// Threading: safe to call from any thread; internally serialised.
71
+ /// The runtime's own dispatch queue starts running once installed.
72
+ - (void)installIfNeeded;
73
+
74
+ /// Diagnostics + tests. Returns `YES` after a successful
75
+ /// `-installIfNeeded`.
76
+ - (BOOL)isInstalled;
77
+
78
+ /// Phase 3c — type of the first-party stitching callback. Invoked
79
+ /// synchronously on the caller thread (`ARSession.delegateQueue` —
80
+ /// typically main queue today) per AR frame. Block must consume
81
+ /// the pixel buffer before returning (ARKit pool reuse contract).
82
+ typedef void (^RNSARFirstPartyCallback)(ARFrame *arFrame,
83
+ RNSARFramePose *pose);
84
+
85
+ /// Phase 3c — install the closure that takes ownership of the
86
+ /// per-frame first-party stitching dispatch. Called from
87
+ /// `RNSARSession.start()` after the incremental consumer is set;
88
+ /// the block then routes `dispatchFrame:pose:` calls through to
89
+ /// the existing `incrementalConsumer.consumeFrame(...)` path.
90
+ ///
91
+ /// Pre-Phase-3c the delegate called the consumer directly. After
92
+ /// Phase 3c the delegate calls `dispatchFrame:pose:` (this class)
93
+ /// which invokes the callback. Net behavior is byte-identical;
94
+ /// the indirection sets up the seam where Phase 4 will fan out to
95
+ /// host worklets without changing the first-party path.
96
+ ///
97
+ /// Pass `nil` to clear (e.g. on `RNSARSession.stop()`). Idempotent.
98
+ - (void)setFirstPartyCallback:(nullable RNSARFirstPartyCallback)callback;
99
+
100
+ /// Dispatch one AR frame through the registered worklets. Called
101
+ /// per `ARFrame` by `RNSARSession.delegate` once Phase 3c lands the
102
+ /// migration (Phase 3b ships this method as a no-op stub so the
103
+ /// runtime can be built + linked + the API surface fixed).
104
+ ///
105
+ /// The Phase 3c implementation will:
106
+ /// 1. Build a `StitcherFrameHostObject` from `arFrame` + `pose`.
107
+ /// 2. Run the first-party stitching synchronously on the caller
108
+ /// thread (preserves today's `ingestFromARCameraView` cost
109
+ /// envelope at the producer site).
110
+ /// 3. If any host worklets are registered, dispatch the host
111
+ /// object onto the worklet runtime's thread + invoke each
112
+ /// worklet via `RNWorklet::WorkletInvoker::call`.
113
+ /// 4. Invalidate the host object after all worklets return.
114
+ ///
115
+ /// Phase 3c gate: install/idempotence tests + this method's
116
+ /// integration test required before merge. See
117
+ /// `docs/plans/handoff/2026-05-26-v0.8.0-phases-2-5-implementation-guide.md`
118
+ /// Phase 3c gate criteria.
119
+ ///
120
+ /// Threading: typically called from `ARSession.delegateQueue` (main
121
+ /// queue by default; Phase 3c will pin it explicitly to a
122
+ /// dedicated queue).
123
+ - (void)dispatchFrame:(ARFrame *)arFrame pose:(RNSARFramePose *)pose
124
+ NS_SWIFT_NAME(dispatchFrame(_:pose:));
125
+
126
+ @end
127
+
128
+ NS_ASSUME_NONNULL_END