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