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.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a61605c0..02973f5a 100644 --- a/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/HostApp/HostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/aws-amplify/amplify-swift", "state" : { - "branch" : "feat/no-light-support", - "revision" : "22e02fa21399122aac1d8b4f6ab23c242c79dae6" + "branch" : "test/no-light-support", + "revision" : "cdee9437c8bae4be8198a9860d09cd79fdb044ba" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stephencelis/SQLite.swift.git", "state" : { - "revision" : "5f5ad81ac0d0a0f3e56e39e646e8423c617df523", - "version" : "0.13.2" + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" } }, { diff --git a/HostApp/HostApp/Views/ExampleLivenessView.swift b/HostApp/HostApp/Views/ExampleLivenessView.swift index f24045d4..5152ca17 100644 --- a/HostApp/HostApp/Views/ExampleLivenessView.swift +++ b/HostApp/HostApp/Views/ExampleLivenessView.swift @@ -9,22 +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: camera), + faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption()), isPresented: Binding( - get: { viewModel.presentationState == .liveness }, + get: { viewModel.presentationState == .liveness(camera) }, set: { _ in } ), onCompletion: { result in @@ -33,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): @@ -46,19 +52,23 @@ 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) default: - viewModel.presentationState = .liveness + viewModel.presentationState = .liveness(camera) } } } ) - .id(isPresented) + .id(containerViewState) case .result: LivenessResultView( sessionID: viewModel.sessionID, - onTryAgain: { isPresented = false }, + onTryAgain: { containerViewState = .startSession }, content: { LivenessResultContentView(fetchResults: viewModel.fetchLivenessResult) } @@ -67,7 +77,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/LivenessCheckErrorContentView.swift b/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift index 866c2c1c..b8df88f1 100644 --- a/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift +++ b/HostApp/HostApp/Views/LivenessCheckErrorContentView.swift @@ -60,6 +60,7 @@ extension LivenessCheckErrorContentView { name: "The camera could not be started.", description: "There might be a hardware issue with the camera." ) + } struct LivenessCheckErrorContentView_Previews: PreviewProvider { 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 } ) ) diff --git a/Package.resolved b/Package.resolved index 13524dd4..02973f5a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/aws-amplify/amplify-swift", "state" : { - "branch" : "feat/no-light-support", - "revision" : "614be628cb01188e519bb0e9e4d90bd83703d139" + "branch" : "test/no-light-support", + "revision" : "cdee9437c8bae4be8198a9860d09cd79fdb044ba" } }, { diff --git a/Package.swift b/Package.swift index 446f12c0..01a52b9a 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ // TODO: Change this before merge to main - .package(url: "https://github.com/aws-amplify/amplify-swift", branch: "feat/no-light-support") + .package(url: "https://github.com/aws-amplify/amplify-swift", branch: "test/no-light-support") ], targets: [ .target( 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..a46dbaa8 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: LivenessCamera - override init() { + init(cameraPosition: LivenessCamera) { + 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..806aa25e 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: LivenessCamera init( onBegin: @escaping () -> Void, beginCheckButtonDisabled: Bool = false, - challenge: Challenge + challenge: Challenge, + cameraPosition: LivenessCamera ) { 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/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/FaceLivenessDetectionError.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift index e90a6f06..19ced079 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift @@ -125,7 +125,7 @@ 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 cameraNotAvailable = FaceLivenessDetectionError( code: 18, message: "The camera is not available.", diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift index f803a863..40e2504d 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 challengeOptions: ChallengeOptions let onCompletion: (Result) -> Void let sessionTask: Task @@ -29,12 +30,14 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, + challengeOptions: ChallengeOptions, isPresented: Binding, onCompletion: @escaping (Result) -> Void ) { self.disableStartView = disableStartView self._isPresented = isPresented self.onCompletion = onCompletion + self.challengeOptions = challengeOptions self.sessionTask = Task { let session = try await AWSPredictionsPlugin.startFaceLivenessSession( @@ -57,29 +60,15 @@ public struct FaceLivenessDetectorView: View { assetWriterInput: LivenessAVAssetWriterInput() ) - let avCpatureDevice = AVCaptureDevice.DiscoverySession( - deviceTypes: [.builtInWideAngleCamera], - mediaType: .video, - position: .front - ).devices.first - - let captureSession = LivenessCaptureSession( - captureDevice: .init(avCaptureDevice: avCpatureDevice), - 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 + isPreviewScreenEnabled: !disableStartView, + challengeOptions: challengeOptions ) ) } @@ -89,6 +78,7 @@ public struct FaceLivenessDetectorView: View { credentialsProvider: AWSCredentialsProvider? = nil, region: String, disableStartView: Bool = false, + challengeOptions: ChallengeOptions, isPresented: Binding, onCompletion: @escaping (Result) -> Void, captureSession: LivenessCaptureSession @@ -96,6 +86,7 @@ public struct FaceLivenessDetectorView: View { self.disableStartView = disableStartView self._isPresented = isPresented self.onCompletion = onCompletion + self.challengeOptions = challengeOptions self.sessionTask = Task { let session = try await AWSPredictionsPlugin.startFaceLivenessSession( @@ -115,11 +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 + isPreviewScreenEnabled: !disableStartView, + challengeOptions: challengeOptions ) ) } @@ -164,23 +155,32 @@ 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) + : 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 { @@ -246,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) } ) @@ -265,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 @@ -276,7 +276,7 @@ public struct FaceLivenessDetectorView: View { enum DisplayState: Equatable { case awaitingChallengeType case awaitingLivenessSession(Challenge) - case displayingGetReadyView(Challenge) + case displayingGetReadyView(Challenge, LivenessCamera) case displayingLiveness case awaitingCameraPermission @@ -286,8 +286,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): @@ -331,3 +331,39 @@ 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,6 +44,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { var initialClientEvent: InitialClientEvent? var faceMatchedTimestamp: UInt64? var noFitStartTime: Date? + let challengeOptions: ChallengeOptions static var attemptCount: Int = 0 static var attemptIdTimeStamp: Date = Date() @@ -59,21 +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 + isPreviewScreenEnabled: Bool, + 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.challengeOptions = challengeOptions self.closeButtonAction = { [weak self] in guard let self else { return } @@ -123,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) @@ -138,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() } @@ -203,7 +205,8 @@ class FaceLivenessDetectionViewModel: ObservableObject { try livenessService?.initializeLivenessStream( withSessionID: sessionID, userAgent: UserAgentValues.standard().userAgentString, - challenges: FaceLivenessSession.supportedChallenges, + challenges: [challengeOptions.faceMovementChallengeOption.challenge, + challengeOptions.faceMovementAndLightChallengeOption.challenge], options: .init( attemptCount: Self.attemptCount, preCheckViewEnabled: isPreviewScreenEnabled) @@ -252,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() @@ -272,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 { @@ -292,7 +295,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { let sessionConfiguration, let initialClientEvent, let faceMatchedTimestamp, - let challenge + let challengeReceived else { return } let finalClientEvent = FinalClientEvent( @@ -307,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() } ) @@ -401,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/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift index e61f8311..62e563ef 100644 --- a/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift +++ b/Sources/FaceLiveness/Views/Liveness/LivenessStateMachine.swift @@ -165,7 +165,7 @@ 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 func == (lhs: LivenessError, rhs: LivenessError) -> Bool { lhs.code == rhs.code diff --git a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift index 3c1dabbf..396ef60a 100644 --- a/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift +++ b/Tests/FaceLivenessTests/CredentialsProviderTestCase.swift @@ -38,11 +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 + isPreviewScreenEnabled: false, + challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front), + faceMovementAndLightChallengeOption: .init()) ) self.videoChunker = videoChunker @@ -66,6 +67,8 @@ final class CredentialsProviderTestCase: XCTestCase { sessionID: UUID().uuidString, credentialsProvider: credentialsProvider, region: "us-east-1", + challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front), + faceMovementAndLightChallengeOption: .init()), isPresented: .constant(true), onCompletion: { _ in } ) @@ -102,6 +105,8 @@ final class CredentialsProviderTestCase: XCTestCase { sessionID: UUID().uuidString, credentialsProvider: credentialsProvider, region: "us-east-1", + 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 5603914a..0307124f 100644 --- a/Tests/FaceLivenessTests/LivenessTests.swift +++ b/Tests/FaceLivenessTests/LivenessTests.swift @@ -29,11 +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 + isPreviewScreenEnabled: false, + challengeOptions: .init(faceMovementChallengeOption: .init(camera: .front), + faceMovementAndLightChallengeOption: .init()) ) self.videoChunker = videoChunker @@ -70,7 +71,8 @@ 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.challengeReceived = Challenge(version: "2.0.0", type: .faceMovementAndLightChallenge) viewModel.livenessState.checkIsFacePrepared() XCTAssertEqual(viewModel.livenessState.state, .pendingFacePreparedConfirmation(.pendingCheck)) @@ -115,7 +117,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,