mirror of https://github.com/kivy/kivy.git
Merge pull request #1692 from kivy/fix-1689
Separate PyGST / Gi Gstreamer
This commit is contained in:
commit
6de7f1dcd0
|
@ -59,6 +59,55 @@ Cython. (Reference: http://mail.scipy.org/pipermail/nipy-devel/2011-March/005709
|
||||||
|
|
||||||
Solution: use `easy_install`, as our documentation said.
|
Solution: use `easy_install`, as our documentation said.
|
||||||
|
|
||||||
|
.. _gstreamer-compatibility:
|
||||||
|
|
||||||
|
GStreamer compatibility
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Starting from 1.8.0 version, Kivy now use by default the Gi bindings, on the
|
||||||
|
platforms that have Gi. We are still in a transition, as Gstreamer 0.10 is now
|
||||||
|
unmaintained by the Gstreamer team. But 1.0 is not accessible with Python
|
||||||
|
everywhere. Here is the compatibility table you can use.
|
||||||
|
|
||||||
|
================= ======== ====== =========================================
|
||||||
|
Gstreamer version Bindings Status Remarks
|
||||||
|
----------------- -------- ------ -----------------------------------------
|
||||||
|
0.10 pygst Works Lot of issues remain with 0.10
|
||||||
|
0.10 gi Buggy Internal issues with pygobject, and video
|
||||||
|
doesn't play.
|
||||||
|
1.0 pygst - No pygst bindings exists for 1.0
|
||||||
|
1.0 gi Works* Linux: works
|
||||||
|
OSX: works with brew
|
||||||
|
Windows: No python bindings available
|
||||||
|
================= ======== ====== =========================================
|
||||||
|
|
||||||
|
Also, we have no reliable way to check if you have 1.0 installed on your
|
||||||
|
system. Trying to import gi, and then pygst, will fail.
|
||||||
|
|
||||||
|
So currently:
|
||||||
|
|
||||||
|
- if you are on Windows: stay on Gstreamer 0.10 with pygst.
|
||||||
|
- if you are on OSX/Linux: install Gstreamer 1.0.x
|
||||||
|
- if you are on OSX/Linux and doesn't want to install 1.0:
|
||||||
|
`export KIVY_VIDEO=pygst`
|
||||||
|
|
||||||
|
If you are on OSX, Brew now have `pygobject3`. You must install it, and
|
||||||
|
re-install gstreamer with introspection options::
|
||||||
|
|
||||||
|
$ brew install pygobject3
|
||||||
|
$ brew install gstreamer --with-gobject-introspection
|
||||||
|
$ brew install gst-plugins-base --with-gobject-introspection
|
||||||
|
$ brew install gst-plugins-good --with-gobject-introspection
|
||||||
|
$ brew install gst-plugins-bad --with-gobject-introspection
|
||||||
|
$ brew install gst-plugins-ugly --with-gobject-introspection
|
||||||
|
|
||||||
|
# then add the gi into your PYTHONPATH (as they don't do it for you)
|
||||||
|
$ export PYTHONPATH=$PYTHONPATH:/usr/local/opt/pygobject3/lib/python2.7/site-packages
|
||||||
|
|
||||||
|
# test it
|
||||||
|
$ python -c 'import gi; from gi.repository import Gst; print Gst.version()'
|
||||||
|
(1L, 2L, 1L, 0L)
|
||||||
|
|
||||||
|
|
||||||
Android FAQ
|
Android FAQ
|
||||||
-----------
|
-----------
|
||||||
|
|
|
@ -188,10 +188,10 @@ else:
|
||||||
kivy_options = {
|
kivy_options = {
|
||||||
'window': ('egl_rpi', 'pygame', 'sdl', 'x11'),
|
'window': ('egl_rpi', 'pygame', 'sdl', 'x11'),
|
||||||
'text': ('pil', 'pygame', 'sdlttf'),
|
'text': ('pil', 'pygame', 'sdlttf'),
|
||||||
'video': ('ffmpeg', 'gstreamer', 'pyglet', 'null'),
|
'video': ('ffmpeg', 'gi', 'pygst', 'pyglet', 'null'),
|
||||||
'audio': ('pygame', 'gstreamer', 'sdl'),
|
'audio': ('pygame', 'gi', 'pygst', 'sdl'),
|
||||||
'image': ('tex', 'imageio', 'dds', 'gif', 'pil', 'pygame'),
|
'image': ('tex', 'imageio', 'dds', 'gif', 'pil', 'pygame'),
|
||||||
'camera': ('opencv', 'gstreamer', 'videocapture'),
|
'camera': ('opencv', 'gi', 'pygst', 'videocapture'),
|
||||||
'spelling': ('enchant', 'osxappkit', ),
|
'spelling': ('enchant', 'osxappkit', ),
|
||||||
'clipboard': ('android', 'pygame', 'dummy'), }
|
'clipboard': ('android', 'pygame', 'dummy'), }
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,14 @@ You should not use the Sound class directly. The class returned by
|
||||||
**SoundLoader.load** will be the best sound provider for that particular file
|
**SoundLoader.load** will be the best sound provider for that particular file
|
||||||
type, so it might return different Sound classes depending the file type.
|
type, so it might return different Sound classes depending the file type.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.8.0
|
||||||
|
|
||||||
|
There is now 2 distinct Gstreamer implementation: one using Gi/Gst working
|
||||||
|
for both Python 2+3 with Gstreamer 1.0, and one using PyGST working only for
|
||||||
|
Python 2 + Gstreamer 0.10.
|
||||||
|
If you have issue with GStreamer, have a look at
|
||||||
|
:ref:`gstreamer-compatibility`
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Recording audio is not supported.
|
Recording audio is not supported.
|
||||||
|
@ -28,6 +36,7 @@ from kivy.logger import Logger
|
||||||
from kivy.event import EventDispatcher
|
from kivy.event import EventDispatcher
|
||||||
from kivy.core import core_register_libs
|
from kivy.core import core_register_libs
|
||||||
from kivy.utils import platform
|
from kivy.utils import platform
|
||||||
|
from kivy.compat import PY2
|
||||||
from kivy.resources import resource_find
|
from kivy.resources import resource_find
|
||||||
from kivy.properties import StringProperty, NumericProperty, OptionProperty, \
|
from kivy.properties import StringProperty, NumericProperty, OptionProperty, \
|
||||||
AliasProperty, BooleanProperty
|
AliasProperty, BooleanProperty
|
||||||
|
@ -183,7 +192,9 @@ class Sound(EventDispatcher):
|
||||||
# XXX test in macosx
|
# XXX test in macosx
|
||||||
audio_libs = []
|
audio_libs = []
|
||||||
if platform != 'win':
|
if platform != 'win':
|
||||||
audio_libs += [('gstreamer', 'audio_gstreamer')]
|
audio_libs += [('gi', 'audio_gi')]
|
||||||
|
if PY2:
|
||||||
|
audio_libs += [('pygst', 'audio_pygst')]
|
||||||
audio_libs += [('sdl', 'audio_sdl')]
|
audio_libs += [('sdl', 'audio_sdl')]
|
||||||
audio_libs += [('pygame', 'audio_pygame')]
|
audio_libs += [('pygame', 'audio_pygame')]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
'''
|
||||||
|
Audio Gi
|
||||||
|
========
|
||||||
|
|
||||||
|
Implementation of Sound with Gi. Gi is both compatible with Python 2 and 3.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from gi.repository import Gst
|
||||||
|
from kivy.core.audio import Sound, SoundLoader
|
||||||
|
from kivy.logger import Logger
|
||||||
|
from kivy.support import install_gobject_iteration
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# initialize the audio/gi. if the older version is used, don't use audio_gi.
|
||||||
|
Gst.init(None)
|
||||||
|
version = Gst.version()
|
||||||
|
if version < (1, 0, 0, 0):
|
||||||
|
raise Exception('Cannot use audio_gi, Gstreamer < 1.0 is not supported.')
|
||||||
|
Logger.info('AudioGi: Using Gstreamer {}'.format(
|
||||||
|
'.'.join(['{}'.format(x) for x in Gst.version()])))
|
||||||
|
install_gobject_iteration()
|
||||||
|
|
||||||
|
|
||||||
|
class SoundGi(Sound):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extensions():
|
||||||
|
return ('wav', 'ogg', 'mp3', )
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._data = None
|
||||||
|
super(SoundGi, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self._data is not None:
|
||||||
|
self._data.set_state(Gst.State.NULL)
|
||||||
|
|
||||||
|
def _on_gst_message(self, bus, message):
|
||||||
|
t = message.type
|
||||||
|
if t == Gst.MessageType.EOS:
|
||||||
|
self._data.set_state(Gst.State.NULL)
|
||||||
|
if self.loop:
|
||||||
|
self.play()
|
||||||
|
else:
|
||||||
|
self.stop()
|
||||||
|
elif t == Gst.MessageType.ERROR:
|
||||||
|
self._data.set_state(Gst.State.NULL)
|
||||||
|
err, debug = message.parse_error()
|
||||||
|
Logger.error('AudioGi: %s' % err)
|
||||||
|
Logger.debug(str(debug))
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def play(self):
|
||||||
|
if not self._data:
|
||||||
|
return
|
||||||
|
self._data.props.volume = self.volume
|
||||||
|
self._data.set_state(Gst.State.PLAYING)
|
||||||
|
super(SoundGi, self).play()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if not self._data:
|
||||||
|
return
|
||||||
|
self._data.set_state(Gst.State.NULL)
|
||||||
|
super(SoundGi, self).stop()
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self.unload()
|
||||||
|
fn = self.filename
|
||||||
|
if fn is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
slash = ''
|
||||||
|
if sys.platform in ('win32', 'cygwin'):
|
||||||
|
slash = '/'
|
||||||
|
|
||||||
|
if fn[0] == '/':
|
||||||
|
uri = 'file://' + slash + fn
|
||||||
|
else:
|
||||||
|
uri = 'file://' + slash + os.path.join(os.getcwd(), fn)
|
||||||
|
|
||||||
|
self._data = Gst.ElementFactory.make('playbin', '')
|
||||||
|
fakesink = Gst.ElementFactory.make('fakesink', '')
|
||||||
|
self._data.props.video_sink = fakesink
|
||||||
|
bus = self._data.get_bus()
|
||||||
|
bus.add_signal_watch()
|
||||||
|
bus.connect('message', self._on_gst_message)
|
||||||
|
self._data.props.uri = uri
|
||||||
|
self._data.set_state(Gst.State.READY)
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
self.stop()
|
||||||
|
self._data = None
|
||||||
|
|
||||||
|
def seek(self, position):
|
||||||
|
if self._data is None:
|
||||||
|
return
|
||||||
|
self._data.seek_simple(
|
||||||
|
Gst.Format.TIME, Gst.SeekFlags.SKIP, position * Gst.SECOND)
|
||||||
|
|
||||||
|
def get_pos(self):
|
||||||
|
if self._data is not None:
|
||||||
|
if self._data.get_state()[1] == Gst.State.PLAYING:
|
||||||
|
try:
|
||||||
|
ret, value = self._data.query_position(Gst.Format.TIME)
|
||||||
|
if ret:
|
||||||
|
return value / float(Gst.SECOND)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def on_volume(self, instance, volume):
|
||||||
|
if self._data is not None:
|
||||||
|
self._data.set_property('volume', volume)
|
||||||
|
|
||||||
|
def _get_length(self):
|
||||||
|
if self._data is not None:
|
||||||
|
if self._data.get_state()[1] != Gst.State.PLAYING:
|
||||||
|
volume_before = self._data.get_property('volume')
|
||||||
|
self._data.set_property('volume', 0)
|
||||||
|
self._data.set_state(Gst.State.PLAYING)
|
||||||
|
try:
|
||||||
|
self._data.get_state()
|
||||||
|
ret, value = self._data.query_duration(Gst.Format.TIME)
|
||||||
|
if ret:
|
||||||
|
return value / float(Gst.SECOND)
|
||||||
|
finally:
|
||||||
|
self._data.set_state(Gst.State.NULL)
|
||||||
|
self._data.set_property('volume', volume_before)
|
||||||
|
else:
|
||||||
|
ret, value = self._data.query_duration(Gst.Format.TIME)
|
||||||
|
if ret:
|
||||||
|
return value / float(Gst.SECOND)
|
||||||
|
return super(SoundGi, self)._get_length()
|
||||||
|
|
||||||
|
SoundLoader.register(SoundGi)
|
|
@ -1,7 +1,17 @@
|
||||||
'''
|
'''
|
||||||
AudioGstreamer: implementation of Sound with GStreamer
|
Audio Gstreamer
|
||||||
|
===============
|
||||||
|
|
||||||
|
Implementation of Sound with GStreamer
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
import gi
|
||||||
|
except ImportError:
|
||||||
|
gi_found = False
|
||||||
|
else:
|
||||||
|
raise Exception('Avoiding PyGST, Gi is better.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pygst
|
import pygst
|
||||||
if not hasattr(pygst, '_gst_already_checked'):
|
if not hasattr(pygst, '_gst_already_checked'):
|
||||||
|
@ -21,7 +31,7 @@ from kivy.support import install_gobject_iteration
|
||||||
install_gobject_iteration()
|
install_gobject_iteration()
|
||||||
|
|
||||||
|
|
||||||
class SoundGstreamer(Sound):
|
class SoundPyGst(Sound):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extensions():
|
def extensions():
|
||||||
|
@ -29,7 +39,7 @@ class SoundGstreamer(Sound):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self._data = None
|
self._data = None
|
||||||
super(SoundGstreamer, self).__init__(**kwargs)
|
super(SoundPyGst, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if self._data is not None:
|
if self._data is not None:
|
||||||
|
@ -46,7 +56,7 @@ class SoundGstreamer(Sound):
|
||||||
elif t == gst.MESSAGE_ERROR:
|
elif t == gst.MESSAGE_ERROR:
|
||||||
self._data.set_state(gst.STATE_NULL)
|
self._data.set_state(gst.STATE_NULL)
|
||||||
err, debug = message.parse_error()
|
err, debug = message.parse_error()
|
||||||
Logger.error('AudioGstreamer: %s' % err)
|
Logger.error('AudioPyGst: %s' % err)
|
||||||
Logger.debug(str(debug))
|
Logger.debug(str(debug))
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
@ -55,13 +65,13 @@ class SoundGstreamer(Sound):
|
||||||
return
|
return
|
||||||
self._data.set_property('volume', self.volume)
|
self._data.set_property('volume', self.volume)
|
||||||
self._data.set_state(gst.STATE_PLAYING)
|
self._data.set_state(gst.STATE_PLAYING)
|
||||||
super(SoundGstreamer, self).play()
|
super(SoundPyGst, self).play()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if not self._data:
|
if not self._data:
|
||||||
return
|
return
|
||||||
self._data.set_state(gst.STATE_NULL)
|
self._data.set_state(gst.STATE_NULL)
|
||||||
super(SoundGstreamer, self).stop()
|
super(SoundPyGst, self).stop()
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
self.unload()
|
self.unload()
|
||||||
|
@ -128,6 +138,6 @@ class SoundGstreamer(Sound):
|
||||||
else:
|
else:
|
||||||
return self._data.query_duration(gst.Format
|
return self._data.query_duration(gst.Format
|
||||||
(gst.FORMAT_TIME))[0] / 1000000000.
|
(gst.FORMAT_TIME))[0] / 1000000000.
|
||||||
return super(SoundGstreamer, self)._get_length()
|
return super(SoundPyGst, self)._get_length()
|
||||||
|
|
||||||
SoundLoader.register(SoundGstreamer)
|
SoundLoader.register(SoundPyGst)
|
|
@ -4,6 +4,15 @@ Camera
|
||||||
|
|
||||||
Core class for acquiring the camera and converting its input into a
|
Core class for acquiring the camera and converting its input into a
|
||||||
:class:`~kivy.graphics.texture.Texture`.
|
:class:`~kivy.graphics.texture.Texture`.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.8.0
|
||||||
|
|
||||||
|
There is now 2 distinct Gstreamer implementation: one using Gi/Gst working
|
||||||
|
for both Python 2+3 with Gstreamer 1.0, and one using PyGST working only for
|
||||||
|
Python 2 + Gstreamer 0.10.
|
||||||
|
If you have issue with GStreamer, have a look at
|
||||||
|
:ref:`gstreamer-compatibility`
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
__all__ = ('CameraBase', 'Camera')
|
__all__ = ('CameraBase', 'Camera')
|
||||||
|
@ -131,7 +140,8 @@ if sys.platform == 'win32':
|
||||||
providers += (('videocapture', 'camera_videocapture',
|
providers += (('videocapture', 'camera_videocapture',
|
||||||
'CameraVideoCapture'), )
|
'CameraVideoCapture'), )
|
||||||
if sys.platform != 'darwin':
|
if sys.platform != 'darwin':
|
||||||
providers += (('gstreamer', 'camera_gstreamer', 'CameraGStreamer'), )
|
providers += (('gi', 'camera_gi', 'CameraGi'), )
|
||||||
|
providers += (('pygst', 'camera_pygst', 'CameraPyGst'), )
|
||||||
|
|
||||||
providers += (('opencv', 'camera_opencv', 'CameraOpenCV'), )
|
providers += (('opencv', 'camera_opencv', 'CameraOpenCV'), )
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
'''
|
||||||
|
Gi Camera
|
||||||
|
=========
|
||||||
|
|
||||||
|
Implement CameraBase with Gi / Gstreamer, working on both Python 2 and 3
|
||||||
|
'''
|
||||||
|
|
||||||
|
__all__ = ('CameraGi', )
|
||||||
|
|
||||||
|
from gi.repository import Gst
|
||||||
|
from kivy.clock import Clock
|
||||||
|
from kivy.graphics.texture import Texture
|
||||||
|
from kivy.core.camera import CameraBase
|
||||||
|
from kivy.support import install_gobject_iteration
|
||||||
|
from kivy.logger import Logger
|
||||||
|
from ctypes import Structure, c_void_p, c_int, string_at
|
||||||
|
from weakref import ref
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
# initialize the camera/gi. if the older version is used, don't use camera_gi.
|
||||||
|
Gst.init(None)
|
||||||
|
version = Gst.version()
|
||||||
|
if version < (1, 0, 0, 0):
|
||||||
|
raise Exception('Cannot use camera_gi, Gstreamer < 1.0 is not supported.')
|
||||||
|
Logger.info('CameraGi: Using Gstreamer {}'.format(
|
||||||
|
'.'.join(['{}'.format(x) for x in Gst.version()])))
|
||||||
|
install_gobject_iteration()
|
||||||
|
|
||||||
|
|
||||||
|
class _MapInfo(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('memory', c_void_p),
|
||||||
|
('flags', c_int),
|
||||||
|
('data', c_void_p) ]
|
||||||
|
# we don't care about the rest
|
||||||
|
|
||||||
|
|
||||||
|
def _on_cameragi_unref(obj):
|
||||||
|
if obj in CameraGi._instances:
|
||||||
|
CameraGi._instances.remove(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class CameraGi(CameraBase):
|
||||||
|
'''Implementation of CameraBase using GStreamer
|
||||||
|
|
||||||
|
:Parameters:
|
||||||
|
`video_src` : str, default is 'v4l2src'
|
||||||
|
Other tested options are: 'dc1394src' for firewire
|
||||||
|
dc camera (e.g. firefly MV). Any gstreamer video source
|
||||||
|
should potentially work.
|
||||||
|
Theoretically a longer string using "!" can be used
|
||||||
|
describing the first part of a gstreamer pipeline.
|
||||||
|
'''
|
||||||
|
|
||||||
|
_instances = []
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._pipeline = None
|
||||||
|
self._camerasink = None
|
||||||
|
self._decodebin = None
|
||||||
|
self._texturesize = None
|
||||||
|
self._video_src = kwargs.get('video_src', 'v4l2src')
|
||||||
|
wk = ref(self, _on_cameragi_unref)
|
||||||
|
CameraGi._instances.append(wk)
|
||||||
|
super(CameraGi, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def init_camera(self):
|
||||||
|
# TODO: This doesn't work when camera resolution is resized at runtime.
|
||||||
|
# There must be some other way to release the camera?
|
||||||
|
if self._pipeline:
|
||||||
|
self._pipeline = None
|
||||||
|
|
||||||
|
video_src = self._video_src
|
||||||
|
if video_src == 'v4l2src':
|
||||||
|
video_src += ' device=/dev/video%d' % self._index
|
||||||
|
elif video_src == 'dc1394src':
|
||||||
|
video_src += ' camera-number=%d' % self._index
|
||||||
|
|
||||||
|
if Gst.version() < (1, 0, 0, 0):
|
||||||
|
caps = 'video/x-raw-rgb,red_mask=(int)0xff0000,' + \
|
||||||
|
'green_mask=(int)0x00ff00,blue_mask=(int)0x0000ff'
|
||||||
|
pl = '{} ! decodebin name=decoder ! ffmpegcolorspace ! appsink ' + \
|
||||||
|
'name=camerasink emit-signals=True caps={}'
|
||||||
|
else:
|
||||||
|
caps = 'video/x-raw,format=RGB'
|
||||||
|
pl = '{} ! decodebin name=decoder ! videoconvert ! appsink ' + \
|
||||||
|
'name=camerasink emit-signals=True caps={}'
|
||||||
|
|
||||||
|
self._pipeline = Gst.parse_launch(pl.format(video_src, caps))
|
||||||
|
self._camerasink = self._pipeline.get_by_name('camerasink')
|
||||||
|
self._camerasink.connect('new-sample', self._gst_new_sample)
|
||||||
|
self._decodebin = self._pipeline.get_by_name('decoder')
|
||||||
|
|
||||||
|
if self._camerasink and not self.stopped:
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def _gst_new_sample(self, *largs):
|
||||||
|
sample = self._camerasink.emit('pull-sample')
|
||||||
|
if sample is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._sample = sample
|
||||||
|
|
||||||
|
if self._texturesize is None:
|
||||||
|
# try to get the camera image size
|
||||||
|
for pad in self._decodebin.srcpads:
|
||||||
|
s = pad.get_current_caps().get_structure(0)
|
||||||
|
self._texturesize = (
|
||||||
|
s.get_value('width'),
|
||||||
|
s.get_value('height'))
|
||||||
|
Clock.schedule_once(self._update)
|
||||||
|
return False
|
||||||
|
|
||||||
|
Clock.schedule_once(self._update)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
super(CameraGi, self).start()
|
||||||
|
self._pipeline.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
super(CameraGi, self).stop()
|
||||||
|
self._pipeline.set_state(Gst.State.PAUSED)
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
self._pipeline.set_state(Gst.State.NULL)
|
||||||
|
|
||||||
|
def _update(self, dt):
|
||||||
|
sample, self._sample = self._sample, None
|
||||||
|
if sample is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._texture is None and self._texturesize is not None:
|
||||||
|
self._texture = Texture.create(
|
||||||
|
size=self._texturesize, colorfmt='rgb')
|
||||||
|
self._texture.flip_vertical()
|
||||||
|
self.dispatch('on_load')
|
||||||
|
|
||||||
|
# decode sample
|
||||||
|
# read the data from the buffer memory
|
||||||
|
try:
|
||||||
|
buf = sample.get_buffer()
|
||||||
|
result, mapinfo = buf.map(Gst.MapFlags.READ)
|
||||||
|
|
||||||
|
# We cannot get the data out of mapinfo, using Gst 1.0.6 + Gi 3.8.0
|
||||||
|
# related bug report: https://bugzilla.gnome.org/show_bug.cgi?id=678663
|
||||||
|
# ie: mapinfo.data is normally a char*, but here, we have an int
|
||||||
|
# So right now, we use ctypes instead to read the mapinfo ourself.
|
||||||
|
addr = mapinfo.__hash__()
|
||||||
|
c_mapinfo = _MapInfo.from_address(addr)
|
||||||
|
|
||||||
|
# now get the memory
|
||||||
|
self._buffer = string_at(c_mapinfo.data, mapinfo.size)
|
||||||
|
self._copy_to_gpu()
|
||||||
|
finally:
|
||||||
|
if mapinfo is not None:
|
||||||
|
buf.unmap(mapinfo)
|
||||||
|
|
||||||
|
|
||||||
|
@atexit.register
|
||||||
|
def camera_gi_clean():
|
||||||
|
# if we leave the python process with some video running, we can hit a
|
||||||
|
# segfault. This is forcing the stop/unload of all remaining videos before
|
||||||
|
# exiting the python process.
|
||||||
|
for weakcamera in CameraGi._instances:
|
||||||
|
camera = weakcamera()
|
||||||
|
if isinstance(camera, CameraGi):
|
||||||
|
camera.stop()
|
||||||
|
camera.unload()
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
'''
|
'''
|
||||||
GStreamer Camera: Implement CameraBase with GStreamer
|
GStreamer Camera
|
||||||
|
================
|
||||||
|
|
||||||
|
Implement CameraBase with GStreamer, based on PyGST
|
||||||
'''
|
'''
|
||||||
|
|
||||||
__all__ = ('CameraGStreamer', )
|
__all__ = ('CameraPyGst', )
|
||||||
|
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.graphics.texture import Texture
|
from kivy.graphics.texture import Texture
|
||||||
|
@ -22,7 +25,7 @@ from kivy.support import install_gobject_iteration
|
||||||
install_gobject_iteration()
|
install_gobject_iteration()
|
||||||
|
|
||||||
|
|
||||||
class CameraGStreamer(CameraBase):
|
class CameraPyGst(CameraBase):
|
||||||
'''Implementation of CameraBase using GStreamer
|
'''Implementation of CameraBase using GStreamer
|
||||||
|
|
||||||
:Parameters:
|
:Parameters:
|
||||||
|
@ -40,7 +43,7 @@ class CameraGStreamer(CameraBase):
|
||||||
self._decodebin = None
|
self._decodebin = None
|
||||||
self._texturesize = None
|
self._texturesize = None
|
||||||
self._video_src = kwargs.get('video_src', 'v4l2src')
|
self._video_src = kwargs.get('video_src', 'v4l2src')
|
||||||
super(CameraGStreamer, self).__init__(**kwargs)
|
super(CameraPyGst, self).__init__(**kwargs)
|
||||||
|
|
||||||
def init_camera(self):
|
def init_camera(self):
|
||||||
# TODO: This doesn't work when camera resolution is resized at runtime.
|
# TODO: This doesn't work when camera resolution is resized at runtime.
|
||||||
|
@ -82,11 +85,11 @@ class CameraGStreamer(CameraBase):
|
||||||
Clock.schedule_once(self._update)
|
Clock.schedule_once(self._update)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
super(CameraGStreamer, self).start()
|
super(CameraPyGst, self).start()
|
||||||
self._pipeline.set_state(gst.STATE_PLAYING)
|
self._pipeline.set_state(gst.STATE_PLAYING)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
super(CameraGStreamer, self).stop()
|
super(CameraPyGst, self).stop()
|
||||||
self._pipeline.set_state(gst.STATE_PAUSED)
|
self._pipeline.set_state(gst.STATE_PAUSED)
|
||||||
|
|
||||||
def _update(self, dt):
|
def _update(self, dt):
|
|
@ -5,6 +5,14 @@ Video
|
||||||
Core class for reading video files and managing the
|
Core class for reading video files and managing the
|
||||||
:class:`kivy.graphics.texture.Texture` video.
|
:class:`kivy.graphics.texture.Texture` video.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.8.0
|
||||||
|
|
||||||
|
There is now 2 distinct Gstreamer implementation: one using Gi/Gst working
|
||||||
|
for both Python 2+3 with Gstreamer 1.0, and one using PyGST working only for
|
||||||
|
Python 2 + Gstreamer 0.10.
|
||||||
|
If you have issue with GStreamer, have a look at
|
||||||
|
:ref:`gstreamer-compatibility`
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Recording is not supported.
|
Recording is not supported.
|
||||||
|
@ -16,6 +24,7 @@ from kivy.clock import Clock
|
||||||
from kivy.core import core_select_lib
|
from kivy.core import core_select_lib
|
||||||
from kivy.event import EventDispatcher
|
from kivy.event import EventDispatcher
|
||||||
from kivy.logger import Logger
|
from kivy.logger import Logger
|
||||||
|
from kivy.compat import PY2
|
||||||
|
|
||||||
|
|
||||||
class VideoBase(EventDispatcher):
|
class VideoBase(EventDispatcher):
|
||||||
|
@ -193,10 +202,18 @@ class VideoBase(EventDispatcher):
|
||||||
|
|
||||||
|
|
||||||
# Load the appropriate provider
|
# Load the appropriate provider
|
||||||
Video = core_select_lib('video', (
|
video_providers = []
|
||||||
('gstreamer', 'video_gstreamer', 'VideoGStreamer'),
|
video_providers += [
|
||||||
|
('gi', 'video_gi', 'VideoGi')]
|
||||||
|
if PY2:
|
||||||
|
# if peoples do not have gi, fallback on pygst, only for python2
|
||||||
|
video_providers += [
|
||||||
|
('pygst', 'video_pygst', 'VideoPyGst')]
|
||||||
|
video_providers += [
|
||||||
('ffmpeg', 'video_ffmpeg', 'VideoFFMpeg'),
|
('ffmpeg', 'video_ffmpeg', 'VideoFFMpeg'),
|
||||||
('pyglet', 'video_pyglet', 'VideoPyglet'),
|
('pyglet', 'video_pyglet', 'VideoPyglet'),
|
||||||
('null', 'video_null', 'VideoNull'),
|
('null', 'video_null', 'VideoNull')]
|
||||||
))
|
|
||||||
|
|
||||||
|
Video = core_select_lib('video', video_providers)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,243 @@
|
||||||
|
'''
|
||||||
|
Video GI
|
||||||
|
========
|
||||||
|
|
||||||
|
Implementation of VideoBase with using pygi / gstreamer. Pygi is both compatible
|
||||||
|
with Python 2 and 3.
|
||||||
|
'''
|
||||||
|
|
||||||
|
#
|
||||||
|
# Important notes: you must take care of glib event + python. If you connect()
|
||||||
|
# directly an event to a python object method, the object will be ref, and will
|
||||||
|
# be never unref.
|
||||||
|
# To prevent memory leak, you must connect() to a func, and you might want to
|
||||||
|
# pass the referenced object with weakref()
|
||||||
|
#
|
||||||
|
|
||||||
|
from gi.repository import Gst
|
||||||
|
from functools import partial
|
||||||
|
from os.path import realpath
|
||||||
|
from threading import Lock
|
||||||
|
from weakref import ref
|
||||||
|
from kivy.compat import PY2
|
||||||
|
from kivy.core.video import VideoBase
|
||||||
|
from kivy.graphics.texture import Texture
|
||||||
|
from kivy.logger import Logger
|
||||||
|
from kivy.support import install_gobject_iteration
|
||||||
|
from ctypes import Structure, c_void_p, c_int, string_at
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
from urllib import pathname2url
|
||||||
|
else:
|
||||||
|
from urllib.request import pathname2url
|
||||||
|
|
||||||
|
# initialize the video/gi. if the older version is used, don't use video_gi.
|
||||||
|
Gst.init(None)
|
||||||
|
version = Gst.version()
|
||||||
|
if version < (1, 0, 0, 0):
|
||||||
|
raise Exception('Cannot use video_gi, Gstreamer < 1.0 is not supported.')
|
||||||
|
Logger.info('VideoGi: Using Gstreamer {}'.format(
|
||||||
|
'.'.join(['{}'.format(x) for x in Gst.version()])))
|
||||||
|
install_gobject_iteration()
|
||||||
|
|
||||||
|
|
||||||
|
class _MapInfo(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('memory', c_void_p),
|
||||||
|
('flags', c_int),
|
||||||
|
('data', c_void_p)]
|
||||||
|
# we don't care about the rest
|
||||||
|
|
||||||
|
|
||||||
|
def _gst_new_buffer(obj, appsink):
|
||||||
|
obj = obj()
|
||||||
|
if not obj:
|
||||||
|
return
|
||||||
|
with obj._buffer_lock:
|
||||||
|
obj._buffer = obj._appsink.emit('pull-sample')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _on_gst_message(bus, message):
|
||||||
|
Logger.trace('VideoGi: (bus) {}'.format(message))
|
||||||
|
# log all error messages
|
||||||
|
if message.type == Gst.MessageType.ERROR:
|
||||||
|
error, debug = list(map(str, message.parse_error()))
|
||||||
|
Logger.error('VideoGi: {}'.format(error))
|
||||||
|
Logger.debug('VideoGi: {}'.format(debug))
|
||||||
|
|
||||||
|
|
||||||
|
def _on_gst_eos(obj, *largs):
|
||||||
|
obj = obj()
|
||||||
|
if not obj:
|
||||||
|
return
|
||||||
|
obj._do_eos()
|
||||||
|
|
||||||
|
|
||||||
|
def _on_videogi_unref(obj):
|
||||||
|
if obj in VideoGi._instances:
|
||||||
|
VideoGi._instances.remove(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoGi(VideoBase):
|
||||||
|
|
||||||
|
_instances = []
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._buffer_lock = Lock()
|
||||||
|
self._buffer = None
|
||||||
|
self._texture = None
|
||||||
|
self._gst_init()
|
||||||
|
wk = ref(self, _on_videogi_unref)
|
||||||
|
VideoGi._instances.append(wk)
|
||||||
|
super(VideoGi, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _gst_init(self):
|
||||||
|
# self._appsink will receive the buffers so we can upload them to GPU
|
||||||
|
self._appsink = Gst.ElementFactory.make('appsink', '')
|
||||||
|
self._appsink.props.caps = Gst.caps_from_string(
|
||||||
|
'video/x-raw,format=RGB')
|
||||||
|
|
||||||
|
self._appsink.props.async = True
|
||||||
|
self._appsink.props.drop = True
|
||||||
|
self._appsink.props.qos = True
|
||||||
|
self._appsink.props.emit_signals = True
|
||||||
|
self._appsink.connect('new-sample', partial(
|
||||||
|
_gst_new_buffer, ref(self)))
|
||||||
|
|
||||||
|
# playbin, takes care of all, loading, playing, etc.
|
||||||
|
self._playbin = Gst.ElementFactory.make('playbin', 'playbin')
|
||||||
|
self._playbin.props.video_sink = self._appsink
|
||||||
|
|
||||||
|
# gstreamer bus, to attach and listen to gst messages
|
||||||
|
self._bus = self._playbin.get_bus()
|
||||||
|
self._bus.add_signal_watch()
|
||||||
|
self._bus.connect('message', _on_gst_message)
|
||||||
|
self._bus.connect('message::eos', partial(
|
||||||
|
_on_gst_eos, ref(self)))
|
||||||
|
|
||||||
|
def _update_texture(self, sample):
|
||||||
|
# texture will be updated with newest buffer/frame
|
||||||
|
|
||||||
|
# read the data from the buffer memory
|
||||||
|
mapinfo = data = None
|
||||||
|
try:
|
||||||
|
buf = sample.get_buffer()
|
||||||
|
result, mapinfo = buf.map(Gst.MapFlags.READ)
|
||||||
|
|
||||||
|
# We cannot get the data out of mapinfo, using Gst 1.0.6 + Gi 3.8.0
|
||||||
|
# related bug report:
|
||||||
|
# https://bugzilla.gnome.org/show_bug.cgi?id=678663
|
||||||
|
# ie: mapinfo.data is normally a char*, but here, we have an int
|
||||||
|
# So right now, we use ctypes instead to read the mapinfo ourself.
|
||||||
|
addr = mapinfo.__hash__()
|
||||||
|
c_mapinfo = _MapInfo.from_address(addr)
|
||||||
|
|
||||||
|
# now get the memory
|
||||||
|
data = string_at(c_mapinfo.data, mapinfo.size)
|
||||||
|
finally:
|
||||||
|
if mapinfo is not None:
|
||||||
|
buf.unmap(mapinfo)
|
||||||
|
|
||||||
|
# upload the data to the GPU
|
||||||
|
info = sample.get_caps().get_structure(0)
|
||||||
|
size = info.get_value('width'), info.get_value('height')
|
||||||
|
|
||||||
|
# texture is not allocated yet, create it first
|
||||||
|
if not self._texture:
|
||||||
|
self._texture = Texture.create(size=size, colorfmt='rgb')
|
||||||
|
self._texture.flip_vertical()
|
||||||
|
self.dispatch('on_load')
|
||||||
|
|
||||||
|
self._texture.blit_buffer(data, size=size, colorfmt='rgb')
|
||||||
|
|
||||||
|
def _update(self, dt):
|
||||||
|
buf = None
|
||||||
|
with self._buffer_lock:
|
||||||
|
buf = self._buffer
|
||||||
|
self._buffer = None
|
||||||
|
if buf is not None:
|
||||||
|
self._update_texture(buf)
|
||||||
|
self.dispatch('on_frame')
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
self._playbin.set_state(Gst.State.NULL)
|
||||||
|
self._buffer = None
|
||||||
|
self._texture = None
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
Logger.debug('VideoGi: Load <{}>'.format(self._filename))
|
||||||
|
self._playbin.set_state(Gst.State.NULL)
|
||||||
|
self._playbin.props.uri = self._get_uri()
|
||||||
|
self._playbin.set_state(Gst.State.READY)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._state = ''
|
||||||
|
self._playbin.set_state(Gst.State.PAUSED)
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
self._state = 'paused'
|
||||||
|
self._playbin.set_state(Gst.State.PAUSED)
|
||||||
|
|
||||||
|
def play(self):
|
||||||
|
self._state = 'playing'
|
||||||
|
self._playbin.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
|
def seek(self, percent):
|
||||||
|
seek_t = percent * self._get_duration() * 10e8
|
||||||
|
seek_format = Gst.Format.TIME
|
||||||
|
seek_flags = Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT
|
||||||
|
self._playbin.seek_simple(seek_format, seek_flags, seek_t)
|
||||||
|
|
||||||
|
#if pipeline is not playing, we need to pull pre-roll to update frame
|
||||||
|
if not self._state == 'playing':
|
||||||
|
with self._buffer_lock:
|
||||||
|
self._buffer = self._appsink.emit('pull-preroll')
|
||||||
|
|
||||||
|
def _get_uri(self):
|
||||||
|
uri = self.filename
|
||||||
|
if not uri:
|
||||||
|
return
|
||||||
|
if not '://' in uri:
|
||||||
|
uri = 'file:' + pathname2url(realpath(uri))
|
||||||
|
return uri
|
||||||
|
|
||||||
|
def _get_position(self):
|
||||||
|
try:
|
||||||
|
ret, value = self._appsink.query_position(Gst.Format.TIME)
|
||||||
|
if ret:
|
||||||
|
return value / float(Gst.SECOND)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def _get_duration(self):
|
||||||
|
try:
|
||||||
|
ret, value = self._playbin.query_duration(Gst.Format.TIME)
|
||||||
|
if ret:
|
||||||
|
return value / float(Gst.SECOND)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def _get_volume(self):
|
||||||
|
self._volume = self._playbin.props.volume
|
||||||
|
return self._volume
|
||||||
|
|
||||||
|
def _set_volume(self, volume):
|
||||||
|
self._playbin.props.volume = volume
|
||||||
|
self._volume = volume
|
||||||
|
|
||||||
|
|
||||||
|
@atexit.register
|
||||||
|
def video_gi_clean():
|
||||||
|
# if we leave the python process with some video running, we can hit a
|
||||||
|
# segfault. This is forcing the stop/unload of all remaining videos before
|
||||||
|
# exiting the python process.
|
||||||
|
for weakvideo in VideoGi._instances:
|
||||||
|
video = weakvideo()
|
||||||
|
if video:
|
||||||
|
video.stop()
|
||||||
|
video.unload()
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
'''
|
'''
|
||||||
VideoGStreamer: implementation of VideoBase with GStreamer
|
Video PyGst
|
||||||
|
===========
|
||||||
|
|
||||||
|
Implementation of a VideoBase using PyGST. This module is compatible only with
|
||||||
|
Python 2.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
#
|
#
|
||||||
# Important notes: you must take care of glib event + python. If you connect()
|
# Important notes: you must take care of glib event + python. If you connect()
|
||||||
# directly an event to a python object method, the object will be ref, and will
|
# directly an event to a python object method, the object will be ref, and will
|
||||||
|
@ -8,79 +13,55 @@ VideoGStreamer: implementation of VideoBase with GStreamer
|
||||||
# To prevent memory leak, you must connect() to a func, and you might want to
|
# To prevent memory leak, you must connect() to a func, and you might want to
|
||||||
# pass the referenced object with weakref()
|
# pass the referenced object with weakref()
|
||||||
#
|
#
|
||||||
from kivy.compat import PY2
|
|
||||||
try:
|
|
||||||
#import pygst
|
|
||||||
#if not hasattr(pygst, '_gst_already_checked'):
|
|
||||||
# pygst.require('0.10')
|
|
||||||
# pygst._gst_already_checked = True
|
|
||||||
if PY2:
|
|
||||||
import gst
|
|
||||||
else:
|
|
||||||
import ctypes
|
|
||||||
import gi
|
|
||||||
from gi.repository import Gst as gst
|
|
||||||
except:
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
import pygst
|
||||||
|
|
||||||
|
if not hasattr(pygst, '_gst_already_checked'):
|
||||||
|
found = False
|
||||||
|
for version in ('1.0', '0.10'):
|
||||||
|
try:
|
||||||
|
pygst.require(version)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if found:
|
||||||
|
pygst._gst_already_checked = True
|
||||||
|
else:
|
||||||
|
raise Exception('Unable to find a valid Gstreamer version to use')
|
||||||
|
|
||||||
|
import gst
|
||||||
|
from functools import partial
|
||||||
from os import path
|
from os import path
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
if PY2:
|
from urllib import pathname2url
|
||||||
from urllib import pathname2url
|
|
||||||
else:
|
|
||||||
from urllib.request import pathname2url
|
|
||||||
from kivy.graphics.texture import Texture
|
|
||||||
from kivy.logger import Logger
|
|
||||||
from functools import partial
|
|
||||||
from weakref import ref
|
from weakref import ref
|
||||||
from kivy.core.video import VideoBase
|
from kivy.core.video import VideoBase
|
||||||
|
from kivy.graphics.texture import Texture
|
||||||
# install the gobject iteration
|
from kivy.logger import Logger
|
||||||
from kivy.support import install_gobject_iteration
|
from kivy.support import install_gobject_iteration
|
||||||
|
|
||||||
|
|
||||||
install_gobject_iteration()
|
install_gobject_iteration()
|
||||||
|
|
||||||
BUF_SAMPLE = 'buffer'
|
|
||||||
_VIDEO_CAPS = ','.join([
|
|
||||||
'video/x-raw-rgb',
|
|
||||||
'red_mask=(int)0xff0000',
|
|
||||||
'green_mask=(int)0x00ff00',
|
|
||||||
'blue_mask=(int)0x0000ff'])
|
|
||||||
|
|
||||||
if not PY2:
|
|
||||||
gst.init(None)
|
|
||||||
gst.STATE_NULL = gst.State.NULL
|
|
||||||
gst.STATE_READY = gst.State.READY
|
|
||||||
gst.STATE_PLAYING = gst.State.PLAYING
|
|
||||||
gst.STATE_PAUSED = gst.State.PAUSED
|
|
||||||
gst.FORMAT_TIME = gst.Format.TIME
|
|
||||||
gst.SEEK_FLAG_FLUSH = gst.SeekFlags.KEY_UNIT
|
|
||||||
gst.SEEK_FLAG_KEY_UNIT = gst.SeekFlags.KEY_UNIT
|
|
||||||
gst.MESSAGE_ERROR = gst.MessageType.ERROR
|
|
||||||
BUF_SAMPLE = 'sample'
|
|
||||||
|
|
||||||
_VIDEO_CAPS = ','.join([
|
|
||||||
'video/x-raw',
|
|
||||||
'format=RGB',
|
|
||||||
'red_mask=(int)0xff0000',
|
|
||||||
'green_mask=(int)0x00ff00',
|
|
||||||
'blue_mask=(int)0x0000ff'])
|
|
||||||
|
|
||||||
|
|
||||||
def _gst_new_buffer(obj, appsink):
|
def _gst_new_buffer(obj, appsink):
|
||||||
obj = obj()
|
obj = obj()
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
with obj._buffer_lock:
|
with obj._buffer_lock:
|
||||||
obj._buffer = obj._videosink.emit('pull-' + BUF_SAMPLE)
|
obj._buffer = obj._appsink.emit('pull-buffer')
|
||||||
|
|
||||||
|
|
||||||
def _on_gst_message(bus, message):
|
def _on_gst_message(bus, message):
|
||||||
Logger.trace('gst-bus: %s' % str(message))
|
Logger.trace('VideoPyGst: (bus) %s' % str(message))
|
||||||
# log all error messages
|
# log all error messages
|
||||||
if message.type == gst.MESSAGE_ERROR:
|
if message.type == gst.MESSAGE_ERROR:
|
||||||
error, debug = list(map(str, message.parse_error()))
|
error, debug = list(map(str, message.parse_error()))
|
||||||
Logger.error('gstreamer_video: %s' % error)
|
Logger.error('VideoPyGst: %s' % error)
|
||||||
Logger.debug('gstreamer_video: %s' % debug)
|
Logger.debug('VideoPyGst: %s' % debug)
|
||||||
|
|
||||||
|
|
||||||
def _on_gst_eos(obj, *largs):
|
def _on_gst_eos(obj, *largs):
|
||||||
|
@ -90,40 +71,33 @@ def _on_gst_eos(obj, *largs):
|
||||||
obj._do_eos()
|
obj._do_eos()
|
||||||
|
|
||||||
|
|
||||||
class VideoGStreamer(VideoBase):
|
class VideoPyGst(VideoBase):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self._buffer_lock = Lock()
|
self._buffer_lock = Lock()
|
||||||
self._buffer = None
|
self._buffer = None
|
||||||
self._texture = None
|
self._texture = None
|
||||||
self._gst_init()
|
self._gst_init()
|
||||||
super(VideoGStreamer, self).__init__(**kwargs)
|
super(VideoPyGst, self).__init__(**kwargs)
|
||||||
|
|
||||||
def _gst_init(self):
|
def _gst_init(self):
|
||||||
# self._videosink will receive the buffers so we can upload them to GPU
|
# self._appsink will receive the buffers so we can upload them to GPU
|
||||||
if PY2:
|
self._appsink = gst.element_factory_make('appsink', '')
|
||||||
self._videosink = gst.element_factory_make('appsink', 'videosink')
|
self._appsink.set_property('caps', gst.Caps(
|
||||||
self._videosink.set_property('caps', gst.Caps(_VIDEO_CAPS))
|
'video/x-raw-rgb,red_mask=(int)0xff0000,'
|
||||||
else:
|
'green_mask=(int)0x00ff00,blue_mask=(int)0x0000ff'))
|
||||||
self._videosink = gst.ElementFactory.make('appsink', 'videosink')
|
|
||||||
self._videosink.set_property('caps',
|
|
||||||
gst.caps_from_string(_VIDEO_CAPS))
|
|
||||||
|
|
||||||
self._videosink.set_property('async', True)
|
self._appsink.set_property('async', True)
|
||||||
self._videosink.set_property('drop', True)
|
self._appsink.set_property('drop', True)
|
||||||
self._videosink.set_property('qos', True)
|
self._appsink.set_property('qos', True)
|
||||||
self._videosink.set_property('emit-signals', True)
|
self._appsink.set_property('emit-signals', True)
|
||||||
self._videosink.connect('new-' + BUF_SAMPLE, partial(
|
self._appsink.connect('new-buffer', partial(
|
||||||
_gst_new_buffer, ref(self)))
|
_gst_new_buffer, ref(self)))
|
||||||
|
|
||||||
# playbin, takes care of all, loading, playing, etc.
|
# playbin, takes care of all, loading, playing, etc.
|
||||||
# XXX playbin2 have some issue when playing some video or streaming :/
|
# XXX playbin2 have some issue when playing some video or streaming :/
|
||||||
#self._playbin = gst.element_factory_make('playbin2', 'playbin')
|
self._playbin = gst.element_factory_make('playbin', 'playbin')
|
||||||
if PY2:
|
self._playbin.set_property('video-sink', self._appsink)
|
||||||
self._playbin = gst.element_factory_make('playbin', 'playbin')
|
|
||||||
else:
|
|
||||||
self._playbin = gst.ElementFactory.make('playbin', 'playbin')
|
|
||||||
self._playbin.set_property('video-sink', self._videosink)
|
|
||||||
|
|
||||||
# gstreamer bus, to attach and listen to gst messages
|
# gstreamer bus, to attach and listen to gst messages
|
||||||
self._bus = self._playbin.get_bus()
|
self._bus = self._playbin.get_bus()
|
||||||
|
@ -134,42 +108,18 @@ class VideoGStreamer(VideoBase):
|
||||||
|
|
||||||
def _update_texture(self, buf):
|
def _update_texture(self, buf):
|
||||||
# texture will be updated with newest buffer/frame
|
# texture will be updated with newest buffer/frame
|
||||||
|
size = None
|
||||||
caps = buf.get_caps()
|
caps = buf.get_caps()
|
||||||
_s = caps.get_structure(0)
|
_s = caps.get_structure(0)
|
||||||
data = size = None
|
size = _s['width'], _s['height']
|
||||||
if PY2:
|
|
||||||
size = _s['width'], _s['height']
|
|
||||||
else:
|
|
||||||
size = _s.get_int('width')[1], _s.get_int('height')[1]
|
|
||||||
if not self._texture:
|
if not self._texture:
|
||||||
# texture is not allocated yet, so create it first
|
# texture is not allocated yet, so create it first
|
||||||
self._texture = Texture.create(size=size, colorfmt='rgb')
|
self._texture = Texture.create(size=size, colorfmt='rgb')
|
||||||
self._texture.flip_vertical()
|
self._texture.flip_vertical()
|
||||||
self.dispatch('on_load')
|
self.dispatch('on_load')
|
||||||
|
|
||||||
# upload texture data to GPU
|
# upload texture data to GPU
|
||||||
if not PY2:
|
self._texture.blit_buffer(buf.data, size=size, colorfmt='rgb')
|
||||||
mapinfo = None
|
|
||||||
try:
|
|
||||||
mem = buf.get_buffer()
|
|
||||||
#from pudb import set_trace; set_trace()
|
|
||||||
result, mapinfo = mem.map(gst.MapFlags.READ)
|
|
||||||
#result, mapinfo = mem.map_range(0, -1, gst.MapFlags.READ)
|
|
||||||
|
|
||||||
# repr(mapinfo) will return <void at 0x1aa3530>
|
|
||||||
# but there is no python attribute to get the address... so we
|
|
||||||
# need to parse it.
|
|
||||||
addr = int(repr(mapinfo.memory).split()[-1][:-1], 16)
|
|
||||||
# now get the memory
|
|
||||||
_size = mem.__sizeof__() + mapinfo.memory.__sizeof__()
|
|
||||||
data = ctypes.string_at(addr + _size, mapinfo.size)
|
|
||||||
#print('got data', len(data), addr)
|
|
||||||
finally:
|
|
||||||
if mapinfo is not None:
|
|
||||||
mem.unmap(mapinfo)
|
|
||||||
else:
|
|
||||||
data = buf.data
|
|
||||||
|
|
||||||
self._texture.blit_buffer(data, size=size, colorfmt='rgb')
|
|
||||||
|
|
||||||
def _update(self, dt):
|
def _update(self, dt):
|
||||||
buf = None
|
buf = None
|
||||||
|
@ -186,7 +136,7 @@ class VideoGStreamer(VideoBase):
|
||||||
self._texture = None
|
self._texture = None
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
Logger.debug('gstreamer_video: Load <%s>' % self._filename)
|
Logger.debug('VideoPyGst: Load <%s>' % self._filename)
|
||||||
self._playbin.set_state(gst.STATE_NULL)
|
self._playbin.set_state(gst.STATE_NULL)
|
||||||
self._playbin.set_property('uri', self._get_uri())
|
self._playbin.set_property('uri', self._get_uri())
|
||||||
self._playbin.set_state(gst.STATE_READY)
|
self._playbin.set_state(gst.STATE_READY)
|
||||||
|
@ -214,7 +164,7 @@ class VideoGStreamer(VideoBase):
|
||||||
#if pipeline is not playing, we need to pull pre-roll to update frame
|
#if pipeline is not playing, we need to pull pre-roll to update frame
|
||||||
if not self._state == 'playing':
|
if not self._state == 'playing':
|
||||||
with self._buffer_lock:
|
with self._buffer_lock:
|
||||||
self._buffer = self._videosink.emit('pull-preroll')
|
self._buffer = self._appsink.emit('pull-preroll')
|
||||||
|
|
||||||
def _get_uri(self):
|
def _get_uri(self):
|
||||||
uri = self.filename
|
uri = self.filename
|
||||||
|
@ -226,7 +176,7 @@ class VideoGStreamer(VideoBase):
|
||||||
|
|
||||||
def _get_position(self):
|
def _get_position(self):
|
||||||
try:
|
try:
|
||||||
value, fmt = self._videosink.query_position(gst.FORMAT_TIME)
|
value, fmt = self._appsink.query_position(gst.FORMAT_TIME)
|
||||||
return value / 10e8
|
return value / 10e8
|
||||||
except:
|
except:
|
||||||
return -1
|
return -1
|
|
@ -9,8 +9,6 @@ Activate other frameworks/toolkits inside the kivy event loop.
|
||||||
__all__ = ('install_gobject_iteration', 'install_twisted_reactor',
|
__all__ = ('install_gobject_iteration', 'install_twisted_reactor',
|
||||||
'install_android')
|
'install_android')
|
||||||
|
|
||||||
from kivy.compat import PY2
|
|
||||||
|
|
||||||
|
|
||||||
def install_gobject_iteration():
|
def install_gobject_iteration():
|
||||||
'''Import and install gobject context iteration inside our event loop.
|
'''Import and install gobject context iteration inside our event loop.
|
||||||
|
@ -19,10 +17,10 @@ def install_gobject_iteration():
|
||||||
|
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
|
|
||||||
if PY2:
|
try:
|
||||||
import gobject
|
|
||||||
else:
|
|
||||||
from gi.repository import GObject as gobject
|
from gi.repository import GObject as gobject
|
||||||
|
except ImportError:
|
||||||
|
import gobject
|
||||||
|
|
||||||
if hasattr(gobject, '_gobject_already_installed'):
|
if hasattr(gobject, '_gobject_already_installed'):
|
||||||
# already installed, don't do it twice.
|
# already installed, don't do it twice.
|
||||||
|
@ -178,9 +176,9 @@ def install_twisted_reactor(**kwargs):
|
||||||
|
|
||||||
# twisted will call the wake function when it needs to do work
|
# twisted will call the wake function when it needs to do work
|
||||||
def reactor_wake(twisted_loop_next):
|
def reactor_wake(twisted_loop_next):
|
||||||
'''Wakeup the twisted reactor to start processing the task queue
|
'''Wakeup the twisted reactor to start processing the task queue
|
||||||
'''
|
'''
|
||||||
|
|
||||||
Logger.trace("Support: twisted wakeup call to schedule task")
|
Logger.trace("Support: twisted wakeup call to schedule task")
|
||||||
q.append(twisted_loop_next)
|
q.append(twisted_loop_next)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue