diff --git a/backend/camera.py b/backend/camera.py index 7b9f158..ba729c8 100644 --- a/backend/camera.py +++ b/backend/camera.py @@ -146,24 +146,22 @@ class Camera: else: return self._capture_simulation(chemin_dest) - def _declencher_af_liveview(self): - """Déclenche l'autofocus LiveView Canon avant la capture (best-effort).""" - try: - cfg = self.camera.get_config() - af = cfg.get_child_by_name("autofocusdrive") - af.set_value(1) - self.camera.set_config(cfg) - time.sleep(0.4) # Attendre que l'AF se verrouille - except Exception: - pass # Non supporté sur ce modèle ou en mode MF - def _capture_gphoto2(self, chemin_dest: Path) -> Path | None: from PIL import Image as PILImage for attempt in range(3): try: with self._gp_lock: - self._declencher_af_liveview() + # Désactiver LiveView : miroir abaissé → AF phase-détection Canon disponible + try: + cfg = self.camera.get_config() + vf = cfg.get_child_by_name("viewfinder") + vf.set_value(0) + self.camera.set_config(cfg) + time.sleep(0.2) # Laisser le miroir descendre + except Exception: + pass + log.info(f"Déclenchement capture DSLR (tentative {attempt+1}/3)") chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE) fichier_camera = gp.CameraFile() self.camera.file_get( diff --git a/backend/main.py b/backend/main.py index b98c6a3..bdcd38a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -259,19 +259,30 @@ async def api_config_update(modifications: dict): @app.post("/api/capturer") async def api_capturer(): - global capture_en_cours + global capture_en_cours, _preview_actif # Verifier le compteur etat = compteur_restant() if etat["actif"] and etat["restantes"] <= 0: return JSONResponse({"erreur": "Limite de photos atteinte"}, status_code=403) capture_en_cours = True - _preview_actif = False # Eviter faux camera_erreur si race condition HTTP avant preview_stop + _preview_actif = False # Eviter faux camera_erreur (race condition HTTP avant preview_stop) + log.info("Capture déclenchée — preview figé, thread en attente de lock") try: loop = asyncio.get_event_loop() - chemin = await loop.run_in_executor(None, camera.capturer) + try: + chemin = await asyncio.wait_for( + loop.run_in_executor(None, camera.capturer), + timeout=20.0 + ) + except asyncio.TimeoutError: + log.error("TIMEOUT capture DSLR après 20s — déconnexion forcée") + camera.deconnecter() + await diffuser_ws({"type": "camera_erreur", "message": "Timeout capture"}) + return JSONResponse({"erreur": "Timeout capture — vérifiez le DSLR"}, status_code=500) finally: capture_en_cours = False + log.info(f"Capture terminée : {chemin}") if chemin is None: return JSONResponse({"erreur": "Echec capture"}, status_code=500) nom = chemin.name diff --git a/frontend/css/style.css b/frontend/css/style.css index 86baad9..d5ec3ab 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -402,6 +402,33 @@ html, body { 100% { opacity: 0; } } +.capture-en-cours { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.5rem; + background: rgba(0,0,0,0.6); + z-index: 15; + font-size: 1.4rem; + color: #fff; +} + +.capture-spinner { + width: 60px; + height: 60px; + border: 6px solid rgba(255,255,255,0.3); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + .capture-info { position: absolute; bottom: 2rem; diff --git a/frontend/index.html b/frontend/index.html index 864f3cd..8548f07 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -97,6 +97,10 @@ 3
+
+
+ Capture en cours... +
diff --git a/frontend/js/camera.js b/frontend/js/camera.js index 19f53ae..be2d19c 100644 --- a/frontend/js/camera.js +++ b/frontend/js/camera.js @@ -11,6 +11,7 @@ const EMOJI_CAR = { 3: '🤪', 2: '😱', 1: '🔥' }; function afficherErreurCapture(titre, detail = '') { arreterPreview(); + document.getElementById('capture-en-cours')?.classList.add('cache'); const el = document.getElementById('preview-erreur-camera'); if (!el) { allerA('accueil'); return; } el.querySelector('p').textContent = titre; @@ -129,9 +130,11 @@ async function lancerCapture() { if (captureAbortee) return; afficherFlash(); + document.getElementById('capture-en-cours').classList.remove('cache'); const promesseCapture = apiPost('/api/capturer').catch(() => null); let resultat = await promesseCapture; + document.getElementById('capture-en-cours').classList.add('cache'); let erreurReseau = false; if (!resultat) {