Skip to content

Commit

Permalink
player: Show error dialog for all playback errors.
Browse files Browse the repository at this point in the history
  • Loading branch information
lazka committed Jun 2, 2014
1 parent 1d91acf commit 7b12c88
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 76 deletions.
1 change: 1 addition & 0 deletions quodlibet/quodlibet.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def main():
app.library = library

from quodlibet.player import PlayerError
# this assumes that nullbe will always succeed
for backend in [config.get("player", "backend"), "nullbe"]:
try:
player = quodlibet.init_backend(backend, app.librarian)
Expand Down
20 changes: 18 additions & 2 deletions quodlibet/quodlibet/player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,25 @@


class PlayerError(Exception):
def __init__(self, short_desc, long_desc):
"""Error raised by player loading/initialization and emitted by the
error signal during playback.
Both short_desc and long_desc are meant for displaying in the UI.
They should be unicode.
"""

def __init__(self, short_desc, long_desc=None):
self.short_desc = short_desc
self.long_desc = long_desc

def __unicode__(self):
return self.short_desc + (
u"\n" + self.long_desc if self.long_desc else u"")

def __repr__(self):
return "%s(%r, %r)" % (
type(self).__name__, repr(self.short_desc), repr(self.long_desc))


def init(backend_name):
"""Imports the player backend module for the given name.
Expand All @@ -33,6 +48,7 @@ def init(backend_name):

raise PlayerError(
_("Invalid audio backend"),
_("The audio backend %r is not installed.") % backend_name)
_("The audio backend '%(backend-name)s' could not be loaded.") % {
"backend-name": backend_name})
else:
return backend
5 changes: 3 additions & 2 deletions quodlibet/quodlibet/player/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ class BasePlayer(GObject.GObject, Equalizer):
(GObject.SignalFlags.RUN_LAST, None, (object, int)),
'paused': (GObject.SignalFlags.RUN_LAST, None, ()),
'unpaused': (GObject.SignalFlags.RUN_LAST, None, ()),
'error': (GObject.SignalFlags.RUN_LAST, None, (object, str)),
}
# (song, PlayerError)
'error': (GObject.SignalFlags.RUN_LAST, None, (object, object)),
}

_gproperties_ = {
'volume': (float, 'player volume', 'the volume of the player',
Expand Down
121 changes: 72 additions & 49 deletions quodlibet/quodlibet/player/gstbe/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,10 @@ def __init_pipeline(self):
return True

pipeline = config.get("player", "gst_pipeline")
pipeline, self._pipeline_desc = GStreamerSink(pipeline)
if not pipeline:
try:
pipeline, self._pipeline_desc = GStreamerSink(pipeline)
except PlayerError as e:
self._error(e)
return False

if self._use_eq and Gst.ElementFactory.find('equalizer-10bands'):
Expand Down Expand Up @@ -259,18 +261,21 @@ def __init_pipeline(self):
assert element is not None, pipeline
bufbin.add(element)

PIPELINE_ERROR = PlayerError(_("Could not create GStreamer pipeline"))

if len(pipeline) > 1:
if not link_many(pipeline):
print_w("Could not link GStreamer pipeline")
self.__destroy_pipeline()
print_w("Linking the GStreamer pipeline failed")
self._error(PIPELINE_ERROR)
return False

# Test to ensure output pipeline can preroll
bufbin.set_state(Gst.State.READY)
result, state, pending = bufbin.get_state(timeout=STATE_CHANGE_TIMEOUT)
if result == Gst.StateChangeReturn.FAILURE:
bufbin.set_state(Gst.State.NULL)
self.__destroy_pipeline()
print_w("Prerolling the GStreamer pipeline failed")
self._error(PIPELINE_ERROR)
return False

# Make the sink of the first element the sink of the bin
Expand All @@ -279,6 +284,11 @@ def __init_pipeline(self):

self.bin = Gst.ElementFactory.make('playbin', None)
assert self.bin

bus = self.bin.get_bus()
bus.add_signal_watch()
self.__bus_id = bus.connect('message', self.__message, self._librarian)

self.bin = BufferingWrapper(self.bin, self)
self.__atf_id = self.bin.connect('about-to-finish',
self.__about_to_finish)
Expand Down Expand Up @@ -326,10 +336,6 @@ def set_prio(x):
# ReplayGain information gets lost when destroying
self.volume = self.volume

bus = self.bin.get_bus()
bus.add_signal_watch()
self.__bus_id = bus.connect('message', self.__message, self._librarian)

if self.song:
self.bin.set_property('uri', self.song("~uri"))

Expand Down Expand Up @@ -386,9 +392,18 @@ def __message(self, bus, message, librarian):
elif message.type == Gst.MessageType.TAG:
self.__tag(message.parse_tag(), librarian)
elif message.type == Gst.MessageType.ERROR:
err, debug = message.parse_error()
err = str(err).decode(const.ENCODING, 'replace')
self._error(err)
gerror, debug_info = message.parse_error()
message = u""
if gerror:
message = gerror.message.decode("utf-8").rstrip(".")
details = None
if debug_info:
# strip the first line, not user friendly
debug_info = "\n".join(debug_info.splitlines()[1:])
# can contain paths, so not sure if utf-8 in all cases
details = debug_info.decode("utf-8", errors="replace")
self._error(PlayerError(message, details))

elif message.type == Gst.MessageType.STREAM_START:
if self._in_gapless_transition:
print_d("Stream changed")
Expand All @@ -402,31 +417,39 @@ def __message(self, bus, message, librarian):
message_name = message.get_structure().get_name()

if message_name == "missing-plugin":
self.stop()
details = \
GstPbutils.missing_plugin_message_get_installer_detail(
message)
if details is not None:
message = (_(
"No GStreamer element found to handle the following "
"media format: %(format_details)r") %
{"format_details": details})
print_w(message)

context = GstPbutils.InstallPluginsContext.new()

# TODO: track success
def install_done_cb(*args):
Gst.update_registry()
res = GstPbutils.install_plugins_async(
[details], context, install_done_cb, None)
print_d("Gstreamer plugin install result: %r" % res)
if res in (GstPbutils.InstallPluginsReturn.HELPER_MISSING,
GstPbutils.InstallPluginsReturn.INTERNAL_FAILURE):
self._error(message)
self.__handle_missing_plugin(message)

return True

def __handle_missing_plugin(self, message):
get_installer_detail = \
GstPbutils.missing_plugin_message_get_installer_detail
get_description = GstPbutils.missing_plugin_message_get_description

details = get_installer_detail(message)
if details is None:
return

self.stop()

format_desc = get_description(message)
title = _(u"No GStreamer element found to handle media format")
details = _(u"Media format: %(format-description)s") % {
"format-description": format_desc.decode("utf-8")}

# TODO: track success
def install_done_cb(*args):
Gst.update_registry()

context = GstPbutils.InstallPluginsContext.new()
res = GstPbutils.install_plugins_async(
[details], context, install_done_cb, None)
print_d("Gstreamer plugin install result: %r" % res)

if res in (GstPbutils.InstallPluginsReturn.HELPER_MISSING,
GstPbutils.InstallPluginsReturn.INTERNAL_FAILURE):
self._error(PlayerError(title, details))

def __about_to_finish(self, pipeline):
print_d("About to finish")

Expand Down Expand Up @@ -522,28 +545,29 @@ def _set_paused(self, paused):
else:
if self.__init_pipeline():
self.bin.set_state(Gst.State.PLAYING)
else:
# Backend error; show message and halt playback
ErrorMessage(None, _("Output Error"),
_("GStreamer output pipeline could not be "
"initialized. The pipeline might be invalid, "
"or the device may be in use. Check the "
"player preferences.")).run()
self.emit((paused and 'paused') or 'unpaused')
self._paused = paused = True

self.emit((paused and 'paused') or 'unpaused')

def _get_paused(self):
return self._paused
paused = property(_get_paused, _set_paused)

def _error(self, message):
def _error(self, player_error):
"""Destroy the pipeline and set the error state.
The passed PlayerError will be emitted through the 'error' signal.
"""

# prevent recursive errors
if self.error:
return

self.__destroy_pipeline()
self.error = True
self.paused = True
print_w(message)
self.emit('error', self.song, message)

print_w(unicode(player_error))
self.emit('error', self.song, player_error)

def seek(self, pos):
"""Seek to a position in the song, in milliseconds."""
Expand Down Expand Up @@ -607,8 +631,7 @@ def _end(self, stopped):
# entire pipeline and recreate it each time we're not in
# a gapless transition.
self.__destroy_pipeline()
if not self.__init_pipeline():
self.paused = True
self.__init_pipeline()
if self.bin:
if self.paused:
self.bin.set_state(Gst.State.PAUSED)
Expand Down Expand Up @@ -718,4 +741,4 @@ def init(librarian):
raise PlayerError(
_("Unable to open input files"),
_("GStreamer has no element to handle reading files. Check "
"your GStreamer installation settings."))
"your GStreamer installation settings."))
22 changes: 9 additions & 13 deletions quodlibet/quodlibet/player/gstbe/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from gi.repository import GLib, Gst

from quodlibet.util.string import decode
from quodlibet.player import PlayerError


def link_many(elements):
Expand Down Expand Up @@ -48,7 +49,7 @@ def iter_to_list(func):
def find_audio_sink():
"""Get the best audio sink available.
Returns (element or None, description).
Returns (element, description) or raises PlayerError.
"""

if os.name == "nt":
Expand All @@ -68,7 +69,7 @@ def find_audio_sink():
if element is not None:
return (element, name)
else:
return (None, "")
raise PlayerError(_("No GStreamer audio sink found"))


def GStreamerSink(pipeline_desc):
Expand All @@ -78,15 +79,16 @@ def GStreamerSink(pipeline_desc):
`pipeline_desc` can be gst-launch syntax for multiple elements
with or without an audiosink.
In case of an error the returned list will be None instead.
In case of an error, raises PlayerError
"""

