Black'd the camera plugin and writer.

Also, proper fix for the multi-inheritance problem of
the ffmpeg writers.
This commit is contained in:
Fabio Manganiello 2023-05-23 20:42:59 +02:00
parent a2f8e2f0d2
commit 1c40e5e843
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
3 changed files with 347 additions and 131 deletions

View file

@ -10,21 +10,39 @@ from contextlib import contextmanager
from datetime import datetime
from multiprocessing import Process
from queue import Queue
from typing import Optional, Union, Dict, Tuple, IO
from typing import Generator, Optional, Union, Dict, Tuple, IO
from platypush.config import Config
from platypush.message.event.camera import CameraRecordingStartedEvent, CameraPictureTakenEvent, \
CameraRecordingStoppedEvent, CameraVideoRenderedEvent
from platypush.message.event.camera import (
CameraRecordingStartedEvent,
CameraPictureTakenEvent,
CameraRecordingStoppedEvent,
CameraVideoRenderedEvent,
)
from platypush.plugins import Plugin, action
from platypush.plugins.camera.model.camera import CameraInfo, Camera
from platypush.plugins.camera.model.exceptions import CameraException, CaptureAlreadyRunningException
from platypush.plugins.camera.model.exceptions import (
CameraException,
CaptureAlreadyRunningException,
)
from platypush.plugins.camera.model.writer import VideoWriter, StreamWriter
from platypush.plugins.camera.model.writer.ffmpeg import FFmpegFileWriter
from platypush.plugins.camera.model.writer.preview import PreviewWriter, PreviewWriterFactory
from platypush.plugins.camera.model.writer.preview import (
PreviewWriter,
PreviewWriterFactory,
)
from platypush.utils import get_plugin_name_by_class
__all__ = ['Camera', 'CameraInfo', 'CameraException', 'CameraPlugin', 'CaptureAlreadyRunningException',
'VideoWriter', 'StreamWriter', 'PreviewWriter']
__all__ = [
'Camera',
'CameraInfo',
'CameraException',
'CameraPlugin',
'CaptureAlreadyRunningException',
'VideoWriter',
'StreamWriter',
'PreviewWriter',
]
class CameraPlugin(Plugin, ABC):
@ -66,15 +84,32 @@ class CameraPlugin(Plugin, ABC):
_camera_info_class = CameraInfo
_video_writer_class = FFmpegFileWriter
def __init__(self, device: Optional[Union[int, str]] = None, resolution: Tuple[int, int] = (640, 480),
frames_dir: Optional[str] = None, warmup_frames: int = 5, warmup_seconds: Optional[float] = 0.,
capture_timeout: Optional[float] = 20.0, scale_x: Optional[float] = None,
scale_y: Optional[float] = None, rotate: Optional[float] = None, grayscale: Optional[bool] = None,
color_transform: Optional[Union[int, str]] = None, fps: float = 16, horizontal_flip: bool = False,
vertical_flip: bool = False, input_format: Optional[str] = None, output_format: Optional[str] = None,
stream_format: str = 'mjpeg', listen_port: Optional[int] = 5000, bind_address: str = '0.0.0.0',
ffmpeg_bin: str = 'ffmpeg', input_codec: Optional[str] = None, output_codec: Optional[str] = None,
**kwargs):
def __init__(
self,
device: Optional[Union[int, str]] = None,
resolution: Tuple[int, int] = (640, 480),
frames_dir: Optional[str] = None,
warmup_frames: int = 5,
warmup_seconds: Optional[float] = 0.0,
capture_timeout: Optional[float] = 20.0,
scale_x: Optional[float] = None,
scale_y: Optional[float] = None,
rotate: Optional[float] = None,
grayscale: Optional[bool] = None,
color_transform: Optional[Union[int, str]] = None,
fps: float = 16,
horizontal_flip: bool = False,
vertical_flip: bool = False,
input_format: Optional[str] = None,
output_format: Optional[str] = None,
stream_format: str = 'mjpeg',
listen_port: Optional[int] = 5000,
bind_address: str = '0.0.0.0',
ffmpeg_bin: str = 'ffmpeg',
input_codec: Optional[str] = None,
output_codec: Optional[str] = None,
**kwargs,
):
"""
:param device: Identifier of the default capturing device.
:param resolution: Default resolution, as a tuple of two integers.
@ -117,22 +152,38 @@ class CameraPlugin(Plugin, ABC):
"""
super().__init__(**kwargs)
self.workdir = os.path.join(Config.get('workdir'), get_plugin_name_by_class(self))
self.workdir = os.path.join(
Config.get('workdir'), get_plugin_name_by_class(self)
)
pathlib.Path(self.workdir).mkdir(mode=0o755, exist_ok=True, parents=True)
# noinspection PyArgumentList
self.camera_info = self._camera_info_class(device, color_transform=color_transform, warmup_frames=warmup_frames,
warmup_seconds=warmup_seconds, rotate=rotate, scale_x=scale_x,
scale_y=scale_y, capture_timeout=capture_timeout, fps=fps,
input_format=input_format, output_format=output_format,
stream_format=stream_format, resolution=resolution,
grayscale=grayscale, listen_port=listen_port,
horizontal_flip=horizontal_flip, vertical_flip=vertical_flip,
ffmpeg_bin=ffmpeg_bin, input_codec=input_codec,
output_codec=output_codec, bind_address=bind_address,
frames_dir=os.path.abspath(
os.path.expanduser(frames_dir or
os.path.join(self.workdir, 'frames'))))
self.camera_info = self._camera_info_class(
device,
color_transform=color_transform,
warmup_frames=warmup_frames,
warmup_seconds=warmup_seconds,
rotate=rotate,
scale_x=scale_x,
scale_y=scale_y,
capture_timeout=capture_timeout,
fps=fps,
input_format=input_format,
output_format=output_format,
stream_format=stream_format,
resolution=resolution,
grayscale=grayscale,
listen_port=listen_port,
horizontal_flip=horizontal_flip,
vertical_flip=vertical_flip,
ffmpeg_bin=ffmpeg_bin,
input_codec=input_codec,
output_codec=output_codec,
bind_address=bind_address,
frames_dir=os.path.abspath(
os.path.expanduser(frames_dir or os.path.join(self.workdir, 'frames'))
),
)
self._devices: Dict[Union[int, str], Camera] = {}
self._streams: Dict[Union[int, str], Camera] = {}
@ -142,7 +193,9 @@ class CameraPlugin(Plugin, ABC):
merged_info.set(**info)
return merged_info
def open_device(self, device: Optional[Union[int, str]] = None, stream: bool = False, **params) -> Camera:
def open_device(
self, device: Optional[Union[int, str]] = None, stream: bool = False, **params
) -> Camera:
"""
Initialize and open a device.
@ -160,7 +213,11 @@ class CameraPlugin(Plugin, ABC):
assert device is not None, 'No device specified/configured'
if device in self._devices:
camera = self._devices[device]
if camera.capture_thread and camera.capture_thread.is_alive() and camera.start_event.is_set():
if (
camera.capture_thread
and camera.capture_thread.is_alive()
and camera.start_event.is_set()
):
raise CaptureAlreadyRunningException(device)
camera.start_event.clear()
@ -177,8 +234,9 @@ class CameraPlugin(Plugin, ABC):
camera.stream = writer_class(camera=camera, plugin=self)
if camera.info.frames_dir:
pathlib.Path(os.path.abspath(os.path.expanduser(camera.info.frames_dir))).mkdir(
mode=0o755, exist_ok=True, parents=True)
pathlib.Path(
os.path.abspath(os.path.expanduser(camera.info.frames_dir))
).mkdir(mode=0o755, exist_ok=True, parents=True)
self._devices[device] = camera
return camera
@ -205,15 +263,20 @@ class CameraPlugin(Plugin, ABC):
:param camera: Camera object. ``camera.info.capture_timeout`` is used as a capture thread termination timeout
if set.
"""
if camera.capture_thread and camera.capture_thread.is_alive() and \
threading.get_ident() != camera.capture_thread.ident:
if (
camera.capture_thread
and camera.capture_thread.is_alive()
and threading.get_ident() != camera.capture_thread.ident
):
try:
camera.capture_thread.join(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)
@contextmanager
def open(self, device: Optional[Union[int, str]] = None, stream: bool = None, **info) -> Camera:
def open(
self, device: Optional[Union[int, str]] = None, stream: bool = None, **info
) -> Generator[Camera, None, None]:
"""
Initialize and open a device using a context manager pattern.
@ -267,6 +330,7 @@ class CameraPlugin(Plugin, ABC):
:param format: Output format.
"""
from PIL import Image
if isinstance(frame, bytes):
frame = list(frame)
elif not isinstance(frame, Image.Image):
@ -278,16 +342,28 @@ class CameraPlugin(Plugin, ABC):
frame.save(filepath, **save_args)
def _store_frame(self, frame, frames_dir: Optional[str] = None, image_file: Optional[str] = None,
*args, **kwargs) -> str:
def _store_frame(
self,
frame,
frames_dir: Optional[str] = None,
image_file: Optional[str] = None,
*args,
**kwargs,
) -> str:
"""
:meth:`.store_frame` wrapper.
"""
if image_file:
filepath = os.path.abspath(os.path.expanduser(image_file))
else:
filepath = os.path.abspath(os.path.expanduser(
os.path.join(frames_dir or '', datetime.now().strftime('%Y-%m-%d_%H-%M-%S-%f.jpg'))))
filepath = os.path.abspath(
os.path.expanduser(
os.path.join(
frames_dir or '',
datetime.now().strftime('%Y-%m-%d_%H-%M-%S-%f.jpg'),
)
)
)
pathlib.Path(filepath).parent.mkdir(mode=0o755, exist_ok=True, parents=True)
self.store_frame(frame, filepath, *args, **kwargs)
@ -295,7 +371,9 @@ class CameraPlugin(Plugin, ABC):
def start_preview(self, camera: Camera):
if camera.preview and not camera.preview.closed:
self.logger.info('A preview window is already active on device {}'.format(camera.info.device))
self.logger.info(
'A preview window is already active on device %s', camera.info.device
)
return
camera.preview = PreviewWriterFactory.get(camera, self)
@ -315,7 +393,9 @@ class CameraPlugin(Plugin, ABC):
camera.preview = None
def frame_processor(self, frame_queue: Queue, camera: Camera, image_file: Optional[str] = None):
def frame_processor(
self, frame_queue: Queue, camera: Camera, image_file: Optional[str] = None
):
while True:
frame = frame_queue.get()
if frame is None:
@ -326,18 +406,31 @@ class CameraPlugin(Plugin, ABC):
frame = self.to_grayscale(frame)
frame = self.rotate_frame(frame, camera.info.rotate)
frame = self.flip_frame(frame, camera.info.horizontal_flip, camera.info.vertical_flip)
frame = self.flip_frame(
frame, camera.info.horizontal_flip, camera.info.vertical_flip
)
frame = self.scale_frame(frame, camera.info.scale_x, camera.info.scale_y)
for output in camera.get_outputs():
output.write(frame)
if camera.info.frames_dir or image_file:
self._store_frame(frame=frame, frames_dir=camera.info.frames_dir, image_file=image_file)
self._store_frame(
frame=frame,
frames_dir=camera.info.frames_dir,
image_file=image_file,
)
def capturing_thread(self, camera: Camera, duration: Optional[float] = None, video_file: Optional[str] = None,
image_file: Optional[str] = None, n_frames: Optional[int] = None, preview: bool = False,
**kwargs):
def capturing_thread(
self,
camera: Camera,
duration: Optional[float] = None,
video_file: Optional[str] = None,
image_file: Optional[str] = None,
n_frames: Optional[int] = None,
preview: bool = False,
**kwargs,
):
"""
Camera capturing thread.
@ -366,25 +459,36 @@ class CameraPlugin(Plugin, ABC):
if duration and camera.info.warmup_seconds:
duration = duration + camera.info.warmup_seconds
if video_file:
camera.file_writer = self._video_writer_class(camera=camera, plugin=self, output_file=video_file)
camera.file_writer = self._video_writer_class(
camera=camera, plugin=self, output_file=video_file
)
frame_queue = Queue()
frame_processor = threading.Thread(target=self.frame_processor,
kwargs=dict(frame_queue=frame_queue, camera=camera, image_file=image_file))
frame_processor = threading.Thread(
target=self.frame_processor,
kwargs={
'frame_queue': frame_queue,
'camera': camera,
'image_file': image_file,
},
)
frame_processor.start()
self.fire_event(CameraRecordingStartedEvent(**evt_args))
try:
while camera.start_event.is_set():
if (duration and time.time() - recording_started_time >= duration) \
or (n_frames and captured_frames >= n_frames):
if (duration and time.time() - recording_started_time >= duration) or (
n_frames and captured_frames >= n_frames
):
break
frame_capture_start = time.time()
try:
frame = self.capture_frame(camera, **kwargs)
if not frame:
self.logger.warning('Invalid frame received, terminating the capture session')
self.logger.warning(
'Invalid frame received, terminating the capture session'
)
break
frame_queue.put(frame)
@ -392,12 +496,20 @@ class CameraPlugin(Plugin, ABC):
self.logger.warning(str(e))
continue
if not n_frames or not camera.info.warmup_seconds or \
(time.time() - recording_started_time >= camera.info.warmup_seconds):
if (
not n_frames
or not camera.info.warmup_seconds
or (
time.time() - recording_started_time
>= camera.info.warmup_seconds
)
):
captured_frames += 1
if camera.info.fps:
wait_time = (1. / camera.info.fps) - (time.time() - frame_capture_start)
wait_time = (1.0 / camera.info.fps) - (
time.time() - frame_capture_start
)
if wait_time > 0:
time.sleep(wait_time)
finally:
@ -407,7 +519,7 @@ class CameraPlugin(Plugin, ABC):
try:
output.close()
except Exception as e:
self.logger.warning('Could not close camera output: {}'.format(str(e)))
self.logger.warning('Could not close camera output: %s', e)
self.close_device(camera, wait_capture=False)
frame_processor.join(timeout=5.0)
@ -426,17 +538,26 @@ class CameraPlugin(Plugin, ABC):
:param camera: An initialized :class:`platypush.plugins.camera.Camera` object.
:param preview: Show a preview of the camera frames.
"""
assert not (camera.capture_thread and camera.capture_thread.is_alive()), \
'A capture session is already in progress'
assert not (
camera.capture_thread and camera.capture_thread.is_alive()
), 'A capture session is already in progress'
camera.capture_thread = threading.Thread(target=self.capturing_thread, args=(camera, *args),
kwargs={'preview': preview, **kwargs})
camera.capture_thread = threading.Thread(
target=self.capturing_thread,
args=(camera, *args),
kwargs={'preview': preview, **kwargs},
)
camera.capture_thread.start()
camera.start_event.set()
@action
def capture_video(self, duration: Optional[float] = None, video_file: Optional[str] = None, preview: bool = False,
**camera) -> Union[str, dict]:
def capture_video(
self,
duration: Optional[float] = None,
video_file: Optional[str] = None,
preview: bool = False,
**camera,
) -> Union[str, dict]:
"""
Capture a video.
@ -448,8 +569,14 @@ class CameraPlugin(Plugin, ABC):
to the recorded resource. Otherwise, it will return the status of the camera device after starting it.
"""
camera = self.open_device(**camera)
self.start_camera(camera, duration=duration, video_file=video_file, frames_dir=None, image_file=None,
preview=preview)
self.start_camera(
camera,
duration=duration,
video_file=video_file,
frames_dir=None,
image_file=None,
preview=preview,
)
if duration:
self.wait_capture(camera)
@ -484,8 +611,12 @@ class CameraPlugin(Plugin, ABC):
"""
with self.open(**camera) as camera:
warmup_frames = camera.info.warmup_frames if camera.info.warmup_frames else 1
self.start_camera(camera, image_file=image_file, n_frames=warmup_frames, preview=preview)
warmup_frames = (
camera.info.warmup_frames if camera.info.warmup_frames else 1
)
self.start_camera(
camera, image_file=image_file, n_frames=warmup_frames, preview=preview
)
self.wait_capture(camera)
return image_file
@ -504,8 +635,13 @@ class CameraPlugin(Plugin, ABC):
return self.capture_image(image_file, **camera)
@action
def capture_sequence(self, duration: Optional[float] = None, n_frames: Optional[int] = None, preview: bool = False,
**camera) -> str:
def capture_sequence(
self,
duration: Optional[float] = None,
n_frames: Optional[int] = None,
preview: bool = False,
**camera,
) -> str:
"""
Capture a sequence of frames from a camera and store them to a directory.
@ -517,12 +653,16 @@ class CameraPlugin(Plugin, ABC):
:return: The directory where the image files have been stored.
"""
with self.open(**camera) as camera:
self.start_camera(camera, duration=duration, n_frames=n_frames, preview=preview)
self.start_camera(
camera, duration=duration, n_frames=n_frames, preview=preview
)
self.wait_capture(camera)
return camera.info.frames_dir
@action
def capture_preview(self, duration: Optional[float] = None, n_frames: Optional[int] = None, **camera) -> dict:
def capture_preview(
self, duration: Optional[float] = None, n_frames: Optional[int] = None, **camera
) -> dict:
"""
Start a camera preview session.
@ -539,10 +679,12 @@ class CameraPlugin(Plugin, ABC):
def _prepare_server_socket(camera: Camera) -> socket.socket:
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(( # lgtm [py/bind-socket-all-network-interfaces]
camera.info.bind_address or '0.0.0.0',
camera.info.listen_port
))
server_socket.bind(
( # lgtm [py/bind-socket-all-network-interfaces]
camera.info.bind_address or '0.0.0.0',
camera.info.listen_port,
)
)
server_socket.listen(1)
server_socket.settimeout(1)
return server_socket
@ -550,16 +692,18 @@ class CameraPlugin(Plugin, ABC):
def _accept_client(self, server_socket: socket.socket) -> Optional[IO]:
try:
sock = server_socket.accept()[0]
self.logger.info('Accepted client connection from {}'.format(sock.getpeername()))
self.logger.info('Accepted client connection from %s', sock.getpeername())
return sock.makefile('wb')
except socket.timeout:
return
def streaming_thread(self, camera: Camera, stream_format: str, duration: Optional[float] = None):
def streaming_thread(
self, camera: Camera, stream_format: str, duration: Optional[float] = None
):
streaming_started_time = time.time()
server_socket = self._prepare_server_socket(camera)
sock = None
self.logger.info('Starting streaming on port {}'.format(camera.info.listen_port))
self.logger.info('Starting streaming on port %s', camera.info.listen_port)
try:
while camera.stream_event.is_set():
@ -576,7 +720,9 @@ class CameraPlugin(Plugin, ABC):
camera = self.open_device(stream=True, **info)
camera.stream.sock = sock
self.start_camera(camera, duration=duration, frames_dir=None, image_file=None)
self.start_camera(
camera, duration=duration, frames_dir=None, image_file=None
)
finally:
self._cleanup_stream(camera, server_socket, sock)
self.logger.info('Stopped camera stream')
@ -586,21 +732,23 @@ class CameraPlugin(Plugin, ABC):
try:
client.close()
except Exception as e:
self.logger.warning('Error on client socket close: {}'.format(str(e)))
self.logger.warning('Error on client socket close: %s', e)
try:
server_socket.close()
except Exception as e:
self.logger.warning('Error on server socket close: {}'.format(str(e)))
self.logger.warning('Error on server socket close: %s', e)
if camera.stream:
try:
camera.stream.close()
except Exception as e:
self.logger.warning('Error while closing the encoding stream: {}'.format(str(e)))
self.logger.warning('Error while closing the encoding stream: %s', e)
@action
def start_streaming(self, duration: Optional[float] = None, stream_format: str = 'mkv', **camera) -> dict:
def start_streaming(
self, duration: Optional[float] = None, stream_format: str = 'mkv', **camera
) -> dict:
"""
Expose the video stream of a camera over a TCP connection.
@ -612,16 +760,25 @@ class CameraPlugin(Plugin, ABC):
camera = self.open_device(stream=True, stream_format=stream_format, **camera)
return self._start_streaming(camera, duration, stream_format)
def _start_streaming(self, camera: Camera, duration: Optional[float], stream_format: str):
def _start_streaming(
self, camera: Camera, duration: Optional[float], stream_format: str
):
assert camera.info.listen_port, 'No listen_port specified/configured'
assert not camera.stream_event.is_set() and camera.info.device not in self._streams, \
'A streaming session is already running for device {}'.format(camera.info.device)
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}'
self._streams[camera.info.device] = camera
camera.stream_event.set()
camera.stream_thread = threading.Thread(target=self.streaming_thread, kwargs=dict(
camera=camera, duration=duration, stream_format=stream_format))
camera.stream_thread = threading.Thread(
target=self.streaming_thread,
kwargs={
'camera': camera,
'duration': duration,
'stream_format': stream_format,
},
)
camera.stream_thread.start()
return self.status(camera.info.device)
@ -655,10 +812,17 @@ class CameraPlugin(Plugin, ABC):
return {
**camera.info.to_dict(),
'active': True if camera.capture_thread and camera.capture_thread.is_alive() else False,
'capturing': True if camera.capture_thread and camera.capture_thread.is_alive() and
camera.start_event.is_set() else False,
'streaming': camera.stream_thread and camera.stream_thread.is_alive() and camera.stream_event.is_set(),
'active': bool(camera.capture_thread and camera.capture_thread.is_alive()),
'capturing': bool(
camera.capture_thread
and camera.capture_thread.is_alive()
and camera.start_event.is_set()
),
'streaming': (
camera.stream_thread
and camera.stream_thread.is_alive()
and camera.stream_event.is_set()
),
}
@action
@ -670,10 +834,7 @@ class CameraPlugin(Plugin, ABC):
if device:
return self._status(device)
return {
id: self._status(device)
for id, camera in self._devices.items()
}
return {id: self._status(device) for id, camera in self._devices.items()}
@staticmethod
def transform_frame(frame, color_transform):
@ -690,6 +851,7 @@ class CameraPlugin(Plugin, ABC):
:param frame: Image frame (default: a ``PIL.Image`` object).
"""
from PIL import ImageOps
return ImageOps.grayscale(frame)
@staticmethod
@ -724,7 +886,9 @@ class CameraPlugin(Plugin, ABC):
return frame
@staticmethod
def scale_frame(frame, scale_x: Optional[float] = None, scale_y: Optional[float] = None):
def scale_frame(
frame, scale_x: Optional[float] = None, scale_y: Optional[float] = None
):
"""
Frame scaling logic. The default implementation assumes that frame is a ``PIL.Image`` object.
@ -733,6 +897,7 @@ class CameraPlugin(Plugin, ABC):
:param scale_y: Y-scale factor.
"""
from PIL import Image
if not (scale_x and scale_y) or (scale_x == 1 and scale_y == 1):
return frame

View file

@ -49,7 +49,7 @@ class VideoWriter(ABC):
"""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
def __exit__(self, *_, **__):
"""
Context manager-based interface.
"""
@ -60,8 +60,9 @@ class FileVideoWriter(VideoWriter, ABC):
"""
Abstract class to handle frames-to-video file operations.
"""
def __init__(self, *args, output_file: str, **kwargs):
VideoWriter.__init__(self, *args, **kwargs)
super().__init__(self, *args, **kwargs)
self.output_file = os.path.abspath(os.path.expanduser(output_file))
@ -69,8 +70,9 @@ class StreamWriter(VideoWriter, ABC):
"""
Abstract class for camera streaming operations.
"""
def __init__(self, *args, sock: Optional[IO] = None, **kwargs):
VideoWriter.__init__(self, *args, **kwargs)
super().__init__(*args, **kwargs)
self.frame: Optional[bytes] = None
self.frame_time: Optional[float] = None
self.buffer = io.BytesIO()
@ -117,16 +119,20 @@ class StreamWriter(VideoWriter, ABC):
try:
self.sock.close()
except Exception as e:
self.logger.warning('Could not close camera resource: {}'.format(str(e)))
self.logger.warning('Could not close camera resource: %s', e)
super().close()
@staticmethod
def get_class_by_name(name: str):
from platypush.plugins.camera.model.writer.index import StreamHandlers
name = name.upper()
assert hasattr(StreamHandlers, name), 'No such stream handler: {}. Supported types: {}'.format(
name, [hndl.name for hndl in list(StreamHandlers)])
assert hasattr(
StreamHandlers, name
), f'No such stream handler: {name}. Supported types: ' + (
', '.join([hndl.name for hndl in list(StreamHandlers)])
)
return getattr(StreamHandlers, name).value

View file

@ -8,7 +8,11 @@ from typing import Optional, Tuple
from PIL.Image import Image
from platypush.plugins.camera.model.camera import Camera
from platypush.plugins.camera.model.writer import VideoWriter, FileVideoWriter, StreamWriter
from platypush.plugins.camera.model.writer import (
VideoWriter,
FileVideoWriter,
StreamWriter,
)
class FFmpegWriter(VideoWriter, ABC):
@ -16,9 +20,17 @@ class FFmpegWriter(VideoWriter, ABC):
Generic FFmpeg encoder for camera frames.
"""
def __init__(self, *args, input_file: str = '-', input_format: str = 'rawvideo', output_file: str = '-',
output_format: Optional[str] = None, pix_fmt: Optional[str] = None,
output_opts: Optional[Tuple] = None, **kwargs):
def __init__(
self,
*args,
input_file: str = '-',
input_format: str = 'rawvideo',
output_file: str = '-',
output_format: Optional[str] = None,
pix_fmt: Optional[str] = None,
output_opts: Optional[Tuple] = None,
**kwargs,
):
super().__init__(*args, **kwargs)
self.input_file = input_file
@ -29,21 +41,34 @@ class FFmpegWriter(VideoWriter, ABC):
self.pix_fmt = pix_fmt
self.output_opts = output_opts or ()
self.logger.info('Starting FFmpeg. Command: {}'.format(' '.join(self.ffmpeg_args)))
self.ffmpeg = subprocess.Popen(self.ffmpeg_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
self.logger.info('Starting FFmpeg. Command: ' + ' '.join(self.ffmpeg_args))
self.ffmpeg = subprocess.Popen(
self.ffmpeg_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
@property
def ffmpeg_args(self):
return [self.camera.info.ffmpeg_bin, '-y',
'-f', self.input_format,
*(('-pix_fmt', self.pix_fmt) if self.pix_fmt else ()),
'-s', '{}x{}'.format(self.width, self.height),
'-r', str(self.camera.info.fps),
'-i', self.input_file,
*(('-f', self.output_format) if self.output_format else ()),
*self.output_opts,
*(('-vcodec', self.camera.info.output_codec) if self.camera.info.output_codec else ()),
self.output_file]
return [
self.camera.info.ffmpeg_bin,
'-y',
'-f',
self.input_format,
*(('-pix_fmt', self.pix_fmt) if self.pix_fmt else ()),
'-s',
f'{self.width}x{self.height}',
'-r',
str(self.camera.info.fps),
'-i',
self.input_file,
*(('-f', self.output_format) if self.output_format else ()),
*self.output_opts,
*(
('-vcodec', self.camera.info.output_codec)
if self.camera.info.output_codec
else ()
),
self.output_file,
]
def is_closed(self):
return self.closed or not self.ffmpeg or self.ffmpeg.poll() is not None
@ -55,7 +80,7 @@ class FFmpegWriter(VideoWriter, ABC):
try:
self.ffmpeg.stdin.write(image.convert('RGB').tobytes())
except Exception as e:
self.logger.warning('FFmpeg send error: {}'.format(str(e)))
self.logger.warning('FFmpeg send error: %s', e)
self.close()
def close(self):
@ -63,7 +88,7 @@ class FFmpegWriter(VideoWriter, ABC):
if self.ffmpeg and self.ffmpeg.stdin:
try:
self.ffmpeg.stdin.close()
except (IOError, OSError):
except OSError:
pass
if self.ffmpeg:
@ -77,7 +102,7 @@ class FFmpegWriter(VideoWriter, ABC):
if self.ffmpeg and self.ffmpeg.stdout:
try:
self.ffmpeg.stdout.close()
except (IOError, OSError):
except OSError:
pass
self.ffmpeg = None
@ -98,10 +123,26 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
Stream camera frames using FFmpeg.
"""
def __init__(self, *args, output_format: str, output_opts: Optional[Tuple] = None, **kwargs):
super().__init__(*args, pix_fmt='rgb24', output_format=output_format, output_opts=output_opts or (
'-tune', 'zerolatency', '-preset', 'superfast', '-trellis', '0',
'-fflags', 'nobuffer'), **kwargs)
def __init__(
self, *args, output_format: str, output_opts: Optional[Tuple] = None, **kwargs
):
super().__init__(
*args,
pix_fmt='rgb24',
output_format=output_format,
output_opts=output_opts
or (
'-tune',
'zerolatency',
'-preset',
'superfast',
'-trellis',
'0',
'-fflags',
'nobuffer',
),
**kwargs,
)
self._reader = threading.Thread(target=self._reader_thread)
self._reader.start()
@ -115,7 +156,7 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
try:
data = self.ffmpeg.stdout.read(1 << 15)
except Exception as e:
self.logger.warning('FFmpeg reader error: {}'.format(str(e)))
self.logger.warning('FFmpeg reader error: %s', e)
break
if not data:
@ -123,7 +164,7 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
if self.frame is None:
latency = time.time() - start_time
self.logger.info('FFmpeg stream latency: {} secs'.format(latency))
self.logger.info('FFmpeg stream latency: %d secs', latency)
with self.ready:
self.frame = data
@ -140,12 +181,16 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
try:
self.ffmpeg.stdin.write(data)
except Exception as e:
self.logger.warning('FFmpeg send error: {}'.format(str(e)))
self.logger.warning('FFmpeg send error: %s', e)
self.close()
def close(self):
super().close()
if self._reader and self._reader.is_alive() and threading.get_ident() != self._reader.ident:
if (
self._reader
and self._reader.is_alive()
and threading.get_ident() != self._reader.ident
):
self._reader.join(timeout=5.0)
self._reader = None