Skip to content

Commit 83e9e33

Browse files
authored
feat: add back camera support (#180)
* chore: back camera support * Add code for error scenarios * update error codes and message * Add challengeOption parameter and remove error codes * Update ChallengeOptions and use camera position based on challenge type received * Add UI changes for selecting back camera in HostApp * add default parameter to ChallengeOptions init * fix formatting * fix test build
1 parent 16a4ded commit 83e9e33

16 files changed

+219
-86
lines changed

HostApp/HostApp.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
9070FFBD285112B5009867D5 /* HostAppUITests */,
132132
9070FFA1285112B4009867D5 /* Products */,
133133
90215EED291E9FB60050F2AD /* Frameworks */,
134+
A5A9AF5054D0FF13505B212A /* AmplifyConfig */,
134135
);
135136
sourceTree = "<group>";
136137
};
@@ -213,6 +214,15 @@
213214
path = Model;
214215
sourceTree = "<group>";
215216
};
217+
A5A9AF5054D0FF13505B212A /* AmplifyConfig */ = {
218+
isa = PBXGroup;
219+
children = (
220+
973619242BA378690003A590 /* awsconfiguration.json */,
221+
973619232BA378690003A590 /* amplifyconfiguration.json */,
222+
);
223+
name = AmplifyConfig;
224+
sourceTree = "<group>";
225+
};
216226
/* End PBXGroup section */
217227

218228
/* Begin PBXNativeTarget section */

HostApp/HostApp/Views/ExampleLivenessView.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,27 @@ import SwiftUI
99
import FaceLiveness
1010

