Esempio n. 1
0
    def __init__(self, librarian=None):
        GStreamerPluginHandler.__init__(self)
        BasePlayer.__init__(self)

        self._librarian = librarian

        self.version_info = "GStreamer: %s" % fver(Gst.version())
        self._pipeline_desc = None

        self._volume = 1.0
        self._paused = True
        self._mute = False

        self._in_gapless_transition = False
        self._active_error = False

        self.bin = None
        self._seeker = None
        self._int_vol_element = None
        self._ext_vol_element = None
        self._ext_mute_element = None
        self._use_eq = False
        self._eq_element = None
        self.__info_buffer = None

        self._lib_id = librarian.connect("changed", self.__songs_changed)
        self.__atf_id = None
        self.__bus_id = None
        self._runner = MainRunner()
Esempio n. 2
0
 def __init__(self, librarian=None):
     GStreamerPluginHandler.__init__(self)
     super(GStreamerPlayer, self).__init__()
     self.version_info = "GStreamer: %s" % fver(Gst.version())
     self._librarian = librarian
     self._pipeline_desc = None
     self._lib_id = librarian.connect("changed", self.__songs_changed)
     self._active_seeks = []
     self._runner = MainRunner()
Esempio n. 3
0
    def __init__(self, librarian=None):
        GStreamerPluginHandler.__init__(self)
        BasePlayer.__init__(self)

        self._librarian = librarian

        self.version_info = "GStreamer: %s" % fver(Gst.version())
        self._pipeline_desc = None

        self._volume = 1.0
        self._paused = True
        self._mute = False

        self._in_gapless_transition = False
        self._active_error = False

        self.bin = None
        self._seeker = None
        self._int_vol_element = None
        self._ext_vol_element = None
        self._ext_mute_element = None
        self._use_eq = False
        self._eq_element = None
        self.__info_buffer = None

        self._lib_id = librarian.connect("changed", self.__songs_changed)
        self.__atf_id = None
        self.__bus_id = None
        self._runner = MainRunner()
Esempio n. 4
0
 def __init__(self, librarian=None):
     GStreamerPluginHandler.__init__(self)
     super(GStreamerPlayer, self).__init__()
     self.version_info = "GStreamer: %s" % fver(Gst.version())
     self._librarian = librarian
     self._pipeline_desc = None
     self._lib_id = librarian.connect("changed", self.__songs_changed)
     self._active_seeks = []
     self._runner = MainRunner()
Esempio n. 5
0
class GStreamerPlayer(BasePlayer, GStreamerPluginHandler):

    def PlayerPreferences(self):
        return GstPlayerPreferences(self, const.DEBUG)

    def __init__(self, librarian=None):
        GStreamerPluginHandler.__init__(self)
        BasePlayer.__init__(self)

        self._librarian = librarian

        self.version_info = "GStreamer: %s" % fver(Gst.version())
        self._pipeline_desc = None

        self._volume = 1.0
        self._paused = True
        self._mute = False

        self._in_gapless_transition = False
        self._active_error = False

        self.bin = None
        self._seeker = None
        self._int_vol_element = None
        self._ext_vol_element = None
        self._ext_mute_element = None
        self._use_eq = False
        self._eq_element = None
        self.__info_buffer = None

        self._lib_id = librarian.connect("changed", self.__songs_changed)
        self.__atf_id = None
        self.__bus_id = None
        self._runner = MainRunner()

    def __songs_changed(self, librarian, songs):
        # replaygain values might have changed, recalc volume
        if self.song and self.song in songs:
            self._reset_replaygain()

    def _destroy(self):
        self._librarian.disconnect(self._lib_id)
        self._runner.abort()
        self.__destroy_pipeline()

    @property
    def name(self):
        name = "GStreamer"
        if self._pipeline_desc:
            name += " (%s)" % self._pipeline_desc
        return name

    @property
    def has_external_volume(self):
        ext = self._ext_vol_element
        if ext is None or not sink_has_external_state(ext):
            return False
        return True

    def _set_buffer_duration(self, duration):
        """Set the stream buffer duration in msecs"""

        config.set("player", "gst_buffer", float(duration) / 1000)

        if self.bin:
            value = duration * Gst.MSECOND
            self.bin.set_property('buffer-duration', value)

    def _print_pipeline(self):
        """Print debug information for the active pipeline to stdout
        (elements, formats, ...)
        """

        if self.bin:
            # self.bin is just a wrapper, so get the real one
            for line in bin_debug([self.bin.bin]):
                print_(line)
        else:
            print_e("No active pipeline.")

    def __init_pipeline(self):
        """Creates a gstreamer pipeline. Returns True on success."""

        if self.bin:
            return True

        # reset error state
        self.error = False

        pipeline = config.get("player", "gst_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'):
            # The equalizer only operates on 16-bit ints or floats, and
            # will only pass these types through even when inactive.
            # We push floats through to this point, then let the second
            # audioconvert handle pushing to whatever the rest of the
            # pipeline supports. As a bonus, this seems to automatically
            # select the highest-precision format supported by the
            # rest of the chain.
            filt = Gst.ElementFactory.make('capsfilter', None)
            filt.set_property('caps',
                              Gst.Caps.from_string('audio/x-raw,format=F32LE'))
            eq = Gst.ElementFactory.make('equalizer-10bands', None)
            self._eq_element = eq
            self.update_eq_values()
            conv = Gst.ElementFactory.make('audioconvert', None)
            resample = Gst.ElementFactory.make('audioresample', None)
            pipeline = [filt, eq, conv, resample] + pipeline

        # playbin2 has started to control the volume through pulseaudio,
        # which means the volume property can change without us noticing.
        # Use our own volume element for now until this works with PA.
        self._int_vol_element = Gst.ElementFactory.make('volume', None)
        pipeline.insert(0, self._int_vol_element)

        # Get all plugin elements and append audio converters.
        # playbin already includes one at the end
        plugin_pipeline = []
        for plugin in self._get_plugin_elements():
            plugin_pipeline.append(plugin)
            plugin_pipeline.append(
                Gst.ElementFactory.make('audioconvert', None))
            plugin_pipeline.append(
                Gst.ElementFactory.make('audioresample', None))
        pipeline = plugin_pipeline + pipeline

        bufbin = Gst.Bin()
        for element in pipeline:
            assert element is not None, pipeline
            bufbin.add(element)

        if len(pipeline) > 1:
            if not link_many(pipeline):
                print_w("Linking the GStreamer pipeline failed")
                self._error(
                    PlayerError(_("Could not create GStreamer pipeline")))
                return False

        # see if the sink provides a volume property, if yes, use it
        sink_element = pipeline[-1]
        if isinstance(sink_element, Gst.Bin):
            sink_element = iter_to_list(sink_element.iterate_recurse)[-1]

        self._ext_vol_element = None
        if hasattr(sink_element.props, "volume"):
            self._ext_vol_element = sink_element

            # In case we use the sink volume directly we can increase buffering
            # without affecting the volume change delay too much and safe some
            # CPU time... (2x default for now).
            if hasattr(sink_element.props, "buffer_time"):
                sink_element.set_property("buffer-time", 400000)

            def ext_volume_notify(*args):
                # gets called from a thread
                GLib.idle_add(self.notify, "volume")

            self._ext_vol_element.connect("notify::volume", ext_volume_notify)

        self._ext_mute_element = None
        if hasattr(sink_element.props, "mute") and \
                sink_element.get_factory().get_name() != "directsoundsink":
            # directsoundsink has a mute property but it doesn't work
            # https://bugzilla.gnome.org/show_bug.cgi?id=755106
            self._ext_mute_element = sink_element

            def mute_notify(*args):
                # gets called from a thread
                GLib.idle_add(self.notify, "mute")

            self._ext_mute_element.connect("notify::mute", mute_notify)

        # Make the sink of the first element the sink of the bin
        gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink'))
        bufbin.add_pad(gpad)

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

        self.bin = BufferingWrapper(bin_, self)
        self._seeker = Seeker(self.bin, self)

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

        self.__atf_id = self.bin.connect('about-to-finish',
            self.__about_to_finish)

        # set buffer duration
        duration = config.getfloat("player", "gst_buffer")
        self._set_buffer_duration(int(duration * 1000))

        # connect playbin to our pluing/volume/eq pipeline
        self.bin.set_property('audio-sink', bufbin)

        # by default playbin will render video -> suppress using fakesink
        fakesink = Gst.ElementFactory.make('fakesink', None)
        self.bin.set_property('video-sink', fakesink)

        # disable all video/text decoding in playbin
        GST_PLAY_FLAG_VIDEO = 1 << 0
        GST_PLAY_FLAG_TEXT = 1 << 2
        flags = self.bin.get_property("flags")
        flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT)
        self.bin.set_property("flags", flags)

        if not self.has_external_volume:
            # Restore volume/ReplayGain and mute state
            self.volume = self._volume
            self.mute = self._mute

        # ReplayGain information gets lost when destroying
        self._reset_replaygain()

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

        return True

    def __destroy_pipeline(self):
        self._remove_plugin_elements()

        if self.__bus_id:
            bus = self.bin.get_bus()
            bus.disconnect(self.__bus_id)
            bus.remove_signal_watch()
            self.__bus_id = None

        if self.__atf_id:
            self.bin.disconnect(self.__atf_id)
            self.__atf_id = None

        if self._seeker is not None:
            self._seeker.destroy()
            self._seeker = None
            self.notify("seekable")

        if self.bin:
            self.bin.set_state(Gst.State.NULL)
            self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)
            # BufferingWrapper cleanup
            self.bin.destroy()
            self.bin = None

        self._in_gapless_transition = False

        self._ext_vol_element = None
        self._int_vol_element = None
        self._ext_mute_element = None
        self._eq_element = None

    def _rebuild_pipeline(self):
        """If a pipeline is active, rebuild it and restore vol, position etc"""

        if not self.bin:
            return

        paused = self.paused
        pos = self.get_position()

        self.__destroy_pipeline()
        self.paused = True
        self.__init_pipeline()
        self.paused = paused
        self.seek(pos)

    def __message(self, bus, message, librarian):
        if message.type == Gst.MessageType.EOS:
            print_d("Stream EOS")
            if not self._in_gapless_transition:
                self._source.next_ended()
            self._end(False)
        elif message.type == Gst.MessageType.TAG:
            self.__tag(message.parse_tag(), librarian)
        elif message.type == Gst.MessageType.ERROR:
            gerror, debug_info = message.parse_error()
            message = u""
            if gerror:
                message = gerror.message.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
            self._error(PlayerError(message, details))
        elif message.type == Gst.MessageType.STATE_CHANGED:
            # pulsesink doesn't notify a volume change on startup
            # and the volume is only valid in > paused states.
            if message.src is self._ext_vol_element:
                self.notify("volume")
            if message.src is self._ext_mute_element:
                self.notify("mute")
        elif message.type == Gst.MessageType.STREAM_START:
            if self._in_gapless_transition:
                print_d("Stream changed")
                self._end(False)
        elif message.type == Gst.MessageType.ELEMENT:
            message_name = message.get_structure().get_name()

            if message_name == "missing-plugin":
                self.__handle_missing_plugin(message)
        elif message.type == Gst.MessageType.CLOCK_LOST:
            print_d("Clock lost")
            self.bin.set_state(Gst.State.PAUSED)
            self.bin.set_state(Gst.State.PLAYING)
        elif message.type == Gst.MessageType.LATENCY:
            print_d("Recalculate latency")
            self.bin.recalculate_latency()
        elif message.type == Gst.MessageType.REQUEST_STATE:
            state = message.parse_request_state()
            print_d("State requested: %s" % Gst.Element.state_get_name(state))
            self.bin.set_state(state)
        elif message.type == Gst.MessageType.DURATION_CHANGED:
            if self.song.fill_length:
                ok, p = self.bin.query_duration(Gst.Format.TIME)
                if ok:
                    p /= float(Gst.SECOND)
                    self.song["~#length"] = p
                    librarian.changed([self.song])

    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")
        error_details = _(u"Media format: %(format-description)s") % {
            "format-description": format_desc}

        def install_done_cb(plugins_return, *args):
            print_d("Gstreamer plugin install return: %r" % plugins_return)
            Gst.update_registry()

        context = GstPbutils.InstallPluginsContext.new()

        # new in 1.6
        if hasattr(context, "set_desktop_id"):
            from gi.repository import Gtk
            context.set_desktop_id(app.id)

        # new in 1.6
        if hasattr(context, "set_startup_notification_id"):
            current_time = Gtk.get_current_event_time()
            context.set_startup_notification_id("_TIME%d" % current_time)

        gdk_window = app.window.get_window()
        if gdk_window:
            try:
                xid = gdk_window.get_xid()
            except AttributeError:  # non X11
                pass
            else:
                context.set_xid(xid)

        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, error_details))

    def __about_to_finish_sync(self):
        """Returns the next song uri to play or None"""

        print_d("About to finish (sync)")

        # Chained oggs falsely trigger a gapless transition.
        # At least for radio streams we can safely ignore it because
        # transitions don't occur there.
        # https://github.com/quodlibet/quodlibet/issues/1454
        # https://bugzilla.gnome.org/show_bug.cgi?id=695474
        if self.song.multisong:
            print_d("multisong: ignore about to finish")
            return

        # mod + gapless deadlocks
        # https://github.com/quodlibet/quodlibet/issues/2780
        if isinstance(self.song, ModFile):
            return

        if config.getboolean("player", "gst_disable_gapless"):
            print_d("Gapless disabled")
            return

        # this can trigger twice, see issue 987
        if self._in_gapless_transition:
            return
        self._in_gapless_transition = True

        print_d("Select next song in mainloop..")
        self._source.next_ended()
        print_d("..done.")

        song = self._source.current
        if song is not None:
            return song("~uri")

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

        try:
            uri = self._runner.call(self.__about_to_finish_sync,
                                    priority=GLib.PRIORITY_HIGH,
                                    timeout=0.5)
        except MainRunnerTimeoutError as e:
            # Due to some locks being held during this signal we can get
            # into a deadlock when a seek or state change event happens
            # in the mainloop before our function gets scheduled.
            # In this case abort and do nothing, which results
            # in a non-gapless transition.
            print_d("About to finish (async): %s" % e)
            return
        except MainRunnerAbortedError as e:
            print_d("About to finish (async): %s" % e)
            return
        except MainRunnerError:
            util.print_exc()
            return

        if uri is not None:
            print_d("About to finish (async): setting uri")
            playbin.set_property('uri', uri)
        print_d("About to finish (async): done")

    def stop(self):
        super(GStreamerPlayer, self).stop()
        self.__destroy_pipeline()

    def do_get_property(self, property):
        if property.name == 'volume':
            if self._ext_vol_element is not None and \
                    sink_has_external_state(self._ext_vol_element) and \
                    sink_state_is_valid(self._ext_vol_element):
                # never read back the volume if we don't have to, e.g.
                # directsoundsink maps volume to an int which makes UI
                # sliders jump if we read the value back
                self._volume = self._ext_vol_element.get_property("volume")
            return self._volume
        elif property.name == "mute":
            if self._ext_mute_element is not None and \
                    sink_has_external_state(self._ext_mute_element) and \
                    sink_state_is_valid(self._ext_mute_element):
                self._mute = self._ext_mute_element.get_property("mute")
            return self._mute
        elif property.name == "seekable":
            if self._seeker is not None:
                return self._seeker.seekable
            return False
        else:
            raise AttributeError

    def _reset_replaygain(self):
        if not self.bin:
            return

        v = 1.0 if self._ext_vol_element is not None else self._volume
        v = self.calc_replaygain_volume(v)
        v = min(10.0, max(0.0, v))
        self._int_vol_element.set_property('volume', v)

    def do_set_property(self, property, v):
        if property.name == 'volume':
            self._volume = v
            if self._ext_vol_element:
                v = min(10.0, max(0.0, v))
                self._ext_vol_element.set_property("volume", v)
            else:
                v = self.calc_replaygain_volume(v)
                if self.bin:
                    v = min(10.0, max(0.0, v))
                    self._int_vol_element.set_property('volume', v)
        elif property.name == 'mute':
            self._mute = v
            if self._ext_mute_element is not None:
                self._ext_mute_element.set_property("mute", v)
            else:
                if self.bin:
                    self._int_vol_element.set_property("mute", v)
        else:
            raise AttributeError

    def get_position(self):
        """Return the current playback position in milliseconds,
        or 0 if no song is playing."""

        if self._seeker:
            return self._seeker.get_position()
        return 0

    @property
    def paused(self):
        return self._paused

    @paused.setter
    def paused(self, paused):
        if paused == self._paused:
            return

        self._paused = paused
        self.emit((paused and 'paused') or 'unpaused')
        # in case a signal handler changed the paused state, abort this
        if self._paused != paused:
            return

        if paused:
            if self.bin:
                if not self.song:
                    # Something wants us to pause between songs, or when
                    # we've got no song playing (probably StopAfterMenu).
                    self.__destroy_pipeline()
                elif self.seekable:
                    self.bin.set_state(Gst.State.PAUSED)
                else:
                    q = Gst.Query.new_buffering(Gst.Format.DEFAULT)
                    if self.bin.query(q):
                        # destroy so that we rebuffer on resume i.e. we don't
                        # want to continue unseekable streams from where we
                        # paused but from where we unpaused.
                        self.__destroy_pipeline()
                    else:
                        self.bin.set_state(Gst.State.PAUSED)
        else:
            if self.song and self.__init_pipeline():
                self.bin.set_state(Gst.State.PLAYING)

    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._active_error:
            return
        self._active_error = True

        self.__destroy_pipeline()
        self.error = True
        self.paused = True

        print_w(player_error)
        self.emit('error', self.song, player_error)
        self._active_error = False

    def seek(self, pos):
        """Seek to a position in the song, in milliseconds."""

        # Don't allow seeking during gapless. We can't go back to the old song.
        if not self.song or self._in_gapless_transition:
            return

        if self.__init_pipeline():
            self._seeker.set_position(pos)

    def sync(self, timeout):
        if self.bin is not None:
            # XXX: This is flaky, try multiple times
            for i in range(5):
                self.bin.get_state(Gst.SECOND * timeout)
                # we have some logic in the main loop, so iterate there
                while GLib.MainContext.default().iteration(False):
                    pass

    def _end(self, stopped, next_song=None):
        print_d("End song")
        song, info = self.song, self.info

        # set the new volume before the signals to avoid delays
        if self._in_gapless_transition:
            self.song = self._source.current
            self._reset_replaygain()

        # We need to set self.song to None before calling our signal
        # handlers. Otherwise, if they try to end the song they're given
        # (e.g. by removing it), then we get in an infinite loop.
        self.__info_buffer = self.song = self.info = None
        if song is not info:
            self.emit('song-ended', info, stopped)
        self.emit('song-ended', song, stopped)

        current = self._source.current if next_song is None else next_song

        # Then, set up the next song.
        self.song = self.info = current

        print_d("Next song")
        if self.song is not None:
            if not self._in_gapless_transition:
                # Due to extensive problems with playbin2, we destroy the
                # entire pipeline and recreate it each time we're not in
                # a gapless transition.
                self.__destroy_pipeline()
                self.__init_pipeline()
            if self.bin:
                if self.paused:
                    self.bin.set_state(Gst.State.PAUSED)
                else:
                    # something unpaused while no song was active
                    if song is None:
                        self.emit("unpaused")
                    self.bin.set_state(Gst.State.PLAYING)
        else:
            self.__destroy_pipeline()

        self._in_gapless_transition = False

        if self._seeker is not None:
            # we could have a gapless transition to a non-seekable -> update
            self._seeker.reset()

        self.emit('song-started', self.song)

        if self.song is None:
            self.paused = True

    def __tag(self, tags, librarian):
        if self.song and self.song.multisong:
            self._fill_stream(tags, librarian)
        elif self.song and self.song.fill_metadata:
            pass

    def _fill_stream(self, tags, librarian):
        # get a new remote file
        new_info = self.__info_buffer
        if not new_info:
            new_info = type(self.song)(self.song["~filename"])
            new_info.multisong = False
            new_info.streamsong = True

            # copy from the old songs
            # we should probably listen to the library for self.song changes
            new_info.update(self.song)
            new_info.update(self.info)

        changed = False
        info_changed = False

        tags = TagListWrapper(tags, merge=True)
        tags = parse_gstreamer_taglist(tags)

        for key, value in sanitize_tags(tags, stream=False).items():
            if self.song.get(key) != value:
                changed = True
                self.song[key] = value

        for key, value in sanitize_tags(tags, stream=True).items():
            if new_info.get(key) != value:
                info_changed = True
                new_info[key] = value

        if info_changed:
            # in case the title changed, make self.info a new instance
            # and emit ended/started for the the old/new one
            if self.info.get("title") != new_info.get("title"):
                if self.info is not self.song:
                    self.emit('song-ended', self.info, False)
                self.info = new_info
                self.__info_buffer = None
                self.emit('song-started', self.info)
            else:
                # in case title didn't changed, update the values of the
                # old instance if there is one and tell the library.
                if self.info is not self.song:
                    self.info.update(new_info)
                    librarian.changed([self.info])
                else:
                    # So we don't loose all tags before the first title
                    # save it for later
                    self.__info_buffer = new_info

        if changed:
            librarian.changed([self.song])

    @property
    def eq_bands(self):
        if Gst.ElementFactory.find('equalizer-10bands'):
            return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011]
        else:
            return []

    def update_eq_values(self):
        need_eq = any(self._eq_values)
        if need_eq != self._use_eq:
            self._use_eq = need_eq
            self._rebuild_pipeline()

        if self._eq_element:
            for band, val in enumerate(self._eq_values):
                self._eq_element.set_property('band%d' % band, val)

    def can_play_uri(self, uri):
        if not Gst.uri_is_valid(uri):
            return False
        try:
            Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '')
        except GLib.GError:
            return False
        return True
