From 659c33837e64076bb56f935ca0573d16dc2a2c73 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 19 Feb 2024 00:07:29 +0000 Subject: [PATCH] [WIP] Using new picamera2 module for camera.pi, and moved old picamera integration to camera.pi.legacy --- platypush/plugins/camera/__init__.py | 16 +- platypush/plugins/camera/cv/__init__.py | 2 +- platypush/plugins/camera/ffmpeg/__init__.py | 2 +- .../plugins/camera/gstreamer/__init__.py | 2 +- .../plugins/camera/ir/mlx90640/__init__.py | 2 +- platypush/plugins/camera/pi/__init__.py | 316 +++++++++--------- .../plugins/camera/pi/legacy/__init__.py | 284 ++++++++++++++++ .../plugins/camera/pi/legacy/manifest.yaml | 25 ++ platypush/plugins/camera/pi/legacy/model.py | 46 +++ platypush/plugins/camera/pi/manifest.yaml | 6 +- platypush/plugins/camera/pi/model.py | 36 +- 11 files changed, 551 insertions(+), 186 deletions(-) create mode 100644 platypush/plugins/camera/pi/legacy/__init__.py create mode 100644 platypush/plugins/camera/pi/legacy/manifest.yaml create mode 100644 platypush/plugins/camera/pi/legacy/model.py diff --git a/platypush/plugins/camera/__init__.py b/platypush/plugins/camera/__init__.py index 1c724a7ab..e12f19ac9 100644 --- a/platypush/plugins/camera/__init__.py +++ b/platypush/plugins/camera/__init__.py @@ -19,7 +19,7 @@ from platypush.message.event.camera import ( CameraRecordingStoppedEvent, CameraVideoRenderedEvent, ) -from platypush.plugins import Plugin, action +from platypush.plugins import RunnablePlugin, action from platypush.plugins.camera.model.camera import CameraInfo, Camera from platypush.plugins.camera.model.exceptions import ( CameraException, @@ -45,7 +45,7 @@ __all__ = [ ] -class CameraPlugin(Plugin, ABC): +class CameraPlugin(RunnablePlugin, ABC): """ Abstract plugin to control camera devices. @@ -176,9 +176,10 @@ class CameraPlugin(Plugin, ABC): def open_device( self, - device: Optional[Union[int, str]], + device: Optional[Union[int, str]] = None, stream: bool = False, redis_queue: Optional[str] = None, + ctx: Optional[dict] = None, **params, ) -> Camera: """ @@ -211,7 +212,7 @@ class CameraPlugin(Plugin, ABC): camera = self._camera_class(info=info) camera.info.set(**params) - camera.object = self.prepare_device(camera) + camera.object = self.prepare_device(camera, **(ctx or {})) if stream and camera.info.stream_format: writer_class = StreamWriter.get_class_by_name(camera.info.stream_format) @@ -288,7 +289,7 @@ class CameraPlugin(Plugin, ABC): self.close_device(camera) @abstractmethod - def prepare_device(self, device: Camera): + def prepare_device(self, device: Camera, **_): """ Prepare a device using the plugin-specific logic - to be implemented by the derived classes. @@ -788,7 +789,7 @@ class CameraPlugin(Plugin, ABC): assert ( not camera.stream_event.is_set() and camera.info.device not in self._streams ), f'A streaming session is already running for device {camera.info.device}' - assert camera.info.device, 'No device name available' + assert camera.info.device is not None, 'No device name available' self._streams[camera.info.device] = camera camera.stream_event.set() @@ -949,5 +950,8 @@ class CameraPlugin(Plugin, ABC): return camera.info.warmup_frames / camera.info.fps return 0 + def main(self): + self.wait_stop() + # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/camera/cv/__init__.py b/platypush/plugins/camera/cv/__init__.py index a2afe403f..497c46c6d 100644 --- a/platypush/plugins/camera/cv/__init__.py +++ b/platypush/plugins/camera/cv/__init__.py @@ -43,7 +43,7 @@ class CameraCvPlugin(CameraPlugin): if video_writer == 'cv': self._video_writer_class = CvFileWriter - def prepare_device(self, device: Camera): + def prepare_device(self, device: Camera, **_): import cv2 cam = cv2.VideoCapture(device.info.device) diff --git a/platypush/plugins/camera/ffmpeg/__init__.py b/platypush/plugins/camera/ffmpeg/__init__.py index 0a9d9a85c..3f0c01fc3 100644 --- a/platypush/plugins/camera/ffmpeg/__init__.py +++ b/platypush/plugins/camera/ffmpeg/__init__.py @@ -34,7 +34,7 @@ class CameraFfmpegPlugin(CameraPlugin): super().__init__(device=device, input_format=input_format, **opts) self.camera_info.ffmpeg_args = ffmpeg_args or () # type: ignore - def prepare_device(self, device: Camera) -> subprocess.Popen: + def prepare_device(self, device: Camera, **_) -> subprocess.Popen: assert isinstance(device, FFmpegCamera) warmup_seconds = self._get_warmup_seconds(device) ffmpeg = [ diff --git a/platypush/plugins/camera/gstreamer/__init__.py b/platypush/plugins/camera/gstreamer/__init__.py index f7a206e74..f724e6cc7 100644 --- a/platypush/plugins/camera/gstreamer/__init__.py +++ b/platypush/plugins/camera/gstreamer/__init__.py @@ -22,7 +22,7 @@ class CameraGstreamerPlugin(CameraPlugin): """ super().__init__(device=device, **opts) - def prepare_device(self, camera: GStreamerCamera) -> Pipeline: + def prepare_device(self, camera: GStreamerCamera, **_) -> Pipeline: pipeline = Pipeline() src = pipeline.add_source('v4l2src', device=camera.info.device) convert = pipeline.add('videoconvert') diff --git a/platypush/plugins/camera/ir/mlx90640/__init__.py b/platypush/plugins/camera/ir/mlx90640/__init__.py index db67b2833..fdaa0705c 100644 --- a/platypush/plugins/camera/ir/mlx90640/__init__.py +++ b/platypush/plugins/camera/ir/mlx90640/__init__.py @@ -65,7 +65,7 @@ class CameraIrMlx90640Plugin(CameraPlugin): def _is_capture_running(self): return self._capture_proc is not None and self._capture_proc.poll() is None - def prepare_device(self, device: Camera): + def prepare_device(self, device: Camera, **_): if not self._is_capture_running(): self._capture_proc = subprocess.Popen( [self.rawrgb_path, '{}'.format(device.info.fps)], diff --git a/platypush/plugins/camera/pi/__init__.py b/platypush/plugins/camera/pi/__init__.py index cd5582211..1b8b261ec 100644 --- a/platypush/plugins/camera/pi/__init__.py +++ b/platypush/plugins/camera/pi/__init__.py @@ -1,208 +1,222 @@ -import threading +import os import time -from typing import Optional, List, Tuple, Union +from typing import Optional, Union from platypush.plugins import action from platypush.plugins.camera import CameraPlugin, Camera -from platypush.plugins.camera.pi.model import PiCameraInfo, PiCamera + +from .model import PiCameraInfo, PiCamera class CameraPiPlugin(CameraPlugin): """ - Plugin to control a Pi camera. + Plugin to interact with a `Pi Camera + `_. - .. warning:: - This plugin is **DEPRECATED**, as it relies on the old ``picamera`` module. - On recent systems, it should be possible to access the Pi Camera through - the ffmpeg or gstreamer integrations. + This integration is intended to work with the `picamera2 + `_ module. + If you are running a very old OS that only provides the deprecated + `picamera `_ module, or you rely on + features that are currently only supported by the old module, you should + use :class:`platypush.plugins.camera.pi_legacy.CameraPiLegacyPlugin` + instead. """ _camera_class = PiCamera _camera_info_class = PiCameraInfo + _awb_modes = [ + "Auto", + "Incandescent", + "Tungsten", + "Fluorescent", + "Indoor", + "Daylight", + "Cloudy", + ] + def __init__( self, device: int = 0, fps: float = 30.0, warmup_seconds: float = 2.0, - sharpness: int = 0, - contrast: int = 0, - brightness: int = 50, - video_stabilization: bool = False, + sharpness: float = 1.0, + contrast: float = 1.0, + brightness: float = 0.0, iso: int = 0, - exposure_compensation: int = 0, - exposure_mode: str = 'auto', - meter_mode: str = 'average', - awb_mode: str = 'auto', - image_effect: str = 'none', - led_pin: Optional[int] = None, - color_effects: Optional[Union[str, List[str]]] = None, - zoom: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0), - **camera + exposure_compensation: float = 0.0, + awb_mode: str = 'Auto', + **camera, ): """ - See https://www.raspberrypi.org/documentation/usage/camera/python/README.md - for a detailed reference about the Pi camera options. + :param device: Camera device number (default: 0). Only supported on + devices with multiple camera slots. + :param fps: Frames per second (default: 30.0). + :param warmup_seconds: Seconds to wait for the camera to warm up + before taking a photo (default: 2.0). + :param sharpness: Sharpness level, as a float between 0.0 and 16.0, + where 1.0 is the default value, and higher values are mapped to + higher sharpness levels. + :param contrast: Contrast level, as a float between 0.0 and 32.0, where + 1.0 is the default value, and higher values are mapped to higher + contrast levels. + :param brightness: Brightness level, as a float between -1.0 and 1.0. + :param video_stabilization: Enable video stabilization (default: False). + Only available on the old picamera module for now. + :param iso: ISO level (default: 0). + :param exposure_compensation: Exposure compensation level, as a float + between -8.0 and 8.0. + :param awb_mode: Auto white balance mode. Allowed values: - :param camera: Options for the base camera plugin (see :class:`platypush.plugins.camera.CameraPlugin`). + - ``Auto`` (default) + - ``Daylight`` + - ``Cloudy`` + - ``Indoor`` + - ``Fluorescent`` + + :param camera: Options for the base camera plugin (see + :class:`platypush.plugins.camera.CameraPlugin`). """ super().__init__( device=device, fps=fps, warmup_seconds=warmup_seconds, **camera ) - self.camera_info.sharpness = sharpness - self.camera_info.contrast = contrast - self.camera_info.brightness = brightness - self.camera_info.video_stabilization = video_stabilization - self.camera_info.iso = iso - self.camera_info.exposure_compensation = exposure_compensation - self.camera_info.meter_mode = meter_mode - self.camera_info.exposure_mode = exposure_mode - self.camera_info.awb_mode = awb_mode - self.camera_info.image_effect = image_effect - self.camera_info.color_effects = color_effects - self.camera_info.zoom = zoom - self.camera_info.led_pin = led_pin + self.camera_info.sharpness = sharpness # type: ignore + self.camera_info.contrast = contrast # type: ignore + self.camera_info.brightness = brightness # type: ignore + self.camera_info.iso = iso # type: ignore + self.camera_info.exposure_compensation = exposure_compensation # type: ignore + self.camera_info.awb_mode = awb_mode # type: ignore - # noinspection DuplicatedCode - def prepare_device(self, device: PiCamera): - # noinspection PyUnresolvedReferences - import picamera + def prepare_device( + self, device: Camera, start: bool = True, video: bool = False, **_ + ): + from libcamera import Transform # type: ignore + from picamera2 import Picamera2 # type: ignore - camera = picamera.PiCamera( - camera_num=device.info.device, - resolution=device.info.resolution, - framerate=device.info.fps, - led_pin=device.info.led_pin, - ) + assert isinstance(device, PiCamera), f'Invalid device type: {type(device)}' + camera = Picamera2(camera_num=device.info.device) + cfg_params = { + 'main': { + 'format': 'XBGR8888' if video else 'BGR888', + **( + {'size': tuple(map(int, device.info.resolution))} + if device.info.resolution + else {} + ), + }, + **( + { + 'transform': Transform( + # It may seem counterintuitive, but the picamera2 library's flip + # definition is the opposite of ours + hflip=device.info.vertical_flip, + vflip=device.info.horizontal_flip, + ), + } + if video + # We don't need to flip the image for individual frames, the base camera + # class methods will take care of that + else {} + ), + 'controls': { + 'Brightness': float(device.info.brightness), + 'Contrast': float(device.info.contrast), + 'Sharpness': float(device.info.sharpness), + 'AwbMode': self._awb_modes.index(device.info.awb_mode), + }, + } - camera.hflip = device.info.horizontal_flip - camera.vflip = device.info.vertical_flip - camera.sharpness = device.info.sharpness - camera.contrast = device.info.contrast - camera.brightness = device.info.brightness - camera.video_stabilization = device.info.video_stabilization - camera.iso = device.info.iso - camera.exposure_compensation = device.info.exposure_compensation - camera.exposure_mode = device.info.exposure_mode - camera.meter_mode = device.info.meter_mode - camera.awb_mode = device.info.awb_mode - camera.image_effect = device.info.image_effect - camera.color_effects = device.info.color_effects - camera.rotation = device.info.rotate or 0 - camera.zoom = device.info.zoom + cfg = ( + camera.create_video_configuration + if video + else camera.create_still_configuration + )(**cfg_params) + + camera.configure(cfg) + if start: + camera.start() + time.sleep(max(1, device.info.warmup_seconds)) return camera - def release_device(self, device: PiCamera): - # noinspection PyUnresolvedReferences - import picamera - + def release_device(self, device: Camera): if device.object: - try: - device.object.stop_recording() - except (ConnectionError, picamera.PiCameraNotRecording): - pass + device.object.stop() + device.object.close() - if device.object and not device.object.closed: - try: - device.object.close() - except (ConnectionError, picamera.PiCameraClosed): - pass - - def capture_frame(self, camera: Camera, *args, **kwargs): - import numpy as np - from PIL import Image - - shape = ( - camera.info.resolution[1] + (camera.info.resolution[1] % 16), - camera.info.resolution[0] + (camera.info.resolution[0] % 32), - 3, - ) - - frame = np.empty(shape, dtype=np.uint8) - camera.object.capture(frame, 'rgb') - return Image.fromarray(frame) - - def start_preview(self, camera: Camera): - """ - Start camera preview. - """ - camera.object.start_preview() - - def stop_preview(self, camera: Camera): - """ - Stop camera preview. - """ - try: - camera.object.stop_preview() - except Exception as e: - self.logger.warning(str(e)) + def capture_frame(self, device: Camera, *_, **__): + assert device.object, 'Camera not open' + return device.object.capture_image('main') @action - def capture_preview( - self, duration: Optional[float] = None, n_frames: Optional[int] = None, **camera - ) -> dict: - camera = self.open_device(**camera) - self.start_preview(camera) + def capture_video( + self, + device: Optional[int] = None, + duration: Optional[float] = None, + video_file: Optional[str] = None, + preview: bool = False, + **camera, + ) -> Optional[Union[str, dict]]: + """ + Capture a video. - if n_frames: - duration = n_frames * (camera.info.fps or 0) - if duration: - threading.Timer(duration, lambda: self.stop_preview(camera)) + :param device: 0-based index of the camera to capture from, if the + device supports multiple cameras. Default: use the configured + camera index or the first available camera. + :param duration: Record duration in seconds (default: None, record + until :meth:`.stop_capture``). + :param video_file: If set, the stream will be recorded to the specified + video file (default: None). + :param camera: Camera parameters override - see constructors parameters. + :param preview: Show a preview of the camera frames. + :return: If duration is specified, the method will wait until the + recording is done and return the local path to the recorded + resource. Otherwise, it will return the status of the camera device + after starting it. + """ + from picamera2 import Picamera2 # type: ignore + from picamera2.encoders import H264Encoder # type: ignore - return self.status() - - def streaming_thread( - self, camera: PiCamera, stream_format: str, duration: Optional[float] = None - ): - server_socket = self._prepare_server_socket(camera) - sock = None - streaming_started_time = time.time() - self.logger.info( - 'Starting streaming on port {}'.format(camera.info.listen_port) + assert video_file, 'Video file is required' + camera = self.open_device( + device=device, ctx={'start': False, 'video': True}, **camera ) - try: - while camera.stream_event.is_set(): - if duration and time.time() - streaming_started_time >= duration: - break + encoder = H264Encoder() + assert camera.object, 'Camera not open' + assert isinstance( + camera.object, Picamera2 + ), f'Invalid camera object type: {type(camera.object)}' - sock = self._accept_client(server_socket) - if not sock: - continue + if preview: + camera.object.start_preview() - if camera.object is None or camera.object.closed: - camera = self.open_device(**camera.info.to_dict()) + # Only H264 is supported for now + camera.object.start_recording(encoder, os.path.expanduser(video_file)) - try: - camera.object.start_recording(sock, format=stream_format) - while camera.stream_event.is_set(): - camera.object.wait_recording(1) - except ConnectionError: - self.logger.info('Client closed connection') - finally: - if sock: - try: - sock.close() - except Exception as e: - self.logger.warning( - 'Error while closing client socket: {}'.format(str(e)) - ) + if duration: + self.wait_stop(duration) + try: + if preview: + camera.object.stop_preview() + finally: + if camera.object: + camera.object.stop_recording() + camera.object.close() - self.close_device(camera) - finally: - self._cleanup_stream(camera, server_socket, sock) - self.logger.info('Stopped camera stream') + return video_file + + return self.status(camera.info.device).output @action def start_streaming( self, duration: Optional[float] = None, stream_format: str = 'h264', **camera ) -> dict: camera = self.open_device(stream_format=stream_format, **camera) - return self._start_streaming(camera, duration, stream_format) + return self._start_streaming(camera, duration, stream_format) # type: ignore # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/camera/pi/legacy/__init__.py b/platypush/plugins/camera/pi/legacy/__init__.py new file mode 100644 index 000000000..8054bae27 --- /dev/null +++ b/platypush/plugins/camera/pi/legacy/__init__.py @@ -0,0 +1,284 @@ +import threading +import time + +from typing import Optional, List, Tuple, Union + +from platypush.plugins import action +from platypush.plugins.camera import CameraPlugin, Camera + +from .model import PiCameraInfo, PiCamera + + +class CameraPiLegacyPlugin(CameraPlugin): + """ + Plugin to control a Pi camera over the legacy ``picamera`` module. + + .. warning:: + This plugin is **DEPRECATED**, as it relies on the old ``picamera`` module. + Recent operating systems should probably use the + :class:`platypush.plugins.camera.pi.CameraPiPlugin` plugin instead, or + the generic v4l2 driver through + :class:`platypush.plugins.camera.ffmpeg.FfmpegCameraPlugin` or + :class:`platypush.plugins.camera.gstreamer.GStreamerCameraPlugin`. + + """ + + _camera_class = PiCamera + _camera_info_class = PiCameraInfo + + def __init__( + self, + device: int = 0, + fps: float = 30.0, + warmup_seconds: float = 2.0, + sharpness: int = 0, + contrast: int = 0, + brightness: int = 50, + video_stabilization: bool = False, + iso: int = 0, + exposure_compensation: int = 0, + exposure_mode: str = 'auto', + meter_mode: str = 'average', + awb_mode: str = 'auto', + image_effect: str = 'none', + led_pin: Optional[int] = None, + color_effects: Optional[Union[str, List[str]]] = None, + zoom: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0), + **camera + ): + """ + :param device: Camera device number (default: 0). Only supported on + devices with multiple camera slots. + :param fps: Frames per second (default: 30.0). + :param warmup_seconds: Seconds to wait for the camera to warm up + before taking a photo (default: 2.0). + :param sharpness: Sharpness level, as an integer between -100 and 100. + :param contrast: Contrast level, as an integer between -100 and 100. + :param brightness: Brightness level, as an integer between 0 and 100. + :param video_stabilization: Enable video stabilization (default: False). + :param iso: ISO level (default: 0). + :param exposure_compensation: Exposure compensation level, as an + integer between -25 and 25. + :param exposure_mode: Exposure mode. Allowed values: + + - ``off`` + - ``auto`` (default) + - ``night`` + - ``nightpreview`` + - ``backlight`` + - ``spotlight`` + - ``sports`` + - ``snow`` + - ``beach`` + - ``verylong`` + - ``fixedfps`` + - ``antishake`` + - ``fireworks`` + + :param meter_mode: Metering mode used for the exposure. Allowed values: + + - ``average`` (default) + - ``spot`` + - ``backlit`` + - ``matrix`` + + :param awb_mode: Auto white balance mode. Allowed values: + + - ``off`` + - ``auto`` (default) + - ``sunlight`` + - ``cloudy`` + - ``shade`` + - ``tungsten`` + - ``fluorescent`` + - ``incandescent`` + - ``flash`` + - ``horizon`` + + :param image_effect: Image effect applied to the camera. Allowed values: + + - ``none`` (default) + - ``negative`` + - ``solarize`` + - ``sketch`` + - ``denoise`` + - ``emboss`` + - ``oilpaint`` + - ``hatch`` + - ``gpen`` + - ``pastel`` + - ``watercolor`` + - ``film`` + - ``blur`` + - ``saturation`` + - ``colorswap`` + - ``washedout`` + - ``posterise`` + - ``colorpoint`` + - ``colorbalance`` + - ``cartoon`` + - ``deinterlace1`` + - ``deinterlace2`` + + :param led_pin: LED PIN number, if the camera LED is wired to a GPIO + PIN and you want to control it. + :param zoom: Camera zoom, in the format ``(x, y, width, height)`` + (default: ``(0.0, 0.0, 1.0, 1.0)``). + :param camera: Options for the base camera plugin (see + :class:`platypush.plugins.camera.CameraPlugin`). + """ + super().__init__( + device=device, fps=fps, warmup_seconds=warmup_seconds, **camera + ) + + self.camera_info.sharpness = sharpness # type: ignore + self.camera_info.contrast = contrast # type: ignore + self.camera_info.brightness = brightness # type: ignore + self.camera_info.video_stabilization = video_stabilization # type: ignore + self.camera_info.iso = iso # type: ignore + self.camera_info.exposure_compensation = exposure_compensation # type: ignore + self.camera_info.meter_mode = meter_mode # type: ignore + self.camera_info.exposure_mode = exposure_mode # type: ignore + self.camera_info.awb_mode = awb_mode # type: ignore + self.camera_info.image_effect = image_effect # type: ignore + self.camera_info.color_effects = color_effects # type: ignore + self.camera_info.zoom = zoom # type: ignore + self.camera_info.led_pin = led_pin # type: ignore + + def prepare_device(self, device: PiCamera): + import picamera + + camera = picamera.PiCamera( + camera_num=device.info.device, + resolution=device.info.resolution, + framerate=device.info.fps, + led_pin=device.info.led_pin, + ) + + camera.hflip = device.info.horizontal_flip + camera.vflip = device.info.vertical_flip + camera.sharpness = device.info.sharpness + camera.contrast = device.info.contrast + camera.brightness = device.info.brightness + camera.video_stabilization = device.info.video_stabilization + camera.iso = device.info.iso + camera.exposure_compensation = device.info.exposure_compensation + camera.exposure_mode = device.info.exposure_mode + camera.meter_mode = device.info.meter_mode + camera.awb_mode = device.info.awb_mode + camera.image_effect = device.info.image_effect + camera.color_effects = device.info.color_effects + camera.rotation = device.info.rotate or 0 + camera.zoom = device.info.zoom + + return camera + + def release_device(self, device: PiCamera): + import picamera + + if device.object: + try: + device.object.stop_recording() + except (ConnectionError, picamera.PiCameraNotRecording): + pass + + if device.object and not device.object.closed: + try: + device.object.close() + except (ConnectionError, picamera.PiCameraClosed): + pass + + def capture_frame(self, camera: Camera, *args, **kwargs): + import numpy as np + from PIL import Image + + shape = ( + camera.info.resolution[1] + (camera.info.resolution[1] % 16), + camera.info.resolution[0] + (camera.info.resolution[0] % 32), + 3, + ) + + frame = np.empty(shape, dtype=np.uint8) + camera.object.capture(frame, 'rgb') + return Image.fromarray(frame) + + def start_preview(self, camera: Camera): + """ + Start camera preview. + """ + camera.object.start_preview() + + def stop_preview(self, camera: Camera): + """ + Stop camera preview. + """ + try: + camera.object.stop_preview() + except Exception as e: + self.logger.warning(str(e)) + + @action + def capture_preview( + self, duration: Optional[float] = None, n_frames: Optional[int] = None, **camera + ) -> dict: + camera = self.open_device(**camera) + self.start_preview(camera) + + if n_frames: + duration = n_frames * (camera.info.fps or 0) + if duration: + threading.Timer(duration, lambda: self.stop_preview(camera)) + + return self.status() + + def streaming_thread( + self, camera: PiCamera, stream_format: str, duration: Optional[float] = None + ): + server_socket = self._prepare_server_socket(camera) + sock = None + streaming_started_time = time.time() + self.logger.info( + 'Starting streaming on port {}'.format(camera.info.listen_port) + ) + + try: + while camera.stream_event.is_set(): + if duration and time.time() - streaming_started_time >= duration: + break + + sock = self._accept_client(server_socket) + if not sock: + continue + + if camera.object is None or camera.object.closed: + camera = self.open_device(**camera.info.to_dict()) + + try: + camera.object.start_recording(sock, format=stream_format) + while camera.stream_event.is_set(): + camera.object.wait_recording(1) + except ConnectionError: + self.logger.info('Client closed connection') + finally: + if sock: + try: + sock.close() + except Exception as e: + self.logger.warning( + 'Error while closing client socket: {}'.format(str(e)) + ) + + self.close_device(camera) + finally: + self._cleanup_stream(camera, server_socket, sock) + self.logger.info('Stopped camera stream') + + @action + def start_streaming( + self, duration: Optional[float] = None, stream_format: str = 'h264', **camera + ) -> dict: + camera = self.open_device(stream_format=stream_format, **camera) + return self._start_streaming(camera, duration, stream_format) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/camera/pi/legacy/manifest.yaml b/platypush/plugins/camera/pi/legacy/manifest.yaml new file mode 100644 index 000000000..893708084 --- /dev/null +++ b/platypush/plugins/camera/pi/legacy/manifest.yaml @@ -0,0 +1,25 @@ +manifest: + events: {} + install: + apk: + - ffmpeg + - py3-numpy + - py3-pillow + apt: + - ffmpeg + - python3-numpy + - python3-pillow + dnf: + - ffmpeg + - python-numpy + - python-pillow + pacman: + - ffmpeg + - python-numpy + - python-pillow + pip: + - picamera + - numpy + - Pillow + package: platypush.plugins.camera.pi + type: plugin diff --git a/platypush/plugins/camera/pi/legacy/model.py b/platypush/plugins/camera/pi/legacy/model.py new file mode 100644 index 000000000..9d2e98961 --- /dev/null +++ b/platypush/plugins/camera/pi/legacy/model.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass +from typing import Optional, Union, List, Tuple + +from platypush.plugins.camera import CameraInfo, Camera + + +@dataclass +class PiCameraInfo(CameraInfo): + sharpness: int = 0 + contrast: int = 0 + brightness: int = 50 + video_stabilization: bool = False + iso: int = 0 + exposure_compensation: int = 0 + exposure_mode: str = 'auto' + meter_mode: str = 'average' + awb_mode: str = 'auto' + image_effect: str = 'none' + color_effects: Optional[Union[str, List[str]]] = None + zoom: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0) + led_pin: Optional[int] = None + + def to_dict(self) -> dict: + return { + 'sharpness': self.sharpness, + 'contrast': self.contrast, + 'brightness': self.brightness, + 'video_stabilization': self.video_stabilization, + 'iso': self.iso, + 'exposure_compensation': self.exposure_compensation, + 'exposure_mode': self.exposure_mode, + 'meter_mode': self.meter_mode, + 'awb_mode': self.awb_mode, + 'image_effect': self.image_effect, + 'color_effects': self.color_effects, + 'zoom': self.zoom, + 'led_pin': self.led_pin, + **super().to_dict() + } + + +class PiCamera(Camera): + info: PiCameraInfo + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/camera/pi/manifest.yaml b/platypush/plugins/camera/pi/manifest.yaml index 6675b9500..37b2753a6 100644 --- a/platypush/plugins/camera/pi/manifest.yaml +++ b/platypush/plugins/camera/pi/manifest.yaml @@ -2,19 +2,23 @@ manifest: events: {} install: apk: + - ffmpeg - py3-numpy - py3-pillow apt: + - ffmpeg - python3-numpy - python3-pillow dnf: + - ffmpeg - python-numpy - python-pillow pacman: + - ffmpeg - python-numpy - python-pillow pip: - - picamera + - picamera2 - numpy - Pillow package: platypush.plugins.camera.pi diff --git a/platypush/plugins/camera/pi/model.py b/platypush/plugins/camera/pi/model.py index 9d2e98961..5d90c111e 100644 --- a/platypush/plugins/camera/pi/model.py +++ b/platypush/plugins/camera/pi/model.py @@ -1,46 +1,34 @@ -from dataclasses import dataclass -from typing import Optional, Union, List, Tuple +from dataclasses import asdict, dataclass from platypush.plugins.camera import CameraInfo, Camera @dataclass class PiCameraInfo(CameraInfo): + """ + PiCamera info dataclass. + """ + sharpness: int = 0 contrast: int = 0 brightness: int = 50 video_stabilization: bool = False iso: int = 0 exposure_compensation: int = 0 - exposure_mode: str = 'auto' + hdr_mode: str = 'auto' meter_mode: str = 'average' awb_mode: str = 'auto' - image_effect: str = 'none' - color_effects: Optional[Union[str, List[str]]] = None - zoom: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0) - led_pin: Optional[int] = None def to_dict(self) -> dict: - return { - 'sharpness': self.sharpness, - 'contrast': self.contrast, - 'brightness': self.brightness, - 'video_stabilization': self.video_stabilization, - 'iso': self.iso, - 'exposure_compensation': self.exposure_compensation, - 'exposure_mode': self.exposure_mode, - 'meter_mode': self.meter_mode, - 'awb_mode': self.awb_mode, - 'image_effect': self.image_effect, - 'color_effects': self.color_effects, - 'zoom': self.zoom, - 'led_pin': self.led_pin, - **super().to_dict() - } + return asdict(self) class PiCamera(Camera): - info: PiCameraInfo + """ + PiCamera model. + """ + + info: PiCameraInfo # type: ignore # vim:sw=4:ts=4:et: