Compare commits

...

2 commits

6 changed files with 54 additions and 79 deletions

View file

@ -40,8 +40,7 @@
<div class="audio-container"> <div class="audio-container">
<audio autoplay preload="none" ref="player" v-if="audioOn"> <audio autoplay preload="none" ref="player" v-if="audioOn">
<!--suppress HtmlUnknownTarget --> <source :src="`/sound/stream.aac?t=${(new Date()).getTime()}`">
<source :src="`/sound/stream?t=${(new Date()).getTime()}`" type="audio/x-wav;codec=pcm">
Your browser does not support audio elements Your browser does not support audio elements
</audio> </audio>
</div> </div>

View file

@ -2,8 +2,7 @@
<div class="sound"> <div class="sound">
<div class="sound-container"> <div class="sound-container">
<audio autoplay preload="none" ref="player" v-if="recording"> <audio autoplay preload="none" ref="player" v-if="recording">
<!--suppress HtmlUnknownTarget --> <source :src="`/sound/stream.aac?t=${(new Date()).getTime()}`">
<source :src="`/sound/stream?t=${(new Date()).getTime()}`" type="audio/x-wav;codec=pcm">
Your browser does not support audio elements Your browser does not support audio elements
</audio> </audio>
</div> </div>

View file

@ -7,6 +7,7 @@ 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 datetime import datetime from datetime import datetime
from multiprocessing import Process from multiprocessing import Process
from queue import Queue from queue import Queue
@ -152,21 +153,21 @@ class CameraPlugin(Plugin, ABC):
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
self.workdir = os.path.join( workdir = Config.get('workdir')
Config.get('workdir'), get_plugin_name_by_class(self) plugin_name = get_plugin_name_by_class(self)
) assert isinstance(workdir, str) and plugin_name
self.workdir = os.path.join(workdir, plugin_name)
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
self.camera_info = self._camera_info_class( self.camera_info = self._camera_info_class(
device, device,
color_transform=color_transform, color_transform=color_transform,
warmup_frames=warmup_frames, warmup_frames=warmup_frames,
warmup_seconds=warmup_seconds, warmup_seconds=warmup_seconds or 0,
rotate=rotate, rotate=rotate,
scale_x=scale_x, scale_x=scale_x,
scale_y=scale_y, scale_y=scale_y,
capture_timeout=capture_timeout, capture_timeout=capture_timeout or 20,
fps=fps, fps=fps,
input_format=input_format, input_format=input_format,
output_format=output_format, output_format=output_format,
@ -227,13 +228,12 @@ class CameraPlugin(Plugin, ABC):
camera.start_event.clear() camera.start_event.clear()
camera.capture_thread = None camera.capture_thread = None
else: else:
# noinspection PyArgumentList
camera = self._camera_class(info=info) camera = self._camera_class(info=info)
camera.info.set(**params) camera.info.set(**params)
camera.object = self.prepare_device(camera) camera.object = self.prepare_device(camera)
if stream: if stream and camera.info.stream_format:
writer_class = StreamWriter.get_class_by_name(camera.info.stream_format) writer_class = StreamWriter.get_class_by_name(camera.info.stream_format)
camera.stream = writer_class( camera.stream = writer_class(
camera=camera, plugin=self, redis_queue=redis_queue camera=camera, plugin=self, redis_queue=redis_queue
@ -283,7 +283,7 @@ class CameraPlugin(Plugin, ABC):
def open( def open(
self, self,
device: Optional[Union[int, str]] = None, device: Optional[Union[int, str]] = None,
stream: bool = None, stream: bool = False,
redis_queue: Optional[str] = None, redis_queue: Optional[str] = None,
**info, **info,
) -> Generator[Camera, None, None]: ) -> Generator[Camera, None, None]:
@ -304,7 +304,8 @@ class CameraPlugin(Plugin, ABC):
) )
yield camera yield camera
finally: finally:
self.close_device(camera) if camera:
self.close_device(camera)
@abstractmethod @abstractmethod
def prepare_device(self, device: Camera): def prepare_device(self, device: Camera):
@ -333,7 +334,6 @@ class CameraPlugin(Plugin, ABC):
""" """
raise NotImplementedError() raise NotImplementedError()
# noinspection PyShadowingBuiltins
@staticmethod @staticmethod
def store_frame(frame, filepath: str, format: Optional[str] = None): def store_frame(frame, filepath: str, format: Optional[str] = None):
""" """
@ -347,7 +347,7 @@ class CameraPlugin(Plugin, ABC):
if isinstance(frame, bytes): if isinstance(frame, bytes):
frame = list(frame) frame = list(frame)
elif not isinstance(frame, Image.Image): if not isinstance(frame, Image.Image):
frame = Image.fromarray(frame) frame = Image.fromarray(frame)
save_args = {} save_args = {}
@ -571,7 +571,7 @@ class CameraPlugin(Plugin, ABC):
video_file: Optional[str] = None, video_file: Optional[str] = None,
preview: bool = False, preview: bool = False,
**camera, **camera,
) -> Union[str, dict]: ) -> Optional[Union[str, dict]]:
""" """
Capture a video. Capture a video.
@ -596,7 +596,7 @@ class CameraPlugin(Plugin, ABC):
self.wait_capture(camera) self.wait_capture(camera)
return video_file return video_file
return self.status(camera.info.device) return self.status(camera.info.device).output
@action @action
def stop_capture(self, device: Optional[Union[int, str]] = None): def stop_capture(self, device: Optional[Union[int, str]] = None):
@ -606,12 +606,12 @@ class CameraPlugin(Plugin, ABC):
:param device: Name/path/ID of the device to stop (default: all the active devices). :param device: Name/path/ID of the device to stop (default: all the active devices).
""" """
devices = self._devices.copy() devices = self._devices.copy()
stop_devices = list(devices.values())[:] stop_devices = list(devices.values())
if device: if device:
stop_devices = [self._devices[device]] if device in self._devices else [] stop_devices = [self._devices[device]] if device in self._devices else []
for device in stop_devices: for dev in stop_devices:
self.close_device(device) 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, preview: bool = False, **camera) -> str:
@ -635,9 +635,8 @@ class CameraPlugin(Plugin, ABC):
return image_file return image_file
# noinspection PyUnusedLocal
@action @action
def take_picture(self, image_file: str, preview: bool = False, **camera) -> str: def take_picture(self, image_file: str, **camera) -> str:
""" """
Alias for :meth:`.capture_image`. Alias for :meth:`.capture_image`.
@ -646,7 +645,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 local path to the saved image. :return: The local path to the saved image.
""" """
return self.capture_image(image_file, **camera) return str(self.capture_image(image_file, **camera).output)
@action @action
def capture_sequence( def capture_sequence(
@ -655,7 +654,7 @@ class CameraPlugin(Plugin, ABC):
n_frames: Optional[int] = None, n_frames: Optional[int] = None,
preview: bool = False, preview: bool = False,
**camera, **camera,
) -> str: ) -> Optional[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.
@ -687,7 +686,7 @@ class CameraPlugin(Plugin, ABC):
""" """
camera = self.open_device(frames_dir=None, **camera) camera = self.open_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) return self.status(camera.info.device) # type: ignore
@staticmethod @staticmethod
def _prepare_server_socket(camera: Camera) -> socket.socket: def _prepare_server_socket(camera: Camera) -> socket.socket:
@ -729,10 +728,11 @@ class CameraPlugin(Plugin, ABC):
continue continue
if camera.info.device not in self._devices: if camera.info.device not in self._devices:
info = camera.info.to_dict() info = asdict(camera.info)
info['stream_format'] = stream_format info['stream_format'] = stream_format
camera = self.open_device(stream=True, **info) camera = self.open_device(stream=True, **info)
assert camera.stream, 'No camera stream available'
camera.stream.sock = sock camera.stream.sock = sock
self.start_camera( self.start_camera(
camera, duration=duration, frames_dir=None, image_file=None camera, duration=duration, frames_dir=None, image_file=None
@ -741,7 +741,9 @@ class CameraPlugin(Plugin, ABC):
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')
def _cleanup_stream(self, camera: Camera, server_socket: socket.socket, client: IO): def _cleanup_stream(
self, camera: Camera, server_socket: socket.socket, client: Optional[IO]
):
if client: if client:
try: try:
client.close() client.close()
@ -772,7 +774,7 @@ class CameraPlugin(Plugin, ABC):
: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(stream=True, stream_format=stream_format, **camera)
return self._start_streaming(camera, duration, stream_format) return self._start_streaming(camera, duration, stream_format) # type: ignore
def _start_streaming( def _start_streaming(
self, camera: Camera, duration: Optional[float], stream_format: str self, camera: Camera, duration: Optional[float], stream_format: str
@ -781,6 +783,7 @@ class CameraPlugin(Plugin, ABC):
assert ( assert (
not camera.stream_event.is_set() and camera.info.device not in self._streams 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}' ), f'A streaming session is already running for device {camera.info.device}'
assert camera.info.device, 'No device name available'
self._streams[camera.info.device] = camera self._streams[camera.info.device] = camera
camera.stream_event.set() camera.stream_event.set()
@ -804,12 +807,12 @@ class CameraPlugin(Plugin, ABC):
:param device: Name/path/ID of the device to stop (default: all the active devices). :param device: Name/path/ID of the device to stop (default: all the active devices).
""" """
streams = self._streams.copy() streams = self._streams.copy()
stop_devices = list(streams.values())[:] stop_devices = list(streams.values())
if device: if device:
stop_devices = [self._streams[device]] if device in self._streams else [] stop_devices = [self._streams[device]] if device in self._streams else []
for device in stop_devices: for dev in stop_devices:
self._stop_streaming(device) self._stop_streaming(dev)
def _stop_streaming(self, camera: Camera): def _stop_streaming(self, camera: Camera):
camera.stream_event.clear() camera.stream_event.clear()
@ -825,7 +828,7 @@ class CameraPlugin(Plugin, ABC):
return {} return {}
return { return {
**camera.info.to_dict(), **asdict(camera.info),
'active': bool(camera.capture_thread and camera.capture_thread.is_alive()), 'active': bool(camera.capture_thread and camera.capture_thread.is_alive()),
'capturing': bool( 'capturing': bool(
camera.capture_thread camera.capture_thread

View file

@ -1,6 +1,6 @@
import math import math
import threading import threading
from dataclasses import dataclass from dataclasses import asdict, dataclass
from typing import Optional, Union, Tuple, Set from typing import Optional, Union, Tuple, Set
import numpy as np import numpy as np
@ -17,8 +17,8 @@ from platypush.plugins.camera.model.writer.preview import PreviewWriter
class CameraInfo: class CameraInfo:
device: Optional[Union[int, str]] device: Optional[Union[int, str]]
bind_address: Optional[str] = None bind_address: Optional[str] = None
capture_timeout: float = 20.0 capture_timeout: float = 0
color_transform: Optional[str] = None color_transform: Optional[Union[int, str]] = None
ffmpeg_bin: Optional[str] = None ffmpeg_bin: Optional[str] = None
fps: Optional[float] = None fps: Optional[float] = None
frames_dir: Optional[str] = None frames_dir: Optional[str] = None
@ -36,42 +36,15 @@ class CameraInfo:
stream_format: Optional[str] = None stream_format: Optional[str] = None
vertical_flip: bool = False vertical_flip: bool = False
warmup_frames: int = 0 warmup_frames: int = 0
warmup_seconds: float = 0.0 warmup_seconds: float = 0
def set(self, **kwargs): def set(self, **kwargs):
for k, v in kwargs.items(): for k, v in kwargs.items():
if hasattr(self, k): if hasattr(self, k):
setattr(self, k, v) setattr(self, k, v)
def to_dict(self) -> dict:
return {
'bind_address': self.bind_address,
'capture_timeout': self.capture_timeout,
'color_transform': self.color_transform,
'device': self.device,
'ffmpeg_bin': self.ffmpeg_bin,
'fps': self.fps,
'frames_dir': self.frames_dir,
'grayscale': self.grayscale,
'horizontal_flip': self.horizontal_flip,
'input_codec': self.input_codec,
'input_format': self.input_format,
'listen_port': self.listen_port,
'output_codec': self.output_codec,
'output_format': self.output_format,
'resolution': list(self.resolution or ()),
'rotate': self.rotate,
'scale_x': self.scale_x,
'scale_y': self.scale_y,
'stream_format': self.stream_format,
'vertical_flip': self.vertical_flip,
'warmup_frames': self.warmup_frames,
'warmup_seconds': self.warmup_seconds,
}
def clone(self): def clone(self):
# noinspection PyArgumentList return self.__class__(**asdict(self))
return self.__class__(**self.to_dict())
@dataclass @dataclass

View file

@ -1,5 +1,3 @@
from dataclasses import dataclass
from typing import IO from typing import IO
from typing_extensions import override from typing_extensions import override
@ -16,7 +14,6 @@ from .._model import AudioState
from ._base import AudioThread from ._base import AudioThread
@dataclass
class AudioRecorder(AudioThread): class AudioRecorder(AudioThread):
""" """
The ``AudioRecorder`` thread is responsible for recording audio from the The ``AudioRecorder`` thread is responsible for recording audio from the
@ -32,7 +29,7 @@ class AudioRecorder(AudioThread):
# _ = frames # _ = frames
# __ = time # __ = time
def callback(indata, outdata, _, __, status): def callback(indata, outdata, _, __, status):
if self.state == AudioState.PAUSED: if self.state != AudioState.RUNNING:
return return
if status: if status:
@ -41,7 +38,7 @@ class AudioRecorder(AudioThread):
try: try:
audio_converter.write(indata.tobytes()) audio_converter.write(indata.tobytes())
except AssertionError as e: except AssertionError as e:
self.logger.warning('Audio recorder callback error: %s', e) self.logger.warning('Audio converter callback error: %s', e)
self.state = AudioState.STOPPED self.state = AudioState.STOPPED
return return
@ -63,8 +60,10 @@ class AudioRecorder(AudioThread):
@override @override
def notify_stop(self): def notify_stop(self):
prev_state = self.state
super().notify_stop() super().notify_stop()
get_bus().post(SoundRecordingStoppedEvent()) if prev_state != AudioState.STOPPED:
get_bus().post(SoundRecordingStoppedEvent())
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
from asyncio.subprocess import PIPE from asyncio.subprocess import PIPE
from logging import getLogger
from queue import Empty from queue import Empty
from queue import Queue from queue import Queue
@ -74,6 +75,7 @@ class ConverterProcess(Thread):
self._closed = False self._closed = False
self._out_queue = Queue() self._out_queue = Queue()
self.ffmpeg = None self.ffmpeg = None
self.logger = getLogger(__name__)
self._loop = None self._loop = None
self._should_stop = Event() self._should_stop = Event()
self._stop_lock = RLock() self._stop_lock = RLock()
@ -157,21 +159,21 @@ class ConverterProcess(Thread):
def run(self): def run(self):
super().run() super().run()
self._loop = get_or_create_event_loop() self._loop = get_or_create_event_loop()
self._loop.run_until_complete(self._audio_proxy(timeout=1)) try:
self._loop.run_until_complete(self._audio_proxy(timeout=1))
except RuntimeError as e:
self.logger.warning(e)
finally:
self.stop()
def stop(self): def stop(self):
with self._stop_lock: with self._stop_lock:
self._should_stop.set() self._should_stop.set()
if self.ffmpeg: if self.ffmpeg:
try: self.ffmpeg.kill()
self.ffmpeg.kill()
except ProcessLookupError:
pass
self.ffmpeg = None self.ffmpeg = None
self._loop = None
if self._loop:
self._loop = None
@property @property
def should_stop(self) -> bool: def should_stop(self) -> bool: