예제 #1
0
 def __init__(self):
     """
         Init playbin
     """
     # In the case of gapless playback, both 'about-to-finish'
     # and 'eos' can occur during the same stream.
     self.__track_in_pipe = False
     self.__cancellable = Gio.Cancellable()
     self.__codecs = Codecs()
     self._current_track = Track()
     self._next_track = Track()
     self._prev_track = Track()
     self._playbin = self._playbin1 = Gst.ElementFactory.make(
         "playbin", "player")
     self._playbin2 = Gst.ElementFactory.make("playbin", "player")
     self._plugins = self._plugins1 = PluginsPlayer(self._playbin1)
     self._plugins2 = PluginsPlayer(self._playbin2)
     for playbin in [self._playbin1, self._playbin2]:
         flags = playbin.get_property("flags")
         flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
         playbin.set_property("flags", flags)
         playbin.set_property("buffer-size", 5 << 20)
         playbin.set_property("buffer-duration", 10 * Gst.SECOND)
         playbin.connect("notify::volume", self.__on_volume_changed)
         playbin.connect("about-to-finish", self._on_stream_about_to_finish)
         bus = playbin.get_bus()
         bus.add_signal_watch()
         bus.connect("message::error", self._on_bus_error)
         bus.connect("message::eos", self._on_bus_eos)
         bus.connect("message::element", self._on_bus_element)
         bus.connect("message::stream-start", self._on_stream_start)
         bus.connect("message::tag", self._on_bus_message_tag)
     self._start_time = 0
예제 #2
0
 def __init__(self):
     """
         Init playbin
     """
     Gst.init(None)
     BasePlayer.__init__(self)
     self._codecs = Codecs()
     self._playbin = Gst.ElementFactory.make('playbin', 'player')
     flags = self._playbin.get_property("flags")
     flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
     self._playbin.set_property('flags', flags)
     self._playbin.set_property('buffer-size', 5 << 20)
     self._playbin.set_property('buffer-duration', 10 * Gst.SECOND)
     ReplayGainPlayer.__init__(self, self._playbin)
     self._playbin.connect('about-to-finish',
                           self._on_stream_about_to_finish)
     bus = self._playbin.get_bus()
     bus.add_signal_watch()
     bus.connect('message::error', self._on_bus_error)
     bus.connect('message::eos', self._on_bus_eos)
     bus.connect('message::element', self._on_bus_element)
     bus.connect('message::stream-start', self._on_stream_start)
     bus.connect("message::tag", self._on_bus_message_tag)
     self._handled_error = None
     self._start_time = 0
예제 #3
0
 def __init__(self):
     """
         Init playbin
     """
     Gst.init(None)
     BasePlayer.__init__(self)
     self.__codecs = Codecs()
     self._playbin = self.__playbin1 = Gst.ElementFactory.make(
         "playbin", "player")
     self.__playbin2 = Gst.ElementFactory.make("playbin", "player")
     self.__preview = None
     self._plugins = self._plugins1 = PluginsPlayer(self.__playbin1)
     self._plugins2 = PluginsPlayer(self.__playbin2)
     self._playbin.connect("notify::volume", self.__on_volume_changed)
     for playbin in [self.__playbin1, self.__playbin2]:
         flags = playbin.get_property("flags")
         flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
         playbin.set_property("flags", flags)
         playbin.set_property("buffer-size", 5 << 20)
         playbin.set_property("buffer-duration", 10 * Gst.SECOND)
         playbin.connect("about-to-finish",
                         self.__on_stream_about_to_finish)
         bus = playbin.get_bus()
         bus.add_signal_watch()
         bus.connect("message::error", self.__on_bus_error)
         bus.connect("message::eos", self.__on_bus_eos)
         bus.connect("message::element", self.__on_bus_element)
         bus.connect("message::stream-start", self._on_stream_start)
         bus.connect("message::tag", self.__on_bus_message_tag)
     self._start_time = 0
예제 #4
0
 def __init__(self):
     """
         Init playbin
     """
     Gst.init(None)
     BasePlayer.__init__(self)
     self._codecs = Codecs()
     self._crossfading = False
     self._playbin = self._playbin1 = Gst.ElementFactory.make(
         'playbin', 'player')
     self._playbin2 = Gst.ElementFactory.make('playbin', 'player')
     self._plugins = self.plugins1 = PluginsPlayer(self._playbin1)
     self.plugins2 = PluginsPlayer(self._playbin2)
     self._volume_id = self._playbin.connect('notify::volume',
                                             self._on_volume_changed)
     for playbin in [self._playbin1, self._playbin2]:
         flags = playbin.get_property("flags")
         flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
         playbin.set_property('flags', flags)
         playbin.set_property('buffer-size', 5 << 20)
         playbin.set_property('buffer-duration', 10 * Gst.SECOND)
         playbin.connect('about-to-finish', self._on_stream_about_to_finish)
         bus = playbin.get_bus()
         bus.add_signal_watch()
         bus.connect('message::error', self._on_bus_error)
         bus.connect('message::eos', self._on_bus_eos)
         bus.connect('message::element', self._on_bus_element)
         bus.connect('message::stream-start', self._on_stream_start)
         bus.connect("message::tag", self._on_bus_message_tag)
     self._handled_error = None
     self._start_time = 0
예제 #5
0
 def __init__(self):
     """
         Init playbin
     """
     Gst.init(None)
     BasePlayer.__init__(self)
     self._codecs = Codecs()
     self._crossfading = False
     self._playbin = self._playbin1 = Gst.ElementFactory.make(
                                                        'playbin', 'player')
     self._playbin2 = Gst.ElementFactory.make('playbin', 'player')
     self._preview = None
     self._plugins = self.plugins1 = PluginsPlayer(self._playbin1)
     self.plugins2 = PluginsPlayer(self._playbin2)
     self._volume_id = self._playbin.connect('notify::volume',
                                             self._on_volume_changed)
     for playbin in [self._playbin1, self._playbin2]:
         flags = playbin.get_property("flags")
         flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
         playbin.set_property('flags', flags)
         playbin.set_property('buffer-size', 5 << 20)
         playbin.set_property('buffer-duration', 10 * Gst.SECOND)
         playbin.connect('about-to-finish',
                         self._on_stream_about_to_finish)
         bus = playbin.get_bus()
         bus.add_signal_watch()
         bus.connect('message::error', self._on_bus_error)
         bus.connect('message::eos', self._on_bus_eos)
         bus.connect('message::element', self._on_bus_element)
         bus.connect('message::stream-start', self._on_stream_start)
         bus.connect("message::tag", self._on_bus_message_tag)
     self._handled_error = None
     self._start_time = 0
예제 #6
0
 def __init__(self):
     """
         Init playbin
     """
     Gst.init(None)
     BasePlayer.__init__(self)
     self._codecs = Codecs()
     self._playbin = Gst.ElementFactory.make('playbin', 'player')
     flags = self._playbin.get_property("flags")
     flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
     self._playbin.set_property('flags', flags)
     self._playbin.set_property('buffer-size', 5 << 20)
     self._playbin.set_property('buffer-duration', 10 * Gst.SECOND)
     ReplayGainPlayer.__init__(self, self._playbin)
     self._playbin.connect('about-to-finish',
                           self._on_stream_about_to_finish)
     bus = self._playbin.get_bus()
     bus.add_signal_watch()
     bus.connect('message::error', self._on_bus_error)
     bus.connect('message::eos', self._on_bus_eos)
     bus.connect('message::element', self._on_bus_element)
     bus.connect('message::stream-start', self._on_stream_start)
     bus.connect("message::tag", self._on_bus_message_tag)
     self._handled_error = None
     self._start_time = 0
예제 #7
0
class BinPlayer(ReplayGainPlayer, BasePlayer):
    """
        Gstreamer bin player
    """
    def __init__(self):
        """
            Init playbin
        """
        Gst.init(None)
        BasePlayer.__init__(self)
        self._codecs = Codecs()
        self._playbin = Gst.ElementFactory.make('playbin', 'player')
        flags = self._playbin.get_property("flags")
        flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
        self._playbin.set_property('flags', flags)
        self._playbin.set_property('buffer-size', 5 << 20)
        self._playbin.set_property('buffer-duration', 10 * Gst.SECOND)
        ReplayGainPlayer.__init__(self, self._playbin)
        self._playbin.connect('about-to-finish',
                              self._on_stream_about_to_finish)
        bus = self._playbin.get_bus()
        bus.add_signal_watch()
        bus.connect('message::error', self._on_bus_error)
        bus.connect('message::eos', self._on_bus_eos)
        bus.connect('message::element', self._on_bus_element)
        bus.connect('message::stream-start', self._on_stream_start)
        bus.connect("message::tag", self._on_bus_message_tag)
        self._handled_error = None
        self._start_time = 0

    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(0)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(0)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def load(self, track, notify=True):
        """
            Stop current track, load track id and play it
            @param track as Track
        """
        self._playbin.set_state(Gst.State.NULL)
        if self._load_track(track):
            if notify:
                self.play()
            else:
                self._playbin.set_state(Gst.State.PLAYING)

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self.current_track.id is None:
            if self.next_track.id is not None:
                self.load(self.next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            self.emit("status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        self._playbin.set_state(Gst.State.PAUSED)
        self.emit("status-changed")

    def stop(self):
        """
            Change player state to STOPPED
        """
        self._playbin.set_state(Gst.State.NULL)
        self.emit("status-changed")

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing():
            self.pause()
        else:
            self.play()

    def seek(self, position):
        """
            Seek current track to position
            @param position as seconds
        """
        # Seems gstreamer doesn't like seeking to end, sometimes
        # doesn't go to next track
        if position > self.current_track.duration - 1:
            self.next()
        else:
            self._playbin.seek_simple(
                Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                position * Gst.SECOND)
            self.emit("seeked", position)

    def get_position_in_track(self):
        """
            Return bin playback position
            @return position as int
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1] / 1000
        return position * 60

    def get_volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.LINEAR)

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        self._playbin.set_volume(GstAudio.StreamVolumeFormat.LINEAR, rate)
        self.emit('volume-changed')

    def next(self):
        """
            Go next track
        """
        pass

#######################
# PRIVATE             #
#######################

    def _load_track(self, track):
        """
            Load track
            @param track as Track
            @return False if track not loaded
        """
        stop = False

        # Stop if needed
        if self.context.next == NextContext.STOP_TRACK:
            stop = True

        # Stop if album changed
        if self.context.next == NextContext.STOP_ALBUM and\
           self.current_track.album.id != track.album.id:
            stop = True

        # Stop if album_artist changed
        if self.context.next == NextContext.STOP_ARTIST and\
           self.current_track.album_artist_id != track.album_artist_id:
            stop = True

        if stop and self.is_playing():
            return False

        self.current_track = track

        try:
            self._playbin.set_property('uri', self.current_track.uri)
        except Exception as e:  # Gstreamer error
            print("BinPlayer::_load_track(): ", e)
            return False

        return True

    def _on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if self.current_track.id >= 0 or\
           self.current_track.duration > 0.0:
            return
        debug("Player::_on_bus_message_tag(): %s" % self.current_track.uri)
        reader = ScannerTagReader()
        tags = message.parse_tag()

        title = reader.get_title(tags, '')
        if title != '':
            self.current_track.name = title
        if self.current_track.name == '':
            self.current_track.name = self.current_track.uri

        artist = reader.get_artists(tags)
        if artist != '':
            self.current_track.artist_names = artist

        # If title set, force artist
        if self.current_track.title != '' and self.current_track.artist == '':
            self.current_track.artist_names = self.current_track.album_artist

        if self.current_track.id == Type.EXTERNALS:
            (b, duration) = self._playbin.query_duration(Gst.Format.TIME)
            if b:
                self.current_track.duration = duration / 1000000000
            # We do not use tagreader as we need to check if value is None
            self.current_track.album_name = tags.get_string_index('album',
                                                                  0)[1]
            if self.current_track.album_name is None:
                self.current_track.album_name = ''
            self.current_track.artist_names = reader.get_artists(tags)
            self.current_track.set_album_artist(reader.get_album_artist(tags))
            if self.current_track.album_artist == '':
                self.current_track.set_album_artist(self.current_track.artist)
            self.current_track.genre_name = reader.get_genres(tags)
        self.emit('current-changed')

    def _on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            if self._codecs is not None:
                self._codecs.append(message)

    def _on_bus_error(self, bus, message):
        """
            Handle first bus error, ignore others
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        debug("Error playing: %s" % self.current_track.uri)
        Lp().window.pulse(False)
        if self._codecs.is_missing_codec(message):
            self._codecs.install()
            Lp().scanner.stop()
        elif Lp().notify is not None:
            Lp().notify.send(
                _("File doesn't exist: %s") % self.current_track.uri)
        self.stop()
        self.emit('current-changed')
        return True

    def _on_bus_eos(self, bus, message):
        """
            On end of stream, stop playing if user ask for,
            go next otherwise
        """
        debug("Player::_on_bus_eos(): %s" % self.current_track.uri)
        if self.context.next not in [
                NextContext.NONE, NextContext.START_NEW_ALBUM
        ]:
            self.stop()
            self.context.next = NextContext.NONE
            if self.next_track.id is not None:
                self._load_track(self.next_track)
            self.emit('current-changed')
        else:
            self.next()

    def _on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst bin
        """
        if self.current_track.id == Type.RADIOS:
            return
        finished = self.current_track
        finished_start_time = self._start_time
        if self.next_track.id is not None:
            self._load_track(self.next_track)
        # Increment popularity
        if not Lp().scanner.is_locked():
            Lp().tracks.set_more_popular(finished.id)
            Lp().albums.set_more_popular(finished.album_id)
        # Scrobble on lastfm
        if Lp().lastfm is not None:
            if finished.album_artist_id == Type.COMPILATIONS:
                artist = finished.artist
            else:
                artist = finished.album_artist
            if time() - finished_start_time > 30:
                Lp().lastfm.scrobble(artist, finished.album_name,
                                     finished.title, int(finished_start_time),
                                     int(finished.duration))

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Emit "current-changed" to notify others components
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self._start_time = time()
        debug("Player::_on_stream_start(): %s" % self.current_track.uri)
        self.emit('current-changed')
        # Update now playing on lastfm
        if Lp().lastfm is not None and self.current_track.id >= 0:
            if self.current_track.album_artist_id == Type.COMPILATIONS:
                artist = self.current_track.artist
            else:
                artist = self.current_track.album_artist
                Lp().lastfm.now_playing(artist, self.current_track.album_name,
                                        self.current_track.title,
                                        int(self.current_track.duration))
        if not Lp().scanner.is_locked():
            Lp().tracks.set_listened_at(self.current_track.id, int(time()))
        self._handled_error = None
예제 #8
0
class BinPlayer(BasePlayer):
    """
        Gstreamer bin player
    """
    def __init__(self):
        """
            Init playbin
        """
        Gst.init(None)
        BasePlayer.__init__(self)
        self.__codecs = Codecs()
        self._playbin = self.__playbin1 = Gst.ElementFactory.make(
            "playbin", "player")
        self.__playbin2 = Gst.ElementFactory.make("playbin", "player")
        self.__preview = None
        self._plugins = self._plugins1 = PluginsPlayer(self.__playbin1)
        self._plugins2 = PluginsPlayer(self.__playbin2)
        self._playbin.connect("notify::volume", self.__on_volume_changed)
        for playbin in [self.__playbin1, self.__playbin2]:
            flags = playbin.get_property("flags")
            flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
            playbin.set_property("flags", flags)
            playbin.set_property("buffer-size", 5 << 20)
            playbin.set_property("buffer-duration", 10 * Gst.SECOND)
            playbin.connect("about-to-finish",
                            self.__on_stream_about_to_finish)
            bus = playbin.get_bus()
            bus.add_signal_watch()
            bus.connect("message::error", self.__on_bus_error)
            bus.connect("message::eos", self.__on_bus_eos)
            bus.connect("message::element", self.__on_bus_element)
            bus.connect("message::stream-start", self._on_stream_start)
            bus.connect("message::tag", self.__on_bus_message_tag)
        self._start_time = 0

    @property
    def preview(self):
        """
            Get a preview bin
            @return Gst.Element
        """
        if self.__preview is None:
            self.__preview = Gst.ElementFactory.make("playbin", "player")
            self.set_preview_output()
        return self.__preview

    def set_preview_output(self):
        """
            Set preview output
        """
        if self.__preview is not None:
            output = Lp().settings.get_value("preview-output").get_string()
            pulse = Gst.ElementFactory.make("pulsesink", "output")
            if pulse is None:
                pulse = Gst.ElementFactory.make("alsasink", "output")
            if pulse is not None:
                pulse.set_property("device", output)
                self.__preview.set_property("audio-sink", pulse)

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def load(self, track):
        """
            Stop current track, load track id and play it
            @param track as Track
        """
        if self._crossfading and\
           self._current_track.id is not None and\
           self.is_playing and\
           self._current_track.id != Type.RADIOS:
            duration = Lp().settings.get_value("mix-duration").get_int32()
            self.__do_crossfade(duration, track, False)
        else:
            self.__load(track)

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self._current_track.id is None:
            if self._next_track.id is not None:
                self.load(self._next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            self.emit("status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        if self._current_track.id == Type.RADIOS:
            self._playbin.set_state(Gst.State.NULL)
        else:
            self._playbin.set_state(Gst.State.PAUSED)
        self.emit("status-changed")

    def stop(self):
        """
            Change player state to STOPPED
        """
        self._playbin.set_state(Gst.State.NULL)
        self.emit("status-changed")

    def stop_all(self):
        """
            Stop all bins, lollypop should quit now
        """
        # Stop
        self.__playbin1.set_state(Gst.State.NULL)
        self.__playbin2.set_state(Gst.State.NULL)

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing:
            self.pause()
        else:
            self.play()

    def seek(self, position):
        """
            Seek current track to position
            @param position as seconds
        """
        if self.locked or self._current_track.id is None:
            return
        # Seems gstreamer doesn"t like seeking to end, sometimes
        # doesn"t go to next track
        if position >= self._current_track.duration:
            self.next()
        else:
            self._playbin.seek_simple(
                Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                position * Gst.SECOND)
            self.emit("seeked", position)

    @property
    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    @property
    def position(self):
        """
            Return bin playback position
            @HACK handle crossefade here, as we know we"re going to be
            called every seconds
            @return position in Gst.SECOND
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1]
        if self._crossfading and self._current_track.duration > 0:
            duration = self._current_track.duration - position / Gst.SECOND
            if duration < Lp().settings.get_value("mix-duration").get_int32():
                self.__do_crossfade(duration)
        return position

    @property
    def current_track(self):
        """
            Current track
        """
        return self._current_track

    @property
    def volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.CUBIC)

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        self.__playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.__playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.emit("volume-changed")

    def next(self):
        """
            Go next track
        """
        pass

#######################
# PROTECTED           #
#######################

    def _load_track(self, track, init_volume=True):
        """
            Load track
            @param track as Track
            @param init volume as bool
            @return False if track not loaded
        """
        if self.__need_to_stop():
            return False
        if init_volume:
            self._plugins.volume.props.volume = 1.0
        debug("BinPlayer::_load_track(): %s" % track.uri)
        try:
            self._current_track = track
            if track.is_web:
                loaded = self._load_web(track)
                # If track not loaded, go next
                if not loaded:
                    self.set_next()
                    GLib.timeout_add(500, self.__load, self.next_track,
                                     init_volume)
                return False  # Return not loaded as handled by load_web()
            else:
                self._playbin.set_property("uri", track.uri)
        except Exception as e:  # Gstreamer error
            print("BinPlayer::_load_track(): ", e)
            return False
        return True

    def _load_web(self, track, play=True):
        """
            Load track url and play it
            @param track as Track
            @param play as bool
            @return True if loading
        """
        if not get_network_available():
            # Force widgets to update (spinners)
            self.emit("current-changed")
            return False
        try:
            from lollypop.web import Web
            if play:
                self.emit("loading-changed", True)
            t = Thread(target=Web.play_track,
                       args=(track, play, self.__set_gv_uri))
            t.daemon = True
            t.start()
            return True
        except Exception as e:
            self._current_track = Track()
            self.stop()
            self.emit("current-changed")
            if Lp().notify is not None:
                Lp().notify.send(str(e), track.uri)
            print("PlayerBin::_load_web()", e)

    def _scrobble(self, finished, finished_start_time):
        """
            Scrobble on lastfm
            @param finished as Track
            @param finished_start_time as int
        """
        # Last.fm policy
        if finished.duration < 30:
            return
        # Scrobble on lastfm
        if Lp().lastfm is not None and Lp().lastfm.session_key:
            artists = ", ".join(finished.artists)
            played = time() - finished_start_time
            # We can scrobble if the track has been played
            # for at least half its duration, or for 4 minutes
            if played >= finished.duration / 2 or played >= 240:
                Lp().lastfm.do_scrobble(artists,
                                        finished.album_name, finished.title,
                                        int(finished_start_time))
        # Scrobble on librefm
        if Lp().librefm is not None and Lp().librefm.session_key:
            artists = ", ".join(finished.artists)
            played = time() - finished_start_time
            # We can scrobble if the track has been played
            # for at least half its duration, or for 4 minutes
            if played >= finished.duration / 2 or played >= 240:
                Lp().librefm.do_scrobble(artists, finished.album_name,
                                         finished.title,
                                         int(finished_start_time))

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Emit "current-changed" to notify others components
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self._start_time = time()
        debug("Player::_on_stream_start(): %s" % self._current_track.uri)
        self.emit("current-changed")
        # Update now playing on lastfm
        # Not supported by librefm
        if Lp().lastfm is not None and\
                Lp().lastfm.session_key and\
                self._current_track.id >= 0:
            artists = ", ".join(self._current_track.artists)
            Lp().lastfm.now_playing(artists, self._current_track.album_name,
                                    self._current_track.title,
                                    int(self._current_track.duration))
        try:
            if not Lp().scanner.is_locked():
                Lp().tracks.set_listened_at(self._current_track.id,
                                            int(time()))
        except:  # Locked database
            pass

#######################
# PRIVATE             #
#######################

    def __update_current_duration(self, reader, track):
        """
            Update current track duration
            @param reader as TagReader
            @param track id as int
        """
        try:
            duration = reader.get_info(track.uri).get_duration() / 1000000000
            if duration != track.duration and duration > 0:
                Lp().tracks.set_duration(track.id, duration)
                self._current_track.set_duration(duration)
                GLib.idle_add(self.emit, "duration-changed", track.id)
        except:
            pass

    def __load(self, track, init_volume=True):
        """
            Stop current track, load track id and play it
            If was playing, do not use play as status doesn"t changed
            @param track as Track
            @param init volume as bool
        """
        was_playing = self.is_playing
        self._playbin.set_state(Gst.State.NULL)
        if self._load_track(track, init_volume):
            if was_playing:
                self._playbin.set_state(Gst.State.PLAYING)
            else:
                self.play()

    def __volume_up(self, playbin, plugins, duration):
        """
            Make volume going up smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are not the active playbin, stop all
        if self._playbin != playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_up = (1.0 - vol) / steps
            rate = vol + vol_up
            if rate < 1.0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self.__volume_up, playbin, plugins,
                                 duration - 0.25)
            else:
                plugins.volume.props.volume = 1.0
        else:
            plugins.volume.props.volume = 1.0

    def __volume_down(self, playbin, plugins, duration):
        """
            Make volume going down smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are again the active playbin, stop all
        if self._playbin == playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_down = vol / steps
            rate = vol - vol_down
            if rate > 0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self.__volume_down, playbin, plugins,
                                 duration - 0.25)
            else:
                plugins.volume.props.volume = 0.0
                playbin.set_state(Gst.State.NULL)
        else:
            plugins.volume.props.volume = 0.0
            playbin.set_state(Gst.State.NULL)

    def __do_crossfade(self, duration, track=None, next=True):
        """
            Crossfade tracks
            @param duration as int
            @param track as Track
            @param next as bool
        """
        # No cossfading if we need to stop
        if self.__need_to_stop() and next:
            return

        if track is None:
            self._scrobble(self._current_track, self._start_time)
            # Increment popularity
            if not Lp().scanner.is_locked():
                Lp().tracks.set_more_popular(self._current_track.id)
                # In party mode, linear popularity
                if self.is_party:
                    pop_to_add = 1
                # In normal mode, based on tracks count
                else:
                    count = Lp().albums.get_tracks_count(
                        self._current_track.album_id)
                    if count:
                        pop_to_add = int(Lp().albums.max_count / count)
                    else:
                        pop_to_add = 0
                if pop_to_add > 0:
                    Lp().albums.set_more_popular(self._current_track.album_id,
                                                 pop_to_add)

        GLib.idle_add(self.__volume_down, self._playbin, self._plugins,
                      duration)
        if self._playbin == self.__playbin2:
            self._playbin = self.__playbin1
            self._plugins = self._plugins1
        else:
            self._playbin = self.__playbin2
            self._plugins = self._plugins2

        if track is not None:
            self.__load(track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin, self._plugins,
                          duration)
        elif next and self._next_track.id is not None:
            self.__load(self._next_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin, self._plugins,
                          duration)
        elif self._prev_track.id is not None:
            self.__load(self._prev_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin, self._plugins,
                          duration)

    def __need_to_stop(self):
        """
            Return True if playback needs to stop
            @return bool
        """
        stop = False
        playback = Lp().settings.get_enum("playback")
        if playback == NextContext.STOP:
            if (not self._albums and
                not self.queue and
                not self._user_playlist_ids) or\
                    playback == self._next_context:
                stop = True
        return stop and self.is_playing

    def __on_volume_changed(self, playbin, sink):
        """
            Update volume
            @param playbin as Gst.Bin
            @param sink as Gst.Sink
        """
        if playbin == self.__playbin1:
            vol = self.__playbin1.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self.__playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        else:
            vol = self.__playbin2.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self.__playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        self.emit("volume-changed")

    def __on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        # Some radio streams send message tag every seconds!
        changed = False
        if self._current_track.persistent == DbPersistent.INTERNAL and\
            (self._current_track.id >= 0 or
             self._current_track.duration > 0.0):
            return
        debug("Player::__on_bus_message_tag(): %s" % self._current_track.uri)
        reader = TagReader()

        # Update duration of non internals
        if self._current_track.persistent != DbPersistent.INTERNAL:
            t = Thread(target=self.__update_current_duration,
                       args=(reader, self._current_track))
            t.daemon = True
            t.start()
            return

        tags = message.parse_tag()
        title = reader.get_title(tags, "")
        if title != "" and self._current_track.name != title:
            self._current_track.name = title
            changed = True
        if self._current_track.name == "":
            self._current_track.name = self._current_track.uri
            changed = True
        artists = reader.get_artists(tags)
        if artists != "" and self._current_track.artists != artists:
            self._current_track.artists = artists.split(",")
            changed = True
        if not self._current_track.artists:
            self._current_track.artists = self._current_track.album_artists
            changed = True

        if self._current_track.id == Type.EXTERNALS:
            (b, duration) = self._playbin.query_duration(Gst.Format.TIME)
            if b:
                self._current_track.duration = duration / 1000000000
            # We do not use tagreader as we need to check if value is None
            self._current_track.album_name = tags.get_string_index("album",
                                                                   0)[1]
            if self._current_track.album_name is None:
                self._current_track.album_name = ""
            self._current_track.genres = reader.get_genres(tags).split(",")
            changed = True
        if changed:
            self.emit("current-changed")

    def __on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            self.__codecs.append(message)

    def __on_bus_error(self, bus, message):
        """
            Handle first bus error, ignore others
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        debug("Error playing: %s" % self._current_track.uri)
        Lp().window.pulse(False)
        if self.__codecs.is_missing_codec(message):
            self.__codecs.install()
            Lp().scanner.stop()
        elif Lp().notify is not None:
            Lp().notify.send(message.parse_error()[0].message)
        self.emit("current-changed")
        return True

    def __on_bus_eos(self, bus, message):
        """
            On end of stream, stop playback
            go next otherwise
        """
        debug("Player::__on_bus_eos(): %s" % self._current_track.uri)
        if self._playbin.get_bus() == bus:
            self.stop()
            self._next_context = NextContext.NONE
            if self._next_track.id is not None:
                self._load_track(self._next_track)
            self.emit("current-changed")

    def __on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst bin
        """
        debug("Player::__on_stream_about_to_finish(): %s" % playbin)
        # Don"t do anything if crossfade on, track already changed
        if self._crossfading:
            return
        if self._current_track.id == Type.RADIOS:
            return
        self._scrobble(self._current_track, self._start_time)
        # Increment popularity
        if not Lp().scanner.is_locked() and self._current_track.id >= 0:
            Lp().tracks.set_more_popular(self._current_track.id)
            # In party mode, linear popularity
            if self.is_party:
                pop_to_add = 1
            # In normal mode, based on tracks count
            else:
                # Some users report an issue where get_tracks_count() return 0
                # See issue #886
                # Don"t understand how this can happen!
                count = Lp().albums.get_tracks_count(
                    self._current_track.album_id)
                if count:
                    pop_to_add = int(Lp().albums.max_count / count)
                else:
                    pop_to_add = 1
            Lp().albums.set_more_popular(self._current_track.album_id,
                                         pop_to_add)
        if self._next_track.id is not None:
            self._load_track(self._next_track)

    def __set_gv_uri(self, uri, track, play):
        """
            Play uri for io
            @param uri as str
            @param track as Track
            @param play as bool
        """
        track.set_uri(uri)
        if play:
            self.load(track)
