diff --git a/docs/source/platypush/plugins/camera.gstreamer.rst b/docs/source/platypush/plugins/camera.gstreamer.rst new file mode 100644 index 00000000..e84d71e0 --- /dev/null +++ b/docs/source/platypush/plugins/camera.gstreamer.rst @@ -0,0 +1,5 @@ +``platypush.plugins.camera.gstreamer`` +====================================== + +.. automodule:: platypush.plugins.camera.gstreamer + :members: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 35374e5f..cd63a43e 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -22,6 +22,7 @@ Plugins platypush/plugins/camera.android.ipcam.rst platypush/plugins/camera.cv.rst platypush/plugins/camera.ffmpeg.rst + platypush/plugins/camera.gstreamer.rst platypush/plugins/camera.ir.mlx90640.rst platypush/plugins/camera.pi.rst platypush/plugins/chat.telegram.rst diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/camera.gstreamer b/platypush/backend/http/static/css/source/webpanel/plugins/camera.gstreamer new file mode 120000 index 00000000..5f839334 --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/camera.gstreamer @@ -0,0 +1 @@ +camera \ No newline at end of file diff --git a/platypush/backend/http/static/js/plugins/camera.gstreamer/index.js b/platypush/backend/http/static/js/plugins/camera.gstreamer/index.js new file mode 100644 index 00000000..59afd23b --- /dev/null +++ b/platypush/backend/http/static/js/plugins/camera.gstreamer/index.js @@ -0,0 +1,15 @@ +Vue.component('camera-gstreamer', { + template: '#tmpl-camera-gstreamer', + mixins: [cameraMixin], + + methods: { + startStreaming: function() { + this._startStreaming('gstreamer'); + }, + + capture: function() { + this._capture('gstreamer'); + }, + }, +}); + diff --git a/platypush/backend/http/templates/nav.html b/platypush/backend/http/templates/nav.html index 9a6c61b6..6da9126e 100644 --- a/platypush/backend/http/templates/nav.html +++ b/platypush/backend/http/templates/nav.html @@ -4,6 +4,7 @@ 'camera.android.ipcam': 'fab fa-android', 'camera.cv': 'fas fa-camera', 'camera.ffmpeg': 'fas fa-camera', + 'camera.gstreamer': 'fas fa-camera', 'camera.pi': 'fab fa-raspberry-pi', 'camera.ir.mlx90640': 'fas fa-sun', 'execute': 'fas fa-play', diff --git a/platypush/backend/http/templates/plugins/camera.gstreamer/index.html b/platypush/backend/http/templates/plugins/camera.gstreamer/index.html new file mode 100644 index 00000000..60510169 --- /dev/null +++ b/platypush/backend/http/templates/plugins/camera.gstreamer/index.html @@ -0,0 +1,6 @@ + + + + diff --git a/platypush/plugins/camera/gstreamer/__init__.py b/platypush/plugins/camera/gstreamer/__init__.py new file mode 100644 index 00000000..7ef6bf62 --- /dev/null +++ b/platypush/plugins/camera/gstreamer/__init__.py @@ -0,0 +1,64 @@ +from typing import Optional + +from PIL import Image +from PIL.Image import Image as ImageType + +from platypush.plugins.camera import CameraPlugin +from platypush.plugins.camera.gstreamer.model import GStreamerCamera, Pipeline, Loop + + +class CameraGstreamerPlugin(CameraPlugin): + """ + Plugin to interact with a camera over GStreamer. + + Requires: + + * **gst-python** (``pip install gst-python``) + + """ + + _camera_class = GStreamerCamera + + def __init__(self, device: Optional[str] = '/dev/video0', **opts): + """ + :param device: Path to the camera device (default ``/dev/video0``). + :param opts: Camera options - see constructor of :class:`platypush.plugins.camera.CameraPlugin`. + """ + super().__init__(device=device, **opts) + + def prepare_device(self, camera: GStreamerCamera) -> Pipeline: + pipeline = Pipeline() + src = pipeline.add('v4l2src', device=camera.info.device) + convert = pipeline.add('videoconvert') + video_filter = pipeline.add( + 'capsfilter', caps='video/x-raw,format=RGB,width={width},height={height},framerate={fps}/1'.format( + width=camera.info.resolution[0], height=camera.info.resolution[1], fps=camera.info.fps)) + + sink = pipeline.add_sink('appsink', name='appsink', sync=False) + pipeline.link(src, convert, video_filter, sink) + return pipeline + + def start_camera(self, camera: GStreamerCamera, preview: bool = False, *args, **kwargs): + super().start_camera(*args, camera=camera, preview=preview, **kwargs) + if camera.object: + camera.object.play() + + def release_device(self, camera: GStreamerCamera): + if camera.object: + camera.object.stop() + + def capture_frame(self, camera: GStreamerCamera, *args, **kwargs) -> Optional[ImageType]: + timed_out = not camera.object.data_ready.wait(timeout=5 + (1. / camera.info.fps)) + if timed_out: + self.logger.warning('Frame capture timeout') + return + + data = camera.object.data + camera.object.data_ready.clear() + if not data and len(data) != camera.info.resolution[0] * camera.info.resolution[1] * 3: + return + + return Image.frombytes('RGB', camera.info.resolution, data) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/camera/gstreamer/model.py b/platypush/plugins/camera/gstreamer/model.py new file mode 100644 index 00000000..91c3734d --- /dev/null +++ b/platypush/plugins/camera/gstreamer/model.py @@ -0,0 +1,119 @@ +import logging +import threading +from dataclasses import dataclass + +from platypush.plugins.camera import CameraInfo, Camera + +# noinspection PyPackageRequirements +import gi +gi.require_version('Gst', '1.0') +gi.require_version('GstApp', '1.0') + +# noinspection PyPackageRequirements,PyUnresolvedReferences +from gi.repository import GLib, Gst, GstApp + +Gst.init(None) + + +class Pipeline: + def __init__(self): + self.pipeline = Gst.Pipeline() + self.logger = logging.getLogger('gst-pipeline') + self.loop = Loop() + self.sink = None + + self.bus = self.pipeline.get_bus() + self.bus.add_signal_watch() + self.bus.connect('message::eos', self.on_eos) + self.bus.connect('message::error', self.on_error) + self.data_ready = threading.Event() + self.data = None + + def add(self, element_name: str, *args, **props): + el = Gst.ElementFactory.make(element_name, *args) + for k, v in props.items(): + if k == 'caps': + v = Gst.caps_from_string(v) + el.set_property(k, v) + + self.pipeline.add(el) + return el + + def add_sink(self, element_name: str, *args, **props): + assert not self.sink, 'A sink element is already set for this pipeline' + sink = self.add(element_name, *args, **props) + sink.connect('new-sample', self.on_buffer) + sink.set_property('emit-signals', True) + self.sink = sink + return sink + + @staticmethod + def link(*elements): + for i, el in enumerate(elements): + if i == len(elements)-1: + break + el.link(elements[i+1]) + + def emit(self, signal, *args, **kwargs): + return self.pipeline.emit(signal, *args, **kwargs) + + def play(self): + assert self.sink, 'No sink element specified through add_sink()' + self.pipeline.set_state(Gst.State.PLAYING) + self.loop.start() + + def pause(self): + self.pipeline.set_state(Gst.State.PAUSED) + + def stop(self): + self.pipeline.set_state(Gst.State.NULL) + self.loop.stop() + + def on_buffer(self, sink): + sample = GstApp.AppSink.pull_sample(sink) + buffer = sample.get_buffer() + size, offset, maxsize = buffer.get_sizes() + self.data = buffer.extract_dup(offset, size) + self.data_ready.set() + return False + + def on_eos(self, *_, **__): + self.logger.info('End of stream event received') + self.stop() + + # noinspection PyUnusedLocal + def on_error(self, bus, msg): + self.logger.warning('GStreamer pipeline error: {}'.format(msg.parse_error())) + self.stop() + + def get_sink(self): + return self.sink + + +class Loop(threading.Thread): + def __init__(self): + super().__init__() + self._loop = GLib.MainLoop() + + def run(self): + self._loop.run() + + def is_running(self) -> bool: + return self.is_alive() or self._loop is not None + + def stop(self): + if not self.is_running(): + return + if self._loop: + self._loop.quit() + if threading.get_ident() != self.ident: + self.join() + self._loop = None + + +class GStreamerCamera(Camera): + info: CameraInfo + object: Pipeline + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index e44b8b86..094d1b88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -300,8 +300,8 @@ croniter # Support for NextCloud integration # git+https://github.com/EnterpriseyIntranet/nextcloud-API.git -# Support for FFmpeg integration -# ffmpeg-python +# Support for GStreamer integration +# gst-python # Generic support for cameras # Pillow diff --git a/setup.py b/setup.py index e015bac0..00b44873 100755 --- a/setup.py +++ b/setup.py @@ -336,7 +336,7 @@ setup( 'imap': ['imapclient'], # Support for NextCloud integration 'nextcloud': ['nextcloud-API @ git+https://github.com/EnterpriseyIntranet/nextcloud-API.git'], - # Support for FFmpeg integration - 'ffmpeg': ['ffmpeg-python'], + # Support for GStreamer integration + 'gstreamer': ['gst-python'], }, )