forked from platypush/platypush
Black'd the camera plugin and writer.
Also, proper fix for the multi-inheritance problem of the ffmpeg writers.
This commit is contained in:
parent
a2f8e2f0d2
commit
1c40e5e843
3 changed files with 347 additions and 131 deletions
|
@ -10,21 +10,39 @@ from contextlib import contextmanager
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
from queue import Queue
|
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.config import Config
|
||||||
from platypush.message.event.camera import CameraRecordingStartedEvent, CameraPictureTakenEvent, \
|
from platypush.message.event.camera import (
|
||||||
CameraRecordingStoppedEvent, CameraVideoRenderedEvent
|
CameraRecordingStartedEvent,
|
||||||
|
CameraPictureTakenEvent,
|
||||||
|
CameraRecordingStoppedEvent,
|
||||||
|
CameraVideoRenderedEvent,
|
||||||
|
)
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
from platypush.plugins.camera.model.camera import CameraInfo, Camera
|
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 import VideoWriter, StreamWriter
|
||||||
from platypush.plugins.camera.model.writer.ffmpeg import FFmpegFileWriter
|
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
|
from platypush.utils import get_plugin_name_by_class
|
||||||
|
|
||||||
__all__ = ['Camera', 'CameraInfo', 'CameraException', 'CameraPlugin', 'CaptureAlreadyRunningException',
|
__all__ = [
|
||||||
'VideoWriter', 'StreamWriter', 'PreviewWriter']
|
'Camera',
|
||||||
|
'CameraInfo',
|
||||||
|
'CameraException',
|
||||||
|
'CameraPlugin',
|
||||||
|
'CaptureAlreadyRunningException',
|
||||||
|
'VideoWriter',
|
||||||
|
'StreamWriter',
|
||||||
|
'PreviewWriter',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CameraPlugin(Plugin, ABC):
|
class CameraPlugin(Plugin, ABC):
|
||||||
|
@ -66,15 +84,32 @@ class CameraPlugin(Plugin, ABC):
|
||||||
_camera_info_class = CameraInfo
|
_camera_info_class = CameraInfo
|
||||||
_video_writer_class = FFmpegFileWriter
|
_video_writer_class = FFmpegFileWriter
|
||||||
|
|
||||||
def __init__(self, device: Optional[Union[int, str]] = None, resolution: Tuple[int, int] = (640, 480),
|
def __init__(
|
||||||
frames_dir: Optional[str] = None, warmup_frames: int = 5, warmup_seconds: Optional[float] = 0.,
|
self,
|
||||||
capture_timeout: Optional[float] = 20.0, scale_x: Optional[float] = None,
|
device: Optional[Union[int, str]] = None,
|
||||||
scale_y: Optional[float] = None, rotate: Optional[float] = None, grayscale: Optional[bool] = None,
|
resolution: Tuple[int, int] = (640, 480),
|
||||||
color_transform: Optional[Union[int, str]] = None, fps: float = 16, horizontal_flip: bool = False,
|
frames_dir: Optional[str] = None,
|
||||||
vertical_flip: bool = False, input_format: Optional[str] = None, output_format: Optional[str] = None,
|
warmup_frames: int = 5,
|
||||||
stream_format: str = 'mjpeg', listen_port: Optional[int] = 5000, bind_address: str = '0.0.0.0',
|
warmup_seconds: Optional[float] = 0.0,
|
||||||
ffmpeg_bin: str = 'ffmpeg', input_codec: Optional[str] = None, output_codec: Optional[str] = None,
|
capture_timeout: Optional[float] = 20.0,
|
||||||
**kwargs):
|
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 device: Identifier of the default capturing device.
|
||||||
:param resolution: Default resolution, as a tuple of two integers.
|
:param resolution: Default resolution, as a tuple of two integers.
|
||||||
|
@ -117,22 +152,38 @@ class CameraPlugin(Plugin, ABC):
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
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)
|
pathlib.Path(self.workdir).mkdir(mode=0o755, exist_ok=True, parents=True)
|
||||||
|
|
||||||
# noinspection PyArgumentList
|
# noinspection PyArgumentList
|
||||||
self.camera_info = self._camera_info_class(device, color_transform=color_transform, warmup_frames=warmup_frames,
|
self.camera_info = self._camera_info_class(
|
||||||
warmup_seconds=warmup_seconds, rotate=rotate, scale_x=scale_x,
|
device,
|
||||||
scale_y=scale_y, capture_timeout=capture_timeout, fps=fps,
|
color_transform=color_transform,
|
||||||
input_format=input_format, output_format=output_format,
|
warmup_frames=warmup_frames,
|
||||||
stream_format=stream_format, resolution=resolution,
|
warmup_seconds=warmup_seconds,
|
||||||
grayscale=grayscale, listen_port=listen_port,
|
rotate=rotate,
|
||||||
horizontal_flip=horizontal_flip, vertical_flip=vertical_flip,
|
scale_x=scale_x,
|
||||||
ffmpeg_bin=ffmpeg_bin, input_codec=input_codec,
|
scale_y=scale_y,
|
||||||
output_codec=output_codec, bind_address=bind_address,
|
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(
|
frames_dir=os.path.abspath(
|
||||||
os.path.expanduser(frames_dir or
|
os.path.expanduser(frames_dir or os.path.join(self.workdir, 'frames'))
|
||||||
os.path.join(self.workdir, 'frames'))))
|
),
|
||||||
|
)
|
||||||
|
|
||||||
self._devices: Dict[Union[int, str], Camera] = {}
|
self._devices: Dict[Union[int, str], Camera] = {}
|
||||||
self._streams: Dict[Union[int, str], Camera] = {}
|
self._streams: Dict[Union[int, str], Camera] = {}
|
||||||
|
@ -142,7 +193,9 @@ class CameraPlugin(Plugin, ABC):
|
||||||
merged_info.set(**info)
|
merged_info.set(**info)
|
||||||
return merged_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.
|
Initialize and open a device.
|
||||||
|
|
||||||
|
@ -160,7 +213,11 @@ class CameraPlugin(Plugin, ABC):
|
||||||
assert device is not None, 'No device specified/configured'
|
assert device is not None, 'No device specified/configured'
|
||||||
if device in self._devices:
|
if device in self._devices:
|
||||||
camera = self._devices[device]
|
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)
|
raise CaptureAlreadyRunningException(device)
|
||||||
|
|
||||||
camera.start_event.clear()
|
camera.start_event.clear()
|
||||||
|
@ -177,8 +234,9 @@ class CameraPlugin(Plugin, ABC):
|
||||||
camera.stream = writer_class(camera=camera, plugin=self)
|
camera.stream = writer_class(camera=camera, plugin=self)
|
||||||
|
|
||||||
if camera.info.frames_dir:
|
if camera.info.frames_dir:
|
||||||
pathlib.Path(os.path.abspath(os.path.expanduser(camera.info.frames_dir))).mkdir(
|
pathlib.Path(
|
||||||
mode=0o755, exist_ok=True, parents=True)
|
os.path.abspath(os.path.expanduser(camera.info.frames_dir))
|
||||||
|
).mkdir(mode=0o755, exist_ok=True, parents=True)
|
||||||
|
|
||||||
self._devices[device] = camera
|
self._devices[device] = camera
|
||||||
return 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
|
:param camera: Camera object. ``camera.info.capture_timeout`` is used as a capture thread termination timeout
|
||||||
if set.
|
if set.
|
||||||
"""
|
"""
|
||||||
if camera.capture_thread and camera.capture_thread.is_alive() and \
|
if (
|
||||||
threading.get_ident() != camera.capture_thread.ident:
|
camera.capture_thread
|
||||||
|
and camera.capture_thread.is_alive()
|
||||||
|
and threading.get_ident() != camera.capture_thread.ident
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
camera.capture_thread.join(timeout=camera.info.capture_timeout)
|
camera.capture_thread.join(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)
|
||||||
|
|
||||||
@contextmanager
|
@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.
|
Initialize and open a device using a context manager pattern.
|
||||||
|
|
||||||
|
@ -267,6 +330,7 @@ class CameraPlugin(Plugin, ABC):
|
||||||
:param format: Output format.
|
:param format: Output format.
|
||||||
"""
|
"""
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
if isinstance(frame, bytes):
|
if isinstance(frame, bytes):
|
||||||
frame = list(frame)
|
frame = list(frame)
|
||||||
elif not isinstance(frame, Image.Image):
|
elif not isinstance(frame, Image.Image):
|
||||||
|
@ -278,16 +342,28 @@ class CameraPlugin(Plugin, ABC):
|
||||||
|
|
||||||
frame.save(filepath, **save_args)
|
frame.save(filepath, **save_args)
|
||||||
|
|
||||||
def _store_frame(self, frame, frames_dir: Optional[str] = None, image_file: Optional[str] = None,
|
def _store_frame(
|
||||||
*args, **kwargs) -> str:
|
self,
|
||||||
|
frame,
|
||||||
|
frames_dir: Optional[str] = None,
|
||||||
|
image_file: Optional[str] = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
:meth:`.store_frame` wrapper.
|
:meth:`.store_frame` wrapper.
|
||||||
"""
|
"""
|
||||||
if image_file:
|
if image_file:
|
||||||
filepath = os.path.abspath(os.path.expanduser(image_file))
|
filepath = os.path.abspath(os.path.expanduser(image_file))
|
||||||
else:
|
else:
|
||||||
filepath = os.path.abspath(os.path.expanduser(
|
filepath = os.path.abspath(
|
||||||
os.path.join(frames_dir or '', datetime.now().strftime('%Y-%m-%d_%H-%M-%S-%f.jpg'))))
|
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)
|
pathlib.Path(filepath).parent.mkdir(mode=0o755, exist_ok=True, parents=True)
|
||||||
self.store_frame(frame, filepath, *args, **kwargs)
|
self.store_frame(frame, filepath, *args, **kwargs)
|
||||||
|
@ -295,7 +371,9 @@ class CameraPlugin(Plugin, ABC):
|
||||||
|
|
||||||
def start_preview(self, camera: Camera):
|
def start_preview(self, camera: Camera):
|
||||||
if camera.preview and not camera.preview.closed:
|
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
|
return
|
||||||
|
|
||||||
camera.preview = PreviewWriterFactory.get(camera, self)
|
camera.preview = PreviewWriterFactory.get(camera, self)
|
||||||
|
@ -315,7 +393,9 @@ class CameraPlugin(Plugin, ABC):
|
||||||
|
|
||||||
camera.preview = None
|
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:
|
while True:
|
||||||
frame = frame_queue.get()
|
frame = frame_queue.get()
|
||||||
if frame is None:
|
if frame is None:
|
||||||
|
@ -326,18 +406,31 @@ class CameraPlugin(Plugin, ABC):
|
||||||
frame = self.to_grayscale(frame)
|
frame = self.to_grayscale(frame)
|
||||||
|
|
||||||
frame = self.rotate_frame(frame, camera.info.rotate)
|
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)
|
frame = self.scale_frame(frame, camera.info.scale_x, camera.info.scale_y)
|
||||||
|
|
||||||
for output in camera.get_outputs():
|
for output in camera.get_outputs():
|
||||||
output.write(frame)
|
output.write(frame)
|
||||||
|
|
||||||
if camera.info.frames_dir or image_file:
|
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,
|
def capturing_thread(
|
||||||
image_file: Optional[str] = None, n_frames: Optional[int] = None, preview: bool = False,
|
self,
|
||||||
**kwargs):
|
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.
|
Camera capturing thread.
|
||||||
|
|
||||||
|
@ -366,25 +459,36 @@ class CameraPlugin(Plugin, ABC):
|
||||||
if duration and camera.info.warmup_seconds:
|
if duration and camera.info.warmup_seconds:
|
||||||
duration = duration + camera.info.warmup_seconds
|
duration = duration + camera.info.warmup_seconds
|
||||||
if video_file:
|
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_queue = Queue()
|
||||||
frame_processor = threading.Thread(target=self.frame_processor,
|
frame_processor = threading.Thread(
|
||||||
kwargs=dict(frame_queue=frame_queue, camera=camera, image_file=image_file))
|
target=self.frame_processor,
|
||||||
|
kwargs={
|
||||||
|
'frame_queue': frame_queue,
|
||||||
|
'camera': camera,
|
||||||
|
'image_file': image_file,
|
||||||
|
},
|
||||||
|
)
|
||||||
frame_processor.start()
|
frame_processor.start()
|
||||||
self.fire_event(CameraRecordingStartedEvent(**evt_args))
|
self.fire_event(CameraRecordingStartedEvent(**evt_args))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while camera.start_event.is_set():
|
while camera.start_event.is_set():
|
||||||
if (duration and time.time() - recording_started_time >= duration) \
|
if (duration and time.time() - recording_started_time >= duration) or (
|
||||||
or (n_frames and captured_frames >= n_frames):
|
n_frames and captured_frames >= n_frames
|
||||||
|
):
|
||||||
break
|
break
|
||||||
|
|
||||||
frame_capture_start = time.time()
|
frame_capture_start = time.time()
|
||||||
try:
|
try:
|
||||||
frame = self.capture_frame(camera, **kwargs)
|
frame = self.capture_frame(camera, **kwargs)
|
||||||
if not frame:
|
if not frame:
|
||||||
self.logger.warning('Invalid frame received, terminating the capture session')
|
self.logger.warning(
|
||||||
|
'Invalid frame received, terminating the capture session'
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
frame_queue.put(frame)
|
frame_queue.put(frame)
|
||||||
|
@ -392,12 +496,20 @@ class CameraPlugin(Plugin, ABC):
|
||||||
self.logger.warning(str(e))
|
self.logger.warning(str(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not n_frames or not camera.info.warmup_seconds or \
|
if (
|
||||||
(time.time() - recording_started_time >= camera.info.warmup_seconds):
|
not n_frames
|
||||||
|
or not camera.info.warmup_seconds
|
||||||
|
or (
|
||||||
|
time.time() - recording_started_time
|
||||||
|
>= camera.info.warmup_seconds
|
||||||
|
)
|
||||||
|
):
|
||||||
captured_frames += 1
|
captured_frames += 1
|
||||||
|
|
||||||
if camera.info.fps:
|
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:
|
if wait_time > 0:
|
||||||
time.sleep(wait_time)
|
time.sleep(wait_time)
|
||||||
finally:
|
finally:
|
||||||
|
@ -407,7 +519,7 @@ class CameraPlugin(Plugin, ABC):
|
||||||
try:
|
try:
|
||||||
output.close()
|
output.close()
|
||||||
except Exception as e:
|
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)
|
self.close_device(camera, wait_capture=False)
|
||||||
frame_processor.join(timeout=5.0)
|
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 camera: An initialized :class:`platypush.plugins.camera.Camera` object.
|
||||||
:param preview: Show a preview of the camera frames.
|
:param preview: Show a preview of the camera frames.
|
||||||
"""
|
"""
|
||||||
assert not (camera.capture_thread and camera.capture_thread.is_alive()), \
|
assert not (
|
||||||
'A capture session is already in progress'
|
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),
|
camera.capture_thread = threading.Thread(
|
||||||
kwargs={'preview': preview, **kwargs})
|
target=self.capturing_thread,
|
||||||
|
args=(camera, *args),
|
||||||
|
kwargs={'preview': preview, **kwargs},
|
||||||
|
)
|
||||||
camera.capture_thread.start()
|
camera.capture_thread.start()
|
||||||
camera.start_event.set()
|
camera.start_event.set()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def capture_video(self, duration: Optional[float] = None, video_file: Optional[str] = None, preview: bool = False,
|
def capture_video(
|
||||||
**camera) -> Union[str, dict]:
|
self,
|
||||||
|
duration: Optional[float] = None,
|
||||||
|
video_file: Optional[str] = None,
|
||||||
|
preview: bool = False,
|
||||||
|
**camera,
|
||||||
|
) -> Union[str, dict]:
|
||||||
"""
|
"""
|
||||||
Capture a video.
|
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.
|
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(**camera)
|
||||||
self.start_camera(camera, duration=duration, video_file=video_file, frames_dir=None, image_file=None,
|
self.start_camera(
|
||||||
preview=preview)
|
camera,
|
||||||
|
duration=duration,
|
||||||
|
video_file=video_file,
|
||||||
|
frames_dir=None,
|
||||||
|
image_file=None,
|
||||||
|
preview=preview,
|
||||||
|
)
|
||||||
|
|
||||||
if duration:
|
if duration:
|
||||||
self.wait_capture(camera)
|
self.wait_capture(camera)
|
||||||
|
@ -484,8 +611,12 @@ class CameraPlugin(Plugin, ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with self.open(**camera) as camera:
|
with self.open(**camera) as camera:
|
||||||
warmup_frames = camera.info.warmup_frames if camera.info.warmup_frames else 1
|
warmup_frames = (
|
||||||
self.start_camera(camera, image_file=image_file, n_frames=warmup_frames, preview=preview)
|
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)
|
self.wait_capture(camera)
|
||||||
|
|
||||||
return image_file
|
return image_file
|
||||||
|
@ -504,8 +635,13 @@ class CameraPlugin(Plugin, ABC):
|
||||||
return self.capture_image(image_file, **camera)
|
return self.capture_image(image_file, **camera)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def capture_sequence(self, duration: Optional[float] = None, n_frames: Optional[int] = None, preview: bool = False,
|
def capture_sequence(
|
||||||
**camera) -> str:
|
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.
|
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.
|
:return: The directory where the image files have been stored.
|
||||||
"""
|
"""
|
||||||
with self.open(**camera) as camera:
|
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)
|
self.wait_capture(camera)
|
||||||
return camera.info.frames_dir
|
return camera.info.frames_dir
|
||||||
|
|
||||||
@action
|
@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.
|
Start a camera preview session.
|
||||||
|
|
||||||
|
@ -539,10 +679,12 @@ class CameraPlugin(Plugin, ABC):
|
||||||
def _prepare_server_socket(camera: Camera) -> socket.socket:
|
def _prepare_server_socket(camera: Camera) -> socket.socket:
|
||||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
server_socket.bind(( # lgtm [py/bind-socket-all-network-interfaces]
|
server_socket.bind(
|
||||||
|
( # lgtm [py/bind-socket-all-network-interfaces]
|
||||||
camera.info.bind_address or '0.0.0.0',
|
camera.info.bind_address or '0.0.0.0',
|
||||||
camera.info.listen_port
|
camera.info.listen_port,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
server_socket.listen(1)
|
server_socket.listen(1)
|
||||||
server_socket.settimeout(1)
|
server_socket.settimeout(1)
|
||||||
return server_socket
|
return server_socket
|
||||||
|
@ -550,16 +692,18 @@ class CameraPlugin(Plugin, ABC):
|
||||||
def _accept_client(self, server_socket: socket.socket) -> Optional[IO]:
|
def _accept_client(self, server_socket: socket.socket) -> Optional[IO]:
|
||||||
try:
|
try:
|
||||||
sock = server_socket.accept()[0]
|
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')
|
return sock.makefile('wb')
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
return
|
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()
|
streaming_started_time = time.time()
|
||||||
server_socket = self._prepare_server_socket(camera)
|
server_socket = self._prepare_server_socket(camera)
|
||||||
sock = None
|
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:
|
try:
|
||||||
while camera.stream_event.is_set():
|
while camera.stream_event.is_set():
|
||||||
|
@ -576,7 +720,9 @@ class CameraPlugin(Plugin, ABC):
|
||||||
camera = self.open_device(stream=True, **info)
|
camera = self.open_device(stream=True, **info)
|
||||||
|
|
||||||
camera.stream.sock = sock
|
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:
|
finally:
|
||||||
self._cleanup_stream(camera, server_socket, sock)
|
self._cleanup_stream(camera, server_socket, sock)
|
||||||
self.logger.info('Stopped camera stream')
|
self.logger.info('Stopped camera stream')
|
||||||
|
@ -586,21 +732,23 @@ class CameraPlugin(Plugin, ABC):
|
||||||
try:
|
try:
|
||||||
client.close()
|
client.close()
|
||||||
except Exception as e:
|
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:
|
try:
|
||||||
server_socket.close()
|
server_socket.close()
|
||||||
except Exception as e:
|
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:
|
if camera.stream:
|
||||||
try:
|
try:
|
||||||
camera.stream.close()
|
camera.stream.close()
|
||||||
except Exception as e:
|
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
|
@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.
|
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)
|
camera = self.open_device(stream=True, stream_format=stream_format, **camera)
|
||||||
return self._start_streaming(camera, duration, stream_format)
|
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 camera.info.listen_port, 'No listen_port specified/configured'
|
||||||
assert not camera.stream_event.is_set() and camera.info.device not in self._streams, \
|
assert (
|
||||||
'A streaming session is already running for device {}'.format(camera.info.device)
|
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
|
self._streams[camera.info.device] = camera
|
||||||
camera.stream_event.set()
|
camera.stream_event.set()
|
||||||
|
|
||||||
camera.stream_thread = threading.Thread(target=self.streaming_thread, kwargs=dict(
|
camera.stream_thread = threading.Thread(
|
||||||
camera=camera, duration=duration, stream_format=stream_format))
|
target=self.streaming_thread,
|
||||||
|
kwargs={
|
||||||
|
'camera': camera,
|
||||||
|
'duration': duration,
|
||||||
|
'stream_format': stream_format,
|
||||||
|
},
|
||||||
|
)
|
||||||
camera.stream_thread.start()
|
camera.stream_thread.start()
|
||||||
return self.status(camera.info.device)
|
return self.status(camera.info.device)
|
||||||
|
|
||||||
|
@ -655,10 +812,17 @@ class CameraPlugin(Plugin, ABC):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**camera.info.to_dict(),
|
**camera.info.to_dict(),
|
||||||
'active': True if camera.capture_thread and camera.capture_thread.is_alive() else False,
|
'active': bool(camera.capture_thread and camera.capture_thread.is_alive()),
|
||||||
'capturing': True if camera.capture_thread and camera.capture_thread.is_alive() and
|
'capturing': bool(
|
||||||
camera.start_event.is_set() else False,
|
camera.capture_thread
|
||||||
'streaming': camera.stream_thread and camera.stream_thread.is_alive() and camera.stream_event.is_set(),
|
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
|
@action
|
||||||
|
@ -670,10 +834,7 @@ class CameraPlugin(Plugin, ABC):
|
||||||
if device:
|
if device:
|
||||||
return self._status(device)
|
return self._status(device)
|
||||||
|
|
||||||
return {
|
return {id: self._status(device) for id, camera in self._devices.items()}
|
||||||
id: self._status(device)
|
|
||||||
for id, camera in self._devices.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def transform_frame(frame, color_transform):
|
def transform_frame(frame, color_transform):
|
||||||
|
@ -690,6 +851,7 @@ class CameraPlugin(Plugin, ABC):
|
||||||
:param frame: Image frame (default: a ``PIL.Image`` object).
|
:param frame: Image frame (default: a ``PIL.Image`` object).
|
||||||
"""
|
"""
|
||||||
from PIL import ImageOps
|
from PIL import ImageOps
|
||||||
|
|
||||||
return ImageOps.grayscale(frame)
|
return ImageOps.grayscale(frame)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -724,7 +886,9 @@ class CameraPlugin(Plugin, ABC):
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
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.
|
:param scale_y: Y-scale factor.
|
||||||
"""
|
"""
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
if not (scale_x and scale_y) or (scale_x == 1 and scale_y == 1):
|
if not (scale_x and scale_y) or (scale_x == 1 and scale_y == 1):
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ class VideoWriter(ABC):
|
||||||
"""
|
"""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
Context manager-based interface.
|
Context manager-based interface.
|
||||||
"""
|
"""
|
||||||
|
@ -60,8 +60,9 @@ class FileVideoWriter(VideoWriter, ABC):
|
||||||
"""
|
"""
|
||||||
Abstract class to handle frames-to-video file operations.
|
Abstract class to handle frames-to-video file operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, output_file: str, **kwargs):
|
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))
|
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.
|
Abstract class for camera streaming operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, sock: Optional[IO] = None, **kwargs):
|
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: Optional[bytes] = None
|
||||||
self.frame_time: Optional[float] = None
|
self.frame_time: Optional[float] = None
|
||||||
self.buffer = io.BytesIO()
|
self.buffer = io.BytesIO()
|
||||||
|
@ -117,16 +119,20 @@ class StreamWriter(VideoWriter, ABC):
|
||||||
try:
|
try:
|
||||||
self.sock.close()
|
self.sock.close()
|
||||||
except Exception as e:
|
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()
|
super().close()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_class_by_name(name: str):
|
def get_class_by_name(name: str):
|
||||||
from platypush.plugins.camera.model.writer.index import StreamHandlers
|
from platypush.plugins.camera.model.writer.index import StreamHandlers
|
||||||
|
|
||||||
name = name.upper()
|
name = name.upper()
|
||||||
assert hasattr(StreamHandlers, name), 'No such stream handler: {}. Supported types: {}'.format(
|
assert hasattr(
|
||||||
name, [hndl.name for hndl in list(StreamHandlers)])
|
StreamHandlers, name
|
||||||
|
), f'No such stream handler: {name}. Supported types: ' + (
|
||||||
|
', '.join([hndl.name for hndl in list(StreamHandlers)])
|
||||||
|
)
|
||||||
|
|
||||||
return getattr(StreamHandlers, name).value
|
return getattr(StreamHandlers, name).value
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,11 @@ from typing import Optional, Tuple
|
||||||
from PIL.Image import Image
|
from PIL.Image import Image
|
||||||
|
|
||||||
from platypush.plugins.camera.model.camera import Camera
|
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):
|
class FFmpegWriter(VideoWriter, ABC):
|
||||||
|
@ -16,9 +20,17 @@ class FFmpegWriter(VideoWriter, ABC):
|
||||||
Generic FFmpeg encoder for camera frames.
|
Generic FFmpeg encoder for camera frames.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, input_file: str = '-', input_format: str = 'rawvideo', output_file: str = '-',
|
def __init__(
|
||||||
output_format: Optional[str] = None, pix_fmt: Optional[str] = None,
|
self,
|
||||||
output_opts: Optional[Tuple] = None, **kwargs):
|
*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)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.input_file = input_file
|
self.input_file = input_file
|
||||||
|
@ -29,21 +41,34 @@ class FFmpegWriter(VideoWriter, ABC):
|
||||||
self.pix_fmt = pix_fmt
|
self.pix_fmt = pix_fmt
|
||||||
self.output_opts = output_opts or ()
|
self.output_opts = output_opts or ()
|
||||||
|
|
||||||
self.logger.info('Starting FFmpeg. Command: {}'.format(' '.join(self.ffmpeg_args)))
|
self.logger.info('Starting FFmpeg. Command: ' + ' '.join(self.ffmpeg_args))
|
||||||
self.ffmpeg = subprocess.Popen(self.ffmpeg_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
self.ffmpeg = subprocess.Popen(
|
||||||
|
self.ffmpeg_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ffmpeg_args(self):
|
def ffmpeg_args(self):
|
||||||
return [self.camera.info.ffmpeg_bin, '-y',
|
return [
|
||||||
'-f', self.input_format,
|
self.camera.info.ffmpeg_bin,
|
||||||
|
'-y',
|
||||||
|
'-f',
|
||||||
|
self.input_format,
|
||||||
*(('-pix_fmt', self.pix_fmt) if self.pix_fmt else ()),
|
*(('-pix_fmt', self.pix_fmt) if self.pix_fmt else ()),
|
||||||
'-s', '{}x{}'.format(self.width, self.height),
|
'-s',
|
||||||
'-r', str(self.camera.info.fps),
|
f'{self.width}x{self.height}',
|
||||||
'-i', self.input_file,
|
'-r',
|
||||||
|
str(self.camera.info.fps),
|
||||||
|
'-i',
|
||||||
|
self.input_file,
|
||||||
*(('-f', self.output_format) if self.output_format else ()),
|
*(('-f', self.output_format) if self.output_format else ()),
|
||||||
*self.output_opts,
|
*self.output_opts,
|
||||||
*(('-vcodec', self.camera.info.output_codec) if self.camera.info.output_codec else ()),
|
*(
|
||||||
self.output_file]
|
('-vcodec', self.camera.info.output_codec)
|
||||||
|
if self.camera.info.output_codec
|
||||||
|
else ()
|
||||||
|
),
|
||||||
|
self.output_file,
|
||||||
|
]
|
||||||
|
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
return self.closed or not self.ffmpeg or self.ffmpeg.poll() is not None
|
return self.closed or not self.ffmpeg or self.ffmpeg.poll() is not None
|
||||||
|
@ -55,7 +80,7 @@ class FFmpegWriter(VideoWriter, ABC):
|
||||||
try:
|
try:
|
||||||
self.ffmpeg.stdin.write(image.convert('RGB').tobytes())
|
self.ffmpeg.stdin.write(image.convert('RGB').tobytes())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('FFmpeg send error: {}'.format(str(e)))
|
self.logger.warning('FFmpeg send error: %s', e)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
@ -63,7 +88,7 @@ class FFmpegWriter(VideoWriter, ABC):
|
||||||
if self.ffmpeg and self.ffmpeg.stdin:
|
if self.ffmpeg and self.ffmpeg.stdin:
|
||||||
try:
|
try:
|
||||||
self.ffmpeg.stdin.close()
|
self.ffmpeg.stdin.close()
|
||||||
except (IOError, OSError):
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.ffmpeg:
|
if self.ffmpeg:
|
||||||
|
@ -77,7 +102,7 @@ class FFmpegWriter(VideoWriter, ABC):
|
||||||
if self.ffmpeg and self.ffmpeg.stdout:
|
if self.ffmpeg and self.ffmpeg.stdout:
|
||||||
try:
|
try:
|
||||||
self.ffmpeg.stdout.close()
|
self.ffmpeg.stdout.close()
|
||||||
except (IOError, OSError):
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.ffmpeg = None
|
self.ffmpeg = None
|
||||||
|
@ -98,10 +123,26 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
Stream camera frames using FFmpeg.
|
Stream camera frames using FFmpeg.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, output_format: str, output_opts: Optional[Tuple] = None, **kwargs):
|
def __init__(
|
||||||
super().__init__(*args, pix_fmt='rgb24', output_format=output_format, output_opts=output_opts or (
|
self, *args, output_format: str, output_opts: Optional[Tuple] = None, **kwargs
|
||||||
'-tune', 'zerolatency', '-preset', 'superfast', '-trellis', '0',
|
):
|
||||||
'-fflags', 'nobuffer'), **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 = threading.Thread(target=self._reader_thread)
|
||||||
self._reader.start()
|
self._reader.start()
|
||||||
|
|
||||||
|
@ -115,7 +156,7 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
try:
|
try:
|
||||||
data = self.ffmpeg.stdout.read(1 << 15)
|
data = self.ffmpeg.stdout.read(1 << 15)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('FFmpeg reader error: {}'.format(str(e)))
|
self.logger.warning('FFmpeg reader error: %s', e)
|
||||||
break
|
break
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
@ -123,7 +164,7 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
|
|
||||||
if self.frame is None:
|
if self.frame is None:
|
||||||
latency = time.time() - start_time
|
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:
|
with self.ready:
|
||||||
self.frame = data
|
self.frame = data
|
||||||
|
@ -140,12 +181,16 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
try:
|
try:
|
||||||
self.ffmpeg.stdin.write(data)
|
self.ffmpeg.stdin.write(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('FFmpeg send error: {}'.format(str(e)))
|
self.logger.warning('FFmpeg send error: %s', e)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
super().close()
|
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.join(timeout=5.0)
|
||||||
self._reader = None
|
self._reader = None
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue