JAAK Visage SDK iOS

⏱️ Tiempo estimado: 45-60 minutos para configuración completa


🎯 ¿Qué aprenderás en este manual?

Este manual te enseñará a implementar y usar el JAAKVisage SDK para detección facial automatizada y grabación de video en aplicaciones iOS nativas utilizando inteligencia artificial con MediaPipe. Requiere conocimientos avanzados en desarrollo iOS, frameworks nativos, gestión de permisos iOS, y arquitectura de aplicaciones iOS.


📋 Antes de Empezar - Lista de Verificación

Asegúrate de tener estos elementos listos:

  • Xcode 12.0+ instalado
  • Dispositivo iOS 12.0+ (físico, no simulador)
  • CocoaPods configurado
  • Conocimientos avanzados de Swift/UIKit/SwiftUI
  • Experiencia con AVFoundation y gestión de permisos
  • Acceso a cámara funcional
  • Persona para pruebas de detección facial

🗂️ Índice de Contenidos

SecciónQué harásTiempo
Paso 1Configurar proyecto y dependencias15 min
Paso 2Implementación básica del SDK25 min
Paso 3Configurar permisos de cámara10 min
Paso 4Manejo de grabación y archivos20 min
Paso 5Implementación avanzada (opcional)30 min
Paso 6Probar detección facial15 min

PASO 1: Configurar Proyecto y Dependencias

🎯 Objetivo

Instalar el JAAKVisage SDK y configurar el entorno de desarrollo.

✅ Requisitos Técnicos

RequisitoVersión¿Obligatorio?Notas
iOS12.0+Versión mínima soportada
Swift5.0+Lenguaje de programación base
Xcode12.0+Entorno de desarrollo
MediaPipe Tasks Vision~0.10.3Motor de detección facial AI (incluido)
AVFoundationSistemaPara captura de cámara
CámaraFísicaDispositivo debe tener cámara

1.1 Instalación CocoaPods

# Instalar CocoaPods si no lo tienes
sudo gem install cocoapods

# Crear Podfile en tu proyecto
cd /ruta/a/tu/proyecto
pod init

1.2 Configuración Podfile

platform :ios, '12.0'
use_frameworks!

target 'YourApp' do
  # SDK JAAKVisage - Versión Beta
  pod 'JAAKVisage', '~> 1.0.0-beta'
end

1.3 Instalación de Dependencias

# Instalar dependencias
pod install

# Abrir workspace (importante: no el .xcodeproj)
open YourApp.xcworkspace

1.4 Verificación de Dependencias

El SDK incluye automáticamente:

  • MediaPipe Tasks Vision ~0.10.3 (motor de detección facial)
  • AVFoundation (captura de cámara)
  • Foundation (funcionalidades base)

PASO 2: Implementación Básica del SDK

🎯 Objetivo

Crear la implementación base del JAAKVisage SDK con detección y grabación automatizada.

2.1 ViewController Básico - UIKit (Recomendado)

import UIKit
import JAAKVisage

class FaceDetectorViewController: UIViewController {
    
    private var detector: JAAKVisageSDK?
    private var previewView: UIView?
    
    @IBOutlet weak var statusLabel: UILabel!
    @IBOutlet weak var startButton: UIButton!
    @IBOutlet weak var restartButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupConfiguration()
        setupFaceDetector()
        setupUI()
    }
    
    private func setupConfiguration() {
        var config = JAAKVisageConfiguration()
        config.videoDuration = 5.0
        config.disableFaceDetection = false
        config.enableInstructions = true
        config.instructionDelay = 5.0
        config.instructionDuration = 2.0
        config.cameraPosition = .front
        
        // Inicializar detector
        detector = JAAKVisageSDK(configuration: config)
        detector?.delegate = self
    }
    
    private func setupFaceDetector() {
        guard let detector = detector else { return }
        
        // Crear vista de preview
        previewView = detector.createPreviewView()
        guard let previewView = previewView else { return }
        
        previewView.translatesAutoresizingMaskIntoConstraints = false
        view.insertSubview(previewView, at: 0)
        
        NSLayoutConstraint.activate([
            previewView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            previewView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.6)
        ])
    }
    
    private func setupUI() {
        statusLabel.text = "Listo para iniciar detección"
        statusLabel.textAlignment = .center
        statusLabel.numberOfLines = 0
        
        startButton.setTitle("Iniciar Detección", for: .normal)
        startButton.backgroundColor = .systemGreen
        startButton.layer.cornerRadius = 8
        
        restartButton.setTitle("Reiniciar Detector", for: .normal)
        restartButton.backgroundColor = .systemOrange
        restartButton.layer.cornerRadius = 8
    }
    
    @IBAction func startButtonTapped(_ sender: UIButton) {
        if sender.titleLabel?.text == "Iniciar Detección" {
            startDetection()
        } else {
            stopDetection()
        }
    }
    
    @IBAction func restartButtonTapped(_ sender: UIButton) {
        restartDetector()
    }
    
    private func startDetection() {
        do {
            try detector?.startDetection()
            statusLabel.text = "Detección iniciada"
            startButton.setTitle("Detener Detección", for: .normal)
            startButton.backgroundColor = .systemRed
        } catch {
            statusLabel.text = "Error al iniciar detección: \(error.localizedDescription)"
        }
    }
    
    private func stopDetection() {
        detector?.stopDetection()
        statusLabel.text = "Detección detenida"
        startButton.setTitle("Iniciar Detección", for: .normal)
        startButton.backgroundColor = .systemGreen
    }
    
    private func restartDetector() {
        do {
            try detector?.restartDetector()
            statusLabel.text = "Detector reiniciado exitosamente"
        } catch {
            statusLabel.text = "Error al reiniciar: \(error.localizedDescription)"
        }
    }
}

// MARK: - JAAKVisageSDKDelegate
extension FaceDetectorViewController: JAAKVisageSDKDelegate {
    
    func faceDetector(_ detector: JAAKVisageSDK, didUpdateStatus status: JAAKVisageStatus) {
        DispatchQueue.main.async {
            switch status {
            case .notLoaded:
                self.statusLabel.text = "Modelos no cargados"
            case .loading:
                self.statusLabel.text = "Cargando modelos de IA..."
            case .loaded:
                self.statusLabel.text = "Listo para detección"
            case .running:
                self.statusLabel.text = "Detectando rostro..."
            case .recording:
                self.statusLabel.text = "¡Grabando video!"
            case .finished:
                self.statusLabel.text = "Grabación completada"
            case .stopped:
                self.statusLabel.text = "Detección detenida"
            case .error:
                self.statusLabel.text = "Error en detección"
            case .faceDetected:
                self.statusLabel.text = "✅ Rostro detectado"
            case .countdown:
                self.statusLabel.text = "Preparando grabación..."
            case .captureComplete:
                self.statusLabel.text = "Captura completa"
            case .processingVideo:
                self.statusLabel.text = "Procesando video..."
            case .videoReady:
                self.statusLabel.text = "Video listo"
            }
        }
    }
    
    func faceDetector(_ detector: JAAKVisageSDK, didCaptureFile result: JAAKFileResult) {
        DispatchQueue.main.async {
            self.statusLabel.text = "¡Video capturado! (\(result.fileSize) bytes)"
            self.processVideoFile(result)
        }
    }
    
    func faceDetector(_ detector: JAAKVisageSDK, didEncounterError error: JAAKVisageError) {
        DispatchQueue.main.async {
            self.statusLabel.text = "Error: \(error.label)"
            self.handleVisageError(error)
        }
    }
    
    func faceDetector(_ detector: JAAKVisageSDK, didDetectFace message: JAAKFaceDetectionMessage) {
        DispatchQueue.main.async {
            if message.faceExists && message.correctPosition {
                self.statusLabel.text = "✅ Rostro detectado en posición correcta"
            } else if message.faceExists {
                self.statusLabel.text = "⚠️ Ajuste la posición del rostro"
            } else {
                self.statusLabel.text = "❌ No se detecta rostro"
            }
        }
    }
    
    private func processVideoFile(_ result: JAAKFileResult) {
        print("=== PROCESANDO VIDEO ===")
        print("Nombre: \(result.fileName ?? "unknown")")
        print("Tamaño: \(result.fileSize) bytes")
        print("Tipo MIME: \(result.mimeType ?? "unknown")")
        
        // Guardar localmente
        saveToDocuments(result)
        
        // Convertir a base64 para servidor
        let base64String = result.base64
        sendToServer(base64String, fileName: result.fileName)
    }
    
    private func handleVisageError(_ error: JAAKVisageError) {
        guard let errorCode = error.code else { return }
        
        switch errorCode {
        case "camera-access":
            showCameraPermissionError()
        case "model-loading":
            showModelLoadingError()
        case "video-recording":
            showRecordingError()
        case "device-not-supported":
            showDeviceNotSupportedError()
        default:
            showGenericError(error.label)
        }
    }
    
    private func saveToDocuments(_ result: JAAKFileResult) {
        guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { 
            return 
        }
        
        let fileName = result.fileName ?? "face_video_\(Date().timeIntervalSince1970).mp4"
        let fileURL = documentsPath.appendingPathComponent(fileName)
        
        do {
            try result.data.write(to: fileURL)
            print("Video guardado en: \(fileURL)")
        } catch {
            print("Error guardando video: \(error)")
        }
    }
    
    private func sendToServer(_ base64String: String, fileName: String?) {
        let payload: [String: Any] = [
            "videoData": base64String,
            "fileName": fileName ?? "face_video.mp4",
            "timestamp": Date().timeIntervalSince1970,
            "platform": "ios",
            "sdkVersion": "1.0.0-beta"
        ]
        
        print("Enviando video al servidor: \(payload.keys)")
        // Implementar llamada HTTP
    }
    
    private func showCameraPermissionError() {
        let alert = UIAlertController(
            title: "Error de Cámara",
            message: "No se puede acceder a la cámara. Verifica los permisos.",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "Configuración", style: .default) { _ in
            self.openSettings()
        })
        alert.addAction(UIAlertAction(title: "Cancelar", style: .cancel))
        present(alert, animated: true)
    }
    
    private func showModelLoadingError() {
        let alert = UIAlertController(
            title: "Error de Modelo",
            message: "No se pudieron cargar los modelos de IA. Reinicia la aplicación.",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "Entendido", style: .default))
        present(alert, animated: true)
    }
    
    private func showRecordingError() {
        statusLabel.text = "Error en grabación, intenta nuevamente"
    }
    
    private func showDeviceNotSupportedError() {
        let alert = UIAlertController(
            title: "Dispositivo No Compatible",
            message: "Este dispositivo no es compatible con la detección facial.",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "Entendido", style: .default))
        present(alert, animated: true)
    }
    
    private func showGenericError(_ message: String) {
        statusLabel.text = "Error: \(message)"
    }
    
    private func openSettings() {
        guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
            return
        }
        
        if UIApplication.shared.canOpenURL(settingsUrl) {
            UIApplication.shared.open(settingsUrl)
        }
    }
}

2.2 Implementación SwiftUI

import SwiftUI
import JAAKVisage

// Manager para controlar el SDK
class VisageManager: ObservableObject {
    @Published var statusMessage = "Listo para detectar rostro"
    @Published var currentStatus: JAAKVisageStatus = .notLoaded
    @Published var isDetectionActive = false
    @Published var lastCapturedFile = ""
    @Published var recordedVideos: [RecordedVideo] = []
    
    private(set) var configuration: JAAKVisageConfiguration
    private var visageUIView: JAAKVisageUIView?
    
    init() {
        var config = JAAKVisageConfiguration()
        config.videoDuration = 5.0
        config.disableFaceDetection = false
        config.enableInstructions = true
        config.instructionDelay = 5.0
        config.instructionDuration = 2.0
        config.cameraPosition = .front
        
        self.configuration = config
    }
    
    func setVisageUIView(_ view: JAAKVisageUIView) {
        self.visageUIView = view
        
        // Auto-iniciar detección después de un breve delay
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.startDetection()
        }
    }
    
    func startDetection() {
        statusMessage = "Iniciando detección facial..."
        isDetectionActive = true
        
        visageUIView?.startDetection()
    }
    
    func stopDetection() {
        statusMessage = "Deteniendo detección facial..."
        isDetectionActive = false
        
        visageUIView?.stopDetection()
    }
    
    func toggleDetection() {
        if isDetectionActive {
            stopDetection()
        } else {
            startDetection()
        }
    }
    
    func restartDetector() {
        statusMessage = "Reiniciando detector..."
        
        do {
            try visageUIView?.restartDetector()
            statusMessage = "Detector reiniciado exitosamente"
        } catch {
            statusMessage = "Error al reiniciar: \(error.localizedDescription)"
        }
    }
    
    func updateConfiguration(_ newConfiguration: JAAKVisageConfiguration) {
        self.configuration = newConfiguration
        
        // Verificar si podemos actualizar dinámicamente
        if let uiView = visageUIView, let faceDetector = uiView.faceDetector {
            faceDetector.updateConfiguration(newConfiguration)
            statusMessage = "Configuración actualizada"
        } else {
            statusMessage = "Configuración actualizada - reinicia detección para aplicar cambios"
        }
    }
}

// Estructura para almacenar videos grabados
struct RecordedVideo: Identifiable {
    let id = UUID()
    let fileName: String
    let data: Data
    let recordedAt: Date
    let duration: TimeInterval?
    
    var formattedDate: String {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .medium
        return formatter.string(from: recordedAt)
    }
    
    var sizeString: String {
        return ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .file)
    }
}

// MARK: - JAAKVisageSDKDelegate
extension VisageManager: JAAKVisageSDKDelegate {
    
    func faceDetector(_ detector: JAAKVisageSDK, didUpdateStatus status: JAAKVisageStatus) {
        DispatchQueue.main.async {
            self.currentStatus = status
            
            switch status {
            case .notLoaded:
                self.statusMessage = "Modelos no cargados"
            case .loading:
                self.statusMessage = "Cargando modelos de IA..."
            case .loaded:
                self.statusMessage = "Listo para detección"
            case .running:
                self.statusMessage = "Detectando rostro..."
            case .recording:
                self.statusMessage = "¡Grabando video!"
            case .finished:
                self.statusMessage = "Grabación completada"
            case .stopped:
                self.statusMessage = "Detección detenida"
            case .error:
                self.statusMessage = "Error en detección"
            case .faceDetected:
                self.statusMessage = "✅ Rostro detectado"
            case .countdown:
                self.statusMessage = "Preparando grabación..."
            case .captureComplete:
                self.statusMessage = "Captura completa"
            case .processingVideo:
                self.statusMessage = "Procesando video..."
            case .videoReady:
                self.statusMessage = "Video listo"
            }
            
            self.isDetectionActive = (status == .running || status == .recording)
        }
    }
    
    func faceDetector(_ detector: JAAKVisageSDK, didDetectFace message: JAAKFaceDetectionMessage) {
        if message.faceExists && message.correctPosition {
            DispatchQueue.main.async {
                self.statusMessage = "✅ Rostro detectado en posición correcta"
            }
        }
    }
    
    func faceDetector(_ detector: JAAKVisageSDK, didCaptureFile result: JAAKFileResult) {
        DispatchQueue.main.async {
            self.lastCapturedFile = result.fileName ?? "Unknown"
            self.statusMessage = "Video capturado: \(result.fileName ?? "Unknown")"
            
            // Si es un archivo de video, agregarlo a la lista
            if let fileName = result.fileName, 
               (fileName.hasSuffix(".mov") || fileName.hasSuffix(".mp4") || fileName.hasSuffix(".m4v")) {
                let recordedVideo = RecordedVideo(
                    fileName: fileName,
                    data: result.data,
                    recordedAt: Date(),
                    duration: nil
                )
                self.recordedVideos.append(recordedVideo)
            }
        }
    }
    
    func faceDetector(_ detector: JAAKVisageSDK, didEncounterError error: JAAKVisageError) {
        DispatchQueue.main.async {
            self.statusMessage = "Error: \(error.label)"
        }
    }
}

// Wrapper para usar UIKit en SwiftUI
struct VisageViewWrapper: UIViewRepresentable {
    let manager: VisageManager
    
    func makeUIView(context: Context) -> JAAKVisageUIView {
        let view = JAAKVisageUIView()
        view.backgroundColor = UIColor.black
        manager.setVisageUIView(view)
        return view
    }
    
    func updateUIView(_ uiView: JAAKVisageUIView, context: Context) {
        if !uiView.isSetup {
            let detector = JAAKVisageSDK(configuration: manager.configuration)
            detector.delegate = manager
            
            let previewView = detector.createPreviewView()
            uiView.addSubview(previewView)
            previewView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                previewView.topAnchor.constraint(equalTo: uiView.topAnchor),
                previewView.leadingAnchor.constraint(equalTo: uiView.leadingAnchor),
                previewView.trailingAnchor.constraint(equalTo: uiView.trailingAnchor),
                previewView.bottomAnchor.constraint(equalTo: uiView.bottomAnchor)
            ])
            
            uiView.faceDetector = detector
            uiView.previewView = previewView
            uiView.isSetup = true
        }
    }
}

// Vista principal de SwiftUI
struct ContentView: View {
    @ObservedObject private var visageManager = VisageManager()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Detección Facial")
                .font(.title)
                .fontWeight(.bold)
            
            // Vista del detector
            VisageViewWrapper(manager: visageManager)
                .frame(height: 400)
                .cornerRadius(16)
                .overlay(
                    RoundedRectangle(cornerRadius: 16)
                        .stroke(visageManager.isDetectionActive ? Color.red : Color.blue, lineWidth: 2)
                )
            
            // Status
            VStack(spacing: 8) {
                Text(visageManager.statusMessage)
                    .font(.body)
                    .foregroundColor(.primary)
                    .multilineTextAlignment(.center)
                
                Text("Estado: \(visageManager.currentStatus.rawValue)")
                    .font(.caption)
                    .foregroundColor(.secondary)
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .background(Color.blue.opacity(0.1))
                    .cornerRadius(4)
            }
            .padding()
            .background(Color.gray.opacity(0.1))
            .cornerRadius(8)
            
            // Controles
            HStack(spacing: 15) {
                Button(action: {
                    visageManager.toggleDetection()
                }) {
                    Text(visageManager.isDetectionActive ? "Detener" : "Iniciar")
                        .foregroundColor(.white)
                        .padding()
                        .background(visageManager.isDetectionActive ? Color.red : Color.green)
                        .cornerRadius(8)
                }
                
                Button(action: {
                    visageManager.restartDetector()
                }) {
                    Text("Reiniciar")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.orange)
                        .cornerRadius(8)
                }
            }
            
            // Lista de videos grabados
            if !visageManager.recordedVideos.isEmpty {
                VStack(alignment: .leading) {
                    Text("Videos Grabados (\(visageManager.recordedVideos.count))")
                        .font(.headline)
                    
                    ForEach(visageManager.recordedVideos.reversed()) { video in
                        VideoRowView(video: video)
                    }
                }
                .padding()
            }
            
            Spacer()
        }
        .padding()
    }
}

struct VideoRowView: View {
    let video: RecordedVideo
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(video.fileName)
                    .font(.headline)
                Text(video.sizeString)
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
            
            Spacer()
            
            Text(video.formattedDate)
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(8)
    }
}

PASO 3: Configurar Permisos de Cámara

🎯 Objetivo

Configurar correctamente los permisos de cámara requeridos por iOS.

3.1 Configuración Info.plist (Método Tradicional)

<key>NSCameraUsageDescription</key>
<string>Esta aplicación necesita acceso a la cámara para detectar rostros y grabar videos</string>

3.2 Configuración en Xcode (Versiones Recientes)

  1. En Xcode, selecciona tu target
  2. Ve a la pestaña "Info"
  3. En "Custom iOS Target Properties", haz clic en "+"
  4. Busca y selecciona "Privacy - Camera Usage Description"
  5. Añade la descripción: "Esta aplicación necesita acceso a la cámara para detectar rostros"

3.3 Manejo de Permisos en Código

import AVFoundation

class PermissionsManager {
    
    static func checkCameraPermission() -> AVAuthorizationStatus {
        return AVCaptureDevice.authorizationStatus(for: .video)
    }
    
    static func requestCameraPermission(completion: @escaping (Bool) -> Void) {
        AVCaptureDevice.requestAccess(for: .video) { granted in
            DispatchQueue.main.async {
                completion(granted)
            }
        }
    }
    
    static func openSettings() {
        guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
            return
        }
        
        if UIApplication.shared.canOpenURL(settingsUrl) {
            UIApplication.shared.open(settingsUrl)
        }
    }
}

3.4 Verificación Antes de Detección

private func checkPermissionsAndStart() {
    let cameraStatus = PermissionsManager.checkCameraPermission()
    
    switch cameraStatus {
    case .authorized:
        startDetection()
    case .notDetermined:
        PermissionsManager.requestCameraPermission { granted in
            if granted {
                self.startDetection()
            } else {
                self.showPermissionAlert()
            }
        }
    case .denied, .restricted:
        showPermissionAlert()
    @unknown default:
        showPermissionAlert()
    }
}

private func showPermissionAlert() {
    let alert = UIAlertController(
        title: "Permisos de Cámara",
        message: "Se requiere acceso a la cámara para la detección facial",
        preferredStyle: .alert
    )
    
    alert.addAction(UIAlertAction(title: "Configuración", style: .default) { _ in
        PermissionsManager.openSettings()
    })
    
    alert.addAction(UIAlertAction(title: "Cancelar", style: .cancel))
    
    present(alert, animated: true)
}

PASO 4: Manejo de Grabación y Archivos

🎯 Objetivo

Implementar el manejo completo de videos grabados según la estructura oficial del SDK.

4.1 Estructura de Respuesta Completa

// Estructura oficial del SDK
public struct JAAKFileResult {
    public let data: Data           // Datos binarios del video
    public let base64: String       // Video codificado en base64
    public let mimeType: String?    // Tipo MIME (video/mp4)
    public let fileName: String?    // Nombre del archivo
    public let fileSize: Int        // Tamaño del archivo en bytes
}

public struct JAAKFaceDetectionMessage {
    public let label: String            // Mensaje descriptivo
    public let details: String?         // Detalles adicionales
    public let faceExists: Bool         // Si se detectó un rostro
    public let correctPosition: Bool    // Si el rostro está en posición correcta
}

public enum JAAKVisageStatus: String, CaseIterable {
    case notLoaded = "not-loaded"
    case loading = "loading"
    case loaded = "loaded"
    case running = "running"
    case recording = "recording"
    case finished = "finished"
    case stopped = "stopped"
    case error = "error"
    case faceDetected = "face-detected"
    case countdown = "countdown"
    case captureComplete = "capture-complete"
    case processingVideo = "processing-video"
    case videoReady = "video-ready"
}

4.2 Procesamiento Completo de Videos

func faceDetector(_ detector: JAAKVisageSDK, didCaptureFile result: JAAKFileResult) {
    DispatchQueue.main.async {
        self.statusLabel.text = "¡Video capturado! (\(result.fileSize) bytes)"
        self.processVideoFile(result)
    }
}

private func processVideoFile(_ result: JAAKFileResult) {
    print("=== PROCESANDO VIDEO ===")
    print("Nombre: \(result.fileName ?? "unknown")")
    print("Tamaño: \(result.fileSize) bytes")
    print("Tipo MIME: \(result.mimeType ?? "unknown")")
    
    // Validar que tenemos datos válidos
    guard !result.data.isEmpty else {
        print("Error: Datos de video vacíos")
        return
    }
    
    // Guardar en documentos locales
    saveToDocuments(result)
    
    // Convertir a base64 para envío al servidor
    let base64String = result.base64
    sendToServer(base64String, fileName: result.fileName)
    
    // Crear registro para la UI
    createVideoRecord(result)
}

