Skip to content

Commit ce74064

Browse files
authored
chore: provide more fine-grained websocket close code on errors (#44)
* chore: improve websocket close code * chore: update web socket close code * chore: update unit test * chore: move websocket close code to liveness error * chore: code cleanup
1 parent adf29ad commit ce74064

File tree

5 files changed

+64
-34
lines changed

5 files changed

+64
-34
lines changed

Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ public struct FaceLivenessDetectorView: View {
170170
isPresented = false
171171
onCompletion(.success(()))
172172
case .encounteredUnrecoverableError(let error):
173-
viewModel.livenessService.closeSocket(with: .normalClosure)
173+
let closeCode = error.webSocketCloseCode ?? .normalClosure
174+
viewModel.livenessService.closeSocket(with: closeCode)
174175
isPresented = false
175176
onCompletion(.failure(mapError(error)))
176177
default:
@@ -188,8 +189,6 @@ public struct FaceLivenessDetectorView: View {
188189
return .sessionTimedOut
189190
case .socketClosed:
190191
return .socketClosed
191-
case .invalidFaceMovementDuringCountdown:
192-
return .countdownFaceTooClose
193192
default:
194193
return .cameraPermissionDenied
195194
}
@@ -228,12 +227,6 @@ public struct FaceLivenessDetectorView: View {
228227
}
229228
}
230229

231-
enum CountdownDisplayState {
232-
case waitingToDisplay
233-
case displaying
234-
case finishedDisplaying
235-
}
236-
237230
enum DisplayState {
238231
case awaitingLivenessSession
239232
case displayingGetReadyView

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

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

1212
fileprivate let initialFaceDistanceThreshold: CGFloat = 0.32
13+
fileprivate let noFitTimeoutInterval: TimeInterval = 7
1314

1415
extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
1516
func process(newResult: FaceDetectionResult) {
@@ -83,17 +84,26 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
8384
}
8485
}
8586

86-
func handleNoMatch(instruction: Instructor.Instruction, percentage: Double) {
87-
let noMatchTimeoutInterval: TimeInterval = 7
87+
func handleNoFaceFit(instruction: Instructor.Instruction, percentage: Double) {
8888
self.livenessState.awaitingFaceMatch(with: instruction, nearnessPercentage: percentage)
89-
if noMatchStartTime == nil {
90-
noMatchStartTime = Date()
89+
if noFitStartTime == nil {
90+
noFitStartTime = Date()
9191
}
92-
if let elapsedTime = noMatchStartTime?.timeIntervalSinceNow, abs(elapsedTime) >= noMatchTimeoutInterval {
92+
if let elapsedTime = noFitStartTime?.timeIntervalSinceNow, abs(elapsedTime) >= noFitTimeoutInterval {
93+
self.livenessState
94+
.unrecoverableStateEncountered(.timedOut)
95+
self.captureSession.stopRunning()
96+
}
97+
}
98+
99+
func handleNoFaceDetected() {
100+
if noFitStartTime == nil {
101+
noFitStartTime = Date()
102+
}
103+
if let elapsedTime = noFitStartTime?.timeIntervalSinceNow, abs(elapsedTime) >= noFitTimeoutInterval {
93104
self.livenessState
94105
.unrecoverableStateEncountered(.timedOut)
95106
self.captureSession.stopRunning()
96-
return
97107
}
98108
}
99109

@@ -109,14 +119,15 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
109119
self.livenessViewControllerDelegate?.displayFreshness(colorSequences: colorSequences)
110120
let generator = UINotificationFeedbackGenerator()
111121
generator.notificationOccurred(.success)
112-
self.noMatchStartTime = nil
122+
self.noFitStartTime = nil
113123

114124
case .tooClose(_, let percentage),
115125
.tooFar(_, let percentage),
116126
.tooFarLeft(_, let percentage),
117127
.tooFarRight(_, let percentage):
118-
self.handleNoMatch(instruction: instruction, percentage: percentage)
119-
default: break
128+
self.handleNoFaceFit(instruction: instruction, percentage: percentage)
129+
case .none:
130+
self.handleNoFaceDetected()
120131
}
121132
}
122133
}

Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class FaceLivenessDetectionViewModel: ObservableObject {
3939
var faceGuideRect: CGRect!
4040
var initialClientEvent: InitialClientEvent?
4141
var faceMatchedTimestamp: UInt64?
42-
var noMatchStartTime: Date?
42+
var noFitStartTime: Date?
4343

4444
init(
4545
faceDetector: FaceDetector,
@@ -108,7 +108,7 @@ class FaceLivenessDetectionViewModel: ObservableObject {
108108
@objc func willResignActive(_ notification: Notification) {
109109
DispatchQueue.main.async {
110110
self.stopRecording()
111-
self.livenessState.unrecoverableStateEncountered(.socketClosed)
111+
self.livenessState.unrecoverableStateEncountered(.viewResignation)
112112
}
113113
}
114114

Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,28 @@ struct LivenessStateMachine {
124124

125125
struct LivenessError: Error, Equatable {
126126
let code: UInt8
127-
128-
static let unknown = LivenessError(code: 0)
129-
static let missingVideoPermission = LivenessError(code: 1)
130-
static let errorWithUnderlyingOSFramework = LivenessError(code: 2)
131-
static let userCancelled = LivenessError(code: 3)
132-
static let timedOut = LivenessError(code: 4)
133-
static let couldNotOpenStream = LivenessError(code: 5)
134-
static let socketClosed = LivenessError(code: 6)
135-
static let invalidFaceMovementDuringCountdown = LivenessError(code: 7)
127+
let webSocketCloseCode: URLSessionWebSocketTask.CloseCode?
128+
129+
static let unknown = LivenessError(code: 0, webSocketCloseCode: .unexpectedRuntimeError)
130+
static let missingVideoPermission = LivenessError(code: 1, webSocketCloseCode: .missingVideoPermission)
131+
static let errorWithUnderlyingOSFramework = LivenessError(code: 2, webSocketCloseCode: .unexpectedRuntimeError)
132+
static let userCancelled = LivenessError(code: 3, webSocketCloseCode: .ovalFitUserClosedSession)
133+
static let timedOut = LivenessError(code: 4, webSocketCloseCode: .ovalFitMatchTimeout)
134+
static let couldNotOpenStream = LivenessError(code: 5, webSocketCloseCode: .unexpectedRuntimeError)
135+
static let socketClosed = LivenessError(code: 6, webSocketCloseCode: .normalClosure)
136+
static let viewResignation = LivenessError(code: 8, webSocketCloseCode: .viewClosure)
136137

137138
static func == (lhs: LivenessError, rhs: LivenessError) -> Bool {
138139
lhs.code == rhs.code
139140
}
140141
}
141142
}
143+
144+
extension URLSessionWebSocketTask.CloseCode {
145+
static let ovalFitMatchTimeout = URLSessionWebSocketTask.CloseCode(rawValue: 4001)
146+
static let ovalFitTimeOutNoFaceDetected = URLSessionWebSocketTask.CloseCode(rawValue: 4002)
147+
static let ovalFitUserClosedSession = URLSessionWebSocketTask.CloseCode(rawValue: 4003)
148+
static let viewClosure = URLSessionWebSocketTask.CloseCode(rawValue: 4004)
149+
static let unexpectedRuntimeError = URLSessionWebSocketTask.CloseCode(rawValue: 4005)
150+
static let missingVideoPermission = URLSessionWebSocketTask.CloseCode(rawValue: 4006)
151+
}

Tests/FaceLivenessTests/LivenessTests.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,18 +140,34 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
140140
}
141141

142142
/// Given: A `FaceLivenessDetectionViewModel`
143-
/// When: The viewModel handles a no match event over a duration of 7 seconds
143+
/// When: The viewModel handles a no fit event over a duration of 7 seconds
144144
/// Then: The end state is `.encounteredUnrecoverableError(.timedOut)`
145-
func testNoMatchTimeoutCheck() async throws {
145+
func testNoFitTimeoutCheck() async throws {
146146
viewModel.livenessService = self.livenessService
147-
self.viewModel.handleNoMatch(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
147+
self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
148148

149149
XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
150150
try await Task.sleep(seconds: 6)
151-
self.viewModel.handleNoMatch(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
151+
self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
152152
XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
153153
try await Task.sleep(seconds: 1)
154-
self.viewModel.handleNoMatch(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
154+
self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
155+
XCTAssertEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
156+
}
157+
158+
/// Given: A `FaceLivenessDetectionViewModel`
159+
/// When: The viewModel handles a no face detected event over a duration of 7 seconds
160+
/// Then: The end state is `.encounteredUnrecoverableError(.timedOut)`
161+
func testNoFaceDetectedTimeoutCheck() async throws {
162+
viewModel.livenessService = self.livenessService
163+
self.viewModel.handleNoFaceDetected()
164+
165+
XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
166+
try await Task.sleep(seconds: 6)
167+
self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
168+
XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
169+
try await Task.sleep(seconds: 1)
170+
self.viewModel.handleNoFaceDetected()
155171
XCTAssertEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
156172
}
157173
}

0 commit comments

Comments
 (0)