Files
boc/quixzoom-capture-pipeline/ios/QuixZoomCapture/Services/CaptureManager.swift
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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)")
}
}