private func saveToDocuments(_ result: JAAKFileResult) {
    guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { 
        print("Error: No se puede acceder al directorio de documentos")
        return 
    }
    
    let fileName = result.fileName ?? "face_video_\(Date().timeIntervalSince1970).mp4"
    let fileURL = documentsPath.appendingPathComponent(fileName)
    
    do {
        try result.data.write(to: fileURL)
        print("Video guardado en: \(fileURL)")
    } catch {
        print("Error guardando video: \(error)")
    }
}

private func sendToServer(_ base64String: String, fileName: String?) {
    let payload: [String: Any] = [
        "videoData": base64String,
        "fileName": fileName ?? "face_video.mp4",
        "timestamp": Date().timeIntervalSince1970,
        "platform": "ios",
        "sdkVersion": "1.0.0-beta",
        "videoSize": base64String.count
    ]
    
    print("Enviando video al servidor: \(payload.keys)")
    // Implementar llamada HTTP aquí
    // NetworkManager.shared.uploadVideo(payload: payload) { result in ... }
}

private func createVideoRecord(_ result: JAAKFileResult) {
    let video = RecordedVideo(
        fileName: result.fileName ?? "video_\(Date().timeIntervalSince1970).mp4",
        data: result.data,
        recordedAt: Date(),
        duration: nil // Podríamos extraer esto de los metadatos si es necesario
    )
    
    // Si usas un manager, actualizar la lista
    // recordedVideos.append(video)
}

4.3 Manejo Completo de Errores

func faceDetector(_ detector: JAAKVisageSDK, didEncounterError error: JAAKVisageError) {
    DispatchQueue.main.async {
        self.statusLabel.text = "Error: \(error.label)"
        self.handleVisageError(error)
    }
}

private func handleVisageError(_ error: JAAKVisageError) {
    guard let errorCode = error.code else {
        showGenericError(error.label)
        return
    }
    
    switch errorCode {
    case "camera-access":
        handleCameraAccessError()
    case "model-loading":
        handleModelLoadingError()
    case "video-recording":
        handleVideoRecordingError()
    case "device-not-supported":
        handleDeviceNotSupportedError()
    case "permission-denied":
        handlePermissionDeniedError()
    default:
        showGenericError(error.label)
    }
}

private func handleCameraAccessError() {
    let alert = UIAlertController(
        title: "Error de Cámara",
        message: "No se puede acceder a la cámara. Verifica que no esté siendo usada por otra aplicación.",
        preferredStyle: .alert
    )
    alert.addAction(UIAlertAction(title: "Entendido", style: .default))
    present(alert, animated: true)
}

private func handleModelLoadingError() {
    let alert = UIAlertController(
        title: "Error de Modelos ML",
        message: "No se pudieron cargar los modelos de MediaPipe. Reinicia la aplicación e intenta nuevamente.",
        preferredStyle: .alert
    )
    alert.addAction(UIAlertAction(title: "Entendido", style: .default))
    present(alert, animated: true)
}

private func handleVideoRecordingError() {
    let alert = UIAlertController(
        title: "Error de Grabación",
        message: "Error durante la grabación del video. Verifica el espacio de almacenamiento disponible.",
        preferredStyle: .alert
    )
    alert.addAction(UIAlertAction(title: "Entendido", style: .default))
    present(alert, animated: true)
}

private func handleDeviceNotSupportedError() {
    let alert = UIAlertController(
        title: "Dispositivo No Compatible",
        message: "Este dispositivo no es compatible con la detección facial de JAAKVisage.",
        preferredStyle: .alert
    )
    alert.addAction(UIAlertAction(title: "Entendido", style: .default))
    present(alert, animated: true)
}

private func handlePermissionDeniedError() {
    let alert = UIAlertController(
        title: "Permisos Denegados",
        message: "Se requiere acceso a la cámara para usar la detección facial. Ve a Configuración para habilitarlo.",
        preferredStyle: .alert
    )
    alert.addAction(UIAlertAction(title: "Configuración", style: .default) { _ in
        PermissionsManager.openSettings()
    })
    alert.addAction(UIAlertAction(title: "Cancelar", style: .cancel))
    present(alert, animated: true)
}

private func showGenericError(_ message: String) {
    let alert = UIAlertController(
        title: "Error",
        message: "Ocurrió un error inesperado: \(message)",
        preferredStyle: .alert
    )
    alert.addAction(UIAlertAction(title: "Entendido", style: .default))
    present(alert, animated: true)
}

PASO 5: Implementación Avanzada (Opcional)

🎯 Objetivo

Implementar funcionalidades avanzadas del SDK con configuración personalizada y componentes adicionales.

5.1 Configuración Avanzada

private func setupAdvancedConfiguration() {
    var config = JAAKVisageConfiguration()
    
    // Configuración básica
    config.videoDuration = 5.0
    config.cameraPosition = .front
    config.disableFaceDetection = false
    
    // Instrucciones avanzadas
    config.enableInstructions = true
    config.instructionDelay = 3.0
    config.instructionDuration = 2.5
    config.instructionsText = [
        "Mira directamente a la cámara",
        "Mantén tu rostro centrado",
        "Asegúrate de tener buena iluminación"
    ]
    
    // Estilos personalizados del timer
    var timerStyles = JAAKTimerStyles()
    timerStyles.strokeColor = UIColor.systemBlue
    timerStyles.strokeWidth = 4.0
    timerStyles.backgroundColor = UIColor.clear
    config.timerStyles = timerStyles
    
    // Estilos personalizados del face tracker
    var faceTrackerStyles = JAAKFaceTrackerStyles()
    faceTrackerStyles.borderColor = UIColor.systemGreen
    faceTrackerStyles.borderWidth = 2.0
    faceTrackerStyles.cornerRadius = 8.0
    config.faceTrackerStyles = faceTrackerStyles
    
    // Configuración de dimensiones
    config.width = view.bounds.width
    config.height = view.bounds.height * 0.6
    
    // Inicializar detector con configuración avanzada
    detector = JAAKVisageSDK(configuration: config)
    detector?.delegate = self
}

