@faceaisdk/react-native-face-sdk 0.1.1

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 (35) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +252 -0
  3. package/android/build.gradle +53 -0
  4. package/android/libs/FaceSDKLib-release.aar +0 -0
  5. package/android/proguard-rules.pro +3 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/com/faceaisdk/reactnative/FaceRNModule.kt +383 -0
  8. package/android/src/main/java/com/faceaisdk/reactnative/FaceRNPackage.kt +16 -0
  9. package/ios/FaceAISDK/CustomToastView.swift +34 -0
  10. package/ios/FaceAISDK/FaceAINaviView.swift +212 -0
  11. package/ios/FaceAISDK/FaceSDKCameraView.swift +40 -0
  12. package/ios/FaceAISDK/FaceSDKLocalizer.swift +21 -0
  13. package/ios/FaceAISDK/LivenessDetectView.swift +317 -0
  14. package/ios/FaceAISDK/ScreenBrightnessHelper.swift +100 -0
  15. package/ios/FaceAISDK/TTSPlayer.swift +357 -0
  16. package/ios/FaceAISDK/VerifyFaceView.swift +284 -0
  17. package/ios/FaceAISDK/addFace/AddFaceByCamera.swift +207 -0
  18. package/ios/FaceAISDK/addFace/AddFaceByImage.swift +174 -0
  19. package/ios/FaceAISDK/addFace/ImagePicker.swift +52 -0
  20. package/ios/FaceAISDK/addFace/VerifyTwoFaceSimiView.swift +210 -0
  21. package/ios/FaceColorExtensions.swift +10 -0
  22. package/ios/FaceRNModule.h +9 -0
  23. package/ios/FaceRNModule.m +197 -0
  24. package/ios/FaceSDKSwiftManager.swift +277 -0
  25. package/ios/Resources/en.lproj/Localizable.strings +51 -0
  26. package/ios/Resources/light_too_high.png +0 -0
  27. package/ios/Resources/zh-Hans.lproj/Localizable.strings +51 -0
  28. package/lib/index.d.ts +22 -0
  29. package/lib/index.js +112 -0
  30. package/lib/types.d.ts +39 -0
  31. package/lib/types.js +2 -0
  32. package/package.json +88 -0
  33. package/react-native-face-sdk.podspec +28 -0
  34. package/src/index.ts +184 -0
  35. package/src/types.ts +90 -0
