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:
parent
6f0aa41f9e
commit
73faa66535
102
app/main.py
102
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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue