Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add back camera support #180

Draft
wants to merge 16 commits into
base: feat/no-light-support
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions HostApp/HostApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -131,6 +131,7 @@
9070FFBD285112B5009867D5 /* HostAppUITests */,
9070FFA1285112B4009867D5 /* Products */,
90215EED291E9FB60050F2AD /* Frameworks */,
A5A9AF5054D0FF13505B212A /* AmplifyConfig */,
);
sourceTree = "<group>";
};
@@ -213,6 +214,15 @@
path = Model;
sourceTree = "<group>";
};
A5A9AF5054D0FF13505B212A /* AmplifyConfig */ = {
isa = PBXGroup;
children = (
973619242BA378690003A590 /* awsconfiguration.json */,
973619232BA378690003A590 /* amplifyconfiguration.json */,
);
name = AmplifyConfig;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Original file line number Diff line number Diff line change
@@ -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"
}
},
{
38 changes: 24 additions & 14 deletions HostApp/HostApp/Views/ExampleLivenessView.swift
Original file line number Diff line number Diff line change
@@ -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<Bool>) {
self.viewModel = .init(sessionID: sessionID)
self._isPresented = isPresented
init(sessionID: String, containerViewState: Binding<ContainerViewState>) {
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:
7 changes: 4 additions & 3 deletions HostApp/HostApp/Views/ExampleLivenessViewModel.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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 {
17 changes: 12 additions & 5 deletions HostApp/HostApp/Views/RootView.swift
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 35 additions & 4 deletions HostApp/HostApp/Views/StartSessionView.swift
Original file line number Diff line number Diff line change
@@ -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
}
)
)
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -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"
}
},
{
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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(
Original file line number Diff line number Diff line change
@@ -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)
}

Original file line number Diff line number Diff line change
@@ -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
10 changes: 7 additions & 3 deletions Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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.",
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ public struct FaceLivenessDetectorView: View {
@State var displayingCameraPermissionsNeededAlert = false

let disableStartView: Bool
let challengeOptions: ChallengeOptions
let onCompletion: (Result<Void, FaceLivenessDetectionError>) -> Void

let sessionTask: Task<FaceLivenessSession, Error>
@@ -29,12 +30,14 @@ public struct FaceLivenessDetectorView: View {
credentialsProvider: AWSCredentialsProvider? = nil,
region: String,
disableStartView: Bool = false,
challengeOptions: ChallengeOptions,
isPresented: Binding<Bool>,
onCompletion: @escaping (Result<Void, FaceLivenessDetectionError>) -> 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,13 +78,15 @@ public struct FaceLivenessDetectorView: View {
credentialsProvider: AWSCredentialsProvider? = nil,
region: String,
disableStartView: Bool = false,
challengeOptions: ChallengeOptions,
isPresented: Binding<Bool>,
onCompletion: @escaping (Result<Void, FaceLivenessDetectionError>) -> Void,
captureSession: LivenessCaptureSession
) {
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, FaceLivenessDetect
}
}
}

public enum LivenessCamera {
case front
case back
}

public struct ChallengeOptions {
let faceMovementChallengeOption: FaceMovementChallengeOption
let faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption

public init(faceMovementChallengeOption: FaceMovementChallengeOption,
faceMovementAndLightChallengeOption: FaceMovementAndLightChallengeOption) {
self.faceMovementChallengeOption = faceMovementChallengeOption
self.faceMovementAndLightChallengeOption = faceMovementAndLightChallengeOption
}
}

public struct FaceMovementChallengeOption {
let challenge: Challenge
let camera: LivenessCamera

public init(camera: LivenessCamera) {
self.challenge = .init(version: "1.0.0", type: .faceMovementChallenge)
self.camera = camera
}
}

public struct FaceMovementAndLightChallengeOption {
let challenge: Challenge
let camera: LivenessCamera

public init() {
self.challenge = .init(version: "2.0.0", type: .faceMovementAndLightChallenge)
self.camera = .front
}
}
Original file line number Diff line number Diff line change
@@ -113,7 +113,7 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
self.faceMatchedTimestamp = Date().timestampMilliseconds

// next step after face match
switch self.challenge?.type {
switch self.challengeReceived?.type {
case .faceMovementAndLightChallenge:
if let colorSequences = colorSequences {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
@@ -146,7 +146,7 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
DispatchQueue.main.async {
self.livenessState
.unrecoverableStateEncountered(.timedOut)
self.captureSession.stopRunning()
self.captureSession?.stopRunning()
}
}
}
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ class FaceLivenessDetectionViewModel: ObservableObject {
@Published var livenessState: LivenessStateMachine

weak var livenessViewControllerDelegate: FaceLivenessViewControllerPresenter?
let captureSession: LivenessCaptureSession
var captureSession: LivenessCaptureSession?
var closeButtonAction: () -> 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 { }
Original file line number Diff line number Diff line change
@@ -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
9 changes: 7 additions & 2 deletions Tests/FaceLivenessTests/CredentialsProviderTestCase.swift
Original file line number Diff line number Diff line change
@@ -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 }
)
10 changes: 6 additions & 4 deletions Tests/FaceLivenessTests/LivenessTests.swift
Original file line number Diff line number Diff line change
@@ -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,