pipe = None
if pipeline_desc:
try:
pipe = [Gst.parse_launch(e) for e in pipeline_desc.split('!')]
except GLib.GError:
print_w(_("Invalid GStreamer output pipeline, trying default."))
except GLib.GError as e:
message = e.message.decode("utf-8")
raise PlayerError(_("Invalid GStreamer output pipeline"), message)

if pipe:
# In case the last element is linkable with a fakesink
Expand All @@ -95,18 +97,12 @@ def GStreamerSink(pipeline_desc):
if link_many([pipe[-1], fake]):
unlink_many([pipe[-1], fake])
default_elm, default_desc = find_audio_sink()
if default_elm:
pipe += [default_elm]
pipeline_desc += " ! " + default_desc
else:
pipe = None
pipe += [default_elm]
pipeline_desc += " ! " + default_desc
else:
elm, pipeline_desc = find_audio_sink()
pipe = [elm]

if not pipe:
print_w(_("Could not create default GStreamer pipeline."))

return pipe, pipeline_desc


Expand Down
3 changes: 2 additions & 1 deletion quodlibet/quodlibet/player/nullbe.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# published by the Free Software Foundation

from quodlibet.player._base import BasePlayer
from quodlibet.player import PlayerError
from quodlibet.qltk.songlist import PlaylistModel


Expand Down Expand Up @@ -47,7 +48,7 @@ def do_set_property(self, property, v):

def _error(self, message):
self.paused = True
self.emit('error', self.song, message)
self.emit('error', self.song, PlayerError(message))

def seek(self, pos):
"""Seek to a position in the song, in milliseconds."""
Expand Down
15 changes: 12 additions & 3 deletions quodlibet/quodlibet/player/xinebe/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class XinePlaylistPlayer(BasePlayer):
_paused = True

def __init__(self, driver, librarian):
"""May raise PlayerError"""

super(XinePlaylistPlayer, self).__init__()
self.name = "xine"
self.version_info = "xine-lib: " + xine_get_version_string()
Expand Down Expand Up @@ -145,7 +147,8 @@ def _event_listener(self, user_data, event):
message = string_at(addressof(msg) + msg.explanation)
else:
message = "xine error %s" % msg.type
GLib.idle_add(self._error, message)
message = message.decode("utf-8", errors="replace")
GLib.idle_add(self._error, PlayerError(message))
return True

def do_set_property(self, property, v):
Expand Down Expand Up @@ -201,13 +204,17 @@ def _set_paused(self, paused):

paused = property(lambda s: s._paused, _set_paused)

def _error(self, message):
def _error(self, player_error=None):
if self._destroyed:
return False

if self.error:
return False

self.error = True
self.paused = True
self.emit('error', self.song, message)
if player_error:
self.emit('error', self.song, player_error)

def seek(self, pos):
"""Seek to a position in the song, in milliseconds."""
Expand Down Expand Up @@ -278,6 +285,8 @@ def can_play_uri(self, uri):


def init(librarian):
"""May raise PlayerError"""

try:
driver = config.get("settings", "xine_driver")
except:
Expand Down
Loading

0 comments on commit 7b12c88

Please sign in to comment.