// // FlutterVitalSignCamera.swift // vital_sign_camera // // Created by Eddie Lau on 24/1/2023. // import Foundation import Flutter import UIKit import PromiseKit import VitalSignEngineCore class FlutterVitalSignCamera: NSObject, FlutterPlatformView { private var camera: VitalSignCamera private var methodChannel: FlutterMethodChannel private var eventSink: FlutterEventSink? func view() -> UIView { return camera } init( frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, binaryMessenger messenger: FlutterBinaryMessenger ) { methodChannel = FlutterMethodChannel(name: "ai.panoptic/channel/flutter_vital_sign_camera_\(viewId)", binaryMessenger: messenger) camera = VitalSignCamera(frame: frame) super.init() camera.faceDetector = MLKitFaceDetector() camera.configure(args: args, onVideoFrameProcessed: onVideoFrameProcessed) methodChannel.setMethodCallHandler(onMethodCall) let channel = FlutterEventChannel(name: "ai.panoptic/channel/flutter_vital_sign_camera_processed_frame_event_\(viewId)", binaryMessenger: messenger) channel.setStreamHandler(self) } func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { switch(call.method){ case "startScanning": NSLog("FlutterVitalSignCamera: startScanning") camera.startScanning().done { result(nil) }.catch { error in result(FlutterError(code: "\(error.code)", message: error.message, details: nil)) } case "stopScanning": NSLog("FlutterVitalSignCamera: stopScanning") camera.stopScanning().done { result(nil) }.catch { error in result(FlutterError(code: "\(error.code)", message: error.message, details: nil)) } case "configure": NSLog("FlutterVitalSignCamera: configure \(call.arguments ?? "nil")") camera.configure(args: call.arguments, onVideoFrameProcessed: onVideoFrameProcessed) default: result(FlutterMethodNotImplemented) } } func onVideoFrameProcessed(result: ProcessResult) { let flutterMap = result.dictionary.flutterMap guard let eventSink = eventSink else { return } eventSink(flutterMap) } } extension FlutterVitalSignCamera: FlutterStreamHandler { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { eventSink = events return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { eventSink = nil return nil } } extension VitalSignCamera { func configure(args:Any?, onVideoFrameProcessed:@escaping (ProcessResult)->Void) { if let args = args as? [String:Any] { configure(dict: args, onVideoFrameProcessed: onVideoFrameProcessed) } else { NSLog("Error: Invalid props: \(args ?? "nil")") } } func configure(dict:Dictionary, onVideoFrameProcessed:@escaping (ProcessResult)->Void) { var device:CameraDevice? if let id = dict["id"] as? String { device = VitalSignCamera.availableCameraDevices().first { $0.id == id } } var isActive = false if let isActiveNumber = dict["isActive"] as? NSNumber { isActive = isActiveNumber.boolValue } guard let userInfoDict = dict["userInfo"] as? Dictionary, let userInfo = UserInfo(flutterMap: userInfoDict) else { NSLog("Error: Invalid userInfo: \(dict["userInfo"] ?? "nil")") return } guard let configDict = dict["config"] as? NSDictionary, let config = VitalSignEngineConfig(flutterMap: configDict) else { NSLog("Error: Invalid config: \(dict["config"] ?? "nil")") return } let previewLayerGravity:PreviewLayerGravity = { if let previewLayerGravityNum = dict["previewLayerGravity"] as? NSNumber, let previewLayerGravity = PreviewLayerGravity(number: previewLayerGravityNum) { return previewLayerGravity } return PreviewLayerGravity.resizeAspectFill }() configure { camera in camera.device = device camera.isActive = isActive camera.userInfo = userInfo camera.config = config camera.previewLayerGravity = previewLayerGravity camera.onVideoFrameProcessed = onVideoFrameProcessed } } } extension UserInfo { init?(flutterMap:Dictionary) { guard let genderObj = flutterMap["gender"] as? NSNumber else { NSLog("Error: Invalid gender: \(flutterMap["gender"] ?? "nil")") return nil } guard let gender = Gender(rawValue: genderObj.intValue) else { NSLog("Error: Invalid gender: \(flutterMap["gender"] ?? "nil")") return nil } guard let ageObj = flutterMap["age"] as? NSNumber else { NSLog("Error: Invalid age: \(flutterMap["age"] ?? "nil")") return nil } let age = ageObj.doubleValue guard let userIdObj = flutterMap["userId"] as? NSString else { NSLog("Error: Invalid userId: \(flutterMap["userId"] ?? "nil")") return nil } self.init( gender: gender, age: age, weight: (flutterMap["weight"] as? NSNumber)?.doubleValue, height: (flutterMap["height"] as? NSNumber)?.doubleValue, smoking: (flutterMap["smoker"] as? NSNumber)?.boolValue, hypertension: Hypertension(hasHypertension: (flutterMap["hypertension"] as? NSNumber)?.boolValue), bpMedication: (flutterMap["bpMedication"] as? NSNumber)?.boolValue, diabetic: Diabetic(hasDiabetic: (flutterMap["diabetic"] as? NSNumber)?.boolValue), waistCircumference: (flutterMap["waistCircumference"] as? NSNumber)?.doubleValue, appUserId: flutterMap["appUserId"] as? String, userId: userIdObj as String ) } } extension Diabetic { init?(hasDiabetic:Bool?) { guard let hasDiabetic = hasDiabetic else { return nil } if (hasDiabetic) { self = .type1 } else { self = .no } } } extension Hypertension { init?(hasHypertension:Bool?) { guard let hasHypertension = hasHypertension else { return nil } if (hasHypertension) { self = .stage1 } else { self = .no } } } extension VitalSignEngineConfig { init?(flutterMap: NSDictionary) { /* public var errorExpirationTime: TimeInterval? */ guard let map = flutterMap as? [String:Any] else { NSLog("Error: Invalid config: \(flutterMap)") return nil } let serverIdObj = map["serverId"] as? NSNumber let serverId = serverIdObj != nil ? ServerId(rawValue: serverIdObj!.intValue) : nil let dataCollectionDuration = map["dataCollectionDuration"] as? NSNumber let dataSendingDuration = map["dataSendingDuration"] as? NSNumber let dataSendingRate = map["dataSendingRate"] as? NSNumber let dataWindowDuration = map["dataWindowDuration"] as? NSNumber let apiKey = map["apiKey"] as? NSString let faceLostTimeout = map["faceLostTimeout"] as? NSNumber let errorExpirationTime = map["errorExpirationTime"] as? NSNumber self.init( serverId: serverId, dataCollectionDuration: dataCollectionDuration?.doubleValue, dataSendingDuration: dataSendingDuration?.doubleValue, dataSendingRate: dataSendingRate?.doubleValue, dataWindowDuration: dataWindowDuration?.doubleValue, apiKey: apiKey as String?, faceLostTimeout: faceLostTimeout?.doubleValue, errorExpirationTime: errorExpirationTime?.doubleValue ) } } extension PreviewLayerGravity { init?(number: NSNumber) { if (number.intValue == 0) { self = .resizeAspectFill } else if (number.intValue == 1) { self = .resizeAspect } else { return nil } } } func toFlutterValue(value:Any) -> Any { if let boolValue = value as? Bool { return NSNumber(value: boolValue) } else if let doubleValue = value as? Double { return NSNumber(value: doubleValue) } else if let intValue = value as? Int { return NSNumber(value: intValue) } else if let int32Value = value as? Int32 { return NSNumber(value: int32Value) } else if let stringValue = value as? String { return stringValue } else if let map = value as? [String:Any] { return map.flutterMap } else if let array = value as? [Any] { return array.map { toFlutterValue(value: $0) } } else { return value } } extension Dictionary { var flutterMap:[String:Any] { var map = [String:Any]() keys.forEach { key in map[key] = toFlutterValue(value: self[key]!) } return map } }