#!/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'