Esempio n. 6
0
class GStreamerPlayer(BasePlayer, GStreamerPluginHandler):
    def PlayerPreferences(self):
        return GstPlayerPreferences(self, const.DEBUG)

    def __init__(self, librarian=None):
        GStreamerPluginHandler.__init__(self)
        BasePlayer.__init__(self)

        self._librarian = librarian

        self.version_info = "GStreamer: %s" % fver(Gst.version())
        self._pipeline_desc = None

        self._volume = 1.0
        self._paused = True
        self._mute = False

        self._in_gapless_transition = False
        self._active_error = False

        self.bin = None
        self._seeker = None
        self._int_vol_element = None
        self._ext_vol_element = None
        self._ext_mute_element = None
        self._use_eq = False
        self._eq_element = None
        self.__info_buffer = None

        self._lib_id = librarian.connect("changed", self.__songs_changed)
        self.__atf_id = None
        self.__bus_id = None
        self._runner = MainRunner()

    def __songs_changed(self, librarian, songs):
        # replaygain values might have changed, recalc volume
        if self.song and self.song in songs:
            self._reset_replaygain()

    def _destroy(self):
        self._librarian.disconnect(self._lib_id)
        self._runner.abort()
        self.__destroy_pipeline()

    @property
    def name(self):
        name = "GStreamer"
        if self._pipeline_desc:
            name += " (%s)" % self._pipeline_desc
        return name

    @property
    def has_external_volume(self):
        ext = self._ext_vol_element
        if ext is None or not sink_has_external_state(ext):
            return False
        return True

    def _set_buffer_duration(self, duration):
        """Set the stream buffer duration in msecs"""

        config.set("player", "gst_buffer", float(duration) / 1000)

        if self.bin:
            value = duration * Gst.MSECOND
            self.bin.set_property('buffer-duration', value)

    def _print_pipeline(self):
        """Print debug information for the active pipeline to stdout
        (elements, formats, ...)
        """

        if self.bin:
            # self.bin is just a wrapper, so get the real one
            for line in bin_debug([self.bin.bin]):
                print_(line)
        else:
            print_e("No active pipeline.")

    def __init_pipeline(self):
        """Creates a gstreamer pipeline. Returns True on success."""

        if self.bin:
            return True

        # reset error state
        self.error = False

        pipeline = config.get("player", "gst_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'):
            # The equalizer only operates on 16-bit ints or floats, and
            # will only pass these types through even when inactive.
            # We push floats through to this point, then let the second
            # audioconvert handle pushing to whatever the rest of the
            # pipeline supports. As a bonus, this seems to automatically
            # select the highest-precision format supported by the
            # rest of the chain.
            filt = Gst.ElementFactory.make('capsfilter', None)
            filt.set_property('caps',
                              Gst.Caps.from_string('audio/x-raw,format=F32LE'))
            eq = Gst.ElementFactory.make('equalizer-10bands', None)
            self._eq_element = eq
            self.update_eq_values()
            conv = Gst.ElementFactory.make('audioconvert', None)
            resample = Gst.ElementFactory.make('audioresample', None)
            pipeline = [filt, eq, conv, resample] + pipeline

        # playbin2 has started to control the volume through pulseaudio,
        # which means the volume property can change without us noticing.
        # Use our own volume element for now until this works with PA.
        self._int_vol_element = Gst.ElementFactory.make('volume', None)
        pipeline.insert(0, self._int_vol_element)

        # Get all plugin elements and append audio converters.
        # playbin already includes one at the end
        plugin_pipeline = []
        for plugin in self._get_plugin_elements():
            plugin_pipeline.append(plugin)
            plugin_pipeline.append(
                Gst.ElementFactory.make('audioconvert', None))
            plugin_pipeline.append(
                Gst.ElementFactory.make('audioresample', None))
        pipeline = plugin_pipeline + pipeline

        bufbin = Gst.Bin()
        for element in pipeline:
            assert element is not None, pipeline
            bufbin.add(element)

        if len(pipeline) > 1:
            if not link_many(pipeline):
                print_w("Linking the GStreamer pipeline failed")
                self._error(
                    PlayerError(_("Could not create GStreamer pipeline")))
                return False

        # see if the sink provides a volume property, if yes, use it
        sink_element = pipeline[-1]
        if isinstance(sink_element, Gst.Bin):
            sink_element = iter_to_list(sink_element.iterate_recurse)[-1]

        self._ext_vol_element = None
        if hasattr(sink_element.props, "volume"):
            self._ext_vol_element = sink_element

            # In case we use the sink volume directly we can increase buffering
            # without affecting the volume change delay too much and safe some
            # CPU time... (2x default for now).
            if hasattr(sink_element.props, "buffer_time"):
                sink_element.set_property("buffer-time", 400000)

            def ext_volume_notify(*args):
                # gets called from a thread
                GLib.idle_add(self.notify, "volume")

            self._ext_vol_element.connect("notify::volume", ext_volume_notify)

        self._ext_mute_element = None
        if hasattr(sink_element.props, "mute") and \
                sink_element.get_factory().get_name() != "directsoundsink":
            # directsoundsink has a mute property but it doesn't work
            # https://bugzilla.gnome.org/show_bug.cgi?id=755106
            self._ext_mute_element = sink_element

            def mute_notify(*args):
                # gets called from a thread
                GLib.idle_add(self.notify, "mute")

            self._ext_mute_element.connect("notify::mute", mute_notify)

        # Make the sink of the first element the sink of the bin
        gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink'))
        bufbin.add_pad(gpad)

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

        self.bin = BufferingWrapper(bin_, self)
        self._seeker = Seeker(self.bin, self)

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

        self.__atf_id = self.bin.connect('about-to-finish',
                                         self.__about_to_finish)

        # set buffer duration
        duration = config.getfloat("player", "gst_buffer")
        self._set_buffer_duration(int(duration * 1000))

        # connect playbin to our pluing/volume/eq pipeline
        self.bin.set_property('audio-sink', bufbin)

        # by default playbin will render video -> suppress using fakesink
        fakesink = Gst.ElementFactory.make('fakesink', None)
        self.bin.set_property('video-sink', fakesink)

        # disable all video/text decoding in playbin
        GST_PLAY_FLAG_VIDEO = 1 << 0
        GST_PLAY_FLAG_TEXT = 1 << 2
        flags = self.bin.get_property("flags")
        flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT)
        self.bin.set_property("flags", flags)

        # find the (uri)decodebin after setup and use autoplug-sort
        # to sort elements like decoders
        def source_setup(*args):
            def autoplug_sort(decode, pad, caps, factories):
                def set_prio(x):
                    i, f = x
                    i = {"mad": -1, "mpg123audiodec": -2}.get(f.get_name(), i)
                    return (i, f)

                return list(
                    zip(*sorted(map(set_prio, enumerate(factories)))))[1]

            for e in iter_to_list(self.bin.iterate_recurse):
                try:
                    e.connect("autoplug-sort", autoplug_sort)
                except TypeError:
                    pass
                else:
                    break

        self.bin.connect("source-setup", source_setup)

        if not self.has_external_volume:
            # Restore volume/ReplayGain and mute state
            self.volume = self._volume
            self.mute = self._mute

        # ReplayGain information gets lost when destroying
        self._reset_replaygain()

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

        return True

    def __destroy_pipeline(self):
        self._remove_plugin_elements()

        if self.__bus_id:
            bus = self.bin.get_bus()
            bus.disconnect(self.__bus_id)
            bus.remove_signal_watch()
            self.__bus_id = None

        if self.__atf_id:
            self.bin.disconnect(self.__atf_id)
            self.__atf_id = None

        if self._seeker is not None:
            self._seeker.destroy()
            self._seeker = None
            self.notify("seekable")

        if self.bin:
            self.bin.set_state(Gst.State.NULL)
            self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)
            # BufferingWrapper cleanup
            self.bin.destroy()
            self.bin = None

        self._in_gapless_transition = False

        self._ext_vol_element = None
        self._int_vol_element = None
        self._ext_mute_element = None
        self._eq_element = None

    def _rebuild_pipeline(self):
        """If a pipeline is active, rebuild it and restore vol, position etc"""

        if not self.bin:
            return

        paused = self.paused
        pos = self.get_position()

        self.__destroy_pipeline()
        self.paused = True
        self.__init_pipeline()
        self.paused = paused
        self.seek(pos)

    def __message(self, bus, message, librarian):
        if message.type == Gst.MessageType.EOS:
            print_d("Stream EOS")
            if not self._in_gapless_transition:
                self._source.next_ended()
            self._end(False)
        elif message.type == Gst.MessageType.TAG:
            self.__tag(message.parse_tag(), librarian)
        elif message.type == Gst.MessageType.ERROR:
            gerror, debug_info = message.parse_error()
            message = u""
            if gerror:
                message = util.gdecode(gerror.message).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 = util.gdecode(debug_info)
            self._error(PlayerError(message, details))
        elif message.type == Gst.MessageType.STATE_CHANGED:
            # pulsesink doesn't notify a volume change on startup
            # and the volume is only valid in > paused states.
            if message.src is self._ext_vol_element:
                self.notify("volume")
            if message.src is self._ext_mute_element:
                self.notify("mute")
        elif message.type == Gst.MessageType.STREAM_START:
            if self._in_gapless_transition:
                print_d("Stream changed")
                self._end(False)
        elif message.type == Gst.MessageType.ELEMENT:
            message_name = message.get_structure().get_name()

            if message_name == "missing-plugin":
                self.__handle_missing_plugin(message)
        elif message.type == Gst.MessageType.CLOCK_LOST:
            print_d("Clock lost")
            self.bin.set_state(Gst.State.PAUSED)
            self.bin.set_state(Gst.State.PLAYING)
        elif message.type == Gst.MessageType.LATENCY:
            print_d("Recalculate latency")
            self.bin.recalculate_latency()
        elif message.type == Gst.MessageType.REQUEST_STATE:
            state = message.parse_request_state()
            print_d("State requested: %s" % Gst.Element.state_get_name(state))
            self.bin.set_state(state)
        elif message.type == Gst.MessageType.DURATION_CHANGED:
            if self.song.fill_length:
                ok, p = self.bin.query_duration(Gst.Format.TIME)
                if ok:
                    p /= float(Gst.SECOND)
                    self.song["~#length"] = p
                    librarian.changed([self.song])

    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")
        error_details = _(u"Media format: %(format-description)s") % {
            "format-description": util.gdecode(format_desc)
        }

        def install_done_cb(plugins_return, *args):
            print_d("Gstreamer plugin install return: %r" % plugins_return)
            Gst.update_registry()

        context = GstPbutils.InstallPluginsContext.new()

        # new in 1.6
        if hasattr(context, "set_desktop_id"):
            from gi.repository import Gtk
            context.set_desktop_id(app.id)

        # new in 1.6
        if hasattr(context, "set_startup_notification_id"):
            current_time = Gtk.get_current_event_time()
            context.set_startup_notification_id("_TIME%d" % current_time)

        gdk_window = app.window.get_window()
        if gdk_window:
            try:
                xid = gdk_window.get_xid()
            except AttributeError:  # non X11
                pass
            else:
                context.set_xid(xid)

        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, error_details))

    def __about_to_finish_sync(self):
        """Returns the next song uri to play or None"""

        print_d("About to finish (sync)")

        # Chained oggs falsely trigger a gapless transition.
        # At least for radio streams we can safely ignore it because
        # transitions don't occur there.
        # https://github.com/quodlibet/quodlibet/issues/1454
        # https://bugzilla.gnome.org/show_bug.cgi?id=695474
        if self.song.multisong:
            print_d("multisong: ignore about to finish")
            return

        if config.getboolean("player", "gst_disable_gapless"):
            print_d("Gapless disabled")
            return

        # this can trigger twice, see issue 987
        if self._in_gapless_transition:
            return
        self._in_gapless_transition = True

        print_d("Select next song in mainloop..")
        self._source.next_ended()
        print_d("..done.")

        song = self._source.current
        if song is not None:
            return song("~uri")

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

        try:
            uri = self._runner.call(self.__about_to_finish_sync,
                                    priority=GLib.PRIORITY_HIGH,
                                    timeout=0.5)
        except MainRunnerTimeoutError as e:
            # Due to some locks being held during this signal we can get
            # into a deadlock when a seek or state change event happens
            # in the mainloop before our function gets scheduled.
            # In this case abort and do nothing, which results
            # in a non-gapless transition.
            print_d("About to finish (async): %s" % e)
            return
        except MainRunnerAbortedError as e:
            print_d("About to finish (async): %s" % e)
            return
        except MainRunnerError:
            util.print_exc()
            return

        if uri is not None:
            print_d("About to finish (async): setting uri")
            playbin.set_property('uri', uri)
        print_d("About to finish (async): done")

    def stop(self):
        super(GStreamerPlayer, self).stop()
        self.__destroy_pipeline()

    def do_get_property(self, property):
        if property.name == 'volume':
            if self._ext_vol_element is not None and \
                    sink_has_external_state(self._ext_vol_element) and \
                    sink_state_is_valid(self._ext_vol_element):
                # never read back the volume if we don't have to, e.g.
                # directsoundsink maps volume to an int which makes UI
                # sliders jump if we read the value back
                self._volume = self._ext_vol_element.get_property("volume")
            return self._volume
        elif property.name == "mute":
            if self._ext_mute_element is not None and \
                    sink_has_external_state(self._ext_mute_element) and \
                    sink_state_is_valid(self._ext_mute_element):
                self._mute = self._ext_mute_element.get_property("mute")
            return self._mute
        elif property.name == "seekable":
            if self._seeker is not None:
                return self._seeker.seekable
            return False
        else:
            raise AttributeError

    def _reset_replaygain(self):
        if not self.bin:
            return

        v = 1.0 if self._ext_vol_element is not None else self._volume
        v = self.calc_replaygain_volume(v)
        v = min(10.0, max(0.0, v))
        self._int_vol_element.set_property('volume', v)

    def do_set_property(self, property, v):
        if property.name == 'volume':
            self._volume = v
            if self._ext_vol_element:
                v = min(10.0, max(0.0, v))
                self._ext_vol_element.set_property("volume", v)
            else:
                v = self.calc_replaygain_volume(v)
                if self.bin:
                    v = min(10.0, max(0.0, v))
                    self._int_vol_element.set_property('volume', v)
        elif property.name == 'mute':
            self._mute = v
            if self._ext_mute_element is not None:
                self._ext_mute_element.set_property("mute", v)
            else:
                if self.bin:
                    self._int_vol_element.set_property("mute", v)
        else:
            raise AttributeError

    def get_position(self):
        """Return the current playback position in milliseconds,
        or 0 if no song is playing."""

        if self._seeker:
            return self._seeker.get_position()
        return 0

    @property
    def paused(self):
        return self._paused

    @paused.setter
    def paused(self, paused):
        if paused == self._paused:
            return

        self._paused = paused
        self.emit((paused and 'paused') or 'unpaused')
        # in case a signal handler changed the paused state, abort this
        if self._paused != paused:
            return

        if paused:
            if self.bin:
                if not self.song:
                    # Something wants us to pause between songs, or when
                    # we've got no song playing (probably StopAfterMenu).
                    self.__destroy_pipeline()
                elif self.seekable:
                    self.bin.set_state(Gst.State.PAUSED)
                else:
                    q = Gst.Query.new_buffering(Gst.Format.DEFAULT)
                    if self.bin.query(q):
                        # destroy so that we rebuffer on resume i.e. we don't
                        # want to continue unseekable streams from where we
                        # paused but from where we unpaused.
                        self.__destroy_pipeline()
                    else:
                        self.bin.set_state(Gst.State.PAUSED)
        else:
            if self.song and self.__init_pipeline():
                self.bin.set_state(Gst.State.PLAYING)

    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._active_error:
            return
        self._active_error = True

        self.__destroy_pipeline()
        self.error = True
        self.paused = True

        print_w(player_error)
        self.emit('error', self.song, player_error)
        self._active_error = False

    def seek(self, pos):
        """Seek to a position in the song, in milliseconds."""

        # Don't allow seeking during gapless. We can't go back to the old song.
        if not self.song or self._in_gapless_transition:
            return

        if self.__init_pipeline():
            self._seeker.set_position(pos)

    def sync(self, timeout):
        if self.bin is not None:
            # XXX: This is flaky, try multiple times
            for i in range(5):
                self.bin.get_state(Gst.SECOND * timeout)
                # we have some logic in the main loop, so iterate there
                while GLib.MainContext.default().iteration(False):
                    pass

    def _end(self, stopped, next_song=None):
        print_d("End song")
        song, info = self.song, self.info

        # set the new volume before the signals to avoid delays
        if self._in_gapless_transition:
            self.song = self._source.current
            self._reset_replaygain()

        # We need to set self.song to None before calling our signal
        # handlers. Otherwise, if they try to end the song they're given
        # (e.g. by removing it), then we get in an infinite loop.
        self.__info_buffer = self.song = self.info = None
        if song is not info:
            self.emit('song-ended', info, stopped)
        self.emit('song-ended', song, stopped)

        current = self._source.current if next_song is None else next_song

        # Then, set up the next song.
        self.song = self.info = current

        print_d("Next song")
        if self.song is not None:
            if not self._in_gapless_transition:
                # Due to extensive problems with playbin2, we destroy the
                # entire pipeline and recreate it each time we're not in
                # a gapless transition.
                self.__destroy_pipeline()
                self.__init_pipeline()
            if self.bin:
                if self.paused:
                    self.bin.set_state(Gst.State.PAUSED)
                else:
                    # something unpaused while no song was active
                    if song is None:
                        self.emit("unpaused")
                    self.bin.set_state(Gst.State.PLAYING)
        else:
            self.__destroy_pipeline()
            self.paused = True

        self._in_gapless_transition = False

        if self._seeker is not None:
            # we could have a gapless transition to a non-seekable -> update
            self._seeker.reset()

        self.emit('song-started', self.song)

    def __tag(self, tags, librarian):
        if self.song and self.song.multisong:
            self._fill_stream(tags, librarian)
        elif self.song and self.song.fill_metadata:
            pass

    def _fill_stream(self, tags, librarian):
        # get a new remote file
        new_info = self.__info_buffer
        if not new_info:
            new_info = type(self.song)(self.song["~filename"])
            new_info.multisong = False
            new_info.streamsong = True

            # copy from the old songs
            # we should probably listen to the library for self.song changes
            new_info.update(self.song)
            new_info.update(self.info)

        changed = False
        info_changed = False

        tags = TagListWrapper(tags, merge=True)
        tags = parse_gstreamer_taglist(tags)

        for key, value in iteritems(sanitize_tags(tags, stream=False)):
            if self.song.get(key) != value:
                changed = True
                self.song[key] = value

        for key, value in iteritems(sanitize_tags(tags, stream=True)):
            if new_info.get(key) != value:
                info_changed = True
                new_info[key] = value

        if info_changed:
            # in case the title changed, make self.info a new instance
            # and emit ended/started for the the old/new one
            if self.info.get("title") != new_info.get("title"):
                if self.info is not self.song:
                    self.emit('song-ended', self.info, False)
                self.info = new_info
                self.__info_buffer = None
                self.emit('song-started', self.info)
            else:
                # in case title didn't changed, update the values of the
                # old instance if there is one and tell the library.
                if self.info is not self.song:
                    self.info.update(new_info)
                    librarian.changed([self.info])
                else:
                    # So we don't loose all tags before the first title
                    # save it for later
                    self.__info_buffer = new_info

        if changed:
            librarian.changed([self.song])

    @property
    def eq_bands(self):
        if Gst.ElementFactory.find('equalizer-10bands'):
            return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011]
        else:
            return []

    def update_eq_values(self):
        need_eq = any(self._eq_values)
        if need_eq != self._use_eq:
            self._use_eq = need_eq
            self._rebuild_pipeline()

        if self._eq_element:
            for band, val in enumerate(self._eq_values):
                self._eq_element.set_property('band%d' % band, val)

    def can_play_uri(self, uri):
        if not Gst.uri_is_valid(uri):
            return False
        try:
            Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '')
        except GLib.GError:
            return False
        return True