1111
struct ExampleLivenessView: View {
12-
@Binding var isPresented: Bool
12+
@Binding var containerViewState: ContainerViewState
1313
@ObservedObject var viewModel: ExampleLivenessViewModel
1414

15-
init(sessionID: String, isPresented: Binding<Bool>) {
16-
self.viewModel = .init(sessionID: sessionID)
17-
self._isPresented = isPresented
15+
init(sessionID: String, containerViewState: Binding<ContainerViewState>) {
16+
self._containerViewState = containerViewState
17+
if case let .liveness(selectedCamera) = _containerViewState.wrappedValue {
18+
self.viewModel = .init(sessionID: sessionID, presentationState: .liveness(selectedCamera))
19+
} else {
20+
self.viewModel = .init(sessionID: sessionID)
21+
}
1822
}
1923

2024
var body: some View {
2125
switch viewModel.presentationState {
22-
case .liveness:
26+
case .liveness(let camera):
2327
FaceLivenessDetectorView(
2428
sessionID: viewModel.sessionID,
2529
region: "us-east-1",
30+
challengeOptions: .init(faceMovementChallengeOption: FaceMovementChallengeOption(camera: camera)),
2631
isPresented: Binding(
27-
get: { viewModel.presentationState == .liveness },
32+
get: { viewModel.presentationState == .liveness(camera) },
2833
set: { _ in }
2934
),
3035
onCompletion: { result in
@@ -33,11 +38,11 @@ struct ExampleLivenessView: View {
3338
case .success:
3439
withAnimation { viewModel.presentationState = .result }
3540
case .failure(.sessionNotFound), .failure(.cameraPermissionDenied), .failure(.accessDenied):
36-
viewModel.presentationState = .liveness
37-
isPresented = false
41+
viewModel.presentationState = .liveness(camera)
42+
containerViewState = .startSession
3843
case .failure(.userCancelled):
39-
viewModel.presentationState = .liveness
40-
isPresented = false
44+
viewModel.presentationState = .liveness(camera)
45+
containerViewState = .startSession
4146
case .failure(.sessionTimedOut):
4247
viewModel.presentationState = .error(.sessionTimedOut)
4348
case .failure(.socketClosed):
@@ -46,6 +51,10 @@ struct ExampleLivenessView: View {
4651
viewModel.presentationState = .error(.countdownFaceTooClose)
4752
case .failure(.invalidSignature):
4853
viewModel.presentationState = .error(.invalidSignature)
54+
case .failure(.faceInOvalMatchExceededTimeLimitError):
55+
viewModel.presentationState = .error(.faceInOvalMatchExceededTimeLimitError)
56+
case .failure(.internalServer):
57+
viewModel.presentationState = .error(.internalServer)
4958
case .failure(.cameraNotAvailable):
5059
viewModel.presentationState = .error(.cameraNotAvailable)
5160
case .failure(.validation):
@@ -58,11 +67,11 @@ struct ExampleLivenessView: View {
5867
}
5968
}
6069
)
61-
.id(isPresented)
70+
.id(containerViewState)
6271
case .result:
6372
LivenessResultView(
6473
sessionID: viewModel.sessionID,
65-
onTryAgain: { isPresented = false },
74+
onTryAgain: { containerViewState = .startSession },
6675
content: {
6776
LivenessResultContentView(fetchResults: viewModel.fetchLivenessResult)
6877
}
@@ -71,7 +80,7 @@ struct ExampleLivenessView: View {
7180
case .error(let detectionError):
7281
LivenessResultView(
7382
sessionID: viewModel.sessionID,
74-
onTryAgain: { isPresented = false },
83+
onTryAgain: { containerViewState = .startSession },
7584
content: {
7685
switch detectionError {
7786
case .socketClosed:

HostApp/HostApp/Views/ExampleLivenessViewModel.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import FaceLiveness
1010
import Amplify
1111

1212
class ExampleLivenessViewModel: ObservableObject {
13-
@Published var presentationState = PresentationState.liveness
13+
@Published var presentationState: PresentationState = .liveness(.front)
1414
let sessionID: String
1515

16-
init(sessionID: String) {
16+
init(sessionID: String, presentationState: PresentationState = .liveness(.front)) {
1717
self.sessionID = sessionID
18+
self.presentationState = presentationState
1819
}
1920

2021
func fetchLivenessResult() async throws -> LivenessResultContentView.Result {
@@ -30,6 +31,6 @@ class ExampleLivenessViewModel: ObservableObject {
3031
}
3132

3233
enum PresentationState: Equatable {
33-
case liveness, result, error(FaceLivenessDetectionError)
34+
case liveness(LivenessCamera), result, error(FaceLivenessDetectionError)
3435
}
3536
}

HostApp/HostApp/Views/RootView.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,32 @@
66
//
77

88
import SwiftUI
9+
import FaceLiveness
910

1011
struct RootView: View {
1112
@EnvironmentObject var sceneDelegate: SceneDelegate
1213
@State var sessionID = ""
13-
@State var isPresentingContainerView = false
14+
@State var containerViewState = ContainerViewState.startSession
1415

1516
var body: some View {
16-
if isPresentingContainerView {
17+
switch containerViewState {
18+
case .liveness:
1719
ExampleLivenessView(
1820
sessionID: sessionID,
19-
isPresented: $isPresentingContainerView
21+
containerViewState: $containerViewState
2022
)
21-
} else {
23+
case .startSession:
2224
StartSessionView(
2325
sessionID: $sessionID,
24-
isPresentingContainerView: $isPresentingContainerView
26+
containerViewState: $containerViewState
2527
)
2628
.background(Color.dynamicColors(light: .white, dark: .secondarySystemBackground))
2729
.edgesIgnoringSafeArea(.all)
2830
}
2931
}
3032
}
33+
34+
enum ContainerViewState: Hashable {
35+
case liveness(LivenessCamera)
36+
case startSession
37+
}

HostApp/HostApp/Views/StartSessionView.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ struct StartSessionView: View {
1212
@EnvironmentObject var sceneDelegate: SceneDelegate
1313
@ObservedObject var viewModel = StartSessionViewModel()
1414
@Binding var sessionID: String
15-
@Binding var isPresentingContainerView: Bool
15+
@Binding var containerViewState: ContainerViewState
1616
@State private var showAlert = false
1717

1818
var body: some View {
@@ -26,7 +26,7 @@ struct StartSessionView: View {
2626
)
2727

2828
button(
29-
text: "Create Liveness Session",
29+
text: "Create Liveness Session (front camera)",
3030
backgroundColor: .dynamicColors(
3131
light: .hex("#047D95"),
3232
dark: .hex("#7dd6e8")
@@ -35,7 +35,7 @@ struct StartSessionView: View {
3535
viewModel.createSession { sessionId, err in
3636
if let sessionId = sessionId {
3737
sessionID = sessionId
38-
isPresentingContainerView = true
38+
containerViewState = .liveness(.front)
3939
}
4040

4141
showAlert = err != nil
@@ -50,7 +50,38 @@ struct StartSessionView: View {
5050
dismissButton: .default(
5151
Text("OK"),
5252
action: {
53-
isPresentingContainerView = false
53+
containerViewState = .startSession
54+
}
55+
)
56+
)
57+
}
58+
59+
button(
60+
text: "Create Liveness Session (back camera)",
61+
backgroundColor: .dynamicColors(
62+
light: .hex("#047D95"),
63+
dark: .hex("#7dd6e8")
64+
),
65+
action: {
66+
viewModel.createSession { sessionId, err in
67+
if let sessionId = sessionId {
68+
sessionID = sessionId
69+
containerViewState = .liveness(.back)
70+
}
71+
72+
showAlert = err != nil
73+
}
74+
},
75+
enabled: viewModel.isSignedIn
76+
)
77+
.alert(isPresented: $showAlert) {
78+
Alert(
79+
title: Text("Error Creating Liveness Session"),
80+
message: Text("Unable to create a liveness session id. Please try again."),
81+
dismissButton: .default(
82+
Text("OK"),
83+
action: {
84+
containerViewState = .startSession
5485
}
5586
)
5687
)

Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct CameraPreviewView: View {
1515

1616
@StateObject var model: CameraPreviewViewModel
1717

18-
init(model: CameraPreviewViewModel = CameraPreviewViewModel()) {
18+
init(model: CameraPreviewViewModel = CameraPreviewViewModel(cameraPosition: .front)) {
1919
self._model = StateObject(wrappedValue: model)
2020
}
2121

Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ class CameraPreviewViewModel: NSObject, ObservableObject {
1616
@Published var buffer: CVPixelBuffer?
1717

1818
var previewCaptureSession: LivenessCaptureSession?
19+
let cameraPosition: LivenessCamera
1920

20-
override init() {
21+
init(cameraPosition: LivenessCamera) {
22+
self.cameraPosition = cameraPosition
23+
2124
super.init()
2225
setupSubscriptions()
2326

2427
let avCaptureDevice = AVCaptureDevice.DiscoverySession(
2528
deviceTypes: [.builtInWideAngleCamera],
2629
mediaType: .video,
27-
position: .front
30+
position: cameraPosition == .front ? .front : .back
2831
).devices.first
2932

3033
let outputDelegate = CameraPreviewOutputSampleBufferDelegate { [weak self] buffer in

Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,24 @@ struct GetReadyPageView: View {
1212
let beginCheckButtonDisabled: Bool
1313
let onBegin: () -> Void
1414
let challenge: Challenge
15+
let cameraPosition: LivenessCamera
1516

1617
init(
1718
onBegin: @escaping () -> Void,
1819
beginCheckButtonDisabled: Bool = false,
19-
challenge: Challenge
20+
challenge: Challenge,
21+
cameraPosition: LivenessCamera
2022
) {
2123
self.onBegin = onBegin
2224
self.beginCheckButtonDisabled = beginCheckButtonDisabled
2325
self.challenge = challenge
26+
self.cameraPosition = cameraPosition
2427
}
2528

2629
var body: some View {
2730
VStack {
2831
ZStack {
29-
CameraPreviewView()
32+
CameraPreviewView(model: CameraPreviewViewModel(cameraPosition: cameraPosition))
3033
VStack {
3134
WarningBox(
3235
titleText: LocalizedStrings.get_ready_photosensitivity_title,
@@ -77,8 +80,9 @@ struct GetReadyPageView: View {
7780

7881
struct GetReadyPageView_Previews: PreviewProvider {
7982
static var previews: some View {
80-
GetReadyPageView(onBegin: {},
81-
challenge: .init(version: "2.0.0",
82-
type: .faceMovementAndLightChallenge))
83+
GetReadyPageView(
84+
onBegin: {},
85+
challenge: .init(version: "2.0.0", type: .faceMovementAndLightChallenge),
86+
cameraPosition: .front)
8387
}
8488
}

Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ struct InstructionContainerView: View {
110110
)
111111
}
112112
case .faceMatched:
113-
if let challenge = viewModel.challenge,
113+
if let challenge = viewModel.challengeReceived,
114114
case .faceMovementAndLightChallenge = challenge.type {
115115
InstructionView(
116116
text: LocalizedStrings.challenge_instruction_hold_still,

Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public struct FaceLivenessDetectionError: Error, Equatable {
125125
message: "The signature on the request is invalid.",
126126
recoverySuggestion: "Ensure the device time is correct and try again."
127127
)
128-
128+
129129
public static let cameraNotAvailable = FaceLivenessDetectionError(
130130
code: 18,
131131
message: "The camera is not available.",

0 commit comments

Comments
 (0)