5.2 Grabación Manual

// Método para grabación manual cuando no se use auto-recorder
private func recordVideoManually() {
    guard let detector = detector else { return }
    
    // Verificar que la detección está activa
    guard detector.isDetectionActive else {
        statusLabel.text = "Inicia la detección antes de grabar"
        return
    }
    
    // Iniciar grabación manual
    detector.recordVideo { [weak self] result in
        DispatchQueue.main.async {
            switch result {
            case .success(let fileResult):
                self?.statusLabel.text = "Video grabado manualmente: \(fileResult.fileName ?? "unknown")"
                self?.processVideoFile(fileResult)
            case .failure(let error):
                self?.statusLabel.text = "Error en grabación manual: \(error.localizedDescription)"
            }
        }
    }
}

5.3 Gestión Avanzada de Estado

// Extension para manejo avanzado de estados
extension FaceDetectorViewController {
    
    private func handleAdvancedStatusUpdate(_ status: JAAKVisageStatus) {
        switch status {
        case .notLoaded:
            updateUI(message: "Inicializando...", color: .systemGray, showSpinner: false)
        case .loading:
            updateUI(message: "Cargando modelos ML...", color: .systemOrange, showSpinner: true)
        case .loaded:
            updateUI(message: "✅ Listo para detección", color: .systemGreen, showSpinner: false)
        case .running:
            updateUI(message: "🔍 Buscando rostro...", color: .systemBlue, showSpinner: false)
        case .faceDetected:
            updateUI(message: "👤 Rostro detectado", color: .systemGreen, showSpinner: false)
        case .countdown:
            updateUI(message: "⏱️ Preparando grabación...", color: .systemOrange, showSpinner: false)
        case .recording:
            updateUI(message: "🎥 Grabando video...", color: .systemRed, showSpinner: false)
        case .processingVideo:
            updateUI(message: "⚙️ Procesando video...", color: .systemPurple, showSpinner: true)
        case .captureComplete:
            updateUI(message: "✅ Captura completada", color: .systemGreen, showSpinner: false)
        case .videoReady:
            updateUI(message: "📹 Video listo", color: .systemGreen, showSpinner: false)
        case .finished:
            updateUI(message: "🎯 Proceso finalizado", color: .systemGreen, showSpinner: false)
        case .stopped:
            updateUI(message: "⏹️ Detección detenida", color: .systemGray, showSpinner: false)
        case .error:
            updateUI(message: "❌ Error en proceso", color: .systemRed, showSpinner: false)
        }
    }
    
    private func updateUI(message: String, color: UIColor, showSpinner: Bool) {
        statusLabel.text = message
        statusLabel.textColor = color
        
        if showSpinner {
            // Mostrar spinner de carga
            startSpinner()
        } else {
            stopSpinner()
        }
    }
    
    private func startSpinner() {
        // Implementar spinner de carga
    }
    
    private func stopSpinner() {
        // Detener spinner de carga
    }
}

5.4 Utilidades y Componentes Adicionales

// Utilidades para trabajar con el SDK
class JAAKVisageUtilities {
    
    static func isDeviceSupported() -> Bool {
        // Verificar si el dispositivo soporta MediaPipe
        let systemVersion = UIDevice.current.systemVersion
        let versionComponents = systemVersion.components(separatedBy: ".")
        guard let majorVersion = Int(versionComponents[0]) else { return false }
        
        return majorVersion >= 12
    }
    
    static func getCameraPermissionStatus() -> AVAuthorizationStatus {
        return AVCaptureDevice.authorizationStatus(for: .video)
    }
    
    static func getAvailableCameras() -> [AVCaptureDevice] {
        let discoverySession = AVCaptureDevice.DiscoverySession(
            deviceTypes: [.builtInWideAngleCamera],
            mediaType: .video,
            position: .unspecified
        )
        return discoverySession.devices
    }
    
    static func formatFileSize(_ bytes: Int) -> String {
        return ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .file)
    }
    
    static func validateVideoData(_ data: Data) -> Bool {
        // Verificar que el data contiene un video válido
        return data.count > 0 && data.count < 100_000_000 // Max 100MB
    }
}

// Extension para trabajar con configuraciones predefinidas
extension JAAKVisageConfiguration {
    
    static func highQualityConfig() -> JAAKVisageConfiguration {
        var config = JAAKVisageConfiguration()
        config.videoDuration = 8.0
        config.enableInstructions = true
        config.instructionDelay = 2.0
        config.instructionDuration = 3.0
        return config
    }
    
    static func quickCaptureConfig() -> JAAKVisageConfiguration {
        var config = JAAKVisageConfiguration()
        config.videoDuration = 3.0
        config.enableInstructions = false
        config.instructionDelay = 0.0
        return config
    }
    
    static func debugConfig() -> JAAKVisageConfiguration {
        var config = JAAKVisageConfiguration()
        config.videoDuration = 5.0
        config.enableInstructions = true
        config.instructionDelay = 1.0
        config.instructionDuration = 4.0
        config.instructionsText = [
            "DEBUG: Posiciona tu rostro",
            "DEBUG: Mantén la posición",
            "DEBUG: Grabando..."
        ]
        return config
    }
}

PASO 6: Probar Detección Facial

🎯 Objetivo

Verificar que todas las funcionalidades del SDK funcionan correctamente.

✅ Lista de Verificación

ElementoDescripción
Compatibilidad del dispositivoDispositivo soporta MediaPipe
Permisos de cámaraApp solicita y obtiene permisos
Detección facialIA detecta rostros automáticamente
Grabación automáticaInicia grabación al detectar rostro
Archivos generadosVideos se guardan correctamente
Manejo de erroresErrores se manejan correctamente
Estados del SDKTransiciones de estado funcionan
Configuración avanzadaParámetros personalizados funcionan

