Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpd: Updated to GStreamer 1.0 #2062

Merged
merged 7 commits into from
Jun 20, 2016
2 changes: 1 addition & 1 deletion beetsplug/bpd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,7 @@ def start_bpd(self, lib, host, port, password, volume, debug):
server.run()
except NoGstreamerError:
global_log.error(u'Gstreamer Python bindings not found.')
global_log.error(u'Install "python-gst0.10", "py27-gst-python", '
global_log.error(u'Install "python-gst-1.0" '
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably recommend the generic pygobject package (i.e., python-gi or python2-gobject) instead of a special-purpose python-gstreamer library.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

u'or similar package to use BPD.')

def commands(self):
Expand Down
77 changes: 52 additions & 25 deletions beetsplug/bpd/gstplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@

import sys
import time
import gobject
import thread
import os
import copy
import urllib

import pygst
pygst.require('0.10')
import gst # noqa
import gi
from gi.repository import GObject, Gst

gi.require_version('Gst', '1.0')

GObject.threads_init()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently, threads_init is no longer necessary and emits a warning now in modern GObject versions. This Pull Request shows us resolving this in the audioread package: https://github.com/beetbox/audioread/pull/30/files

Gst.init(None)

class QueryError(Exception):
pass

class GstPlayer(object):
"""A music player abstracting GStreamer's Playbin element.
Expand All @@ -57,8 +62,19 @@ def __init__(self, finished_callback=None):

# Set up the Gstreamer player. From the pygst tutorial:
# http://pygstdocs.berlios.de/pygst-tutorial/playbin.html
self.player = gst.element_factory_make("playbin2", "player")
fakesink = gst.element_factory_make("fakesink", "fakesink")
####
# Updated to GStreamer 1.0 with:
# https://wiki.ubuntu.com/Novacut/GStreamer1.0
self.player = Gst.ElementFactory.make("playbin", "player")

if self.player is None:
raise RuntimeError("Could not create playbin")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these user-visible errors, please use beets.ui.UserError instead of the built-in Python RuntimeError for readable error messages.


fakesink = Gst.ElementFactory.make("fakesink", "fakesink")

if fakesink is None:
raise RuntimeError("Could not create fakesink")

self.player.set_property("video-sink", fakesink)
bus = self.player.get_bus()
bus.add_signal_watch()
Expand All @@ -74,21 +90,21 @@ def _get_state(self):
"""Returns the current state flag of the playbin."""
# gst's get_state function returns a 3-tuple; we just want the
# status flag in position 1.
return self.player.get_state()[1]
return self.player.get_state(Gst.CLOCK_TIME_NONE)[1]

def _handle_message(self, bus, message):
"""Callback for status updates from GStreamer."""
if message.type == gst.MESSAGE_EOS:
if message.type == Gst.MessageType.EOS:
# file finished playing
self.player.set_state(gst.STATE_NULL)
self.player.set_state(Gst.State.NULL)
self.playing = False
self.cached_time = None
if self.finished_callback:
self.finished_callback()

elif message.type == gst.MESSAGE_ERROR:
elif message.type == Gst.MessageType.ERROR:
# error
self.player.set_state(gst.STATE_NULL)
self.player.set_state(Gst.State.NULL)
err, debug = message.parse_error()
print(u"Error: {0}".format(err))
self.playing = False
Expand All @@ -109,27 +125,27 @@ def play_file(self, path):
"""Immediately begin playing the audio file at the given
path.
"""
self.player.set_state(gst.STATE_NULL)
self.player.set_state(Gst.State.NULL)
if isinstance(path, unicode):
path = path.encode('utf8')
uri = 'file://' + urllib.quote(path)
self.player.set_property("uri", uri)
self.player.set_state(gst.STATE_PLAYING)
self.player.set_state(Gst.State.PLAYING)
self.playing = True

def play(self):
"""If paused, resume playback."""
if self._get_state() == gst.STATE_PAUSED:
self.player.set_state(gst.STATE_PLAYING)
if self._get_state() == Gst.State.PAUSED:
self.player.set_state(Gst.State.PLAYING)
self.playing = True

def pause(self):
"""Pause playback."""
self.player.set_state(gst.STATE_PAUSED)
self.player.set_state(Gst.State.PAUSED)

def stop(self):
"""Halt playback."""
self.player.set_state(gst.STATE_NULL)
self.player.set_state(Gst.State.NULL)
self.playing = False
self.cached_time = None

Expand All @@ -139,27 +155,36 @@ def run(self):
Call this function before trying to play any music with
play_file() or play().
"""

# If we don't use the MainLoop, messages are never sent.
gobject.threads_init()

def start():
loop = gobject.MainLoop()
loop = GObject.MainLoop()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modern GObject apparently recommends GLib.MainLoop() instead. Also in this PR: https://github.com/beetbox/audioread/pull/30/files

loop.run()

thread.start_new_thread(start, ())

def time(self):
"""Returns a tuple containing (position, length) where both
values are integers in seconds. If no stream is available,
returns (0, 0).
"""
fmt = gst.Format(gst.FORMAT_TIME)
fmt = Gst.Format(Gst.Format.TIME)
try:
pos = self.player.query_position(fmt, None)[0] / (10 ** 9)
length = self.player.query_duration(fmt, None)[0] / (10 ** 9)
posq = self.player.query_position(fmt)
if not posq[0]:
raise QueryError("query_position failed")
pos = posq[1] / (10 ** 9)

lengthq = self.player.query_duration(fmt)
if not lengthq[0]:
raise QueryError("query_duration failed")
length = lengthq[1] / (10 ** 9)

self.cached_time = (pos, length)
return (pos, length)

except gst.QueryError:
except QueryError:
# Stream not ready. For small gaps of time, for instance
# after seeking, the time values are unavailable. For this
# reason, we cache recent.
Expand All @@ -175,9 +200,9 @@ def seek(self, position):
self.stop()
return

fmt = gst.Format(gst.FORMAT_TIME)
fmt = Gst.Format(Gst.Format.TIME)
ns = position * 10 ** 9 # convert to nanoseconds
self.player.seek_simple(fmt, gst.SEEK_FLAG_FLUSH, ns)
self.player.seek_simple(fmt, Gst.SeekFlags.FLUSH, ns)

# save new cached time
self.cached_time = (position, cur_len)
Expand Down Expand Up @@ -208,12 +233,14 @@ def play_complicated(paths):
def next_song():
my_paths.pop(0)
p.play_file(my_paths[0])

p = GstPlayer(next_song)
p.run()
p.play_file(my_paths[0])
while my_paths:
time.sleep(1)


if __name__ == '__main__':
# A very simple command-line player. Just give it names of audio
# files on the command line; these are all played in sequence.
Expand Down