You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
290 lines
9.4 KiB
Swift
290 lines
9.4 KiB
Swift
//
|
|
// 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<String,Any>, 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<String,Any>,
|
|
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<String,Any>) {
|
|
|
|
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<String,Any> {
|
|
var flutterMap:[String:Any] {
|
|
var map = [String:Any]()
|
|
keys.forEach { key in
|
|
map[key] = toFlutterValue(value: self[key]!)
|
|
}
|
|
return map
|
|
}
|
|
}
|