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.
128 lines
4.5 KiB
Python
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()
|