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ón | Qué harás | Tiempo |
---|---|---|
Paso 1 | Configurar proyecto y dependencias | 15 min |
Paso 2 | Implementación básica del SDK | 25 min |
Paso 3 | Configurar permisos de cámara | 10 min |
Paso 4 | Manejo de grabación y archivos | 20 min |
Paso 5 | Implementación avanzada (opcional) | 30 min |
Paso 6 | Probar detección facial | 15 min |
PASO 1: Configurar Proyecto y Dependencias
🎯 Objetivo
Instalar el JAAKVisage SDK y configurar el entorno de desarrollo.
✅ Requisitos Técnicos
Requisito | Versión | ¿Obligatorio? | Notas |
---|---|---|---|
iOS | 12.0+ | Sí | Versión mínima soportada |
Swift | 5.0+ | Sí | Lenguaje de programación base |
Xcode | 12.0+ | Sí | Entorno de desarrollo |
MediaPipe Tasks Vision | ~0.10.3 | Sí | Motor de detección facial AI (incluido) |
AVFoundation | Sistema | Sí | Para captura de cámara |
Cámara | Física | Sí | Dispositivo 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)
- En Xcode, selecciona tu target
- Ve a la pestaña "Info"
- En "Custom iOS Target Properties", haz clic en "+"
- Busca y selecciona "Privacy - Camera Usage Description"
- 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
Elemento | ✅ | Descripción |
---|---|---|
Compatibilidad del dispositivo | ☐ | Dispositivo soporta MediaPipe |
Permisos de cámara | ☐ | App solicita y obtiene permisos |
Detección facial | ☐ | IA detecta rostros automáticamente |
Grabación automática | ☐ | Inicia grabación al detectar rostro |
Archivos generados | ☐ | Videos se guardan correctamente |
Manejo de errores | ☐ | Errores se manejan correctamente |
Estados del SDK | ☐ | Transiciones de estado funcionan |
Configuración avanzada | ☐ | Pará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
- Instalar aplicación en dispositivo físico
- Abrir aplicación por primera vez
- Verificar solicitud de permisos automática
- Conceder permisos de cámara
- Confirmar que la cámara inicia correctamente
Paso C: Probar Detección Básica
- Presionar botón "Iniciar Detección"
- Posicionar rostro frente a la cámara
- Esperar detección automática (overlay verde)
- Verificar inicio de grabación automática
- 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
Problema | Causa Probable | Solució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étodo | Descripción | Ejemplo |
---|---|---|
JAAKVisageSDK(configuration:) | Constructor del SDK | JAAKVisageSDK(configuration: config) |
startDetection() | Inicia detección facial | try detector.startDetection() |
stopDetection() | Detiene detección | detector.stopDetection() |
restartDetector() | Reinicia el detector | try detector.restartDetector() |
createPreviewView() | Crea vista de preview | let view = detector.createPreviewView() |
recordVideo(completion:) | Grabación manual | detector.recordVideo { result in } |
updateConfiguration(_:) | Actualiza configuración | detector.updateConfiguration(config) |
⚙️ Propiedades de Configuración
Propiedad | Descripción | Tipo | Valor por Defecto |
---|---|---|---|
videoDuration | Duración de grabación | TimeInterval | 5.0 |
cameraPosition | Posición de cámara | AVCaptureDevice.Position | .front |
disableFaceDetection | Deshabilitar detección | Bool | false |
enableInstructions | Mostrar instrucciones | Bool | false |
instructionDelay | Delay de instrucciones | TimeInterval | 5.0 |
instructionDuration | Duración de instrucciones | TimeInterval | 2.0 |
instructionsText | Textos personalizados | [String] | [] |
width | Ancho de vista | CGFloat | 0 (auto) |
height | Alto de vista | CGFloat | 0 (auto) |
timerStyles | Estilos del timer | JAAKTimerStyles | default |
faceTrackerStyles | Estilos del tracker | JAAKFaceTrackerStyles | default |
📡 Estados del SDK
JAAKVisageStatus
notLoaded
- Modelos no cargadosloading
- Cargando modelos MLloaded
- Listo para detecciónrunning
- Detectando rostrofaceDetected
- Rostro detectadocountdown
- Cuenta regresivarecording
- Grabando videoprocessingVideo
- Procesando videocaptureComplete
- Captura completavideoReady
- Video listofinished
- Proceso finalizadostopped
- Detección detenidaerror
- Error en proceso
📊 Estructura de Delegados
JAAKVisageSDKDelegate
faceDetector(_:didUpdateStatus:)
- Actualización de estadofaceDetector(_:didDetectFace:)
- Detección de rostrofaceDetector(_:didCaptureFile:)
- Archivo capturadofaceDetector(_: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ámaramodel-loading
- Error cargando modelos MLvideo-recording
- Error en grabacióndevice-not-supported
- Dispositivo no compatiblepermission-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")
Updated 5 days ago