import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:meta_wearables_dat/meta_wearables_dat.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:mymuseum_visitapp/PlatformChannels/audio_routing_channel.dart'; enum GlassesState { disconnected, connecting, connected, streaming } /// Gère la connexion aux lunettes Ray-Ban Meta via le SDK DAT. /// /// Cycle de vie intentionnel : /// 1. initialize() — init SDK, écoute les streams d'état (au démarrage app) /// 2. connect() — enregistrement DAT + permission caméra + HFP audio routing /// Le téléphone est connecté aux lunettes, micro HFP actif. /// PAS de stream vidéo encore. /// 3. startStream() — démarre le stream vidéo (uniquement quand caméra nécessaire) /// 4. stopStream() — arrête le stream, reste connecté /// 5. disconnect() — déconnexion complète class MetaGlassesService { MetaGlassesService._(); static final MetaGlassesService instance = MetaGlassesService._(); final ValueNotifier state = ValueNotifier(GlassesState.disconnected); /// Appelé avec le chemin de la photo après capturePhoto(). void Function(String photoPath)? onPhotoCaptured; bool get isConnected => state.value == GlassesState.connected || state.value == GlassesState.streaming; // ── 1. Initialisation SDK ───────────────────────────────────────────────── Future initialize() async { if (!Platform.isAndroid) return; try { final ok = await Wearables.instance.initialize(); if (!ok) { debugPrint('[MetaGlassesService] SDK init failed'); return; } } catch (e) { // ALREADY_INITIALIZED = hot restart, le SDK natif garde son état → OK if (e.toString().contains('ALREADY_INITIALIZED')) { debugPrint('[MetaGlassesService] Already initialized (hot restart) — continuing'); } else { debugPrint('[MetaGlassesService] Init error: $e'); return; } } Wearables.instance.registrationStateStream.listen(_onRegistrationState); Wearables.instance.streamStateStream.listen(_onStreamState); Wearables.instance.devicesStream.listen((devices) { debugPrint('[MetaGlassesService] Devices: $devices'); if (devices.isNotEmpty) _onDeviceConnected(); }); debugPrint('[MetaGlassesService] Initialized'); } // ── 2. Connexion (sans stream vidéo) ────────────────────────────────────── /// Connecte aux lunettes et active le routing audio HFP. /// Le micro des lunettes devient actif pour le wake word. /// PAS de stream vidéo — utiliser [startStream] séparément. Future connect() async { if (!Platform.isAndroid) return; state.value = GlassesState.connecting; await Permission.camera.request(); await Wearables.instance.startRegistration(); await _ensureCameraPermission(); debugPrint('[MetaGlassesService] Connected (no stream yet)'); } // ── 3. Stream vidéo (sur demande) ───────────────────────────────────────── /// Démarre le stream vidéo continu (nécessaire pour capturePhoto). /// Appeler uniquement quand la caméra est requise. Future startStream() async { if (!isConnected) return; await Wearables.instance.startStream(videoQuality: 'MEDIUM', frameRate: 24); debugPrint('[MetaGlassesService] Stream started'); } Future stopStream() async { await Wearables.instance.stopStream(); if (state.value == GlassesState.streaming) { state.value = GlassesState.connected; } debugPrint('[MetaGlassesService] Stream stopped'); } // ── 4. Déconnexion ──────────────────────────────────────────────────────── Future disconnect() async { await Wearables.instance.stopStream(); await AudioRoutingChannel.restoreDefaultOutput(); state.value = GlassesState.disconnected; } // ── Capture photo ───────────────────────────────────────────────────────── Future requestPhotoCapture() async { if (!isConnected) { debugPrint('[MetaGlassesService] Not connected — capture ignored'); onPhotoCaptured?.call(''); return; } bool streamStartedByUs = false; if (state.value != GlassesState.streaming) { await startStream(); streamStartedByUs = true; // Attend que le stream soit réellement actif (max 4s) final sw = Stopwatch()..start(); while (state.value != GlassesState.streaming && sw.elapsed.inSeconds < 8) { await Future.delayed(const Duration(milliseconds: 200)); } if (state.value != GlassesState.streaming) { debugPrint('[MetaGlassesService] Stream not ready after 8s — capture aborted'); onPhotoCaptured?.call(''); return; } // Délai de stabilisation si le stream vient juste de démarrer await Future.delayed(const Duration(milliseconds: 1500)); } try { final photo = await Wearables.instance.capturePhoto(); debugPrint('[MetaGlassesService] Photo captured: ${photo.path}'); onPhotoCaptured?.call(photo.path); } catch (e) { debugPrint('[MetaGlassesService] capturePhoto error: $e'); onPhotoCaptured?.call(''); } finally { if (streamStartedByUs) { await stopStream(); debugPrint('[MetaGlassesService] Stream stopped after photo'); } } } /// Démarre le stream et attend qu'il soit actif (max 4s). /// Retourne true si le stream est prêt. Future ensureStreaming() async { if (!isConnected) return false; if (state.value == GlassesState.streaming) return true; final alreadyStarted = state.value == GlassesState.streaming; await startStream(); final sw = Stopwatch()..start(); while (state.value != GlassesState.streaming && sw.elapsed.inSeconds < 8) { await Future.delayed(const Duration(milliseconds: 200)); } if (state.value != GlassesState.streaming) return false; // Si on vient de passer à streaming via 'started' (pas 'streaming'), attendre un peu // que le SDK soit vraiment prêt pour capturePhoto() if (!alreadyStarted) await Future.delayed(const Duration(milliseconds: 1500)); return true; } /// Capture un seul frame depuis le stream (doit être déjà actif). /// Retourne le chemin du fichier temporaire, ou null si pas de frame en 2s. Future grabFrame() async { if (state.value != GlassesState.streaming) return null; final completer = Completer(); StreamSubscription? sub; sub = Wearables.instance.videoFramesStream.listen((frame) { if (!completer.isCompleted) completer.complete(frame); sub?.cancel(); }, onError: (_) { if (!completer.isCompleted) completer.complete(null); }); Future.delayed(const Duration(seconds: 2), () { if (!completer.isCompleted) completer.complete(null); sub?.cancel(); }); final bytes = await completer.future; if (bytes == null) return null; final dir = await getTemporaryDirectory(); final file = File('${dir.path}/qr_frame_${DateTime.now().millisecondsSinceEpoch}.jpg'); await file.writeAsBytes(bytes); return file.path; } // ── Audio HFP (micro lunettes) ──────────────────────────────────────────── /// Appelé quand les lunettes sont détectées. /// On ne force PAS MODE_IN_COMMUNICATION ici — ça dégraderait le TTS (HFP 8kHz). /// Android route automatiquement le micro HFP vers SpeechRecognizer quand connecté. /// A2DP reste actif pour la sortie TTS haute qualité. void _onDeviceConnected() { debugPrint('[MetaGlassesService] Glasses connected — A2DP + HFP active (no forced routing)'); } // ── Callbacks SDK ───────────────────────────────────────────────────────── Future _ensureCameraPermission() async { try { final status = await Wearables.instance.checkCameraPermission(); debugPrint('[MetaGlassesService] Camera permission: $status'); if (status.toLowerCase() == 'granted') return; await Wearables.instance.requestCameraPermission(); } catch (e) { debugPrint('[MetaGlassesService] Camera permission error: $e'); } } void _onRegistrationState(RegistrationState s) { debugPrint('[MetaGlassesService] Registration: ${s.state} error=${s.error}'); switch (s.state.toLowerCase()) { case 'registered': case 'available': if (state.value == GlassesState.connecting) { state.value = GlassesState.connected; } case 'unregistered': case 'unavailable': state.value = GlassesState.disconnected; } } void _onStreamState(String s) { debugPrint('[MetaGlassesService] Stream state: $s'); final lower = s.toLowerCase(); // 'streaming' = flux vidéo actif, capturePhoto() fonctionne immédiatement // 'started' = stream initialisé — capturePhoto() peut fonctionner après un court délai if (lower.contains('streaming') || lower == 'started') { state.value = GlassesState.streaming; } else if (lower == 'stopped' || lower == 'closed') { if (state.value == GlassesState.streaming) { state.value = GlassesState.connected; } } } void dispose() => state.dispose(); }