Skip to content

Commit

Permalink
chore: provide more fine-grained websocket close code on errors (#44)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
phantumcode authored Aug 10, 2023
1 parent adf29ad commit ce74064
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ public struct FaceLivenessDetectorView: View {
isPresented = false
onCompletion(.success(()))
case .encounteredUnrecoverableError(let error):
viewModel.livenessService.closeSocket(with: .normalClosure)
let closeCode = error.webSocketCloseCode ?? .normalClosure
viewModel.livenessService.closeSocket(with: closeCode)
isPresented = false
onCompletion(.failure(mapError(error)))
default:
Expand All @@ -188,8 +189,6 @@ public struct FaceLivenessDetectorView: View {
return .sessionTimedOut
case .socketClosed:
return .socketClosed
case .invalidFaceMovementDuringCountdown:
return .countdownFaceTooClose
default:
return .cameraPermissionDenied
}
Expand Down Expand Up @@ -228,12 +227,6 @@ public struct FaceLivenessDetectorView: View {
}
}

enum CountdownDisplayState {
case waitingToDisplay
case displaying
case finishedDisplaying
}

enum DisplayState {
case awaitingLivenessSession
case displayingGetReadyView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftUI
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

fileprivate let initialFaceDistanceThreshold: CGFloat = 0.32
fileprivate let noFitTimeoutInterval: TimeInterval = 7

extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
func process(newResult: FaceDetectionResult) {
Expand Down Expand Up @@ -83,17 +84,26 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
}
}

func handleNoMatch(instruction: Instructor.Instruction, percentage: Double) {
let noMatchTimeoutInterval: TimeInterval = 7
func handleNoFaceFit(instruction: Instructor.Instruction, percentage: Double) {
self.livenessState.awaitingFaceMatch(with: instruction, nearnessPercentage: percentage)
if noMatchStartTime == nil {
noMatchStartTime = Date()
if noFitStartTime == nil {
noFitStartTime = Date()
}
if let elapsedTime = noMatchStartTime?.timeIntervalSinceNow, abs(elapsedTime) >= noMatchTimeoutInterval {
if let elapsedTime = noFitStartTime?.timeIntervalSinceNow, abs(elapsedTime) >= noFitTimeoutInterval {
self.livenessState
.unrecoverableStateEncountered(.timedOut)
self.captureSession.stopRunning()
}
}

func handleNoFaceDetected() {
if noFitStartTime == nil {
noFitStartTime = Date()
}
if let elapsedTime = noFitStartTime?.timeIntervalSinceNow, abs(elapsedTime) >= noFitTimeoutInterval {
self.livenessState
.unrecoverableStateEncountered(.timedOut)
self.captureSession.stopRunning()
return
}
}

Expand All @@ -109,14 +119,15 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
self.livenessViewControllerDelegate?.displayFreshness(colorSequences: colorSequences)
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
self.noMatchStartTime = nil
self.noFitStartTime = nil

case .tooClose(_, let percentage),
.tooFar(_, let percentage),
.tooFarLeft(_, let percentage),
.tooFarRight(_, let percentage):
self.handleNoMatch(instruction: instruction, percentage: percentage)
default: break
self.handleNoFaceFit(instruction: instruction, percentage: percentage)
case .none:
self.handleNoFaceDetected()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class FaceLivenessDetectionViewModel: ObservableObject {
var faceGuideRect: CGRect!
var initialClientEvent: InitialClientEvent?
var faceMatchedTimestamp: UInt64?
var noMatchStartTime: Date?
var noFitStartTime: Date?

init(
faceDetector: FaceDetector,
Expand Down Expand Up @@ -108,7 +108,7 @@ class FaceLivenessDetectionViewModel: ObservableObject {
@objc func willResignActive(_ notification: Notification) {
DispatchQueue.main.async {
self.stopRecording()
self.livenessState.unrecoverableStateEncountered(.socketClosed)
self.livenessState.unrecoverableStateEncountered(.viewResignation)
}
}

Expand Down
28 changes: 19 additions & 9 deletions Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,28 @@ struct LivenessStateMachine {

struct LivenessError: Error, Equatable {
let code: UInt8

static let unknown = LivenessError(code: 0)
static let missingVideoPermission = LivenessError(code: 1)
static let errorWithUnderlyingOSFramework = LivenessError(code: 2)
static let userCancelled = LivenessError(code: 3)
static let timedOut = LivenessError(code: 4)
static let couldNotOpenStream = LivenessError(code: 5)
static let socketClosed = LivenessError(code: 6)
static let invalidFaceMovementDuringCountdown = LivenessError(code: 7)
let webSocketCloseCode: URLSessionWebSocketTask.CloseCode?

static let unknown = LivenessError(code: 0, webSocketCloseCode: .unexpectedRuntimeError)
static let missingVideoPermission = LivenessError(code: 1, webSocketCloseCode: .missingVideoPermission)
static let errorWithUnderlyingOSFramework = LivenessError(code: 2, webSocketCloseCode: .unexpectedRuntimeError)
static let userCancelled = LivenessError(code: 3, webSocketCloseCode: .ovalFitUserClosedSession)
static let timedOut = LivenessError(code: 4, webSocketCloseCode: .ovalFitMatchTimeout)
static let couldNotOpenStream = LivenessError(code: 5, webSocketCloseCode: .unexpectedRuntimeError)
static let socketClosed = LivenessError(code: 6, webSocketCloseCode: .normalClosure)
static let viewResignation = LivenessError(code: 8, webSocketCloseCode: .viewClosure)

static func == (lhs: LivenessError, rhs: LivenessError) -> Bool {
lhs.code == rhs.code
}
}
}

extension URLSessionWebSocketTask.CloseCode {
static let ovalFitMatchTimeout = URLSessionWebSocketTask.CloseCode(rawValue: 4001)
static let ovalFitTimeOutNoFaceDetected = URLSessionWebSocketTask.CloseCode(rawValue: 4002)
static let ovalFitUserClosedSession = URLSessionWebSocketTask.CloseCode(rawValue: 4003)
static let viewClosure = URLSessionWebSocketTask.CloseCode(rawValue: 4004)
static let unexpectedRuntimeError = URLSessionWebSocketTask.CloseCode(rawValue: 4005)
static let missingVideoPermission = URLSessionWebSocketTask.CloseCode(rawValue: 4006)
}
26 changes: 21 additions & 5 deletions Tests/FaceLivenessTests/LivenessTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,34 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
}

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

XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
try await Task.sleep(seconds: 6)
self.viewModel.handleNoMatch(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
try await Task.sleep(seconds: 1)
self.viewModel.handleNoMatch(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
XCTAssertEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
}

/// Given: A `FaceLivenessDetectionViewModel`
/// When: The viewModel handles a no face detected event over a duration of 7 seconds
/// Then: The end state is `.encounteredUnrecoverableError(.timedOut)`
func testNoFaceDetectedTimeoutCheck() async throws {
viewModel.livenessService = self.livenessService
self.viewModel.handleNoFaceDetected()

XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
try await Task.sleep(seconds: 6)
self.viewModel.handleNoFaceFit(instruction: .tooFar(nearnessPercentage: 0.2), percentage: 0.2)
XCTAssertNotEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
try await Task.sleep(seconds: 1)
self.viewModel.handleNoFaceDetected()
XCTAssertEqual(self.viewModel.livenessState.state, .encounteredUnrecoverableError(.timedOut))
}
}

0 comments on commit ce74064

Please sign in to comment.