- receive_cam.py : pipeline GStreamer UDP 5600 → MJPEG HTTP 5601 - CLAUDE.md : Pi Zero IP, ports 5600/5601, commandes de session caméra Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
3.0 KiB
Python
115 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Réception flux H.264 UDP depuis Pi Zero (port 5600) via GStreamer.
|
|
Exposition MJPEG sur port 5601 pour consommation par YOLOv8 et l'interface web.
|
|
"""
|
|
|
|
import cv2
|
|
import threading
|
|
import time
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
|
# Pipeline GStreamer : UDP RTP H.264 → frames décodées
|
|
GSTREAMER_PIPELINE = (
|
|
"udpsrc port=5600 "
|
|
"! application/x-rtp,payload=96 "
|
|
"! rtph264depay "
|
|
"! avdec_h264 "
|
|
"! videoconvert "
|
|
"! video/x-raw,format=BGR "
|
|
"! appsink drop=true max-buffers=1"
|
|
)
|
|
|
|
MJPEG_PORT = 5601
|
|
MJPEG_QUALITY = 70 # Compromis qualité/débit pour le réseau local
|
|
|
|
# Frame partagée entre le thread GStreamer et le serveur MJPEG
|
|
_frame_lock = threading.Lock()
|
|
_current_frame = None
|
|
_running = True
|
|
|
|
|
|
def capture_loop():
|
|
"""Lit les frames depuis GStreamer et met à jour _current_frame."""
|
|
global _current_frame, _running
|
|
|
|
cap = cv2.VideoCapture(GSTREAMER_PIPELINE, cv2.CAP_GSTREAMER)
|
|
if not cap.isOpened():
|
|
print("[CAM] Impossible d'ouvrir le pipeline GStreamer — Pi Zero connecté ?")
|
|
_running = False
|
|
return
|
|
|
|
print("[CAM] Pipeline GStreamer ouvert, réception flux...")
|
|
while _running:
|
|
ret, frame = cap.read()
|
|
if not ret:
|
|
print("[CAM] Flux perdu, nouvelle tentative...")
|
|
time.sleep(1)
|
|
continue
|
|
with _frame_lock:
|
|
_current_frame = frame
|
|
|
|
cap.release()
|
|
print("[CAM] Pipeline fermé.")
|
|
|
|
|
|
class MJPEGHandler(BaseHTTPRequestHandler):
|
|
def log_message(self, format, *args):
|
|
pass # Silencieux pour ne pas polluer les logs WireClaw
|
|
|
|
def do_GET(self):
|
|
if self.path != "/":
|
|
self.send_error(404)
|
|
return
|
|
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
|
self.end_headers()
|
|
|
|
while _running:
|
|
with _frame_lock:
|
|
frame = _current_frame
|
|
|
|
if frame is None:
|
|
time.sleep(0.05)
|
|
continue
|
|
|
|
_, jpeg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, MJPEG_QUALITY])
|
|
data = jpeg.tobytes()
|
|
|
|
try:
|
|
self.wfile.write(
|
|
f"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: {len(data)}\r\n\r\n".encode()
|
|
+ data
|
|
+ b"\r\n"
|
|
)
|
|
self.wfile.flush()
|
|
except (BrokenPipeError, ConnectionResetError):
|
|
break
|
|
|
|
time.sleep(1 / 30) # ~30 fps max
|
|
|
|
|
|
def main():
|
|
global _running
|
|
|
|
# Thread de capture GStreamer
|
|
t = threading.Thread(target=capture_loop, daemon=True)
|
|
t.start()
|
|
|
|
server = HTTPServer(("0.0.0.0", MJPEG_PORT), MJPEGHandler)
|
|
print(f"[CAM] MJPEG stream sur http://0.0.0.0:{MJPEG_PORT}/")
|
|
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
_running = False
|
|
server.shutdown()
|
|
print("[CAM] Arrêt.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|