Merge pull request #1692 from kivy/fix-1689

Separate PyGST / Gi Gstreamer
This commit is contained in:
Mathieu Virbel 2013-12-14 11:29:12 -08:00
commit 6de7f1dcd0
12 changed files with 733 additions and 136 deletions

View File

@ -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
-----------

View File

@ -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'), }

View File

@ -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')]

136
kivy/core/audio/audio_gi.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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'), )

View File

@ -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()

View File

@ -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):

View File

@ -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)

243
kivy/core/video/video_gi.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -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)