Esempio n. 7
0
class GStreamerPlayer(BasePlayer, GStreamerPluginHandler):
    __gproperties__ = BasePlayer._gproperties_
    __gsignals__ = BasePlayer._gsignals_

    _paused = True
    _in_gapless_transition = False
    _last_position = 0

    bin = None
    _vol_element = None
    _use_eq = False
    _eq_element = None

    __atf_id = None
    __bus_id = None
    __source_setup_id = None

    __info_buffer = None

    def PlayerPreferences(self):
        return GstPlayerPreferences(self, const.DEBUG)

    def __init__(self, librarian=None):
        GStreamerPluginHandler.__init__(self)
        super(GStreamerPlayer, self).__init__()
        self.version_info = "GStreamer: %s" % fver(Gst.version())
        self._librarian = librarian
        self._pipeline_desc = None
        self._lib_id = librarian.connect("changed", self.__songs_changed)
        self._active_seeks = []
        self._active_error = False
        self._runner = MainRunner()

    def __songs_changed(self, librarian, songs):
        # replaygain values might have changed, recalc volume
        if self.song and self.song in songs:
            self.volume = self.volume

    def _destroy(self):
        self._librarian.disconnect(self._lib_id)
        self._runner.abort()
        self.__destroy_pipeline()

    @property
    def name(self):
        name = "GStreamer"
        if self._pipeline_desc:
            name += " (%s)" % self._pipeline_desc
        return name

    def _set_buffer_duration(self, duration):
        """Set the stream buffer duration in msecs"""

        config.set("player", "gst_buffer", float(duration) / 1000)

        if self.bin:
            value = duration * Gst.MSECOND
            self.bin.set_property('buffer-duration', value)

    def _print_pipeline(self):
        """Print debug information for the active pipeline to stdout
        (elements, formats, ...)
        """

        if self.bin:
            # self.bin is just a wrapper, so get the real one
            for line in bin_debug([self.bin.bin]):
                print_(line)
        else:
            print_e("No active pipeline.")

    def __init_pipeline(self):
        """Creates a gstreamer pipeline. Returns True on success."""

        if self.bin:
            return True

        # reset error state
        self.error = False

        pipeline = config.get("player", "gst_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'):
            # The equalizer only operates on 16-bit ints or floats, and
            # will only pass these types through even when inactive.
            # We push floats through to this point, then let the second
            # audioconvert handle pushing to whatever the rest of the
            # pipeline supports. As a bonus, this seems to automatically
            # select the highest-precision format supported by the
            # rest of the chain.
            filt = Gst.ElementFactory.make('capsfilter', None)
            filt.set_property('caps',
                              Gst.Caps.from_string('audio/x-raw,format=F32LE'))
            eq = Gst.ElementFactory.make('equalizer-10bands', None)
            self._eq_element = eq
            self.update_eq_values()
            conv = Gst.ElementFactory.make('audioconvert', None)
            resample = Gst.ElementFactory.make('audioresample', None)
            pipeline = [filt, eq, conv, resample] + pipeline

        # playbin2 has started to control the volume through pulseaudio,
        # which means the volume property can change without us noticing.
        # Use our own volume element for now until this works with PA.
        self._vol_element = Gst.ElementFactory.make('volume', None)
        pipeline.insert(0, self._vol_element)

        # Get all plugin elements and append audio converters.
        # playbin already includes one at the end
        plugin_pipeline = []
        for plugin in self._get_plugin_elements():
            plugin_pipeline.append(plugin)
            plugin_pipeline.append(
                Gst.ElementFactory.make('audioconvert', None))
            plugin_pipeline.append(
                Gst.ElementFactory.make('audioresample', None))
        pipeline = plugin_pipeline + pipeline

        bufbin = Gst.Bin()
        for element in pipeline:
            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("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)
            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
        gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink'))
        bufbin.add_pad(gpad)

        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)

        # set buffer duration
        duration = config.getfloat("player", "gst_buffer")
        self._set_buffer_duration(int(duration * 1000))

        # connect playbin to our pluing/volume/eq pipeline
        self.bin.set_property('audio-sink', bufbin)

        # by default playbin will render video -> suppress using fakesink
        fakesink = Gst.ElementFactory.make('fakesink', None)
        self.bin.set_property('video-sink', fakesink)

        # disable all video/text decoding in playbin
        GST_PLAY_FLAG_VIDEO = 1 << 0
        GST_PLAY_FLAG_TEXT = 1 << 2
        flags = self.bin.get_property("flags")
        flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT)
        self.bin.set_property("flags", flags)

        # find the (uri)decodebin after setup and use autoplug-sort
        # to sort elements like decoders
        def source_setup(*args):
            def autoplug_sort(decode, pad, caps, factories):
                def set_prio(x):
                    i, f = x
                    i = {"mad": -1, "mpg123audiodec": -2}.get(f.get_name(), i)
                    return (i, f)

                return zip(*sorted(map(set_prio, enumerate(factories))))[1]

            for e in iter_to_list(self.bin.iterate_recurse):
                try:
                    e.connect("autoplug-sort", autoplug_sort)
                except TypeError:
                    pass
                else:
                    break

        self.__source_setup_id = self.bin.connect("source-setup", source_setup)

        # ReplayGain information gets lost when destroying
        self.volume = self.volume

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

        return True

    def __destroy_pipeline(self):
        self._remove_plugin_elements()

        if self.__bus_id:
            bus = self.bin.get_bus()
            bus.disconnect(self.__bus_id)
            bus.remove_signal_watch()
            self.__bus_id = None

        if self.__atf_id:
            self.bin.disconnect(self.__atf_id)
            self.__atf_id = None

        if self.__source_setup_id:
            self.bin.disconnect(self.__source_setup_id)
            self.__source_setup_id = None

        if self.bin:
            self.bin.set_state(Gst.State.NULL)
            self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)
            self.bin.set_property('audio-sink', None)
            self.bin.set_property('video-sink', None)
            # BufferingWrapper cleanup
            self.bin.destroy()
            self.bin = None

        self._in_gapless_transition = False
        self._last_position = 0
        self._active_seeks = []

        self._vol_element = None
        self._eq_element = None

    def _rebuild_pipeline(self):
        """If a pipeline is active, rebuild it and restore vol, position etc"""

        if not self.bin:
            return

        paused = self.paused
        pos = self.get_position()

        self.__destroy_pipeline()
        self.paused = True
        self.__init_pipeline()
        self.paused = paused
        self.seek(pos)

    def __message(self, bus, message, librarian):
        if message.type == Gst.MessageType.EOS:
            print_d("Stream EOS")
            if not self._in_gapless_transition:
                self._source.next_ended()
            self._end(False)
        elif message.type == Gst.MessageType.TAG:
            self.__tag(message.parse_tag(), librarian)
        elif message.type == Gst.MessageType.ERROR:
            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")
                self._end(False)
        elif message.type == Gst.MessageType.ASYNC_DONE:
            if self._active_seeks:
                song, pos = self._active_seeks.pop(0)
                if song is self.song:
                    self.emit("seek", song, pos)
        elif message.type == Gst.MessageType.ELEMENT:
            message_name = message.get_structure().get_name()

            if message_name == "missing-plugin":
                self.__handle_missing_plugin(message)
        elif message.type == Gst.MessageType.CLOCK_LOST:
            print_d("Clock lost")
            self.bin.set_state(Gst.State.PAUSED)
            self.bin.set_state(Gst.State.PLAYING)
        elif message.type == Gst.MessageType.LATENCY:
            print_d("Recalculate latency")
            self.bin.recalculate_latency()
        elif message.type == Gst.MessageType.REQUEST_STATE:
            state = message.parse_request_state()
            print_d("State requested: %s" % Gst.Element.state_get_name(state))
            self.bin.set_state(state)
        elif message.type == Gst.MessageType.DURATION_CHANGED:
            if self.song.fill_length:
                ok, p = self.bin.query_duration(Gst.Format.TIME)
                if ok:
                    p /= float(Gst.SECOND)
                    self.song["~#length"] = p
                    librarian.changed([self.song])

    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")
        error_details = _(u"Media format: %(format-description)s") % {
            "format-description": format_desc.decode("utf-8")
        }

        def install_done_cb(plugins_return, *args):
            print_d("Gstreamer plugin install return: %r" % plugins_return)
            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, error_details))

    def __about_to_finish_sync(self):
        """Returns a tuple (ok, next_song). ok is True if the next song
        should be set.
        """

        print_d("About to finish (sync)")

        # Chained oggs falsely trigger a gapless transition.
        # At least for radio streams we can safely ignore it because
        # transitions don't occur there.
        # https://github.com/quodlibet/quodlibet/issues/1454
        # https://bugzilla.gnome.org/show_bug.cgi?id=695474
        if self.song.multisong:
            print_d("multisong: ignore about to finish")
            return (False, None)

        if config.getboolean("player", "gst_disable_gapless"):
            print_d("Gapless disabled")
            return (False, None)

        # this can trigger twice, see issue 987
        if self._in_gapless_transition:
            return (False, None)
        self._in_gapless_transition = True

        print_d("Select next song in mainloop..")
        self._source.next_ended()
        print_d("..done.")

        return (True, self._source.current)

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

        try:
            ok, song = self._runner.call(self.__about_to_finish_sync,
                                         priority=GLib.PRIORITY_HIGH,
                                         timeout=0.5)
        except MainRunnerTimeoutError as e:
            # Due to some locks being held during this signal we can get
            # into a deadlock when a seek or state change event happens
            # in the mainloop before our function gets scheduled.
            # In this case abort and do nothing, which results
            # in a non-gapless transition.
            print_d("About to finish (async): %s" % e)
            return
        except MainRunnerAbortedError:
            print_d("About to finish (async): %s" % e)
            return
        except MainRunnerError:
            util.print_exc()
            return

        if ok:
            print_d("About to finish (async): setting uri")
            uri = song("~uri") if song is not None else None
            playbin.set_property('uri', uri)
        print_d("About to finish (async): done")

    def stop(self):
        super(GStreamerPlayer, self).stop()
        self.__destroy_pipeline()

    def do_set_property(self, property, v):
        if property.name == 'volume':
            self._volume = v
            if self.song and config.getboolean("player", "replaygain"):
                profiles = filter(None, self.replaygain_profiles)[0]
                fb_gain = config.getfloat("player", "fallback_gain")
                pa_gain = config.getfloat("player", "pre_amp_gain")
                scale = self.song.replay_gain(profiles, pa_gain, fb_gain)
                v = min(10.0, max(0.0, v * scale))  # volume supports 0..10
            if self.bin:
                self._vol_element.set_property('volume', v)
        else:
            raise AttributeError

    def get_position(self):
        """Return the current playback position in milliseconds,
        or 0 if no song is playing."""

        p = self._last_position
        if self.song and self.bin:
            # While we are actively seeking return the last wanted position.
            # query_position() returns 0 while in this state
            if self._active_seeks:
                return self._active_seeks[-1][1]

            ok, p = self.bin.query_position(Gst.Format.TIME)
            if ok:
                p //= Gst.MSECOND
                # During stream seeking querying the position fails.
                # Better return the last valid one instead of 0.
                self._last_position = p
        return p

    @property
    def paused(self):
        return self._paused

    @paused.setter
    def paused(self, paused):
        if paused == self._paused:
            return

        self._paused = paused
        self.emit((paused and 'paused') or 'unpaused')
        # in case a signal handler changed the paused state, abort this
        if self._paused != paused:
            return

        if paused:
            if self.bin:
                if not self.song:
                    # Something wants us to pause between songs, or when
                    # we've got no song playing (probably StopAfterMenu).
                    self.__destroy_pipeline()
                elif self.song.is_file:
                    # fast path
                    self.bin.set_state(Gst.State.PAUSED)
                else:
                    # seekable streams (seem to) have a duration >= 0
                    ok, d = self.bin.query_duration(Gst.Format.TIME)
                    if not ok:
                        d = -1

                    if d >= 0:
                        self.bin.set_state(Gst.State.PAUSED)
                    else:
                        # destroy so that we rebuffer on resume
                        self.__destroy_pipeline()
        else:
            if self.song and self.__init_pipeline():
                self.bin.set_state(Gst.State.PLAYING)

    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._active_error:
            return
        self._active_error = True

        self.__destroy_pipeline()
        self.error = True
        self.paused = True

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

    def seek(self, pos):
        """Seek to a position in the song, in milliseconds."""
        # Don't allow seeking during gapless. We can't go back to the old song.
        if not self.song or self._in_gapless_transition:
            return

        if self.__init_pipeline():
            # ensure any pending state changes have completed and we have
            # at least paused state, so we can seek
            state = self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)[1]
            if state < Gst.State.PAUSED:
                self.bin.set_state(Gst.State.PAUSED)
                self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)

            pos = max(0, int(pos))
            gst_time = pos * Gst.MSECOND
            event = Gst.Event.new_seek(1.0, Gst.Format.TIME,
                                       Gst.SeekFlags.FLUSH, Gst.SeekType.SET,
                                       gst_time, Gst.SeekType.NONE, 0)
            if self.bin.send_event(event):
                # to get a good estimate for when get_position fails
                self._last_position = pos
                # For cases where we get the position directly after
                # a seek and the seek is not done, GStreamer returns
                # a valid 0 position. To prevent this we try to emit seek only
                # after it is done. Every flushing seek will trigger
                # an async_done message on the bus, so we queue the seek
                # event here and emit in the bus message callback.
                self._active_seeks.append((self.song, pos))

    def _end(self, stopped, next_song=None):
        print_d("End song")
        song, info = self.song, self.info

        # set the new volume before the signals to avoid delays
        if self._in_gapless_transition:
            self.song = self._source.current
            self.volume = self.volume

        # We need to set self.song to None before calling our signal
        # handlers. Otherwise, if they try to end the song they're given
        # (e.g. by removing it), then we get in an infinite loop.
        self.__info_buffer = self.song = self.info = None
        if song is not info:
            self.emit('song-ended', info, stopped)
        self.emit('song-ended', song, stopped)

        current = self._source.current if next_song is None else next_song

        # Then, set up the next song.
        self.song = self.info = current
        self.emit('song-started', self.song)

        print_d("Next song")
        if self.song is not None:
            if not self._in_gapless_transition:
                self.volume = self.volume
                # Due to extensive problems with playbin2, we destroy the
                # entire pipeline and recreate it each time we're not in
                # a gapless transition.
                self.__destroy_pipeline()
                self.__init_pipeline()
            if self.bin:
                if self.paused:
                    self.bin.set_state(Gst.State.PAUSED)
                else:
                    # something unpaused while no song was active
                    if song is None:
                        self.emit("unpaused")
                    self.bin.set_state(Gst.State.PLAYING)
        else:
            self.__destroy_pipeline()
            self.paused = True

        self._in_gapless_transition = False

    def __tag(self, tags, librarian):
        if self.song and self.song.multisong:
            self._fill_stream(tags, librarian)
        elif self.song and self.song.fill_metadata:
            pass

    def _fill_stream(self, tags, librarian):
        # get a new remote file
        new_info = self.__info_buffer
        if not new_info:
            new_info = type(self.song)(self.song["~filename"])
            new_info.multisong = False
            new_info.streamsong = True

            # copy from the old songs
            # we should probably listen to the library for self.song changes
            new_info.update(self.song)
            new_info.update(self.info)

        changed = False
        info_changed = False

        tags = TagListWrapper(tags, merge=True)
        tags = parse_gstreamer_taglist(tags)

        for key, value in sanitize_tags(tags, stream=False).iteritems():
            if self.song.get(key) != value:
                changed = True
                self.song[key] = value

        for key, value in sanitize_tags(tags, stream=True).iteritems():
            if new_info.get(key) != value:
                info_changed = True
                new_info[key] = value

        if info_changed:
            # in case the title changed, make self.info a new instance
            # and emit ended/started for the the old/new one
            if self.info.get("title") != new_info.get("title"):
                if self.info is not self.song:
                    self.emit('song-ended', self.info, False)
                self.info = new_info
                self.__info_buffer = None
                self.emit('song-started', self.info)
            else:
                # in case title didn't changed, update the values of the
                # old instance if there is one and tell the library.
                if self.info is not self.song:
                    self.info.update(new_info)
                    librarian.changed([self.info])
                else:
                    # So we don't loose all tags before the first title
                    # save it for later
                    self.__info_buffer = new_info

        if changed:
            librarian.changed([self.song])

    @property
    def eq_bands(self):
        if Gst.ElementFactory.find('equalizer-10bands'):
            return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011]
        else:
            return []

    def update_eq_values(self):
        need_eq = any(self._eq_values)
        if need_eq != self._use_eq:
            self._use_eq = need_eq
            self._rebuild_pipeline()

        if self._eq_element:
            for band, val in enumerate(self._eq_values):
                self._eq_element.set_property('band%d' % band, val)

    def can_play_uri(self, uri):
        if not Gst.uri_is_valid(uri):
            return False
        try:
            Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '')
        except GLib.GError:
            return False
        return True
Esempio n. 8
0
class GStreamerPlayer(BasePlayer, GStreamerPluginHandler):
    __gproperties__ = BasePlayer._gproperties_
    __gsignals__ = BasePlayer._gsignals_

    _paused = True
    _in_gapless_transition = False
    _last_position = 0

    bin = None
    _vol_element = None
    _use_eq = False
    _eq_element = None

    __atf_id = None
    __bus_id = None
    __source_setup_id = None

    __info_buffer = None

    def PlayerPreferences(self):
        return GstPlayerPreferences(self, const.DEBUG)

    def __init__(self, librarian=None):
        GStreamerPluginHandler.__init__(self)
        super(GStreamerPlayer, self).__init__()
        self.version_info = "GStreamer: %s" % fver(Gst.version())
        self._librarian = librarian
        self._pipeline_desc = None
        self._lib_id = librarian.connect("changed", self.__songs_changed)
        self._active_seeks = []
        self._active_error = False
        self._runner = MainRunner()

    def __songs_changed(self, librarian, songs):
        # replaygain values might have changed, recalc volume
        if self.song and self.song in songs:
            self.volume = self.volume

    def _destroy(self):
        self._librarian.disconnect(self._lib_id)
        self._runner.abort()
        self.__destroy_pipeline()

    @property
    def name(self):
        name = "GStreamer"
        if self._pipeline_desc:
            name += " (%s)" % self._pipeline_desc
        return name

    def _set_buffer_duration(self, duration):
        """Set the stream buffer duration in msecs"""

        config.set("player", "gst_buffer", float(duration) / 1000)

        if self.bin:
            value = duration * Gst.MSECOND
            self.bin.set_property('buffer-duration', value)

    def _print_pipeline(self):
        """Print debug information for the active pipeline to stdout
        (elements, formats, ...)
        """

        if self.bin:
            # self.bin is just a wrapper, so get the real one
            for line in bin_debug([self.bin.bin]):
                print_(line)
        else:
            print_e("No active pipeline.")

    def __init_pipeline(self):
        """Creates a gstreamer pipeline. Returns True on success."""

        if self.bin:
            return True

        # reset error state
        self.error = False

        pipeline = config.get("player", "gst_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'):
            # The equalizer only operates on 16-bit ints or floats, and
            # will only pass these types through even when inactive.
            # We push floats through to this point, then let the second
            # audioconvert handle pushing to whatever the rest of the
            # pipeline supports. As a bonus, this seems to automatically
            # select the highest-precision format supported by the
            # rest of the chain.
            filt = Gst.ElementFactory.make('capsfilter', None)
            filt.set_property('caps',
                              Gst.Caps.from_string('audio/x-raw,format=F32LE'))
            eq = Gst.ElementFactory.make('equalizer-10bands', None)
            self._eq_element = eq
            self.update_eq_values()
            conv = Gst.ElementFactory.make('audioconvert', None)
            resample = Gst.ElementFactory.make('audioresample', None)
            pipeline = [filt, eq, conv, resample] + pipeline

        # playbin2 has started to control the volume through pulseaudio,
        # which means the volume property can change without us noticing.
        # Use our own volume element for now until this works with PA.
        self._vol_element = Gst.ElementFactory.make('volume', None)
        pipeline.insert(0, self._vol_element)

        # Get all plugin elements and append audio converters.
        # playbin already includes one at the end
        plugin_pipeline = []
        for plugin in self._get_plugin_elements():
            plugin_pipeline.append(plugin)
            plugin_pipeline.append(
                Gst.ElementFactory.make('audioconvert', None))
            plugin_pipeline.append(
                Gst.ElementFactory.make('audioresample', None))
        pipeline = plugin_pipeline + pipeline

        bufbin = Gst.Bin()
        for element in pipeline:
            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("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)
            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
        gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink'))
        bufbin.add_pad(gpad)

        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)

        # set buffer duration
        duration = config.getfloat("player", "gst_buffer")
        self._set_buffer_duration(int(duration * 1000))

        # connect playbin to our pluing/volume/eq pipeline
        self.bin.set_property('audio-sink', bufbin)

        # by default playbin will render video -> suppress using fakesink
        fakesink = Gst.ElementFactory.make('fakesink', None)
        self.bin.set_property('video-sink', fakesink)

        # disable all video/text decoding in playbin
        GST_PLAY_FLAG_VIDEO = 1 << 0
        GST_PLAY_FLAG_TEXT = 1 << 2
        flags = self.bin.get_property("flags")
        flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT)
        self.bin.set_property("flags", flags)

        # find the (uri)decodebin after setup and use autoplug-sort
        # to sort elements like decoders
        def source_setup(*args):
            def autoplug_sort(decode, pad, caps, factories):
                def set_prio(x):
                    i, f = x
                    i = {
                        "mad": -1,
                        "mpg123audiodec": -2
                    }.get(f.get_name(), i)
                    return (i, f)
                return zip(*sorted(map(set_prio, enumerate(factories))))[1]

            for e in iter_to_list(self.bin.iterate_recurse):
                try:
                    e.connect("autoplug-sort", autoplug_sort)
                except TypeError:
                    pass
                else:
                    break
        self.__source_setup_id = self.bin.connect("source-setup", source_setup)

        # ReplayGain information gets lost when destroying
        self.volume = self.volume

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

        return True

    def __destroy_pipeline(self):
        self._remove_plugin_elements()

        if self.__bus_id:
            bus = self.bin.get_bus()
            bus.disconnect(self.__bus_id)
            bus.remove_signal_watch()
            self.__bus_id = None

        if self.__atf_id:
            self.bin.disconnect(self.__atf_id)
            self.__atf_id = None

        if self.__source_setup_id:
            self.bin.disconnect(self.__source_setup_id)
            self.__source_setup_id = None

        if self.bin:
            self.bin.set_state(Gst.State.NULL)
            self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)
            self.bin.set_property('audio-sink', None)
            self.bin.set_property('video-sink', None)
            # BufferingWrapper cleanup
            self.bin.destroy()
            self.bin = None

        self._in_gapless_transition = False
        self._last_position = 0
        self._active_seeks = []

        self._vol_element = None
        self._eq_element = None

    def _rebuild_pipeline(self):
        """If a pipeline is active, rebuild it and restore vol, position etc"""

        if not self.bin:
            return

        paused = self.paused
        pos = self.get_position()

        self.__destroy_pipeline()
        self.paused = True
        self.__init_pipeline()
        self.paused = paused
        self.seek(pos)

    def __message(self, bus, message, librarian):
        if message.type == Gst.MessageType.EOS:
            print_d("Stream EOS")
            if not self._in_gapless_transition:
                self._source.next_ended()
            self._end(False)
        elif message.type == Gst.MessageType.TAG:
            self.__tag(message.parse_tag(), librarian)
        elif message.type == Gst.MessageType.ERROR:
            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")
                self._end(False)
        elif message.type == Gst.MessageType.ASYNC_DONE:
            if self._active_seeks:
                song, pos = self._active_seeks.pop(0)
                if song is self.song:
                    self.emit("seek", song, pos)
        elif message.type == Gst.MessageType.ELEMENT:
            message_name = message.get_structure().get_name()

            if message_name == "missing-plugin":
                self.__handle_missing_plugin(message)
        elif message.type == Gst.MessageType.CLOCK_LOST:
            print_d("Clock lost")
            self.bin.set_state(Gst.State.PAUSED)
            self.bin.set_state(Gst.State.PLAYING)
        elif message.type == Gst.MessageType.LATENCY:
            print_d("Recalculate latency")
            self.bin.recalculate_latency()
        elif message.type == Gst.MessageType.REQUEST_STATE:
            state = message.parse_request_state()
            print_d("State requested: %s" % Gst.Element.state_get_name(state))
            self.bin.set_state(state)
        elif message.type == Gst.MessageType.DURATION_CHANGED:
            if self.song.fill_length:
                ok, p = self.bin.query_duration(Gst.Format.TIME)
                if ok:
                    p /= float(Gst.SECOND)
                    self.song["~#length"] = p
                    librarian.changed([self.song])

    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")
        error_details = _(u"Media format: %(format-description)s") % {
            "format-description": format_desc.decode("utf-8")}

        def install_done_cb(plugins_return, *args):
            print_d("Gstreamer plugin install return: %r" % plugins_return)
            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, error_details))

    def __about_to_finish_sync(self):
        """Returns a tuple (ok, next_song). ok is True if the next song
        should be set.
        """

        print_d("About to finish (sync)")

        # Chained oggs falsely trigger a gapless transition.
        # At least for radio streams we can safely ignore it because
        # transitions don't occur there.
        # https://github.com/quodlibet/quodlibet/issues/1454
        # https://bugzilla.gnome.org/show_bug.cgi?id=695474
        if self.song.multisong:
            print_d("multisong: ignore about to finish")
            return (False, None)

        if config.getboolean("player", "gst_disable_gapless"):
            print_d("Gapless disabled")
            return (False, None)

        # this can trigger twice, see issue 987
        if self._in_gapless_transition:
            return (False, None)
        self._in_gapless_transition = True

        print_d("Select next song in mainloop..")
        self._source.next_ended()
        print_d("..done.")

        return (True, self._source.current)

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

        try:
            ok, song = self._runner.call(self.__about_to_finish_sync,
                                         priority=GLib.PRIORITY_HIGH,
                                         timeout=0.5)
        except MainRunnerTimeoutError as e:
            # Due to some locks being held during this signal we can get
            # into a deadlock when a seek or state change event happens
            # in the mainloop before our function gets scheduled.
            # In this case abort and do nothing, which results
            # in a non-gapless transition.
            print_d("About to finish (async): %s" % e)
            return
        except MainRunnerAbortedError:
            print_d("About to finish (async): %s" % e)
            return
        except MainRunnerError:
            util.print_exc()
            return

        if ok:
            print_d("About to finish (async): setting uri")
            uri = song("~uri") if song is not None else None
            playbin.set_property('uri', uri)
        print_d("About to finish (async): done")

    def stop(self):
        super(GStreamerPlayer, self).stop()
        self.__destroy_pipeline()

    def do_set_property(self, property, v):
        if property.name == 'volume':
            self._volume = v
            if self.song and config.getboolean("player", "replaygain"):
                profiles = filter(None, self.replaygain_profiles)[0]
                fb_gain = config.getfloat("player", "fallback_gain")
                pa_gain = config.getfloat("player", "pre_amp_gain")
                scale = self.song.replay_gain(profiles, pa_gain, fb_gain)
                v = min(10.0, max(0.0, v * scale)) # volume supports 0..10
            if self.bin:
                self._vol_element.set_property('volume', v)
        else:
            raise AttributeError

    def get_position(self):
        """Return the current playback position in milliseconds,
        or 0 if no song is playing."""

        p = self._last_position
        if self.song and self.bin:
            # While we are actively seeking return the last wanted position.
            # query_position() returns 0 while in this state
            if self._active_seeks:
                return self._active_seeks[-1][1]

            ok, p = self.bin.query_position(Gst.Format.TIME)
            if ok:
                p //= Gst.MSECOND
                # During stream seeking querying the position fails.
                # Better return the last valid one instead of 0.
                self._last_position = p
        return p

    @property
    def paused(self):
        return self._paused

    @paused.setter
    def paused(self, paused):
        if paused == self._paused:
            return

        self._paused = paused
        self.emit((paused and 'paused') or 'unpaused')
        # in case a signal handler changed the paused state, abort this
        if self._paused != paused:
            return

        if paused:
            if self.bin:
                if not self.song:
                    # Something wants us to pause between songs, or when
                    # we've got no song playing (probably StopAfterMenu).
                    self.__destroy_pipeline()
                elif self.song.is_file:
                    # fast path
                    self.bin.set_state(Gst.State.PAUSED)
                else:
                    # seekable streams (seem to) have a duration >= 0
                    ok, d = self.bin.query_duration(Gst.Format.TIME)
                    if not ok:
                        d = -1

                    if d >= 0:
                        self.bin.set_state(Gst.State.PAUSED)
                    else:
                        # destroy so that we rebuffer on resume
                        self.__destroy_pipeline()
        else:
            if self.song and self.__init_pipeline():
                self.bin.set_state(Gst.State.PLAYING)

    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._active_error:
            return
        self._active_error = True

        self.__destroy_pipeline()
        self.error = True
        self.paused = True

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

    def seek(self, pos):
        """Seek to a position in the song, in milliseconds."""
        # Don't allow seeking during gapless. We can't go back to the old song.
        if not self.song or self._in_gapless_transition:
            return

        if self.__init_pipeline():
            # ensure any pending state changes have completed and we have
            # at least paused state, so we can seek
            state = self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)[1]
            if state < Gst.State.PAUSED:
                self.bin.set_state(Gst.State.PAUSED)
                self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)

            pos = max(0, int(pos))
            gst_time = pos * Gst.MSECOND
            event = Gst.Event.new_seek(
                1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH,
                Gst.SeekType.SET, gst_time, Gst.SeekType.NONE, 0)
            if self.bin.send_event(event):
                # to get a good estimate for when get_position fails
                self._last_position = pos
                # For cases where we get the position directly after
                # a seek and the seek is not done, GStreamer returns
                # a valid 0 position. To prevent this we try to emit seek only
                # after it is done. Every flushing seek will trigger
                # an async_done message on the bus, so we queue the seek
                # event here and emit in the bus message callback.
                self._active_seeks.append((self.song, pos))

    def _end(self, stopped, next_song=None):
        print_d("End song")
        song, info = self.song, self.info

        # set the new volume before the signals to avoid delays
        if self._in_gapless_transition:
            self.song = self._source.current
            self.volume = self.volume

        # We need to set self.song to None before calling our signal
        # handlers. Otherwise, if they try to end the song they're given
        # (e.g. by removing it), then we get in an infinite loop.
        self.__info_buffer = self.song = self.info = None
        if song is not info:
            self.emit('song-ended', info, stopped)
        self.emit('song-ended', song, stopped)

        current = self._source.current if next_song is None else next_song

        # Then, set up the next song.
        self.song = self.info = current
        self.emit('song-started', self.song)

        print_d("Next song")
        if self.song is not None:
            if not self._in_gapless_transition:
                self.volume = self.volume
                # Due to extensive problems with playbin2, we destroy the
                # entire pipeline and recreate it each time we're not in
                # a gapless transition.
                self.__destroy_pipeline()
                self.__init_pipeline()
            if self.bin:
                if self.paused:
                    self.bin.set_state(Gst.State.PAUSED)
                else:
                    # something unpaused while no song was active
                    if song is None:
                        self.emit("unpaused")
                    self.bin.set_state(Gst.State.PLAYING)
        else:
            self.__destroy_pipeline()
            self.paused = True

        self._in_gapless_transition = False

    def __tag(self, tags, librarian):
        if self.song and self.song.multisong:
            self._fill_stream(tags, librarian)
        elif self.song and self.song.fill_metadata:
            pass

    def _fill_stream(self, tags, librarian):
        # get a new remote file
        new_info = self.__info_buffer
        if not new_info:
            new_info = type(self.song)(self.song["~filename"])
            new_info.multisong = False
            new_info.streamsong = True

            # copy from the old songs
            # we should probably listen to the library for self.song changes
            new_info.update(self.song)
            new_info.update(self.info)

        changed = False
        info_changed = False

        tags = TagListWrapper(tags, merge=True)
        tags = parse_gstreamer_taglist(tags)

        for key, value in sanitize_tags(tags, stream=False).iteritems():
            if self.song.get(key) != value:
                changed = True
                self.song[key] = value

        for key, value in sanitize_tags(tags, stream=True).iteritems():
            if new_info.get(key) != value:
                info_changed = True
                new_info[key] = value

        if info_changed:
            # in case the title changed, make self.info a new instance
            # and emit ended/started for the the old/new one
            if self.info.get("title") != new_info.get("title"):
                if self.info is not self.song:
                    self.emit('song-ended', self.info, False)
                self.info = new_info
                self.__info_buffer = None
                self.emit('song-started', self.info)
            else:
                # in case title didn't changed, update the values of the
                # old instance if there is one and tell the library.
                if self.info is not self.song:
                    self.info.update(new_info)
                    librarian.changed([self.info])
                else:
                    # So we don't loose all tags before the first title
                    # save it for later
                    self.__info_buffer = new_info

        if changed:
            librarian.changed([self.song])

    @property
    def eq_bands(self):
        if Gst.ElementFactory.find('equalizer-10bands'):
            return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011]
        else:
            return []

    def update_eq_values(self):
        need_eq = any(self._eq_values)
        if need_eq != self._use_eq:
            self._use_eq = need_eq
            self._rebuild_pipeline()

        if self._eq_element:
            for band, val in enumerate(self._eq_values):
                self._eq_element.set_property('band%d' % band, val)

    def can_play_uri(self, uri):
        if not Gst.uri_is_valid(uri):
            return False
        try:
            Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '')
        except GLib.GError:
            return False
        return True