Switch to jpeg output

This commit is contained in:
2026-03-16 18:06:06 -07:00
parent c681c35955
commit c80183bd31
3 changed files with 35 additions and 34 deletions

View File

@@ -43,7 +43,7 @@ def camera_stop() -> tuple[Response, int]:
def camera_stream() -> Response:
def generate() -> Generator[bytes, Any, Any]:
for frame in camera.frames():
yield (b"--frame\r\n" b"Content-Type: video/H264\r\n\r\n" + frame + b"\r\n")
yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n")
return Response(
generate(),

View File

@@ -1,3 +1,4 @@
import io
import logging
import threading
from collections.abc import Iterator
@@ -6,8 +7,6 @@ logger = logging.getLogger(__name__)
try:
from picamera2 import Picamera2
from picamera2.encoders import H264Encoder
from picamera2.outputs import FileOutput
PICAMERA_AVAILABLE = True
except ImportError:
@@ -15,24 +14,13 @@ except ImportError:
logger.warning("picamera2 not available — running in mock mode")
class StreamOutput:
"""Thread-safe buffer that holds the latest H.264 frame."""
def __init__(self) -> None:
self.frame: bytes = b""
self.condition = threading.Condition()
def write(self, data: bytes) -> None:
with self.condition:
self.frame = data
self.condition.notify_all()
class Camera:
def __init__(self) -> None:
self._picam: Picamera2 | None = None
self._output: StreamOutput | None = None
self._encoder: H264Encoder | None = None
self._thread: threading.Thread | None = None
self._frame: bytes = b""
self._lock = threading.Lock()
self._stop_event = threading.Event()
self.running = False
def start(self) -> None:
@@ -45,37 +33,46 @@ class Camera:
self._picam = Picamera2()
config = self._picam.create_video_configuration(
main={"size": (1280, 720)},
main={"size": (1280, 720), "format": "RGB888"},
)
self._picam.configure(config)
self._output = StreamOutput()
self._encoder = H264Encoder(bitrate=2_000_000)
self._picam.start_recording(self._encoder, FileOutput(self._output))
self._picam.start()
self._stop_event.clear()
self._thread = threading.Thread(target=self._capture_loop, daemon=True)
self._thread.start()
self.running = True
logger.info("Camera started")
def _capture_loop(self) -> None:
assert self._picam is not None
while not self._stop_event.is_set():
buffer = io.BytesIO()
self._picam.capture_file(buffer, format="jpeg")
with self._lock:
self._frame = buffer.getvalue()
def stop(self) -> None:
if not self.running:
return
self._stop_event.set()
if self._thread:
self._thread.join(timeout=2)
if self._picam:
self._picam.stop_recording()
self._picam.stop()
self._picam.close()
self._picam = None
self.running = False
logger.info("Camera stopped")
def get_frame(self) -> bytes:
with self._lock:
return self._frame
def frames(self) -> Iterator[bytes]:
"""Yield H.264 frames for multipart streaming."""
if not PICAMERA_AVAILABLE:
# yield a small empty frame in mock mode
"""Yield JPEG frames for multipart streaming."""
while self.running:
yield b""
return
assert self._output is not None
while self.running:
with self._output.condition:
self._output.condition.wait()
frame = self._output.frame
frame = self.get_frame()
if frame:
yield frame

View File

@@ -120,6 +120,10 @@
btnStart.disabled = true;
status.textContent = "Starting…";
await postAction("/camera/start");
// small delay to let camera initialise
await new Promise(r => setTimeout(r, 500));
streamImg.src = "/camera/stream";
streamImg.style.display = "block";
placeholder.style.display = "none";