""" Mini serveur HTTP sur l'agent Windows pour les captures d'ecran a la demande. Ecoute sur le port 5006 (configurable via RPA_CAPTURE_PORT). Endpoints : GET /capture -> screenshot frais en base64 (JPEG) GET /health -> {"status": "ok"} """ import threading import logging import json import base64 import io import os import time from http.server import HTTPServer, BaseHTTPRequestHandler logger = logging.getLogger(__name__) CAPTURE_PORT = int(os.environ.get("RPA_CAPTURE_PORT", "5006")) class CaptureHandler(BaseHTTPRequestHandler): """Retourne un screenshot frais a chaque requete GET /capture.""" def do_GET(self): if self.path == "/capture": self._handle_capture() elif self.path == "/health": self._send_json(200, {"status": "ok"}) else: self._send_json(404, {"error": "not found"}) def do_OPTIONS(self): """Gestion CORS preflight.""" self.send_response(200) self._cors_headers() self.send_header("Content-Length", "0") self.end_headers() # ------------------------------------------------------------------ def _handle_capture(self): """Capture l'ecran principal et le renvoie en base64 JPEG.""" t0 = time.perf_counter() try: import mss from PIL import Image with mss.mss() as sct: monitor = sct.monitors[1] # ecran principal raw = sct.grab(monitor) img = Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX") buf = io.BytesIO() img.save(buf, format="JPEG", quality=80) img_b64 = base64.b64encode(buf.getvalue()).decode() elapsed_ms = (time.perf_counter() - t0) * 1000 logger.info(f"Capture {img.width}x{img.height} en {elapsed_ms:.0f}ms") self._send_json(200, { "image": img_b64, "width": img.width, "height": img.height, "format": "jpeg", "source": "windows_live", "capture_ms": round(elapsed_ms), }) except Exception as e: logger.error(f"Erreur capture : {e}") self._send_json(500, {"error": str(e)}) # ------------------------------------------------------------------ def _send_json(self, code: int, data: dict): body = json.dumps(data).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self._cors_headers() self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _cors_headers(self): self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") def log_message(self, format, *args): """Supprime les logs HTTP par defaut (trop verbeux).""" pass class CaptureServer: """Serveur de capture d'ecran en temps reel (thread daemon).""" def __init__(self, port: int = CAPTURE_PORT): self._port = port self._server: HTTPServer | None = None self._thread: threading.Thread | None = None def start(self): """Demarre le serveur dans un thread daemon.""" try: self._server = HTTPServer(("0.0.0.0", self._port), CaptureHandler) self._thread = threading.Thread( target=self._server.serve_forever, daemon=True ) self._thread.start() logger.info(f"Capture server demarre sur le port {self._port}") print(f"[CAPTURE] Serveur de capture demarre sur le port {self._port}") except Exception as e: logger.error(f"Impossible de demarrer le capture server : {e}") print(f"[CAPTURE] ERREUR demarrage : {e}") def stop(self): """Arrete le serveur proprement.""" if self._server: self._server.shutdown() logger.info("Capture server arrete")