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 base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
@@ -37,7 +39,49 @@ clients_ws: list[WebSocket] = []
|
|||||||
|
|
||||||
capture_en_cours = False
|
capture_en_cours = False
|
||||||
_dslr_erreurs = 0
|
_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():
|
async def surveiller_dslr():
|
||||||
"""Verifie periodiquement si un DSLR est branche et bascule dessus."""
|
"""Verifie periodiquement si un DSLR est branche et bascule dessus."""
|
||||||
@@ -78,11 +122,18 @@ async def surveiller_dslr():
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Demarrage et arret de l'application."""
|
"""Demarrage et arret de l'application."""
|
||||||
|
global _loop_principal
|
||||||
log.info("Demarrage du photobooth")
|
log.info("Demarrage du photobooth")
|
||||||
|
_loop_principal = asyncio.get_event_loop()
|
||||||
camera.connecter()
|
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
|
yield
|
||||||
task.cancel()
|
task_dslr.cancel()
|
||||||
|
task_push.cancel()
|
||||||
log.info("Arret du photobooth")
|
log.info("Arret du photobooth")
|
||||||
camera.deconnecter()
|
camera.deconnecter()
|
||||||
|
|
||||||
@@ -632,19 +683,11 @@ async def traiter_message_ws(msg: dict, ws: WebSocket):
|
|||||||
|
|
||||||
if type_msg == "ping":
|
if type_msg == "ping":
|
||||||
await ws.send_json({"type": "pong"})
|
await ws.send_json({"type": "pong"})
|
||||||
elif type_msg == "preview":
|
elif type_msg == "preview_start":
|
||||||
import time as _time
|
global _preview_actif
|
||||||
global _dernier_envoi_erreur_preview
|
_preview_actif = True
|
||||||
donnees = camera.preview()
|
elif type_msg == "preview_stop":
|
||||||
if donnees:
|
_preview_actif = False
|
||||||
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"})
|
|
||||||
|
|
||||||
|
|
||||||
async def diffuser_ws(message: dict):
|
async def diffuser_ws(message: dict):
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* Module capture photo - Preview live, compte a rebours, declenchement */
|
/* Module capture photo - Preview live, compte a rebours, declenchement */
|
||||||
|
|
||||||
let previewActif = false;
|
let previewActif = false;
|
||||||
let previewInterval = null;
|
|
||||||
let previewTimeoutId = null;
|
let previewTimeoutId = null;
|
||||||
const PREVIEW_TIMEOUT_MS = 3000;
|
const PREVIEW_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
@@ -45,16 +44,14 @@ function lancerPreview() {
|
|||||||
if (previewActif) return;
|
if (previewActif) return;
|
||||||
previewActif = true;
|
previewActif = true;
|
||||||
afficherErreurPreview(false);
|
afficherErreurPreview(false);
|
||||||
|
wsEnvoyer({ type: 'preview_start' });
|
||||||
previewInterval = setInterval(() => {
|
|
||||||
if (previewActif) wsEnvoyer({ type: 'preview' });
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
resetPreviewTimeout();
|
resetPreviewTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
function arreterPreview() {
|
function arreterPreview() {
|
||||||
|
if (!previewActif) return;
|
||||||
previewActif = false;
|
previewActif = false;
|
||||||
|
wsEnvoyer({ type: 'preview_stop' });
|
||||||
if (previewInterval) {
|
if (previewInterval) {
|
||||||
clearInterval(previewInterval);
|
clearInterval(previewInterval);
|
||||||
previewInterval = null;
|
previewInterval = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user