"""MJPEG HTTP stream server for browser-based monitoring.""" from __future__ import annotations import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer from typing import Optional import cv2 from saqr.utils.logger import get_logger log = get_logger("Inference", "streaming") _stream_frame: Optional[bytes] = None _stream_lock = threading.Lock() class MJPEGHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/": self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(b'' b'' b'') elif self.path == "/stream": self.send_response(200) self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame") self.end_headers() while True: with _stream_lock: jpeg = _stream_frame if jpeg is None: time.sleep(0.03) continue try: self.wfile.write(b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n") except BrokenPipeError: break else: self.send_error(404) def log_message(self, format, *args): pass def start_stream_server(port: int = 8080): server = HTTPServer(("0.0.0.0", port), MJPEGHandler) t = threading.Thread(target=server.serve_forever, daemon=True) t.start() log.info(f"MJPEG stream server started on http://0.0.0.0:{port}") return server def update_stream_frame(frame): global _stream_frame _, jpeg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 70]) with _stream_lock: _stream_frame = jpeg.tobytes()