🔍 Proceso de Prueba

Paso A: Verificar Compatibilidad

// Agregar a tu código de prueba
private func testCompatibility() {
    print("=== PRUEBA DE COMPATIBILIDAD ===")
    
    let isSupported = JAAKVisageUtilities.isDeviceSupported()
    print("Dispositivo compatible: \(isSupported)")
    
    let cameraStatus = JAAKVisageUtilities.getCameraPermissionStatus()
    print("Estado cámara: \(cameraStatus)")
    
    let availableCameras = JAAKVisageUtilities.getAvailableCameras()
    print("Cámaras disponibles: \(availableCameras.count)")
    
    let systemVersion = UIDevice.current.systemVersion
    print("Versión iOS: \(systemVersion)")
    
    assert(isSupported, "El dispositivo debe ser compatible")
}

Paso B: Probar Permisos

  1. Instalar aplicación en dispositivo físico
  2. Abrir aplicación por primera vez
  3. Verificar solicitud de permisos automática
  4. Conceder permisos de cámara
  5. Confirmar que la cámara inicia correctamente

Paso C: Probar Detección Básica

  1. Presionar botón "Iniciar Detección"
  2. Posicionar rostro frente a la cámara
  3. Esperar detección automática (overlay verde)
  4. Verificar inicio de grabación automática
  5. Revisar logs para confirmar procesamiento

Paso D: Probar Estados del SDK

private func testSDKStates() {
    guard let detector = detector else { return }
    
    // Verificar estado inicial
    print("Estado inicial: \(detector.currentStatus)")
    
    // Iniciar detección
    do {
        try detector.startDetection()
        print("Detección iniciada")
    } catch {
        print("Error iniciando detección: \(error)")
    }
    
    // Verificar cambios de estado
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        print("Estado después de 2s: \(detector.currentStatus)")
    }
}

Paso E: Probar Configuración Avanzada

private func testAdvancedConfiguration() {
    // Probar configuración de alta calidad
    let highQualityConfig = JAAKVisageConfiguration.highQualityConfig()
    detector?.updateConfiguration(highQualityConfig)
    
    // Probar configuración rápida
    let quickConfig = JAAKVisageConfiguration.quickCaptureConfig()
    detector?.updateConfiguration(quickConfig)
    
    // Probar configuración debug
    let debugConfig = JAAKVisageConfiguration.debugConfig()
    detector?.updateConfiguration(debugConfig)
    
    print("Configuraciones avanzadas probadas")
}

🛠️ Debugging y Troubleshooting

Verificar Estado del SDK

private func debugSDKState() {
    print("=== DEBUG DEL SDK ===")
    
    // Verificar compatibilidad
    let isSupported = JAAKVisageUtilities.isDeviceSupported()
    print("Compatible: \(isSupported)")
    
    // Verificar estado de la cámara
    let cameraStatus = JAAKVisageUtilities.getCameraPermissionStatus()
    print("Permisos cámara: \(cameraStatus)")
    
    // Verificar dispositivos disponibles
    let cameras = JAAKVisageUtilities.getAvailableCameras()
    print("Cámaras disponibles: \(cameras.count)")
    
    // Verificar estado del detector
    if let detector = detector {
        print("Detector inicializado: Sí")
        print("Estado actual: \(detector.currentStatus)")
        print("Detección activa: \(detector.isDetectionActive)")
    } else {
        print("Detector inicializado: No")
    }
}

Habilitar Logging Detallado

private func enableDetailedLogging() {
    var config = JAAKVisageConfiguration.debugConfig()
    
    // Configurar logging detallado
    config.instructionsText = [
        "DEBUG: Inicializando MediaPipe...",
        "DEBUG: Detectando rostro...",
        "DEBUG: Grabando video...",
        "DEBUG: Procesando resultado..."
    ]
    
    detector = JAAKVisageSDK(configuration: config)
    detector?.delegate = self
}

Problemas Comunes y Soluciones

ProblemaCausa ProbableSolución
"Modelos no cargan"MediaPipe no se inicializa correctamente✅ Verificar iOS 12.0+, reiniciar app
"Cámara no inicia"Permisos denegados o cámara ocupada✅ Verificar permisos y cerrar otras apps
"No detecta rostros"Iluminación pobre o MediaPipe no funciona✅ Mejorar iluminación y verificar logs
"Grabación falla"Espacio insuficiente o error de MediaPipe✅ Verificar espacio y reiniciar SDK
"Estados inconsistentes"Configuración incorrecta o threading✅ Verificar configuración y usar main thread
"Crashes al iniciar"Dependencias faltantes o configuración mal✅ Verificar pod install y configuración

Test de Integración Completa

func testCompleteIntegration() {
    // 1. Verificar compatibilidad
    XCTAssertTrue(JAAKVisageUtilities.isDeviceSupported())
    
    // 2. Crear configuración
    let config = JAAKVisageConfiguration()
    let detector = JAAKVisageSDK(configuration: config)
    XCTAssertNotNil(detector)
    
    // 3. Verificar delegado
    detector.delegate = self
    XCTAssertNotNil(detector.delegate)
    
    // 4. Crear preview view
    let previewView = detector.createPreviewView()
    XCTAssertNotNil(previewView)
    
    // 5. Iniciar detección
    let expectation = XCTestExpectation(description: "Detection start")
    do {
        try detector.startDetection()
        expectation.fulfill()
    } catch {
        XCTFail("Error starting detection: \(error)")
    }
    
    wait(for: [expectation], timeout: 5.0)
    
    // 6. Verificar estado
    XCTAssertTrue(detector.isDetectionActive)
    
    print("✅ Test de integración completado")
}

📚 Referencia Completa del SDK

🔧 Métodos Principales

