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
|
@@ -1001,14 +1001,17 @@ class IncrementalStitcher(
|
|
|
1001
1001
|
// per accepted frame on a mid-tier device. Pass null to use
|
|
1002
1002
|
// the legacy JPEG path.
|
|
1003
1003
|
//
|
|
1004
|
-
// OWNERSHIP:
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1004
|
+
// OWNERSHIP: wrapped in `TransferredNV21` (audit #4A,
|
|
1005
|
+
// v0.10.0). The wrapper enforces single-use: the engine
|
|
1006
|
+
// calls `.takeOnce()` on the producer thread before
|
|
1007
|
+
// dispatching to `workScope`; subsequent attempts to extract
|
|
1008
|
+
// the bytes throw. Callers MUST construct a fresh
|
|
1009
|
+
// `TransferredNV21` per frame and MUST NOT hand the same
|
|
1010
|
+
// instance to two consumers (e.g., a sync gate-eval + an
|
|
1011
|
+
// async workScope.launch). The Frame Processor plugin and
|
|
1012
|
+
// the AR camera view both allocate fresh NV21 arrays per
|
|
1013
|
+
// frame; the wrapper is a defensive-programming guard.
|
|
1014
|
+
nv21PixelData: TransferredNV21? = null,
|
|
1012
1015
|
nv21PixelWidth: Int = 0,
|
|
1013
1016
|
nv21PixelHeight: Int = 0,
|
|
1014
1017
|
) {
|
|
@@ -1215,11 +1218,20 @@ class IncrementalStitcher(
|
|
|
1215
1218
|
)
|
|
1216
1219
|
return
|
|
1217
1220
|
}
|
|
1221
|
+
// v0.10.0 audit #4A — extract the wrapped bytes ONCE on the
|
|
1222
|
+
// producer thread before dispatching to workScope. This
|
|
1223
|
+
// makes the transfer-of-ownership explicit + caught early:
|
|
1224
|
+
// if a caller accidentally passes the same TransferredNV21
|
|
1225
|
+
// instance to a sync consumer earlier, takeOnce() would
|
|
1226
|
+
// have already thrown there. Capturing `pixelBytes` by
|
|
1227
|
+
// value inside the coroutine sidesteps any chance of the
|
|
1228
|
+
// wrapper being read from two threads.
|
|
1229
|
+
val pixelBytes: ByteArray? = if (hasPixelData) nv21PixelData!!.takeOnce() else null
|
|
1218
1230
|
workScope.launch {
|
|
1219
1231
|
val state: WritableMap? = if (firstwins != null) {
|
|
1220
1232
|
val tele = if (hasPixelData) {
|
|
1221
1233
|
firstwins.addFramePixelData(
|
|
1222
|
-
nv21 =
|
|
1234
|
+
nv21 = pixelBytes!!,
|
|
1223
1235
|
nv21Width = nv21PixelWidth,
|
|
1224
1236
|
nv21Height = nv21PixelHeight,
|
|
1225
1237
|
qx = qx, qy = qy, qz = qz, qw = qw,
|
|
@@ -1246,7 +1258,7 @@ class IncrementalStitcher(
|
|
|
1246
1258
|
} else {
|
|
1247
1259
|
val tele = if (hasPixelData) {
|
|
1248
1260
|
hybrid!!.addFramePixelData(
|
|
1249
|
-
nv21 =
|
|
1261
|
+
nv21 = pixelBytes!!,
|
|
1250
1262
|
nv21Width = nv21PixelWidth,
|
|
1251
1263
|
nv21Height = nv21PixelHeight,
|
|
1252
1264
|
qx = qx, qy = qy, qz = qz, qw = qw,
|
|
@@ -1410,7 +1422,14 @@ class IncrementalStitcher(
|
|
|
1410
1422
|
// `addFramePixelData` instead of JPEG-decoding a
|
|
1411
1423
|
// separately-written path. Batch-keyframe mode
|
|
1412
1424
|
// ignores these (it uses `grayData` + `onAccept`).
|
|
1413
|
-
|
|
1425
|
+
//
|
|
1426
|
+
// v0.10.0 audit #4A — wrap in TransferredNV21 so the
|
|
1427
|
+
// engine takes ownership exactly once on the producer
|
|
1428
|
+
// thread (engine calls `.takeOnce()` before workScope).
|
|
1429
|
+
// Misuse (handing this same instance to two consumers)
|
|
1430
|
+
// throws at the second `.takeOnce()` site, not silently
|
|
1431
|
+
// corrupting frames.
|
|
1432
|
+
nv21PixelData = TransferredNV21(nv21Bytes),
|
|
1414
1433
|
nv21PixelWidth = width,
|
|
1415
1434
|
nv21PixelHeight = height,
|
|
1416
1435
|
onAccept = { targetPath ->
|
|
@@ -55,19 +55,33 @@ class RNImageStitcherPackage : ReactPackage {
|
|
|
55
55
|
) { proxy, options ->
|
|
56
56
|
CvFlowGateFrameProcessor(proxy, options)
|
|
57
57
|
}
|
|
58
|
+
// v0.9.0 Layer 1 — register `save_frame_as_jpeg`
|
|
59
|
+
// alongside the cv_flow_gate plugin. Same lifecycle,
|
|
60
|
+
// same defensive error handling (the outer try/catch
|
|
61
|
+
// covers both registrations). Either both register
|
|
62
|
+
// or neither does — if vc isn't on the classpath,
|
|
63
|
+
// both calls are skipped together.
|
|
64
|
+
FrameProcessorPluginRegistry.addFrameProcessorPlugin(
|
|
65
|
+
SaveFrameAsJpegPlugin.PLUGIN_NAME,
|
|
66
|
+
) { proxy, options ->
|
|
67
|
+
SaveFrameAsJpegPlugin(proxy, options)
|
|
68
|
+
}
|
|
58
69
|
fpPluginRegistered = true
|
|
59
70
|
} catch (e: NoClassDefFoundError) {
|
|
60
71
|
android.util.Log.i(
|
|
61
72
|
"RNImageStitcherPackage",
|
|
62
73
|
"vision-camera FrameProcessorPluginRegistry not on classpath — "
|
|
63
|
-
+ "skipping cv_flow_gate_process_frame
|
|
64
|
-
+ "(host app doesn't appear to use
|
|
74
|
+
+ "skipping cv_flow_gate_process_frame + save_frame_as_jpeg "
|
|
75
|
+
+ "plugin registration (host app doesn't appear to use "
|
|
76
|
+
+ "Frame Processors).",
|
|
65
77
|
)
|
|
66
78
|
fpPluginRegistered = true // don't retry every package init
|
|
67
79
|
} catch (e: Throwable) {
|
|
68
80
|
android.util.Log.w(
|
|
69
81
|
"RNImageStitcherPackage",
|
|
70
|
-
"Failed to register
|
|
82
|
+
"Failed to register Frame Processor plugins "
|
|
83
|
+
+ "(cv_flow_gate_process_frame / save_frame_as_jpeg): "
|
|
84
|
+
+ e.message,
|
|
71
85
|
)
|
|
72
86
|
fpPluginRegistered = true
|
|
73
87
|
}
|
|
@@ -87,6 +101,10 @@ class RNImageStitcherPackage : ReactPackage {
|
|
|
87
101
|
RNSARSession(reactContext),
|
|
88
102
|
IncrementalStitcher(reactContext),
|
|
89
103
|
FileBridge(reactContext),
|
|
104
|
+
// v0.8.0 Phase 4b.ii — Android JSI installer for the
|
|
105
|
+
// host-worklet `__stitcherProxy` global. Mirror of
|
|
106
|
+
// iOS' `StitcherJsiInstaller`.
|
|
107
|
+
StitcherJsiInstallerModule(reactContext),
|
|
90
108
|
)
|
|
91
109
|
}
|
|
92
110
|
|
|
@@ -287,8 +287,20 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
287
287
|
// contract was already in place for Phase 4.
|
|
288
288
|
appendPose(camera, frame.timestamp)
|
|
289
289
|
|
|
290
|
-
// Forward to the incremental stitcher if engaged
|
|
291
|
-
|
|
290
|
+
// Forward to the incremental stitcher if engaged, OR if any
|
|
291
|
+
// host worklets are registered (v0.8.0 Phase 4b.iii). iOS'
|
|
292
|
+
// `RNSARWorkletRuntime.dispatchFrame:pose:` fires on every
|
|
293
|
+
// AR frame regardless of capture state; Android needs the
|
|
294
|
+
// same semantic so host worklets see the AR-mode preview
|
|
295
|
+
// stream, not just capture frames.
|
|
296
|
+
//
|
|
297
|
+
// `hasHostWorklets()` is a microsecond atomic-read on the
|
|
298
|
+
// native registry — cheap enough to hit per frame. When
|
|
299
|
+
// no host worklets are registered AND no capture is active,
|
|
300
|
+
// the entire forwardToIncremental branch (including the
|
|
301
|
+
// ~3-5ms NV21 pack) is skipped — same cost envelope as
|
|
302
|
+
// before Phase 4b.iii.
|
|
303
|
+
if (ingestActive || StitcherWorkletRuntime.hasHostWorklets()) {
|
|
292
304
|
forwardToIncremental(frame, camera)
|
|
293
305
|
}
|
|
294
306
|
|
|
@@ -514,6 +526,23 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
514
526
|
// (Was: eager JPEG encode for non-batch-keyframe modes,
|
|
515
527
|
// written to `tmpJpegFile`, passed as `legacyJpegPath`.
|
|
516
528
|
// See the v0.3 / F8.6 entries in CHANGELOG.md.)
|
|
529
|
+
//
|
|
530
|
+
// v0.8.0 Phase 3c — route through the worklet runtime's
|
|
531
|
+
// `runFirstParty` indirection. The lambda body is the
|
|
532
|
+
// unchanged engine ingest call; the indirection sets up
|
|
533
|
+
// the seam where Phase 4 will fan out to host worklets
|
|
534
|
+
// without touching this first-party path. Synchronous
|
|
535
|
+
// invocation preserves the ARCore Image ownership contract
|
|
536
|
+
// — the engine consumes the TransferredNV21 inside the
|
|
537
|
+
// lambda before ARCore recycles the Image.
|
|
538
|
+
StitcherWorkletRuntime.installIfNeeded()
|
|
539
|
+
// v0.8.0 Phase 4b.iii — only run first-party stitching when
|
|
540
|
+
// the host has actively engaged capture (`setIncrementalIngestionActive(true)`).
|
|
541
|
+
// The host-worklet dispatch below runs regardless, so AR-mode
|
|
542
|
+
// preview frames stream through registered host worklets even
|
|
543
|
+
// before/after capture.
|
|
544
|
+
if (ingestActive) {
|
|
545
|
+
StitcherWorkletRuntime.runFirstParty {
|
|
517
546
|
module.ingestFromARCameraView(
|
|
518
547
|
tx = tArr[0].toDouble(),
|
|
519
548
|
ty = tArr[1].toDouble(),
|
|
@@ -538,7 +567,17 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
538
567
|
legacyJpegPath = null,
|
|
539
568
|
// F8.6 — pixel-data path for live engines. Batch-
|
|
540
569
|
// keyframe mode ignores these (bails earlier).
|
|
541
|
-
|
|
570
|
+
//
|
|
571
|
+
// v0.10.0 audit #4A — wrap `packed.nv21` in
|
|
572
|
+
// TransferredNV21 so ownership is enforced at runtime.
|
|
573
|
+
// The AR caller passes the SAME `packed.nv21` array as
|
|
574
|
+
// both `grayData` (sync, gate-eval read) and
|
|
575
|
+
// `nv21PixelData` (async, engine ingest). Today no race
|
|
576
|
+
// because grayData is consumed inside evaluateWithFrame
|
|
577
|
+
// before workScope.launch fires; the wrapper makes a
|
|
578
|
+
// future refactor that reorders consumption fail loudly
|
|
579
|
+
// instead of silently corrupting frames.
|
|
580
|
+
nv21PixelData = TransferredNV21(packed.nv21),
|
|
542
581
|
nv21PixelWidth = packed.width,
|
|
543
582
|
nv21PixelHeight = packed.height,
|
|
544
583
|
onAccept = { targetPath ->
|
|
@@ -555,6 +594,42 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
555
594
|
) != null
|
|
556
595
|
},
|
|
557
596
|
)
|
|
597
|
+
} // closes StitcherWorkletRuntime.runFirstParty { … } (v0.8.0 Phase 3c)
|
|
598
|
+
} // closes `if (ingestActive)` (v0.8.0 Phase 4b.iii)
|
|
599
|
+
|
|
600
|
+
// ── v0.8.0 Phase 4b.iii — host-worklet fan-out ─────────────
|
|
601
|
+
//
|
|
602
|
+
// Dispatch the AR frame to every host worklet registered via
|
|
603
|
+
// `globalThis.__stitcherProxy.install(workletFn)` (the
|
|
604
|
+
// `useFrameProcessor` hook's AR-mode path). The native side
|
|
605
|
+
// fast-path early-exits when the registry is empty (~ns
|
|
606
|
+
// cost), so this call is free for first-party-only deployments.
|
|
607
|
+
//
|
|
608
|
+
// Map the trackingState back to the JS-visible string set.
|
|
609
|
+
// `RNSARSession.TRACKING_*` are int codes; we re-derive the
|
|
610
|
+
// string here instead of plumbing it through. (Could be
|
|
611
|
+
// refactored into a helper if/when other call sites need
|
|
612
|
+
// it.)
|
|
613
|
+
val trackingStateStr = when (camera.trackingState) {
|
|
614
|
+
TrackingState.TRACKING -> "normal"
|
|
615
|
+
TrackingState.PAUSED -> "limited"
|
|
616
|
+
TrackingState.STOPPED -> "notAvailable"
|
|
617
|
+
else -> ""
|
|
618
|
+
}
|
|
619
|
+
StitcherWorkletRuntime.dispatchToHostWorklets(
|
|
620
|
+
nv21Bytes = packed.nv21,
|
|
621
|
+
width = packed.width,
|
|
622
|
+
height = packed.height,
|
|
623
|
+
qx = qarr[0].toDouble(),
|
|
624
|
+
qy = qarr[1].toDouble(),
|
|
625
|
+
qz = qarr[2].toDouble(),
|
|
626
|
+
qw = qarr[3].toDouble(),
|
|
627
|
+
tx = tArr[0].toDouble(),
|
|
628
|
+
ty = tArr[1].toDouble(),
|
|
629
|
+
tz = tArr[2].toDouble(),
|
|
630
|
+
timestampNs = frame.timestamp.toDouble(),
|
|
631
|
+
trackingState = trackingStateStr,
|
|
632
|
+
)
|
|
558
633
|
}
|
|
559
634
|
|
|
560
635
|
private fun applyDisplayGeometry() {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
package io.imagestitcher.rn
|
|
3
|
+
|
|
4
|
+
import android.graphics.ImageFormat
|
|
5
|
+
import android.media.Image
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.annotation.Keep
|
|
8
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
9
|
+
import com.mrousavy.camera.frameprocessors.Frame
|
|
10
|
+
import com.mrousavy.camera.frameprocessors.FrameProcessorPlugin
|
|
11
|
+
import com.mrousavy.camera.frameprocessors.VisionCameraProxy
|
|
12
|
+
import io.imagestitcher.rn.ar.YuvImageConverter
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* v0.9.0 Layer 1 — Android vc Frame Processor plugin that JPEG-
|
|
16
|
+
* encodes the supplied frame to a host-supplied path. Mirror of
|
|
17
|
+
* iOS' `SaveFrameAsJpegPlugin.mm`.
|
|
18
|
+
*
|
|
19
|
+
* Plugin name (must match iOS): `save_frame_as_jpeg`.
|
|
20
|
+
*
|
|
21
|
+
* ## Wrapping the existing encoder
|
|
22
|
+
*
|
|
23
|
+
* The lib already encodes JPEGs from NV21 bytes via
|
|
24
|
+
* `YuvImageConverter.encodeJpegFromNV21` — that's the path
|
|
25
|
+
* `RNSARCameraView.kt`'s keyframe-accept callback uses (line 589,
|
|
26
|
+
* `onAccept = { targetPath -> ... encodeJpegFromNV21(...) }`).
|
|
27
|
+
* This plugin reuses that exact encoder so:
|
|
28
|
+
* - JPEG output is byte-equivalent to the keyframe-accept output
|
|
29
|
+
* (same encoder, same quality knob)
|
|
30
|
+
* - No new encoder maintenance burden
|
|
31
|
+
*
|
|
32
|
+
* vision-camera's `Frame.image` is an `android.media.Image` in
|
|
33
|
+
* `YUV_420_888` format (the camera's native). We pass it through
|
|
34
|
+
* `YuvImageConverter.packNV21(image)` to extract the dense NV21
|
|
35
|
+
* byte array + dims, then `encodeJpegFromNV21(packed, file, q, rot)`
|
|
36
|
+
* does the JPEG write.
|
|
37
|
+
*
|
|
38
|
+
* ## Plugin contract (matches iOS surface exactly)
|
|
39
|
+
*
|
|
40
|
+
* Arguments dict:
|
|
41
|
+
* - `path` (string, REQUIRED): absolute output path.
|
|
42
|
+
* - `quality` (number, optional): 0-100 JPEG quality. Default 75.
|
|
43
|
+
* Clamped to [1, 100].
|
|
44
|
+
*
|
|
45
|
+
* Returns:
|
|
46
|
+
* - On success: `{ "ok" => true, "path" => ..., "width" => ...,
|
|
47
|
+
* "height" => ... }`
|
|
48
|
+
* - On failure: `{ "ok" => false, "error" => "..." }`
|
|
49
|
+
*
|
|
50
|
+
* Errors surfaced via the result map (not thrown) — host worklets
|
|
51
|
+
* can branch on `result.ok` without try/catch. Same convention
|
|
52
|
+
* as iOS.
|
|
53
|
+
*
|
|
54
|
+
* ## Lifetime / threading
|
|
55
|
+
*
|
|
56
|
+
* The supplied `Frame` (and its `Image`) is valid only for the
|
|
57
|
+
* duration of this callback — vision-camera closes the underlying
|
|
58
|
+
* `Image` on return. All Image access (NV21 pack + JPEG encode)
|
|
59
|
+
* happens synchronously inside `callback()`.
|
|
60
|
+
*
|
|
61
|
+
* ## Format restriction
|
|
62
|
+
*
|
|
63
|
+
* Only `YUV_420_888` input is supported (vc Android's standard).
|
|
64
|
+
* Anything else returns `{ ok: false, error: "unsupported format" }`.
|
|
65
|
+
* No format conversion fallback — that would mask bugs in the host
|
|
66
|
+
* camera config.
|
|
67
|
+
*
|
|
68
|
+
* ## Registration
|
|
69
|
+
*
|
|
70
|
+
* Registered in `RNImageStitcherPackage.kt`'s companion-object
|
|
71
|
+
* `ensureFrameProcessorPluginRegistered()`, alongside
|
|
72
|
+
* `cv_flow_gate_process_frame`. Same defensive
|
|
73
|
+
* NoClassDefFoundError handling — if vc isn't on the host's
|
|
74
|
+
* classpath, registration is silently skipped (and the plugin's
|
|
75
|
+
* `init { … }` calls below never happen).
|
|
76
|
+
*/
|
|
77
|
+
@DoNotStrip
|
|
78
|
+
@Keep
|
|
79
|
+
class SaveFrameAsJpegPlugin(
|
|
80
|
+
@Suppress("UNUSED_PARAMETER") proxy: VisionCameraProxy,
|
|
81
|
+
@Suppress("UNUSED_PARAMETER") options: Map<String, Any>?,
|
|
82
|
+
) : FrameProcessorPlugin() {
|
|
83
|
+
|
|
84
|
+
override fun callback(frame: Frame, params: Map<String, Any>?): Any {
|
|
85
|
+
val path = params?.get("path") as? String
|
|
86
|
+
?: return mapOf(
|
|
87
|
+
"ok" to false,
|
|
88
|
+
"error" to "missing required `path` argument",
|
|
89
|
+
)
|
|
90
|
+
val rawQuality = (params["quality"] as? Number)?.toInt() ?: 75
|
|
91
|
+
val quality = rawQuality.coerceIn(1, 100)
|
|
92
|
+
|
|
93
|
+
// Frame may throw if vc already released it.
|
|
94
|
+
val image: Image = try {
|
|
95
|
+
frame.image
|
|
96
|
+
} catch (e: Throwable) {
|
|
97
|
+
return mapOf(
|
|
98
|
+
"ok" to false,
|
|
99
|
+
"error" to "frame invalid: ${e.message}",
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (image.format != ImageFormat.YUV_420_888) {
|
|
104
|
+
return mapOf(
|
|
105
|
+
"ok" to false,
|
|
106
|
+
"error" to "unsupported format ${image.format} (need YUV_420_888)",
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Pack NV21 — same call site RNSARCameraView uses.
|
|
111
|
+
val packed = YuvImageConverter.packNV21(image)
|
|
112
|
+
?: return mapOf(
|
|
113
|
+
"ok" to false,
|
|
114
|
+
"error" to "YuvImageConverter.packNV21 returned null",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Reuse the lib's existing JPEG encoder. Rotation is 0 here
|
|
118
|
+
// (the host's frame is in camera-native orientation; if they
|
|
119
|
+
// want display orientation they can pass an `orientation`
|
|
120
|
+
// arg in a future version — for v0.9.0 the worklet emits
|
|
121
|
+
// raw-camera-oriented JPEGs, matching the keyframe-accept
|
|
122
|
+
// pipeline's behaviour).
|
|
123
|
+
//
|
|
124
|
+
// Signature: `encodeJpegFromNV21(packed, outputPath: String,
|
|
125
|
+
// jpegQuality: Int, displayRotation: Int): String?` — returns
|
|
126
|
+
// the written path on success, null on failure.
|
|
127
|
+
val encodedPath: String? = try {
|
|
128
|
+
YuvImageConverter.encodeJpegFromNV21(
|
|
129
|
+
packed,
|
|
130
|
+
path,
|
|
131
|
+
jpegQuality = quality,
|
|
132
|
+
displayRotation = 0,
|
|
133
|
+
)
|
|
134
|
+
} catch (e: Throwable) {
|
|
135
|
+
return mapOf(
|
|
136
|
+
"ok" to false,
|
|
137
|
+
"error" to "encodeJpegFromNV21 threw: ${e.message}",
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
if (encodedPath == null) {
|
|
141
|
+
return mapOf(
|
|
142
|
+
"ok" to false,
|
|
143
|
+
"error" to "encodeJpegFromNV21 returned null",
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return mapOf(
|
|
148
|
+
"ok" to true,
|
|
149
|
+
"path" to encodedPath,
|
|
150
|
+
"width" to packed.width,
|
|
151
|
+
"height" to packed.height,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
companion object {
|
|
156
|
+
private const val TAG = "SaveFrameAsJpegPlugin"
|
|
157
|
+
|
|
158
|
+
/// Plugin name; MUST match iOS + the JS-side
|
|
159
|
+
/// `initFrameProcessorPlugin('save_frame_as_jpeg')` call.
|
|
160
|
+
const val PLUGIN_NAME = "save_frame_as_jpeg"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
package io.imagestitcher.rn
|
|
3
|
+
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
7
|
+
import com.facebook.react.bridge.ReactMethod
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* v0.8.0 Phase 4b.ii — Android-side JSI installer for the host
|
|
11
|
+
* worklet proxy. Mirror of iOS' `StitcherJsiInstaller`.
|
|
12
|
+
*
|
|
13
|
+
* The module exposes one synchronous method, `install()`, which JS
|
|
14
|
+
* calls once at lib bootstrap (via the
|
|
15
|
+
* `ensureStitcherProxyInstalled` helper in
|
|
16
|
+
* `src/stitching/ensureStitcherProxyInstalled.ts`). We reach into
|
|
17
|
+
* the main JS runtime via `ReactApplicationContext.getJavaScriptContextHolder().get()`
|
|
18
|
+
* — the canonical bridgeless-compatible accessor in modern RN
|
|
19
|
+
* (worklets-core's `WorkletsModule` uses the same pattern, verified
|
|
20
|
+
* working on RN 0.84.1 + new arch + Hermes).
|
|
21
|
+
*
|
|
22
|
+
* The native `nativeInstall(jsiRuntimeRef)` JNI then casts the long
|
|
23
|
+
* back to a `jsi::Runtime*` and calls into the shared C++
|
|
24
|
+
* `retailens::installStitcherProxy(runtime)` (in
|
|
25
|
+
* `cpp/stitcher_proxy_jsi.{hpp,cpp}`). Identical destination on
|
|
26
|
+
* both platforms — `globalThis.__stitcherProxy` exposes the same
|
|
27
|
+
* `install` / `uninstall` / `count` host functions.
|
|
28
|
+
*
|
|
29
|
+
* ## Returning `Boolean` (not `Promise`) from a sync method
|
|
30
|
+
*
|
|
31
|
+
* `isBlockingSynchronousMethod = true` + `Boolean` return is the
|
|
32
|
+
* documented pattern for "I'm doing one-shot native setup that
|
|
33
|
+
* needs to complete before the next JS line runs." Same shape as
|
|
34
|
+
* `WorkletsModule.install()`.
|
|
35
|
+
*
|
|
36
|
+
* ## What we DON'T do here (Phase 4b.ii follow-up)
|
|
37
|
+
*
|
|
38
|
+
* Phase 4b.ii's MVP installs the proxy ONLY. Host worklets that
|
|
39
|
+
* register through `__stitcherProxy.install` land in the native
|
|
40
|
+
* `retailens::StitcherWorkletRegistry`. Per-frame fan-out from
|
|
41
|
+
* Android's `StitcherWorkletRuntime` is a separate piece of work
|
|
42
|
+
* (Phase 4b.ii follow-up) — needs the Kotlin↔JNI bridge that
|
|
43
|
+
* constructs a `StitcherFrameJsiHostObject` from an `ArImage` +
|
|
44
|
+
* pose and posts it through a worklet runtime. Until that lands,
|
|
45
|
+
* Android-registered worklets behave exactly like iOS-registered
|
|
46
|
+
* worklets BEFORE Phase 4b.i: they exist in the registry but
|
|
47
|
+
* aren't invoked.
|
|
48
|
+
*
|
|
49
|
+
* The proxy install itself is still useful as a foundation —
|
|
50
|
+
* verifies the JNI handshake works, exercises the bridgeless
|
|
51
|
+
* runtime accessor, and gives us a `count()` smoke test for the
|
|
52
|
+
* device verification step.
|
|
53
|
+
*/
|
|
54
|
+
class StitcherJsiInstallerModule(
|
|
55
|
+
private val reactContext: ReactApplicationContext,
|
|
56
|
+
) : ReactContextBaseJavaModule(reactContext) {
|
|
57
|
+
override fun getName(): String = NAME
|
|
58
|
+
|
|
59
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
60
|
+
fun install(): Boolean {
|
|
61
|
+
return try {
|
|
62
|
+
// `getJavaScriptContextHolder().get()` returns a raw
|
|
63
|
+
// `jsi::Runtime*` boxed as `Long`. Same accessor
|
|
64
|
+
// worklets-core's `WorkletsModule.install()` uses;
|
|
65
|
+
// documented to work in both legacy + bridgeless modes
|
|
66
|
+
// on RN 0.71+.
|
|
67
|
+
val holder = reactContext.javaScriptContextHolder
|
|
68
|
+
if (holder == null) {
|
|
69
|
+
Log.e(TAG, "getJavaScriptContextHolder() returned null; runtime unreachable")
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
val runtimeRef = holder.get()
|
|
73
|
+
if (runtimeRef == 0L) {
|
|
74
|
+
Log.e(TAG, "JavaScriptContextHolder.get() returned 0; runtime not initialized yet")
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
val ok = nativeInstall(runtimeRef)
|
|
78
|
+
if (!ok) {
|
|
79
|
+
Log.e(TAG, "nativeInstall(runtimeRef=$runtimeRef) returned false")
|
|
80
|
+
}
|
|
81
|
+
ok
|
|
82
|
+
} catch (t: Throwable) {
|
|
83
|
+
Log.e(TAG, "install() threw — falling back to JS-side registry", t)
|
|
84
|
+
false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private external fun nativeInstall(jsiRuntimeRef: Long): Boolean
|
|
89
|
+
|
|
90
|
+
companion object {
|
|
91
|
+
const val NAME = "StitcherJsiInstaller"
|
|
92
|
+
private const val TAG = "StitcherJsiInstaller"
|
|
93
|
+
|
|
94
|
+
init {
|
|
95
|
+
// The Phase 3a JNI shim (`libimage_stitcher.so`) absorbed
|
|
96
|
+
// the JSI-install JNI binding from Phase 4b.ii. Loading
|
|
97
|
+
// it once is enough — Android's loader deduplicates,
|
|
98
|
+
// so even if `IncrementalStitcher.kt`'s init block
|
|
99
|
+
// already loaded the lib, calling again is a cheap no-op.
|
|
100
|
+
System.loadLibrary("image_stitcher")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|