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