react-native-vision-camera-spoof-detector 1.0.28 → 1.1.7

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.

Potentially problematic release.


This version of react-native-vision-camera-spoof-detector might be problematic. Click here for more details.

Files changed (67) hide show
  1. package/.npmignore +16 -99
  2. package/README.md +5 -4
  3. package/android/CMakeLists.txt +29 -0
  4. package/android/build.gradle +26 -12
  5. package/android/fix-prefab.gradle +4 -0
  6. package/android/src/main/AndroidManifest.xml +1 -1
  7. package/android/src/main/cpp/CMakeLists.txt +18 -86
  8. package/android/src/main/cpp/FaceAntiSpoofJSI.cpp +80 -6
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/faceantispoof/FaceAntiSpoofModule.kt +108 -103
  11. package/android/src/main/java/com/faceantispoof/FaceAntiSpoofPackage.kt +12 -27
  12. package/android/src/main/java/com/faceantispoof/FaceAntiSpoofingAdvanced.kt +58 -1
  13. package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorFactory.kt +15 -0
  14. package/android/src/main/java/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorOutput.kt +107 -0
  15. package/index.d.ts +14 -1
  16. package/index.js +26 -57
  17. package/nitro.json +20 -0
  18. package/nitrogen/generated/.gitattributes +1 -0
  19. package/nitrogen/generated/android/VisionCameraSpoofDetector+autolinking.cmake +81 -0
  20. package/nitrogen/generated/android/VisionCameraSpoofDetector+autolinking.gradle +27 -0
  21. package/nitrogen/generated/android/VisionCameraSpoofDetectorOnLoad.cpp +58 -0
  22. package/nitrogen/generated/android/VisionCameraSpoofDetectorOnLoad.hpp +34 -0
  23. package/nitrogen/generated/android/c++/JAntiSpoofingResult.hpp +77 -0
  24. package/nitrogen/generated/android/c++/JFunc_void_AntiSpoofingResult.hpp +78 -0
  25. package/nitrogen/generated/android/c++/JFunc_void_std__exception_ptr.hpp +76 -0
  26. package/nitrogen/generated/android/c++/JHybridSpoofDetectorFactorySpec.cpp +75 -0
  27. package/nitrogen/generated/android/c++/JHybridSpoofDetectorFactorySpec.hpp +63 -0
  28. package/nitrogen/generated/android/c++/JSize.hpp +61 -0
  29. package/nitrogen/generated/android/c++/JSpoofDetectorOutputOptions.hpp +91 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/AntiSpoofingResult.kt +76 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/Func_void_AntiSpoofingResult.kt +78 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/Func_void_std__exception_ptr.kt +78 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/HybridSpoofDetectorFactorySpec.kt +57 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/Size.kt +56 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/SpoofDetectorOutputOptions.kt +65 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/spoofdetector/VisionCameraSpoofDetectorOnLoad.kt +35 -0
  37. package/nitrogen/generated/shared/c++/AntiSpoofingResult.hpp +103 -0
  38. package/nitrogen/generated/shared/c++/HybridSpoofDetectorFactorySpec.cpp +21 -0
  39. package/nitrogen/generated/shared/c++/HybridSpoofDetectorFactorySpec.hpp +67 -0
  40. package/nitrogen/generated/shared/c++/Size.hpp +87 -0
  41. package/nitrogen/generated/shared/c++/SpoofDetectorOutputOptions.hpp +98 -0
  42. package/package.json +12 -13
  43. package/react-native-vision-camera-spoof-detector.podspec +4 -4
  44. package/src/specs/SpoofDetectorFactory.nitro.ts +23 -0
  45. package/.gitattributes +0 -2
  46. package/CONTRIBUTING.md +0 -280
  47. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  48. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  49. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  50. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  51. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  52. package/android/.gradle/8.9/gc.properties +0 -0
  53. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  54. package/android/.gradle/9.2.0/checksums/md5-checksums.bin +0 -0
  55. package/android/.gradle/9.2.0/checksums/sha1-checksums.bin +0 -0
  56. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  57. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  58. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  59. package/android/.gradle/9.2.0/gc.properties +0 -0
  60. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  61. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  62. package/android/.gradle/vcs-1/gc.properties +0 -0
  63. package/android/build/reports/problems/problems-report.html +0 -659
  64. package/android/src/main/assets/spoof_model_scale_2_7.tflite +0 -0
  65. package/android/src/main/assets/spoof_model_scale_4_0.tflite +0 -0
  66. package/android/src/main/java/com/faceantispoof/FaceAntiSpoofFrameProcessor.kt +0 -198
  67. /package/{android/src/main → lib}/assets/FaceAntiSpoofing.tflite +0 -0
@@ -4,6 +4,8 @@ import com.facebook.react.bridge.ReactApplicationContext
4
4
  import com.facebook.react.bridge.ReactContextBaseJavaModule
5
5
  import com.facebook.react.bridge.ReactMethod
6
6
  import com.facebook.react.bridge.Promise
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.ReadableArray
7
9
  import com.facebook.react.module.annotations.ReactModule
8
10
 
9
11
  @ReactModule(name = FaceAntiSpoofModule.NAME)
@@ -11,17 +13,27 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
11
13
 
12
14
  companion object {
13
15
  const val NAME = "FaceAntiSpoof"
16
+ private var activeModule: FaceAntiSpoofModule? = null
17
+
18
+ @JvmStatic
19
+ fun processFrameFromJSI(
20
+ planes: Array<ByteArray>,
21
+ width: Int,
22
+ height: Int,
23
+ rowStrides: IntArray,
24
+ pixelStrides: IntArray
25
+ ): DoubleArray {
26
+ val module = activeModule
27
+ ?: return doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0)
28
+ return module.processNativeFrame(planes, width, height, rowStrides, pixelStrides)
29
+ }
14
30
  }
15
31
 
16
32
  private var faceAntiSpoofing: FaceAntiSpoofingAdvanced? = null
17
33
  private var isModuleInitialized = false
18
34
 
19
35
  init {
20
- try {
21
- System.loadLibrary("faceantispoof")
22
- } catch (e: Exception) {
23
- android.util.Log.e("FaceAntiSpoof", "Error loading native library", e)
24
- }
36
+ activeModule = this
25
37
 
26
38
  try {
27
39
  // Initialize face anti-spoofing with app assets
@@ -33,57 +45,6 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
33
45
  isModuleInitialized = false
34
46
  }
35
47
 
36
- try {
37
- tryRegisterJSI()
38
- } catch (e: Exception) {
39
- android.util.Log.e("FaceAntiSpoof", "Error starting JSI registration", e)
40
- }
41
- }
42
-
43
- private fun tryRegisterJSI() {
44
- try {
45
- Thread {
46
- try {
47
- Thread.sleep(500)
48
- val holder = reactApplicationContext.javaScriptContextHolder ?: return@Thread
49
-
50
- // Try nativePointer
51
- try {
52
- val field = holder.javaClass.getDeclaredField("nativePointer")
53
- field.isAccessible = true
54
- val jsContextPtr = field.getLong(holder)
55
- if (jsContextPtr != 0L) {
56
- try {
57
- registerJSIFunction(jsContextPtr)
58
- } catch (e: Exception) {
59
- android.util.Log.d("FaceAntiSpoof", "JSI registration not available: ${e.message}")
60
- }
61
- return@Thread
62
- }
63
- } catch (_: Exception) { }
64
-
65
- // Try mContext
66
- try {
67
- val field = holder.javaClass.getDeclaredField("mContext")
68
- field.isAccessible = true
69
- val jsContextPtr = field.getLong(holder)
70
- if (jsContextPtr != 0L) {
71
- try {
72
- registerJSIFunction(jsContextPtr)
73
- } catch (e: Exception) {
74
- android.util.Log.d("FaceAntiSpoof", "JSI registration not available: ${e.message}")
75
- }
76
- return@Thread
77
- }
78
- } catch (_: Exception) { }
79
-
80
- } catch (e: Exception) {
81
- android.util.Log.d("FaceAntiSpoof", "Error in JSI registration thread: ${e.message}")
82
- }
83
- }.start()
84
- } catch (e: Exception) {
85
- android.util.Log.d("FaceAntiSpoof", "Failed to start thread for JSI registration: ${e.message}")
86
- }
87
48
  }
88
49
 
89
50
  override fun getName(): String = NAME
@@ -126,6 +87,94 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
126
87
  }
127
88
  }
128
89
 
90
+ @ReactMethod
91
+ fun processFrame(
92
+ frameData: ReadableArray,
93
+ width: Int,
94
+ height: Int,
95
+ rowStrides: ReadableArray,
96
+ pixelStrides: ReadableArray,
97
+ promise: Promise
98
+ ) {
99
+ try {
100
+ if (frameData.size() < 3 || rowStrides.size() < 3 || pixelStrides.size() < 3) {
101
+ promise.reject("INVALID_FRAME", "Expected Y, U and V camera planes")
102
+ return
103
+ }
104
+
105
+ if (!isModuleInitialized || faceAntiSpoofing == null) {
106
+ promise.reject("NOT_INITIALIZED", "Face anti-spoofing is not initialized")
107
+ return
108
+ }
109
+
110
+ val planes = Array(3) { planeIndex ->
111
+ val plane = frameData.getArray(planeIndex)
112
+ ?: throw IllegalArgumentException("Camera plane $planeIndex is null")
113
+ ByteArray(plane.size()) { index -> plane.getInt(index).toByte() }
114
+ }
115
+ val yuvRowStrides = IntArray(3) { rowStrides.getDouble(it).toInt() }
116
+ val yuvPixelStrides = IntArray(3) { pixelStrides.getDouble(it).toInt().coerceAtLeast(1) }
117
+ val result = faceAntiSpoofing!!.advancedAntiSpoofing(
118
+ planes,
119
+ width,
120
+ height,
121
+ yuvRowStrides,
122
+ yuvPixelStrides
123
+ )
124
+ val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
125
+ result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
126
+ promise.resolve(
127
+ Arguments.makeNativeMap(
128
+ mapOf(
129
+ "isLive" to isLive,
130
+ "label" to if (isLive) "Live Face" else "Spoof Face",
131
+ "neuralNetworkScore" to result.neuralNetworkScore.toDouble(),
132
+ "laplacianScore" to result.laplacianScore,
133
+ "combinedScore" to result.combinedScore.toDouble(),
134
+ "confidence" to result.confidence.toDouble()
135
+ )
136
+ )
137
+ )
138
+ } catch (e: Exception) {
139
+ promise.reject("PROCESS_ERROR", "Failed to process camera frame", e)
140
+ }
141
+ }
142
+
143
+ @Synchronized
144
+ private fun processNativeFrame(
145
+ planes: Array<ByteArray>,
146
+ width: Int,
147
+ height: Int,
148
+ rowStrides: IntArray,
149
+ pixelStrides: IntArray
150
+ ): DoubleArray {
151
+ if (!isModuleInitialized || faceAntiSpoofing == null || planes.size < 3) {
152
+ return doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0)
153
+ }
154
+
155
+ return try {
156
+ val result = faceAntiSpoofing!!.advancedAntiSpoofing(
157
+ planes,
158
+ width,
159
+ height,
160
+ rowStrides,
161
+ pixelStrides
162
+ )
163
+ val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
164
+ result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
165
+ doubleArrayOf(
166
+ if (isLive) 1.0 else 0.0,
167
+ result.neuralNetworkScore.toDouble(),
168
+ result.laplacianScore.toDouble(),
169
+ result.combinedScore.toDouble(),
170
+ result.confidence.toDouble()
171
+ )
172
+ } catch (error: Exception) {
173
+ android.util.Log.e("FaceAntiSpoof", "Native frame processing failed", error)
174
+ doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0)
175
+ }
176
+ }
177
+
129
178
  @ReactMethod