예제 #9
0
class BinPlayer(ReplayGainPlayer, BasePlayer):
    """
        Gstreamer bin player
    """

    def __init__(self):
        """
            Init playbin
        """
        Gst.init(None)
        BasePlayer.__init__(self)
        self._codecs = Codecs()
        self._playbin = Gst.ElementFactory.make('playbin', 'player')
        flags = self._playbin.get_property("flags")
        flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
        self._playbin.set_property('flags', flags)
        ReplayGainPlayer.__init__(self, self._playbin)
        self._playbin.connect('about-to-finish',
                              self._on_stream_about_to_finish)
        bus = self._playbin.get_bus()
        bus.add_signal_watch()
        bus.connect('message::error', self._on_bus_error)
        bus.connect('message::eos', self._on_bus_eos)
        bus.connect('message::element', self._on_bus_element)
        bus.connect('message::stream-start', self._on_stream_start)
        bus.connect("message::tag", self._on_bus_message_tag)
        self._handled_error = None
        self._start_time = 0

    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(0)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(0)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def load(self, track):
        """
            Stop current track, load track id and play it
            @param track as Track
        """
        self._stop()
        if self._load_track(track):
            self.play()

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self.current_track.id is None:
            if self.next_track.id is not None:
                self.load(self.next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            self.emit("status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        self._playbin.set_state(Gst.State.PAUSED)
        self.emit("status-changed")

    def stop(self):
        """
            Change player state to STOPPED
        """
        self._stop()
        self.emit("status-changed")

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing():
            self.pause()
        else:
            self.play()

    def seek(self, position):
        """
            Seek current track to position
            @param position as seconds
        """
        # Seems gstreamer doesn't like seeking to end, sometimes
        # doesn't go to next track
        if position > self.current_track.duration - 1:
            self.next()
        else:
            self._playbin.seek_simple(Gst.Format.TIME,
                                      Gst.SeekFlags.FLUSH |
                                      Gst.SeekFlags.KEY_UNIT,
                                      position * Gst.SECOND)
            self.emit("seeked", position)

    def get_position_in_track(self):
        """
            Return bin playback position
            @return position as int
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1] / 1000
        return position * 60

    def get_volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.LINEAR)

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        self._playbin.set_volume(GstAudio.StreamVolumeFormat.LINEAR, rate)
        self.emit('volume-changed')

    def next(self):
        """
            Go next track
        """
        pass

#######################
# PRIVATE             #
#######################
    def _stop(self):
        """
            Stop current track (for track change)
        """
        self._playbin.set_state(Gst.State.NULL)

    def _load_track(self, track, sql=None):
        """
            Load track
            @param track as Track
            @param sql as sqlite cursor
            @return False if track not loaded
        """
        stop = False

        # Stop if needed
        if self.context.next == NextContext.STOP_TRACK:
            stop = True

        # Stop if album changed
        if self.context.next == NextContext.STOP_ALBUM and\
           self.current_track.album.id != track.album.id:
            stop = True

        # Stop if album_artist changed
        if self.context.next == NextContext.STOP_ARTIST and\
           self.current_track.album_artist_id != track.album_artist_id:
            stop = True

        if stop and self.is_playing():
            return False

        self.current_track = track

        try:
            self._playbin.set_property('uri',
                                       self.current_track.uri)
        except Exception as e:  # Gstreamer error
            print("BinPlayer::_load_track(): ", e)
            return False

        return True

    def _on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if self.current_track.id >= 0 or\
           self.current_track.duration > 0.0:
            return
        debug("Player::_on_bus_message_tag(): %s" % self.current_track.uri)
        reader = ScannerTagReader()
        tags = message.parse_tag()

        title = reader.get_title(tags, '')
        if title != '':
            self.current_track.name = title
        if self.current_track.name == '':
            self.current_track.name = self.current_track.uri

        artist = reader.get_artists(tags)
        if artist != '':
            self.current_track.artist_names = artist

        # If title set, force artist
        if self.current_track.title != '' and self.current_track.artist == '':
            self.current_track.artist_names = self.current_track.album_artist

        if self.current_track.id == Type.EXTERNALS:
            (b, duration) = self._playbin.query_duration(Gst.Format.TIME)
            if b:
                self.current_track.duration = duration/1000000000
            # We do not use tagreader as we need to check if value is None
            self.current_track.album_name = tags.get_string_index('album',
                                                                  0)[1]
            if self.current_track.album_name is None:
                self.current_track.album_name = ''
            self.current_track.artist_names = reader.get_artists(tags)
            self.current_track.set_album_artist(reader.get_album_artist(tags))
            if self.current_track.album_artist == '':
                self.current_track.set_album_artist(self.current_track.artist)
            self.current_track.genre_name = reader.get_genres(tags)
        self.emit('current-changed')

    def _on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            if self._codecs is not None:
                self._codecs.append(message)

    def _on_bus_error(self, bus, message):
        """
            Handle first bus error, ignore others
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        debug("Error playing: %s" % self.current_track.uri)
        if self._codecs.is_missing_codec(message):
            self._codecs.install()
            Lp.scanner.stop()
        elif Lp.notify is not None:
            Lp.notify.send(_("File doesn't exist: %s") %
                           self.current_track.uri)
        self.stop()
        self.emit('current-changed')
        return True

    def _on_bus_eos(self, bus, message):
        """
            On end of stream, stop playing if user ask for,
            go next otherwise
        """
        debug("Player::_on_bus_eos(): %s" % self.current_track.uri)
        if self.context.next not in [NextContext.NONE,
                                     NextContext.START_NEW_ALBUM]:
            self.stop()
            self.context.next = NextContext.NONE
            if self.next_track.id is not None:
                self._load_track(self.next_track)
            self.emit('current-changed')
        else:
            self.next()

    def _on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst bin
        """
        if self.current_track.id == Type.RADIOS:
            return
        finished = self.current_track
        finished_start_time = self._start_time
        if self.next_track.id is not None:
            self._load_track(self.next_track)
        # We are in a thread, we need to create a new cursor
        sql = Lp.db.get_cursor()
        # Increment popularity
        if not Lp.scanner.is_locked():
            Lp.tracks.set_more_popular(finished.id, sql)
            Lp.albums.set_more_popular(finished.album_id, sql)
        # Scrobble on lastfm
        if Lp.lastfm is not None:
            if finished.album_artist_id == Type.COMPILATIONS:
                artist = finished.artist
            else:
                artist = finished.album_artist
            if time() - finished_start_time > 30:
                Lp.lastfm.scrobble(artist,
                                   finished.album_name,
                                   finished.title,
                                   int(finished_start_time),
                                   int(finished.duration))

        sql.close()

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Emit "current-changed" to notify others components
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self._start_time = time()
        debug("Player::_on_stream_start(): %s" % self.current_track.uri)
        if self.current_track.id >= 0:
            self.emit('current-changed')
        # Update now playing on lastfm
        if Lp.lastfm is not None and self.current_track.id >= 0:
            if self.current_track.album_artist_id == Type.COMPILATIONS:
                artist = self.current_track.artist
            else:
                artist = self.current_track.album_artist
                Lp.lastfm.now_playing(artist,
                                      self.current_track.album_name,
                                      self.current_track.title,
                                      int(self.current_track.duration))
        if not Lp.scanner.is_locked():
            Lp.tracks.set_listened_at(self.current_track.id, int(time()))
        self._handled_error = None
예제 #10
0
class BinPlayer(BasePlayer):
    """
        Gstreamer bin player
    """

    def __init__(self):
        """
            Init playbin
        """
        Gst.init(None)
        BasePlayer.__init__(self)
        self._codecs = Codecs()
        self._crossfading = False
        self._playbin = self._playbin1 = Gst.ElementFactory.make(
                                                           'playbin', 'player')
        self._playbin2 = Gst.ElementFactory.make('playbin', 'player')
        self._preview = None
        self._plugins = self.plugins1 = PluginsPlayer(self._playbin1)
        self.plugins2 = PluginsPlayer(self._playbin2)
        self._volume_id = self._playbin.connect('notify::volume',
                                                self._on_volume_changed)
        for playbin in [self._playbin1, self._playbin2]:
            flags = playbin.get_property("flags")
            flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
            playbin.set_property('flags', flags)
            playbin.set_property('buffer-size', 5 << 20)
            playbin.set_property('buffer-duration', 10 * Gst.SECOND)
            playbin.connect('about-to-finish',
                            self._on_stream_about_to_finish)
            bus = playbin.get_bus()
            bus.add_signal_watch()
            bus.connect('message::error', self._on_bus_error)
            bus.connect('message::eos', self._on_bus_eos)
            bus.connect('message::element', self._on_bus_element)
            bus.connect('message::stream-start', self._on_stream_start)
            bus.connect("message::tag", self._on_bus_message_tag)
        self._handled_error = None
        self._start_time = 0

    @property
    def preview(self):
        """
            Get a preview bin
            @return Gst.Element
        """
        if self._preview is None:
            self._preview = Gst.ElementFactory.make('playbin', 'player')
            self.set_preview_output()
        return self._preview

    def set_preview_output(self):
        """
            Set preview output
        """
        if self._preview is not None:
            output = Lp().settings.get_value('preview-output').get_string()
            pulse = Gst.ElementFactory.make('pulsesink', 'output')
            pulse.set_property('device', output)
            self._preview.set_property('audio-sink', pulse)

    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def load(self, track):
        """
            Stop current track, load track id and play it
            @param track as Track
        """
        if self._crossfading and\
           self.current_track.id is not None and\
           self.is_playing() and\
           self.current_track.id != Type.RADIOS:
            duration = Lp().settings.get_value('mix-duration').get_int32()
            self._do_crossfade(duration, track, False)
        else:
            self._load(track)

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self.current_track.id is None:
            if self._next_track.id is not None:
                self.load(self._next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            self.emit("status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        self._playbin.set_state(Gst.State.PAUSED)
        self.emit("status-changed")

    def stop(self):
        """
            Change player state to STOPPED
        """
        self._playbin.set_state(Gst.State.NULL)
        self.emit("status-changed")

    def stop_all(self):
        """
            Stop all bins, lollypop should quit now
        """
        # Stop
        self._playbin1.set_state(Gst.State.NULL)
        self._playbin2.set_state(Gst.State.NULL)

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing():
            self.pause()
        else:
            self.play()

    def seek(self, position):
        """
            Seek current track to position
            @param position as seconds
        """
        if self.locked or self.current_track.id is None:
            return
        # Seems gstreamer doesn't like seeking to end, sometimes
        # doesn't go to next track
        if position >= self.current_track.duration:
            self.next()
        else:
            self._playbin.seek_simple(Gst.Format.TIME,
                                      Gst.SeekFlags.FLUSH |
                                      Gst.SeekFlags.KEY_UNIT,
                                      position * Gst.SECOND)
            self.emit("seeked", position)

    @property
    def position(self):
        """
            Return bin playback position
            @HACK handle crossefade here, as we know we're going to be
            called every seconds
            @return position as int
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1] / 1000
        if self._crossfading and self.current_track.duration > 0:
            duration = self.current_track.duration - position / 1000000
            if duration < Lp().settings.get_value('mix-duration').get_int32():
                self._do_crossfade(duration)
        return position * 60

    @property
    def volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.CUBIC)

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        self._playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self._playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.emit('volume-changed')

    def next(self):
        """
            Go next track
        """
        pass

#######################
# PRIVATE             #
#######################
    def _load(self, track, init_volume=True):
        """
            Stop current track, load track id and play it
            If was playing, do not use play as status doesn't changed
            @param track as Track
            @param init volume as bool
        """
        was_playing = self.is_playing()
        self._playbin.set_state(Gst.State.NULL)
        if self._load_track(track, init_volume):
            if was_playing:
                self._playbin.set_state(Gst.State.PLAYING)
            else:
                self.play()

    def _volume_up(self, playbin, plugins, duration):
        """
            Make volume going up smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are not the active playbin, stop all
        if self._playbin != playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_up = (1.0 - vol) / steps
            rate = vol + vol_up
            if rate < 1.0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self._volume_up,
                                 playbin, plugins, duration - 0.25)
            else:
                plugins.volume.props.volume = 1.0
        else:
            plugins.volume.props.volume = 1.0

    def _volume_down(self, playbin, plugins, duration):
        """
            Make volume going down smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are again the active playbin, stop all
        if self._playbin == playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_down = vol / steps
            rate = vol - vol_down
            if rate > 0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self._volume_down,
                                 playbin, plugins, duration - 0.25)
            else:
                plugins.volume.props.volume = 0.0
                playbin.set_state(Gst.State.NULL)
        else:
            plugins.volume.props.volume = 0.0
            playbin.set_state(Gst.State.NULL)

    def _do_crossfade(self, duration, track=None, next=True):
        """
            Crossfade tracks
            @param duration as int
            @param track as Track
            @param next as bool
        """
        # No cossfading if we need to stop
        if self._need_to_stop() and next:
            return
        if self._playbin.query_position(Gst.Format.TIME)[1] / 1000000000 >\
                self.current_track.duration - 10:
            self._track_finished(self.current_track, self._start_time)
        GLib.idle_add(self._volume_down, self._playbin,
                      self._plugins, duration)
        if self._playbin == self._playbin2:
            self._playbin = self._playbin1
            self._plugins = self.plugins1
        else:
            self._playbin = self._playbin2
            self._plugins = self.plugins2

        if track is not None:
            self._load(track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self._volume_up, self._playbin,
                          self._plugins, duration)
        elif next and self._next_track.id is not None:
            self._load(self._next_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self._volume_up, self._playbin,
                          self._plugins, duration)
        elif self._prev_track.id is not None:
            self._load(self._prev_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self._volume_up, self._playbin,
                          self._plugins, duration)

    def _need_to_stop(self):
        """
            Return True if playback needs to stop
            @return bool
        """
        stop = False
        if self._context.next != NextContext.NONE:
            # Stop if needed
            if self._context.next == NextContext.STOP_TRACK:
                stop = True
            elif self._context.next == self._finished:
                stop = True
        return stop and self.is_playing()

    def _load_track(self, track, init_volume=True):
        """
            Load track
            @param track as Track
            @param init volume as bool
            @return False if track not loaded
        """
        if self._need_to_stop():
            return False
        if init_volume:
            self._plugins.volume.props.volume = 1.0
        debug("BinPlayer::_load_track(): %s" % track.uri)
        try:
            if track.id in self._queue:
                self._queue_track = track
                self._queue.remove(track.id)
                self.emit('queue-changed')
                self._playbin.set_property('uri', self._queue_track.uri)
            else:
                self._current_track = track
                self._queue_track = None
                self._playbin.set_property('uri', self.current_track.uri)
        except Exception as e:  # Gstreamer error
            print("BinPlayer::_load_track(): ", e)
            self._queue_track = None
            return False
        return True

    def _track_finished(self, finished, finished_start_time):
        """
            Do some actions for played track
            @param finished as Track
            @param finished_start_time as int
        """
        # Increment popularity
        if not Lp().scanner.is_locked():
            Lp().tracks.set_more_popular(finished.id)
            Lp().albums.set_more_popular(finished.album_id)
        # Scrobble on lastfm
        if Lp().lastfm is not None:
            artists = ", ".join(finished.artists)
            if time() - finished_start_time > 30:
                Lp().lastfm.scrobble(artists,
                                     finished.album_name,
                                     finished.title,
                                     int(finished_start_time),
                                     int(finished.duration))

    def _on_volume_changed(self, playbin, sink):
        """
            Update volume
            @param playbin as Gst.Bin
            @param sink as Gst.Sink
        """
        if playbin == self._playbin1:
            vol = self._playbin1.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self._playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        else:
            vol = self._playbin2.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self._playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        self.emit('volume-changed')

    def _on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if self.current_track.id >= 0 or\
           self.current_track.duration > 0.0:
            return
        debug("Player::_on_bus_message_tag(): %s" % self.current_track.uri)
        reader = ScannerTagReader()
        tags = message.parse_tag()

        title = reader.get_title(tags, '')
        if title != '':
            self.current_track.name = title
        if self.current_track.name == '':
            self.current_track.name = self.current_track.uri
        artists = reader.get_artists(tags)
        if artists != '':
            self.current_track.artists = artists.split(',')
        if not self.current_track.artists:
            self.current_track.artists = self.current_track.album_artists

        if self.current_track.id == Type.EXTERNALS:
            (b, duration) = self._playbin.query_duration(Gst.Format.TIME)
            if b:
                self.current_track.duration = duration/1000000000
            # We do not use tagreader as we need to check if value is None
            self.current_track.album_name = tags.get_string_index('album',
                                                                  0)[1]
            if self.current_track.album_name is None:
                self.current_track.album_name = ''
            self.current_track.artists = reader.get_artists(tags).split(',')
            self.current_track.set_album_artists(
                                      reader.get_album_artist(tags).split(','))
            if self.current_track.album_artist == '':
                self.current_track.set_album_artists(
                                                   self.current_track.artists)
            self.current_track.genres = reader.get_genres(tags).split(',')
        self.emit('current-changed')

    def _on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            self._codecs.append(message)

    def _on_bus_error(self, bus, message):
        """
            Handle first bus error, ignore others
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        debug("Error playing: %s" % self.current_track.uri)
        Lp().window.pulse(False)
        if self._codecs.is_missing_codec(message):
            self._codecs.install()
            Lp().scanner.stop()
        elif Lp().notify is not None:
            Lp().notify.send(_("File doesn't exist: %s") %
                             self.current_track.uri)
        self.stop()
        self.emit('current-changed')
        return True

    def _on_bus_eos(self, bus, message):
        """
            On end of stream, stop playback
            go next otherwise
        """
        debug("Player::_on_bus_eos(): %s" % self.current_track.uri)
        if self._playbin.get_bus() == bus:
            self.stop()
            self._finished = NextContext.NONE
            if Lp().settings.get_value('repeat'):
                self._context.next = NextContext.NONE
            else:
                self._context.next = NextContext.STOP_ALL
            if self._next_track.id is not None:
                self._load_track(self._next_track)
            self.emit('current-changed')

    def _on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst bin
        """
        debug("Player::_on_stream_about_to_finish(): %s" % playbin)
        # Don't do anything if crossfade on
        if self._crossfading:
            return
        if self.current_track.id == Type.RADIOS:
            return
        finished = self.current_track
        finished_start_time = self._start_time
        if self._next_track.id is not None:
            self._load_track(self._next_track)
        self._track_finished(finished, finished_start_time)

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Emit "current-changed" to notify others components
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self._start_time = time()
        debug("Player::_on_stream_start(): %s" % self.current_track.uri)
        self.emit('current-changed')
        # Update now playing on lastfm
        if Lp().lastfm is not None and self.current_track.id >= 0:
            artists = ", ".join(self.current_track.artists)
            Lp().lastfm.now_playing(artists,
                                    self.current_track.album_name,
                                    self.current_track.title,
                                    int(self.current_track.duration))
        if not Lp().scanner.is_locked():
            Lp().tracks.set_listened_at(self.current_track.id, int(time()))
        self._handled_error = None
