Added GStreamer camera plugin [relates to #151]
This commit is contained in:
parent
b30145dfc9
commit
0a9c4fc3a7
10 changed files with 216 additions and 4 deletions
5
docs/source/platypush/plugins/camera.gstreamer.rst
Normal file
5
docs/source/platypush/plugins/camera.gstreamer.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.plugins.camera.gstreamer``
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.camera.gstreamer
|
||||||
|
:members:
|
|
@ -22,6 +22,7 @@ Plugins
|
||||||
platypush/plugins/camera.android.ipcam.rst
|
platypush/plugins/camera.android.ipcam.rst
|
||||||
platypush/plugins/camera.cv.rst
|
platypush/plugins/camera.cv.rst
|
||||||
platypush/plugins/camera.ffmpeg.rst
|
platypush/plugins/camera.ffmpeg.rst
|
||||||
|
platypush/plugins/camera.gstreamer.rst
|
||||||
platypush/plugins/camera.ir.mlx90640.rst
|
platypush/plugins/camera.ir.mlx90640.rst
|
||||||
platypush/plugins/camera.pi.rst
|
platypush/plugins/camera.pi.rst
|
||||||
platypush/plugins/chat.telegram.rst
|
platypush/plugins/chat.telegram.rst
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
camera
|
|
@ -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');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
'camera.android.ipcam': 'fab fa-android',
|
'camera.android.ipcam': 'fab fa-android',
|
||||||
'camera.cv': 'fas fa-camera',
|
'camera.cv': 'fas fa-camera',
|
||||||
'camera.ffmpeg': 'fas fa-camera',
|
'camera.ffmpeg': 'fas fa-camera',
|
||||||
|
'camera.gstreamer': 'fas fa-camera',
|
||||||
'camera.pi': 'fab fa-raspberry-pi',
|
'camera.pi': 'fab fa-raspberry-pi',
|
||||||
'camera.ir.mlx90640': 'fas fa-sun',
|
'camera.ir.mlx90640': 'fas fa-sun',
|
||||||
'execute': 'fas fa-play',
|
'execute': 'fas fa-play',
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='js/plugins/camera/index.js') }}"></script>
|
||||||
|
|
||||||
|
<script type="text/x-template" id="tmpl-camera-gstreamer">
|
||||||
|
{% include 'plugins/camera/index.html' %}
|
||||||
|
</script>
|
||||||
|
|
64
platypush/plugins/camera/gstreamer/__init__.py
Normal file
64
platypush/plugins/camera/gstreamer/__init__.py
Normal file
|
@ -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:
|
119
platypush/plugins/camera/gstreamer/model.py
Normal file
119
platypush/plugins/camera/gstreamer/model.py
Normal file
|
@ -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:
|
|
@ -300,8 +300,8 @@ croniter
|
||||||
# Support for NextCloud integration
|
# Support for NextCloud integration
|
||||||
# git+https://github.com/EnterpriseyIntranet/nextcloud-API.git
|
# git+https://github.com/EnterpriseyIntranet/nextcloud-API.git
|
||||||
|
|
||||||
# Support for FFmpeg integration
|
# Support for GStreamer integration
|
||||||
# ffmpeg-python
|
# gst-python
|
||||||
|
|
||||||
# Generic support for cameras
|
# Generic support for cameras
|
||||||
# Pillow
|
# Pillow
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -336,7 +336,7 @@ setup(
|
||||||
'imap': ['imapclient'],
|
'imap': ['imapclient'],
|
||||||
# Support for NextCloud integration
|
# Support for NextCloud integration
|
||||||
'nextcloud': ['nextcloud-API @ git+https://github.com/EnterpriseyIntranet/nextcloud-API.git'],
|
'nextcloud': ['nextcloud-API @ git+https://github.com/EnterpriseyIntranet/nextcloud-API.git'],
|
||||||
# Support for FFmpeg integration
|
# Support for GStreamer integration
|
||||||
'ffmpeg': ['ffmpeg-python'],
|
'gstreamer': ['gst-python'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue