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
64
app/main.py
64
app/main.py
|
|
@ -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,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 = [
|
cmd = [
|
||||||
"rpicam-vid",
|
"rpicam-vid",
|
||||||
"-t", "0",
|
"-t", "0",
|
||||||
|
|
@ -35,33 +54,52 @@ def generate_frames():
|
||||||
"-o", "-",
|
"-o", "-",
|
||||||
"--nopreview",
|
"--nopreview",
|
||||||
]
|
]
|
||||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
process = subprocess.Popen(
|
||||||
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
|
||||||
boundary = b"--frame\r\nContent-Type: image/jpeg\r\n\r\n"
|
)
|
||||||
buf = b""
|
buf = b""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while self._running:
|
||||||
chunk = process.stdout.read(4096)
|
chunk = process.stdout.read(4096)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
buf += chunk
|
buf += chunk
|
||||||
|
|
||||||
start = buf.find(b"\xff\xd8")
|
start = buf.find(b"\xff\xd8")
|
||||||
end = buf.find(b"\xff\xd9")
|
end = buf.find(b"\xff\xd9")
|
||||||
|
|
||||||
if start != -1 and end != -1 and end > start:
|
if start != -1 and end != -1 and end > start:
|
||||||
frame = buf[start:end + 2]
|
frame = buf[start:end + 2]
|
||||||
buf = buf[end + 2:]
|
buf = buf[end + 2:]
|
||||||
yield boundary + frame + b"\r\n"
|
with self._lock:
|
||||||
|
self._frame = frame
|
||||||
finally:
|
finally:
|
||||||
process.kill()
|
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")
|
@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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue