Preview push 30fps : thread de fond + diffusion WS sans polling
Avant : frontend poll WS toutes les 100ms -> gphoto2 bloquant -> lag
Apres : thread dedie capture en continu a 30fps, pusher asyncio
diffuse a 20fps a tous les clients, frontend dit juste
preview_start / preview_stop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user