130
179
  fun isAvailable(promise: Promise) {
131
180
  try {
@@ -163,51 +212,9 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
163
212
 
164
213
  @ReactMethod
165
214
  fun install(promise: Promise) {
166
- try {
167
- val holder = reactApplicationContext.javaScriptContextHolder
168
-
169
- if (holder != null) {
170
- // Try nativePointer
171
- try {
172
- val f = holder.javaClass.getDeclaredField("nativePointer")
173
- f.isAccessible = true
174
- val ptr = f.getLong(holder)
175
- if (ptr != 0L) {
176
- try {
177
- registerJSIFunction(ptr)
178
- promise.resolve(true)
179
- return
180
- } catch (e: Exception) {
181
- android.util.Log.d("FaceAntiSpoof", "JSI not available: ${e.message}")
182
- }
183
- }
184
- } catch (_: Exception) {}
185
-
186
- // Try mContext
187
- try {
188
- val f = holder.javaClass.getDeclaredField("mContext")
189
- f.isAccessible = true
190
- val ptr = f.getLong(holder)
191
- if (ptr != 0L) {
192
- try {
193
- registerJSIFunction(ptr)
194
- promise.resolve(true)
195
- return
196
- } catch (e: Exception) {
197
- android.util.Log.d("FaceAntiSpoof", "JSI not available: ${e.message}")
198
- }
199
- }
200
- } catch (_: Exception) {}
201
-
202
- promise.resolve(false)
203
- } else {
204
- promise.resolve(false)
205
- }
206
-
207
- } catch (e: Exception) {
208
- android.util.Log.e("FaceAntiSpoof", "install() error", e)
209
- promise.reject("INSTALL_ERROR", "Failed to install JSI function", e)
210
- }
215
+ // VisionCamera 5 uses the Nitro CameraOutput. The old reflective JSI
216
+ // installer targets a native library that is no longer part of this module.
217
+ promise.resolve(false)
211
218
  }
212
219
 
213
220
  @ReactMethod
@@ -237,6 +244,4 @@ class FaceAntiSpoofModule(reactContext: ReactApplicationContext) : ReactContextB
237
244
  super.onCatalystInstanceDestroy()
238
245
  }
239
246
 
240
- // Native JSI registration - optional, may not be available
241
- private external fun registerJSIFunction(jsContext: Long)
242
247
  }
@@ -1,37 +1,22 @@
1
- package com.faceantispoof
1
+ package com.margelo.nitro.camera.spoofdetector
2
2
 
3
- import com.facebook.react.ReactPackage
3
+ import com.facebook.react.BaseReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
- import com.facebook.react.uimanager.ViewManager
7
- import com.mrousavy.camera.frameprocessors.FrameProcessorPluginRegistry
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.faceantispoof.FaceAntiSpoofModule
8
8
 
9
- class FaceAntiSpoofPackage : ReactPackage {
9
+ class FaceAntiSpoofPackage : BaseReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
11
+ if (name == FaceAntiSpoofModule.NAME) FaceAntiSpoofModule(reactContext) else null
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
14
+ HashMap()
15
+ }
10
16
 
11
17
  companion object {
12
18
  init {
13
- try {
14
- FrameProcessorPluginRegistry.addFrameProcessorPlugin("faceAntiSpoof") { proxy, options ->
15
- val plugin = FaceAntiSpoofFrameProcessor()
16
- try {
17
- plugin.initialize(proxy.context.assets)
18
- } catch (e: Exception) {
19
- android.util.Log.e("FaceAntiSpoof", "Failed to initialize plugin with assets", e)
20
- }
21
- plugin
22
- }
23
- } catch (e: Exception) {
24
- android.util.Log.e("FaceAntiSpoof", "Plugin already registered or failed: ${e.message}", e)
25
- }
19
+ VisionCameraSpoofDetectorOnLoad.initializeNative()
26
20
  }
27
21
  }
28
-
29
- override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
30
- // Only return the module — DO NOT register plugin here.
31
- return listOf(FaceAntiSpoofModule(reactContext))
32
- }
33
-
34
- override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
35
- return emptyList()
36
- }
37
22
  }
@@ -24,7 +24,7 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
24
24
  const val BASE_THRESHOLD = 0.2f
25
25
  const val ROUTE_INDEX = 6
26
26
  const val LAPLACE_THRESHOLD = 50
27
- const val LAPLACIAN_THRESHOLD = 4000 // Increased from 1000 - real faces have ~3500+ laplacian score
27
+ const val LAPLACIAN_THRESHOLD = 1000
28
28
  const val MODEL_FILE = "FaceAntiSpoofing.tflite"
29
29
  private const val TAG = "FaceAntiSpoofing"
30
30
 
@@ -165,6 +165,25 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
165
165
  }
166
166
  }
167
167
 
168
+ fun advancedAntiSpoofing(
169
+ planes: Array<ByteArray>,
170
+ width: Int,
171
+ height: Int,
172
+ rowStrides: IntArray,
173
+ pixelStrides: IntArray
174
+ ): AntiSpoofingResult {
175
+ if (planes.size < 3) {
176
+ throw IllegalArgumentException("Expected Y, U and V planes")
177
+ }
178
+
179
+ val bitmap = yuv420ToBitmap(planes, width, height, rowStrides, pixelStrides)
180
+ return try {
181
+ runBlocking { advancedAntiSpoofing(bitmap).getOrThrow() }
182
+ } finally {
183
+ bitmap.recycle()
184
+ }
185
+ }
186
+
168
187
  // FROM WORKING VERSION: neuralNetworkAntiSpoofing with Bitmap
