@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.
- package/LICENSE +1 -0
- package/README.md +252 -0
- package/android/build.gradle +53 -0
- package/android/libs/FaceSDKLib-release.aar +0 -0
- package/android/proguard-rules.pro +3 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/faceaisdk/reactnative/FaceRNModule.kt +383 -0
- package/android/src/main/java/com/faceaisdk/reactnative/FaceRNPackage.kt +16 -0
- package/ios/FaceAISDK/CustomToastView.swift +34 -0
- package/ios/FaceAISDK/FaceAINaviView.swift +212 -0
- package/ios/FaceAISDK/FaceSDKCameraView.swift +40 -0
- package/ios/FaceAISDK/FaceSDKLocalizer.swift +21 -0
- package/ios/FaceAISDK/LivenessDetectView.swift +317 -0
- package/ios/FaceAISDK/ScreenBrightnessHelper.swift +100 -0
- package/ios/FaceAISDK/TTSPlayer.swift +357 -0
- package/ios/FaceAISDK/VerifyFaceView.swift +284 -0
- package/ios/FaceAISDK/addFace/AddFaceByCamera.swift +207 -0
- package/ios/FaceAISDK/addFace/AddFaceByImage.swift +174 -0
- package/ios/FaceAISDK/addFace/ImagePicker.swift +52 -0
- package/ios/FaceAISDK/addFace/VerifyTwoFaceSimiView.swift +210 -0
- package/ios/FaceColorExtensions.swift +10 -0
- package/ios/FaceRNModule.h +9 -0
- package/ios/FaceRNModule.m +197 -0
- package/ios/FaceSDKSwiftManager.swift +277 -0
- package/ios/Resources/en.lproj/Localizable.strings +51 -0
- package/ios/Resources/light_too_high.png +0 -0
- package/ios/Resources/zh-Hans.lproj/Localizable.strings +51 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +112 -0
- package/lib/types.d.ts +39 -0
- package/lib/types.js +2 -0
- package/package.json +88 -0
- package/react-native-face-sdk.podspec +28 -0
- package/src/index.ts +184 -0
- 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
|
+
}
|