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 json
import re
import time
import os
import threading
from flask import Flask, Response, render_template, request, jsonify
app = Flask(__name__)
@ -21,10 +21,29 @@ STREAM_FPS = 20
# ---------------------------------------------------------------------------
# Kamera-Stream
# Kamera-Broadcaster ein Prozess, N Clients
# ---------------------------------------------------------------------------
def generate_frames():
class CameraBroadcaster:
"""
Startet rpicam-vid einmal in einem Hintergrund-Thread.
Alle Clients lesen denselben aktuellen Frame keine parallelen Kameraprozesse.
"""
def __init__(self):
self._frame: bytes | None = None
self._lock = threading.Lock()
self._thread: threading.Thread | None = None
self._running = False
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",
@ -35,33 +54,52 @@ def generate_frames():
"-o", "-",
"--nopreview",
]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
boundary = b"--frame\r\nContent-Type: image/jpeg\r\n\r\n"
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
)
buf = b""
try:
while True:
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:]
yield boundary + frame + b"\r\n"
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:
frame = self.get_frame()
if frame and frame is not last_frame:
last_frame = frame
yield boundary + frame + b"\r\n"
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)