@@ -0,0 +1,100 @@
1
+ import UIKit
2
+
3
+ // 亮度控制单例工具,为了兼容uniapp Flutter等插件
4
+ public class ScreenBrightnessHelper {
5
+
6
+ // 单例入口
7
+ public static let shared = ScreenBrightnessHelper()
8
+
9
+ // 内部状态
10
+ private var originalBrightness: CGFloat?
11
+ private var wasIdleTimerDisabled: Bool = false
12
+
13
+ // 防止重复设置的标记
14
+ private var isMaximized = false
15
+
16
+ private init() {}
17
+
18
+ /// 保存当前环境并调至最亮 (线程安全)
19
+ public func maximizeBrightness() {
20
+ runOnMain { [weak self] in
21
+ guard let self = self else { return }
22
+
23
+ // 1. 如果当前没有处于“已调亮”状态,才保存原始值
24
+ // 这样可以防止连续调用 maximize 导致把 1.0 误保存为原始亮度
25
+ if !self.isMaximized {
26
+ self.originalBrightness = self.getCurrentBrightness()
27
+ self.wasIdleTimerDisabled = UIApplication.shared.isIdleTimerDisabled
28
+ self.isMaximized = true
29
+ }
30
+
31
+ // 2. 调亮屏幕
32
+ self.setBrightness(1.0)
33
+
34
+ // 3. 禁止自动锁屏
35
+ UIApplication.shared.isIdleTimerDisabled = true
36
+ }
37
+ }
38
+
39
+ /// 恢复环境 (线程安全)
40
+ public func restoreBrightness() {
41
+ runOnMain { [weak self] in
42
+ guard let self = self else { return }
43
+
44
+ // 只有在“已调亮”状态下才执行恢复
45
+ guard self.isMaximized, let original = self.originalBrightness else { return }
46
+
47
+ // 1. 恢复亮度
48
+ self.setBrightness(original)
49
+
50
+ // 2. 恢复锁屏设置
51
+ UIApplication.shared.isIdleTimerDisabled = self.wasIdleTimerDisabled
52
+
53
+ // 3. 重置状态
54
+ self.isMaximized = false
55
+ self.originalBrightness = nil
56
+ }
57
+ }
58
+
59
+ // MARK: - 内部私有方法
60
+
61
+ /// 获取当前亮度 (兼容 iOS 15+)
62
+ private func getCurrentBrightness() -> CGFloat {
63
+ if #available(iOS 15.0, *) {
64
+ // 优先获取活跃的前台 Scene
65
+ let scene = UIApplication.shared.connectedScenes
66
+ .filter { $0.activationState == .foregroundActive }
67
+ .compactMap { $0 as? UIWindowScene }
68
+ .first
69
+ return scene?.screen.brightness ?? UIScreen.main.brightness
70
+ } else {
71
+ return UIScreen.main.brightness
72
+ }
73
+ }
74
+
75
+ private func setBrightness(_ value: CGFloat) {
76
+ if #available(iOS 15.0, *) {
77
+ if let scene = UIApplication.shared.connectedScenes
78
+ .filter({ $0.activationState == .foregroundActive })
79
+ .compactMap({ $0 as? UIWindowScene })
80
+ .first {
81
+ scene.screen.brightness = value
82
+ } else {
83
+ UIScreen.main.brightness = value
84
+ }
85
+ } else {
86
+ UIScreen.main.brightness = value
87
+ }
88
+ }
89
+
90
+ /// 辅助方法:确保闭包在主线程执行
91
+ private func runOnMain(_ block: @escaping () -> Void) {
92
+ if Thread.isMainThread {
93
+ block()
94
+ } else {
95
+ DispatchQueue.main.async {
96
+ block()
97
+ }
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,357 @@
1
+ import Foundation
2
+ import AVFoundation
3
+ import os.log
4
+ #if canImport(UIKit)
5
+ import UIKit
6
+ #endif
7
+
8
+ // MARK: - TTSPlayer
9
+
10
+ /// iOS 原生语音播报管理器(兼容 iOS 15 ~ 26)
11
+ final class TTSPlayer: NSObject {
12
+
13
+ static let shared = TTSPlayer()
14
+
15
+ // MARK: - 播报策略
16
+
17
+ enum Policy {
18
+ case interrupt
19
+ case enqueue
20
+ case dropIfBusy
21
+ }
22
+
23
+ // MARK: - 状态
24
+
25
+ enum State: Equatable {
26
+ case idle
27
+ case speaking(String)
28
+ case paused
29
+ }
30
+
31
+ var onStateChanged: ((State) -> Void)?
32
+
33
+ /// State 仅限在主线程读写,确保外部 UI 绑定的绝对安全
34
+ private(set) var state: State = .idle {
35
+ didSet {
36
+ guard state != oldValue else { return }
37
+ onStateChanged?(state)
38
+ }
39
+ }
40
+
41
+ var isSpeaking: Bool {
42
+ if case .speaking = state { return true }
43
+ return false
44
+ }
45
+
46
+ // MARK: - Private
47
+
48
+ private let synthesizer = AVSpeechSynthesizer()
49
+ private let log: OSLog
50
+
51
+ // 💡 核心优化:创建一个专用的默认优先级后台串行队列,隔离所有耗时/阻塞 API
52
+ private let workQueue = DispatchQueue(label: "com.faceAI.sdk.ttsPlayer", qos: .default)
53
+
54
+ // 以下变量现在仅在 workQueue 中访问,天然线程安全
55
+ private var isSessionActive = false
56
+ private var pendingDeactivation: DispatchWorkItem?
57
+ private let sessionDeactivationDelay: TimeInterval = 1.5
58
+
59
+ private var voiceCache: [String: AVSpeechSynthesisVoice] = [:]
60
+
61
+ private var lastSpokenText: String?
62
+ private var lastSpokenTime: CFAbsoluteTime = 0
63
+ private let dedupInterval: TimeInterval = 0.5
64
+
65
+ private var interruptedBySystem = false
66
+
67
+ // MARK: - Init
68
+
69
+ private override init() {
70
+ self.log = OSLog(subsystem: "com.faceAI.sdk", category: "TTSPlayer")
71
+ super.init()
72
+ synthesizer.delegate = self
73
+ addObservers()
74
+ }
75
+
76
+ deinit {
77
+ NotificationCenter.default.removeObserver(self)
78
+ }
79
+
80
+ // MARK: - Public API
81
+
82
+ /// 播报文本
83
+ func speak(_ text: String?,
84
+ language: String? = nil,
85
+ rate: Float = 0.5,
86
+ pitch: Float = 0.98,
87
+ policy: Policy = .dropIfBusy) {
88
+ guard let text = text, !text.isEmpty else { return }
89
+
90
+ // 💡 核心优化:将所有操作派发到后台队列,避免阻塞主线程
91
+ workQueue.async { [weak self] in
92
+ guard let self = self else { return }
93
+
94
+ let now = CFAbsoluteTimeGetCurrent()
95
+ if text == self.lastSpokenText && (now - self.lastSpokenTime) < self.dedupInterval {
96
+ return
97
+ }
98
+
99
+ switch policy {
100
+ case .interrupt:
101
+ self.synthesizer.stopSpeaking(at: .immediate)
102
+ case .enqueue:
103
+ break
104
+ case .dropIfBusy:
105
+ // 现在在后台队列查询 isSpeaking,不会引起优先级反转
106
+ if self.synthesizer.isSpeaking {
107
+ return
108
+ }
109
+ }
110
+
111
+ self.activateSessionIfNeeded()
112
+
113
+ let utterance = AVSpeechUtterance(string: text)
114
+ let clampedRate = min(max(rate, 0), 1)
115
+ utterance.rate = AVSpeechUtteranceMinimumSpeechRate
116
+ + clampedRate * (AVSpeechUtteranceMaximumSpeechRate - AVSpeechUtteranceMinimumSpeechRate)
117
+
118
+ utterance.pitchMultiplier = min(max(pitch, 0.5), 2.0)
119
+ utterance.preUtteranceDelay = 0.05
120
+ utterance.postUtteranceDelay = 0.15
121
+
122
+ // speechVoices 查询也被移到了后台执行
123
+ utterance.voice = self.cachedVoice(for: language)
124
+
125
+ self.lastSpokenText = text
126
+ self.lastSpokenTime = now
127
+
128
+ self.synthesizer.speak(utterance)
129
+ }
130
+ }
131
+
132
+ func pause() {
133
+ workQueue.async { [weak self] in
134
+ guard let self = self else { return }
135
+ if self.synthesizer.isSpeaking {
136
+ self.synthesizer.pauseSpeaking(at: .word)
137
+ }
138
+ }
139
+ }
140
+
141
+ func resume() {
142
+ workQueue.async { [weak self] in
143
+ guard let self = self else { return }
144
+ if self.synthesizer.isPaused {
145
+ self.activateSessionIfNeeded()
146
+ self.synthesizer.continueSpeaking()
147
+ }
148
+ }
149
+ }
150
+
151
+ func stop() {
152
+ workQueue.async { [weak self] in
153
+ guard let self = self else { return }
154
+ self.stopSynthesizerIfActive()
155
+ }
156
+ }
157
+
158
+ func release() {
159
+ workQueue.async { [weak self] in
160
+ guard let self = self else { return }
161
+ self.stopSynthesizerIfActive()
162
+ self.deactivateSessionNow()
163
+ self.voiceCache.removeAll()
164
+ }
165
+ }
166
+
167
+ private func stopSynthesizerIfActive() {
168
+ if synthesizer.isSpeaking || synthesizer.isPaused {
169
+ synthesizer.stopSpeaking(at: .immediate)
170
+ }
171
+ }
172
+
173
+ // MARK: - Audio Session (仅在 workQueue 内调用)
174
+
175
+ private func activateSessionIfNeeded() {
176
+ pendingDeactivation?.cancel()
177
+ pendingDeactivation = nil
178
+
179
+ guard !isSessionActive else { return }
180
+ do {
181
+ let session = AVAudioSession.sharedInstance()
182
+ try session.setCategory(.playback, mode: .spokenAudio, options: [.duckOthers])
183
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
184
+ isSessionActive = true
185
+ } catch {
186
+ os_log("AudioSession activate failed: %{public}@", log: log, type: .error, error.localizedDescription)
187
+ }
188
+ }
189
+
190
+ private func scheduleDeactivation() {
191
+ pendingDeactivation?.cancel()
192
+ let item = DispatchWorkItem { [weak self] in
193
+ self?.deactivateSessionNow()
194
+ }
195
+ pendingDeactivation = item
196
+ // 在后台队列调度延时任务
197
+ workQueue.asyncAfter(deadline: .now() + sessionDeactivationDelay, execute: item)
198
+ }
199
+
200
+ private func deactivateSessionNow() {
201
+ pendingDeactivation?.cancel()
202
+ pendingDeactivation = nil
203
+ guard isSessionActive else { return }
204
+ do {
205
+ try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
206
+ isSessionActive = false
207
+ } catch {
208
+ os_log("AudioSession deactivate failed: %{public}@", log: log, type: .error, error.localizedDescription)
209
+ }
210
+ }
211
+
212
+ // MARK: - Observers
213
+
214
+ private func addObservers() {
215
+ let nc = NotificationCenter.default
216
+ nc.addObserver(self, selector: #selector(handleInterruption),
217
+ name: AVAudioSession.interruptionNotification, object: nil)
218
+ nc.addObserver(self, selector: #selector(handleRouteChange),
219
+ name: AVAudioSession.routeChangeNotification, object: nil)
220
+ #if canImport(UIKit)
221
+ nc.addObserver(self, selector: #selector(handleDidEnterBackground),
222
+ name: UIApplication.didEnterBackgroundNotification, object: nil)
223
+ #endif
224
+ }
225
+
226
+ @objc private func handleInterruption(_ notification: Notification) {
227
+ guard let info = notification.userInfo,
228
+ let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
229
+ let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
230
+
231
+ let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
232
+ let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
233
+
234
+ // 统一派发到工作队列处理
235
+ workQueue.async { [weak self] in
236
+ guard let self = self else { return }
237
+
238
+ switch type {
239
+ case .began:
240
+ self.interruptedBySystem = self.synthesizer.isSpeaking || self.synthesizer.isPaused
241
+ self.isSessionActive = false
242
+
243
+ case .ended:
244
+ let shouldResume = self.interruptedBySystem
245
+ self.interruptedBySystem = false
246
+
247
+ if shouldResume, options.contains(.shouldResume) {
248
+ self.activateSessionIfNeeded()
249
+ self.synthesizer.continueSpeaking()
250
+ } else if shouldResume {
251
+ self.stopSynthesizerIfActive()
252
+ }
253
+
254
+ @unknown default:
255
+ break
256
+ }
257
+ }
258
+ }
259
+
260
+ @objc private func handleRouteChange(_ notification: Notification) {
261
+ guard let info = notification.userInfo,
262
+ let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
263
+ let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }
264
+
265
+ if reason == .oldDeviceUnavailable {
266
+ workQueue.async { [weak self] in
267
+ if self?.synthesizer.isSpeaking == true {
268
+ self?.synthesizer.pauseSpeaking(at: .word)
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ @objc private func handleDidEnterBackground() {
275
+ workQueue.async { [weak self] in
276
+ self?.stopSynthesizerIfActive()
277
+ }
278
+ }
279
+
280
+ // MARK: - Voice Selection (仅在 workQueue 内调用)
281
+
282
+ private func cachedVoice(for language: String?) -> AVSpeechSynthesisVoice? {
283
+ let lang = language ?? Locale.preferredLanguages.first ?? "en-US"
284
+ if let cached = voiceCache[lang] { return cached }
285
+ let voice = bestVoice(for: lang)
286
+ if let voice = voice { voiceCache[lang] = voice }
287
+ return voice
288
+ }
289
+
290
+ private func bestVoice(for lang: String) -> AVSpeechSynthesisVoice? {
291
+ let voices = AVSpeechSynthesisVoice.speechVoices()
292
+
293
+ let candidates: [AVSpeechSynthesisVoice] = {
294
+ let exact = voices.filter { $0.language == lang }
295
+ if !exact.isEmpty { return exact }
296
+
297
+ let prefix = lang.components(separatedBy: "-").first ?? lang
298
+ let prefixed = voices.filter { $0.language.hasPrefix(prefix) }
299
+ if !prefixed.isEmpty { return prefixed }
300
+
301
+ if prefix == "zh" {
302
+ let cn = voices.filter { $0.language == "zh-CN" }
303
+ if !cn.isEmpty { return cn }
304
+ let tw = voices.filter { $0.language == "zh-TW" }
305
+ if !tw.isEmpty { return tw }
306
+ }
307
+
308
+ return voices.filter { $0.language == "en-US" }
309
+ }()
310
+
311
+ return candidates.max(by: { $0.quality.rawValue < $1.quality.rawValue })
312
+ }
313
+
314
+ // MARK: - Helpers
315
+
316
+ /// 安全地向主线程抛出状态变更
317
+ private func updateStateOnMainThread(_ newState: State) {
318
+ if Thread.isMainThread {
319
+ self.state = newState
320
+ } else {
321
+ DispatchQueue.main.async { [weak self] in
322
+ self?.state = newState
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ // MARK: - AVSpeechSynthesizerDelegate
329
+
330
+ extension TTSPlayer: AVSpeechSynthesizerDelegate {
331
+
332
+ func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
333
+ updateStateOnMainThread(.speaking(utterance.speechString))
334
+ }
335
+
336
+ func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
337
+ updateStateOnMainThread(.idle)
338
+ workQueue.async { [weak self] in
339
+ self?.scheduleDeactivation()
340
+ }
341
+ }
342
+
343
+ func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
344
+ updateStateOnMainThread(.idle)
345
+ workQueue.async { [weak self] in
346
+ self?.scheduleDeactivation()
347
+ }
348
+ }
349
+
350
+ func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
351
+ updateStateOnMainThread(.paused)
352
+ }
353
+
354
+ func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) {
355
+ updateStateOnMainThread(.speaking(utterance.speechString))
356
+ }
357
+ }