Switch to jpeg output
This commit is contained in:
@@ -43,7 +43,7 @@ def camera_stop() -> tuple[Response, int]:
|
|||||||
def camera_stream() -> Response:
|
def camera_stream() -> Response:
|
||||||
def generate() -> Generator[bytes, Any, Any]:
|
def generate() -> Generator[bytes, Any, Any]:
|
||||||
for frame in camera.frames():
|
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(
|
return Response(
|
||||||
generate(),
|
generate(),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
@@ -6,8 +7,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from picamera2 import Picamera2
|
from picamera2 import Picamera2
|
||||||
from picamera2.encoders import H264Encoder
|
|
||||||
from picamera2.outputs import FileOutput
|
|
||||||
|
|
||||||
PICAMERA_AVAILABLE = True
|
PICAMERA_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -15,24 +14,13 @@ except ImportError:
|
|||||||
logger.warning("picamera2 not available — running in mock mode")
|
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:
|
class Camera:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._picam: Picamera2 | None = None
|
self._picam: Picamera2 | None = None
|
||||||
self._output: StreamOutput | None = None
|
self._thread: threading.Thread | None = None
|
||||||
self._encoder: H264Encoder | None = None
|
self._frame: bytes = b""
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._stop_event = threading.Event()
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
@@ -45,37 +33,46 @@ class Camera:
|
|||||||
|
|
||||||
self._picam = Picamera2()
|
self._picam = Picamera2()
|
||||||
config = self._picam.create_video_configuration(
|
config = self._picam.create_video_configuration(
|
||||||
main={"size": (1280, 720)},
|
main={"size": (1280, 720), "format": "RGB888"},
|
||||||
)
|
)
|
||||||
self._picam.configure(config)
|
self._picam.configure(config)
|
||||||
self._output = StreamOutput()
|
self._picam.start()
|
||||||
self._encoder = H264Encoder(bitrate=2_000_000)
|
self._stop_event.clear()
|
||||||
self._picam.start_recording(self._encoder, FileOutput(self._output))
|
self._thread = threading.Thread(target=self._capture_loop, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
self.running = True
|
self.running = True
|
||||||
logger.info("Camera started")
|
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:
|
def stop(self) -> None:
|
||||||
if not self.running:
|
if not self.running:
|
||||||
return
|
return
|
||||||
|
self._stop_event.set()
|
||||||
|
if self._thread:
|
||||||
|
self._thread.join(timeout=2)
|
||||||
if self._picam:
|
if self._picam:
|
||||||
self._picam.stop_recording()
|
self._picam.stop()
|
||||||
self._picam.close()
|
self._picam.close()
|
||||||
self._picam = None
|
self._picam = None
|
||||||
self.running = False
|
self.running = False
|
||||||
logger.info("Camera stopped")
|
logger.info("Camera stopped")
|
||||||
|
|
||||||
|
def get_frame(self) -> bytes:
|
||||||
|
with self._lock:
|
||||||
|
return self._frame
|
||||||
|
|
||||||
def frames(self) -> Iterator[bytes]:
|
def frames(self) -> Iterator[bytes]:
|
||||||
"""Yield H.264 frames for multipart streaming."""
|
"""Yield JPEG frames for multipart streaming."""
|
||||||
if not PICAMERA_AVAILABLE:
|
|
||||||
# yield a small empty frame in mock mode
|
|
||||||
while self.running:
|
while self.running:
|
||||||
yield b""
|
frame = self.get_frame()
|
||||||
return
|
if frame:
|
||||||
assert self._output is not None
|
|
||||||
while self.running:
|
|
||||||
with self._output.condition:
|
|
||||||
self._output.condition.wait()
|
|
||||||
frame = self._output.frame
|
|
||||||
yield frame
|
yield frame
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,10 @@
|
|||||||
btnStart.disabled = true;
|
btnStart.disabled = true;
|
||||||
status.textContent = "Starting…";
|
status.textContent = "Starting…";
|
||||||
await postAction("/camera/start");
|
await postAction("/camera/start");
|
||||||
|
|
||||||
|
// small delay to let camera initialise
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
streamImg.src = "/camera/stream";
|
streamImg.src = "/camera/stream";
|
||||||
streamImg.style.display = "block";
|
streamImg.style.display = "block";
|
||||||
placeholder.style.display = "none";
|
placeholder.style.display = "none";
|
||||||
|
|||||||
Reference in New Issue
Block a user