diff --git a/docs/source/platypush/plugins/camera.gstreamer.rst b/docs/source/platypush/plugins/camera.gstreamer.rst
new file mode 100644
index 0000000000..e84d71e025
--- /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 35374e5f47..cd63a43e5f 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 0000000000..5f839334fe
--- /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 0000000000..59afd23bd8
--- /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 9a6c61b6d9..6da9126e7e 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 0000000000..6051016935
--- /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 0000000000..7ef6bf62a3
--- /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 0000000000..91c3734d13
--- /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 e44b8b860e..094d1b8868 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 e015bac0fc..00b448739c 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'],
},
)