"""Accès simple à la caméra d'Alfred, avec aperçu MJPEG activé à la demande. Une seule lecture par instance Alfred. La vidéo ne démarre que lorsque l'utilisateur clique sur Activer, pour éviter de monopoliser la caméra. """ import os import threading import time _CAMERA_LOCK = threading.Lock() def snapshot(path, index=0): import cv2 if not _CAMERA_LOCK.acquire(blocking=False): raise RuntimeError('Caméra déjà utilisée par la prévisualisation Alfred') try: cap = cv2.VideoCapture(index) try: ok, frame = cap.read() finally: cap.release() if not ok: raise RuntimeError('Impossible de lire la caméra locale') cv2.imwrite(path, frame) return path finally: _CAMERA_LOCK.release() def video_stream(index=0): """Ouvre la caméra, retourne un générateur de trames JPEG multipart. L'appelant doit retourner ce générateur dans une réponse Flask en streaming. L'absence de cv2 ou de webcam déclenche une exception avant HTTP 200. """ import cv2 if not _CAMERA_LOCK.acquire(blocking=False): raise RuntimeError('Prévisualisation vidéo déjà active') cap = None try: cap = cv2.VideoCapture(index) if not cap.isOpened(): cap.release() raise RuntimeError(f'Caméra {index} indisponible') cap.set(cv2.CAP_PROP_FRAME_WIDTH, int(os.getenv('CAMERA_WIDTH', '640'))) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, int(os.getenv('CAMERA_HEIGHT', '480'))) fps = min(15.0, max(1.0, float(os.getenv('CAMERA_FPS', '8')))) quality = min(95, max(35, int(os.getenv('CAMERA_JPEG_QUALITY', '75')))) except Exception: if cap is not None: cap.release() _CAMERA_LOCK.release() raise def frames(): try: while True: ok, frame = cap.read() if not ok: break ok_jpeg, encoded = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, quality]) if ok_jpeg: yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + encoded.tobytes() + b'\r\n' time.sleep(1.0 / fps) finally: cap.release() _CAMERA_LOCK.release() return frames()