@arfuhad/react-native-smart-camera 0.1.1 → 0.1.5
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 +130 -0
- package/README.md +259 -206
- package/android/build.gradle +15 -33
- package/android/src/main/java/com/smartcamera/FaceDetectorFrameProcessorPlugin.kt +324 -0
- package/android/src/main/java/com/smartcamera/SmartCameraPackage.kt +28 -0
- package/build/detection/blinkProcessor.js +2 -1
- package/build/detection/blinkProcessor.js.map +1 -1
- package/build/detection/faceDetector.d.ts +4 -4
- package/build/detection/faceDetector.d.ts.map +1 -1
- package/build/detection/faceDetector.js +31 -11
- package/build/detection/faceDetector.js.map +1 -1
- package/build/detection/index.d.ts +1 -1
- package/build/detection/index.d.ts.map +1 -1
- package/build/detection/index.js +1 -1
- package/build/detection/index.js.map +1 -1
- package/build/detection/staticImageDetector.d.ts +11 -11
- package/build/detection/staticImageDetector.d.ts.map +1 -1
- package/build/detection/staticImageDetector.js +19 -23
- package/build/detection/staticImageDetector.js.map +1 -1
- package/build/hooks/index.d.ts +6 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +8 -0
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/useBlinkDetection.d.ts +27 -16
- package/build/hooks/useBlinkDetection.d.ts.map +1 -1
- package/build/hooks/useBlinkDetection.js +63 -37
- package/build/hooks/useBlinkDetection.js.map +1 -1
- package/build/hooks/useFaceDetection.js +3 -2
- package/build/hooks/useFaceDetection.js.map +1 -1
- package/build/hooks/useFaceDetector.d.ts +123 -0
- package/build/hooks/useFaceDetector.d.ts.map +1 -0
- package/build/hooks/useFaceDetector.js +133 -0
- package/build/hooks/useFaceDetector.js.map +1 -0
- package/build/hooks/useSmartCamera.js.map +1 -1
- package/build/hooks/useSmartCameraWebRTC.d.ts +3 -5
- package/build/hooks/useSmartCameraWebRTC.d.ts.map +1 -1
- package/build/hooks/useSmartCameraWebRTC.js +19 -87
- package/build/hooks/useSmartCameraWebRTC.js.map +1 -1
- package/build/hooks/useWebRTC.d.ts +88 -0
- package/build/hooks/useWebRTC.d.ts.map +1 -0
- package/build/hooks/useWebRTC.js +394 -0
- package/build/hooks/useWebRTC.js.map +1 -0
- package/build/hooks/useWebRTCWithDetection.d.ts +89 -0
- package/build/hooks/useWebRTCWithDetection.d.ts.map +1 -0
- package/build/hooks/useWebRTCWithDetection.js +131 -0
- package/build/hooks/useWebRTCWithDetection.js.map +1 -0
- package/build/index.d.ts +24 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +38 -13
- package/build/index.js.map +1 -1
- package/build/types.d.ts +28 -12
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/utils/index.js.map +1 -1
- package/build/webrtc/WebRTCBridge.d.ts +3 -0
- package/build/webrtc/WebRTCBridge.d.ts.map +1 -1
- package/build/webrtc/WebRTCBridge.js +12 -15
- package/build/webrtc/WebRTCBridge.js.map +1 -1
- package/build/webrtc/WebRTCManager.d.ts +148 -0
- package/build/webrtc/WebRTCManager.d.ts.map +1 -0
- package/build/webrtc/WebRTCManager.js +383 -0
- package/build/webrtc/WebRTCManager.js.map +1 -0
- package/build/webrtc/index.d.ts +3 -1
- package/build/webrtc/index.d.ts.map +1 -1
- package/build/webrtc/index.js +5 -0
- package/build/webrtc/index.js.map +1 -1
- package/build/webrtc/types.d.ts +212 -4
- package/build/webrtc/types.d.ts.map +1 -1
- package/build/webrtc/types.js +34 -1
- package/build/webrtc/types.js.map +1 -1
- package/ios/FaceDetectorFrameProcessorPlugin.m +11 -0
- package/ios/FaceDetectorFrameProcessorPlugin.swift +304 -0
- package/package.json +13 -12
- package/react-native-smart-camera.podspec +32 -0
- package/src/detection/blinkProcessor.ts +127 -0
- package/src/detection/faceDetector.ts +78 -0
- package/src/detection/index.ts +3 -0
- package/src/detection/staticImageDetector.ts +53 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/useBlinkDetection.ts +127 -0
- package/src/hooks/useFaceDetection.ts +105 -0
- package/src/hooks/useFaceDetector.ts +191 -0
- package/src/hooks/useSmartCamera.ts +83 -0
- package/src/hooks/useSmartCameraWebRTC.ts +120 -0
- package/src/hooks/useWebRTC.ts +453 -0
- package/src/hooks/useWebRTCWithDetection.ts +181 -0
- package/src/index.ts +170 -0
- package/src/types.ts +636 -0
- package/src/utils/index.ts +355 -0
- package/src/webrtc/WebRTCBridge.ts +127 -0
- package/src/webrtc/WebRTCManager.ts +453 -0
- package/src/webrtc/index.ts +50 -0
- package/src/webrtc/types.ts +361 -0
- package/android/src/main/java/expo/modules/smartcamera/ImageLoader.kt +0 -106
- package/android/src/main/java/expo/modules/smartcamera/MLKitFaceDetector.kt +0 -273
- package/android/src/main/java/expo/modules/smartcamera/SmartCameraModule.kt +0 -205
- package/android/src/main/java/expo/modules/smartcamera/SmartCameraView.kt +0 -153
- package/android/src/main/java/expo/modules/smartcamera/WebRTCFrameBridge.kt +0 -184
- package/build/SmartCamera.d.ts +0 -17
- package/build/SmartCamera.d.ts.map +0 -1
- package/build/SmartCamera.js +0 -270
- package/build/SmartCamera.js.map +0 -1
- package/build/SmartCameraModule.d.ts +0 -112
- package/build/SmartCameraModule.d.ts.map +0 -1
- package/build/SmartCameraModule.js +0 -121
- package/build/SmartCameraModule.js.map +0 -1
- package/build/SmartCameraView.d.ts +0 -8
- package/build/SmartCameraView.d.ts.map +0 -1
- package/build/SmartCameraView.js +0 -7
- package/build/SmartCameraView.js.map +0 -1
- package/expo-module.config.json +0 -9
- package/ios/MLKitFaceDetector.swift +0 -310
- package/ios/SmartCamera.podspec +0 -33
- package/ios/SmartCameraModule.swift +0 -225
- package/ios/SmartCameraView.swift +0 -146
- package/ios/WebRTCFrameBridge.swift +0 -150
package/build/webrtc/types.js
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebRTC-related types for the SmartCamera module
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Default Configurations
|
|
6
|
+
// =============================================================================
|
|
7
|
+
/**
|
|
8
|
+
* Default ICE servers (Google STUN servers)
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_ICE_SERVERS = [
|
|
11
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
12
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Default peer connection configuration
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_PEER_CONNECTION_CONFIG = {
|
|
18
|
+
iceServers: DEFAULT_ICE_SERVERS,
|
|
19
|
+
iceTransportPolicy: 'all',
|
|
20
|
+
bundlePolicy: 'balanced',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Default media constraints
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_MEDIA_CONSTRAINTS = {
|
|
26
|
+
video: {
|
|
27
|
+
width: { ideal: 1280 },
|
|
28
|
+
height: { ideal: 720 },
|
|
29
|
+
frameRate: { ideal: 30 },
|
|
30
|
+
facingMode: 'user',
|
|
31
|
+
},
|
|
32
|
+
audio: {
|
|
33
|
+
echoCancellation: true,
|
|
34
|
+
noiseSuppression: true,
|
|
35
|
+
autoGainControl: true,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
5
38
|
//# sourceMappingURL=types.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/webrtc/types.ts"],"names":[],"mappings":"AAAA;;GAEG
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/webrtc/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAkUH,gFAAgF;AAChF,yBAAyB;AACzB,gFAAgF;AAEhF;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAgB;IAC9C,EAAE,IAAI,EAAE,8BAA8B,EAAE;IACxC,EAAE,IAAI,EAAE,+BAA+B,EAAE;CAC1C,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAyB;IAClE,UAAU,EAAE,mBAAmB;IAC/B,kBAAkB,EAAE,KAAK;IACzB,YAAY,EAAE,UAAU;CACzB,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAqB;IACzD,KAAK,EAAE;QACL,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;QACtB,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;QACtB,SAAS,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QACxB,UAAU,EAAE,MAAM;KACnB;IACD,KAAK,EAAE;QACL,gBAAgB,EAAE,IAAI;QACtB,gBAAgB,EAAE,IAAI;QACtB,eAAe,EAAE,IAAI;KACtB;CACF,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#import <VisionCamera/FrameProcessorPlugin.h>
|
|
2
|
+
#import <VisionCamera/FrameProcessorPluginRegistry.h>
|
|
3
|
+
|
|
4
|
+
#if __has_include("react_native_smart_camera-Swift.h")
|
|
5
|
+
#import "react_native_smart_camera-Swift.h"
|
|
6
|
+
#else
|
|
7
|
+
#import <react_native_smart_camera/react_native_smart_camera-Swift.h>
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
// Register the frame processor plugin
|
|
11
|
+
VISION_EXPORT_FRAME_PROCESSOR(FaceDetectorFrameProcessorPlugin, detectFaces)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import VisionCamera
|
|
3
|
+
import MLKitFaceDetection
|
|
4
|
+
import MLKitVision
|
|
5
|
+
|
|
6
|
+
@objc(FaceDetectorFrameProcessorPlugin)
|
|
7
|
+
public class FaceDetectorFrameProcessorPlugin: FrameProcessorPlugin {
|
|
8
|
+
|
|
9
|
+
private var faceDetector: FaceDetector?
|
|
10
|
+
private var currentOptions: FaceDetectorOptions?
|
|
11
|
+
|
|
12
|
+
// Auto mode options (stored separately as they don't affect detector)
|
|
13
|
+
private var autoMode: Bool = false
|
|
14
|
+
private var windowWidth: CGFloat = 1.0
|
|
15
|
+
private var windowHeight: CGFloat = 1.0
|
|
16
|
+
private var cameraFacing: String = "front"
|
|
17
|
+
|
|
18
|
+
public override init(proxy: VisionCameraProxyHolder, options: [AnyHashable: Any]! = [:]) {
|
|
19
|
+
super.init(proxy: proxy, options: options)
|
|
20
|
+
print("[FaceDetectorPlugin] Initialized")
|
|
21
|
+
updateDetectorOptions(options as? [String: Any] ?? [:])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private func updateDetectorOptions(_ options: [String: Any]) {
|
|
25
|
+
let performanceMode = options["performanceMode"] as? String ?? "fast"
|
|
26
|
+
let landmarkMode = options["landmarkMode"] as? String ?? "none"
|
|
27
|
+
let contourMode = options["contourMode"] as? String ?? "none"
|
|
28
|
+
let classificationMode = options["classificationMode"] as? String ?? "none"
|
|
29
|
+
let minFaceSize = options["minFaceSize"] as? CGFloat ?? 0.15
|
|
30
|
+
let trackingEnabled = options["trackingEnabled"] as? Bool ?? false
|
|
31
|
+
|
|
32
|
+
// Update auto mode options
|
|
33
|
+
autoMode = options["autoMode"] as? Bool ?? false
|
|
34
|
+
windowWidth = options["windowWidth"] as? CGFloat ?? 1.0
|
|
35
|
+
windowHeight = options["windowHeight"] as? CGFloat ?? 1.0
|
|
36
|
+
cameraFacing = options["cameraFacing"] as? String ?? "front"
|
|
37
|
+
|
|
38
|
+
let newOptions = FaceDetectorOptions()
|
|
39
|
+
|
|
40
|
+
switch performanceMode {
|
|
41
|
+
case "accurate":
|
|
42
|
+
newOptions.performanceMode = .accurate
|
|
43
|
+
default:
|
|
44
|
+
newOptions.performanceMode = .fast
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
switch landmarkMode {
|
|
48
|
+
case "all":
|
|
49
|
+
newOptions.landmarkMode = .all
|
|
50
|
+
default:
|
|
51
|
+
newOptions.landmarkMode = .none
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
switch contourMode {
|
|
55
|
+
case "all":
|
|
56
|
+
newOptions.contourMode = .all
|
|
57
|
+
default:
|
|
58
|
+
newOptions.contourMode = .none
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
switch classificationMode {
|
|
62
|
+
case "all":
|
|
63
|
+
newOptions.classificationMode = .all
|
|
64
|
+
default:
|
|
65
|
+
newOptions.classificationMode = .none
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
newOptions.minFaceSize = minFaceSize
|
|
69
|
+
newOptions.isTrackingEnabled = trackingEnabled
|
|
70
|
+
|
|
71
|
+
faceDetector = FaceDetector.faceDetector(options: newOptions)
|
|
72
|
+
currentOptions = newOptions
|
|
73
|
+
print("[FaceDetectorPlugin] Detector updated with new options")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public override func callback(_ frame: Frame, withArguments arguments: [AnyHashable: Any]?) -> Any? {
|
|
77
|
+
if let args = arguments as? [String: Any], !args.isEmpty {
|
|
78
|
+
updateDetectorOptions(args)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
guard let detector = faceDetector else {
|
|
82
|
+
print("[FaceDetectorPlugin] Detector not initialized")
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
guard let pixelBuffer = CMSampleBufferGetImageBuffer(frame.buffer) else {
|
|
87
|
+
print("[FaceDetectorPlugin] Could not get pixel buffer")
|
|
88
|
+
return []
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let image = VisionImage(buffer: frame.buffer)
|
|
92
|
+
image.orientation = getImageOrientation(frame.orientation)
|
|
93
|
+
|
|
94
|
+
let frameWidth = CVPixelBufferGetWidth(pixelBuffer)
|
|
95
|
+
let frameHeight = CVPixelBufferGetHeight(pixelBuffer)
|
|
96
|
+
|
|
97
|
+
var detectedFaces: [Face] = []
|
|
98
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
99
|
+
|
|
100
|
+
detector.process(image) { faces, error in
|
|
101
|
+
if let error = error {
|
|
102
|
+
print("[FaceDetectorPlugin] Detection error: \(error.localizedDescription)")
|
|
103
|
+
} else if let faces = faces {
|
|
104
|
+
detectedFaces = faces
|
|
105
|
+
}
|
|
106
|
+
semaphore.signal()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_ = semaphore.wait(timeout: .now() + 0.1)
|
|
110
|
+
|
|
111
|
+
return detectedFaces.map { face in
|
|
112
|
+
faceToDict(
|
|
113
|
+
face,
|
|
114
|
+
frameWidth: frameWidth,
|
|
115
|
+
frameHeight: frameHeight,
|
|
116
|
+
autoMode: autoMode,
|
|
117
|
+
windowWidth: windowWidth,
|
|
118
|
+
windowHeight: windowHeight,
|
|
119
|
+
cameraFacing: cameraFacing
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private func getImageOrientation(_ orientation: Orientation) -> UIImage.Orientation {
|
|
125
|
+
switch orientation {
|
|
126
|
+
case .portrait:
|
|
127
|
+
return .right
|
|
128
|
+
case .portraitUpsideDown:
|
|
129
|
+
return .left
|
|
130
|
+
case .landscapeLeft:
|
|
131
|
+
return .up
|
|
132
|
+
case .landscapeRight:
|
|
133
|
+
return .down
|
|
134
|
+
@unknown default:
|
|
135
|
+
return .right
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func faceToDict(
|
|
140
|
+
_ face: Face,
|
|
141
|
+
frameWidth: Int,
|
|
142
|
+
frameHeight: Int,
|
|
143
|
+
autoMode: Bool,
|
|
144
|
+
windowWidth: CGFloat,
|
|
145
|
+
windowHeight: CGFloat,
|
|
146
|
+
cameraFacing: String
|
|
147
|
+
) -> [String: Any] {
|
|
148
|
+
var result: [String: Any] = [:]
|
|
149
|
+
|
|
150
|
+
// Calculate scale factors for autoMode
|
|
151
|
+
let scaleX = autoMode ? windowWidth / CGFloat(frameWidth) : 1.0 / CGFloat(frameWidth)
|
|
152
|
+
let scaleY = autoMode ? windowHeight / CGFloat(frameHeight) : 1.0 / CGFloat(frameHeight)
|
|
153
|
+
let mirrorX = autoMode && cameraFacing == "front"
|
|
154
|
+
|
|
155
|
+
// Bounding box
|
|
156
|
+
let bounds = face.frame
|
|
157
|
+
let x: CGFloat
|
|
158
|
+
if mirrorX {
|
|
159
|
+
x = windowWidth - ((bounds.origin.x + bounds.width) * scaleX)
|
|
160
|
+
} else {
|
|
161
|
+
x = bounds.origin.x * scaleX
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
result["bounds"] = [
|
|
165
|
+
"x": x,
|
|
166
|
+
"y": bounds.origin.y * scaleY,
|
|
167
|
+
"width": bounds.width * scaleX,
|
|
168
|
+
"height": bounds.height * scaleY
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
// Angles
|
|
172
|
+
result["rollAngle"] = face.headEulerAngleZ
|
|
173
|
+
result["pitchAngle"] = face.headEulerAngleX
|
|
174
|
+
result["yawAngle"] = face.headEulerAngleY
|
|
175
|
+
|
|
176
|
+
// Classification probabilities
|
|
177
|
+
if face.hasSmilingProbability {
|
|
178
|
+
result["smilingProbability"] = face.smilingProbability
|
|
179
|
+
}
|
|
180
|
+
if face.hasLeftEyeOpenProbability {
|
|
181
|
+
result["leftEyeOpenProbability"] = face.leftEyeOpenProbability
|
|
182
|
+
}
|
|
183
|
+
if face.hasRightEyeOpenProbability {
|
|
184
|
+
result["rightEyeOpenProbability"] = face.rightEyeOpenProbability
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Tracking ID
|
|
188
|
+
if face.hasTrackingID {
|
|
189
|
+
result["trackingId"] = face.trackingID
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Landmarks
|
|
193
|
+
var landmarks: [String: Any] = [:]
|
|
194
|
+
|
|
195
|
+
if let leftEye = face.landmark(ofType: .leftEye) {
|
|
196
|
+
landmarks["leftEye"] = pointToDict(leftEye.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
197
|
+
}
|
|
198
|
+
if let rightEye = face.landmark(ofType: .rightEye) {
|
|
199
|
+
landmarks["rightEye"] = pointToDict(rightEye.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
200
|
+
}
|
|
201
|
+
if let noseBase = face.landmark(ofType: .noseBase) {
|
|
202
|
+
landmarks["noseBase"] = pointToDict(noseBase.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
203
|
+
}
|
|
204
|
+
if let leftCheek = face.landmark(ofType: .leftCheek) {
|
|
205
|
+
landmarks["leftCheek"] = pointToDict(leftCheek.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
206
|
+
}
|
|
207
|
+
if let rightCheek = face.landmark(ofType: .rightCheek) {
|
|
208
|
+
landmarks["rightCheek"] = pointToDict(rightCheek.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
209
|
+
}
|
|
210
|
+
if let mouthLeft = face.landmark(ofType: .mouthLeft) {
|
|
211
|
+
landmarks["mouthLeft"] = pointToDict(mouthLeft.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
212
|
+
}
|
|
213
|
+
if let mouthRight = face.landmark(ofType: .mouthRight) {
|
|
214
|
+
landmarks["mouthRight"] = pointToDict(mouthRight.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
215
|
+
}
|
|
216
|
+
if let mouthBottom = face.landmark(ofType: .mouthBottom) {
|
|
217
|
+
landmarks["mouthBottom"] = pointToDict(mouthBottom.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
218
|
+
}
|
|
219
|
+
if let leftEar = face.landmark(ofType: .leftEar) {
|
|
220
|
+
landmarks["leftEar"] = pointToDict(leftEar.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
221
|
+
}
|
|
222
|
+
if let rightEar = face.landmark(ofType: .rightEar) {
|
|
223
|
+
landmarks["rightEar"] = pointToDict(rightEar.position, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if !landmarks.isEmpty {
|
|
227
|
+
result["landmarks"] = landmarks
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// All contours (like reference package)
|
|
231
|
+
var contours: [String: Any] = [:]
|
|
232
|
+
|
|
233
|
+
if let contour = face.contour(ofType: .face) {
|
|
234
|
+
contours["face"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
235
|
+
}
|
|
236
|
+
if let contour = face.contour(ofType: .leftEyebrowTop) {
|
|
237
|
+
contours["leftEyebrowTop"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
238
|
+
}
|
|
239
|
+
if let contour = face.contour(ofType: .leftEyebrowBottom) {
|
|
240
|
+
contours["leftEyebrowBottom"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
241
|
+
}
|
|
242
|
+
if let contour = face.contour(ofType: .rightEyebrowTop) {
|
|
243
|
+
contours["rightEyebrowTop"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
244
|
+
}
|
|
245
|
+
if let contour = face.contour(ofType: .rightEyebrowBottom) {
|
|
246
|
+
contours["rightEyebrowBottom"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
247
|
+
}
|
|
248
|
+
if let contour = face.contour(ofType: .leftEye) {
|
|
249
|
+
contours["leftEye"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
250
|
+
}
|
|
251
|
+
if let contour = face.contour(ofType: .rightEye) {
|
|
252
|
+
contours["rightEye"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
253
|
+
}
|
|
254
|
+
if let contour = face.contour(ofType: .upperLipTop) {
|
|
255
|
+
contours["upperLipTop"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
256
|
+
}
|
|
257
|
+
if let contour = face.contour(ofType: .upperLipBottom) {
|
|
258
|
+
contours["upperLipBottom"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
259
|
+
}
|
|
260
|
+
if let contour = face.contour(ofType: .lowerLipTop) {
|
|
261
|
+
contours["lowerLipTop"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
262
|
+
}
|
|
263
|
+
if let contour = face.contour(ofType: .lowerLipBottom) {
|
|
264
|
+
contours["lowerLipBottom"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
265
|
+
}
|
|
266
|
+
if let contour = face.contour(ofType: .noseBridge) {
|
|
267
|
+
contours["noseBridge"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
268
|
+
}
|
|
269
|
+
if let contour = face.contour(ofType: .noseBottom) {
|
|
270
|
+
contours["noseBottom"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
271
|
+
}
|
|
272
|
+
if let contour = face.contour(ofType: .leftCheek) {
|
|
273
|
+
contours["leftCheek"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
274
|
+
}
|
|
275
|
+
if let contour = face.contour(ofType: .rightCheek) {
|
|
276
|
+
contours["rightCheek"] = contour.points.map { pointToDict($0, scaleX: scaleX, scaleY: scaleY, mirrorX: mirrorX, windowWidth: windowWidth) }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if !contours.isEmpty {
|
|
280
|
+
result["contours"] = contours
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private func pointToDict(
|
|
287
|
+
_ point: VisionPoint,
|
|
288
|
+
scaleX: CGFloat,
|
|
289
|
+
scaleY: CGFloat,
|
|
290
|
+
mirrorX: Bool,
|
|
291
|
+
windowWidth: CGFloat
|
|
292
|
+
) -> [String: CGFloat] {
|
|
293
|
+
let x: CGFloat
|
|
294
|
+
if mirrorX {
|
|
295
|
+
x = windowWidth - (point.x * scaleX)
|
|
296
|
+
} else {
|
|
297
|
+
x = point.x * scaleX
|
|
298
|
+
}
|
|
299
|
+
return [
|
|
300
|
+
"x": x,
|
|
301
|
+
"y": point.y * scaleY
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arfuhad/react-native-smart-camera",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "VisionCamera frame processor plugin for face detection, blink detection, and WebRTC streaming",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
7
|
+
"react-native": "src/index.ts",
|
|
8
|
+
"source": "src/index.ts",
|
|
7
9
|
"scripts": {
|
|
8
|
-
"build": "
|
|
10
|
+
"build": "tsc --project tsconfig.build.json",
|
|
9
11
|
"build:plugin": "tsc --project plugin/tsconfig.json",
|
|
10
|
-
"clean": "
|
|
11
|
-
"lint": "
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"prepublishOnly": "expo-module prepublishOnly",
|
|
15
|
-
"expo-module": "expo-module"
|
|
12
|
+
"clean": "rm -rf build plugin/build",
|
|
13
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
14
|
+
"prepare": "npm run build && npm run build:plugin",
|
|
15
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"react-native",
|
|
@@ -23,18 +23,19 @@
|
|
|
23
23
|
"webrtc",
|
|
24
24
|
"vision-camera",
|
|
25
25
|
"ml-kit",
|
|
26
|
-
"expo-module",
|
|
27
26
|
"frame-processor"
|
|
28
27
|
],
|
|
29
28
|
"files": [
|
|
29
|
+
"src",
|
|
30
30
|
"build",
|
|
31
31
|
"ios",
|
|
32
32
|
"android",
|
|
33
33
|
"plugin/build",
|
|
34
34
|
"app.plugin.js",
|
|
35
|
-
"
|
|
35
|
+
"react-native-smart-camera.podspec",
|
|
36
36
|
"README.md",
|
|
37
|
-
"ARCHITECTURE.md"
|
|
37
|
+
"ARCHITECTURE.md",
|
|
38
|
+
"CHANGELOG.md"
|
|
38
39
|
],
|
|
39
40
|
"repository": {
|
|
40
41
|
"type": "git",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'react-native-smart-camera'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = { :ios => '13.4' }
|
|
14
|
+
s.swift_version = '5.4'
|
|
15
|
+
s.source = { git: package['repository']['url'], tag: "v#{s.version}" }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
19
|
+
|
|
20
|
+
# React Native dependency
|
|
21
|
+
s.dependency 'React-Core'
|
|
22
|
+
|
|
23
|
+
# VisionCamera for frame processor API
|
|
24
|
+
s.dependency 'VisionCamera'
|
|
25
|
+
|
|
26
|
+
# Google ML Kit Face Detection
|
|
27
|
+
s.dependency 'GoogleMLKit/FaceDetection', '~> 5.0.0'
|
|
28
|
+
|
|
29
|
+
# Exclude test files
|
|
30
|
+
s.exclude_files = "ios/Tests/**/*"
|
|
31
|
+
end
|
|
32
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Face, BlinkEvent } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Threshold for considering an eye as closed
|
|
5
|
+
*/
|
|
6
|
+
const EYE_CLOSED_THRESHOLD = 0.4;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Threshold for considering an eye as open
|
|
10
|
+
*/
|
|
11
|
+
const EYE_OPEN_THRESHOLD = 0.6;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* State for tracking blink across frames
|
|
15
|
+
*/
|
|
16
|
+
interface BlinkState {
|
|
17
|
+
wasEyesClosed: boolean;
|
|
18
|
+
lastBlinkTimestamp: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Global blink state per face tracking ID
|
|
22
|
+
const blinkStates = new Map<number, BlinkState>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Process faces to detect blinks
|
|
26
|
+
*
|
|
27
|
+
* @param faces - Array of detected faces
|
|
28
|
+
* @param lastBlinkTimestamp - Timestamp of the last detected blink
|
|
29
|
+
* @param debounceMs - Minimum time between blinks in milliseconds
|
|
30
|
+
* @returns BlinkEvent if a blink was detected, null otherwise
|
|
31
|
+
*/
|
|
32
|
+
export function processBlinkFromFaces(
|
|
33
|
+
faces: Face[],
|
|
34
|
+
lastBlinkTimestamp: number,
|
|
35
|
+
debounceMs: number = 300
|
|
36
|
+
): BlinkEvent | null {
|
|
37
|
+
'worklet';
|
|
38
|
+
|
|
39
|
+
if (faces.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Use the first face (most prominent)
|
|
44
|
+
const face = faces[0];
|
|
45
|
+
|
|
46
|
+
// Ensure we have eye classification data
|
|
47
|
+
if (
|
|
48
|
+
face.leftEyeOpenProbability === undefined ||
|
|
49
|
+
face.rightEyeOpenProbability === undefined
|
|
50
|
+
) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const leftEyeOpen = face.leftEyeOpenProbability;
|
|
55
|
+
const rightEyeOpen = face.rightEyeOpenProbability;
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
|
|
58
|
+
// Get or create blink state for this face
|
|
59
|
+
const faceId = face.trackingId ?? 0;
|
|
60
|
+
let state = blinkStates.get(faceId);
|
|
61
|
+
|
|
62
|
+
if (!state) {
|
|
63
|
+
state = {
|
|
64
|
+
wasEyesClosed: false,
|
|
65
|
+
lastBlinkTimestamp: 0,
|
|
66
|
+
};
|
|
67
|
+
blinkStates.set(faceId, state);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if eyes are currently closed
|
|
71
|
+
const eyesClosed = leftEyeOpen < EYE_CLOSED_THRESHOLD && rightEyeOpen < EYE_CLOSED_THRESHOLD;
|
|
72
|
+
|
|
73
|
+
// Check if eyes are currently open
|
|
74
|
+
const eyesOpen = leftEyeOpen > EYE_OPEN_THRESHOLD && rightEyeOpen > EYE_OPEN_THRESHOLD;
|
|
75
|
+
|
|
76
|
+
// Detect blink: transition from closed to open
|
|
77
|
+
const isBlink = state.wasEyesClosed && eyesOpen && (now - state.lastBlinkTimestamp) > debounceMs;
|
|
78
|
+
|
|
79
|
+
// Update state
|
|
80
|
+
if (eyesClosed) {
|
|
81
|
+
state.wasEyesClosed = true;
|
|
82
|
+
} else if (eyesOpen) {
|
|
83
|
+
state.wasEyesClosed = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isBlink) {
|
|
87
|
+
state.lastBlinkTimestamp = now;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
timestamp: now,
|
|
91
|
+
leftEyeOpen,
|
|
92
|
+
rightEyeOpen,
|
|
93
|
+
isBlink: true,
|
|
94
|
+
faceId: face.trackingId,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reset blink state for all faces
|
|
103
|
+
*/
|
|
104
|
+
export function resetBlinkStates(): void {
|
|
105
|
+
blinkStates.clear();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get current eye state without blink detection
|
|
110
|
+
* Useful for real-time eye tracking UI
|
|
111
|
+
*/
|
|
112
|
+
export function getEyeState(face: Face): { leftOpen: number; rightOpen: number } | null {
|
|
113
|
+
'worklet';
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
face.leftEyeOpenProbability === undefined ||
|
|
117
|
+
face.rightEyeOpenProbability === undefined
|
|
118
|
+
) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
leftOpen: face.leftEyeOpenProbability,
|
|
124
|
+
rightOpen: face.rightEyeOpenProbability,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { VisionCameraProxy, type Frame } from 'react-native-vision-camera';
|
|
2
|
+
import type { Face, FrameProcessorOptions } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Initialize the face detector frame processor plugin
|
|
6
|
+
* This registers the native plugin with VisionCamera
|
|
7
|
+
*/
|
|
8
|
+
const plugin = VisionCameraProxy.initFrameProcessorPlugin('detectFaces', {});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default face detection options
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_OPTIONS: FrameProcessorOptions = {
|
|
14
|
+
performanceMode: 'fast',
|
|
15
|
+
landmarkMode: 'none',
|
|
16
|
+
contourMode: 'none',
|
|
17
|
+
classificationMode: 'none',
|
|
18
|
+
minFaceSize: 0.15,
|
|
19
|
+
trackingEnabled: false,
|
|
20
|
+
cameraFacing: 'front',
|
|
21
|
+
autoMode: false,
|
|
22
|
+
windowWidth: 1.0,
|
|
23
|
+
windowHeight: 1.0,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect faces in a camera frame
|
|
28
|
+
*
|
|
29
|
+
* @param frame - The camera frame from VisionCamera
|
|
30
|
+
* @param options - Face detection options
|
|
31
|
+
* @returns Array of detected faces
|
|
32
|
+
*/
|
|
33
|
+
export function detectFaces(frame: Frame, options?: Partial<FrameProcessorOptions>): Face[] {
|
|
34
|
+
'worklet';
|
|
35
|
+
|
|
36
|
+
if (plugin == null) {
|
|
37
|
+
// Plugin not available - return empty array
|
|
38
|
+
// This can happen if the native module is not properly linked
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mergedOptions: FrameProcessorOptions = {
|
|
43
|
+
...DEFAULT_OPTIONS,
|
|
44
|
+
...options,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Convert options to a format compatible with VisionCamera plugin API
|
|
49
|
+
const pluginOptions: Record<string, string | number | boolean | undefined> = {
|
|
50
|
+
performanceMode: mergedOptions.performanceMode,
|
|
51
|
+
landmarkMode: mergedOptions.landmarkMode,
|
|
52
|
+
contourMode: mergedOptions.contourMode,
|
|
53
|
+
classificationMode: mergedOptions.classificationMode,
|
|
54
|
+
minFaceSize: mergedOptions.minFaceSize,
|
|
55
|
+
trackingEnabled: mergedOptions.trackingEnabled,
|
|
56
|
+
cameraFacing: mergedOptions.cameraFacing,
|
|
57
|
+
autoMode: mergedOptions.autoMode,
|
|
58
|
+
windowWidth: mergedOptions.windowWidth,
|
|
59
|
+
windowHeight: mergedOptions.windowHeight,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = plugin.call(frame, pluginOptions) as Face[] | null | undefined;
|
|
63
|
+
return result ?? [];
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Return empty array on error in worklet context
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if the face detector plugin is available
|
|
72
|
+
* @returns true if the plugin is registered and available
|
|
73
|
+
*/
|
|
74
|
+
export function isFaceDetectorAvailable(): boolean {
|
|
75
|
+
return plugin != null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|