예제 #11
0
class BinPlayer(BasePlayer):
    """
        Gstreamer bin player
    """

    def __init__(self):
        """
            Init playbin
        """
        Gst.init(None)
        BasePlayer.__init__(self)
        self.__codecs = Codecs()
        self._playbin = self.__playbin1 = Gst.ElementFactory.make(
                                                           'playbin', 'player')
        self.__playbin2 = Gst.ElementFactory.make('playbin', 'player')
        self.__preview = None
        self._plugins = self._plugins1 = PluginsPlayer(self.__playbin1)
        self._plugins2 = PluginsPlayer(self.__playbin2)
        self._playbin.connect('notify::volume', self.__on_volume_changed)
        for playbin in [self.__playbin1, self.__playbin2]:
            flags = playbin.get_property("flags")
            flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
            playbin.set_property('flags', flags)
            playbin.set_property('buffer-size', 5 << 20)
            playbin.set_property('buffer-duration', 10 * Gst.SECOND)
            playbin.connect('about-to-finish',
                            self.__on_stream_about_to_finish)
            bus = playbin.get_bus()
            bus.add_signal_watch()
            bus.connect('message::error', self.__on_bus_error)
            bus.connect('message::eos', self.__on_bus_eos)
            bus.connect('message::element', self.__on_bus_element)
            bus.connect('message::stream-start', self._on_stream_start)
            bus.connect("message::tag", self.__on_bus_message_tag)
        self._start_time = 0

    @property
    def preview(self):
        """
            Get a preview bin
            @return Gst.Element
        """
        if self.__preview is None:
            self.__preview = Gst.ElementFactory.make('playbin', 'player')
            PluginsPlayer(self.__preview)
            self.set_preview_output()
        return self.__preview

    def set_preview_output(self):
        """
            Set preview output
        """
        if self.__preview is not None:
            output = Lp().settings.get_value('preview-output').get_string()
            pulse = Gst.ElementFactory.make('pulsesink', 'output')
            if pulse is None:
                pulse = Gst.ElementFactory.make('alsasink', 'output')
            if pulse is not None:
                pulse.set_property('device', output)
                self.__preview.set_property('audio-sink', pulse)

    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def load(self, track):
        """
            Stop current track, load track id and play it
            @param track as Track
        """
        if self._crossfading and\
           self.current_track.id is not None and\
           self.is_playing() and\
           self.current_track.id != Type.RADIOS:
            duration = Lp().settings.get_value('mix-duration').get_int32()
            self.__do_crossfade(duration, track, False)
        else:
            self.__load(track)

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self.current_track.id is None:
            if self._next_track.id is not None:
                self.load(self._next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            self.emit("status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        if self.current_track.id == Type.RADIOS:
            self._playbin.set_state(Gst.State.NULL)
        else:
            self._playbin.set_state(Gst.State.PAUSED)
        self.emit("status-changed")

    def stop(self):
        """
            Change player state to STOPPED
        """
        self._playbin.set_state(Gst.State.NULL)
        self.emit("status-changed")

    def stop_all(self):
        """
            Stop all bins, lollypop should quit now
        """
        # Stop
        self.__playbin1.set_state(Gst.State.NULL)
        self.__playbin2.set_state(Gst.State.NULL)

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing():
            self.pause()
        else:
            self.play()

    def seek(self, position):
        """
            Seek current track to position
            @param position as seconds
        """
        if self.locked or self.current_track.id is None:
            return
        # Seems gstreamer doesn't like seeking to end, sometimes
        # doesn't go to next track
        if position >= self.current_track.duration:
            self.next()
        else:
            self._playbin.seek_simple(Gst.Format.TIME,
                                      Gst.SeekFlags.FLUSH |
                                      Gst.SeekFlags.KEY_UNIT,
                                      position * Gst.SECOND)
            self.emit("seeked", position)

    @property
    def position(self):
        """
            Return bin playback position
            @HACK handle crossefade here, as we know we're going to be
            called every seconds
            @return position as int
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1] / 1000
        if self._crossfading and self.current_track.duration > 0:
            duration = self.current_track.duration - position / 1000000
            if duration < Lp().settings.get_value('mix-duration').get_int32():
                self.__do_crossfade(duration)
        return position * 60

    @property
    def volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.CUBIC)

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        self.__playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.__playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.emit('volume-changed')

    def next(self):
        """
            Go next track
        """
        pass

#######################
# PROTECTED           #
#######################
    def _load_track(self, track, init_volume=True):
        """
            Load track
            @param track as Track
            @param init volume as bool
            @return False if track not loaded
        """
        if self.__need_to_stop():
            return False
        if init_volume:
            self._plugins.volume.props.volume = 1.0
        debug("BinPlayer::_load_track(): %s" % track.uri)
        try:
            if track.id in self._queue:
                self._queue_track = track
                self._queue.remove(track.id)
                self.emit('queue-changed')
            else:
                self._current_track = track
                self._queue_track = None
            if track.is_web:
                loaded = self._load_web(track)
                # If track not loaded, go next
                if not loaded:
                    self.set_next()
                    GLib.timeout_add(500, self.__load,
                                     self.next_track, init_volume)
                return False  # Return not loaded as handled by load_web()
            else:
                self._playbin.set_property('uri', track.uri)
        except Exception as e:  # Gstreamer error
            print("BinPlayer::_load_track(): ", e)
            self._queue_track = None
            return False
        return True

    def _load_web(self, track, play=True):
        """
            Load track url and play it
            @param track as Track
            @param play as bool
            @return True if loading
        """
        if not get_network_available():
            # Force widgets to update (spinners)
            self.emit('current-changed')
            return False
        try:
            from lollypop.web import Web
            if play:
                self.emit('loading-changed', True)
            t = Thread(target=Web.play_track,
                       args=(track, play, self.__set_gv_uri))
            t.daemon = True
            t.start()
            return True
        except Exception as e:
            self._current_track = Track()
            self.stop()
            self.emit('current-changed')
            if Lp().notify is not None:
                Lp().notify.send(str(e), track.uri)
            print("PlayerBin::_load_web()", e)

    def _scrobble(self, finished, finished_start_time):
        """
            Scrobble on lastfm
            @param finished as Track
            @param finished_start_time as int
        """
        # Last.fm policy
        if finished.duration < 30:
            return
        # Scrobble on lastfm
        if Lp().lastfm is not None:
            artists = ", ".join(finished.artists)
            played = time() - finished_start_time
            # We can scrobble if the track has been played
            # for at least half its duration, or for 4 minutes
            if played >= finished.duration / 2 or played >= 240:
                Lp().lastfm.do_scrobble(artists,
                                        finished.album_name,
                                        finished.title,
                                        int(finished_start_time))

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Emit "current-changed" to notify others components
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self._start_time = time()
        debug("Player::_on_stream_start(): %s" % self.current_track.uri)
        self.emit('current-changed')
        # Update now playing on lastfm
        if Lp().lastfm is not None and self.current_track.id >= 0:
            artists = ", ".join(self.current_track.artists)
            Lp().lastfm.now_playing(artists,
                                    self.current_track.album_name,
                                    self.current_track.title,
                                    int(self.current_track.duration))
        if not Lp().scanner.is_locked():
            Lp().tracks.set_listened_at(self.current_track.id, int(time()))

#######################
# PRIVATE             #
#######################
    def __update_current_duration(self, reader, track):
        """
            Update current track duration
            @param reader as TagReader
            @param track id as int
        """
        try:
            duration = reader.get_info(track.uri).get_duration() / 1000000000
            if duration != track.duration and duration > 0:
                Lp().tracks.set_duration(track.id, duration)
                # We modify mtime to be sure not looking for tags again
                Lp().tracks.set_mtime(track.id, 1)
                self.current_track.set_duration(duration)
                GLib.idle_add(self.emit, 'duration-changed', track.id)
        except:
            pass

    def __load(self, track, init_volume=True):
        """
            Stop current track, load track id and play it
            If was playing, do not use play as status doesn't changed
            @param track as Track
            @param init volume as bool
        """
        was_playing = self.is_playing()
        self._playbin.set_state(Gst.State.NULL)
        if self._load_track(track, init_volume):
            if was_playing:
                self._playbin.set_state(Gst.State.PLAYING)
            else:
                self.play()

    def __volume_up(self, playbin, plugins, duration):
        """
            Make volume going up smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are not the active playbin, stop all
        if self._playbin != playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_up = (1.0 - vol) / steps
            rate = vol + vol_up
            if rate < 1.0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self.__volume_up,
                                 playbin, plugins, duration - 0.25)
            else:
                plugins.volume.props.volume = 1.0
        else:
            plugins.volume.props.volume = 1.0

    def __volume_down(self, playbin, plugins, duration):
        """
            Make volume going down smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are again the active playbin, stop all
        if self._playbin == playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_down = vol / steps
            rate = vol - vol_down
            if rate > 0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self.__volume_down,
                                 playbin, plugins, duration - 0.25)
            else:
                plugins.volume.props.volume = 0.0
                playbin.set_state(Gst.State.NULL)
        else:
            plugins.volume.props.volume = 0.0
            playbin.set_state(Gst.State.NULL)

    def __do_crossfade(self, duration, track=None, next=True):
        """
            Crossfade tracks
            @param duration as int
            @param track as Track
            @param next as bool
        """
        # No cossfading if we need to stop
        if self.__need_to_stop() and next:
            return

        if track is None:
            self._scrobble(self.current_track, self._start_time)
            # Increment popularity
            if not Lp().scanner.is_locked():
                Lp().tracks.set_more_popular(self.current_track.id)
                # In party mode, linear popularity
                if self.is_party:
                    pop_to_add = 1
                # In normal mode, based on tracks count
                else:
                    pop_to_add = int(Lp().albums.max_count /
                                     Lp().albums.get_tracks_count(
                                                  self.current_track.album_id))
                Lp().albums.set_more_popular(self.current_track.album_id,
                                             pop_to_add)

        GLib.idle_add(self.__volume_down, self._playbin,
                      self._plugins, duration)
        if self._playbin == self.__playbin2:
            self._playbin = self.__playbin1
            self._plugins = self._plugins1
        else:
            self._playbin = self.__playbin2
            self._plugins = self._plugins2

        if track is not None:
            self.__load(track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin,
                          self._plugins, duration)
        elif next and self._next_track.id is not None:
            self.__load(self._next_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin,
                          self._plugins, duration)
        elif self._prev_track.id is not None:
            self.__load(self._prev_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin,
                          self._plugins, duration)

    def __need_to_stop(self):
        """
            Return True if playback needs to stop
            @return bool
        """
        stop = False
        playback = Lp().settings.get_enum('playback')
        if playback == NextContext.STOP:
            if not self._albums or playback == self._next_context:
                stop = True
        return stop and self.is_playing()

    def __on_volume_changed(self, playbin, sink):
        """
            Update volume
            @param playbin as Gst.Bin
            @param sink as Gst.Sink
        """
        if playbin == self.__playbin1:
            vol = self.__playbin1.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self.__playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        else:
            vol = self.__playbin2.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self.__playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        self.emit('volume-changed')

    def __on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        # Some radio streams send message tag every seconds!
        changed = False
        if (self.current_track.persistent == DbPersistent.INTERNAL or
            self.current_track.mtime != 0) and\
            (self.current_track.id >= 0 or
             self.current_track.duration > 0.0):
            return
        debug("Player::__on_bus_message_tag(): %s" % self.current_track.uri)
        reader = TagReader()

        # Update duration of non internals
        if self.current_track.persistent != DbPersistent.INTERNAL:
            t = Thread(target=self.__update_current_duration,
                       args=(reader, self.current_track))
            t.daemon = True
            t.start()
            return

        tags = message.parse_tag()
        title = reader.get_title(tags, '')
        if title != '' and self.current_track.name != title:
            self.current_track.name = title
            changed = True
        if self.current_track.name == '':
            self.current_track.name = self.current_track.uri
            changed = True
        artists = reader.get_artists(tags)
        if artists != '' and self.current_track.artists != artists:
            self.current_track.artists = artists.split(',')
            changed = True
        if not self.current_track.artists:
            self.current_track.artists = self.current_track.album_artists
            changed = True

        if self.current_track.id == Type.EXTERNALS:
            (b, duration) = self._playbin.query_duration(Gst.Format.TIME)
            if b:
                self.current_track.duration = duration/1000000000
            # We do not use tagreader as we need to check if value is None
            self.current_track.album_name = tags.get_string_index('album',
                                                                  0)[1]
            if self.current_track.album_name is None:
                self.current_track.album_name = ''
            self.current_track.genres = reader.get_genres(tags).split(',')
            changed = True
        if changed:
            self.emit('current-changed')

    def __on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            self.__codecs.append(message)

    def __on_bus_error(self, bus, message):
        """
            Handle first bus error, ignore others
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        debug("Error playing: %s" % self.current_track.uri)
        Lp().window.pulse(False)
        if self.__codecs.is_missing_codec(message):
            self.__codecs.install()
            Lp().scanner.stop()
        elif Lp().notify is not None:
            Lp().notify.send(message.parse_error()[0].message)
        self.emit('current-changed')
        return True

    def __on_bus_eos(self, bus, message):
        """
            On end of stream, stop playback
            go next otherwise
        """
        debug("Player::__on_bus_eos(): %s" % self.current_track.uri)
        if self._playbin.get_bus() == bus:
            self.stop()
            self._next_context = NextContext.NONE
            if self._next_track.id is not None:
                self._load_track(self._next_track)
            self.emit('current-changed')

    def __on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst bin
        """
        debug("Player::__on_stream_about_to_finish(): %s" % playbin)
        # Don't do anything if crossfade on, track already changed
        if self._crossfading:
            return
        if self.current_track.id == Type.RADIOS:
            return
        self._scrobble(self.current_track, self._start_time)
        # Increment popularity
        if not Lp().scanner.is_locked():
            Lp().tracks.set_more_popular(self.current_track.id)
            # In party mode, linear popularity
            if self.is_party:
                pop_to_add = 1
            # In normal mode, based on tracks count
            else:
                pop_to_add = int(Lp().albums.max_count /
                                 Lp().albums.get_tracks_count(
                                                  self.current_track.album_id))
            Lp().albums.set_more_popular(self.current_track.album_id,
                                         pop_to_add)
        if self._next_track.id is not None:
            self._load_track(self._next_track)

    def __set_gv_uri(self, uri, track, play):
        """
            Play uri for io
            @param uri as str
            @param track as Track
            @param play as bool
        """
        track.set_uri(uri)
        if play:
            self.load(track)
예제 #12
0
class BinPlayer(BasePlayer):
    """
        Gstreamer bin player
    """
    def __init__(self):
        """
            Init playbin
        """
        Gst.init(None)
        BasePlayer.__init__(self)
        self.__codecs = Codecs()
        self._playbin = self.__playbin1 = Gst.ElementFactory.make(
            'playbin', 'player')
        self.__playbin2 = Gst.ElementFactory.make('playbin', 'player')
        self.__preview = None
        self._plugins = self._plugins1 = PluginsPlayer(self.__playbin1)
        self._plugins2 = PluginsPlayer(self.__playbin2)
        self._playbin.connect('notify::volume', self.__on_volume_changed)
        for playbin in [self.__playbin1, self.__playbin2]:
            flags = playbin.get_property("flags")
            flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
            playbin.set_property('flags', flags)
            playbin.set_property('buffer-size', 5 << 20)
            playbin.set_property('buffer-duration', 10 * Gst.SECOND)
            playbin.connect('about-to-finish',
                            self.__on_stream_about_to_finish)
            bus = playbin.get_bus()
            bus.add_signal_watch()
            bus.connect('message::error', self.__on_bus_error)
            bus.connect('message::eos', self.__on_bus_eos)
            bus.connect('message::element', self.__on_bus_element)
            bus.connect('message::stream-start', self._on_stream_start)
            bus.connect("message::tag", self.__on_bus_message_tag)
        self._start_time = 0

    @property
    def preview(self):
        """
            Get a preview bin
            @return Gst.Element
        """
        if self.__preview is None:
            self.__preview = Gst.ElementFactory.make('playbin', 'player')
            PluginsPlayer(self.__preview)
            self.set_preview_output()
        return self.__preview

    def set_preview_output(self):
        """
            Set preview output
        """
        if self.__preview is not None:
            output = Lp().settings.get_value('preview-output').get_string()
            pulse = Gst.ElementFactory.make('pulsesink', 'output')
            pulse.set_property('device', output)
            self.__preview.set_property('audio-sink', pulse)

    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def load(self, track):
        """
            Stop current track, load track id and play it
            @param track as Track
        """
        if self._crossfading and\
           self.current_track.id is not None and\
           self.is_playing() and\
           self.current_track.id != Type.RADIOS:
            duration = Lp().settings.get_value('mix-duration').get_int32()
            self.__do_crossfade(duration, track, False)
        else:
            self.__load(track)

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self.current_track.id is None:
            if self._next_track.id is not None:
                self.load(self._next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            self.emit("status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        self._playbin.set_state(Gst.State.PAUSED)
        self.emit("status-changed")

    def stop(self):
        """
            Change player state to STOPPED
        """
        self._playbin.set_state(Gst.State.NULL)
        self.emit("status-changed")

    def stop_all(self):
        """
            Stop all bins, lollypop should quit now
        """
        # Stop
        self.__playbin1.set_state(Gst.State.NULL)
        self.__playbin2.set_state(Gst.State.NULL)

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing():
            self.pause()
        else:
            self.play()

    def seek(self, position):
        """
            Seek current track to position
            @param position as seconds
        """
        if self.locked or self.current_track.id is None:
            return
        # Seems gstreamer doesn't like seeking to end, sometimes
        # doesn't go to next track
        if position >= self.current_track.duration:
            self.next()
        else:
            self._playbin.seek_simple(
                Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                position * Gst.SECOND)
            self.emit("seeked", position)

    @property
    def position(self):
        """
            Return bin playback position
            @HACK handle crossefade here, as we know we're going to be
            called every seconds
            @return position as int
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1] / 1000
        if self._crossfading and self.current_track.duration > 0:
            duration = self.current_track.duration - position / 1000000
            if duration < Lp().settings.get_value('mix-duration').get_int32():
                self.__do_crossfade(duration)
        return position * 60

    @property
    def volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.CUBIC)

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        self.__playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.__playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.emit('volume-changed')

    def next(self):
        """
            Go next track
        """
        pass

#######################
# PROTECTED           #
#######################

    def _load_track(self, track, init_volume=True):
        """
            Load track
            @param track as Track
            @param init volume as bool
            @return False if track not loaded
        """
        if self.__need_to_stop():
            return False
        if init_volume:
            self._plugins.volume.props.volume = 1.0
        debug("BinPlayer::_load_track(): %s" % track.uri)
        try:
            if track.id in self._queue:
                self._queue_track = track
                self._queue.remove(track.id)
                self.emit('queue-changed')
            else:
                self._current_track = track
                self._queue_track = None
            if track.is_youtube:
                loaded = self._load_youtube(track)
                # If track not loaded, go next
                if not loaded:
                    self.set_next()
                    GLib.timeout_add(500, self.__load, self.next_track,
                                     init_volume)
                return False  # Return not loaded as handled by load_youtube()
            else:
                self._playbin.set_property('uri', track.uri)
        except Exception as e:  # Gstreamer error
            print("BinPlayer::_load_track(): ", e)
            self._queue_track = None
            return False
        return True

    def _load_youtube(self, track, play=True):
        """
            Load track url and play it
            @param track as Track
            @param play as bool
            @return True if loading
        """
        if not Gio.NetworkMonitor.get_default().get_network_available():
            # Force widgets to update (spinners)
            self.emit('current-changed')
            return False
        argv = ["youtube-dl", "-g", "-f", "bestaudio", track.uri, None]
        try:
            self.emit('loading-changed')
            (s, pid, i, o, err) = GLib.spawn_async_with_pipes(
                None, argv, None, GLib.SpawnFlags.SEARCH_PATH
                | GLib.SpawnFlags.DO_NOT_REAP_CHILD, None)
            io = GLib.IOChannel(o)
            io.add_watch(GLib.IO_IN | GLib.IO_HUP,
                         self.__set_gv_uri,
                         track,
                         play,
                         priority=GLib.PRIORITY_HIGH)
            return True
        except Exception as e:
            print("Youtube::__get_youtube_uri()", e)

    def _scrobble(self, finished, finished_start_time):
        """
            Scrobble on lastfm
            @param finished as Track
            @param finished_start_time as int
        """
        # Last.fm policy
        if finished.duration < 30:
            return
        # Scrobble on lastfm
        if Lp().lastfm is not None:
            artists = ", ".join(finished.artists)
            played = time() - finished_start_time
            # We can scrobble if the track has been played
            # for at least half its duration, or for 4 minutes
            if played >= finished.duration / 2 or played >= 240:
                Lp().lastfm.scrobble(artists, finished.album_name,
                                     finished.title, int(finished_start_time),
                                     int(finished.duration))

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Emit "current-changed" to notify others components
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self._start_time = time()
        debug("Player::_on_stream_start(): %s" % self.current_track.uri)
        self.emit('current-changed')
        # Update now playing on lastfm
        if Lp().lastfm is not None and self.current_track.id >= 0:
            artists = ", ".join(self.current_track.artists)
            Lp().lastfm.now_playing(artists, self.current_track.album_name,
                                    self.current_track.title,
                                    int(self.current_track.duration))
        if not Lp().scanner.is_locked():
            Lp().tracks.set_listened_at(self.current_track.id, int(time()))

