From c80183bd3174ee20ac9bc66e488aa2d3c6ec2fe8 Mon Sep 17 00:00:00 2001 From: Andrew Kettel Date: Mon, 16 Mar 2026 18:06:06 -0700 Subject: [PATCH] Switch to jpeg output --- src/app.py | 2 +- src/camera.py | 63 +++++++++++++++++++--------------------- src/templates/index.html | 4 +++ 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/app.py b/src/app.py index c922c93..6d124b7 100644 --- a/src/app.py +++ b/src/app.py @@ -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(), diff --git a/src/camera.py b/src/camera.py index 4c22fe0..57699bf 100644 --- a/src/camera.py +++ b/src/camera.py @@ -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,38 +33,47 @@ 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 - while self.running: - yield b"" - return - assert self._output is not None + """Yield JPEG frames for multipart streaming.""" while self.running: - with self._output.condition: - self._output.condition.wait() - frame = self._output.frame - yield frame + frame = self.get_frame() + if frame: + yield frame camera = Camera() diff --git a/src/templates/index.html b/src/templates/index.html index 7cbbf75..1121e31 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -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";