67 lines
2.1 KiB
Python
67 lines
2.1 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 utils.config import load_config
|
|
from utils.logger import get_logger
|
|
|
|
log = get_logger("Inference", "streaming")
|
|
_CFG = load_config("core")["stream"]
|
|
|
|
_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: Optional[int] = None, host: Optional[str] = None):
|
|
h = host or _CFG["host"]
|
|
p = port if port is not None else _CFG["port"]
|
|
server = HTTPServer((h, p), MJPEGHandler)
|
|
t = threading.Thread(target=server.serve_forever, daemon=True)
|
|
t.start()
|
|
log.info(f"MJPEG stream server started on http://{h}:{p}")
|
|
return server
|
|
|
|
|
|
def update_stream_frame(frame):
|
|
global _stream_frame
|
|
_, jpeg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, _CFG["jpeg_quality"]])
|
|
with _stream_lock:
|
|
_stream_frame = jpeg.tobytes()
|