From d66363f86c5c3dc5772beba1fd6cf948a02b7e37 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Wed, 24 Jul 2024 10:48:09 -0700 Subject: [PATCH 1/9] chore: back camera support --- .../HostApp/Views/ExampleLivenessView.swift | 1 + .../AV/LivenessCaptureDevice.swift | 5 +++++ .../GetReadyPage/CameraPreviewView.swift | 2 +- .../GetReadyPage/CameraPreviewViewModel.swift | 7 +++++-- .../Views/GetReadyPage/GetReadyPageView.swift | 10 +++++++--- .../Liveness/FaceLivenessDetectionView.swift | 20 ++++++++++++------- .../CredentialsProviderTestCase.swift | 2 ++ 7 files changed, 34 insertions(+), 13 deletions(-) diff --git a/HostApp/HostApp/Views/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift index 5f6868b6..dff13890 100644 --- a/HostApp/HostApp/Views/ExampleLivenessView.swift +++ b/HostApp/HostApp/Views/ExampleLivenessView.swift @@ -23,6 +23,7 @@ struct ExampleLivenessView: View { FaceLivenessDetectorView( sessionID: viewModel.sessionID, region: "us-east-1", + cameraPosition: .front, isPresented: Binding( get: { viewModel.presentationState == .liveness }, set: { _ in } diff --git a/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift b/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift index 1d67e913..3935459e 100644 --- a/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift +++ b/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift @@ -37,3 +37,8 @@ struct LivenessCaptureDevice { } } } + +public enum LivenessCaptureDevicePosition { + case front + case back +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift index 2e8530f3..19e2f483 100644 --- a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift +++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift @@ -15,7 +15,7 @@ struct CameraPreviewView: View { @StateObject var model: CameraPreviewViewModel - init(model: CameraPreviewViewModel = CameraPreviewViewModel()) { + init(model: CameraPreviewViewModel = CameraPreviewViewModel(cameraPosition: .front)) { self._model = StateObject(wrappedValue: model) } diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift index b50173b0..f40645b5 100644 --- a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift +++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift @@ -16,15 +16,18 @@ class CameraPreviewViewModel: NSObject, ObservableObject { @Published var buffer: CVPixelBuffer? var previewCaptureSession: LivenessCaptureSession? + let cameraPosition: LivenessCaptureDevicePosition - override init() { + init(cameraPosition: LivenessCaptureDevicePosition) { + self.cameraPosition = cameraPosition + super.init() setupSubscriptions() let avCaptureDevice = AVCaptureDevice.DiscoverySession( deviceTypes: [.builtInWideAngleCamera], mediaType: .video, - position: .front + position: cameraPosition == .front ? .front : .back ).devices.first let outputDelegate = CameraPreviewOutputSampleBufferDelegate { [weak self] buffer in diff --git a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift index 0c52ccff..d5a87b5b 100644 --- a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift +++ b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift @@ -12,21 +12,24 @@ struct GetReadyPageView: View { let beginCheckButtonDisabled: Bool let onBegin: () -> Void let challenge: Challenge + let cameraPosition: LivenessCaptureDevicePosition init( onBegin: @escaping () -> Void, beginCheckButtonDisabled: Bool = false, - challenge: Challenge + challenge: Challenge, + cameraPosition: LivenessCaptureDevicePosition ) { self.onBegin = onBegin self.beginCheckButtonDisabled = beginCheckButtonDisabled self.challenge = challenge + self.cameraPosition = cameraPosition } var body: some View { VStack { ZStack { - CameraPreviewView() + CameraPreviewView(model: CameraPreviewViewModel(cameraPosition: cameraPosition)) VStack { WarningBox( titleText: LocalizedStrings.get_ready_photosensitivity_title, @@ -79,6 +82,7 @@ struct GetReadyPageView_Previews: PreviewProvider { static var previews: some View { GetReadyPageView(onBegin: {}, challenge: .init(version: "2.0.0", - type: .faceMovementAndLightChallenge)) + type: .faceMovementAndLightChallenge), + cameraPosition: .front) } } diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift index f803a863..a38f54bd 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift @@ -20,6 +20,7 @@ public struct FaceLivenessDetectorView: View { @State var displayingCameraPermissionsNeededAlert = false let disableStartView: Bool + let cameraPosition: LivenessCaptureDevicePosition let onCompletion: (Result) -> Void let sessionTask: Task @@ -29,11 +30,13 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, + cameraPosition: LivenessCaptureDevicePosition, isPresented: Binding, onCompletion: @escaping (Result) -> Void ) { self.disableStartView = disableStartView self._isPresented = isPresented + self.cameraPosition = cameraPosition self.onCompletion = onCompletion self.sessionTask = Task { @@ -60,7 +63,7 @@ public struct FaceLivenessDetectorView: View { let avCpatureDevice = AVCaptureDevice.DiscoverySession( deviceTypes: [.builtInWideAngleCamera], mediaType: .video, - position: .front + position: cameraPosition == .front ? .front : .back ).devices.first let captureSession = LivenessCaptureSession( @@ -89,6 +92,7 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, + cameraPosition: LivenessCaptureDevicePosition, isPresented: Binding, onCompletion: @escaping (Result) -> Void, captureSession: LivenessCaptureSession @@ -96,6 +100,7 @@ public struct FaceLivenessDetectorView: View { self.disableStartView = disableStartView self._isPresented = isPresented self.onCompletion = onCompletion + self.cameraPosition = cameraPosition self.sessionTask = Task { let session = try await AWSPredictionsPlugin.startFaceLivenessSession( @@ -166,21 +171,22 @@ public struct FaceLivenessDetectorView: View { do { let newState = disableStartView ? DisplayState.displayingLiveness - : DisplayState.displayingGetReadyView(challenge) + : DisplayState.displayingGetReadyView(challenge, cameraPosition) guard self.displayState != newState else { return } self.displayState = newState } } } - case .displayingGetReadyView(let challenge): + case .displayingGetReadyView(let challenge, let cameraPosition): GetReadyPageView( onBegin: { guard displayState != .displayingLiveness else { return } displayState = .displayingLiveness }, beginCheckButtonDisabled: false, - challenge: challenge + challenge: challenge, + cameraPosition: cameraPosition ) .onAppear { DispatchQueue.main.async { @@ -276,7 +282,7 @@ public struct FaceLivenessDetectorView: View { enum DisplayState: Equatable { case awaitingChallengeType case awaitingLivenessSession(Challenge) - case displayingGetReadyView(Challenge) + case displayingGetReadyView(Challenge, LivenessCaptureDevicePosition) case displayingLiveness case awaitingCameraPermission @@ -286,8 +292,8 @@ enum DisplayState: Equatable { return true case (let .awaitingLivenessSession(c1), let .awaitingLivenessSession(c2)): return c1.type == c2.type && c1.version == c2.version - case (let .displayingGetReadyView(c1), let .displayingGetReadyView(c2)): - return c1.type == c2.type && c1.version == c2.version + case (let .displayingGetReadyView(c1, position1), let .displayingGetReadyView(c2, position2)): + return c1.type == c2.type && c1.version == c2.version && position1 == position2 case (.displayingLiveness, .displayingLiveness): return true case (.awaitingCameraPermission, .awaitingCameraPermission): diff --git a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift index 3c1dabbf..59eb19eb 100644 --- a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift +++ b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift @@ -66,6 +66,7 @@ final class CredentialsProviderTestCase: XCTestCase { sessionID: UUID().uuidString, credentialsProvider: credentialsProvider, region: "us-east-1", + cameraPosition: .front, isPresented: .constant(true), onCompletion: { _ in } ) @@ -102,6 +103,7 @@ final class CredentialsProviderTestCase: XCTestCase { sessionID: UUID().uuidString, credentialsProvider: credentialsProvider, region: "us-east-1", + cameraPosition: .front, isPresented: .constant(true), onCompletion: { _ in } ) From 985b67cde83d280460e5def3798724707af22648 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Thu, 22 Aug 2024 12:01:14 -0700 Subject: [PATCH 2/9] Add code for error scenarios --- HostApp/HostApp.xcodeproj/project.pbxproj | 10 +++++++++ .../HostApp/Views/ExampleLivenessView.swift | 2 +- .../Liveness/FaceLivenessDetectionError.swift | 6 ++++++ .../Liveness/FaceLivenessDetectionView.swift | 21 ++++++++++--------- .../FaceLivenessDetectionViewModel.swift | 18 ++++++++++++++-- .../CredentialsProviderTestCase.swift | 3 ++- Tests/FaceLivenessTests/LivenessTests.swift | 3 ++- 7 files changed, 48 insertions(+), 15 deletions(-) diff --git a/HostApp/HostApp.xcodeproj/project.pbxproj b/HostApp/HostApp.xcodeproj/project.pbxproj index 7d1314c5..76052c19 100644 --- a/HostApp/HostApp.xcodeproj/project.pbxproj +++ b/HostApp/HostApp.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ 9070FFBD285112B5009867D5 /* HostAppUITests */, 9070FFA1285112B4009867D5 /* Products */, 90215EED291E9FB60050F2AD /* Frameworks */, + A5A9AF5054D0FF13505B212A /* AmplifyConfig */, ); sourceTree = ""; }; @@ -213,6 +214,15 @@ path = Model; sourceTree = ""; }; + A5A9AF5054D0FF13505B212A /* AmplifyConfig */ = { + isa = PBXGroup; + children = ( + 973619242BA378690003A590 /* awsconfiguration.json */, + 973619232BA378690003A590 /* amplifyconfiguration.json */, + ); + name = AmplifyConfig; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ diff --git a/HostApp/HostApp/Views/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift index dff13890..e27ebc68 100644 --- a/HostApp/HostApp/Views/ExampleLivenessView.swift +++ b/HostApp/HostApp/Views/ExampleLivenessView.swift @@ -23,7 +23,7 @@ struct ExampleLivenessView: View { FaceLivenessDetectorView( sessionID: viewModel.sessionID, region: "us-east-1", - cameraPosition: .front, + cameraPosition: .back, isPresented: Binding( get: { viewModel.presentationState == .liveness }, set: { _ in } diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift index e90a6f06..f24afd5b 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift @@ -125,6 +125,12 @@ public struct FaceLivenessDetectionError: Error, Equatable { message: "The signature on the request is invalid.", recoverySuggestion: "Ensure the device time is correct and try again." ) + + public static let invalidCameraPositionSelected = FaceLivenessDetectionError( + code: 18, + message: "The camera position selected is incompatible with the liveness challenge type requested.", + recoverySuggestion: "Please ensure the camera position is supported for the liveness challenge type requested." + ) public static let cameraNotAvailable = FaceLivenessDetectionError( code: 18, diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift index a38f54bd..44c98285 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift @@ -30,7 +30,7 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, - cameraPosition: LivenessCaptureDevicePosition, + cameraPosition: LivenessCaptureDevicePosition = .front, isPresented: Binding, onCompletion: @escaping (Result) -> Void ) { @@ -59,15 +59,14 @@ public struct FaceLivenessDetectorView: View { assetWriterDelegate: VideoChunker.AssetWriterDelegate(), assetWriterInput: LivenessAVAssetWriterInput() ) - - let avCpatureDevice = AVCaptureDevice.DiscoverySession( - deviceTypes: [.builtInWideAngleCamera], - mediaType: .video, - position: cameraPosition == .front ? .front : .back - ).devices.first + + let avCaptureDevice = AVCaptureDevice.default( + .builtInWideAngleCamera, + for: .video, + position: cameraPosition == .front ? .front : .back) let captureSession = LivenessCaptureSession( - captureDevice: .init(avCaptureDevice: avCpatureDevice), + captureDevice: .init(avCaptureDevice: avCaptureDevice), outputDelegate: OutputSampleBufferCapturer( faceDetector: faceDetector, videoChunker: videoChunker @@ -82,7 +81,8 @@ public struct FaceLivenessDetectorView: View { videoChunker: videoChunker, closeButtonAction: { onCompletion(.failure(.userCancelled)) }, sessionID: sessionID, - isPreviewScreenEnabled: !disableStartView + isPreviewScreenEnabled: !disableStartView, + cameraPosition: cameraPosition ) ) } @@ -124,7 +124,8 @@ public struct FaceLivenessDetectorView: View { videoChunker: captureSession.outputSampleBufferCapturer!.videoChunker, closeButtonAction: { onCompletion(.failure(.userCancelled)) }, sessionID: sessionID, - isPreviewScreenEnabled: !disableStartView + isPreviewScreenEnabled: !disableStartView, + cameraPosition: cameraPosition ) ) } diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift index 42e7149a..d660c0c9 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift @@ -44,6 +44,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { var initialClientEvent: InitialClientEvent? var faceMatchedTimestamp: UInt64? var noFitStartTime: Date? + let cameraPosition: LivenessCaptureDevicePosition static var attemptCount: Int = 0 static var attemptIdTimeStamp: Date = Date() @@ -64,7 +65,8 @@ class FaceLivenessDetectionViewModel: ObservableObject { stateMachine: LivenessStateMachine = .init(state: .initial), closeButtonAction: @escaping () -> Void, sessionID: String, - isPreviewScreenEnabled: Bool + isPreviewScreenEnabled: Bool, + cameraPosition: LivenessCaptureDevicePosition ) { self.closeButtonAction = closeButtonAction self.videoChunker = videoChunker @@ -74,6 +76,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { self.faceDetector = faceDetector self.faceInOvalMatching = faceInOvalMatching self.isPreviewScreenEnabled = isPreviewScreenEnabled + self.cameraPosition = cameraPosition self.closeButtonAction = { [weak self] in guard let self else { return } @@ -124,7 +127,18 @@ class FaceLivenessDetectionViewModel: ObservableObject { livenessService?.register( listener: { [weak self] _challenge in self?.challenge = _challenge - onChallengeTypeReceived(_challenge) + guard _challenge.type == .faceMovementAndLightChallenge, + self?.cameraPosition == .back else { + onChallengeTypeReceived(_challenge) + return + } + + // incompatible camera position with challenge type + // return error + DispatchQueue.main.async { + self?.livenessState + .unrecoverableStateEncountered(.invalidCameraPositionSelecteed) + } }, on: .challenge) } diff --git a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift index 59eb19eb..5b82119a 100644 --- a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift +++ b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift @@ -42,7 +42,8 @@ final class CredentialsProviderTestCase: XCTestCase { videoChunker: videoChunker, closeButtonAction: {}, sessionID: UUID().uuidString, - isPreviewScreenEnabled: false + isPreviewScreenEnabled: false, + cameraPosition: .front ) self.videoChunker = videoChunker diff --git a/Tests/FaceLivenessTests/LivenessTests.swift b/Tests/FaceLivenessTests/LivenessTests.swift index 5603914a..45b090c4 100644 --- a/Tests/FaceLivenessTests/LivenessTests.swift +++ b/Tests/FaceLivenessTests/LivenessTests.swift @@ -33,7 +33,8 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase { videoChunker: videoChunker, closeButtonAction: {}, sessionID: UUID().uuidString, - isPreviewScreenEnabled: false + isPreviewScreenEnabled: false, + cameraPosition: .front ) self.videoChunker = videoChunker From e49baf403802401c0b490111a5e3c853dd7b25c9 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Thu, 22 Aug 2024 15:00:23 -0700 Subject: [PATCH 3/9] update error codes and message --- .../Liveness/FaceLivenessDetectionError.swift | 14 +++++++------- .../Views/Liveness/LivenessStateMachine.swift | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift index f24afd5b..5a626572 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift @@ -126,16 +126,16 @@ public struct FaceLivenessDetectionError: Error, Equatable { recoverySuggestion: "Ensure the device time is correct and try again." ) - public static let invalidCameraPositionSelected = FaceLivenessDetectionError( - code: 18, - message: "The camera position selected is incompatible with the liveness challenge type requested.", - recoverySuggestion: "Please ensure the camera position is supported for the liveness challenge type requested." - ) - public static let cameraNotAvailable = FaceLivenessDetectionError( code: 18, message: "The camera is not available.", - recoverySuggestion: "There might be a hardware issue." + recoverySuggestion: "There might be a hardware issue or the selected camera is not available." + ) + + public static let invalidCameraPositionSelected = FaceLivenessDetectionError( + code: 19, + message: "The camera position selected is incompatible with the liveness challenge type requested.", + recoverySuggestion: "Please ensure the camera position is supported for the liveness challenge type requested." ) public static func == (lhs: FaceLivenessDetectionError, rhs: FaceLivenessDetectionError) -> Bool { diff --git a/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift index e61f8311..5df7ec63 100644 --- a/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift +++ b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift @@ -165,7 +165,8 @@ struct LivenessStateMachine { 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 let cameraNotAvailable = LivenessError(code: 9, webSocketCloseCode: .missingVideoPermission) + static let cameraNotAvailable = LivenessError(code: 9, webSocketCloseCode: .unexpectedRuntimeError) + static let invalidCameraPositionSelecteed = LivenessError(code: 10, webSocketCloseCode: .unexpectedRuntimeError) static func == (lhs: LivenessError, rhs: LivenessError) -> Bool { lhs.code == rhs.code From db4e7e044831beb0008308652badb016b573b003 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Fri, 30 Aug 2024 12:39:59 -0700 Subject: [PATCH 4/9] Add challengeOption parameter and remove error codes --- .../HostApp/Views/ExampleLivenessView.swift | 2 +- .../AV/LivenessCaptureDevice.swift | 5 -- .../GetReadyPage/CameraPreviewViewModel.swift | 4 +- .../Views/GetReadyPage/GetReadyPageView.swift | 4 +- .../Liveness/FaceLivenessDetectionError.swift | 8 +--- .../Liveness/FaceLivenessDetectionView.swift | 48 +++++++++++++++---- .../FaceLivenessDetectionViewModel.swift | 22 +++------ .../Views/Liveness/LivenessStateMachine.swift | 1 - 8 files changed, 53 insertions(+), 41 deletions(-) diff --git a/HostApp/HostApp/Views/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift index e27ebc68..656b86a0 100644 --- a/HostApp/HostApp/Views/ExampleLivenessView.swift +++ b/HostApp/HostApp/Views/ExampleLivenessView.swift @@ -23,7 +23,7 @@ struct ExampleLivenessView: View { FaceLivenessDetectorView( sessionID: viewModel.sessionID, region: "us-east-1", - cameraPosition: .back, + challengeOption: .faceMovementAndLightChallenge, isPresented: Binding( get: { viewModel.presentationState == .liveness }, set: { _ in } diff --git a/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift b/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift index 3935459e..1d67e913 100644 --- a/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift +++ b/Sources/FaceLiveness/AV/LivenessCaptureDevice.swift @@ -37,8 +37,3 @@ struct LivenessCaptureDevice { } } } - -public enum LivenessCaptureDevicePosition { - case front - case back -} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift index f40645b5..a46dbaa8 100644 --- a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift +++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift @@ -16,9 +16,9 @@ class CameraPreviewViewModel: NSObject, ObservableObject { @Published var buffer: CVPixelBuffer? var previewCaptureSession: LivenessCaptureSession? - let cameraPosition: LivenessCaptureDevicePosition + let cameraPosition: LivenessCamera - init(cameraPosition: LivenessCaptureDevicePosition) { + init(cameraPosition: LivenessCamera) { self.cameraPosition = cameraPosition super.init() diff --git a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift index d5a87b5b..806aa25e 100644 --- a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift +++ b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift @@ -12,13 +12,13 @@ struct GetReadyPageView: View { let beginCheckButtonDisabled: Bool let onBegin: () -> Void let challenge: Challenge - let cameraPosition: LivenessCaptureDevicePosition + let cameraPosition: LivenessCamera init( onBegin: @escaping () -> Void, beginCheckButtonDisabled: Bool = false, challenge: Challenge, - cameraPosition: LivenessCaptureDevicePosition + cameraPosition: LivenessCamera ) { self.onBegin = onBegin self.beginCheckButtonDisabled = beginCheckButtonDisabled diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift index 5a626572..19ced079 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift @@ -129,13 +129,7 @@ public struct FaceLivenessDetectionError: Error, Equatable { public static let cameraNotAvailable = FaceLivenessDetectionError( code: 18, message: "The camera is not available.", - recoverySuggestion: "There might be a hardware issue or the selected camera is not available." - ) - - public static let invalidCameraPositionSelected = FaceLivenessDetectionError( - code: 19, - message: "The camera position selected is incompatible with the liveness challenge type requested.", - recoverySuggestion: "Please ensure the camera position is supported for the liveness challenge type requested." + recoverySuggestion: "There might be a hardware issue." ) public static func == (lhs: FaceLivenessDetectionError, rhs: FaceLivenessDetectionError) -> Bool { diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift index 44c98285..af975ec6 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift @@ -20,7 +20,7 @@ public struct FaceLivenessDetectorView: View { @State var displayingCameraPermissionsNeededAlert = false let disableStartView: Bool - let cameraPosition: LivenessCaptureDevicePosition + let cameraPosition: LivenessCamera let onCompletion: (Result) -> Void let sessionTask: Task @@ -30,13 +30,13 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, - cameraPosition: LivenessCaptureDevicePosition = .front, + challengeOption: ChallengeOption, isPresented: Binding, onCompletion: @escaping (Result) -> Void ) { self.disableStartView = disableStartView self._isPresented = isPresented - self.cameraPosition = cameraPosition + self.cameraPosition = challengeOption.camera self.onCompletion = onCompletion self.sessionTask = Task { @@ -82,7 +82,8 @@ public struct FaceLivenessDetectorView: View { closeButtonAction: { onCompletion(.failure(.userCancelled)) }, sessionID: sessionID, isPreviewScreenEnabled: !disableStartView, - cameraPosition: cameraPosition + cameraPosition: challengeOption.camera, + challengeOption: challengeOption ) ) } @@ -92,7 +93,7 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, - cameraPosition: LivenessCaptureDevicePosition, + challengeOption: ChallengeOption, isPresented: Binding, onCompletion: @escaping (Result) -> Void, captureSession: LivenessCaptureSession @@ -100,7 +101,7 @@ public struct FaceLivenessDetectorView: View { self.disableStartView = disableStartView self._isPresented = isPresented self.onCompletion = onCompletion - self.cameraPosition = cameraPosition + self.cameraPosition = challengeOption.camera self.sessionTask = Task { let session = try await AWSPredictionsPlugin.startFaceLivenessSession( @@ -125,7 +126,8 @@ public struct FaceLivenessDetectorView: View { closeButtonAction: { onCompletion(.failure(.userCancelled)) }, sessionID: sessionID, isPreviewScreenEnabled: !disableStartView, - cameraPosition: cameraPosition + cameraPosition: challengeOption.camera, + challengeOption: challengeOption ) ) } @@ -283,7 +285,7 @@ public struct FaceLivenessDetectorView: View { enum DisplayState: Equatable { case awaitingChallengeType case awaitingLivenessSession(Challenge) - case displayingGetReadyView(Challenge, LivenessCaptureDevicePosition) + case displayingGetReadyView(Challenge, LivenessCamera) case displayingLiveness case awaitingCameraPermission @@ -338,3 +340,33 @@ private func map(detectionCompletion: @escaping (Result Void, sessionID: String, isPreviewScreenEnabled: Bool, - cameraPosition: LivenessCaptureDevicePosition + cameraPosition: LivenessCamera, + challengeOption: ChallengeOption ) { self.closeButtonAction = closeButtonAction self.videoChunker = videoChunker @@ -77,6 +79,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { self.faceInOvalMatching = faceInOvalMatching self.isPreviewScreenEnabled = isPreviewScreenEnabled self.cameraPosition = cameraPosition + self.challengeOption = challengeOption self.closeButtonAction = { [weak self] in guard let self else { return } @@ -127,18 +130,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { livenessService?.register( listener: { [weak self] _challenge in self?.challenge = _challenge - guard _challenge.type == .faceMovementAndLightChallenge, - self?.cameraPosition == .back else { - onChallengeTypeReceived(_challenge) - return - } - - // incompatible camera position with challenge type - // return error - DispatchQueue.main.async { - self?.livenessState - .unrecoverableStateEncountered(.invalidCameraPositionSelecteed) - } + onChallengeTypeReceived(_challenge) }, on: .challenge) } @@ -217,7 +209,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { try livenessService?.initializeLivenessStream( withSessionID: sessionID, userAgent: UserAgentValues.standard().userAgentString, - challenges: FaceLivenessSession.supportedChallenges, + challenges: [challengeOption.challenge], options: .init( attemptCount: Self.attemptCount, preCheckViewEnabled: isPreviewScreenEnabled) diff --git a/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift index 5df7ec63..62e563ef 100644 --- a/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift +++ b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift @@ -166,7 +166,6 @@ struct LivenessStateMachine { static let socketClosed = LivenessError(code: 6, webSocketCloseCode: .normalClosure) static let viewResignation = LivenessError(code: 8, webSocketCloseCode: .viewClosure) static let cameraNotAvailable = LivenessError(code: 9, webSocketCloseCode: .unexpectedRuntimeError) - static let invalidCameraPositionSelecteed = LivenessError(code: 10, webSocketCloseCode: .unexpectedRuntimeError) static func == (lhs: LivenessError, rhs: LivenessError) -> Bool { lhs.code == rhs.code From 0528d69b6d343a780554757210675c8e34fe7ce6 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Tue, 10 Sep 2024 13:31:29 -0700 Subject: [PATCH 5/9] Update ChallengeOptions and use camera position based on challenge type received --- .../HostApp/Views/ExampleLivenessView.swift | 7 +- .../InstructionContainerView.swift | 2 +- .../Liveness/FaceLivenessDetectionView.swift | 85 +++++++++---------- ...ViewModel+FaceDetectionResultHandler.swift | 4 +- .../FaceLivenessDetectionViewModel.swift | 62 +++++++++----- .../CredentialsProviderTestCase.swift | 10 ++- Tests/FaceLivenessTests/LivenessTests.swift | 6 +- 7 files changed, 100 insertions(+), 76 deletions(-) diff --git a/HostApp/HostApp/Views/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift index 656b86a0..345d89ee 100644 --- a/HostApp/HostApp/Views/ExampleLivenessView.swift +++ b/HostApp/HostApp/Views/ExampleLivenessView.swift @@ -23,7 +23,8 @@ struct ExampleLivenessView: View { FaceLivenessDetectorView( sessionID: viewModel.sessionID, region: "us-east-1", - challengeOption: .faceMovementAndLightChallenge, + challengeOptions: .init(faceMovementChallengeOption: FaceMovementChallengeOption(camera: .front), + faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption()), isPresented: Binding( get: { viewModel.presentationState == .liveness }, set: { _ in } @@ -47,6 +48,10 @@ struct ExampleLivenessView: View { viewModel.presentationState = .error(.countdownFaceTooClose) case .failure(.invalidSignature): viewModel.presentationState = .error(.invalidSignature) + case .failure(.faceInOvalMatchExceededTimeLimitError): + viewModel.presentationState = .error(.faceInOvalMatchExceededTimeLimitError) + case .failure(.internalServer): + viewModel.presentationState = .error(.internalServer) case .failure(.cameraNotAvailable): viewModel.presentationState = .error(.cameraNotAvailable) case .failure(.validation): diff --git a/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift b/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift index 5ed45ae7..01dcd37f 100644 --- a/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift +++ b/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift @@ -110,7 +110,7 @@ struct InstructionContainerView: View { ) } case .faceMatched: - if let challenge = viewModel.challenge, + if let challenge = viewModel.challengeReceived, case .faceMovementAndLightChallenge = challenge.type { InstructionView( text: LocalizedStrings.challenge_instruction_hold_still, diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift index af975ec6..40e2504d 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift @@ -20,7 +20,7 @@ public struct FaceLivenessDetectorView: View { @State var displayingCameraPermissionsNeededAlert = false let disableStartView: Bool - let cameraPosition: LivenessCamera + let challengeOptions: ChallengeOptions let onCompletion: (Result) -> Void let sessionTask: Task @@ -30,14 +30,14 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, - challengeOption: ChallengeOption, + challengeOptions: ChallengeOptions, isPresented: Binding, onCompletion: @escaping (Result) -> Void ) { self.disableStartView = disableStartView self._isPresented = isPresented - self.cameraPosition = challengeOption.camera self.onCompletion = onCompletion + self.challengeOptions = challengeOptions self.sessionTask = Task { let session = try await AWSPredictionsPlugin.startFaceLivenessSession( @@ -59,31 +59,16 @@ public struct FaceLivenessDetectorView: View { assetWriterDelegate: VideoChunker.AssetWriterDelegate(), assetWriterInput: LivenessAVAssetWriterInput() ) - - let avCaptureDevice = AVCaptureDevice.default( - .builtInWideAngleCamera, - for: .video, - position: cameraPosition == .front ? .front : .back) - - let captureSession = LivenessCaptureSession( - captureDevice: .init(avCaptureDevice: avCaptureDevice), - outputDelegate: OutputSampleBufferCapturer( - faceDetector: faceDetector, - videoChunker: videoChunker - ) - ) self._viewModel = StateObject( wrappedValue: .init( faceDetector: faceDetector, faceInOvalMatching: faceInOvalStateMatching, - captureSession: captureSession, videoChunker: videoChunker, closeButtonAction: { onCompletion(.failure(.userCancelled)) }, sessionID: sessionID, isPreviewScreenEnabled: !disableStartView, - cameraPosition: challengeOption.camera, - challengeOption: challengeOption + challengeOptions: challengeOptions ) ) } @@ -93,7 +78,7 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, - challengeOption: ChallengeOption, + challengeOptions: ChallengeOptions, isPresented: Binding, onCompletion: @escaping (Result) -> Void, captureSession: LivenessCaptureSession @@ -101,7 +86,7 @@ public struct FaceLivenessDetectorView: View { self.disableStartView = disableStartView self._isPresented = isPresented self.onCompletion = onCompletion - self.cameraPosition = challengeOption.camera + self.challengeOptions = challengeOptions self.sessionTask = Task { let session = try await AWSPredictionsPlugin.startFaceLivenessSession( @@ -121,13 +106,11 @@ public struct FaceLivenessDetectorView: View { wrappedValue: .init( faceDetector: captureSession.outputSampleBufferCapturer!.faceDetector, faceInOvalMatching: faceInOvalStateMatching, - captureSession: captureSession, videoChunker: captureSession.outputSampleBufferCapturer!.videoChunker, closeButtonAction: { onCompletion(.failure(.userCancelled)) }, sessionID: sessionID, isPreviewScreenEnabled: !disableStartView, - cameraPosition: challengeOption.camera, - challengeOption: challengeOption + challengeOptions: challengeOptions ) ) } @@ -172,6 +155,14 @@ public struct FaceLivenessDetectorView: View { .onAppear { Task { do { + let cameraPosition: LivenessCamera + switch challenge.type { + case .faceMovementAndLightChallenge: + cameraPosition = challengeOptions.faceMovementAndLightChallengeOption.camera + case .faceMovementChallenge: + cameraPosition = challengeOptions.faceMovementChallengeOption.camera + } + let newState = disableStartView ? DisplayState.displayingLiveness : DisplayState.displayingGetReadyView(challenge, cameraPosition) @@ -255,7 +246,7 @@ public struct FaceLivenessDetectorView: View { for: .video, completionHandler: { accessGranted in guard accessGranted == true else { return } - guard let challenge = viewModel.challenge else { return } + guard let challenge = viewModel.challengeReceived else { return } displayState = .awaitingLivenessSession(challenge) } ) @@ -274,7 +265,7 @@ public struct FaceLivenessDetectorView: View { case .restricted, .denied: alertCameraAccessNeeded() case .authorized: - guard let challenge = viewModel.challenge else { return } + guard let challenge = viewModel.challengeReceived else { return } displayState = .awaitingLivenessSession(challenge) @unknown default: break @@ -341,32 +332,38 @@ private func map(detectionCompletion: @escaping (Result Void let videoChunker: VideoChunker let sessionID: String @@ -35,7 +35,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { var hasSentFirstVideo = false var layerRectConverted: (CGRect) -> CGRect = { $0 } var sessionConfiguration: FaceLivenessSession.SessionConfiguration? - var challenge: Challenge? + var challengeReceived: Challenge? var normalizeFace: (DetectedFace) -> DetectedFace = { $0 } var provideSingleFrame: ((UIImage) -> Void)? var cameraViewRect = CGRect.zero @@ -44,8 +44,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { var initialClientEvent: InitialClientEvent? var faceMatchedTimestamp: UInt64? var noFitStartTime: Date? - let cameraPosition: LivenessCamera - let challengeOption: ChallengeOption + let challengeOptions: ChallengeOptions static var attemptCount: Int = 0 static var attemptIdTimeStamp: Date = Date() @@ -61,25 +60,21 @@ class FaceLivenessDetectionViewModel: ObservableObject { init( faceDetector: FaceDetector, faceInOvalMatching: FaceInOvalMatching, - captureSession: LivenessCaptureSession, videoChunker: VideoChunker, stateMachine: LivenessStateMachine = .init(state: .initial), closeButtonAction: @escaping () -> Void, sessionID: String, isPreviewScreenEnabled: Bool, - cameraPosition: LivenessCamera, - challengeOption: ChallengeOption + challengeOptions: ChallengeOptions ) { self.closeButtonAction = closeButtonAction self.videoChunker = videoChunker self.livenessState = stateMachine self.sessionID = sessionID - self.captureSession = captureSession self.faceDetector = faceDetector self.faceInOvalMatching = faceInOvalMatching self.isPreviewScreenEnabled = isPreviewScreenEnabled - self.cameraPosition = cameraPosition - self.challengeOption = challengeOption + self.challengeOptions = challengeOptions self.closeButtonAction = { [weak self] in guard let self else { return } @@ -129,7 +124,8 @@ class FaceLivenessDetectionViewModel: ObservableObject { livenessService?.register( listener: { [weak self] _challenge in - self?.challenge = _challenge + self?.challengeReceived = _challenge + self?.configureCaptureSession(challenge: _challenge) onChallengeTypeReceived(_challenge) }, on: .challenge) @@ -144,16 +140,16 @@ class FaceLivenessDetectionViewModel: ObservableObject { } func startSession() { - captureSession.startSession() + captureSession?.startSession() } func stopRecording() { - captureSession.stopRunning() + captureSession?.stopRunning() } func configureCamera(withinFrame frame: CGRect) -> CALayer? { do { - let avLayer = try captureSession.configureCamera(frame: frame) + let avLayer = try captureSession?.configureCamera(frame: frame) DispatchQueue.main.async { self.livenessState.checkIsFacePrepared() } @@ -209,7 +205,8 @@ class FaceLivenessDetectionViewModel: ObservableObject { try livenessService?.initializeLivenessStream( withSessionID: sessionID, userAgent: UserAgentValues.standard().userAgentString, - challenges: [challengeOption.challenge], + challenges: [challengeOptions.faceMovementChallengeOption.challenge, + challengeOptions.faceMovementAndLightChallengeOption.challenge], options: .init( attemptCount: Self.attemptCount, preCheckViewEnabled: isPreviewScreenEnabled) @@ -258,7 +255,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { videoStartTime: UInt64 ) { guard initialClientEvent == nil else { return } - guard let challenge else { return } + guard let challengeReceived else { return } videoChunker.start() @@ -278,8 +275,8 @@ class FaceLivenessDetectionViewModel: ObservableObject { do { try livenessService?.send( .initialFaceDetected(event: _initialClientEvent, - challenge: .init(version: challenge.version, - type: challenge.type)), + challenge: .init(version: challengeReceived.version, + type: challengeReceived.type)), eventDate: { .init() } ) } catch { @@ -298,7 +295,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { let sessionConfiguration, let initialClientEvent, let faceMatchedTimestamp, - let challenge + let challengeReceived else { return } let finalClientEvent = FinalClientEvent( @@ -313,8 +310,8 @@ class FaceLivenessDetectionViewModel: ObservableObject { do { try livenessService?.send( .final(event: finalClientEvent, - challenge: .init(version: challenge.version, - type: challenge.type)), + challenge: .init(version: challengeReceived.version, + type: challengeReceived.type)), eventDate: { .init() } ) @@ -407,6 +404,29 @@ class FaceLivenessDetectionViewModel: ObservableObject { } return data } + + func configureCaptureSession(challenge: Challenge) { + let cameraPosition: LivenessCamera + switch challenge.type { + case .faceMovementChallenge: + cameraPosition = challengeOptions.faceMovementChallengeOption.camera + case .faceMovementAndLightChallenge: + cameraPosition = challengeOptions.faceMovementAndLightChallengeOption.camera + } + + let avCaptureDevice = AVCaptureDevice.default( + .builtInWideAngleCamera, + for: .video, + position: cameraPosition == .front ? .front : .back) + + self.captureSession = LivenessCaptureSession( + captureDevice: .init(avCaptureDevice: avCaptureDevice), + outputDelegate: OutputSampleBufferCapturer( + faceDetector: self.faceDetector, + videoChunker: self.videoChunker + ) + ) + } } extension FaceLivenessDetectionViewModel: FaceDetectionSessionConfigurationWrapper { } diff --git a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift index 5b82119a..396ef60a 100644 --- a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift +++ b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift @@ -38,12 +38,12 @@ final class CredentialsProviderTestCase: XCTestCase { let viewModel = FaceLivenessDetectionViewModel( faceDetector: faceDetector, faceInOvalMatching: .init(instructor: .init()), - captureSession: captureSession, videoChunker: videoChunker, closeButtonAction: {}, sessionID: UUID().uuidString, isPreviewScreenEnabled: false, - cameraPosition: .front + challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front), + faceMovementAndLightChallengeOption: .init()) ) self.videoChunker = videoChunker @@ -67,7 +67,8 @@ final class CredentialsProviderTestCase: XCTestCase { sessionID: UUID().uuidString, credentialsProvider: credentialsProvider, region: "us-east-1", - cameraPosition: .front, + challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front), + faceMovementAndLightChallengeOption: .init()), isPresented: .constant(true), onCompletion: { _ in } ) @@ -104,7 +105,8 @@ final class CredentialsProviderTestCase: XCTestCase { sessionID: UUID().uuidString, credentialsProvider: credentialsProvider, region: "us-east-1", - cameraPosition: .front, + challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front), + faceMovementAndLightChallengeOption: .init()), isPresented: .constant(true), onCompletion: { _ in } ) diff --git a/Tests/FaceLivenessTests/LivenessTests.swift b/Tests/FaceLivenessTests/LivenessTests.swift index 45b090c4..2699eef7 100644 --- a/Tests/FaceLivenessTests/LivenessTests.swift +++ b/Tests/FaceLivenessTests/LivenessTests.swift @@ -29,12 +29,12 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase { let viewModel = FaceLivenessDetectionViewModel( faceDetector: faceDetector, faceInOvalMatching: .init(instructor: .init()), - captureSession: captureSession, videoChunker: videoChunker, closeButtonAction: {}, sessionID: UUID().uuidString, isPreviewScreenEnabled: false, - cameraPosition: .front + challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front), + faceMovementAndLightChallengeOption: .init()) ) self.videoChunker = videoChunker @@ -116,7 +116,7 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase { /// Then: The end state of this flow is `.recording(ovalDisplayed: false)` func testTransitionToRecordingState() async throws { viewModel.livenessService = self.livenessService - viewModel.challenge = Challenge(version: "2.0.0", type: .faceMovementAndLightChallenge) + viewModel.challengeReceived = Challenge(version: "2.0.0", type: .faceMovementAndLightChallenge) let face = FaceLivenessSession.OvalMatchChallenge.Face( distanceThreshold: 0.32, From 2ed090696c40a52715a6b27d00b20a7da3996dc8 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Mon, 16 Sep 2024 13:00:09 -0700 Subject: [PATCH 6/9] Add UI changes for selecting back camera in HostApp --- .../HostApp/Views/ExampleLivenessView.swift | 32 ++++++++------- .../Views/ExampleLivenessViewModel.swift | 7 ++-- HostApp/HostApp/Views/RootView.swift | 17 +++++--- HostApp/HostApp/Views/StartSessionView.swift | 39 +++++++++++++++++-- 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/HostApp/HostApp/Views/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift index 345d89ee..a75a17fe 100644 --- a/HostApp/HostApp/Views/ExampleLivenessView.swift +++ b/HostApp/HostApp/Views/ExampleLivenessView.swift @@ -9,24 +9,28 @@ import SwiftUI import FaceLiveness struct ExampleLivenessView: View { - @Binding var isPresented: Bool + @Binding var containerViewState: ContainerViewState @ObservedObject var viewModel: ExampleLivenessViewModel - init(sessionID: String, isPresented: Binding) { - self.viewModel = .init(sessionID: sessionID) - self._isPresented = isPresented + init(sessionID: String, containerViewState: Binding) { + self._containerViewState = containerViewState + if case let .liveness(selectedCamera) = _containerViewState.wrappedValue { + self.viewModel = .init(sessionID: sessionID, presentationState: .liveness(selectedCamera)) + } else { + self.viewModel = .init(sessionID: sessionID) + } } var body: some View { switch viewModel.presentationState { - case .liveness: + case .liveness(let camera): FaceLivenessDetectorView( sessionID: viewModel.sessionID, region: "us-east-1", - challengeOptions: .init(faceMovementChallengeOption: FaceMovementChallengeOption(camera: .front), + challengeOptions: .init(faceMovementChallengeOption: FaceMovementChallengeOption(camera: camera), faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption()), isPresented: Binding( - get: { viewModel.presentationState == .liveness }, + get: { viewModel.presentationState == .liveness(camera) }, set: { _ in } ), onCompletion: { result in @@ -35,11 +39,11 @@ struct ExampleLivenessView: View { case .success: withAnimation { viewModel.presentationState = .result } case .failure(.sessionNotFound), .failure(.cameraPermissionDenied), .failure(.accessDenied): - viewModel.presentationState = .liveness - isPresented = false + viewModel.presentationState = .liveness(camera) + containerViewState = .startSession case .failure(.userCancelled): - viewModel.presentationState = .liveness - isPresented = false + viewModel.presentationState = .liveness(camera) + containerViewState = .startSession case .failure(.sessionTimedOut): viewModel.presentationState = .error(.sessionTimedOut) case .failure(.socketClosed): @@ -64,11 +68,11 @@ struct ExampleLivenessView: View { } } ) - .id(isPresented) + .id(containerViewState) case .result: LivenessResultView( sessionID: viewModel.sessionID, - onTryAgain: { isPresented = false }, + onTryAgain: { containerViewState = .startSession }, content: { LivenessResultContentView(fetchResults: viewModel.fetchLivenessResult) } @@ -77,7 +81,7 @@ struct ExampleLivenessView: View { case .error(let detectionError): LivenessResultView( sessionID: viewModel.sessionID, - onTryAgain: { isPresented = false }, + onTryAgain: { containerViewState = .startSession }, content: { switch detectionError { case .socketClosed: diff --git a/HostApp/HostApp/Views/ExampleLivenessViewModel.swift b/HostApp/HostApp/Views/ExampleLivenessViewModel.swift index a04571bc..7dade2fb 100644 --- a/HostApp/HostApp/Views/ExampleLivenessViewModel.swift +++ b/HostApp/HostApp/Views/ExampleLivenessViewModel.swift @@ -10,11 +10,12 @@ import FaceLiveness import Amplify class ExampleLivenessViewModel: ObservableObject { - @Published var presentationState = PresentationState.liveness + @Published var presentationState: PresentationState = .liveness(.front) let sessionID: String - init(sessionID: String) { + init(sessionID: String, presentationState: PresentationState = .liveness(.front)) { self.sessionID = sessionID + self.presentationState = presentationState } func fetchLivenessResult() async throws -> LivenessResultContentView.Result { @@ -30,6 +31,6 @@ class ExampleLivenessViewModel: ObservableObject { } enum PresentationState: Equatable { - case liveness, result, error(FaceLivenessDetectionError) + case liveness(LivenessCamera), result, error(FaceLivenessDetectionError) } } diff --git a/HostApp/HostApp/Views/RootView.swift b/HostApp/HostApp/Views/RootView.swift index 7600f1b4..59a3c815 100644 --- a/HostApp/HostApp/Views/RootView.swift +++ b/HostApp/HostApp/Views/RootView.swift @@ -6,25 +6,32 @@ // import SwiftUI +import FaceLiveness struct RootView: View { @EnvironmentObject var sceneDelegate: SceneDelegate @State var sessionID = "" - @State var isPresentingContainerView = false + @State var containerViewState = ContainerViewState.startSession var body: some View { - if isPresentingContainerView { + switch containerViewState { + case .liveness: ExampleLivenessView( sessionID: sessionID, - isPresented: $isPresentingContainerView + containerViewState: $containerViewState ) - } else { + case .startSession: StartSessionView( sessionID: $sessionID, - isPresentingContainerView: $isPresentingContainerView + containerViewState: $containerViewState ) .background(Color.dynamicColors(light: .white, dark: .secondarySystemBackground)) .edgesIgnoringSafeArea(.all) } } } + +enum ContainerViewState: Hashable { + case liveness(LivenessCamera) + case startSession +} diff --git a/HostApp/HostApp/Views/StartSessionView.swift b/HostApp/HostApp/Views/StartSessionView.swift index 42f64401..6905e9b2 100644 --- a/HostApp/HostApp/Views/StartSessionView.swift +++ b/HostApp/HostApp/Views/StartSessionView.swift @@ -12,7 +12,7 @@ struct StartSessionView: View { @EnvironmentObject var sceneDelegate: SceneDelegate @ObservedObject var viewModel = StartSessionViewModel() @Binding var sessionID: String - @Binding var isPresentingContainerView: Bool + @Binding var containerViewState: ContainerViewState @State private var showAlert = false var body: some View { @@ -26,7 +26,7 @@ struct StartSessionView: View { ) button( - text: "Create Liveness Session", + text: "Create Liveness Session (front camera)", backgroundColor: .dynamicColors( light: .hex("#047D95"), dark: .hex("#7dd6e8") @@ -35,7 +35,7 @@ struct StartSessionView: View { viewModel.createSession { sessionId, err in if let sessionId = sessionId { sessionID = sessionId - isPresentingContainerView = true + containerViewState = .liveness(.front) } showAlert = err != nil @@ -50,7 +50,38 @@ struct StartSessionView: View { dismissButton: .default( Text("OK"), action: { - isPresentingContainerView = false + containerViewState = .startSession + } + ) + ) + } + + button( + text: "Create Liveness Session (back camera)", + backgroundColor: .dynamicColors( + light: .hex("#047D95"), + dark: .hex("#7dd6e8") + ), + action: { + viewModel.createSession { sessionId, err in + if let sessionId = sessionId { + sessionID = sessionId + containerViewState = .liveness(.back) + } + + showAlert = err != nil + } + }, + enabled: viewModel.isSignedIn + ) + .alert(isPresented: $showAlert) { + Alert( + title: Text("Error Creating Liveness Session"), + message: Text("Unable to create a liveness session id. Please try again."), + dismissButton: .default( + Text("OK"), + action: { + containerViewState = .startSession } ) ) From a4a880b6d35066a8f245df9fe94a1371742e59b2 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Thu, 19 Jun 2025 11:24:56 -0700 Subject: [PATCH 7/9] add default parameter to ChallengeOptions init --- HostApp/HostApp/Views/ExampleLivenessView.swift | 3 +-- .../Views/Liveness/FaceLivenessDetectionView.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/HostApp/HostApp/Views/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift index a75a17fe..03e4dd55 100644 --- a/HostApp/HostApp/Views/ExampleLivenessView.swift +++ b/HostApp/HostApp/Views/ExampleLivenessView.swift @@ -27,8 +27,7 @@ struct ExampleLivenessView: View { FaceLivenessDetectorView( sessionID: viewModel.sessionID, region: "us-east-1", - challengeOptions: .init(faceMovementChallengeOption: FaceMovementChallengeOption(camera: camera), - faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption()), + challengeOptions: .init(faceMovementChallengeOption: FaceMovementChallengeOption(camera: camera)), isPresented: Binding( get: { viewModel.presentationState == .liveness(camera) }, set: { _ in } diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift index 40e2504d..8f5e1af4 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift @@ -342,7 +342,7 @@ public struct ChallengeOptions { let faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption public init(faceMovementChallengeOption: FaceMovementChallengeOption, - faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption) { + faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption = .init()) { self.faceMovementChallengeOption = faceMovementChallengeOption self.faceMovementAndLightChallengeOption = faceMovementAndLightChallengeOption } From f5bcbbf99af8a3839c806ecf866002dd5a4b5b33 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Fri, 20 Jun 2025 08:27:48 -0700 Subject: [PATCH 8/9] fix formatting --- .../Views/GetReadyPage/GetReadyPageView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift index 806aa25e..0142ca13 100644 --- a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift +++ b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift @@ -80,9 +80,9 @@ struct GetReadyPageView: View { struct GetReadyPageView_Previews: PreviewProvider { static var previews: some View { - GetReadyPageView(onBegin: {}, - challenge: .init(version: "2.0.0", - type: .faceMovementAndLightChallenge), - cameraPosition: .front) + GetReadyPageView( + onBegin: {}, + challenge: .init(version: "2.0.0", type: .faceMovementAndLightChallenge), + cameraPosition: .front) } } From 846eb45db23246dc3a39ad1cf17015ac97aecd71 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Fri, 20 Jun 2025 08:35:01 -0700 Subject: [PATCH 9/9] fix test build --- Tests/FaceLivenessTests/LivenessTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FaceLivenessTests/LivenessTests.swift b/Tests/FaceLivenessTests/LivenessTests.swift index 2699eef7..89db7315 100644 --- a/Tests/FaceLivenessTests/LivenessTests.swift +++ b/Tests/FaceLivenessTests/LivenessTests.swift @@ -71,7 +71,7 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase { /// Then: The end state of this flow is `.faceMatched` func testHappyPathToMatchedFace() async throws { viewModel.livenessService = self.livenessService - viewModel.challenge = Challenge(version: "2.0.0", type: .faceMovementAndLightChallenge) + viewModel.challengeReceived = Challenge(version: "2.0.0", type: .faceMovementAndLightChallenge) viewModel.livenessState.checkIsFacePrepared() XCTAssertEqual(viewModel.livenessState.state, .pendingFacePreparedConfirmation(.pendingCheck))