Switch to jpeg output
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user