From 73faa665350ce35d851788b0dfd83d48e8b00e58 Mon Sep 17 00:00:00 2001 From: Julian Vollmer Date: Mon, 18 May 2026 16:54:15 +0200 Subject: [PATCH] Support 3 parallel stream clients via CameraBroadcaster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorher: pro Client ein eigener rpicam-vid-Prozess → 2. und 3. Client schlagen fehl. Jetzt: CameraBroadcaster startet rpicam-vid einmal in einem Hintergrund-Thread, alle Clients lesen denselben aktuellen Frame. Flask läuft mit threaded=True. Co-Authored-By: Claude Sonnet 4.6 --- app/main.py | 102 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 32 deletions(-) diff --git a/app/main.py b/app/main.py index e19a43d..255e47f 100644 --- a/app/main.py +++ b/app/main.py @@ -7,10 +7,10 @@ Babycam Flask Webinterface """ import subprocess -import json import re import time import os +import threading from flask import Flask, Response, render_template, request, jsonify app = Flask(__name__) @@ -21,47 +21,85 @@ STREAM_FPS = 20 # --------------------------------------------------------------------------- -# Kamera-Stream +# Kamera-Broadcaster – ein Prozess, N Clients # --------------------------------------------------------------------------- -def generate_frames(): - cmd = [ - "rpicam-vid", - "-t", "0", - "--width", str(STREAM_WIDTH), - "--height", str(STREAM_HEIGHT), - "--framerate", str(STREAM_FPS), - "--codec", "mjpeg", - "-o", "-", - "--nopreview", - ] - process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) +class CameraBroadcaster: + """ + Startet rpicam-vid einmal in einem Hintergrund-Thread. + Alle Clients lesen denselben aktuellen Frame – keine parallelen Kameraprozesse. + """ - boundary = b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" - buf = b"" + def __init__(self): + self._frame: bytes | None = None + self._lock = threading.Lock() + self._thread: threading.Thread | None = None + self._running = False - try: + def start(self): + if self._thread and self._thread.is_alive(): + return + self._running = True + self._thread = threading.Thread(target=self._capture, daemon=True) + self._thread.start() + + def _capture(self): + cmd = [ + "rpicam-vid", + "-t", "0", + "--width", str(STREAM_WIDTH), + "--height", str(STREAM_HEIGHT), + "--framerate", str(STREAM_FPS), + "--codec", "mjpeg", + "-o", "-", + "--nopreview", + ] + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) + buf = b"" + try: + while self._running: + chunk = process.stdout.read(4096) + if not chunk: + break + buf += chunk + start = buf.find(b"\xff\xd8") + end = buf.find(b"\xff\xd9") + if start != -1 and end != -1 and end > start: + frame = buf[start:end + 2] + buf = buf[end + 2:] + with self._lock: + self._frame = frame + finally: + process.kill() + self._running = False + + def get_frame(self) -> bytes | None: + with self._lock: + return self._frame + + def generate(self): + """Generator für einen MJPEG-Client.""" + boundary = b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + last_frame = None while True: - chunk = process.stdout.read(4096) - if not chunk: - break - buf += chunk - - start = buf.find(b"\xff\xd8") - end = buf.find(b"\xff\xd9") - - if start != -1 and end != -1 and end > start: - frame = buf[start:end + 2] - buf = buf[end + 2:] + frame = self.get_frame() + if frame and frame is not last_frame: + last_frame = frame yield boundary + frame + b"\r\n" - finally: - process.kill() + else: + time.sleep(1 / STREAM_FPS) + + +camera = CameraBroadcaster() +camera.start() @app.route("/stream") def stream(): return Response( - generate_frames(), + camera.generate(), mimetype="multipart/x-mixed-replace; boundary=frame", ) @@ -216,4 +254,4 @@ def wifi_connect(): # --------------------------------------------------------------------------- if __name__ == "__main__": - app.run(host="0.0.0.0", port=80, debug=False) + app.run(host="0.0.0.0", port=80, debug=False, threaded=True)