Support 3 parallel stream clients via CameraBroadcaster

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 <noreply@anthropic.com>
This commit is contained in:
Julian Vollmer 2026-05-18 16:54:15 +02:00
parent 6f0aa41f9e
commit 73faa66535
1 changed files with 70 additions and 32 deletions

View File

@ -7,10 +7,10 @@ Babycam Flask Webinterface
""" """
import subprocess import subprocess
import json
import re import re
import time import time
import os import os
import threading
from flask import Flask, Response, render_template, request, jsonify from flask import Flask, Response, render_template, request, jsonify
app = Flask(__name__) app = Flask(__name__)
@ -21,47 +21,85 @@ STREAM_FPS = 20
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Kamera-Stream # Kamera-Broadcaster ein Prozess, N Clients
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def generate_frames(): class CameraBroadcaster:
cmd = [ """
"rpicam-vid", Startet rpicam-vid einmal in einem Hintergrund-Thread.
"-t", "0", Alle Clients lesen denselben aktuellen Frame keine parallelen Kameraprozesse.
"--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)
boundary = b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" def __init__(self):
buf = b"" 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: while True:
chunk = process.stdout.read(4096) frame = self.get_frame()
if not chunk: if frame and frame is not last_frame:
break last_frame = frame
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:]
yield boundary + frame + b"\r\n" yield boundary + frame + b"\r\n"
finally: else:
process.kill() time.sleep(1 / STREAM_FPS)
camera = CameraBroadcaster()
camera.start()
@app.route("/stream") @app.route("/stream")
def stream(): def stream():
return Response( return Response(
generate_frames(), camera.generate(),
mimetype="multipart/x-mixed-replace; boundary=frame", mimetype="multipart/x-mixed-replace; boundary=frame",
) )
@ -216,4 +254,4 @@ def wifi_connect():
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if __name__ == "__main__": 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)