#######################
# PRIVATE             #
#######################

    def __update_current_duration(self, reader, track):
        """
            Update current track duration
            @param reader as TagReader
            @param track id as int
        """
        try:
            duration = reader.get_info(track.uri).get_duration() / 1000000000
            if duration != track.duration and duration > 0:
                Lp().tracks.set_duration(track.id, duration)
                # We modify mtime to be sure not looking for tags again
                Lp().tracks.set_mtime(track.id, 1)
                self.current_track.set_duration(duration)
                GLib.idle_add(self.emit, 'duration-changed', track.id)
        except:
            pass

    def __load(self, track, init_volume=True):
        """
            Stop current track, load track id and play it
            If was playing, do not use play as status doesn't changed
            @param track as Track
            @param init volume as bool
        """
        was_playing = self.is_playing()
        self._playbin.set_state(Gst.State.NULL)
        if self._load_track(track, init_volume):
            if was_playing:
                self._playbin.set_state(Gst.State.PLAYING)
            else:
                self.play()

    def __volume_up(self, playbin, plugins, duration):
        """
            Make volume going up smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are not the active playbin, stop all
        if self._playbin != playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_up = (1.0 - vol) / steps
            rate = vol + vol_up
            if rate < 1.0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self.__volume_up, playbin, plugins,
                                 duration - 0.25)
            else:
                plugins.volume.props.volume = 1.0
        else:
            plugins.volume.props.volume = 1.0

    def __volume_down(self, playbin, plugins, duration):
        """
            Make volume going down smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are again the active playbin, stop all
        if self._playbin == playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_down = vol / steps
            rate = vol - vol_down
            if rate > 0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self.__volume_down, playbin, plugins,
                                 duration - 0.25)
            else:
                plugins.volume.props.volume = 0.0
                playbin.set_state(Gst.State.NULL)
        else:
            plugins.volume.props.volume = 0.0
            playbin.set_state(Gst.State.NULL)

    def __do_crossfade(self, duration, track=None, next=True):
        """
            Crossfade tracks
            @param duration as int
            @param track as Track
            @param next as bool
        """
        # No cossfading if we need to stop
        if self.__need_to_stop() and next:
            return
        if self._playbin.query_position(Gst.Format.TIME)[1] / 1000000000 >\
                self.current_track.duration - 10:
            self._scrobble(self.current_track, self._start_time)
        GLib.idle_add(self.__volume_down, self._playbin, self._plugins,
                      duration)
        if self._playbin == self.__playbin2:
            self._playbin = self.__playbin1
            self._plugins = self._plugins1
        else:
            self._playbin = self.__playbin2
            self._plugins = self._plugins2

        if track is not None:
            self.__load(track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin, self._plugins,
                          duration)
        elif next and self._next_track.id is not None:
            self.__load(self._next_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin, self._plugins,
                          duration)
        elif self._prev_track.id is not None:
            self.__load(self._prev_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin, self._plugins,
                          duration)

    def __need_to_stop(self):
        """
            Return True if playback needs to stop
            @return bool
        """
        stop = False
        if self._context.next != NextContext.NONE:
            # Stop if needed
            if self._context.next == NextContext.STOP_TRACK:
                stop = True
            elif self._context.next == self._finished:
                stop = True
        return stop and self.is_playing()

    def __on_volume_changed(self, playbin, sink):
        """
            Update volume
            @param playbin as Gst.Bin
            @param sink as Gst.Sink
        """
        if playbin == self.__playbin1:
            vol = self.__playbin1.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self.__playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        else:
            vol = self.__playbin2.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self.__playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        self.emit('volume-changed')

    def __on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        # Some radio streams send message tag every seconds!
        changed = False
        if (self.current_track.persistent == DbPersistent.INTERNAL or
            self.current_track.mtime != 0) and\
            (self.current_track.id >= 0 or
             self.current_track.duration > 0.0):
            return
        debug("Player::__on_bus_message_tag(): %s" % self.current_track.uri)
        reader = TagReader()

        # Update duration of non internals
        if self.current_track.persistent != DbPersistent.INTERNAL:
            t = Thread(target=self.__update_current_duration,
                       args=(reader, self.current_track))
            t.daemon = True
            t.start()
            return

        tags = message.parse_tag()
        title = reader.get_title(tags, '')
        if title != '' and self.current_track.name != title:
            self.current_track.name = title
            changed = True
        if self.current_track.name == '':
            self.current_track.name = self.current_track.uri
            changed = True
        artists = reader.get_artists(tags)
        if artists != '' and self.current_track.artists != artists:
            self.current_track.artists = artists.split(',')
            changed = True
        if not self.current_track.artists:
            self.current_track.artists = self.current_track.album_artists
            changed = True

        if self.current_track.id == Type.EXTERNALS:
            (b, duration) = self._playbin.query_duration(Gst.Format.TIME)
            if b:
                self.current_track.duration = duration / 1000000000
            # We do not use tagreader as we need to check if value is None
            self.current_track.album_name = tags.get_string_index('album',
                                                                  0)[1]
            if self.current_track.album_name is None:
                self.current_track.album_name = ''
            self.current_track.artists = reader.get_artists(tags).split(',')
            self.current_track.set_album_artists(
                reader.get_album_artist(tags).split(','))
            if self.current_track.album_artist == '':
                self.current_track.set_album_artists(
                    self.current_track.artists)
            self.current_track.genres = reader.get_genres(tags).split(',')
            changed = True
        if changed:
            self.emit('current-changed')

    def __on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            self.__codecs.append(message)

    def __on_bus_error(self, bus, message):
        """
            Handle first bus error, ignore others
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        debug("Error playing: %s" % self.current_track.uri)
        Lp().window.pulse(False)
        if self.__codecs.is_missing_codec(message):
            self.__codecs.install()
            Lp().scanner.stop()
        elif Lp().notify is not None:
            Lp().notify.send(message.parse_error()[0].message)
        self.emit('current-changed')
        return True

    def __on_bus_eos(self, bus, message):
        """
            On end of stream, stop playback
            go next otherwise
        """
        debug("Player::__on_bus_eos(): %s" % self.current_track.uri)
        if self._playbin.get_bus() == bus:
            self.stop()
            self._finished = NextContext.NONE
            if Lp().settings.get_value('repeat'):
                self._context.next = NextContext.NONE
            else:
                self._context.next = NextContext.STOP_ALL
            if self._next_track.id is not None:
                self._load_track(self._next_track)
            self.emit('current-changed')

    def __on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst bin
        """
        debug("Player::__on_stream_about_to_finish(): %s" % playbin)
        # Don't do anything if crossfade on
        if self._crossfading:
            return
        if self.current_track.id == Type.RADIOS:
            return
        # For Last.fm scrobble
        finished = self.current_track
        finished_start_time = self._start_time
        if self._next_track.id is not None:
            self._load_track(self._next_track)
        self._scrobble(finished, finished_start_time)
        # Increment popularity
        if not Lp().scanner.is_locked():
            Lp().tracks.set_more_popular(finished.id)
            Lp().albums.set_more_popular(finished.album_id)

    def __set_gv_uri(self, io, condition, track, play):
        """
            Play uri for io
            @param io as GLib.IOChannel
            @param condition as Constant
            @param track as Track
            @param play as bool
        """
        track.set_uri(io.readline())
        if play:
            self.load(track)
        return False
예제 #13
0
class BinPlayer:
    """
        Gstreamer bin player
    """
    def __init__(self):
        """
            Init playbin
        """
        # In the case of gapless playback, both 'about-to-finish'
        # and 'eos' can occur during the same stream.
        self.__track_in_pipe = False
        self.__cancellable = Gio.Cancellable()
        self.__codecs = Codecs()
        self._current_track = Track()
        self._next_track = Track()
        self._prev_track = Track()
        self._playbin = self._playbin1 = Gst.ElementFactory.make(
            "playbin", "player")
        self._playbin2 = Gst.ElementFactory.make("playbin", "player")
        self._plugins = self._plugins1 = PluginsPlayer(self._playbin1)
        self._plugins2 = PluginsPlayer(self._playbin2)
        for playbin in [self._playbin1, self._playbin2]:
            flags = playbin.get_property("flags")
            flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
            playbin.set_property("flags", flags)
            playbin.set_property("buffer-size", 5 << 20)
            playbin.set_property("buffer-duration", 10 * Gst.SECOND)
            playbin.connect("notify::volume", self.__on_volume_changed)
            playbin.connect("about-to-finish", self._on_stream_about_to_finish)
            bus = playbin.get_bus()
            bus.add_signal_watch()
            bus.connect("message::error", self._on_bus_error)
            bus.connect("message::eos", self._on_bus_eos)
            bus.connect("message::element", self._on_bus_element)
            bus.connect("message::stream-start", self._on_stream_start)
            bus.connect("message::tag", self._on_bus_message_tag)
        self._start_time = 0

    def load(self, track):
        """
            Load track and play it
            @param track as Track
        """
        self._playbin.set_state(Gst.State.NULL)
        if self._load_track(track):
            self.play()

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self._current_track.id is None:
            if self._next_track.id is not None:
                self.load(self._next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            emit_signal(self, "status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        self._playbin.set_state(Gst.State.PAUSED)
        emit_signal(self, "status-changed")

    def stop(self):
        """
            Change player state to STOPPED
            @param force as bool
        """
        self._current_track = Track()
        self._current_track = Track()
        self._prev_track = Track()
        self._next_track = Track()
        emit_signal(self, "current-changed")
        emit_signal(self, "prev-changed")
        emit_signal(self, "next-changed")
        self._playbin.set_state(Gst.State.NULL)
        emit_signal(self, "status-changed")

    def stop_all(self):
        """
            Stop all bins, lollypop should quit now
        """
        # Stop
        self._playbin1.set_state(Gst.State.NULL)
        self._playbin2.set_state(Gst.State.NULL)

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing:
            self.pause()
        else:
            self.play()

    def reload_track(self):
        """
            Reload track at current position
        """
        if self.current_track.id is None:
            return
        position = self.position
        self.load(self.current_track)
        GLib.timeout_add(100, self.seek, position)

    def seek(self, position):
        """
            Seek current track to position
            @param position as int (ms)
        """
        if self._current_track.id is None:
            return
        # Seems gstreamer doesn't like seeking to end, sometimes
        # doesn't go to next track
        if position >= self._current_track.duration:
            self.next()
        else:
            self._playbin.seek_simple(
                Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                position * 1000000)
            emit_signal(self, "seeked", position)

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        if rate < 0.0:
            rate = 0.0
        elif rate > 1.0:
            rate = 1.0
        self._playbin.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)

    @property
    def plugins(self):
        """
            Get plugins
            @return [PluginsPlayer]
        """
        return [self._plugins1, self._plugins2]

    @property
    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    @property
    def position(self):
        """
            Return bin playback position
            @HACK handle crossefade here, as we know we're going to be
            called every seconds
            @return position as int (ms)
        """
        return self.__get_bin_position(self._playbin)

    @property
    def remaining(self):
        """
            Return remaining duration
            @return duration as int (ms)
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1] / 1000000
        duration = self._current_track.duration
        return int(duration - position)

    @property
    def current_track(self):
        """
            Current track
        """
        return self._current_track

    @property
    def volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.CUBIC)

#######################
# PROTECTED           #
#######################

    def _load_track(self, track):
        """
            Load track
            @param track as Track
            @return False if track not loaded
        """
        self.__track_in_pipe = True
        Logger.debug("BinPlayer::_load_track(): %s" % track.uri)
        try:
            emit_signal(self, "loading-changed", False, self._current_track)
            self._current_track = track
            # If track_uri is different, preload has happened
            # See Player.set_next()
            track_uri = App().tracks.get_uri(track.id)
            if track.is_web and track.uri == track_uri:
                emit_signal(self, "loading-changed", True, track)
                self.__load_from_web(track)
                return False
            else:
                self._playbin.set_property("uri", track.uri)
        except Exception as e:  # Gstreamer error
            Logger.error("BinPlayer::_load_track(): %s" % e)
            return False
        return True

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Handle stream start: scrobbling, notify, ...
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self.__track_in_pipe = False
        emit_signal(self, "loading-changed", False, self._current_track)
        self._start_time = time()
        Logger.debug("Player::_on_stream_start(): %s" %
                     self._current_track.uri)
        emit_signal(self, "current-changed")
        for scrobbler in App().ws_director.scrobblers:
            scrobbler.playing_now(self._current_track)

    def _on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if self._current_track.storage_type != StorageType.EXTERNAL:
            return
        Logger.debug("Player::__on_bus_message_tag(): %s" %
                     self._current_track.uri)
        reader = TagReader()
        tags = message.parse_tag()
        title = reader.get_title(tags, "")
        if len(title) > 1 and self._current_track.title != title:
            self._current_track.set_name(title)
            emit_signal(self, "current-changed")

    def _on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            self.__codecs.append(message)

    def _on_bus_error(self, bus, message):
        """
            Try a codec install and update current track
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if self._current_track.is_web:
            emit_signal(self, "loading-changed", False, self._current_track)
        Logger.info("Player::_on_bus_error(): %s" % message.parse_error()[1])
        if self.current_track.id is not None and self.current_track.id >= 0:
            if self.__codecs.is_missing_codec(message):
                self.__codecs.install()
                App().scanner.stop()
                self.stop()
            else:
                (error, parsed) = message.parse_error()
                App().notify.send("Lollypop", parsed)
                self.stop()

    def _on_bus_eos(self, bus, message):
        """
            If we are current bus, try to restart playback
            Else stop playback
        """
        if self.__track_in_pipe:
            return
        if self._playbin.get_bus() == bus:
            if self._next_track.id is None:
                self.stop()
            else:
                self._load_track(self._next_track)
                self.next()

    def _on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst.Bin
        """
        try:
            Logger.debug("Player::__on_stream_about_to_finish(): %s" % playbin)
            # Don't do anything if crossfade on, track already scrobbled
            # See TransitionsPlayer
            if not self.crossfading:
                self._on_track_finished(App().player.current_track)
                if self._next_track.id is not None:
                    self._load_track(self._next_track)
        except Exception as e:
            Logger.error("BinPlayer::_on_stream_about_to_finish(): %s", e)

#######################
# PRIVATE             #
#######################

    def __load_from_web(self, track):
        """
            Load track from web
            @param track as Track
        """
        if get_network_available():
            self.__cancellable.cancel()
            self.__cancellable = Gio.Cancellable.new()
            from lollypop.helper_web import WebHelper
            helper = WebHelper(track, self.__cancellable)
            helper.connect("loaded", self.__on_web_helper_loaded, track,
                           self.__cancellable)
            helper.load()
        else:
            self.skip_album()

    def __get_bin_position(self, playbin):
        """
            Get position for playbin
            @param playbin as Gst.Bin
            @return position as int (ms)
        """
        return playbin.query_position(Gst.Format.TIME)[1] / 1000000

    def __update_current_duration(self, track):
        """
            Update current track duration
            @param track as Track
        """
        try:
            discoverer = Discoverer()
            duration = discoverer.get_info(track.uri).get_duration() / 1000000
            if duration != track.duration and duration > 0:
                App().tracks.set_duration(track.id, int(duration))
                track.reset("duration")
                emit_signal(self, "duration-changed", track.id)
        except Exception as e:
            Logger.error("BinPlayer::__update_current_duration(): %s" % e)

    def __on_volume_changed(self, playbin, sink):
        """
            Emit volume-changed signal
            @param playbin as Gst.Bin
            @param sink as Gst.Sink
        """
        App().settings.set_value("volume-rate", GLib.Variant("d", self.volume))
        emit_signal(self, "volume-changed")

    def __on_web_helper_loaded(self, helper, uri, track, cancellable):
        """
            Play track URI
            @param helper as WebHelper
            @param uri as str
            @param track as Track
            @param cancellable as Gio.Cancellable
        """
        if cancellable.is_cancelled():
            return
        if uri:
            track.set_uri(uri)
            self.load(track)
            App().task_helper.run(self.__update_current_duration, track)
        else:
            GLib.idle_add(App().notify.send, "Lollypop",
                          _("Can't find this track on YouTube"))
            self.next()
예제 #14
0
class BinPlayer(BasePlayer):
    """
        Gstreamer bin player
    """
    def __init__(self):
        """
            Init playbin
        """
        Gst.init(None)
        BasePlayer.__init__(self)
        self._codecs = Codecs()
        self._crossfading = False
        self._playbin = self._playbin1 = Gst.ElementFactory.make(
            'playbin', 'player')
        self._playbin2 = Gst.ElementFactory.make('playbin', 'player')
        self._plugins = self.plugins1 = PluginsPlayer(self._playbin1)
        self.plugins2 = PluginsPlayer(self._playbin2)
        self._volume_id = self._playbin.connect('notify::volume',
                                                self._on_volume_changed)
        for playbin in [self._playbin1, self._playbin2]:
            flags = playbin.get_property("flags")
            flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
            playbin.set_property('flags', flags)
            playbin.set_property('buffer-size', 5 << 20)
            playbin.set_property('buffer-duration', 10 * Gst.SECOND)
            playbin.connect('about-to-finish', self._on_stream_about_to_finish)
            bus = playbin.get_bus()
            bus.add_signal_watch()
            bus.connect('message::error', self._on_bus_error)
            bus.connect('message::eos', self._on_bus_eos)
            bus.connect('message::element', self._on_bus_element)
            bus.connect('message::stream-start', self._on_stream_start)
            bus.connect("message::tag", self._on_bus_message_tag)
        self._handled_error = None
        self._start_time = 0

    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def load(self, track):
        """
            Stop current track, load track id and play it
            @param track as Track
        """
        if self._crossfading and\
           self.current_track.id is not None and\
           self.is_playing() and\
           self.current_track.id != Type.RADIOS:
            duration = Lp().settings.get_value('mix-duration').get_int32()
            self._do_crossfade(duration, track, False)
        else:
            self._load(track)

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self.current_track.id is None:
            if self.next_track.id is not None:
                self.load(self.next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            self.emit("status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        self._playbin.set_state(Gst.State.PAUSED)
        self.emit("status-changed")

    def stop(self):
        """
            Change player state to STOPPED
        """
        self._playbin.set_state(Gst.State.NULL)
        self.emit("status-changed")

    def stop_all(self):
        """
            Stop all bins, lollypop should quit now
        """
        # Stop
        self._playbin1.set_state(Gst.State.NULL)
        self._playbin2.set_state(Gst.State.NULL)

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing():
            self.pause()
        else:
            self.play()

    def seek(self, position):
        """
            Seek current track to position
            @param position as seconds
        """
        # Seems gstreamer doesn't like seeking to end, sometimes
        # doesn't go to next track
        if position > self.current_track.duration - 1:
            self.next()
        else:
            self._playbin.seek_simple(
                Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                position * Gst.SECOND)
            self.emit("seeked", position)

    def get_position_in_track(self):
        """
            Return bin playback position
            @HACK handle crossefade here, as we know we're going to be
            called every seconds
            @return position as int
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1] / 1000
        if self._crossfading and self.current_track.duration > 0:
            duration = self.current_track.duration - position / 1000000
            if duration < Lp().settings.get_value('mix-duration').get_int32():
                self._do_crossfade(duration)
        return position * 60

    def get_volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.CUBIC)

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        self._playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self._playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.emit('volume-changed')

    def next(self):
        """
            Go next track
        """
        pass

#######################
# PRIVATE             #
#######################

    def _load(self, track, init_volume=True):
        """
            Stop current track, load track id and play it
            If was playing, do not use play as status doesn't changed
            @param track as Track
            @param init volume as bool
        """
        was_playing = self.is_playing()
        self._playbin.set_state(Gst.State.NULL)
        if self._load_track(track, init_volume):
            if was_playing:
                self._playbin.set_state(Gst.State.PLAYING)
            else:
                self.play()

    def _volume_up(self, playbin, plugins, duration):
        """
            Make volume going up smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are not the active playbin, stop all
        if self._playbin != playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_up = (1.0 - vol) / steps
            rate = vol + vol_up
            if rate < 1.0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self._volume_up, playbin, plugins,
                                 duration - 0.25)
            else:
                plugins.volume.props.volume = 1.0
        else:
            plugins.volume.props.volume = 1.0

    def _volume_down(self, playbin, plugins, duration):
        """
            Make volume going down smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are again the active playbin, stop all
        if self._playbin == playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_down = vol / steps
            rate = vol - vol_down
            if rate > 0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self._volume_down, playbin, plugins,
                                 duration - 0.25)
            else:
                plugins.volume.props.volume = 0.0
                playbin.set_state(Gst.State.NULL)
        else:
            plugins.volume.props.volume = 0.0
            playbin.set_state(Gst.State.NULL)

    def _do_crossfade(self, duration, track=None, next=True):
        """
            Crossfade tracks
            @param duration as int
            @param track as Track
            @param next as bool
        """
        # No cossfading if we need to stop
        if self._need_to_stop() and next:
            return
        if self._playbin.query_position(Gst.Format.TIME)[
                1] / 1000000000 > self.current_track.duration - 10:
            self._track_finished(self.current_track, self._start_time)
        GLib.idle_add(self._volume_down, self._playbin, self._plugins,
                      duration)
        if self._playbin == self._playbin2:
            self._playbin = self._playbin1
            self._plugins = self.plugins1
        else:
            self._playbin = self._playbin2
            self._plugins = self.plugins2

        if track is not None:
            self._load(track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self._volume_up, self._playbin, self._plugins,
                          duration)
        elif next and self.next_track.id is not None:
            self._load(self.next_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self._volume_up, self._playbin, self._plugins,
                          duration)
        elif self.prev_track.id is not None:
            self._load(self.prev_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self._volume_up, self._playbin, self._plugins,
                          duration)

    def _need_to_stop(self):
        """
            Return True if playback needs to stop
            @return bool
        """
        stop = False
        if self.context.next != NextContext.NONE:
            # Stop if needed
            if self.context.next == NextContext.STOP_TRACK:
                stop = True
            elif self.context.next == self._finished:
                stop = True
        return stop and self.is_playing()

    def _load_track(self, track, init_volume=True):
        """
            Load track
            @param track as Track
            @param init volume as bool
            @return False if track not loaded
        """
        if self._need_to_stop():
            return False
        if init_volume:
            self._plugins.volume.props.volume = 1.0
        debug("BinPlayer::_load_track(): %s" % track.uri)
        self.current_track = track
        try:
            self._playbin.set_property('uri', self.current_track.uri)
        except Exception as e:  # Gstreamer error
            print("BinPlayer::_load_track(): ", e)
            return False
        return True

    def _track_finished(self, finished, finished_start_time):
        """
            Do some actions for played track
            @param finished as Track
            @param finished_start_time as int
        """
        # Increment popularity
        if not Lp().scanner.is_locked():
            Lp().tracks.set_more_popular(finished.id)
            Lp().albums.set_more_popular(finished.album_id)
        # Scrobble on lastfm
        if Lp().lastfm is not None:
            if finished.album_artist_id == Type.COMPILATIONS:
                artist = finished.artist
            else:
                artist = finished.album_artist
            if time() - finished_start_time > 30:
                Lp().lastfm.scrobble(artist, finished.album_name,
                                     finished.title, int(finished_start_time),
                                     int(finished.duration))

    def _on_volume_changed(self, playbin, sink):
        """
            Update volume
            @param playbin as Gst.Bin
            @param sink as Gst.Sink
        """
        if playbin == self._playbin1:
            vol = self._playbin1.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self._playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        else:
            vol = self._playbin2.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self._playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        self.emit('volume-changed')

    def _on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if self.current_track.id >= 0 or\
           self.current_track.duration > 0.0:
            return
        debug("Player::_on_bus_message_tag(): %s" % self.current_track.uri)
        reader = ScannerTagReader()
        tags = message.parse_tag()

        title = reader.get_title(tags, '')
        if title != '':
            self.current_track.name = title
        if self.current_track.name == '':
            self.current_track.name = self.current_track.uri

        artist = reader.get_artists(tags)
        if artist != '':
            self.current_track.artist_names = artist

        # If title set, force artist
        if self.current_track.title != '' and self.current_track.artist == '':
            self.current_track.artist_names = self.current_track.album_artist

        if self.current_track.id == Type.EXTERNALS:
            (b, duration) = self._playbin.query_duration(Gst.Format.TIME)
            if b:
                self.current_track.duration = duration / 1000000000
            # We do not use tagreader as we need to check if value is None
            self.current_track.album_name = tags.get_string_index('album',
                                                                  0)[1]
            if self.current_track.album_name is None:
                self.current_track.album_name = ''
            self.current_track.artist_names = reader.get_artists(tags)
            self.current_track.set_album_artists(reader.get_album_artist(tags))
            if self.current_track.album_artist == '':
                self.current_track.set_album_artists(self.current_track.artist)
            self.current_track.genre_name = reader.get_genres(tags)
        self.emit('current-changed')

    def _on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            if self._codecs is not None:
                self._codecs.append(message)

    def _on_bus_error(self, bus, message):
        """
            Handle first bus error, ignore others
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        debug("Error playing: %s" % self.current_track.uri)
        Lp().window.pulse(False)
        if self._codecs.is_missing_codec(message):
            self._codecs.install()
            Lp().scanner.stop()
        elif Lp().notify is not None:
            Lp().notify.send(
                _("File doesn't exist: %s") % self.current_track.uri)
        self.stop()
        self.emit('current-changed')
        return True

    def _on_bus_eos(self, bus, message):
        """
            On end of stream, stop playback
            go next otherwise
        """
        debug("Player::_on_bus_eos(): %s" % self.current_track.uri)
        if self._playbin.get_bus() == bus:
            self.stop()
            self._finished = NextContext.NONE
            self.context.next = NextContext.NONE
            if self.next_track.id is not None:
                self._load_track(self.next_track)
            self.emit('current-changed')

    def _on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst bin
        """
        debug("Player::_on_stream_about_to_finish(): %s" % playbin)
        # Don't do anything if crossfade on
        if self._crossfading:
            return
        if self.current_track.id == Type.RADIOS:
            return
        finished = self.current_track
        finished_start_time = self._start_time
        if self.next_track.id is not None:
            self._load_track(self.next_track)
        self._track_finished(finished, finished_start_time)

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Emit "current-changed" to notify others components
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self._start_time = time()
        debug("Player::_on_stream_start(): %s" % self.current_track.uri)
        self.emit('current-changed')
        # Update now playing on lastfm
        if Lp().lastfm is not None and self.current_track.id >= 0:
            if self.current_track.album_artist_ids[0] == Type.COMPILATIONS:
                artist = self.current_track.artist
            else:
                artist = self.current_track.album_artist
                Lp().lastfm.now_playing(artist, self.current_track.album_name,
                                        self.current_track.title,
                                        int(self.current_track.duration))
        if not Lp().scanner.is_locked():
            Lp().tracks.set_listened_at(self.current_track.id, int(time()))
        self._handled_error = None
