|
| 1 | +// |
| 2 | +// Copyright Amazon.com Inc. or its affiliates. |
| 3 | +// All Rights Reserved. |
| 4 | +// |
| 5 | +// SPDX-License-Identifier: Apache-2.0 |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | +import Amplify |
| 10 | + |
| 11 | +@_spi(PredictionsFaceLiveness) |
| 12 | +public final class FaceLivenessSession: LivenessService { |
| 13 | + let websocket: WebSocketSession |
| 14 | + let eventStreamEncoder: EventStream.Encoder |
| 15 | + let eventStreamDecoder: EventStream.Decoder |
| 16 | + let signer: SigV4Signer |
| 17 | + let baseURL: URL |
| 18 | + var serverEventListeners: [LivenessEventKind.Server: (FaceLivenessSession.SessionConfiguration) -> Void] = [:] |
| 19 | + var onComplete: (ServerDisconnection) -> Void = { _ in } |
| 20 | + |
| 21 | + init( |
| 22 | + websocket: WebSocketSession, |
| 23 | + signer: SigV4Signer, |
| 24 | + baseURL: URL |
| 25 | + ) { |
| 26 | + self.eventStreamEncoder = EventStream.Encoder() |
| 27 | + self.eventStreamDecoder = EventStream.Decoder() |
| 28 | + self.signer = signer |
| 29 | + self.baseURL = baseURL |
| 30 | + |
| 31 | + self.websocket = websocket |
| 32 | + |
| 33 | + websocket.onMessageReceived { [weak self] result in |
| 34 | + self?.receive(result: result) ?? false |
| 35 | + } |
| 36 | + |
| 37 | + websocket.onSocketClosed { [weak self] closeCode in |
| 38 | + self?.onComplete(.unexpectedClosure(closeCode)) |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + public func register( |
| 43 | + onComplete: @escaping (ServerDisconnection) -> Void |
| 44 | + ) { |
| 45 | + self.onComplete = onComplete |
| 46 | + } |
| 47 | + |
| 48 | + public func register( |
| 49 | + listener: @escaping (FaceLivenessSession.SessionConfiguration) -> Void, |
| 50 | + on event: LivenessEventKind.Server |
| 51 | + ) { |
| 52 | + serverEventListeners[event] = listener |
| 53 | + } |
| 54 | + |
| 55 | + public func initializeLivenessStream(withSessionID sessionID: String, userAgent: String = "") throws { |
| 56 | + var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) |
| 57 | + components?.queryItems = [ |
| 58 | + URLQueryItem(name: "session-id", value: sessionID), |
| 59 | + URLQueryItem(name: "challenge-versions", value: "FaceMovementAndLightChallenge_1.0.0"), |
| 60 | + URLQueryItem(name: "video-width", value: "480"), |
| 61 | + URLQueryItem(name: "video-height", value: "640"), |
| 62 | + URLQueryItem(name: "x-amz-user-agent", value: userAgent) |
| 63 | + ] |
| 64 | + |
| 65 | + guard let url = components?.url |
| 66 | + else { throw FaceLivenessSessionError.invalidURL } |
| 67 | + |
| 68 | + let signedConnectionURL = signer.sign(url: url) |
| 69 | + websocket.open(url: signedConnectionURL) |
| 70 | + } |
| 71 | + |
| 72 | + public func send<T>( |
| 73 | + _ event: LivenessEvent<T>, |
| 74 | + eventDate: () -> Date = Date.init |
| 75 | + ) { |
| 76 | + let encodedPayload = eventStreamEncoder.encode( |
| 77 | + payload: event.payload, |
| 78 | + headers: [ |
| 79 | + ":content-type": .string("application/json"), |
| 80 | + ":event-type": .string(event.eventTypeHeader), |
| 81 | + ":message-type": .string("event") |
| 82 | + ] |
| 83 | + ) |
| 84 | + |
| 85 | + let eventDate = eventDate() |
| 86 | + |
| 87 | + let signedPayload = signer.signWithPreviousSignature( |
| 88 | + payload: encodedPayload, |
| 89 | + dateHeader: (key: ":date", value: eventDate) |
| 90 | + ) |
| 91 | + |
| 92 | + let encodedEvent = eventStreamEncoder.encode( |
| 93 | + payload: encodedPayload, |
| 94 | + headers: [ |
| 95 | + ":date": .timestamp(eventDate), |
| 96 | + ":chunk-signature": .data(signedPayload) |
| 97 | + ] |
| 98 | + ) |
| 99 | + |
| 100 | + websocket.send( |
| 101 | + message: .data(encodedEvent), |
| 102 | + onError: { error in } |
| 103 | + ) |
| 104 | + } |
| 105 | + |
| 106 | + private func fallbackDecoding(_ message: EventStream.Message) -> Bool { |
| 107 | + // We only care about two events above. |
| 108 | + // Just in case the header value changes (it shouldn't) |
| 109 | + // We'll try to decode each of these events |
| 110 | + if let payload = try? JSONDecoder().decode(ServerSessionInformationEvent.self, from: message.payload) { |
| 111 | + let sessionConfiguration = sessionConfiguration(from: payload) |
| 112 | + self.serverEventListeners[.challenge]?(sessionConfiguration) |
| 113 | + } else if (try? JSONDecoder().decode(DisconnectEvent.self, from: message.payload)) != nil { |
| 114 | + onComplete(.disconnectionEvent) |
| 115 | + return false |
| 116 | + } |
| 117 | + return true |
| 118 | + } |
| 119 | + |
| 120 | + private func receive(result: Result<URLSessionWebSocketTask.Message, Error>) -> Bool { |
| 121 | + switch result { |
| 122 | + case .success(.data(let data)): |
| 123 | + |
| 124 | + do { |
| 125 | + let message = try self.eventStreamDecoder.decode(data: data) |
| 126 | + guard let eventType = message.headers.first(where: { $0.name == ":event-type" }) |
| 127 | + else { return fallbackDecoding(message) } |
| 128 | + |
| 129 | + switch eventType.value { |
| 130 | + case "ServerSessionInformationEvent": |
| 131 | + // :event-type ServerSessionInformationEvent |
| 132 | + let payload = try JSONDecoder().decode( |
| 133 | + ServerSessionInformationEvent.self, from: message.payload |
| 134 | + ) |
| 135 | + let sessionConfiguration = sessionConfiguration(from: payload) |
| 136 | + serverEventListeners[.challenge]?(sessionConfiguration) |
| 137 | + case "DisconnectionEvent": |
| 138 | + // :event-type DisconnectionEvent |
| 139 | + onComplete(.disconnectionEvent) |
| 140 | + return false |
| 141 | + default: |
| 142 | + return true |
| 143 | + } |
| 144 | + } catch {} |
| 145 | + return true |
| 146 | + case .success: |
| 147 | + return true |
| 148 | + case .failure: |
| 149 | + return true |
| 150 | + } |
| 151 | + } |
| 152 | +} |
0 commit comments