"""MJPEG streaming handler for HTTP multipart/x-mixed-replace. Each client gets its own handler thread. Frames are pulled from the shared camera capture singleton — no queuing, just the latest frame. """ import io import threading import time import socketserver from http import server BOUNDARY = b"--myboundary" # HTML template for the web UI INDEX_HTML = ( "" "" "" "" "" "Camera Feed" "" "" "" "
Connecting...
" "" "" "" ) class MJPEGHandler(server.BaseHTTPRequestHandler): """Serve a single MJPEG stream to one HTTP client.""" server_version = "CameraWebUI/0.1" def log_message(self, format, *args): # noqa: A002 """Suppress per-request logging to reduce noise.""" pass def do_GET(self): if self.path == "/" or self.path == "": self._serve_index() elif self.path == "/stream": self._serve_stream() else: self.send_error(404) def _serve_index(self): self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(INDEX_HTML.encode("utf-8")) def _serve_stream(self): self.send_response(200) self.send_header( "Content-Type", "multipart/x-mixed-replace; boundary=%s" % BOUNDARY.decode() ) self.send_header("Cache-Control", "no-cache") self.send_header("Connection", "close") self.end_headers() from camera.capture import get_camera cam = get_camera() try: last_frame = None while True: frame = cam.frame() if frame is not None and frame is not last_frame: self._write_frame(frame) last_frame = frame else: time.sleep(0.05) except (BrokenPipeError, ConnectionResetError, OSError): pass def _write_frame(self, frame: bytes) -> None: """Write one MJPEG frame with boundary and headers.""" w = self.wfile w.write(BOUNDARY) w.write(b"\r\n") w.write(b"Content-Type: image/jpeg\r\n") w.write(b"Content-Length: %d\r\n\r\n" % len(frame)) w.write(frame) w.write(b"\r\n") w.flush() class ThreadedServer(socketserver.ThreadingMixIn, server.HTTPServer): """HTTP server that handles each request in a new thread.""" daemon_threads = True allow_reuse_address = True def run(host: str = "0.0.0.0", port: int = 8080): """Start the HTTP server and block.""" from camera.capture import get_camera cam = get_camera() try: cam.start() except FileNotFoundError: print(f"Error: {cam._device} not found — is the camera connected?") raise SystemExit(1) server = ThreadedServer((host, port), MJPEGHandler) print(f"Server running on http://{host}:{port}") try: server.serve_forever() except KeyboardInterrupt: pass finally: cam.stop() server.server_close()