Skip to content

Commit eb0dd55

Browse files
authored
chore: remove countdown from liveness session check (#36)
* chore: remove countdown from liveness session check * update state logic * remove unused constant * add back missing instructional text
1 parent 151ceb0 commit eb0dd55

9 files changed

+64
-237
lines changed

Sources/FaceLiveness/Utilities/LocalizedStringKey+Liveness.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,6 @@ extension LocalizedStringKey {
8383
"amplify_ui_liveness_challenge_recording_indicator_label"
8484
)
8585

86-
/// en = "Hold face position during countdown."
87-
static let challenge_instruction_hold_face_during_countdown = LocalizedStringKey(
88-
"amplify_ui_liveness_challenge_instruction_hold_face_during_countdown"
89-
)
90-
9186
/// en = "Hold face in oval for colored lights."
9287
static let challenge_instruction_hold_face_during_freshness = LocalizedStringKey(
9388
"amplify_ui_liveness_challenge_instruction_hold_face_during_freshness"

Sources/FaceLiveness/Views/Countdown/CountdownInstructionContainerView.swift

Lines changed: 0 additions & 52 deletions
This file was deleted.

Sources/FaceLiveness/Views/Countdown/CountdownView+ViewModel.swift

Lines changed: 0 additions & 67 deletions
This file was deleted.

Sources/FaceLiveness/Views/Countdown/CountdownView.swift

Lines changed: 0 additions & 64 deletions
This file was deleted.

Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ struct InstructionContainerView: View {
9191
percentage: 0.2
9292
)
9393
.frame(width: 200, height: 30)
94+
case .pendingFacePreparedConfirmation(let reason):
95+
InstructionView(
96+
text: .init(reason.rawValue),
97+
backgroundColor: .livenessBackground
98+
)
99+
case .completedDisplayingFreshness:
100+
InstructionView(
101+
text: .challenge_verifying,
102+
backgroundColor: .livenessBackground
103+
)
104+
.onAppear {
105+
UIAccessibility.post(
106+
notification: .announcement,
107+
argument: NSLocalizedString(
108+
"amplify_ui_liveness_challenge_verifying",
109+
bundle: .module,
110+
comment: ""
111+
)
112+
)
113+
}
94114
default:
95115
EmptyView()
96116
}

Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import SwiftUI
1010
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin
1111

1212
fileprivate let initialFaceDistanceThreshold: CGFloat = 0.32
13-
fileprivate let countdownFaceDistanceThreshold: CGFloat = 0.37
1413

1514
extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
1615
func process(newResult: FaceDetectionResult) {
@@ -34,25 +33,20 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
3433
switch livenessState.state {
3534
case .pendingFacePreparedConfirmation:
3635
if face.faceDistance <= initialFaceDistanceThreshold {
37-
DispatchQueue.main.async {
38-
self.livenessState.startCountdown()
39-
self.initializeLivenessStream()
40-
}
36+
DispatchQueue.main.async {
37+
self.livenessState.awaitingRecording()
38+
self.initializeLivenessStream()
39+
}
40+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
41+
self.livenessState.beginRecording()
42+
}
4143
return
4244
} else {
4345
DispatchQueue.main.async {
4446
self.livenessState.faceNotPrepared(reason: .faceTooClose)
4547
}
4648
return
4749
}
48-
case .countingDown:
49-
if face.faceDistance >= countdownFaceDistanceThreshold {
50-
DispatchQueue.main.async {
51-
self.livenessState.unrecoverableStateEncountered(
52-
.invalidFaceMovementDuringCountdown
53-
)
54-
}
55-
}
5650
case .recording(ovalDisplayed: false):
5751
drawOval()
5852
sendInitialFaceDetectedEvent(

Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,6 @@ struct LivenessStateMachine {
2525
state = .pendingFacePreparedConfirmation(reason)
2626
}
2727

28-
mutating func openSocket() {
29-
switch state {
30-
case .pendingFacePreparedConfirmation, .countingDown:
31-
state = .socketOpened
32-
default:
33-
break
34-
}
35-
}
36-
3728
mutating func awaitingFaceMatch(with instruction: Instructor.Instruction, nearnessPercentage: Double) {
3829
let reason: FaceNotPreparedReason
3930
let percentage: Double
@@ -56,16 +47,11 @@ struct LivenessStateMachine {
5647
state = .awaitingFaceInOvalMatch(reason, percentage)
5748
}
5849

59-
mutating func awaitingServerInfoEvent() {
60-
guard case .socketOpened = state else { return }
61-
state = .awaitingServerInfoEvent
62-
}
63-
64-
mutating func receivedServerInfoEvent() throws {
65-
guard case .awaitingServerInfoEvent = state else { return }
66-
state = .serverInfoEventReceived
50+
mutating func awaitingRecording() {
51+
guard case .pendingFacePreparedConfirmation = state else { return }
52+
state = .waitForRecording
6753
}
68-
54+
6955
mutating func unrecoverableStateEncountered(_ error: LivenessError) {
7056
switch state {
7157
case .encounteredUnrecoverableError, .completed:
@@ -83,11 +69,6 @@ struct LivenessStateMachine {
8369
state = .recording(ovalDisplayed: true)
8470
}
8571

86-
mutating func startCountdown() {
87-
guard case .pendingFacePreparedConfirmation = state else { return }
88-
state = .countingDown
89-
}
90-
9172
mutating func faceMatched() {
9273
state = .faceMatched
9374
}
@@ -106,7 +87,7 @@ struct LivenessStateMachine {
10687

10788
var shouldDisplayRecordingIcon: Bool {
10889
switch state {
109-
case .initial, .pendingFacePreparedConfirmation, .encounteredUnrecoverableError, .countingDown:
90+
case .initial, .pendingFacePreparedConfirmation, .encounteredUnrecoverableError:
11091
return false
11192
default: return true
11293
}
@@ -115,10 +96,6 @@ struct LivenessStateMachine {
11596
enum State: Equatable {
11697
case initial
11798
case pendingFacePreparedConfirmation(FaceNotPreparedReason)
118-
case socketOpened
119-
case awaitingServerInfoEvent
120-
case serverInfoEventReceived
121-
case countingDown
12299
case recording(ovalDisplayed: Bool)
123100
case awaitingFaceInOvalMatch(FaceNotPreparedReason, Double)
124101
case faceMatched
@@ -129,6 +106,7 @@ struct LivenessStateMachine {
129106
case awaitingDisconnectEvent
130107
case disconnectEventReceived
131108
case encounteredUnrecoverableError(LivenessError)
109+
case waitForRecording
132110
}
133111

134112
enum FaceNotPreparedReason: String, Equatable {

Sources/FaceLiveness/Views/Liveness/_FaceLivenessDetectionView.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,6 @@ struct _FaceLivenessDetectionView<VideoView: View>: View {
5050
)
5151

5252
Spacer()
53-
54-
CountdownInstructionContainerView(
55-
viewModel: viewModel,
56-
onCountdownComplete: {
57-
viewModel.livenessState.beginRecording()
58-
}
59-
)
60-
.padding(.bottom)
6153
}
6254
.padding([.leading, .trailing])
6355
.aspectRatio(3/4, contentMode: .fit)

Tests/FaceLivenessTests/LivenessTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,35 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
100100
"initializeLivenessStream(withSessionID:userAgent:)"
101101
])
102102
}
103+
104+
/// Given: A `FaceLivenessDetectionViewModel`
105+
/// When: The viewModel is processes a single face result with a face distance less than the inital face distance
106+
/// Then: The end state of this flow is `.recording(ovalDisplayed: false)` and initializeLivenessStream(withSessionID:userAgent:) is called
107+
func testTransitionToRecordingState() async throws {
108+
viewModel.livenessService = self.livenessService
109+
110+
viewModel.livenessState.checkIsFacePrepared()
111+
XCTAssertEqual(viewModel.livenessState.state, .pendingFacePreparedConfirmation(.pendingCheck))
112+
XCTAssertEqual(faceDetector.interactions, [
113+
"setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)"
114+
])
115+
XCTAssertEqual(livenessService.interactions, [])
116+
117+
let boundingBox = CGRect(x: 0.26788579725878847, y: 0.40317180752754211, width: 0.45549795395626447, height: 0.34162446856498718)
118+
let leftEye = CGPoint(x: 0.61124476128552629, y: 0.4918237030506134)
119+
let rightEye = CGPoint(x: 0.38036393762719456, y: 0.48050540685653687)
120+
let nose = CGPoint(x: 0.48489856674964926, y: 0.54713362455368042)
121+
let mouth = CGPoint(x: 0.47411978167652435, y: 0.63170802593231201)
122+
let detectedFace = DetectedFace(boundingBox: boundingBox, leftEye: leftEye, rightEye: rightEye, nose: nose, mouth: mouth, confidence: 0.971859633)
123+
viewModel.process(newResult: .singleFace(detectedFace))
124+
try await Task.sleep(seconds: 1)
125+
126+
XCTAssertEqual(viewModel.livenessState.state, .recording(ovalDisplayed: false))
127+
XCTAssertEqual(faceDetector.interactions, [
128+
"setResultHandler(detectionResultHandler:) (FaceLivenessDetectionViewModel)"
129+
])
130+
XCTAssertEqual(livenessService.interactions, [
131+
"initializeLivenessStream(withSessionID:userAgent:)"
132+
])
133+
}
103134
}

0 commit comments

Comments
 (0)