Fixed setting of `output_file` on `FfmpegWriter`.
continuous-integration/drone/push Build is passing Details

Also, fixed parameters passed to camera
writer objects.
This commit is contained in:
Fabio Manganiello 2024-02-17 00:01:47 +01:00
parent a38ef6bc7a
commit c59446fdb1
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
3 changed files with 86 additions and 50 deletions

View File

@ -4,7 +4,6 @@ import pathlib
import socket import socket
import threading import threading
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import asdict from dataclasses import asdict
@ -107,8 +106,8 @@ class CameraPlugin(Plugin, ABC):
video/sequence is captured (default: 0). video/sequence is captured (default: 0).
:param capture_timeout: Maximum number of seconds to wait between the programmed termination of a capture :param capture_timeout: Maximum number of seconds to wait between the programmed termination of a capture
session and the moment the device is released. 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_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_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 color_transform: Color transformation to apply to the images.
:param grayscale: Whether the output should be converted to grayscale. :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 :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: if video_file:
self.fire_event(CameraVideoRenderedEvent(filename=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. Start a camera capture session.
@ -548,6 +547,7 @@ class CameraPlugin(Plugin, ABC):
@action @action
def capture_video( def capture_video(
self, self,
device: Optional[Union[int, str]] = None,
duration: Optional[float] = None, duration: Optional[float] = None,
video_file: Optional[str] = None, video_file: Optional[str] = None,
preview: bool = False, preview: bool = False,
@ -556,6 +556,7 @@ class CameraPlugin(Plugin, ABC):
""" """
Capture a video. 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 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 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 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 :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. 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( self.start_camera(
camera, camera,
duration=duration, duration=duration,
@ -595,17 +596,24 @@ class CameraPlugin(Plugin, ABC):
self.close_device(dev) self.close_device(dev)
@action @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. Capture an image.
:param image_file: Path where the output image will be stored. :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 camera: Camera parameters override - see constructors parameters.
:param preview: Show a preview of the camera frames. :param preview: Show a preview of the camera frames.
:return: The local path to the saved image. :return: The local path to the saved image.
""" """
with self.open(**camera) as camera: with self.open(device=device, **camera) as camera:
warmup_frames = ( warmup_frames = (
camera.info.warmup_frames if camera.info.warmup_frames else 1 camera.info.warmup_frames if camera.info.warmup_frames else 1
) )
@ -617,20 +625,23 @@ class CameraPlugin(Plugin, ABC):
return image_file return image_file
@action @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`. Alias for :meth:`.capture_image`.
:param image_file: Path where the output image will be stored. :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 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: 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 @action
def capture_sequence( def capture_sequence(
self, self,
device: Optional[Union[int, str]] = None,
duration: Optional[float] = None, duration: Optional[float] = None,
n_frames: Optional[int] = None, n_frames: Optional[int] = None,
preview: bool = False, 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. 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 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 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 :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. :param preview: Show a preview of the camera frames.
:return: The directory where the image files have been stored. :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( self.start_camera(
camera, duration=duration, n_frames=n_frames, preview=preview camera, duration=duration, n_frames=n_frames, preview=preview
) )
@ -655,17 +667,22 @@ class CameraPlugin(Plugin, ABC):
@action @action
def capture_preview( 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: ) -> dict:
""" """
Start a camera preview session. 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 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 n_frames: Number of frames to display before closing (default: until :meth:`.stop_capture` is called).
:param camera: Camera object properties. :param camera: Camera object properties.
:return: The status of the device. :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) self.start_camera(camera, duration=duration, n_frames=n_frames, preview=True)
return self.status(camera.info.device) # type: ignore return self.status(camera.info.device) # type: ignore
@ -744,17 +761,24 @@ class CameraPlugin(Plugin, ABC):
@action @action
def start_streaming( 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: ) -> dict:
""" """
Expose the video stream of a camera over a TCP connection. 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 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 stream_format: Format of the output stream - e.g. ``h264``, ``mjpeg``, ``mkv`` etc. (default: ``mkv``).
:param camera: Camera object properties - see constructor parameters. :param camera: Camera object properties - see constructor parameters.
:return: The status of the device. :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 return self._start_streaming(camera, duration, stream_format) # type: ignore
def _start_streaming( def _start_streaming(
@ -900,7 +924,7 @@ class CameraPlugin(Plugin, ABC):
return frame return frame
size = (int(frame.size[0] * scale_x), int(frame.size[1] * scale_y)) 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 @staticmethod
def encode_frame(frame, encoding: str = 'jpeg') -> bytes: def encode_frame(frame, encoding: str = 'jpeg') -> bytes:

View File

@ -1,12 +1,13 @@
import signal import signal
import subprocess import subprocess
from typing import Optional, Tuple from typing import Iterable, Optional
from PIL import Image from PIL import Image
from PIL.Image import Image as ImageType from PIL.Image import Image as ImageType
from platypush.plugins.camera import CameraPlugin from platypush.plugins.camera import CameraPlugin
from platypush.plugins.camera.ffmpeg.model import FFmpegCamera, FFmpegCameraInfo from platypush.plugins.camera.ffmpeg.model import FFmpegCamera, FFmpegCameraInfo
from platypush.plugins.camera.model.camera import Camera
class CameraFfmpegPlugin(CameraPlugin): class CameraFfmpegPlugin(CameraPlugin):
@ -21,75 +22,83 @@ class CameraFfmpegPlugin(CameraPlugin):
self, self,
device: Optional[str] = '/dev/video0', device: Optional[str] = '/dev/video0',
input_format: str = 'v4l2', input_format: str = 'v4l2',
ffmpeg_args: Tuple[str] = (), ffmpeg_args: Iterable[str] = (),
**opts **opts,
): ):
""" """
:param device: Path to the camera device (default: ``/dev/video0``). :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 ffmpeg_args: Extra options to be passed to the FFmpeg executable.
:param opts: Camera options - see constructor of :class:`platypush.plugins.camera.CameraPlugin`. :param opts: Camera options - see constructor of :class:`platypush.plugins.camera.CameraPlugin`.
""" """
super().__init__(device=device, input_format=input_format, **opts) 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: def prepare_device(self, device: Camera) -> subprocess.Popen:
warmup_seconds = self._get_warmup_seconds(camera) assert isinstance(device, FFmpegCamera)
warmup_seconds = self._get_warmup_seconds(device)
ffmpeg = [ ffmpeg = [
camera.info.ffmpeg_bin, device.info.ffmpeg_bin,
'-y', '-y',
'-f', '-f',
camera.info.input_format, device.info.input_format,
'-i', '-i',
camera.info.device, device.info.device,
'-s', '-s',
'{}x{}'.format(*camera.info.resolution), *(
(f'{device.info.resolution[0]}x{device.info.resolution[1]}',)
if device.info.resolution
else ()
),
'-ss', '-ss',
str(warmup_seconds), 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', '-pix_fmt',
'rgb24', 'rgb24',
'-f', '-f',
'rawvideo', '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) proc = subprocess.Popen(ffmpeg, stdout=subprocess.PIPE)
# Start in suspended mode # Start in suspended mode
proc.send_signal(signal.SIGSTOP) proc.send_signal(signal.SIGSTOP)
return proc return proc
def start_camera( def start_camera(self, camera: Camera, *args, preview: bool = False, **kwargs):
self, camera: FFmpegCamera, preview: bool = False, *args, **kwargs assert isinstance(camera, FFmpegCamera)
):
super().start_camera(*args, camera=camera, preview=preview, **kwargs) super().start_camera(*args, camera=camera, preview=preview, **kwargs)
if camera.object: if camera.object:
camera.object.send_signal(signal.SIGCONT) camera.object.send_signal(signal.SIGCONT)
def release_device(self, camera: FFmpegCamera): def release_device(self, device: Camera):
if camera.object: assert isinstance(device, FFmpegCamera)
camera.object.terminate() if device.object:
if camera.object.stdout: device.object.terminate()
camera.object.stdout.close() if device.object.stdout:
camera.object = None 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: if camera.object and camera.object.poll() is None:
try: try:
camera.object.wait(timeout=camera.info.capture_timeout) camera.object.wait(timeout=camera.info.capture_timeout)
except Exception as e: 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( def capture_frame(self, device: Camera, *_, **__) -> Optional[ImageType]:
self, camera: FFmpegCamera, *args, **kwargs assert isinstance(device, FFmpegCamera)
) -> Optional[ImageType]: assert device.info.resolution, 'Resolution not set'
raw_size = camera.info.resolution[0] * camera.info.resolution[1] * 3 assert device.object.stdout, 'Camera not started'
data = camera.object.stdout.read(raw_size)
raw_size = device.info.resolution[0] * device.info.resolution[1] * 3
data = device.object.stdout.read(raw_size)
if len(data) < raw_size: if len(data) < raw_size:
return return
return Image.frombytes('RGB', camera.info.resolution, data) return Image.frombytes('RGB', device.info.resolution, data)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -3,7 +3,6 @@ import logging
import multiprocessing import multiprocessing
import os import os
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional, IO from typing import Optional, IO
@ -63,8 +62,12 @@ class FileVideoWriter(VideoWriter, ABC):
""" """
def __init__(self, *args, output_file: str, **kwargs): def __init__(self, *args, output_file: str, **kwargs):
super().__init__(self, *args, **kwargs) super().__init__(
self.output_file = os.path.abspath(os.path.expanduser(output_file)) self,
*args,
output_file=os.path.abspath(os.path.expanduser(output_file)),
**kwargs,
)
class StreamWriter(VideoWriter, ABC): class StreamWriter(VideoWriter, ABC):