diff --git a/backend/main.py b/backend/main.py index ad638ef..72178db 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,6 +2,8 @@ import asyncio import base64 import json import logging +import threading +import time from pathlib import Path from contextlib import asynccontextmanager @@ -37,7 +39,49 @@ clients_ws: list[WebSocket] = [] capture_en_cours = False _dslr_erreurs = 0 -_dernier_envoi_erreur_preview = 0.0 # timestamp du dernier camera_erreur envoye depuis le preview + +# --- Preview push : thread de fond qui capture en continu --- +_derniere_frame_preview: bytes | None = None # derniere frame JPEG capturee +_preview_actif = False # True quand au moins un client veut le preview +_preview_lock = threading.Lock() +_loop_principal: asyncio.AbstractEventLoop | None = None # loop asyncio principal + + +def _thread_preview(): + """Thread de fond : capture les frames DSLR en continu et les stocke.""" + global _derniere_frame_preview, _preview_actif + while True: + if not _preview_actif or capture_en_cours: + time.sleep(0.05) + continue + try: + donnees = camera.preview() + with _preview_lock: + _derniere_frame_preview = donnees + except Exception: + with _preview_lock: + _derniere_frame_preview = None + time.sleep(0.033) # ~30 fps max + + +async def _pusher_preview(): + """Tache asyncio : pousse la derniere frame a tous les clients WS abonnes.""" + global _preview_actif + while True: + await asyncio.sleep(0.05) # 20 fps max envoye aux clients + if not clients_ws: + _preview_actif = False + continue + with _preview_lock: + donnees = _derniere_frame_preview + if donnees is None: + # Signaler l'erreur si le DSLR est sense etre connecte + if camera.mode == "gphoto2" and _preview_actif: + await diffuser_ws({"type": "camera_erreur", "message": "DSLR ne repond pas au preview"}) + _preview_actif = False + continue + b64 = base64.b64encode(donnees).decode("ascii") + await diffuser_ws({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"}) async def surveiller_dslr(): """Verifie periodiquement si un DSLR est branche et bascule dessus.""" @@ -78,11 +122,18 @@ async def surveiller_dslr(): @asynccontextmanager async def lifespan(app: FastAPI): """Demarrage et arret de l'application.""" + global _loop_principal log.info("Demarrage du photobooth") + _loop_principal = asyncio.get_event_loop() camera.connecter() - task = asyncio.create_task(surveiller_dslr()) + # Thread de capture preview (bloquant, tourne en parallele) + t = threading.Thread(target=_thread_preview, daemon=True) + t.start() + task_dslr = asyncio.create_task(surveiller_dslr()) + task_push = asyncio.create_task(_pusher_preview()) yield - task.cancel() + task_dslr.cancel() + task_push.cancel() log.info("Arret du photobooth") camera.deconnecter() @@ -632,19 +683,11 @@ async def traiter_message_ws(msg: dict, ws: WebSocket): if type_msg == "ping": await ws.send_json({"type": "pong"}) - elif type_msg == "preview": - import time as _time - global _dernier_envoi_erreur_preview - donnees = camera.preview() - if donnees: - b64 = base64.b64encode(donnees).decode("ascii") - await ws.send_json({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"}) - # Si le DSLR est cense etre connecte mais le preview echoue, signaler l'erreur - if camera.mode == "gphoto2" and not camera.preview_dslr_ok: - now = _time.monotonic() - if now - _dernier_envoi_erreur_preview > 5.0: - _dernier_envoi_erreur_preview = now - await ws.send_json({"type": "camera_erreur", "message": "Le DSLR ne repond pas au preview"}) + elif type_msg == "preview_start": + global _preview_actif + _preview_actif = True + elif type_msg == "preview_stop": + _preview_actif = False async def diffuser_ws(message: dict): diff --git a/frontend/js/camera.js b/frontend/js/camera.js index 7639945..437b36d 100644 --- a/frontend/js/camera.js +++ b/frontend/js/camera.js @@ -1,7 +1,6 @@ /* Module capture photo - Preview live, compte a rebours, declenchement */ let previewActif = false; -let previewInterval = null; let previewTimeoutId = null; const PREVIEW_TIMEOUT_MS = 3000; @@ -45,16 +44,14 @@ function lancerPreview() { if (previewActif) return; previewActif = true; afficherErreurPreview(false); - - previewInterval = setInterval(() => { - if (previewActif) wsEnvoyer({ type: 'preview' }); - }, 100); - + wsEnvoyer({ type: 'preview_start' }); resetPreviewTimeout(); } function arreterPreview() { + if (!previewActif) return; previewActif = false; + wsEnvoyer({ type: 'preview_stop' }); if (previewInterval) { clearInterval(previewInterval); previewInterval = null;