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.
|
||||
|
||||
.. _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
|
||||
-----------
|
||||
|
|
|
@ -188,10 +188,10 @@ else:
|
|||
kivy_options = {
|
||||
'window': ('egl_rpi', 'pygame', 'sdl', 'x11'),
|
||||
'text': ('pil', 'pygame', 'sdlttf'),
|
||||
'video': ('ffmpeg', 'gstreamer', 'pyglet', 'null'),
|
||||
'audio': ('pygame', 'gstreamer', 'sdl'),
|
||||
'video': ('ffmpeg', 'gi', 'pygst', 'pyglet', 'null'),
|
||||
'audio': ('pygame', 'gi', 'pygst', 'sdl'),
|
||||
'image': ('tex', 'imageio', 'dds', 'gif', 'pil', 'pygame'),
|
||||
'camera': ('opencv', 'gstreamer', 'videocapture'),
|
||||
'camera': ('opencv', 'gi', 'pygst', 'videocapture'),
|
||||
'spelling': ('enchant', 'osxappkit', ),
|
||||
'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
|
||||
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::
|
||||
|
||||
Recording audio is not supported.
|
||||
|
@ -28,6 +36,7 @@ from kivy.logger import Logger
|
|||
from kivy.event import EventDispatcher
|
||||
from kivy.core import core_register_libs
|
||||
from kivy.utils import platform
|
||||
from kivy.compat import PY2
|
||||
from kivy.resources import resource_find
|
||||
from kivy.properties import StringProperty, NumericProperty, OptionProperty, \
|
||||
AliasProperty, BooleanProperty
|
||||
|
@ -183,7 +192,9 @@ class Sound(EventDispatcher):
|
|||
# XXX test in macosx
|
||||
audio_libs = []
|
||||
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 += [('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:
|
||||
import pygst
|
||||
if not hasattr(pygst, '_gst_already_checked'):
|
||||
|
@ -21,7 +31,7 @@ from kivy.support import install_gobject_iteration
|
|||
install_gobject_iteration()
|
||||
|
||||
|
||||
class SoundGstreamer(Sound):
|
||||
class SoundPyGst(Sound):
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
|
@ -29,7 +39,7 @@ class SoundGstreamer(Sound):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
self._data = None
|
||||
super(SoundGstreamer, self).__init__(**kwargs)
|
||||
super(SoundPyGst, self).__init__(**kwargs)
|
||||
|
||||
def __del__(self):
|
||||
if self._data is not None:
|
||||
|
@ -46,7 +56,7 @@ class SoundGstreamer(Sound):
|
|||
elif t == gst.MESSAGE_ERROR:
|
||||
self._data.set_state(gst.STATE_NULL)
|
||||
err, debug = message.parse_error()
|
||||
Logger.error('AudioGstreamer: %s' % err)
|
||||
Logger.error('AudioPyGst: %s' % err)
|
||||
Logger.debug(str(debug))
|
||||
self.stop()
|
||||
|
||||
|
@ -55,13 +65,13 @@ class SoundGstreamer(Sound):
|
|||
return
|
||||
self._data.set_property('volume', self.volume)
|
||||
self._data.set_state(gst.STATE_PLAYING)
|
||||
super(SoundGstreamer, self).play()
|
||||
super(SoundPyGst, self).play()
|
||||
|
||||
def stop(self):
|
||||
if not self._data:
|
||||
return
|
||||
self._data.set_state(gst.STATE_NULL)
|
||||
super(SoundGstreamer, self).stop()
|
||||
super(SoundPyGst, self).stop()
|
||||
|
||||
def load(self):
|
||||
self.unload()
|
||||
|
@ -128,6 +138,6 @@ class SoundGstreamer(Sound):
|
|||
else:
|
||||
return self._data.query_duration(gst.Format
|
||||
(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
|
||||
: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')
|
||||
|
@ -131,7 +140,8 @@ if sys.platform == 'win32':
|
|||
providers += (('videocapture', 'camera_videocapture',
|
||||
'CameraVideoCapture'), )
|
||||
if sys.platform != 'darwin':
|
||||
providers += (('gstreamer', 'camera_gstreamer', 'CameraGStreamer'), )
|
||||
providers += (('gi', 'camera_gi', 'CameraGi'), )
|
||||
providers += (('pygst', 'camera_pygst', 'CameraPyGst'), )
|
||||
|
||||
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.graphics.texture import Texture
|
||||
|
@ -22,7 +25,7 @@ from kivy.support import install_gobject_iteration
|
|||
install_gobject_iteration()
|
||||
|
||||
|
||||
class CameraGStreamer(CameraBase):
|
||||
class CameraPyGst(CameraBase):
|
||||
'''Implementation of CameraBase using GStreamer
|
||||
|
||||
:Parameters:
|
||||
|
@ -40,7 +43,7 @@ class CameraGStreamer(CameraBase):
|
|||
self._decodebin = None
|
||||
self._texturesize = None
|
||||
self._video_src = kwargs.get('video_src', 'v4l2src')
|
||||
super(CameraGStreamer, self).__init__(**kwargs)
|
||||
super(CameraPyGst, self).__init__(**kwargs)
|
||||
|
||||
def init_camera(self):
|
||||
# TODO: This doesn't work when camera resolution is resized at runtime.
|
||||
|
@ -82,11 +85,11 @@ class CameraGStreamer(CameraBase):
|
|||
Clock.schedule_once(self._update)
|
||||
|
||||
def start(self):
|
||||
super(CameraGStreamer, self).start()
|
||||
super(CameraPyGst, self).start()
|
||||
self._pipeline.set_state(gst.STATE_PLAYING)
|
||||
|
||||
def stop(self):
|
||||
super(CameraGStreamer, self).stop()
|
||||
super(CameraPyGst, self).stop()
|
||||
self._pipeline.set_state(gst.STATE_PAUSED)
|
||||
|
||||
def _update(self, dt):
|
|
@ -5,6 +5,14 @@ Video
|
|||
Core class for reading video files and managing the
|
||||
: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::
|
||||
|
||||
Recording is not supported.
|
||||
|
@ -16,6 +24,7 @@ from kivy.clock import Clock
|
|||
from kivy.core import core_select_lib
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.logger import Logger
|
||||
from kivy.compat import PY2
|
||||
|
||||
|
||||
class VideoBase(EventDispatcher):
|
||||
|
@ -193,10 +202,18 @@ class VideoBase(EventDispatcher):
|
|||
|
||||
|
||||
# Load the appropriate provider
|
||||
Video = core_select_lib('video', (
|
||||
('gstreamer', 'video_gstreamer', 'VideoGStreamer'),
|
||||
video_providers = []
|
||||
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'),
|
||||
('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()
|
||||
# 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
|
||||
# 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 threading import Lock
|
||||
if PY2:
|
||||
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 urllib import pathname2url
|
||||
from weakref import ref
|
||||
from kivy.core.video import VideoBase
|
||||
|
||||
# install the gobject iteration
|
||||
from kivy.graphics.texture import Texture
|
||||
from kivy.logger import Logger
|
||||
from kivy.support import 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):
|
||||
obj = obj()
|
||||
if not obj:
|
||||
return
|
||||
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):
|
||||
Logger.trace('gst-bus: %s' % str(message))
|
||||
Logger.trace('VideoPyGst: (bus) %s' % str(message))
|
||||
# log all error messages
|
||||
if message.type == gst.MESSAGE_ERROR:
|
||||
error, debug = list(map(str, message.parse_error()))
|
||||
Logger.error('gstreamer_video: %s' % error)
|
||||
Logger.debug('gstreamer_video: %s' % debug)
|
||||
Logger.error('VideoPyGst: %s' % error)
|
||||
Logger.debug('VideoPyGst: %s' % debug)
|
||||
|
||||
|
||||
def _on_gst_eos(obj, *largs):
|
||||
|
@ -90,40 +71,33 @@ def _on_gst_eos(obj, *largs):
|
|||
obj._do_eos()
|
||||
|
||||
|
||||
class VideoGStreamer(VideoBase):
|
||||
class VideoPyGst(VideoBase):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._buffer_lock = Lock()
|
||||
self._buffer = None
|
||||
self._texture = None
|
||||
self._gst_init()
|
||||
super(VideoGStreamer, self).__init__(**kwargs)
|
||||
super(VideoPyGst, self).__init__(**kwargs)
|
||||
|
||||
def _gst_init(self):
|
||||
# self._videosink will receive the buffers so we can upload them to GPU
|
||||
if PY2:
|
||||
self._videosink = gst.element_factory_make('appsink', 'videosink')
|
||||
self._videosink.set_property('caps', gst.Caps(_VIDEO_CAPS))
|
||||
else:
|
||||
self._videosink = gst.ElementFactory.make('appsink', 'videosink')
|
||||
self._videosink.set_property('caps',
|
||||
gst.caps_from_string(_VIDEO_CAPS))
|
||||
# self._appsink will receive the buffers so we can upload them to GPU
|
||||
self._appsink = gst.element_factory_make('appsink', '')
|
||||
self._appsink.set_property('caps', gst.Caps(
|
||||
'video/x-raw-rgb,red_mask=(int)0xff0000,'
|
||||
'green_mask=(int)0x00ff00,blue_mask=(int)0x0000ff'))
|
||||
|
||||
self._videosink.set_property('async', True)
|
||||
self._videosink.set_property('drop', True)
|
||||
self._videosink.set_property('qos', True)
|
||||
self._videosink.set_property('emit-signals', True)
|
||||
self._videosink.connect('new-' + BUF_SAMPLE, partial(
|
||||
self._appsink.set_property('async', True)
|
||||
self._appsink.set_property('drop', True)
|
||||
self._appsink.set_property('qos', True)
|
||||
self._appsink.set_property('emit-signals', True)
|
||||
self._appsink.connect('new-buffer', partial(
|
||||
_gst_new_buffer, ref(self)))
|
||||
|
||||
# playbin, takes care of all, loading, playing, etc.
|
||||
# XXX playbin2 have some issue when playing some video or streaming :/
|
||||
#self._playbin = gst.element_factory_make('playbin2', 'playbin')
|
||||
if PY2:
|
||||
self._playbin = gst.element_factory_make('playbin', 'playbin')
|
||||
else:
|
||||
self._playbin = gst.ElementFactory.make('playbin', 'playbin')
|
||||
self._playbin.set_property('video-sink', self._videosink)
|
||||
self._playbin = gst.element_factory_make('playbin', 'playbin')
|
||||
self._playbin.set_property('video-sink', self._appsink)
|
||||
|
||||
# gstreamer bus, to attach and listen to gst messages
|
||||
self._bus = self._playbin.get_bus()
|
||||
|
@ -134,42 +108,18 @@ class VideoGStreamer(VideoBase):
|
|||
|
||||
def _update_texture(self, buf):
|
||||
# texture will be updated with newest buffer/frame
|
||||
size = None
|
||||
caps = buf.get_caps()
|
||||
_s = caps.get_structure(0)
|
||||
data = size = None
|
||||
if PY2:
|
||||
size = _s['width'], _s['height']
|
||||
else:
|
||||
size = _s.get_int('width')[1], _s.get_int('height')[1]
|
||||
size = _s['width'], _s['height']
|
||||
if not self._texture:
|
||||
# texture is not allocated yet, so create it first
|
||||
self._texture = Texture.create(size=size, colorfmt='rgb')
|
||||
self._texture.flip_vertical()
|
||||
self.dispatch('on_load')
|
||||
|
||||
# upload texture data to GPU
|
||||
if not PY2:
|
||||
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')
|
||||
self._texture.blit_buffer(buf.data, size=size, colorfmt='rgb')
|
||||
|
||||
def _update(self, dt):
|
||||
buf = None
|
||||
|
@ -186,7 +136,7 @@ class VideoGStreamer(VideoBase):
|
|||
self._texture = None
|
||||
|
||||
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_property('uri', self._get_uri())
|
||||
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 not self._state == 'playing':
|
||||
with self._buffer_lock:
|
||||
self._buffer = self._videosink.emit('pull-preroll')
|
||||
self._buffer = self._appsink.emit('pull-preroll')
|
||||
|
||||
def _get_uri(self):
|
||||
uri = self.filename
|
||||
|
@ -226,7 +176,7 @@ class VideoGStreamer(VideoBase):
|
|||
|
||||
def _get_position(self):
|
||||
try:
|
||||
value, fmt = self._videosink.query_position(gst.FORMAT_TIME)
|
||||
value, fmt = self._appsink.query_position(gst.FORMAT_TIME)
|
||||
return value / 10e8
|
||||
except:
|
||||
return -1
|
|
@ -9,8 +9,6 @@ Activate other frameworks/toolkits inside the kivy event loop.
|
|||
__all__ = ('install_gobject_iteration', 'install_twisted_reactor',
|
||||
'install_android')
|
||||
|
||||
from kivy.compat import PY2
|
||||
|
||||
|
||||
def install_gobject_iteration():
|
||||
'''Import and install gobject context iteration inside our event loop.
|
||||
|
@ -19,10 +17,10 @@ def install_gobject_iteration():
|
|||
|
||||
from kivy.clock import Clock
|
||||
|
||||
if PY2:
|
||||
import gobject
|
||||
else:
|
||||
try:
|
||||
from gi.repository import GObject as gobject
|
||||
except ImportError:
|
||||
import gobject
|
||||
|
||||
if hasattr(gobject, '_gobject_already_installed'):
|
||||
# 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
|
||||
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")
|
||||
q.append(twisted_loop_next)
|
||||
|
||||
|
|
Loading…
Reference in New Issue