diff --git a/platypush/plugins/camera/__init__.py b/platypush/plugins/camera/__init__.py index c2d4f049..1c724a7a 100644 --- a/platypush/plugins/camera/__init__.py +++ b/platypush/plugins/camera/__init__.py @@ -4,7 +4,6 @@ import pathlib import socket import threading import time - from abc import ABC, abstractmethod from contextlib import contextmanager from dataclasses import asdict @@ -107,8 +106,8 @@ class CameraPlugin(Plugin, ABC): video/sequence is captured (default: 0). :param capture_timeout: Maximum number of seconds to wait between the programmed termination of a capture session and the moment the device is released. - :param scale_x: If set, the images will be scaled along the x axis by the specified factor - :param scale_y: If set, the images will be scaled along the y axis by the specified factor + :param scale_x: If set, the images will be scaled along the x-axis by the specified factor + :param scale_y: If set, the images will be scaled along the y-axis by the specified factor :param color_transform: Color transformation to apply to the images. :param grayscale: Whether the output should be converted to grayscale. :param rotate: If set, the images will be rotated by the specified number of degrees @@ -526,7 +525,7 @@ class CameraPlugin(Plugin, ABC): if video_file: self.fire_event(CameraVideoRenderedEvent(filename=video_file)) - def start_camera(self, camera: Camera, preview: bool = False, *args, **kwargs): + def start_camera(self, camera: Camera, *args, preview: bool = False, **kwargs): """ Start a camera capture session. @@ -548,6 +547,7 @@ class CameraPlugin(Plugin, ABC): @action def capture_video( self, + device: Optional[Union[int, str]] = None, duration: Optional[float] = None, video_file: Optional[str] = None, preview: bool = False, @@ -556,6 +556,7 @@ class CameraPlugin(Plugin, ABC): """ Capture a video. + :param device: Name/path/ID of the device to capture from (default: None, use the default device). :param duration: Record duration in seconds (default: None, record until ``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. @@ -563,7 +564,7 @@ class CameraPlugin(Plugin, ABC): :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. """ - camera = self.open_device(**camera) + camera = self.open_device(device=device, **camera) self.start_camera( camera, duration=duration, @@ -595,17 +596,24 @@ class CameraPlugin(Plugin, ABC): self.close_device(dev) @action - def capture_image(self, image_file: str, preview: bool = False, **camera) -> str: + def capture_image( + self, + image_file: str, + device: Optional[Union[int, str]] = None, + preview: bool = False, + **camera, + ) -> str: """ Capture an image. :param image_file: Path where the output image will be stored. + :param device: Name/path/ID of the device to capture from (default: None, use the default device). :param camera: Camera parameters override - see constructors parameters. :param preview: Show a preview of the camera frames. :return: The local path to the saved image. """ - with self.open(**camera) as camera: + with self.open(device=device, **camera) as camera: warmup_frames = ( camera.info.warmup_frames if camera.info.warmup_frames else 1 ) @@ -617,20 +625,23 @@ class CameraPlugin(Plugin, ABC): return image_file @action - def take_picture(self, image_file: str, **camera) -> str: + def take_picture( + self, image_file: str, device: Optional[Union[int, str]] = None, **camera + ) -> str: """ Alias for :meth:`.capture_image`. :param image_file: Path where the output image will be stored. + :param device: Name/path/ID of the device to capture from (default: None, use the default device). :param camera: Camera parameters override - see constructors parameters. - :param preview: Show a preview of the camera frames. :return: The local path to the saved image. """ - return str(self.capture_image(image_file, **camera).output) + return str(self.capture_image(image_file, device=device, **camera).output) @action def capture_sequence( self, + device: Optional[Union[int, str]] = None, duration: Optional[float] = None, n_frames: Optional[int] = None, preview: bool = False, @@ -639,6 +650,7 @@ class CameraPlugin(Plugin, ABC): """ Capture a sequence of frames from a camera and store them to a directory. + :param device: Name/path/ID of the device to capture from (default: None, use the default device). :param duration: Duration of the sequence in seconds (default: until :meth:`.stop_capture` is called). :param n_frames: Number of images to be captured (default: until :meth:`.stop_capture` is called). :param camera: Camera parameters override - see constructors parameters. ``frames_dir`` and ``fps`` in @@ -646,7 +658,7 @@ class CameraPlugin(Plugin, ABC): :param preview: Show a preview of the camera frames. :return: The directory where the image files have been stored. """ - with self.open(**camera) as camera: + with self.open(device=device, **camera) as camera: self.start_camera( camera, duration=duration, n_frames=n_frames, preview=preview ) @@ -655,17 +667,22 @@ class CameraPlugin(Plugin, ABC): @action def capture_preview( - self, duration: Optional[float] = None, n_frames: Optional[int] = None, **camera + self, + device: Optional[Union[int, str]] = None, + duration: Optional[float] = None, + n_frames: Optional[int] = None, + **camera, ) -> dict: """ Start a camera preview session. + :param device: Name/path/ID of the device to capture from (default: None, use the default device). :param duration: Preview duration (default: until :meth:`.stop_capture` is called). :param n_frames: Number of frames to display before closing (default: until :meth:`.stop_capture` is called). :param camera: Camera object properties. :return: The status of the device. """ - camera = self.open_device(frames_dir=None, **camera) + camera = self.open_device(device=device, frames_dir=None, **camera) self.start_camera(camera, duration=duration, n_frames=n_frames, preview=True) return self.status(camera.info.device) # type: ignore @@ -744,17 +761,24 @@ class CameraPlugin(Plugin, ABC): @action def start_streaming( - self, duration: Optional[float] = None, stream_format: str = 'mkv', **camera + self, + device: Optional[Union[int, str]] = None, + duration: Optional[float] = None, + stream_format: str = 'mkv', + **camera, ) -> dict: """ Expose the video stream of a camera over a TCP connection. + :param device: Name/path/ID of the device to capture from (default: None, use the default device). :param duration: Streaming thread duration (default: until :meth:`.stop_streaming` is called). :param stream_format: Format of the output stream - e.g. ``h264``, ``mjpeg``, ``mkv`` etc. (default: ``mkv``). :param camera: Camera object properties - see constructor parameters. :return: The status of the device. """ - camera = self.open_device(stream=True, stream_format=stream_format, **camera) + camera = self.open_device( + device=device, stream=True, stream_format=stream_format, **camera + ) return self._start_streaming(camera, duration, stream_format) # type: ignore def _start_streaming( @@ -900,7 +924,7 @@ class CameraPlugin(Plugin, ABC): return frame size = (int(frame.size[0] * scale_x), int(frame.size[1] * scale_y)) - return frame.resize(size, Image.ANTIALIAS) + return frame.resize(size, Image.BICUBIC) @staticmethod def encode_frame(frame, encoding: str = 'jpeg') -> bytes: diff --git a/platypush/plugins/camera/ffmpeg/__init__.py b/platypush/plugins/camera/ffmpeg/__init__.py index 14801bc8..0a9d9a85 100644 --- a/platypush/plugins/camera/ffmpeg/__init__.py +++ b/platypush/plugins/camera/ffmpeg/__init__.py @@ -1,12 +1,13 @@ import signal import subprocess -from typing import Optional, Tuple +from typing import Iterable, Optional from PIL import Image from PIL.Image import Image as ImageType from platypush.plugins.camera import CameraPlugin from platypush.plugins.camera.ffmpeg.model import FFmpegCamera, FFmpegCameraInfo +from platypush.plugins.camera.model.camera import Camera class CameraFfmpegPlugin(CameraPlugin): @@ -21,75 +22,83 @@ class CameraFfmpegPlugin(CameraPlugin): self, device: Optional[str] = '/dev/video0', input_format: str = 'v4l2', - ffmpeg_args: Tuple[str] = (), - **opts + ffmpeg_args: Iterable[str] = (), + **opts, ): """ :param device: Path to the camera device (default: ``/dev/video0``). - :param input_format: FFmpeg input format for the the camera device (default: ``v4l2``). + :param input_format: FFmpeg input format for the camera device (default: ``v4l2``). :param ffmpeg_args: Extra options to be passed to the FFmpeg executable. :param opts: Camera options - see constructor of :class:`platypush.plugins.camera.CameraPlugin`. """ super().__init__(device=device, input_format=input_format, **opts) - self.camera_info.ffmpeg_args = ffmpeg_args or () + self.camera_info.ffmpeg_args = ffmpeg_args or () # type: ignore - def prepare_device(self, camera: FFmpegCamera) -> subprocess.Popen: - warmup_seconds = self._get_warmup_seconds(camera) + def prepare_device(self, device: Camera) -> subprocess.Popen: + assert isinstance(device, FFmpegCamera) + warmup_seconds = self._get_warmup_seconds(device) ffmpeg = [ - camera.info.ffmpeg_bin, + device.info.ffmpeg_bin, '-y', '-f', - camera.info.input_format, + device.info.input_format, '-i', - camera.info.device, + device.info.device, '-s', - '{}x{}'.format(*camera.info.resolution), + *( + (f'{device.info.resolution[0]}x{device.info.resolution[1]}',) + if device.info.resolution + else () + ), '-ss', str(warmup_seconds), - *(('-r', str(camera.info.fps)) if camera.info.fps else ()), + *(('-r', str(device.info.fps)) if device.info.fps else ()), '-pix_fmt', 'rgb24', '-f', 'rawvideo', - *camera.info.ffmpeg_args, + *device.info.ffmpeg_args, '-', ] - self.logger.info('Running FFmpeg: {}'.format(' '.join(ffmpeg))) + self.logger.info('Running FFmpeg command: "%s"', ' '.join(ffmpeg)) proc = subprocess.Popen(ffmpeg, stdout=subprocess.PIPE) # Start in suspended mode proc.send_signal(signal.SIGSTOP) return proc - def start_camera( - self, camera: FFmpegCamera, preview: bool = False, *args, **kwargs - ): + def start_camera(self, camera: Camera, *args, preview: bool = False, **kwargs): + assert isinstance(camera, FFmpegCamera) super().start_camera(*args, camera=camera, preview=preview, **kwargs) if camera.object: camera.object.send_signal(signal.SIGCONT) - def release_device(self, camera: FFmpegCamera): - if camera.object: - camera.object.terminate() - if camera.object.stdout: - camera.object.stdout.close() - camera.object = None + def release_device(self, device: Camera): + assert isinstance(device, FFmpegCamera) + if device.object: + device.object.terminate() + if device.object.stdout: + device.object.stdout.close() + device.object = None # type: ignore - def wait_capture(self, camera: FFmpegCamera) -> None: + def wait_capture(self, camera: Camera): + assert isinstance(camera, FFmpegCamera) if camera.object and camera.object.poll() is None: try: camera.object.wait(timeout=camera.info.capture_timeout) except Exception as e: - self.logger.warning('Error on FFmpeg capture wait: {}'.format(str(e))) + self.logger.warning('Error on FFmpeg capture wait: %s', e) - def capture_frame( - self, camera: FFmpegCamera, *args, **kwargs - ) -> Optional[ImageType]: - raw_size = camera.info.resolution[0] * camera.info.resolution[1] * 3 - data = camera.object.stdout.read(raw_size) + def capture_frame(self, device: Camera, *_, **__) -> Optional[ImageType]: + assert isinstance(device, FFmpegCamera) + assert device.info.resolution, 'Resolution not set' + assert device.object.stdout, 'Camera not started' + + raw_size = device.info.resolution[0] * device.info.resolution[1] * 3 + data = device.object.stdout.read(raw_size) if len(data) < raw_size: return - return Image.frombytes('RGB', camera.info.resolution, data) + return Image.frombytes('RGB', device.info.resolution, data) # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/camera/model/writer/__init__.py b/platypush/plugins/camera/model/writer/__init__.py index cc7102ad..5bca53bf 100644 --- a/platypush/plugins/camera/model/writer/__init__.py +++ b/platypush/plugins/camera/model/writer/__init__.py @@ -3,7 +3,6 @@ import logging import multiprocessing import os import time - from abc import ABC, abstractmethod from typing import Optional, IO @@ -63,8 +62,12 @@ class FileVideoWriter(VideoWriter, ABC): """ def __init__(self, *args, output_file: str, **kwargs): - super().__init__(self, *args, **kwargs) - self.output_file = os.path.abspath(os.path.expanduser(output_file)) + super().__init__( + self, + *args, + output_file=os.path.abspath(os.path.expanduser(output_file)), + **kwargs, + ) class StreamWriter(VideoWriter, ABC):