From 471ac56afebb7c999ac29b3232a6cdbadbd5480d Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:51:39 -0600 Subject: [PATCH] feat(ux): update get ready page with new preview screen (#78) * add preview/hair check screen * chore(ux): add error log message * chore(ux): update handling of camera permissions * chore: reset integration test * fix build error * fix build error * chore: misc ux updates * chore: update sample and integration app results screen * chore: update UX * chore: minor UX tweak on oval dimension * chore: remove camera permission title * chore: update UX * chore: update per review feedback * chore: update localization strings * update readme --- .../LivenessResultContentView+Result.swift | 6 +- .../Views/LivenessResultContentView.swift | 44 +++++++++- HostApp/README.md | 2 +- .../AV/CMSampleBuffer+Rotate.swift | 42 ---------- .../AV/LivenessCaptureSession.swift | 41 +++++---- .../AV/OutputSampleBufferCapturer.swift | 2 +- Sources/FaceLiveness/AV/VideoChunker.swift | 10 +-- .../Contents.json | 21 ----- .../illustration_face_good_fit.png | Bin 3737 -> 0 bytes .../Contents.json | 21 ----- .../illustration_face_too_close.png | Bin 2434 -> 0 bytes .../Contents.json | 21 ----- .../illustration_face_too_far.png | Bin 6143 -> 0 bytes .../Resources/Base.lproj/Localizable.strings | 19 ++--- .../Utilities/CGImage+Convert.swift | 26 ++++++ .../Utilities/Color+Liveness.swift | 9 +- .../Utilities/LivenessLocalizedStrings.swift | 45 ++++------ .../CameraPermissionView.swift | 79 ++++++++++++++++++ .../GetReadyPage/CameraPreviewView.swift | 50 +++++++++++ .../GetReadyPage/CameraPreviewViewModel.swift | 64 ++++++++++++++ .../Views/GetReadyPage/GetReadyPageView.swift | 78 ++--------------- .../Views/GetReadyPage/ImageFrameView.swift | 35 ++++++++ .../OvalIllustrationExamples.swift | 43 ---------- .../OvalIllustrationIconView.swift | 47 ----------- .../GetReadyPage/OvalIllustrationView.swift | 36 -------- .../InstructionContainerView.swift | 11 ++- .../Views/Liveness/CameraView.swift | 8 -- .../Liveness/FaceLivenessDetectionView.swift | 47 +++++++---- ...ViewModel+FaceDetectionResultHandler.swift | 4 +- .../FaceLivenessDetectionViewModel.swift | 19 +++-- .../FaceLiveness/Views/ProgressBarView.swift | 2 +- .../MockLivenessCaptureSession.swift | 6 +- .../LivenessResultContentView+Result.swift | 6 +- .../Views/LivenessResultContentView.swift | 43 ++++++++++ 34 files changed, 477 insertions(+), 410 deletions(-) delete mode 100644 Sources/FaceLiveness/AV/CMSampleBuffer+Rotate.swift delete mode 100644 Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_good_fit.imageset/Contents.json delete mode 100644 Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_good_fit.imageset/illustration_face_good_fit.png delete mode 100644 Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_close.imageset/Contents.json delete mode 100644 Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_close.imageset/illustration_face_too_close.png delete mode 100644 Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_far.imageset/Contents.json delete mode 100644 Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_far.imageset/illustration_face_too_far.png create mode 100644 Sources/FaceLiveness/Utilities/CGImage+Convert.swift create mode 100644 Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift create mode 100644 Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift create mode 100644 Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift create mode 100644 Sources/FaceLiveness/Views/GetReadyPage/ImageFrameView.swift delete mode 100644 Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationExamples.swift delete mode 100644 Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationIconView.swift delete mode 100644 Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationView.swift diff --git a/HostApp/HostApp/Views/LivenessResultContentView+Result.swift b/HostApp/HostApp/Views/LivenessResultContentView+Result.swift index 749e88a7..3f57982f 100644 --- a/HostApp/HostApp/Views/LivenessResultContentView+Result.swift +++ b/HostApp/HostApp/Views/LivenessResultContentView+Result.swift @@ -14,7 +14,8 @@ extension LivenessResultContentView { let valueTextColor: Color let valueBackgroundColor: Color let auditImage: Data? - + let isLive: Bool + init(livenessResult: LivenessResult) { guard livenessResult.confidenceScore > 0 else { text = "" @@ -22,9 +23,10 @@ extension LivenessResultContentView { valueTextColor = .clear valueBackgroundColor = .clear auditImage = nil + isLive = false return } - + isLive = livenessResult.isLive let truncated = String(format: "%.4f", livenessResult.confidenceScore) value = truncated if livenessResult.isLive { diff --git a/HostApp/HostApp/Views/LivenessResultContentView.swift b/HostApp/HostApp/Views/LivenessResultContentView.swift index b1787015..8ebda4ff 100644 --- a/HostApp/HostApp/Views/LivenessResultContentView.swift +++ b/HostApp/HostApp/Views/LivenessResultContentView.swift @@ -17,7 +17,10 @@ struct LivenessResultContentView: View { Text("Result:") Text(result.text) .fontWeight(.semibold) - + .foregroundColor(result.valueTextColor) + .padding(6) + .background(result.valueBackgroundColor) + .cornerRadius(8) } .padding(.bottom, 12) @@ -42,6 +45,20 @@ struct LivenessResultContentView: View { .frame(maxWidth: .infinity, idealHeight: 268) .background(Color.secondary.opacity(0.1)) } + + if !result.isLive { + steps() + .padding() + .background( + Rectangle() + .foregroundColor( + .dynamicColors( + light: .hex("#ECECEC"), + dark: .darkGray + ) + ) + .cornerRadius(6)) + } } .padding(.bottom, 16) .onAppear { @@ -54,6 +71,31 @@ struct LivenessResultContentView: View { } } } + + private func steps() -> some View { + func step(number: Int, text: String) -> some View { + HStack(alignment: .top) { + Text("\(number).") + Text(text) + } + } + + return VStack( + alignment: .leading, + spacing: 8 + ) { + Text("Tips to pass the video check:") + .fontWeight(.semibold) + step(number: 1, text: "Maximize your screen's brightness.") + .accessibilityElement(children: .combine) + + step(number: 2, text: "Avoid very bright lighting conditions, such as direct sunlight.") + .accessibilityElement(children: .combine) + + step(number: 3, text: "Remove sunglasses, mask, hat, or anything blocking your face.") + .accessibilityElement(children: .combine) + } + } } diff --git a/HostApp/README.md b/HostApp/README.md index 38b60704..1234c14b 100644 --- a/HostApp/README.md +++ b/HostApp/README.md @@ -29,7 +29,7 @@ cd amplify-ui-swift-livenes/HostApp 7. Once signed in and authenticated, the "Create Liveness Session" is enabled. Click the button to generate and get a session id from your backend. -8. Once a session id is created, the Liveness Check screen is displayed. Follow the instructions and click on Begin Check button to begin liveness verification. +8. Once a session id is created, the Liveness Check screen is displayed. Follow the instructions and click on Start video check button to begin liveness verification. ## Provision AWS Backend Resources diff --git a/Sources/FaceLiveness/AV/CMSampleBuffer+Rotate.swift b/Sources/FaceLiveness/AV/CMSampleBuffer+Rotate.swift deleted file mode 100644 index a5ecedd9..00000000 --- a/Sources/FaceLiveness/AV/CMSampleBuffer+Rotate.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import AVFoundation -import CoreImage - -extension CMSampleBuffer { - func rotateRightUpMirrored() -> CVPixelBuffer? { - guard let pixelBuffer = CMSampleBufferGetImageBuffer(self) else { - return nil - } - - var cvPixelBufferPtr: CVPixelBuffer? - - let error = CVPixelBufferCreate( - kCFAllocatorDefault, - CVPixelBufferGetHeight(pixelBuffer), - CVPixelBufferGetWidth(pixelBuffer), - kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, - nil, - &cvPixelBufferPtr - ) - - guard error == kCVReturnSuccess, - let cvPixelBuffer = cvPixelBufferPtr - else { - return nil - } - - let ciImage = CIImage(cvPixelBuffer: pixelBuffer) - .oriented(.right) - .oriented(.upMirrored) - - let context = CIContext(options: nil) - context.render(ciImage, to: cvPixelBuffer) - return cvPixelBuffer - } -} diff --git a/Sources/FaceLiveness/AV/LivenessCaptureSession.swift b/Sources/FaceLiveness/AV/LivenessCaptureSession.swift index 995a09ae..9cd8eccf 100644 --- a/Sources/FaceLiveness/AV/LivenessCaptureSession.swift +++ b/Sources/FaceLiveness/AV/LivenessCaptureSession.swift @@ -11,15 +11,34 @@ import AVFoundation class LivenessCaptureSession { let captureDevice: LivenessCaptureDevice private let captureQueue = DispatchQueue(label: "com.amazonaws.faceliveness.cameracapturequeue") - let outputDelegate: OutputSampleBufferCapturer + let outputDelegate: AVCaptureVideoDataOutputSampleBufferDelegate var captureSession: AVCaptureSession? + + var outputSampleBufferCapturer: OutputSampleBufferCapturer? { + return outputDelegate as? OutputSampleBufferCapturer + } - init(captureDevice: LivenessCaptureDevice, outputDelegate: OutputSampleBufferCapturer) { + init(captureDevice: LivenessCaptureDevice, outputDelegate: AVCaptureVideoDataOutputSampleBufferDelegate) { self.captureDevice = captureDevice self.outputDelegate = outputDelegate } func startSession(frame: CGRect) throws -> CALayer { + try startSession() + + guard let captureSession = captureSession else { + throw LivenessCaptureSessionError.captureSessionUnavailable + } + + let previewLayer = previewLayer( + frame: frame, + for: captureSession + ) + + return previewLayer + } + + func startSession() throws { guard let camera = captureDevice.avCaptureDevice else { throw LivenessCaptureSessionError.cameraUnavailable } @@ -44,17 +63,10 @@ class LivenessCaptureSession { captureSession.startRunning() } - let previewLayer = previewLayer( - frame: frame, - for: captureSession - ) - videoOutput.setSampleBufferDelegate( outputDelegate, queue: captureQueue ) - - return previewLayer } func stopRunning() { @@ -83,6 +95,11 @@ class LivenessCaptureSession { _ output: AVCaptureVideoDataOutput, for captureSession: AVCaptureSession ) throws { + if captureSession.canAddOutput(output) { + captureSession.addOutput(output) + } else { + throw LivenessCaptureSessionError.captureSessionOutputUnavailable + } output.videoSettings = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA ] @@ -92,12 +109,6 @@ class LivenessCaptureSession { .forEach { $0.videoOrientation = .portrait } - - if captureSession.canAddOutput(output) { - captureSession.addOutput(output) - } else { - throw LivenessCaptureSessionError.captureSessionOutputUnavailable - } } private func previewLayer( diff --git a/Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift b/Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift index 0ec9b65c..23f6defb 100644 --- a/Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift +++ b/Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift @@ -24,7 +24,7 @@ class OutputSampleBufferCapturer: NSObject, AVCaptureVideoDataOutputSampleBuffer ) { videoChunker.consume(sampleBuffer) - guard let imageBuffer = sampleBuffer.rotateRightUpMirrored() + guard let imageBuffer = sampleBuffer.imageBuffer else { return } faceDetector.detectFaces(from: imageBuffer) diff --git a/Sources/FaceLiveness/AV/VideoChunker.swift b/Sources/FaceLiveness/AV/VideoChunker.swift index 326e2bc1..7e17e2f3 100644 --- a/Sources/FaceLiveness/AV/VideoChunker.swift +++ b/Sources/FaceLiveness/AV/VideoChunker.swift @@ -34,9 +34,9 @@ final class VideoChunker { func start() { guard state == .pending else { return } - state = .writing assetWriter.startWriting() assetWriter.startSession(atSourceTime: .zero) + state = .writing } func finish(singleFrame: @escaping (UIImage) -> Void) { @@ -49,8 +49,8 @@ final class VideoChunker { func consume(_ buffer: CMSampleBuffer) { if state == .awaitingSingleFrame { - guard let rotated = buffer.rotateRightUpMirrored() else { return } - let singleFrame = singleFrame(from: rotated) + guard let imageBuffer = buffer.imageBuffer else { return } + let singleFrame = singleFrame(from: imageBuffer) provideSingleFrame?(singleFrame) state = .complete } @@ -66,10 +66,10 @@ final class VideoChunker { if assetWriterInput.isReadyForMoreMediaData { let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer).seconds let presentationTime = CMTime(seconds: timestamp - startTimeSeconds, preferredTimescale: 600) - guard let rotated = buffer.rotateRightUpMirrored() else { return } + guard let imageBuffer = buffer.imageBuffer else { return } pixelBufferAdaptor.append( - rotated, + imageBuffer, withPresentationTime: presentationTime ) } diff --git a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_good_fit.imageset/Contents.json b/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_good_fit.imageset/Contents.json deleted file mode 100644 index a46c819a..00000000 --- a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_good_fit.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "illustration_face_good_fit.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_good_fit.imageset/illustration_face_good_fit.png b/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_good_fit.imageset/illustration_face_good_fit.png deleted file mode 100644 index 80f924b48c975575a44a949df0ad7a6d03a23450..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3737 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 DfiDsp diff --git a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_close.imageset/Contents.json b/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_close.imageset/Contents.json deleted file mode 100644 index aaa99aa6..00000000 --- a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_close.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "illustration_face_too_close.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_close.imageset/illustration_face_too_close.png b/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_close.imageset/illustration_face_too_close.png deleted file mode 100644 index daedb319511c8e2344dfe38ef2eb1691698744cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2434 zcmV-|34Qj7P)Dg|00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP{8YfPrL(a?k#U0D)jVZuj*1yT)ET;K{5G`Jz5qAMaMvO*IS zNDK)H7{HyHl$|b!ZHN#ECWDk#K5A!Z>CmQ~=lkF3Yx*%?t#j_joB1V^e#|S(egAvT zz3-lL&$S%K=^>&cWH8RNQKI-5Iq^|)CdY{q(I5G*U)( zU=>-ljbsu=iRX+AEy>byW~M{X=wq^LLu3W&$pp+0&%L?R|J-q1UYuBgh^{4C?$~M9 zg|r_NwHFWWG2xC+dAGJq|L52+O}mqzl2EJG0@|c8!cAgKk|^9Pu=ar)%>mZ0qk!6pr<-sdItr+jc;ca3L*YCyadIZcNf-1H zZz7(YRqD*l7>LP?4(K5sW_AzmkP+Zn@L_~>Knw8_kMC*In{xpR;c)S)Q7#^Cf>eeiiYAmx=SiiDxMHd#p+3(5K#$*?o*j~cxKXd5Q#zXAFy19Sy~ zoj84t_Uzn32fBBs{H#`0J=qN{#Elg35JE`|-p+a*dUNM%8%Ss1G1j+-GhbwvP|3Pi z*i-1GOjvTAsOT;n$SNGbJ_-rpB01v4N2$<|F|+0u4=L(Cezr@8i~uvcc!~HEDoEfu znLQ4_Lfj-aoGYNx@){@(r8YJag{+ zBH=(}V30O-?H8dTKe6fE{fmSHXfI-!9I%O(=>wye?mN^&egPpZL>to4-``Ihu!+~# z*O&2q^wO)pT_Zp6`H62czHgl6MWfjlW>dKt(?A>G8Z%*>c-bruAos&`GPz5STsrb{ zU=c6Z4~twD(}|8|UjodfBQFOQ@wnWCsrb_Jf&qOKjdCTIHW(#dES6ggFQJ-&)8Kpb?&RPDukr;Z(AX)jAu z!P}Pryi|6qXJUNVyJM>_CMv1GVmlV&Fa6Rx+uo15yM%CdC`?vGe#b_3@roqj5Krq$ zw?#5j6whFa;u#E!c-C{}RAd1R+r5gGCktc5v&=h}ha1bzf1)0bcqK*g7Ga9zS*67> zZyrqBn|UnHz~js~;p)tM;z<$^%Te0rfEs0a;$M~{9+H@{Wp62`drQ$;8WJzhlec8!WLzAYs|r|PO?8#J z`kg4<*wR5<@RlitzsK08=(rZu>4n@gZuVeozB8tbqOyiK;g6)RsG>rJd&0&po?Tf> zg$Tkx#=A{C>lLKytl;-JauSdV&~;kX^~8-7@$A4VDkOx{MbrwKk5@wjP!eR0(&lS$9%zx!O-LPupSRIg>?v68d{EV2W1mv|* zu6XW>gRdtplE-!F8%?ZQ|5mxRYu1oVk~tQ#MBRlpHO%PCGK_G`Gd1!ksG>wbLZ^uFiDa5T(=Old(yG$vbuTRuaN2W-LW1~>RyLD zOmrf2CZrXjM3tA!B-~}48h7cTyr6)Xyex2^NF2Iz^|H8O8@6^SNo9xYM(5f0X%BsM z_+wEVZZ5)IDww3q`wX&<+RjK~TAb#b>g*x(d@q%K&yFqZWga0KTqNp;9q8_&y*sxo zx(1!iu793Xqlb84YBHJQnI=up6S42+EpbyZ_T)1k6II&1qHpO$1HxUddzo}VAMq%8 zs&ir}bE$iX*o(uKE?yD!owvDTWJh~=S!WY@2Xlxc0<2`+%uL693*kaxBOK69Jh0r7j~K5Y4$fNy0IpFp{UW>8nDjd+Z_80`T*Sx4SbwZ4BaFrSg2ed96hqQ^3rgkM2m9TKG+ZeYH z{j2`DH&5(wle7#4H;F}aF{{)(VM@y}ZxQZIv{$!~<5~m8L1$`$I1tv>(4w`q&BYAy zW`OnKg~y3}iU`rHx<=z-j7Ev)F;7Iio-I9uBcqHeYG4@RMch{}8Hgt$O-~Vx zM`?O&*!?<|{trssX@5nv8_KCorlgk`*>`9853EnN7f7IYMF0Q*07*qoM6N<$f@$5V A$^ZZW diff --git a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_far.imageset/Contents.json b/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_far.imageset/Contents.json deleted file mode 100644 index 98f6d2b6..00000000 --- a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_far.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "illustration_face_too_far.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_far.imageset/illustration_face_too_far.png b/Sources/FaceLiveness/Resources/Assets.xcassets/illustration_face_too_far.imageset/illustration_face_too_far.png deleted file mode 100644 index 30128a7cfe6d5e7c935c928917d855c286fc4d06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6143 zcmVDg|00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP=lL!=$2RPHdV9&|8wq=^-EO1YoE$1Hnn8sH)A(^3 zqe)3e@<7n3=4NVZKg&roLQxd95n0&A8b_m~@b^dAG5ZUG%auu$3oEE>Rw<<+(vQ6Q zFPrE@UA-=0Aj)R3klkhpSlCO8Md1{&b}18m-#5L`Y#3+fp2`yUGTT=D;fnodQeBzb~3 z?3tiZ79&oxZAMbGof0sTA|%f6zA_P!VlOsLauo|Z#{Yv&HC{(5BIU?4-Y|#FO72Vt zzmZ1C36&s?r~s2nz6sJaA_GQpLVrVi*GQz5ZEmLx9&Abzm{R105(f#eXv7H^q)Oay zVY~Qda9Z$PZqE$TIIaT-QbT^5|6jM$5x{k%DuF3O9^y7#Ra#Wl73xhIUJD5lp@4oK z2s4~3>{-D0zLCffG6SIw4J4nbY@I;g$OxqhQii-wj~(YX2+l$_yJ4aBib8Cc=A@{1 zJUAd%3~Evkq!>d33O+etkT_im4Zp*#CnJFC!u5T0=m@0>Y?K11Jz39FUARFxt_-pm zqzVT{0-?@?cWF?WI@;UYslUIUZ-CqF=37xwk@i#icXYBsLyeFg!*{r@o`GTR{noRX zvtn*JB^ya6554&WBM)$8*adNbVvsQXhLeG;qeqV_4Gj(0Q`?y)>c7xQZikiZOwBoL zs!|yk9w9Fa!oB_g6_m^&hts7jUAmOmrYu{wj3`+321%$<9m9!X8bRqD9HttUG{|Fd z;B=PUP9~Bbq`&iCH7C%;oP$h_wGJ&`TA%^u9J$~8Qe3B z1U{A$>l*S=bJrjZkex1Cwu;uST}x}$tl^&f{TCC?q4$uK%3AQS3v$BrFz^4KAon`@6)vbRE zi4-c9#!Tv%CdIY%^m_=2?FIA7>Egwelq4jNJV+a)4#>~WNY; zzO#oeDb1ys*%>4YUosNU`7=3*8#ZjfTe^2^JkQ6H**yauethM+uO_Q5m^|`6I(&pX zDH#qMygB|}sH%c3d-mxksodq0N?IsRwD)lmfBLhZb178DsuFrGwSUN?3^A#N-CKM4 zDoO&9liNJq+`@@dPu{UC%!IEdBEFhiQ2MnMxc`S(Hu7rxV@1`8L|VeLGDM z5u0D9zy|EQGPdQDf8Qvn1v zAOAon&?tSP9d!*&WHwACua()kF{yJH^N0$&F-qY`7v;Mgl!_1pzVN~e{I!-VRh$lc z(rllpA zG&aL-*Q4IHwl-xSyCI0-(gCaI70?qu`By^xDKrPm4xx&xj~z7&rjaLlCDaTH5%iGY z!3XZAm2>hbUEpBeak(-P8}i1nRvPg7$!yHd%cSbJ-_$D?T$^O5s#BN-GhhmNMn}cYD${5zi{}C_|K`P* zLxNs%(ZT=TNrzenlR{uQ%dbNEM2wnYw~>QwC@P!fxR^p-TYCrJXJ?H$BnJ;3R0=JF z6bm}~hH2%prL<)6LQY^^uZ1oQ`p8TmoOyrm8<0NTWd?!3sWUBP223RnjT1KFrT^MN z^QLD}EP%?~a{D*EuCW zq9Jyxm8@DSth&0IM*Cw|Et%Q*T1#KJ{Tx#G{1ZQ-{kvbL-1*C;lL*^~pj}ko zMLSQQX(ri_N*+`P_cAf2#K8k|I&$O)^AlrNYc^M2kje}PLE^6M|EZIBmPu`*flkN* zc0JYSL_3*Hhw79)&QmUVEDHfRY&Lx;9>|+L%|)>QQupA!cSiLc5G^FJWbrE6`&m2x zokRg8E0!e+8vLsV>>V8>6H>`*ZBs8k(@<~)d3iA=pK;T#xNHT*0wIZq-`-7ke*5p} zkDY;ps8o!lL5hzI1AJ{-$k5Z%M>1iK=-*|y)~>I?8fH?`1;jX3twh>sQC7GN*!2sgOpYQllZU_q-YwXbA`|fta8IbV-c& zq>(2|BO9LRi|ZeFg8sS3FMKd^*E=~ zp*IY{Y_tQ4Aa!fk{kb%$3kQ%`y?ZBJbK@=CvB7Zpa0vK3K_knsWjJj73o@M+c{XoI z)%7-(77LjR9u@Z2c1$RBr<&XN7B8UQ!~IrXPB#DFaBMy}-5-`f%#VP_X>21i7yN}x zr$yR8d!aU2BLZS(4!vB7WOyY=sB#pH!!b>11%G=e;tfw~m(#06`!TE%oK z?rp+5iv-OVhG_Zfb@a%6V^W73h-yM;h6O}KLmZHs!95aZyop9M5(b@V!v=?PnLQs!2}ex;r~HO4^;mjmATZ?kCvPcnlmV-tKoj6 z9eGx5*^LRNOHGREx^r}R@9wCj$qXRY6wcGB+i#`oH-4LP)Ii<1ssu*{^EkaM$2q|6 z$8<_Oq&O@CgX1M{BB{GDgdhpI+Skdx{s3 zpl7zf6joh^0ya?q*Z9zP@1ify>EVPvMVhZnp)Efm%kD{$Jh7;o)oL9#?N|V+vA)+r zAMDu`L{!E4oAfGqIDoWoe*aN^+{11@>UgmN4$zCc_wZvBbfXPP)~{O5XWq89wi6+M+pUKChh0zGR2Rt}mxSYHtiUFt3ZDplJ4lsxcBfXFMk_JNb4I zo~I^xK@N+s`Mwc3O=lGbKecDOPN}_Yha8#pB%L*)#h0=ov%Ufk(ksUPQnm>!HFoN@D2-Nu21*;jtAK5FN-g2 zh71Z+_0po=24SS8GH%5tk_xG6)3B5f-y;iwS{E`Bt?d&J5h!D>cvDOK zIZGKpD$XdK@OquTKHi>l9qo8ap`dlpNGg4D$uq3;B_OKkU2Uf)jIHvr-OP@(QanJq zEYi3w9z6Zr3;MuehgBK3u9<8|c35=pYg}XI7P{$~9WRah2JBC~)dJ=&O!(Vz!=My@ z{LHU(sS_~FnAO@$QX$>jB*VZQ;|Cb;_`m!2Q}su|u@y2HfBfQ46c6}s)cz?UB_jdY zVc}#!UB3GAOF@%l&Yk6IQ%{klzvL;CykfX~?lijR2md_ILCMRF%xRenul@2Vy5qq| zdFm?>I97X-J4qn!dyhUoZp61%Eg83Nr%ae`^NfJXsmLCo4IA$Xk`mO{sy~iB+X0CA z3-P4RjDQWr2I8)+oH5~u01Upx-k?57CZv#Ox2fYdunZMxmI4jwIX`~?;#;<2N4j|BB z9ihAK+sX@`ur0V*PxWq1nX=IFni~4*yn;#3QG!=;N>9lXJDp&@k^woArrnyTTl=Y#<3fpWh*nCC zrJ%_GN9qsN?@c)+O&d9_zlNp}bv#*5MUmvNs10RxJ;mrUpX-P&?m};W=qEpm=vKIKOFz|lSKWFyuLeYd4V&(XY9gq?&MAIo^Z{9FQ_8QpTcK~$fb>1LV<#{DOf^J7%sD`S4+-S4S>H)c zpiX9l-YbH8WKpneWTX(z8ID1qKcGsUxF3mzWXT)V-;2j>hV!!N{)gzz?Z4mzOa&0c z$&Rts2htyV`dQj=aZyy?3t5Std|qMm*u=e%0coK^Zmzm$wrAMO8)WpvVgZi>=Va0M9{!O! zOuaYp6FAT@zJAwt=_{qZfXv0i<&mddGz-+ZIzhvp zetLjOAjWw{8VgPYc%TR!c5MGOtzS?W^P1LLkghMg+&?tL?Vd}LvjRvXuTXnbi#B>X zZo3x>Ot-=)GE@$BzV>@gK(vyG5CpLyb)AlTy-&BpM$OI5`pj67l+H^>wFwUkh8k*f zG!Td@T$l(LJ_xT;ga?S-4r&27Y+|@z9kvprL1JN@&;hbC;#21UhCv9)^RsIk(qcTB z&l{39KM!6O^gWg@9P)S)o?gE&=nV=R1c?B$D6nB-?w*)m(A3<@3A_2~E9mf^-*WPC z-l1w=+-qCIPJVXs1b5Cn!}5s(q&~$6P7BEE?Hy1@j#(2Y=y(QDxY5$a7!Q)x)2l9O zI#E^y?_nHiBG7$K9T7J$4g+00=AJM-bpq%d- zv3RDMX6o+ir$i!=`NE2M^Cw-ZBT`^EW9h6p{J6L%`t(|~kWQYM4OBX7HYEyJ$u<&u z7bzfpcqU%FKn7^((k1-3WM=HMpdy8I@=!&F3dzdKqRUn;r$iu8_D+C1Ekp6op(`v~ zMEM1UJVcUJJvYLX1;T~hTfAy5B?1T&%1RR_dAYgSG~kJU4qe%j%lUpz0=2?qFonFb zS*3hmvTzC6o$=W!=7v+}fRE2bPZ*fF>tUfspEq{9hjx`LGEZd!raWp`sLsywYVWN7 z_`sCk0ZF~yQS$f#?52+LZZ^%W(1MvZx~(FQw!iobeqOvIqLE>FZPzxsZCwHNvv9Ar zZG@W7v;W8b@|_mSW~sAm$J7ZT5C;O$PBT;{VA^w{;dLV0QnG02IPySLmuHmvwBDcN zQfPUZoz@gPXjZ%Pg+OxH6 z+~*(>+b4})I)27SSW2xh%SzK2p*G8SGAmYH&G&O=my%g9?Sa3@vcGfSeZH*4|NQz% z`uvMpy1)n%L{&0^E}LtoQbtyeGxAjE?>_9ISB~0f)8B07OBhc@qF8t3$OmuH-RqsS zW?^J%97KNH=%WL*!&KMirxG_F_iZJoP2rBpx{X`8#9epgdNK*-KCM|i7k!}PB+aP* zcTQL|L`?>exb47snpe4+=QTyr8Z(ZNkcOn8Qtt0QK@Z+g7%^!Rq2+>~6Z!g44<~Zd zH@`)dE56Dcl~P)<^b#@&=8|WWzRNrQg`EDxFK2lAypL+mc<8fc5A_VHX|LJS?KEc^ zQ^O0hDA$!(AFZJ;mrgHwgdaod%(8b3O{UpA0O_(hvuE*Sbz}CmRMq`5B^q-I?0oyn zrRGdf@y_0${$D0_CKAVC`U8I}=Bmy?yX!0DvC6G?$p)?;?EtD4R*=~+ojml;`E$$p zac$mTQ#u3M14xF{0j84&pqC=!-kmX@x*hW=9ntB$gi9SrONP_|Ng)rmZ~nY;eq5WM zp3+yBdlTQ6B}wXlq>u-ox1!Oo8;#l5Q97ZCT`#U1*I$_=sRNQm9Q&tPY<91sGg<2>6#+ZfoV8ZJ-c&%+$pg@nFTJ>m zAJ^yJOy`^zQz`-1BkbHI7gZ(Ay9`B2kOv^p74gyu7QCm51JiFW<<|q?`b1$l6_7II z0dTwFz^IPFcalyZh&x(v55FD|m&~wWBBT^~z@-;eQW+~~3Ida*LXkZY#Nm2UjX1#6 zl29~2-4FFYpuBG%&Fy=K%mN&zx}2N&F+ze=@GK{Qxh0`!AbL6oL=f+><}(8KNvQz} zK+2FT@6@C{6;-7XDJyvbZS802_!qVOkuD$(Qdm5Ah!Tx9rY7r|w1Gf`11pwXYRahR zIM9^x} zsCclFki`7qhN#pjqzjS;^(aV#v{h-|Zkiyy$P)-95y CGImage? { + guard let pixelBuffer = cvPixelBuffer else { + return nil + } + + var image: CGImage? + VTCreateCGImageFromCVPixelBuffer( + pixelBuffer, + options: nil, + imageOut: &image + ) + + return image + } +} diff --git a/Sources/FaceLiveness/Utilities/Color+Liveness.swift b/Sources/FaceLiveness/Utilities/Color+Liveness.swift index 1b0e6367..6d998b58 100644 --- a/Sources/FaceLiveness/Utilities/Color+Liveness.swift +++ b/Sources/FaceLiveness/Utilities/Color+Liveness.swift @@ -39,12 +39,17 @@ extension Color { ) static let livenessWarningBackground = Color.dynamicColors( - light: .hex("#F5D9BC"), + light: .hex("#B8CEF9"), dark: .hex("#663300") ) static let livenessWarningLabel = Color.dynamicColors( - light: .hex("#663300"), + light: .hex("#002266"), dark: .hex("#EFBF8F") ) + + static let livenessPreviewBorder = Color.dynamicColors( + light: .hex("#AEB3B7"), + dark: .white + ) } diff --git a/Sources/FaceLiveness/Utilities/LivenessLocalizedStrings.swift b/Sources/FaceLiveness/Utilities/LivenessLocalizedStrings.swift index ba45e269..b0ad3dc2 100644 --- a/Sources/FaceLiveness/Utilities/LivenessLocalizedStrings.swift +++ b/Sources/FaceLiveness/Utilities/LivenessLocalizedStrings.swift @@ -10,10 +10,7 @@ import SwiftUI enum LocalizedStrings { /// en = "Liveness Check" static let get_ready_page_title = "amplify_ui_liveness_get_ready_page_title".localized() - - /// en = "You will go through a face verification process to prove that you are a real person. Your screen's brightness will temporarily be set to 100% for highest accuracy." - static let get_ready_page_description = "amplify_ui_liveness_get_ready_page_description".localized() - + /// en = "Photosensitivity Warning" static let get_ready_photosensitivity_title = "amplify_ui_liveness_get_ready_photosensitivity_title".localized() @@ -29,27 +26,9 @@ enum LocalizedStrings { /// en = "A small percentage of individuals may experience epileptic seizures when exposed to colored lights. Use caution if you, or anyone in your family, have an epileptic condition." static let get_ready_photosensitivity_dialog_description = "amplify_ui_liveness_get_ready_photosensitivity_dialog_description".localized() - /// en = "Follow the instructions to complete the check:" - static let get_ready_steps_title = "amplify_ui_liveness_get_ready_steps_title".localized() - - /// en = "Make sure your face is not covered with sunglasses or a mask." - static let get_ready_face_not_covered = "amplify_ui_liveness_get_ready_face_not_covered".localized() - - /// en = "Move to a well-lit place that is not in direct sunlight." - static let get_ready_lighting = "amplify_ui_liveness_get_ready_lighting".localized() - - /// en = "When an oval appears, fill the oval with your face in it." - static let get_ready_fit_face = "amplify_ui_liveness_get_ready_fit_face".localized() - - /// en = "Begin Check" + /// en = "Start video check" static let get_ready_begin_check = "amplify_ui_liveness_get_ready_begin_check".localized() - /// en = "Illustration demonstrating good fit of face in oval." - static let get_ready_illustration_good_fit_a11y = "amplify_ui_liveness_get_ready_illustration_good_fit_a11y".localized() - - /// en = "Illustration demonstrating face too far from screen." - static let get_ready_illustration_too_far_a11y = "amplify_ui_liveness_get_ready_illustration_too_far_a11y".localized() - /// en = "REC" static let challenge_recording_indicator_label = "amplify_ui_liveness_challenge_recording_indicator_label".localized() @@ -68,7 +47,7 @@ enum LocalizedStrings { /// en = "Hold still" static let challenge_instruction_hold_still = "amplify_ui_liveness_challenge_instruction_hold_still".localized() - /// en = "Ensure only one face is in front of camera" + /// en = "Only one face per check" static let challenge_instruction_multiple_faces_detected = "amplify_ui_liveness_challenge_instruction_multiple_faces_detected".localized() /// en = "Connecting..." @@ -94,11 +73,19 @@ enum LocalizedStrings { /// en = "Close" static let close_button_a11y = "amplify_ui_liveness_close_button_a11y".localized() - - /// en = "Good fit" - static let get_ready_good_fit_example = "amplify_ui_liveness_get_ready_good_fit_example".localized() - /// en = "Too far" - static let get_ready_too_far_example = "amplify_ui_liveness_get_ready_too_far_example".localized() + /// en = "Center your face" + static let preview_center_your_face_text = "amplify_ui_liveness_center_your_face_text".localized() + + /// en = "Liveness check" + static let camera_permission_page_title = "amplify_ui_liveness_camera_permission_page_title".localized() + + /// en = "Change Camera Setting" + static let camera_permission_change_setting_button_title = "amplify_ui_liveness_camera_permission_button_title".localized() + + /// en = "Camera is not accessible" + static let camera_permission_change_setting_header = "amplify_ui_liveness_camera_permission_button_header".localized() + /// en = "You may have to go into settings to grant camera permissions and close the app and retry" + static let camera_permission_change_setting_description = "amplify_ui_liveness_camera_permission_button_description".localized() } diff --git a/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift b/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift new file mode 100644 index 00000000..e5edbf3f --- /dev/null +++ b/Sources/FaceLiveness/Views/CameraPermission/CameraPermissionView.swift @@ -0,0 +1,79 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct CameraPermissionView: View { + @Binding var displayingCameraPermissionsNeededAlert: Bool + + init( + displayingCameraPermissionsNeededAlert: Binding = .constant(false) + ) { + self._displayingCameraPermissionsNeededAlert = displayingCameraPermissionsNeededAlert + } + + var body: some View { + VStack(alignment: .leading) { + Spacer() + VStack { + Text(LocalizedStrings.camera_permission_change_setting_header) + .font(.title2) + .fontWeight(.medium) + .multilineTextAlignment(.center) + .padding(8) + + Text(LocalizedStrings.camera_permission_change_setting_description) + .multilineTextAlignment(.center) + .padding(8) + } + Spacer() + editPermissionButton + } + .alert(isPresented: $displayingCameraPermissionsNeededAlert) { + Alert( + title: Text(LocalizedStrings.camera_setting_alert_title), + message: Text(LocalizedStrings.camera_setting_alert_message), + primaryButton: .default( + Text(LocalizedStrings.camera_setting_alert_update_setting_button_text).bold(), + action: { + goToSettingsAppPage() + }), + secondaryButton: .default( + Text(LocalizedStrings.camera_setting_alert_not_now_button_text) + ) + ) + } + } + + private func goToSettingsAppPage() { + guard let settingsAppURL = URL(string: UIApplication.openSettingsURLString) + else { return } + UIApplication.shared.open(settingsAppURL, options: [:]) + } + + private var editPermissionButton: some View { + Button( + action: goToSettingsAppPage, + label: { + Text(LocalizedStrings.camera_permission_change_setting_button_title) + .foregroundColor(.livenessPrimaryLabel) + .frame(maxWidth: .infinity) + } + ) + .frame(height: 52) + ._background { Color.livenessPrimaryBackground } + .cornerRadius(14) + .padding([.leading, .trailing]) + .padding(.bottom, 16) + } +} + +struct CameraPermissionView_Previews: PreviewProvider { + static var previews: some View { + CameraPermissionView() + } +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift new file mode 100644 index 00000000..f78e8df0 --- /dev/null +++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewView.swift @@ -0,0 +1,50 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct CameraPreviewView: View { + private static let previewWidthRatio = 0.6 + private static let previewHeightRatio = 0.55 + private static let previewXPositionRatio = 0.5 + private static let previewYPositionRatio = 0.6 + + @StateObject var model: CameraPreviewViewModel + + init(model: CameraPreviewViewModel = CameraPreviewViewModel()) { + self._model = StateObject(wrappedValue: model) + } + + var body: some View { + ZStack { + ImageFrameView(image: model.currentImageFrame) + .edgesIgnoringSafeArea(.all) + .mask( + GeometryReader { geometry in + Ellipse() + .frame(width: geometry.size.width*Self.previewWidthRatio, + height: geometry.size.height*Self.previewHeightRatio) + .position(x: geometry.size.width*Self.previewXPositionRatio, + y: geometry.size.height*Self.previewYPositionRatio) + }) + GeometryReader { geometry in + Ellipse() + .stroke(Color.livenessPreviewBorder, style: StrokeStyle(lineWidth: 3)) + .frame(width: geometry.size.width*Self.previewWidthRatio, + height: geometry.size.height*Self.previewHeightRatio) + .position(x: geometry.size.width*Self.previewXPositionRatio, + y: geometry.size.height*Self.previewYPositionRatio) + } + } + } +} + +struct CameraPreviewView_Previews: PreviewProvider { + static var previews: some View { + CameraPreviewView() + } +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift new file mode 100644 index 00000000..eb661451 --- /dev/null +++ b/Sources/FaceLiveness/Views/GetReadyPage/CameraPreviewViewModel.swift @@ -0,0 +1,64 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import CoreImage +import Combine +import AVFoundation +import Amplify + +class CameraPreviewViewModel: NSObject, ObservableObject { + @Published var currentImageFrame: CGImage? + @Published var buffer: CVPixelBuffer? + + var previewCaptureSession: LivenessCaptureSession? + + override init() { + super.init() + setupSubscriptions() + + let avCaptureDevice = AVCaptureDevice.DiscoverySession( + deviceTypes: [.builtInWideAngleCamera], + mediaType: .video, + position: .front + ).devices.first + + self.previewCaptureSession = LivenessCaptureSession( + captureDevice: .init(avCaptureDevice: avCaptureDevice), + outputDelegate: self + ) + + do { + try self.previewCaptureSession?.startSession() + } catch { + Amplify.Logging.default.error("Error starting preview capture session with error: \(error)") + } + } + + func setupSubscriptions() { + self.$buffer + .receive(on: RunLoop.main) + .compactMap { + return CGImage.convert(from: $0) + } + .assign(to: &$currentImageFrame) + } +} + +extension CameraPreviewViewModel: AVCaptureVideoDataOutputSampleBufferDelegate { + func captureOutput( + _ output: AVCaptureOutput, + didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) { + if let buffer = sampleBuffer.imageBuffer { + DispatchQueue.main.async { + self.buffer = buffer + } + } + } +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift index 404d261e..00ecb9b7 100644 --- a/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift +++ b/Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift @@ -8,75 +8,36 @@ import SwiftUI struct GetReadyPageView: View { - @Binding var displayingCameraPermissionsNeededAlert: Bool let beginCheckButtonDisabled: Bool let onBegin: () -> Void init( - displayingCameraPermissionsNeededAlert: Binding = .constant(false), onBegin: @escaping () -> Void, beginCheckButtonDisabled: Bool = false ) { - self._displayingCameraPermissionsNeededAlert = displayingCameraPermissionsNeededAlert self.onBegin = onBegin self.beginCheckButtonDisabled = beginCheckButtonDisabled } var body: some View { VStack { - ScrollView { - VStack(alignment: .leading) { - Text(LocalizedStrings.get_ready_page_title) - .font(.system(size: 34, weight: .semibold)) - .accessibilityAddTraits(.isHeader) - .padding(.bottom, 8) - - Text(LocalizedStrings.get_ready_page_description) - .padding(.bottom, 8) - + ZStack { + CameraPreviewView() + VStack { WarningBox( titleText: LocalizedStrings.get_ready_photosensitivity_title, bodyText: LocalizedStrings.get_ready_photosensitivity_description, popoverContent: { photosensitivityWarningPopoverContent } ) .accessibilityElement(children: .combine) - .padding(.bottom, 8) - - Text(LocalizedStrings.get_ready_steps_title) - .fontWeight(.semibold) - .padding(.bottom, 16) - - OvalIllustrationExamples() - .accessibilityHidden(true) - .padding(.bottom) - - steps() - } - .padding() + Text(LocalizedStrings.preview_center_your_face_text) + .font(.title) + .multilineTextAlignment(.center) + Spacer() + }.padding() } - beginCheckButton } - .alert(isPresented: $displayingCameraPermissionsNeededAlert) { - Alert( - title: Text(LocalizedStrings.camera_setting_alert_title), - message: Text(LocalizedStrings.camera_setting_alert_message), - primaryButton: .default( - Text(LocalizedStrings.camera_setting_alert_update_setting_button_text).bold(), - action: { - goToSettingsAppPage() - }), - secondaryButton: .default( - Text(LocalizedStrings.camera_setting_alert_not_now_button_text) - ) - ) - } - } - - private func goToSettingsAppPage() { - guard let settingsAppURL = URL(string: UIApplication.openSettingsURLString) - else { return } - UIApplication.shared.open(settingsAppURL, options: [:]) } private var beginCheckButton: some View { @@ -107,29 +68,6 @@ struct GetReadyPageView: View { Spacer() } } - - private func steps() -> some View { - func step(number: Int, text: String) -> some View { - HStack(alignment: .top) { - Text("\(number).") - Text(text) - } - } - - return VStack( - alignment: .leading, - spacing: 16 - ) { - step(number: 1, text: LocalizedStrings.get_ready_fit_face) - .accessibilityElement(children: .combine) - - step(number: 2, text: LocalizedStrings.get_ready_face_not_covered) - .accessibilityElement(children: .combine) - - step(number: 3, text: LocalizedStrings.get_ready_lighting) - .accessibilityElement(children: .combine) - } - } } struct GetReadyPageView_Previews: PreviewProvider { diff --git a/Sources/FaceLiveness/Views/GetReadyPage/ImageFrameView.swift b/Sources/FaceLiveness/Views/GetReadyPage/ImageFrameView.swift new file mode 100644 index 00000000..63052904 --- /dev/null +++ b/Sources/FaceLiveness/Views/GetReadyPage/ImageFrameView.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SwiftUI + +struct ImageFrameView: View { + var image: CGImage? + + var body: some View { + if let image = image { + GeometryReader { geometry in + Image(decorative: image, scale: 1.0, orientation: .upMirrored) + .resizable() + .scaledToFill() + .frame( + width: geometry.size.width, + height: geometry.size.height, + alignment: .center) + .clipped() + } + } else { + Color.black + } + } +} + +struct ImageFrameView_Previews: PreviewProvider { + static var previews: some View { + ImageFrameView() + } +} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationExamples.swift b/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationExamples.swift deleted file mode 100644 index b188cff8..00000000 --- a/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationExamples.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import SwiftUI - -struct OvalIllustrationExamples: View { - var body: some View { - HStack(spacing: 16) { - OvalIllustrationView( - icon: .checkmark(backgroundColor: .hex("#365E3D")), - text: { Text(LocalizedStrings.get_ready_good_fit_example) }, - primaryColor: .hex("#365E3D"), - secondaryColor: .hex("#D6F5DB"), - illustration: { Image("illustration_face_good_fit", bundle: .module) } - ) - .accessibilityElement(children: .ignore) - .accessibilityLabel(Text(LocalizedStrings.get_ready_illustration_good_fit_a11y)) - - OvalIllustrationView( - icon: .xmark(backgroundColor: .hex("#660000")), - text: { Text(LocalizedStrings.get_ready_too_far_example) }, - primaryColor: .hex("#660000"), - secondaryColor: .hex("#F5BCBC"), - illustration: { Image("illustration_face_too_far", bundle: .module) } - ) - .accessibilityElement(children: .ignore) - .accessibilityLabel(Text(LocalizedStrings.get_ready_illustration_too_far_a11y)) - - Spacer() - } - } -} - -struct OvalIllustrationExamples_Previews: PreviewProvider { - static var previews: some View { - OvalIllustrationExamples() - .background(Color.purple) - } -} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationIconView.swift b/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationIconView.swift deleted file mode 100644 index 1f1ecbad..00000000 --- a/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationIconView.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import SwiftUI - -struct OvalIllustrationIconView: View { - let systemName: String - let iconColor: Color - let backgroundColor: Color - - init( - systemName: String, - iconColor: Color = .white, - backgroundColor: Color - ) { - self.systemName = systemName - self.iconColor = iconColor - self.backgroundColor = backgroundColor - } - - var body: some View { - Image(systemName: systemName) - .font(.system(size: 12, weight: .heavy)) - .foregroundColor(iconColor) - .frame(width: 15, height: 15) - .padding(5) - .background(backgroundColor) - } - - static func checkmark(backgroundColor: Color) -> Self { - OvalIllustrationIconView( - systemName: "checkmark", - backgroundColor: backgroundColor - ) - } - - static func xmark(backgroundColor: Color) -> Self { - OvalIllustrationIconView( - systemName: "xmark", - backgroundColor: backgroundColor - ) - } -} diff --git a/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationView.swift b/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationView.swift deleted file mode 100644 index 54e94916..00000000 --- a/Sources/FaceLiveness/Views/GetReadyPage/OvalIllustrationView.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import SwiftUI -import Amplify - -struct OvalIllustrationView: View { - let icon: OvalIllustrationIconView - let text: () -> Text - let primaryColor: Color - let secondaryColor: Color - let illustration: () -> Illustration - - var body: some View { - VStack(alignment: .leading) { - ZStack(alignment: .topLeading) { - VStack(alignment: .leading, spacing: 0) { - illustration() - .border(primaryColor, width: 0.8) - - text() - .bold() - .foregroundColor(primaryColor) - .padding(4) - } - .background(secondaryColor) - - icon - } - } - } -} diff --git a/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift b/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift index a1ae560d..ccce17f1 100644 --- a/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift +++ b/Sources/FaceLiveness/Views/Instruction/InstructionContainerView.swift @@ -82,7 +82,9 @@ struct InstructionContainerView: View { case .pendingFacePreparedConfirmation(let reason): InstructionView( text: .init(reason.rawValue), - backgroundColor: .livenessBackground + backgroundColor: .livenessPrimaryBackground, + textColor: .livenessPrimaryLabel, + font: .title ) case .completedDisplayingFreshness: InstructionView( @@ -95,6 +97,13 @@ struct InstructionContainerView: View { argument: LocalizedStrings.challenge_verifying ) } + case .faceMatched: + InstructionView( + text: LocalizedStrings.challenge_instruction_hold_still, + backgroundColor: .livenessPrimaryBackground, + textColor: .livenessPrimaryLabel, + font: .title + ) default: EmptyView() } diff --git a/Sources/FaceLiveness/Views/Liveness/CameraView.swift b/Sources/FaceLiveness/Views/Liveness/CameraView.swift index 6c8214c7..e984bce4 100644 --- a/Sources/FaceLiveness/Views/Liveness/CameraView.swift +++ b/Sources/FaceLiveness/Views/Liveness/CameraView.swift @@ -32,11 +32,3 @@ struct CameraView: UIViewControllerRepresentable { context: Context ) {} } - -class _CameraViewCoordinator: NSObject { - let livenessViewController: _LivenessViewController - - init(livenessViewController: _LivenessViewController) { - self.livenessViewController = livenessViewController - } -} diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift index 23c03ae0..64098e5f 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionView.swift @@ -16,7 +16,7 @@ import Amplify public struct FaceLivenessDetectorView: View { @StateObject var viewModel: FaceLivenessDetectionViewModel @Binding var isPresented: Bool - @State var displayState: DisplayState = .awaitingLivenessSession + @State var displayState: DisplayState = .awaitingCameraPermission @State var displayingCameraPermissionsNeededAlert = false let disableStartView: Bool @@ -114,10 +114,10 @@ public struct FaceLivenessDetectorView: View { self._viewModel = StateObject( wrappedValue: .init( - faceDetector: captureSession.outputDelegate.faceDetector, + faceDetector: captureSession.outputSampleBufferCapturer!.faceDetector, faceInOvalMatching: faceInOvalStateMatching, captureSession: captureSession, - videoChunker: captureSession.outputDelegate.videoChunker, + videoChunker: captureSession.outputSampleBufferCapturer!.videoChunker, closeButtonAction: { onCompletion(.failure(.userCancelled)) }, sessionID: sessionID ) @@ -131,13 +131,14 @@ public struct FaceLivenessDetectorView: View { .onAppear { Task { do { + let newState = disableStartView + ? DisplayState.displayingLiveness + : DisplayState.displayingGetReadyView + guard self.displayState != newState else { return } let session = try await sessionTask.value viewModel.livenessService = session viewModel.registerServiceEvents() - - self.displayState = disableStartView - ? .displayingLiveness - : .displayingGetReadyView + self.displayState = newState } catch { throw FaceLivenessDetectionError.accessDenied } @@ -146,10 +147,17 @@ public struct FaceLivenessDetectorView: View { case .displayingGetReadyView: GetReadyPageView( - displayingCameraPermissionsNeededAlert: $displayingCameraPermissionsNeededAlert, - onBegin: beginButtonTapped, + onBegin: { + guard displayState != .displayingLiveness else { return } + displayState = .displayingLiveness + }, beginCheckButtonDisabled: false ) + .onAppear { + DispatchQueue.main.async { + UIScreen.main.brightness = 1.0 + } + } case .displayingLiveness: _FaceLivenessDetectionView( viewModel: viewModel, @@ -171,13 +179,18 @@ public struct FaceLivenessDetectorView: View { onCompletion(.success(())) case .encounteredUnrecoverableError(let error): let closeCode = error.webSocketCloseCode ?? .normalClosure - viewModel.livenessService.closeSocket(with: closeCode) + viewModel.livenessService?.closeSocket(with: closeCode) isPresented = false onCompletion(.failure(mapError(error))) default: break } } + case .awaitingCameraPermission: + CameraPermissionView(displayingCameraPermissionsNeededAlert: $displayingCameraPermissionsNeededAlert) + .onAppear { + checkCameraPermission() + } } } @@ -199,10 +212,7 @@ public struct FaceLivenessDetectorView: View { for: .video, completionHandler: { accessGranted in guard accessGranted == true else { return } - displayState = .displayingLiveness - DispatchQueue.main.async { - UIScreen.main.brightness = 1.0 - } + displayState = .awaitingLivenessSession } ) @@ -211,16 +221,16 @@ public struct FaceLivenessDetectorView: View { private func alertCameraAccessNeeded() { displayingCameraPermissionsNeededAlert = true } - - private func beginButtonTapped() { + + private func checkCameraPermission() { let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) switch cameraAuthorizationStatus { case .notDetermined: requestCameraPermission() - case .authorized: - displayState = .displayingLiveness case .restricted, .denied: alertCameraAccessNeeded() + case .authorized: + displayState = .awaitingLivenessSession @unknown default: break } @@ -231,6 +241,7 @@ enum DisplayState { case awaitingLivenessSession case displayingGetReadyView case displayingLiveness + case awaitingCameraPermission } enum InstructionState { diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift index 22c33a74..95ebe163 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift @@ -111,7 +111,9 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler { case .match: self.livenessState.faceMatched() self.faceMatchedTimestamp = Date().timestampMilliseconds - self.livenessViewControllerDelegate?.displayFreshness(colorSequences: colorSequences) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.livenessViewControllerDelegate?.displayFreshness(colorSequences: colorSequences) + } let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) self.noFitStartTime = nil diff --git a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift index a29622f5..709ac49e 100644 --- a/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift +++ b/Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel.swift @@ -24,7 +24,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { var closeButtonAction: () -> Void let videoChunker: VideoChunker let sessionID: String - var livenessService: LivenessService! + var livenessService: LivenessService? let faceDetector: FaceDetector let faceInOvalMatching: FaceInOvalMatching let challengeID: String = UUID().uuidString @@ -90,7 +90,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { } func registerServiceEvents() { - livenessService.register(onComplete: { [weak self] reason in + livenessService?.register(onComplete: { [weak self] reason in self?.stopRecording() switch reason { @@ -106,7 +106,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { } }) - livenessService.register( + livenessService?.register( listener: { [weak self] _sessionConfiguration in self?.sessionConfiguration = _sessionConfiguration }, @@ -115,6 +115,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { } @objc func willResignActive(_ notification: Notification) { + guard self.livenessState.state != .initial else { return } DispatchQueue.main.async { self.stopRecording() self.livenessState.unrecoverableStateEncountered(.viewResignation) @@ -174,7 +175,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { func initializeLivenessStream() { do { - try livenessService.initializeLivenessStream( + try livenessService?.initializeLivenessStream( withSessionID: sessionID, userAgent: UserAgentValues.standard().userAgentString ) @@ -197,7 +198,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { ) do { - try livenessService.send( + try livenessService?.send( .freshness(event: freshnessEvent), eventDate: { .init() } ) @@ -238,7 +239,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { initialClientEvent = _initialClientEvent do { - try livenessService.send( + try livenessService?.send( .initialFaceDetected(event: _initialClientEvent), eventDate: { .init() } ) @@ -270,7 +271,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { ) do { - try livenessService.send( + try livenessService?.send( .final(event: finalClientEvent), eventDate: { .init() } ) @@ -296,7 +297,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { let videoEvent = VideoEvent.init(chunk: data, timestamp: timestamp) do { - try livenessService.send( + try livenessService?.send( .video(event: videoEvent), eventDate: { eventDate } ) @@ -335,7 +336,7 @@ class FaceLivenessDetectionViewModel: ObservableObject { let videoEvent = VideoEvent.init(chunk: data, timestamp: timestamp) do { - try livenessService.send( + try livenessService?.send( .video(event: videoEvent), eventDate: { eventDate } ) diff --git a/Sources/FaceLiveness/Views/ProgressBarView.swift b/Sources/FaceLiveness/Views/ProgressBarView.swift index a1cfc5b2..fbba9108 100644 --- a/Sources/FaceLiveness/Views/ProgressBarView.swift +++ b/Sources/FaceLiveness/Views/ProgressBarView.swift @@ -27,7 +27,7 @@ struct ProgressBarView: View { .foregroundColor(emptyColor) Rectangle() - .cornerRadius(8, corners: [.topLeft, .bottomLeft]) + .cornerRadius(8, corners: .allCorners) .frame( width: min(percentage, 1) * proxy.size.width, height: proxy.size.height - 8 diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift index b7e0d0c2..248cf8a6 100644 --- a/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Extension/MockLivenessCaptureSession.swift @@ -95,10 +95,10 @@ final class MockLivenessCaptureSession: LivenessCaptureSession { sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer) if let sampleBuffer = sampleBuffer { - self.outputDelegate.videoChunker.consume(sampleBuffer) - guard let imageBuffer = sampleBuffer.rotateRightUpMirrored() + self.outputSampleBufferCapturer?.videoChunker.consume(sampleBuffer) + guard let imageBuffer = sampleBuffer.imageBuffer else { return } - self.outputDelegate.faceDetector.detectFaces(from: imageBuffer) + self.outputSampleBufferCapturer?.faceDetector.detectFaces(from: imageBuffer) } } } diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift index 749e88a7..3f57982f 100644 --- a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView+Result.swift @@ -14,7 +14,8 @@ extension LivenessResultContentView { let valueTextColor: Color let valueBackgroundColor: Color let auditImage: Data? - + let isLive: Bool + init(livenessResult: LivenessResult) { guard livenessResult.confidenceScore > 0 else { text = "" @@ -22,9 +23,10 @@ extension LivenessResultContentView { valueTextColor = .clear valueBackgroundColor = .clear auditImage = nil + isLive = false return } - + isLive = livenessResult.isLive let truncated = String(format: "%.4f", livenessResult.confidenceScore) value = truncated if livenessResult.isLive { diff --git a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift index b1787015..eb2cbbd0 100644 --- a/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift +++ b/Tests/IntegrationTestApp/IntegrationTestApp/Views/LivenessResultContentView.swift @@ -17,6 +17,10 @@ struct LivenessResultContentView: View { Text("Result:") Text(result.text) .fontWeight(.semibold) + .foregroundColor(result.valueTextColor) + .padding(6) + .background(result.valueBackgroundColor) + .cornerRadius(8) } .padding(.bottom, 12) @@ -42,6 +46,20 @@ struct LivenessResultContentView: View { .frame(maxWidth: .infinity, idealHeight: 268) .background(Color.secondary.opacity(0.1)) } + + if !result.isLive { + steps() + .padding() + .background( + Rectangle() + .foregroundColor( + .dynamicColors( + light: .hex("#ECECEC"), + dark: .darkGray + ) + ) + .cornerRadius(6)) + } } .padding(.bottom, 16) .onAppear { @@ -54,6 +72,31 @@ struct LivenessResultContentView: View { } } } + + private func steps() -> some View { + func step(number: Int, text: String) -> some View { + HStack(alignment: .top) { + Text("\(number).") + Text(text) + } + } + + return VStack( + alignment: .leading, + spacing: 8 + ) { + Text("Tips to pass the video check:") + .fontWeight(.semibold) + step(number: 1, text: "Maximize your screen's brightness.") + .accessibilityElement(children: .combine) + + step(number: 2, text: "Avoid very bright lighting conditions, such as direct sunlight.") + .accessibilityElement(children: .combine) + + step(number: 3, text: "Remove sunglasses, mask, hat, or anything blocking your face.") + .accessibilityElement(children: .combine) + } + } }