MétodoDescripciónEjemplo
JAAKVisageSDK(configuration:)Constructor del SDKJAAKVisageSDK(configuration: config)
startDetection()Inicia detección facialtry detector.startDetection()
stopDetection()Detiene deteccióndetector.stopDetection()
restartDetector()Reinicia el detectortry detector.restartDetector()
createPreviewView()Crea vista de previewlet view = detector.createPreviewView()
recordVideo(completion:)Grabación manualdetector.recordVideo { result in }
updateConfiguration(_:)Actualiza configuracióndetector.updateConfiguration(config)

⚙️ Propiedades de Configuración

PropiedadDescripciónTipoValor por Defecto
videoDurationDuración de grabaciónTimeInterval5.0
cameraPositionPosición de cámaraAVCaptureDevice.Position.front
disableFaceDetectionDeshabilitar detecciónBoolfalse
enableInstructionsMostrar instruccionesBoolfalse
instructionDelayDelay de instruccionesTimeInterval5.0
instructionDurationDuración de instruccionesTimeInterval2.0
instructionsTextTextos personalizados[String][]
widthAncho de vistaCGFloat0 (auto)
heightAlto de vistaCGFloat0 (auto)
timerStylesEstilos del timerJAAKTimerStylesdefault
faceTrackerStylesEstilos del trackerJAAKFaceTrackerStylesdefault

📡 Estados del SDK

JAAKVisageStatus

  • notLoaded - Modelos no cargados
  • loading - Cargando modelos ML
  • loaded - Listo para detección
  • running - Detectando rostro
  • faceDetected - Rostro detectado
  • countdown - Cuenta regresiva
  • recording - Grabando video
  • processingVideo - Procesando video
  • captureComplete - Captura completa
  • videoReady - Video listo
  • finished - Proceso finalizado
  • stopped - Detección detenida
  • error - Error en proceso

📊 Estructura de Delegados

JAAKVisageSDKDelegate

  • faceDetector(_:didUpdateStatus:) - Actualización de estado
  • faceDetector(_:didDetectFace:) - Detección de rostro
  • faceDetector(_:didCaptureFile:) - Archivo capturado
  • faceDetector(_:didEncounterError:) - Error encontrado

⚠️ Tipos de Error

public struct JAAKVisageError: LocalizedError {
    public let label: String        // Descripción del error
    public let code: String?        // Código de error específico
    public let details: Any?        // Detalles adicionales del error
}

Códigos de Error Comunes

  • camera-access - Error de acceso a cámara
  • model-loading - Error cargando modelos ML
  • video-recording - Error en grabación
  • device-not-supported - Dispositivo no compatible
  • permission-denied - Permisos denegados

🚨 Solución de Problemas Avanzada

Problemas de Compatibilidad

Error: "Modelos ML no se cargan"

// ✅ Verificar antes de usar
if !JAAKVisageUtilities.isDeviceSupported() {
    showAlert(title: "Dispositivo No Compatible", 
              message: "Este dispositivo no soporta MediaPipe")
    return
}

// Verificar memoria disponible
let memoryInfo = ProcessInfo.processInfo.physicalMemory
if memoryInfo < 1_000_000_000 { // Menos de 1GB
    print("⚠️ Memoria baja detectada")
}

Error: "MediaPipe no funciona"

// ✅ Reiniciar detector si falla
private func handleMediaPipeError() {
    do {
        try detector?.restartDetector()
        print("Detector reiniciado exitosamente")
    } catch {
        print("Error reiniciando detector: \(error)")
        // Recrear detector completamente
        recreateDetector()
    }
}

private func recreateDetector() {
    detector = nil
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
        self.setupConfiguration()
        self.setupFaceDetector()
    }
}

Problemas de Rendimiento

Detección lenta en dispositivos antiguos

// ✅ Configuración optimizada para dispositivos lentos
var config = JAAKVisageConfiguration()
config.videoDuration = 3.0  // Reducir duración
config.enableInstructions = false  // Deshabilitar instrucciones
config.instructionDelay = 0.0  // Sin delay

// Usar configuración de captura rápida
config = JAAKVisageConfiguration.quickCaptureConfig()

Consumo excesivo de memoria

// ✅ Limpiar recursos al salir
override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    
    // Detener detección
    detector?.stopDetection()
    
    // Limpiar recursos
    previewView?.removeFromSuperview()
    previewView = nil
    detector = nil
    
    // Forzar liberación de memoria
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        // Permitir que el sistema libere memoria
    }
}

Problemas de Calidad

Detección inconsistente

// ✅ Optimizar configuración para mejor detección
var config = JAAKVisageConfiguration()
config.enableInstructions = true
config.instructionDelay = 2.0
config.instructionDuration = 4.0
config.instructionsText = [
    "Mira directamente a la cámara",
    "Asegúrate de tener buena iluminación",
    "Mantén tu rostro centrado en la pantalla"
]

// Usar estilos que ayuden al usuario
var faceTrackerStyles = JAAKFaceTrackerStyles()
faceTrackerStyles.borderColor = UIColor.systemGreen
faceTrackerStyles.borderWidth = 3.0
config.faceTrackerStyles = faceTrackerStyles

📞 ¿Necesitas Ayuda?

🆘 Información para Soporte

Cuando contactes al soporte, incluye siempre:

Información del Dispositivo

// Agregar este código para obtener información de debug
private func generateDebugInfo() -> String {
    let device = UIDevice.current
    let isSupported = JAAKVisageUtilities.isDeviceSupported()
    let cameraStatus = JAAKVisageUtilities.getCameraPermissionStatus()
    let cameras = JAAKVisageUtilities.getAvailableCameras()
    
    return """
    === INFORMACIÓN DE DEBUG ===
    Dispositivo: \(device.model)
    iOS: \(device.systemVersion)
    SDK Compatible: \(isSupported)
    Permisos Cámara: \(cameraStatus)
    Cámaras disponibles: \(cameras.count)
    Memoria disponible: \(ProcessInfo.processInfo.physicalMemory / 1024 / 1024) MB
    Estado del detector: \(detector?.currentStatus.rawValue ?? "no inicializado")