bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
349 lines
11 KiB
Swift
349 lines
11 KiB
Swift
import Foundation
|
|
import AVFoundation
|
|
import CoreLocation
|
|
import CoreMotion
|
|
|
|
/**
|
|
* QUIXZOOM Capture Manager
|
|
*
|
|
* Hanterar inspelning, telemetri och uppladdning.
|
|
* 4 lägen: Recording, Guided, Mission, Adaptive
|
|
*/
|
|
class CaptureManager: NSObject, ObservableObject {
|
|
|
|
// MARK: - Published Properties
|
|
@Published var currentSession: CaptureSession?
|
|
@Published var isRecording = false
|
|
@Published var currentInstruction: AIInstruction?
|
|
@Published var uploadProgress: Double = 0
|
|
@Published var qualityScore: Double = 0
|
|
@Published var gpsSignal: Double = 0
|
|
|
|
// MARK: - Services
|
|
private var captureSession: AVCaptureSession?
|
|
private var videoOutput: AVCaptureMovieFileOutput?
|
|
private var locationManager: CLLocationManager?
|
|
private var motionManager: CMMotionManager?
|
|
private var altimeter: CMAltimeter?
|
|
|
|
// MARK: - Data
|
|
private var telemetryTimer: Timer?
|
|
private var frameCount = 0
|
|
private var videoFileURL: URL?
|
|
|
|
// MARK: - Constants
|
|
private let telemetryRate = 10.0 // Hz
|
|
private let videoQuality = AVCaptureSession.Preset.hd1920x1080
|
|
|
|
// MARK: - Initialization
|
|
override init() {
|
|
super.init()
|
|
setupLocationManager()
|
|
setupMotionManager()
|
|
}
|
|
|
|
// MARK: - Session Management
|
|
|
|
func startSession(mode: CaptureSession.CaptureMode, config: SessionConfig) {
|
|
let session = CaptureSession(
|
|
id: UUID().uuidString,
|
|
missionId: generateMissionId(),
|
|
startTime: Date(),
|
|
endTime: nil,
|
|
mode: mode,
|
|
status: .preparing,
|
|
config: config,
|
|
frames: [],
|
|
telemetry: [],
|
|
path: [],
|
|
instructions: [],
|
|
qualityScore: 0,
|
|
reliabilityScore: 0
|
|
)
|
|
|
|
currentSession = session
|
|
|
|
// Starta kamera
|
|
setupCamera()
|
|
|
|
// Starta inspelning
|
|
startRecording()
|
|
|
|
// Starta telemetri
|
|
startTelemetryCollection()
|
|
|
|
currentSession?.status = .recording
|
|
isRecording = true
|
|
|
|
print("[CAPTURE] Session started: \(session.id)")
|
|
print("[CAPTURE] Mode: \(mode.rawValue)")
|
|
print("[CAPTURE] Mission ID: \(session.missionId)")
|
|
}
|
|
|
|
func stopSession() {
|
|
stopRecording()
|
|
stopTelemetryCollection()
|
|
|
|
currentSession?.endTime = Date()
|
|
currentSession?.status = .completed
|
|
isRecording = false
|
|
|
|
// Starta uppladdning
|
|
uploadSession()
|
|
|
|
print("[CAPTURE] Session stopped: \(currentSession?.id ?? "unknown")")
|
|
}
|
|
|
|
// MARK: - Camera Setup
|
|
|
|
private func setupCamera() {
|
|
captureSession = AVCaptureSession()
|
|
captureSession?.sessionPreset = videoQuality
|
|
|
|
guard let session = captureSession else { return }
|
|
|
|
// Video input
|
|
guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
|
|
let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
|
|
session.canAddInput(videoInput) else {
|
|
print("[CAPTURE] Failed to setup video input")
|
|
return
|
|
}
|
|
|
|
session.addInput(videoInput)
|
|
|
|
// Audio input
|
|
guard let audioDevice = AVCaptureDevice.default(for: .audio),
|
|
let audioInput = try? AVCaptureDeviceInput(device: audioDevice),
|
|
session.canAddInput(audioInput) else {
|
|
print("[CAPTURE] Failed to setup audio input")
|
|
return
|
|
}
|
|
|
|
session.addInput(audioInput)
|
|
|
|
// Video output
|
|
let movieOutput = AVCaptureMovieFileOutput()
|
|
guard session.canAddOutput(movieOutput) else {
|
|
print("[CAPTURE] Failed to add movie output")
|
|
return
|
|
}
|
|
|
|
session.addOutput(movieOutput)
|
|
videoOutput = movieOutput
|
|
|
|
// Starta session
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
session.startRunning()
|
|
}
|
|
}
|
|
|
|
private func startRecording() {
|
|
guard let output = videoOutput else { return }
|
|
|
|
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
let missionId = currentSession?.missionId ?? "unknown"
|
|
videoFileURL = documentsPath.appendingPathComponent("\(missionId).mov")
|
|
|
|
guard let url = videoFileURL else { return }
|
|
|
|
output.startRecording(to: url, recordingDelegate: self)
|
|
print("[CAPTURE] Recording started: \(url.lastPathComponent)")
|
|
}
|
|
|
|
private func stopRecording() {
|
|
videoOutput?.stopRecording()
|
|
print("[CAPTURE] Recording stopped")
|
|
}
|
|
|
|
// MARK: - Telemetry
|
|
|
|
private func setupLocationManager() {
|
|
locationManager = CLLocationManager()
|
|
locationManager?.delegate = self
|
|
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
|
|
locationManager?.allowsBackgroundLocationUpdates = true
|
|
locationManager?.startUpdatingLocation()
|
|
}
|
|
|
|
private func setupMotionManager() {
|
|
motionManager = CMMotionManager()
|
|
motionManager?.accelerometerUpdateInterval = 1.0 / telemetryRate
|
|
motionManager?.gyroUpdateInterval = 1.0 / telemetryRate
|
|
motionManager?.magnetometerUpdateInterval = 1.0 / telemetryRate
|
|
}
|
|
|
|
private func startTelemetryCollection() {
|
|
// Starta accelerometer
|
|
if motionManager?.isAccelerometerAvailable == true {
|
|
motionManager?.startAccelerometerUpdates()
|
|
}
|
|
|
|
// Starta gyroskop
|
|
if motionManager?.isGyroAvailable == true {
|
|
motionManager?.startGyroUpdates()
|
|
}
|
|
|
|
// Starta magnetometer
|
|
if motionManager?.isMagnetometerAvailable == true {
|
|
motionManager?.startMagnetometerUpdates()
|
|
}
|
|
|
|
// Starta höjdmätare
|
|
if CMAltimeter.isRelativeAltitudeAvailable() {
|
|
altimeter = CMAltimeter()
|
|
altimeter?.startRelativeAltitudeUpdates(to: .main) { [weak self] data, error in
|
|
// Hantera höjd-data
|
|
}
|
|
}
|
|
|
|
// Samla telemetri med jämna intervall
|
|
telemetryTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / telemetryRate, repeats: true) { [weak self] _ in
|
|
self?.collectTelemetry()
|
|
}
|
|
}
|
|
|
|
private func stopTelemetryCollection() {
|
|
telemetryTimer?.invalidate()
|
|
telemetryTimer = nil
|
|
|
|
motionManager?.stopAccelerometerUpdates()
|
|
motionManager?.stopGyroUpdates()
|
|
motionManager?.stopMagnetometerUpdates()
|
|
altimeter?.stopRelativeAltitudeUpdates()
|
|
}
|
|
|
|
private func collectTelemetry() {
|
|
guard let session = currentSession else { return }
|
|
|
|
// GPS
|
|
let location = locationManager?.location
|
|
let gps = GPSPoint(
|
|
latitude: location?.coordinate.latitude ?? 0,
|
|
longitude: location?.coordinate.longitude ?? 0,
|
|
accuracy: location?.horizontalAccuracy ?? 0,
|
|
altitude: location?.altitude ?? 0,
|
|
timestamp: Date()
|
|
)
|
|
|
|
// IMU
|
|
let accelData = motionManager?.accelerometerData
|
|
let gyroData = motionManager?.gyroData
|
|
let magData = motionManager?.magnetometerData
|
|
|
|
let imu = IMUData(
|
|
accelerometer: XYZ(
|
|
x: accelData?.acceleration.x ?? 0,
|
|
y: accelData?.acceleration.y ?? 0,
|
|
z: accelData?.acceleration.z ?? 0
|
|
),
|
|
gyroscope: XYZ(
|
|
x: gyroData?.rotationRate.x ?? 0,
|
|
y: gyroData?.rotationRate.y ?? 0,
|
|
z: gyroData?.rotationRate.z ?? 0
|
|
),
|
|
magnetometer: magData != nil ? XYZ(
|
|
x: magData!.magneticField.x,
|
|
y: magData!.magneticField.y,
|
|
z: magData!.magneticField.z
|
|
) : nil
|
|
)
|
|
|
|
// Kompass
|
|
let compass = location?.course ?? 0
|
|
|
|
// Skapa telemetri
|
|
let telemetry = TelemetryData(
|
|
timestamp: Date(),
|
|
gps: gps,
|
|
imu: imu,
|
|
compass: compass,
|
|
altitude: location?.altitude ?? 0
|
|
)
|
|
|
|
// Lägg till i session
|
|
currentSession?.telemetry.append(telemetry)
|
|
currentSession?.path.append(gps)
|
|
|
|
// Uppdatera GPS-signal
|
|
gpsSignal = min(1.0, 50.0 / (gps.accuracy + 1.0))
|
|
}
|
|
|
|
// MARK: - Upload
|
|
|
|
private func uploadSession() {
|
|
guard let session = currentSession else { return }
|
|
|
|
currentSession?.status = .uploading
|
|
|
|
// TODO: Implementera uppladdning till backend
|
|
// 1. Ladda upp video-fil
|
|
// 2. Ladda upp metadata (JSON)
|
|
// 3. Bekräftelse från server
|
|
|
|
print("[CAPTURE] Uploading session: \(session.id)")
|
|
|
|
// Simulera uppladdning
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
|
self?.uploadProgress = 1.0
|
|
self?.currentSession?.status = .completed
|
|
print("[CAPTURE] Upload complete: \(session.id)")
|
|
}
|
|
}
|
|
|
|
// MARK: - AI Instructions
|
|
|
|
func handleInstruction(_ instruction: AIInstruction) {
|
|
currentInstruction = instruction
|
|
currentSession?.instructions.append(instruction)
|
|
|
|
// Visa instruktion för användaren
|
|
print("[CAPTURE] AI Instruction: \(instruction.text)")
|
|
}
|
|
|
|
func completeInstruction() {
|
|
currentInstruction?.completed = true
|
|
currentInstruction?.completedAt = Date()
|
|
currentInstruction = nil
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func generateMissionId() -> String {
|
|
let prefix = "QZ"
|
|
let date = DateFormatter()
|
|
date.dateFormat = "yyyyMMdd"
|
|
let dateStr = date.string(from: Date())
|
|
let random = String(format: "%04d", Int.random(in: 0...9999))
|
|
return "\(prefix)-\(dateStr)-\(random)"
|
|
}
|
|
}
|
|
|
|
// MARK: - AVCaptureFileOutputRecordingDelegate
|
|
|
|
extension CaptureManager: AVCaptureFileOutputRecordingDelegate {
|
|
func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
|
|
print("[CAPTURE] Recording started to: \(fileURL.lastPathComponent)")
|
|
}
|
|
|
|
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
|
|
if let error = error {
|
|
print("[CAPTURE] Recording error: \(error.localizedDescription)")
|
|
} else {
|
|
print("[CAPTURE] Recording finished: \(outputFileURL.lastPathComponent)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - CLLocationManagerDelegate
|
|
|
|
extension CaptureManager: CLLocationManagerDelegate {
|
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
// GPS-uppdateringar hanteras i collectTelemetry
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
print("[CAPTURE] GPS error: \(error.localizedDescription)")
|
|
}
|
|
}
|