예제 #15
0
class BinPlayer(BasePlayer):
    """
        Gstreamer bin player
    """

    def __init__(self):
        """
            Init playbin
        """
        BasePlayer.__init__(self)
        self.__cancellable = Gio.Cancellable()
        self.__codecs = Codecs()
        self._playbin = self.__playbin1 = Gst.ElementFactory.make(
            "playbin", "player")
        self.__playbin2 = Gst.ElementFactory.make("playbin", "player")
        self._plugins = self._plugins1 = PluginsPlayer(self.__playbin1)
        self._plugins2 = PluginsPlayer(self.__playbin2)
        self._playbin.connect("notify::volume", self.__on_volume_changed)
        for playbin in [self.__playbin1, self.__playbin2]:
            flags = playbin.get_property("flags")
            flags &= ~GstPlayFlags.GST_PLAY_FLAG_VIDEO
            playbin.set_property("flags", flags)
            playbin.set_property("buffer-size", 5 << 20)
            playbin.set_property("buffer-duration", 10 * Gst.SECOND)
            playbin.connect("about-to-finish",
                            self._on_stream_about_to_finish)
            bus = playbin.get_bus()
            bus.add_signal_watch()
            bus.connect("message::error", self._on_bus_error)
            bus.connect("message::eos", self._on_bus_eos)
            bus.connect("message::element", self._on_bus_element)
            bus.connect("message::stream-start", self._on_stream_start)
            bus.connect("message::tag", self._on_bus_message_tag)
        self._start_time = 0

    def get_status(self):
        """
            Playback status
            @return Gstreamer state
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            state = pending
        elif (ok != Gst.StateChangeReturn.SUCCESS):
            state = Gst.State.NULL
        return state

    def load(self, track):
        """
            Stop current track, load track id and play it
            @param track as Track
        """
        if self._crossfading and\
           self._current_track.id is not None and\
           self.is_playing and\
           self._current_track.id != Type.RADIOS:
            duration = App().settings.get_value(
                "transition-duration").get_int32()
            self.__do_crossfade(duration, track)
        else:
            self.__load(track)

    def play(self):
        """
            Change player state to PLAYING
        """
        # No current playback, song in queue
        if self._current_track.id is None:
            if self._next_track.id is not None:
                self.load(self._next_track)
        else:
            self._playbin.set_state(Gst.State.PLAYING)
            self.emit("status-changed")

    def pause(self):
        """
            Change player state to PAUSED
        """
        if self._current_track.id == Type.RADIOS:
            self._playbin.set_state(Gst.State.NULL)
        else:
            self._playbin.set_state(Gst.State.PAUSED)
        self.emit("status-changed")

    def stop(self, force=False):
        """
            Change player state to STOPPED
            @param force as bool
        """
        self._current_track = Track()
        self._playbin.set_state(Gst.State.NULL)
        self.emit("status-changed")
        self.emit("current-changed")
        if force:
            self._prev_track = Track()
            self._next_track = Track()
            App().player.emit("prev-changed")
            App().player.emit("next-changed")
            self._albums = []
            self.reset_history()

    def stop_all(self):
        """
            Stop all bins, lollypop should quit now
        """
        # Stop
        self.__playbin1.set_state(Gst.State.NULL)
        self.__playbin2.set_state(Gst.State.NULL)

    def play_pause(self):
        """
            Set playing if paused
            Set paused if playing
        """
        if self.is_playing:
            self.pause()
        else:
            self.play()

    def reload_track(self):
        """
            Reload track at current position
        """
        if self.current_track.id is None:
            return
        position = self.position
        self.__load(self.current_track)
        GLib.timeout_add(100, self.seek, position / Gst.SECOND)

    def seek(self, position):
        """
            Seek current track to position
            @param position as seconds
        """
        if self._current_track.id is None:
            return
        # Seems gstreamer doesn't like seeking to end, sometimes
        # doesn"t go to next track
        if position >= self._current_track.duration:
            self.next()
        else:
            self._playbin.seek_simple(Gst.Format.TIME,
                                      Gst.SeekFlags.FLUSH |
                                      Gst.SeekFlags.KEY_UNIT,
                                      position * Gst.SECOND)
            self.emit("seeked", position)

    @property
    def plugins(self):
        """
            Get plugins
            @return [PluginsPlayer]
        """
        return [self._plugins1, self._plugins2]

    @property
    def is_playing(self):
        """
            True if player is playing
            @return bool
        """
        ok, state, pending = self._playbin.get_state(Gst.CLOCK_TIME_NONE)
        if ok == Gst.StateChangeReturn.ASYNC:
            return pending == Gst.State.PLAYING
        elif ok == Gst.StateChangeReturn.SUCCESS:
            return state == Gst.State.PLAYING
        else:
            return False

    @property
    def position(self):
        """
            Return bin playback position
            @HACK handle crossefade here, as we know we're going to be
            called every seconds
            @return position in Gst.SECOND
        """
        position = self._playbin.query_position(Gst.Format.TIME)[1]
        if self._crossfading and self._current_track.duration > 0:
            duration = self._current_track.duration - position / Gst.SECOND
            if duration < App().settings.get_value(
                    "transition-duration").get_int32():
                self.__do_crossfade(duration)
        return position

    @property
    def current_track(self):
        """
            Current track
        """
        return self._current_track

    @property
    def volume(self):
        """
            Return player volume rate
            @return rate as double
        """
        return self._playbin.get_volume(GstAudio.StreamVolumeFormat.CUBIC)

    def set_volume(self, rate):
        """
            Set player volume rate
            @param rate as double
        """
        if rate < 0.0:
            rate = 0.0
        elif rate > 1.0:
            rate = 1.0
        self.__playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)
        self.__playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, rate)

    def next(self):
        """
            Go next track
        """
        pass

#######################
# PROTECTED           #
#######################
    def _load_track(self, track, init_volume=True):
        """
            Load track
            @param track as Track
            @param init volume as bool
            @return False if track not loaded
        """
        if init_volume:
            self._plugins.volume.props.volume = 1.0
        Logger.debug("BinPlayer::_load_track(): %s" % track.uri)
        try:
            self.__cancellable.cancel()
            self.__cancellable = Gio.Cancellable()
            self._current_track = track
            # We check track is URI track, if yes, do a load from Web
            # Will not work if we add another music provider one day
            track_uri = App().tracks.get_uri(track.id)
            if track.is_web and track.uri == track_uri:
                self.emit("loading-changed", True)
                App().task_helper.run(self._load_from_web, track)
                return False
            else:
                self._playbin.set_property("uri", track.uri)
        except Exception as e:  # Gstreamer error
            Logger.error("BinPlayer::_load_track(): %s" % e)
            return False
        return True

    def _load_from_web(self, track, play=True):
        """
            Load track from web
            @param track as Track
            @param play as bool
        """
        def play_uri(uri):
            track.set_uri(uri)
            if play:
                self.load(track)
                App().task_helper.run(self.__update_current_duration,
                                      track, uri)

        from lollypop.helper_web import WebHelper
        helper = WebHelper()
        helper.set_uri(track, self.__cancellable)
        uri = helper.get_track_content(track)
        GLib.idle_add(play_uri, uri)

    def _scrobble(self, finished, finished_start_time):
        """
            Scrobble on lastfm
            @param finished as Track
            @param finished_start_time as int
        """
        played = time() - finished_start_time
        # Last.fm policy, force it for ListenBrainz too
        if finished.duration < 30:
            return
        # We can listen if the track has been played
        # for at least half its duration, or for 4 minutes
        if played >= finished.duration / 2 or played >= 240:
            for scrobbler in App().scrobblers:
                if scrobbler.available:
                    scrobbler.listen(finished, int(finished_start_time))

    def _on_stream_start(self, bus, message):
        """
            On stream start
            Handle stream start: scrobbling, notify, ...
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self.emit("loading-changed", False)
        self._start_time = time()
        Logger.debug("Player::_on_stream_start(): %s" %
                     self._current_track.uri)
        self.emit("current-changed")
        for scrobbler in App().scrobblers:
            if scrobbler.available:
                scrobbler.playing_now(self._current_track)
        App().tracks.set_listened_at(self._current_track.id, int(time()))

    def _on_bus_message_tag(self, bus, message):
        """
            Read tags from stream
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        # Some radio streams send message tag every seconds!
        changed = False
        if self._current_track.id >= 0 or self._current_track.duration > 0.0:
            return
        Logger.debug("Player::__on_bus_message_tag(): %s" %
                     self._current_track.uri)
        reader = TagReader()
        tags = message.parse_tag()
        title = reader.get_title(tags, "")
        if len(title) > 1 and self._current_track.name != title:
            self._current_track.name = title
            changed = True
            if self._current_track.name == "":
                self._current_track.name = self._current_track.uri
        artists = reader.get_artists(tags)
        if len(artists) > 1 and self._current_track.artists != artists:
            self._current_track.artists = artists.split(",")
            changed = True
            if not self._current_track.artists:
                self._current_track.artists = self._current_track.album_artists
        if changed:
            self.emit("current-changed")

    def _on_bus_element(self, bus, message):
        """
            Set elements for missings plugins
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        if GstPbutils.is_missing_plugin_message(message):
            self.__codecs.append(message)

    def _on_bus_error(self, bus, message):
        """
            Try a codec install and update current track
            @param bus as Gst.Bus
            @param message as Gst.Message
        """
        self.emit("loading-changed", False)
        Logger.info("Player::_on_bus_error(): %s" % message.parse_error()[1])
        if self.current_track.id is not None and self.current_track.id >= 0:
            if self.__codecs.is_missing_codec(message):
                self.__codecs.install()
                App().scanner.stop()
                self.stop()
            elif App().notify is not None:
                (error, parsed) = message.parse_error()
                App().notify.send(parsed)
                self.stop()

    def _on_bus_eos(self, bus, message):
        """
            Continue playback if possible and wanted
            go next otherwise
        """
        # Don't do anything if crossfade on, track already changed
        if self._crossfading:
            return
        if self._current_track.id == Type.RADIOS:
            return
        if self.is_playing and self._playbin.get_bus() == bus:
            if self._next_track.id is None:
                # We are in gstreamer thread
                GLib.idle_add(self.stop)
                # Reenable as it has been disabled by do_crossfading()
                self.update_crossfading()
            else:
                self._load_track(self._next_track)
                self.next()

    def _on_stream_about_to_finish(self, playbin):
        """
            When stream is about to finish, switch to next track without gap
            @param playbin as Gst.Bin
        """
        Logger.debug("Player::__on_stream_about_to_finish(): %s" % playbin)
        # Don't do anything if crossfade on, track already changed
        if self._crossfading:
            return
        if self._current_track.id == Type.RADIOS:
            return
        self._scrobble(self._current_track, self._start_time)
        # Increment popularity
        if self._current_track.id is not None and self._current_track.id >= 0:
            App().tracks.set_more_popular(self._current_track.id)
            # In party mode, linear popularity
            if self.is_party:
                pop_to_add = 1
            # In normal mode, based on tracks count
            else:
                # Some users report an issue where get_tracks_count() return 0
                # See issue #886
                # Don"t understand how this can happen!
                count = App().albums.get_tracks_count(
                    self._current_track.album_id)
                if count:
                    pop_to_add = int(App().albums.max_count / count)
                else:
                    pop_to_add = 1
            App().albums.set_more_popular(self._current_track.album_id,
                                          pop_to_add)
        if self._next_track.id is None:
            # We are in gstreamer thread
            GLib.idle_add(self.stop)
            # Reenable as it has been disabled by do_crossfading()
            self.update_crossfading()
        else:
            self._load_track(self._next_track)

#######################
# PRIVATE             #
#######################
    def __load(self, track, init_volume=True):
        """
            Stop current track, load track id and play it
            If was playing, do not use play as status doesn"t changed
            @param track as Track
            @param init volume as bool
        """
        was_playing = self.is_playing
        self._playbin.set_state(Gst.State.NULL)
        if self._load_track(track, init_volume):
            if was_playing:
                self._playbin.set_state(Gst.State.PLAYING)
            else:
                self.play()

    def __volume_up(self, playbin, plugins, duration):
        """
            Make volume going up smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are not the active playbin, stop all
        if self._playbin != playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_up = (1.0 - vol) / steps
            rate = vol + vol_up
            if rate < 1.0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self.__volume_up,
                                 playbin, plugins, duration - 0.25)
            else:
                plugins.volume.props.volume = 1.0
        else:
            plugins.volume.props.volume = 1.0

    def __volume_down(self, playbin, plugins, duration):
        """
            Make volume going down smoothly
            @param playbin as Gst.Bin
            @param plugins as PluginsPlayer
            @param duration as int
        """
        # We are again the active playbin, stop all
        if self._playbin == playbin:
            return
        if duration > 0:
            vol = plugins.volume.props.volume
            steps = duration / 0.25
            vol_down = vol / steps
            rate = vol - vol_down
            if rate > 0:
                plugins.volume.props.volume = rate
                GLib.timeout_add(250, self.__volume_down,
                                 playbin, plugins, duration - 0.25)
            else:
                plugins.volume.props.volume = 0.0
                playbin.set_state(Gst.State.NULL)
        else:
            plugins.volume.props.volume = 0.0
            playbin.set_state(Gst.State.NULL)

    def __do_crossfade(self, duration, track=None):
        """
            Crossfade tracks
            @param duration as int
            @param track as Track
        """
        if track is None:
            self._scrobble(self._current_track, self._start_time)
            # Increment popularity
            App().tracks.set_more_popular(self._current_track.id)
            # In party mode, linear popularity
            if self.is_party:
                pop_to_add = 1
            # In normal mode, based on tracks count
            else:
                count = App().albums.get_tracks_count(
                    self._current_track.album_id)
                if count:
                    pop_to_add = int(App().albums.max_count / count)
                else:
                    pop_to_add = 0
            if pop_to_add > 0:
                App().albums.set_more_popular(self._current_track.album_id,
                                              pop_to_add)

        GLib.idle_add(self.__volume_down, self._playbin,
                      self._plugins, duration)
        if self._playbin == self.__playbin2:
            self._playbin = self.__playbin1
            self._plugins = self._plugins1
        else:
            self._playbin = self.__playbin2
            self._plugins = self._plugins2

        if track is not None and track.id is not None:
            self.__load(track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin,
                          self._plugins, duration)
        elif self._next_track.id is not None:
            self.__load(self._next_track, False)
            self._plugins.volume.props.volume = 0
            GLib.idle_add(self.__volume_up, self._playbin,
                          self._plugins, duration)

    def __update_current_duration(self, track, uri):
        """
            Update current track duration
            @param track as Track
            @param uri as str
        """
        try:
            reader = TagReader()
            duration = reader.get_info(uri).get_duration() / 1000000000
            if duration != track.duration and duration > 0:
                App().tracks.set_duration(track.id, int(duration))
                track.reset("duration")
                GLib.idle_add(self.emit, "duration-changed", track.id)
        except Exception as e:
            Logger.error("BinPlayer::__update_current_duration(): %s" % e)

    def __on_volume_changed(self, playbin, sink):
        """
            Update volume
            @param playbin as Gst.Bin
            @param sink as Gst.Sink
        """
        if playbin == self.__playbin1:
            vol = self.__playbin1.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self.__playbin2.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        else:
            vol = self.__playbin2.get_volume(GstAudio.StreamVolumeFormat.CUBIC)
            self.__playbin1.set_volume(GstAudio.StreamVolumeFormat.CUBIC, vol)
        self.emit("volume-changed")