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

Adding Active Liveness Metadata #265

Merged
merged 7 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
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
95 changes: 61 additions & 34 deletions Sources/SmileID/Classes/Camera/CameraManager.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import AVFoundation
import Foundation
import SwiftUI

class CameraManager: NSObject, ObservableObject {
Expand All @@ -21,7 +21,9 @@ class CameraManager: NSObject, ObservableObject {
@Published var sampleBuffer: CVPixelBuffer?
@Published var capturedImage: Data?

var sampleBufferPublisher: Published<CVPixelBuffer?>.Publisher { $sampleBuffer }
var sampleBufferPublisher: Published<CVPixelBuffer?>.Publisher {
$sampleBuffer
}
var capturedImagePublisher: Published<Data?>.Publisher { $capturedImage }
let videoOutputQueue = DispatchQueue(
label: "com.smileidentity.videooutput",
Expand All @@ -48,7 +50,8 @@ class CameraManager: NSObject, ObservableObject {
self.orientation = orientation
super.init()
sessionQueue.async {
self.videoOutput.setSampleBufferDelegate(self, queue: self.videoOutputQueue)
self.videoOutput.setSampleBufferDelegate(
self, queue: self.videoOutputQueue)
}
checkPermissions()
}
Expand All @@ -60,28 +63,28 @@ class CameraManager: NSObject, ObservableObject {
}

private func checkPermissions() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .notDetermined:
sessionQueue.suspend()
AVCaptureDevice.requestAccess(for: .video) { authorized in
if !authorized {
self.status = .unauthorized
self.set(error: .deniedAuthorization)
}
self.sessionQueue.resume()
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .notDetermined:
sessionQueue.suspend()
AVCaptureDevice.requestAccess(for: .video) { authorized in
if !authorized {
self.status = .unauthorized
self.set(error: .deniedAuthorization)
}
self.sessionQueue.resume()
}
case .restricted:
status = .unauthorized
set(error: .restrictedAuthorization)
case .denied:
status = .unauthorized
set(error: .deniedAuthorization)
case .authorized:
break
@unknown default:
status = .unauthorized
set(error: .unknownAuthorization)
}
case .restricted:
status = .unauthorized
set(error: .restrictedAuthorization)
case .denied:
status = .unauthorized
set(error: .deniedAuthorization)
case .authorized:
break
@unknown default:
status = .unauthorized
set(error: .unknownAuthorization)
}
}

private func addCameraInput(position: AVCaptureDevice.Position) {
Expand All @@ -90,7 +93,8 @@ class CameraManager: NSObject, ObservableObject {
status = .failed
return
}
cameraName = camera.uniqueID

getCameraName(for: camera)

do {
let cameraInput = try AVCaptureDeviceInput(device: camera)
Expand All @@ -106,14 +110,29 @@ class CameraManager: NSObject, ObservableObject {
}
}

private func getCameraForPosition(_ position: AVCaptureDevice.Position) -> AVCaptureDevice? {
private func getCameraName(for camera: AVCaptureDevice) {
var manufacturer: String
if #available(iOS 14.0, *) {
manufacturer = camera.manufacturer
} else {
manufacturer = "Apple Inc."
}
cameraName =
"\(manufacturer) \(camera.localizedName) \(camera.deviceType.rawValue)"
}

private func getCameraForPosition(_ position: AVCaptureDevice.Position)
-> AVCaptureDevice? {
switch position {
case .front:
return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
return AVCaptureDevice.default(
.builtInWideAngleCamera, for: .video, position: .front)
case .back:
return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
return AVCaptureDevice.default(
.builtInWideAngleCamera, for: .video, position: .back)
default:
return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
return AVCaptureDevice.default(
.builtInWideAngleCamera, for: .video, position: .front)
}
}

Expand All @@ -124,7 +143,10 @@ class CameraManager: NSObject, ObservableObject {
session.addOutput(photoOutput)
session.addOutput(videoOutput)
videoOutput.videoSettings =
[kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
[
kCVPixelBufferPixelFormatTypeKey as String:
kCVPixelFormatType_32BGRA
]
if orientation == .portrait {
let videoConnection = videoOutput.connection(with: .video)
videoConnection?.videoOrientation = .portrait
Expand All @@ -139,15 +161,17 @@ class CameraManager: NSObject, ObservableObject {
checkPermissions()
sessionQueue.async { [self] in
if !session.isRunning {
if let currentInput = session.inputs.first as? AVCaptureDeviceInput {
if let currentInput = session.inputs.first
as? AVCaptureDeviceInput {
session.removeInput(currentInput)
}
addCameraInput(position: position)
configureVideoOutput()
session.startRunning()
} else {
session.beginConfiguration()
if let currentInput = session.inputs.first as? AVCaptureDeviceInput {
if let currentInput = session.inputs.first
as? AVCaptureDeviceInput {
session.removeInput(currentInput)
}
addCameraInput(position: position)
Expand All @@ -172,7 +196,9 @@ class CameraManager: NSObject, ObservableObject {
}

internal func capturePhoto() {
guard let connection = photoOutput.connection(with: .video), connection.isEnabled, connection.isActive else {
guard let connection = photoOutput.connection(with: .video),
connection.isEnabled, connection.isActive
else {
set(error: .cameraUnavailable)
print("Camera unavailable")
return
Expand All @@ -189,7 +215,8 @@ extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate {
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
else { return }
self.sampleBuffer = imageBuffer
}
}
Expand Down
97 changes: 71 additions & 26 deletions Sources/SmileID/Classes/Networking/Models/v2/Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public struct Metadata: Codable {
Metadata(items: [
.sdk,
.sdkVersion,
.activeLivenessVersion,
.clientIP,
.fingerprint,
.deviceModel,
Expand Down Expand Up @@ -49,15 +50,34 @@ public class Metadatum: Codable {
}

public static let sdk = Metadatum(name: "sdk", value: "iOS")
public static let sdkVersion = Metadatum(name: "sdk_version", value: SmileID.version)
public static let clientIP = Metadatum(name: "client_ip", value: getIPAddress(useIPv4: true))
public static let fingerprint = Metadatum(name: "fingerprint", value: SmileID.deviceId)
public static let deviceModel = Metadatum(name: "device_model", value: UIDevice.current.modelName)
public static let deviceOS = Metadatum(name: "device_os", value: UIDevice.current.systemVersion)
public static let sdkVersion = Metadatum(
name: "sdk_version", value: SmileID.version)
public static let activeLivenessVersion = Metadatum(
name: "active_liveness_version", value: "1.0.0")
public static let clientIP = Metadatum(
name: "client_ip", value: getIPAddress(useIPv4: true))
public static let fingerprint = Metadatum(
name: "fingerprint", value: SmileID.deviceId)
public static let deviceModel = Metadatum(
name: "device_model", value: UIDevice.current.modelName)
public static let deviceOS = Metadatum(
name: "device_os", value: UIDevice.current.systemVersion)

public class ActiveLivenessType: Metadatum {
public init(livenessType: LivenessType) {
super.init(
name: "active_liveness_type", value: livenessType.rawValue)
}

public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
}

public class SelfieImageOrigin: Metadatum {
public init(cameraFacing: CameraFacingValue) {
super.init(name: "selfie_image_origin", value: cameraFacing.rawValue)
super.init(
name: "selfie_image_origin", value: cameraFacing.rawValue)
}

public required init(from decoder: Decoder) throws {
Expand All @@ -67,7 +87,9 @@ public class Metadatum: Codable {

public class SelfieCaptureDuration: Metadatum {
public init(duration: TimeInterval) {
super.init(name: "selfie_capture_duration_ms", value: String(Int(duration * 1000)))
super.init(
name: "selfie_capture_duration_ms",
value: String(Int(duration * 1000)))
}

public required init(from decoder: Decoder) throws {
Expand All @@ -77,7 +99,8 @@ public class Metadatum: Codable {

public class DocumentFrontImageOrigin: Metadatum {
public init(origin: DocumentImageOriginValue) {
super.init(name: "document_front_image_origin", value: origin.rawValue)
super.init(
name: "document_front_image_origin", value: origin.rawValue)
}

public required init(from decoder: Decoder) throws {
Expand All @@ -87,7 +110,8 @@ public class Metadatum: Codable {

public class DocumentBackImageOrigin: Metadatum {
public init(origin: DocumentImageOriginValue) {
super.init(name: "document_back_image_origin", value: origin.rawValue)
super.init(
name: "document_back_image_origin", value: origin.rawValue)
}

public required init(from decoder: Decoder) throws {
Expand All @@ -97,7 +121,8 @@ public class Metadatum: Codable {

public class DocumentFrontCaptureRetries: Metadatum {
public init(retries: Int) {
super.init(name: "document_front_capture_retries", value: String(retries))
super.init(
name: "document_front_capture_retries", value: String(retries))
}

public required init(from decoder: Decoder) throws {
Expand All @@ -107,7 +132,8 @@ public class Metadatum: Codable {

public class DocumentBackCaptureRetries: Metadatum {
public init(retries: Int) {
super.init(name: "document_back_capture_retries", value: String(retries))
super.init(
name: "document_back_capture_retries", value: String(retries))
}

public required init(from decoder: Decoder) throws {
Expand All @@ -117,7 +143,9 @@ public class Metadatum: Codable {

public class DocumentFrontCaptureDuration: Metadatum {
public init(duration: TimeInterval) {
super.init(name: "document_front_capture_duration_ms", value: String(Int(duration * 1000)))
super.init(
name: "document_front_capture_duration_ms",
value: String(Int(duration * 1000)))
}

public required init(from decoder: Decoder) throws {
Expand All @@ -127,7 +155,9 @@ public class Metadatum: Codable {

public class DocumentBackCaptureDuration: Metadatum {
public init(duration: TimeInterval) {
super.init(name: "document_back_capture_duration_ms", value: String(Int(duration * 1000)))
super.init(
name: "document_back_capture_duration_ms",
value: String(Int(duration * 1000)))
}

public required init(from decoder: Decoder) throws {
Expand All @@ -136,6 +166,11 @@ public class Metadatum: Codable {
}
}

public enum LivenessType: String, Codable {
case headPose = "head_pose"
case smile = "smile"
}

public enum DocumentImageOriginValue: String {
case gallery
case cameraAutoCapture = "camera_auto_capture"
Expand Down Expand Up @@ -172,16 +207,19 @@ func getIPAddress(useIPv4: Bool) -> String {
if name == "en0" || name == "en1" || name == "pdp_ip0"
|| name == "pdp_ip1" || name == "pdp_ip2" || name == "pdp_ip3" {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
&hostname, socklen_t(hostname.count),
nil, socklen_t(0), NI_NUMERICHOST)
getnameinfo(
interface.ifa_addr,
socklen_t(interface.ifa_addr.pointee.sa_len),
&hostname, socklen_t(hostname.count),
nil, socklen_t(0), NI_NUMERICHOST)
address = String(cString: hostname)

if (useIPv4 && addrFamily == UInt8(AF_INET)) ||
(!useIPv4 && addrFamily == UInt8(AF_INET6)) {
if (useIPv4 && addrFamily == UInt8(AF_INET))
|| (!useIPv4 && addrFamily == UInt8(AF_INET6)) {
if !useIPv4 {
if let percentIndex = address.firstIndex(of: "%") {
address = String(address[..<percentIndex]).uppercased()
address = String(address[..<percentIndex])
.uppercased()
} else {
address = address.uppercased()
}
Expand Down Expand Up @@ -209,30 +247,37 @@ public class LocalMetadata: ObservableObject {
extension UIDevice {
var modelName: String {
#if targetEnvironment(simulator)
let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]!
let identifier = ProcessInfo().environment[
"SIMULATOR_MODEL_IDENTIFIER"]!
#else
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
guard let value = element.value as? Int8, value != 0 else {
return identifier
}
return identifier + String(UnicodeScalar(UInt8(value)))
}
#endif
return DeviceModel.all.first { $0.identifier == identifier }?.model ?? identifier
return DeviceModel.all.first { $0.identifier == identifier }?.model
?? identifier
}

struct DeviceModel: Decodable {
let identifier: String
let model: String
static var all: [DeviceModel] {
_ = UIDevice.current.name
guard let devicesUrl = SmileIDResourcesHelper.bundle.url(
forResource: "devicemodels", withExtension: "json"
) else { return [] }
guard
let devicesUrl = SmileIDResourcesHelper.bundle.url(
forResource: "devicemodels", withExtension: "json"
)
else { return [] }
do {
let data = try Data(contentsOf: devicesUrl)
let devices = try JSONDecoder().decode([DeviceModel].self, from: data)
let devices = try JSONDecoder().decode(
[DeviceModel].self, from: data)
return devices
} catch {
print("Error decoding device models: \(error)")
Expand Down
Loading
Loading