169
188
  private fun neuralNetworkAntiSpoofing(bitmap: Bitmap): Result<Float> {
170
189
  return try {
@@ -365,6 +384,44 @@ class FaceAntiSpoofingAdvanced(private val assetManager: AssetManager) {
365
384
  return bitmap
366
385
  }
367
386
 
387
+ private fun yuv420ToBitmap(
388
+ planes: Array<ByteArray>,
389
+ width: Int,
390
+ height: Int,
391
+ rowStrides: IntArray,
392
+ pixelStrides: IntArray
393
+ ): Bitmap {
394
+ val pixels = IntArray(width * height)
395
+ val yPlane = planes[0]
396
+ val uPlane = planes[1]
397
+ val vPlane = planes[2]
398
+
399
+ fun sample(plane: ByteArray, row: Int, column: Int, rowStride: Int, pixelStride: Int): Int {
400
+ val index = row * rowStride + column * pixelStride
401
+ return if (index in plane.indices) plane[index].toInt() and 0xFF else 128
402
+ }
403
+
404
+ for (y in 0 until height) {
405
+ for (x in 0 until width) {
406
+ val yValue = sample(yPlane, y, x, rowStrides[0], pixelStrides[0])
407
+ val chromaRow = y / 2
408
+ val chromaColumn = x / 2
409
+ val uValue = sample(uPlane, chromaRow, chromaColumn, rowStrides[1], pixelStrides[1]) - 128
410
+ val vValue = sample(vPlane, chromaRow, chromaColumn, rowStrides[2], pixelStrides[2]) - 128
411
+
412
+ val red = (yValue + 1.402f * vValue).coerceIn(0f, 255f).toInt()
413
+ val green = (yValue - 0.344f * uValue - 0.714f * vValue).coerceIn(0f, 255f).toInt()
414
+ val blue = (yValue + 1.772f * uValue).coerceIn(0f, 255f).toInt()
415
+ pixels[y * width + x] = (0xFF shl 24) or
416
+ (red shl 16) or
417
+ (green shl 8) or
418
+ blue
419
+ }
420
+ }
421
+
422
+ return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
423
+ }
424
+
368
425
  @Throws(IOException::class)
369
426
  private fun loadModelFile(assetManager: AssetManager, modelName: String): MappedByteBuffer {
370
427
  return try {
@@ -0,0 +1,15 @@
1
+ package com.margelo.nitro.camera.spoofdetector
2
+
3
+ import androidx.annotation.Keep
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+ import com.margelo.nitro.camera.HybridCameraOutputSpec
6
+
7
+ @DoNotStrip
8
+ @Keep
9
+ class HybridSpoofDetectorFactory : HybridSpoofDetectorFactorySpec() {
10
+ @DoNotStrip
11
+ @Keep
12
+ override fun createSpoofDetectorOutput(options: SpoofDetectorOutputOptions): HybridCameraOutputSpec {
13
+ return HybridSpoofDetectorOutput(options)
14
+ }
15
+ }
@@ -0,0 +1,107 @@
1
+ package com.margelo.nitro.camera.spoofdetector
2
+
3
+ import androidx.camera.core.ImageAnalysis
4
+ import androidx.camera.core.ImageProxy
5
+ import androidx.camera.core.resolutionselector.ResolutionSelector
6
+ import androidx.camera.core.resolutionselector.ResolutionStrategy
7
+ import com.margelo.nitro.NitroModules
8
+ import com.margelo.nitro.camera.CameraOrientation
9
+ import com.margelo.nitro.camera.HybridCameraOutputSpec
10
+ import com.margelo.nitro.camera.MediaType
11
+ import com.margelo.nitro.camera.MirrorMode
12
+ import com.margelo.nitro.camera.extensions.setAllowDroppingLateFrames
13
+ import com.margelo.nitro.camera.extensions.surfaceRotation
14
+ import com.margelo.nitro.camera.public.NativeCameraOutput
15
+ import com.faceantispoof.FaceAntiSpoofingAdvanced
16
+ import java.util.concurrent.Executors
17
+ import java.util.concurrent.atomic.AtomicBoolean
18
+
19
+ class HybridSpoofDetectorOutput(
20
+ private val options: SpoofDetectorOutputOptions,
21
+ ) : HybridCameraOutputSpec(), ImageAnalysis.Analyzer, NativeCameraOutput {
22
+ override val mediaType: MediaType = MediaType.VIDEO
23
+ override val mirrorMode: MirrorMode = MirrorMode.AUTO
24
+ override var outputOrientation: CameraOrientation = CameraOrientation.UP
25
+ set(value) {
26
+ field = value
27
+ imageAnalysis?.targetRotation = value.surfaceRotation
28
+ }
29
+ override val currentResolution: com.margelo.nitro.camera.Size?
30
+ get() = null
31
+
32
+ private val executor = Executors.newSingleThreadExecutor()
33
+ private val busy = AtomicBoolean(false)
34
+ private val detector = FaceAntiSpoofingAdvanced(
35
+ NitroModules.applicationContext?.assets ?: error("FaceAntiSpoof: no application context")
36
+ )
37
+ private var imageAnalysis: ImageAnalysis? = null
38
+
39
+ override fun createUseCase(
40
+ mirrorMode: MirrorMode,
41
+ config: NativeCameraOutput.Config,
42
+ ): NativeCameraOutput.PreparedUseCase {
43
+ val target = options.targetResolution
44
+ val selector = target?.let {
45
+ ResolutionSelector.Builder()
46
+ .setResolutionStrategy(
47
+ ResolutionStrategy(
48
+ android.util.Size(it.width.toInt(), it.height.toInt()),
49
+ ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
50
+ )
51
+ )
52
+ .build()
53
+ }
54
+ val analysis = ImageAnalysis.Builder()
55
+ .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
56
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
57
+ .setAllowDroppingLateFrames(true)
58
+ .apply { if (selector != null) setResolutionSelector(selector) }
59
+ .build()
60
+ return NativeCameraOutput.PreparedUseCase(analysis) {
61
+ imageAnalysis = analysis
62
+ analysis.setAnalyzer(executor, this)
63
+ }
64
+ }
65
+
66
+ override fun analyze(image: ImageProxy) {
67
+ if (!busy.compareAndSet(false, true)) {
68
+ image.close()
69
+ return
70
+ }
71
+ try {
72
+ val planes = image.planes.take(3).map { plane ->
73
+ val buffer = plane.buffer.duplicate()
74
+ ByteArray(buffer.remaining()).also(buffer::get)
75
+ }.toTypedArray()
76
+ val rowStrides = image.planes.take(3).map { it.rowStride }.toIntArray()
77
+ val pixelStrides = image.planes.take(3).map { it.pixelStride.coerceAtLeast(1) }.toIntArray()
78
+ val result = detector.advancedAntiSpoofing(
79
+ planes, image.width, image.height, rowStrides, pixelStrides
80
+ )
81
+ val isLive = result.neuralNetworkScore < FaceAntiSpoofingAdvanced.BASE_THRESHOLD &&
82
+ result.laplacianScore > FaceAntiSpoofingAdvanced.LAPLACIAN_THRESHOLD
83
+ options.onResult.invoke(
84
+ AntiSpoofingResult(
85
+ isLive,
86
+ if (isLive) "Live Face" else "Spoof Face",
87
+ result.neuralNetworkScore.toDouble(),
88
+ result.laplacianScore.toDouble(),
89
+ result.combinedScore.toDouble(),
90
+ result.confidence.toDouble(),
91
+ )
92
+ )
93
+ } catch (error: Throwable) {
94
+ options.onError?.invoke(error)
95
+ } finally {
96
+ image.close()
97
+ busy.set(false)
98
+ }
99
+ }
100
+
101
+ override fun dispose() {
102
+ imageAnalysis?.clearAnalyzer()
103
+ detector.close()
104
+ executor.shutdown()
105
+ super.dispose()
106
+ }
107
+ }
package/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * TypeScript type definitions
5
5
  */
6
6
 
7
- import type { Frame, FrameProcessor } from 'react-native-vision-camera';
7
+ import type { CameraFrameOutput, Frame, FrameOutputOptions, FrameProcessor } from 'react-native-vision-camera';
8
8
  import type { SharedValue } from 'react-native-reanimated';
9
9
 
10
10
  // ============================================================================
@@ -281,6 +281,19 @@ export interface UseFaceDetectionFrameProcessorReturn {
281
281
  antiSpoofState: AntiSpoofState;
282
282
  }
283
283
 
284
+ /**
285
+ * VisionCamera 5 frame-output options for spoof detection.
286
+ * The callback runs on the React Native runtime after native inference.
287
+ */
288
+ export interface UseFaceAntiSpoofFrameOutputOptions extends Partial<FrameOutputOptions> {
289
+ onResult?: (result: FaceAntiSpoofingResult) => void;
290
+ }
291
+
292
+ /** Create a VisionCamera 5 frame output backed by the native detector. */
293
+ export function useFaceAntiSpoofFrameOutput(
294
+ options?: UseFaceAntiSpoofFrameOutputOptions,
295
+ ): CameraFrameOutput;
296
+
284
297
  // ============================================================================
285
298
  // Module Functions
286
299
  // ============================================================================
package/index.js CHANGED
@@ -1,60 +1,31 @@
1
- import { NativeModules } from 'react-native';
2
- import { VisionCameraProxy } from 'react-native-vision-camera';
1
+ import { useMemo, useRef } from 'react';
2
+ import { NativeModules } from 'react-native';
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+ import { scheduleOnRN } from 'react-native-worklets';
3
5
 
4
6
  const { FaceAntiSpoof } = NativeModules;
5
7
 
6
- // Initialize the plugin before any worklet captures this module.
7
- let _faceAntiSpoofPlugin = null;
8
-
9
- try {
10
- _faceAntiSpoofPlugin = VisionCameraProxy.initFrameProcessorPlugin('faceAntiSpoof', {});
11
- } catch (e) {
12
- console.warn('[FaceAntiSpoof] Failed to init frame processor plugin:', e);
13
- }
14
-
15
- function ensurePluginInitialized() {
16
- if (!_faceAntiSpoofPlugin) {
17
- try {
18
- _faceAntiSpoofPlugin = VisionCameraProxy.initFrameProcessorPlugin('faceAntiSpoof', {});
19
- } catch (e) {
20
- console.warn('[FaceAntiSpoof] Failed to init frame processor plugin:', e);
21
- _faceAntiSpoofPlugin = null;
22
- }
23
- }
24
- return !!_faceAntiSpoofPlugin;
25
- }
26
-
27
- // Worklet called by VisionCamera
28
- export const faceAntiSpoofFrameProcessor = function (frame) {
29
- 'worklet';
30
- try {
31
- if (!_faceAntiSpoofPlugin) {
32
- console.warn('[FaceAntiSpoof] Plugin not initialized');
33
- return null;
34
- }
35
-
36
- if (typeof _faceAntiSpoofPlugin.call !== 'function') {
37
- console.warn('[FaceAntiSpoof] Plugin call method not available');
38
- return null;
39
- }
40
-
41
- if (!frame) {
42
- console.warn('[FaceAntiSpoof] Frame is null or undefined');
43
- return null;
44
- }
45
-
46
- const result = _faceAntiSpoofPlugin.call(frame);
47
-
48
- // Validate result structure
49
- if (result && typeof result === 'object') {
50
- return result;
51
- }
52
-
53
- return null;
54
- } catch (err) {
55
- console.error('[FaceAntiSpoof] Error in frame processor:', err);
56
- return null;
57
- }
8
+ export const useFaceAntiSpoofFrameOutput = ({ onResult, ...options } = {}) => {
9
+ const resultRef = useRef(onResult);
10
+ resultRef.current = onResult;
11
+ const errorRef = useRef(options.onError);
12
+ errorRef.current = options.onError;
13
+ const factory = useMemo(
14
+ () => NitroModules.createHybridObject('SpoofDetectorFactory'),
15
+ [],
16
+ );
17
+ return useMemo(
18
+ () => factory.createSpoofDetectorOutput({
19
+ targetResolution: options.targetResolution ?? { width: 256, height: 256 },
20
+ onResult(result) {
21
+ resultRef.current?.(result);
22
+ },
23
+ onError(error) {
24
+ errorRef.current?.(error);
25
+ },
26
+ }),
27
+ [factory, options.targetResolution],
28
+ );
58
29
  };
59
30
 
60
31
  export default FaceAntiSpoof;
@@ -86,9 +57,7 @@ export const initializeFaceAntiSpoof = async () => {
86
57
  }
87
58
 
88
59
  const isSuccess = result && status.pluginAvailable;
89
- // If native initialization succeeded, register the frame processor plugin for worklets
90
- const pluginInitialized = isSuccess ? ensurePluginInitialized() : false;
91
- const initialized = isSuccess && pluginInitialized;
60
+ const initialized = isSuccess;
92
61
  console.log('[FaceAntiSpoof] Initialization result:', initialized);
93
62
  return initialized;
94
63
  } catch (error) {
package/nitro.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://nitro.margelo.com/nitro.schema.json",
3
+ "cxxNamespace": ["camera", "spoofdetector"],
4
+ "ios": {
5
+ "iosModuleName": "VisionCameraSpoofDetector"
6
+ },
7
+ "android": {
8
+ "androidNamespace": ["camera", "spoofdetector"],
9
+ "androidCxxLibName": "VisionCameraSpoofDetector"
10
+ },
11
+ "autolinking": {
12
+ "SpoofDetectorFactory": {
13
+ "android": {
14
+ "language": "kotlin",
15
+ "implementationClassName": "HybridSpoofDetectorFactory"
16
+ }
17
+ }
18
+ },
19
+ "ignorePaths": ["**/node_modules"]
20
+ }
@@ -0,0 +1 @@
1
+ ** linguist-generated=true