63 lines
2.0 KiB
Python
63 lines
2.0 KiB
Python
"""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'<html><body style="margin:0;background:#000">'
|
|
b'<img src="/stream" style="width:100%;height:auto">'
|
|
b'</body></html>')
|
|
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()
|