Files
ExoMy_Cuno/ExoMy_Software-master/scripts/libcamera_mjpeg_server.py
T
2026-05-25 17:26:56 +02:00

171 lines
5.1 KiB
Python

#!/usr/bin/env python3
import http.server
import json
import os
import urllib.parse
import socketserver
import subprocess
import threading
import time
BOUNDARY = b'--frame'
LATEST_JPEG = None
LATEST_LOCK = threading.Lock()
PORT = 8081
RUNTIME_CONFIG_FILE = '/tmp/exomy_camera_settings.json'
DEFAULT_CAMERA_SETTINGS = {
'width': 1296,
'height': 972,
'fps': 10,
}
def load_camera_settings():
settings = dict(DEFAULT_CAMERA_SETTINGS)
try:
with open(RUNTIME_CONFIG_FILE, 'r', encoding='utf-8') as handle:
raw_settings = json.load(handle)
except (FileNotFoundError, json.JSONDecodeError, OSError, ValueError, TypeError):
return settings
try:
width = int(raw_settings.get('width', settings['width']))
height = int(raw_settings.get('height', settings['height']))
fps = int(raw_settings.get('fps', settings['fps']))
except (TypeError, ValueError):
return settings
settings['width'] = max(320, min(2592, width))
settings['height'] = max(240, min(1944, height))
settings['fps'] = max(1, min(30, fps))
return settings
CAMERA_SETTINGS = load_camera_settings()
WIDTH = CAMERA_SETTINGS['width']
HEIGHT = CAMERA_SETTINGS['height']
FPS = CAMERA_SETTINGS['fps']
def camera_reader():
global LATEST_JPEG
cmd = [
'rpicam-vid',
'--timeout', '0',
'--nopreview',
'--width', str(WIDTH),
'--height', str(HEIGHT),
'--framerate', str(FPS),
'--codec', 'mjpeg',
'-o', '-',
]
while True:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=0)
buffer = bytearray()
try:
while True:
chunk = proc.stdout.read(4096)
if not chunk:
break
buffer.extend(chunk)
while True:
start = buffer.find(b'\xff\xd8')
if start == -1:
if len(buffer) > 1024 * 1024:
del buffer[:-2]
break
end = buffer.find(b'\xff\xd9', start + 2)
if end == -1:
if start > 0:
del buffer[:start]
break
jpeg = bytes(buffer[start:end + 2])
del buffer[:end + 2]
with LATEST_LOCK:
LATEST_JPEG = jpeg
finally:
proc.kill()
proc.wait()
time.sleep(1)
class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
class Handler(http.server.BaseHTTPRequestHandler):
def normalized_path(self):
return urllib.parse.urlsplit(self.path).path
def do_HEAD(self):
path = self.normalized_path()
if path in ('/', '/index.html'):
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.end_headers()
return
if path == '/stream.mjpg':
self.send_response(200)
self.send_header('Age', '0')
self.send_header('Cache-Control', 'no-cache, private')
self.send_header('Pragma', 'no-cache')
self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=frame')
self.end_headers()
return
self.send_error(404)
def do_GET(self):
path = self.normalized_path()
if path in ('/', '/index.html'):
body = b'<html><body><img src="/stream.mjpg" /></body></html>'
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
return
if path != '/stream.mjpg':
self.send_error(404)
return
self.send_response(200)
self.send_header('Age', '0')
self.send_header('Cache-Control', 'no-cache, private')
self.send_header('Pragma', 'no-cache')
self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=frame')
self.end_headers()
while True:
with LATEST_LOCK:
frame = LATEST_JPEG
if frame is None:
time.sleep(0.1)
continue
try:
self.wfile.write(BOUNDARY + b'\r\n')
self.wfile.write(b'Content-Type: image/jpeg\r\n')
self.wfile.write(f'Content-Length: {len(frame)}\r\n\r\n'.encode('ascii'))
self.wfile.write(frame)
self.wfile.write(b'\r\n')
self.wfile.flush()
time.sleep(1.0 / FPS)
except (BrokenPipeError, ConnectionResetError):
break
def log_message(self, fmt, *args):
return
if __name__ == '__main__':
threading.Thread(target=camera_reader, daemon=True).start()
with ReusableThreadingTCPServer(('0.0.0.0', PORT), Handler) as server:
server.daemon_threads = True
server.serve_forever()