You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MAI/hdmi_usb_bridge.py

128 lines
4.5 KiB
Python

import argparse
import json
import os
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
import cv2
CAMERA_LOCK = threading.Lock()
def clamp_query(query, name, default, minimum, maximum):
try:
value = int(query.get(name, [default])[0])
except (TypeError, ValueError):
value = default
return max(minimum, min(maximum, value))
def open_camera(index, width, height, fps, attempts=10):
backend = cv2.CAP_DSHOW if os.name == "nt" else cv2.CAP_ANY
for _ in range(max(1, attempts)):
cap = cv2.VideoCapture(index, backend)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
cap.set(cv2.CAP_PROP_FPS, fps)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
ok, frame = cap.read()
if cap.isOpened() and ok and frame is not None:
return cap, frame
cap.release()
time.sleep(0.5)
return None, None
class Handler(BaseHTTPRequestHandler):
server_version = "HDMIUSBBridge/1.0"
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == "/health":
body = json.dumps({"ok": True, "pid": os.getpid()}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
if parsed.path != "/stream.mjpg":
self.send_error(404)
return
if not CAMERA_LOCK.acquire(blocking=False):
self.send_error(409, "camera is already in use")
return
query = parse_qs(parsed.query)
index = clamp_query(query, "index", 0, 0, 16)
width = clamp_query(query, "width", 1920, 160, 3840)
height = clamp_query(query, "height", 1080, 120, 2160)
fps = clamp_query(query, "fps", 30, 1, 120)
quality = clamp_query(query, "quality", 85, 40, 95)
cap, frame = open_camera(index, width, height, fps)
try:
if cap is None:
self.send_error(503, f"camera {index} did not return a frame")
return
self.send_response(200)
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
self.send_header("Cache-Control", "no-store")
self.end_headers()
interval = 1.0 / fps
next_frame_at = time.perf_counter()
read_failures = 0
while True:
now = time.perf_counter()
if now < next_frame_at:
time.sleep(next_frame_at - now)
elif now - next_frame_at > 3.0 * interval:
next_frame_at = now
next_frame_at += interval
encoded, jpeg = cv2.imencode(
".jpg",
frame,
[cv2.IMWRITE_JPEG_QUALITY, quality],
)
if encoded:
payload = jpeg.tobytes()
self.wfile.write(b"--frame\r\n")
self.wfile.write(b"Content-Type: image/jpeg\r\n")
self.wfile.write(f"Content-Length: {len(payload)}\r\n\r\n".encode("ascii"))
self.wfile.write(payload)
self.wfile.write(b"\r\n")
ok, next_frame = cap.read()
if ok and next_frame is not None:
frame = next_frame
read_failures = 0
else:
read_failures += 1
if read_failures >= 60:
break
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
pass
finally:
if cap is not None:
cap.release()
CAMERA_LOCK.release()
def log_message(self, format_text, *args):
print(f"[hdmi-bridge] {self.address_string()} {format_text % args}", flush=True)
def main():
parser = argparse.ArgumentParser(description="Windows HDMI USB to MJPEG bridge")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8091)
args = parser.parse_args()
print(f"HDMI USB bridge listening on http://{args.host}:{args.port}", flush=True)
ThreadingHTTPServer((args.host, args.port), Handler).serve_forever()
if __name__ == "__main__":
main()