No blocking portals

This commit is contained in:
Julian Stirling 2025-12-14 16:08:59 +00:00
parent ca3f339cbe
commit e7f669cb56
4 changed files with 13 additions and 28 deletions

View file

@ -70,18 +70,17 @@ class OpenCVCamera(BaseCamera):
return False return False
def _capture_frames(self) -> None: def _capture_frames(self) -> None:
portal = lt.get_blocking_portal(self)
while self._capture_enabled: while self._capture_enabled:
ret, frame = self.cap.read() ret, frame = self.cap.read()
if not ret: if not ret:
LOGGER.error(f"Failed to capture frame from camera {self.camera_index}") LOGGER.error(f"Failed to capture frame from camera {self.camera_index}")
break break
jpeg = cv2.imencode(".jpg", frame)[1].tobytes() jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
self.mjpeg_stream.add_frame(jpeg, portal) self.mjpeg_stream.add_frame(jpeg)
jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[ jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[
1 1
].tobytes() ].tobytes()
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal) self.lores_mjpeg_stream.add_frame(jpeg_lores)
@lt.action @lt.action
def discard_frames(self) -> None: def discard_frames(self) -> None:

View file

@ -68,9 +68,7 @@ class MissingCalibrationError(RuntimeError):
class PicameraStreamOutput(Output): class PicameraStreamOutput(Output):
"""An Output class that sends frames to a stream.""" """An Output class that sends frames to a stream."""
def __init__( def __init__(self, stream: lt.outputs.MJPEGStream) -> None:
self, stream: lt.outputs.MJPEGStream, portal: lt.deps.BlockingPortal
) -> None:
"""Create an output that puts frames in an MJPEGStream. """Create an output that puts frames in an MJPEGStream.
We need to pass the stream object, and also the blocking portal, because We need to pass the stream object, and also the blocking portal, because
@ -80,7 +78,6 @@ class PicameraStreamOutput(Output):
""" """
Output.__init__(self) Output.__init__(self)
self.stream = stream self.stream = stream
self.portal = portal
def outputframe( def outputframe(
self, self,
@ -91,7 +88,7 @@ class PicameraStreamOutput(Output):
_audio: bool = False, _audio: bool = False,
) -> None: ) -> None:
"""Add a frame to the stream's ringbuffer.""" """Add a frame to the stream's ringbuffer."""
self.stream.add_frame(frame, self.portal) self.stream.add_frame(frame)
class SensorMode(BaseModel): class SensorMode(BaseModel):
@ -480,18 +477,12 @@ class StreamingPiCamera2(BaseCamera):
stream_name = "lores" if main_resolution[0] > 1280 else "main" stream_name = "lores" if main_resolution[0] > 1280 else "main"
picam.start_recording( picam.start_recording(
MJPEGEncoder(self.mjpeg_bitrate), MJPEGEncoder(self.mjpeg_bitrate),
PicameraStreamOutput( PicameraStreamOutput(self.mjpeg_stream),
self.mjpeg_stream,
lt.get_blocking_portal(self),
),
name=stream_name, name=stream_name,
) )
picam.start_encoder( picam.start_encoder(
MJPEGEncoder(100000000), MJPEGEncoder(100000000),
PicameraStreamOutput( PicameraStreamOutput(self.lores_mjpeg_stream),
self.lores_mjpeg_stream,
lt.get_blocking_portal(self),
),
name="lores", name="lores",
) )
except Exception as e: except Exception as e:
@ -515,9 +506,8 @@ class StreamingPiCamera2(BaseCamera):
else: else:
self.stream_active = False self.stream_active = False
if stop_web_stream: if stop_web_stream:
portal = lt.get_blocking_portal(self) self.mjpeg_stream.stop()
self.mjpeg_stream.stop(portal) self.lores_mjpeg_stream.stop()
self.lores_mjpeg_stream.stop(portal)
LOGGER.info("Stopped MJPEG stream.") LOGGER.info("Stopped MJPEG stream.")
# Adding a sleep to prevent camera getting confused by rapid commands # Adding a sleep to prevent camera getting confused by rapid commands

View file

@ -290,7 +290,6 @@ class SimulatedCamera(BaseCamera):
noise_level: float = lt.property(default=2.0) noise_level: float = lt.property(default=2.0)
def _capture_frames(self) -> None: def _capture_frames(self) -> None:
portal = lt.get_blocking_portal(self)
last_frame_t = time.time() last_frame_t = time.time()
while self._capture_enabled: while self._capture_enabled:
wait_time = last_frame_t - time.time() - self.frame_interval wait_time = last_frame_t - time.time() - self.frame_interval
@ -299,9 +298,9 @@ class SimulatedCamera(BaseCamera):
last_frame_t = time.time() last_frame_t = time.time()
try: try:
frame = self.generate_frame() frame = self.generate_frame()
self.mjpeg_stream.add_frame(_frame2bytes(frame), portal) self.mjpeg_stream.add_frame(_frame2bytes(frame))
ds_frame = frame.resize((320, 240), resample=Image.NEAREST) ds_frame = frame.resize((320, 240), resample=Image.NEAREST)
self.lores_mjpeg_stream.add_frame(_frame2bytes(ds_frame), portal) self.lores_mjpeg_stream.add_frame(_frame2bytes(ds_frame))
except Exception as e: except Exception as e:
LOGGER.exception(f"Failed to capture frame: {e}, retrying...") LOGGER.exception(f"Failed to capture frame: {e}, retrying...")

View file

@ -51,8 +51,6 @@ def test_handle_broken_frame():
camera.mjpeg_stream.grab_frame = flaky_grabber camera.mjpeg_stream.grab_frame = flaky_grabber
with camera_server(camera): with camera_server(camera):
portal = lt.get_blocking_portal(camera)
# Check that this does cause broken frames. # Check that this does cause broken frames.
# The noqa is because we don't know exactly when the error is thrown so we # The noqa is because we don't know exactly when the error is thrown so we
# can't have a single simple statement in the pytest raises. # can't have a single simple statement in the pytest raises.
@ -60,13 +58,13 @@ def test_handle_broken_frame():
OSError, match="broken data stream when reading image file" OSError, match="broken data stream when reading image file"
): ):
for _i in range(15): for _i in range(15):
jpeg = camera.grab_jpeg(portal) jpeg = camera.grab_jpeg()
np.asarray(Image.open(jpeg.open())) np.asarray(Image.open(jpeg.open()))
# Check that grab_as_array handles the broken frames and completes without # Check that grab_as_array handles the broken frames and completes without
# the same error. # the same error.
for _i in range(15): for _i in range(15):
array = camera.grab_as_array(portal) array = camera.grab_as_array()
assert isinstance(array, np.ndarray) assert isinstance(array, np.ndarray)
@ -74,8 +72,7 @@ def test_simulation_cam_calibration():
"""Test that the simulated camera can be calibrated and reports calibration correctly.""" """Test that the simulated camera can be calibrated and reports calibration correctly."""
camera = SimulatedCamera() camera = SimulatedCamera()
with camera_server(camera): with camera_server(camera):
portal = lt.get_blocking_portal(camera)
assert camera.calibration_required assert camera.calibration_required
camera.full_auto_calibrate(portal) camera.full_auto_calibrate()
assert not camera.calibration_required assert not camera.calibration_required
assert camera.background_detector_status.ready assert camera.background_detector_status.ready