fix: reconnexion continue au flux GStreamer au lieu d'un timeout fixe
L'ancienne sonde abandonnait définitivement après 15s et restait bloquée en mode synthétique même quand le flux réel arrivait juste après (latence Mycelium variable, attente du premier keyframe H.264). Remplacé par un thread de fond (gstreamer_watcher) qui retente en continu et bascule la source active dès qu'une frame valide est lue, y compris après une perte de flux en cours de route. Démarrage MJPEG immédiat, plus d'attente bloquante.
This commit is contained in:
111
receive_cam.py
111
receive_cam.py
@ -49,6 +49,12 @@ _frame_lock = threading.Lock()
|
|||||||
_current_frame = None
|
_current_frame = None
|
||||||
_running = True
|
_running = True
|
||||||
|
|
||||||
|
# Source active partagée entre le watcher GStreamer et la boucle de capture :
|
||||||
|
# "synthetic" par défaut, bascule vers "gstreamer" dès qu'une frame réelle arrive.
|
||||||
|
_source_lock = threading.Lock()
|
||||||
|
_active_cap: cv2.VideoCapture | None = None
|
||||||
|
_active_source = "synthetic"
|
||||||
|
|
||||||
# Dernières stats Pi Zero reçues via UDP 5602 (thread-safe)
|
# Dernières stats Pi Zero reçues via UDP 5602 (thread-safe)
|
||||||
_stats_lock = threading.Lock()
|
_stats_lock = threading.Lock()
|
||||||
_pi_stats: dict | None = None
|
_pi_stats: dict | None = None
|
||||||
@ -119,15 +125,39 @@ def draw_overlay(frame: np.ndarray) -> np.ndarray:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _probe_gstreamer(result: list) -> None:
|
def gstreamer_watcher() -> None:
|
||||||
"""Thread : tente une lecture GStreamer et stocke le cap si succès."""
|
"""
|
||||||
cap = cv2.VideoCapture(GSTREAMER_PIPELINE, cv2.CAP_GSTREAMER)
|
Thread de fond permanent : tant que la source active n'est pas "gstreamer",
|
||||||
if cap.isOpened():
|
tente d'ouvrir le pipeline et de lire une première frame (le read() bloque
|
||||||
ret, _ = cap.read() # bloque jusqu'à la première frame
|
naturellement jusqu'à ce que des paquets valides arrivent, ce qui remplace
|
||||||
if ret:
|
l'ancien timeout fixe). Bascule la source active dès qu'une frame arrive,
|
||||||
result.append(cap)
|
y compris après un démarrage en mode synthétique ou une perte de flux.
|
||||||
return
|
"""
|
||||||
cap.release()
|
global _active_cap, _active_source
|
||||||
|
|
||||||
|
while _running:
|
||||||
|
with _source_lock:
|
||||||
|
already_connected = _active_source == "gstreamer"
|
||||||
|
if already_connected:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
cap = cv2.VideoCapture(GSTREAMER_PIPELINE, cv2.CAP_GSTREAMER)
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ret, _ = cap.read() # bloque jusqu'à la première frame valide
|
||||||
|
if not ret:
|
||||||
|
cap.release()
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("[CAM] Flux GStreamer UDP reçu.")
|
||||||
|
with _source_lock:
|
||||||
|
_active_cap = cap
|
||||||
|
_active_source = "gstreamer"
|
||||||
|
|
||||||
|
|
||||||
def _make_waiting_frame() -> np.ndarray:
|
def _make_waiting_frame() -> np.ndarray:
|
||||||
@ -140,50 +170,34 @@ def _make_waiting_frame() -> np.ndarray:
|
|||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
def open_capture() -> tuple[cv2.VideoCapture | None, str]:
|
|
||||||
"""
|
|
||||||
Priorité de source :
|
|
||||||
1. Flux GStreamer UDP 5600 (Pi Zero)
|
|
||||||
2. Frame synthétique (pas de hardware)
|
|
||||||
"""
|
|
||||||
print("[CAM] Sonde flux GStreamer UDP 5600 (timeout 15 s)...")
|
|
||||||
result: list = []
|
|
||||||
t = threading.Thread(target=_probe_gstreamer, args=(result,), daemon=True)
|
|
||||||
t.start()
|
|
||||||
t.join(timeout=15)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
print("[CAM] Flux GStreamer UDP reçu.")
|
|
||||||
return result[0], "gstreamer"
|
|
||||||
|
|
||||||
print("[CAM] Flux UDP indisponible — mode synthétique (attente Pi Zero)")
|
|
||||||
return None, "synthetic"
|
|
||||||
|
|
||||||
|
|
||||||
def capture_loop(model: YOLO) -> None:
|
def capture_loop(model: YOLO) -> None:
|
||||||
"""Lit les frames, passe dans YOLOv8, stocke le résultat annoté."""
|
"""
|
||||||
global _current_frame, _running
|
Lit les frames de la source active (gstreamer ou synthétique), passe dans
|
||||||
|
YOLOv8, stocke le résultat annoté. La source est mise à jour en tâche de
|
||||||
|
fond par gstreamer_watcher() : ce thread n'a pas besoin d'attendre le flux
|
||||||
|
réel au démarrage, il bascule dessus dès qu'il devient disponible.
|
||||||
|
"""
|
||||||
|
global _current_frame, _running, _active_cap, _active_source
|
||||||
|
|
||||||
cap, source = open_capture()
|
print("[CAM] Démarrage en mode synthétique — connexion GStreamer en tâche de fond.")
|
||||||
print(f"[CAM] Source active : {source}")
|
synthetic_frame = _make_waiting_frame()
|
||||||
|
|
||||||
# Frame synthétique réutilisée en boucle (pas d'inférence : frame vide)
|
|
||||||
synthetic_frame = _make_waiting_frame() if source == "synthetic" else None
|
|
||||||
|
|
||||||
while _running:
|
while _running:
|
||||||
|
with _source_lock:
|
||||||
|
cap, source = _active_cap, _active_source
|
||||||
|
|
||||||
if source == "synthetic":
|
if source == "synthetic":
|
||||||
frame = synthetic_frame
|
frame = synthetic_frame
|
||||||
time.sleep(1 / 10) # 10 fps synthétiques
|
time.sleep(1 / 10) # 10 fps synthétiques
|
||||||
else:
|
else:
|
||||||
ret, frame = cap.read()
|
ret, frame = cap.read()
|
||||||
if not ret:
|
if not ret:
|
||||||
if source == "gstreamer":
|
print("[CAM] Flux GStreamer perdu, retour en mode synthétique.")
|
||||||
print("[CAM] Flux GStreamer perdu, nouvelle tentative...")
|
cap.release()
|
||||||
time.sleep(1)
|
with _source_lock:
|
||||||
continue
|
_active_cap = None
|
||||||
print("[CAM] Webcam perdue, arrêt.")
|
_active_source = "synthetic"
|
||||||
_running = False
|
continue
|
||||||
break
|
|
||||||
|
|
||||||
# Inférence YOLOv8 — skip en mode synthétique (frame vide sans objet)
|
# Inférence YOLOv8 — skip en mode synthétique (frame vide sans objet)
|
||||||
if model is not None and source != "synthetic":
|
if model is not None and source != "synthetic":
|
||||||
@ -198,8 +212,9 @@ def capture_loop(model: YOLO) -> None:
|
|||||||
with _frame_lock:
|
with _frame_lock:
|
||||||
_current_frame = annotated
|
_current_frame = annotated
|
||||||
|
|
||||||
if cap is not None:
|
with _source_lock:
|
||||||
cap.release()
|
if _active_cap is not None:
|
||||||
|
_active_cap.release()
|
||||||
print("[CAM] Capture terminée.")
|
print("[CAM] Capture terminée.")
|
||||||
|
|
||||||
|
|
||||||
@ -261,7 +276,11 @@ def main():
|
|||||||
ts = threading.Thread(target=sideband_loop, daemon=True)
|
ts = threading.Thread(target=sideband_loop, daemon=True)
|
||||||
ts.start()
|
ts.start()
|
||||||
|
|
||||||
# Thread de capture + inférence
|
# Thread de fond : connexion/reconnexion continue au flux GStreamer réel
|
||||||
|
tg = threading.Thread(target=gstreamer_watcher, daemon=True)
|
||||||
|
tg.start()
|
||||||
|
|
||||||
|
# Thread de capture + inférence (démarre immédiatement en mode synthétique)
|
||||||
t = threading.Thread(target=capture_loop, args=(model,), daemon=True)
|
t = threading.Thread(target=capture_loop, args=(model,), daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user