def __init__(self, librarian=None): GStreamerPluginHandler.__init__(self) BasePlayer.__init__(self) self._librarian = librarian self.version_info = "GStreamer: %s" % fver(Gst.version()) self._pipeline_desc = None self._volume = 1.0 self._paused = True self._mute = False self._in_gapless_transition = False self._active_error = False self.bin = None self._seeker = None self._int_vol_element = None self._ext_vol_element = None self._ext_mute_element = None self._use_eq = False self._eq_element = None self.__info_buffer = None self._lib_id = librarian.connect("changed", self.__songs_changed) self.__atf_id = None self.__bus_id = None self._runner = MainRunner()
def __init__(self, librarian=None): GStreamerPluginHandler.__init__(self) super(GStreamerPlayer, self).__init__() self.version_info = "GStreamer: %s" % fver(Gst.version()) self._librarian = librarian self._pipeline_desc = None self._lib_id = librarian.connect("changed", self.__songs_changed) self._active_seeks = [] self._runner = MainRunner()
class GStreamerPlayer(BasePlayer, GStreamerPluginHandler): def PlayerPreferences(self): return GstPlayerPreferences(self, const.DEBUG) def __init__(self, librarian=None): GStreamerPluginHandler.__init__(self) BasePlayer.__init__(self) self._librarian = librarian self.version_info = "GStreamer: %s" % fver(Gst.version()) self._pipeline_desc = None self._volume = 1.0 self._paused = True self._mute = False self._in_gapless_transition = False self._active_error = False self.bin = None self._seeker = None self._int_vol_element = None self._ext_vol_element = None self._ext_mute_element = None self._use_eq = False self._eq_element = None self.__info_buffer = None self._lib_id = librarian.connect("changed", self.__songs_changed) self.__atf_id = None self.__bus_id = None self._runner = MainRunner() def __songs_changed(self, librarian, songs): # replaygain values might have changed, recalc volume if self.song and self.song in songs: self._reset_replaygain() def _destroy(self): self._librarian.disconnect(self._lib_id) self._runner.abort() self.__destroy_pipeline() @property def name(self): name = "GStreamer" if self._pipeline_desc: name += " (%s)" % self._pipeline_desc return name @property def has_external_volume(self): ext = self._ext_vol_element if ext is None or not sink_has_external_state(ext): return False return True def _set_buffer_duration(self, duration): """Set the stream buffer duration in msecs""" config.set("player", "gst_buffer", float(duration) / 1000) if self.bin: value = duration * Gst.MSECOND self.bin.set_property('buffer-duration', value) def _print_pipeline(self): """Print debug information for the active pipeline to stdout (elements, formats, ...) """ if self.bin: # self.bin is just a wrapper, so get the real one for line in bin_debug([self.bin.bin]): print_(line) else: print_e("No active pipeline.") def __init_pipeline(self): """Creates a gstreamer pipeline. Returns True on success.""" if self.bin: return True # reset error state self.error = False pipeline = config.get("player", "gst_pipeline") try: pipeline, self._pipeline_desc = GStreamerSink(pipeline) except PlayerError as e: self._error(e) return False if self._use_eq and Gst.ElementFactory.find('equalizer-10bands'): # The equalizer only operates on 16-bit ints or floats, and # will only pass these types through even when inactive. # We push floats through to this point, then let the second # audioconvert handle pushing to whatever the rest of the # pipeline supports. As a bonus, this seems to automatically # select the highest-precision format supported by the # rest of the chain. filt = Gst.ElementFactory.make('capsfilter', None) filt.set_property('caps', Gst.Caps.from_string('audio/x-raw,format=F32LE')) eq = Gst.ElementFactory.make('equalizer-10bands', None) self._eq_element = eq self.update_eq_values() conv = Gst.ElementFactory.make('audioconvert', None) resample = Gst.ElementFactory.make('audioresample', None) pipeline = [filt, eq, conv, resample] + pipeline # playbin2 has started to control the volume through pulseaudio, # which means the volume property can change without us noticing. # Use our own volume element for now until this works with PA. self._int_vol_element = Gst.ElementFactory.make('volume', None) pipeline.insert(0, self._int_vol_element) # Get all plugin elements and append audio converters. # playbin already includes one at the end plugin_pipeline = [] for plugin in self._get_plugin_elements(): plugin_pipeline.append(plugin) plugin_pipeline.append( Gst.ElementFactory.make('audioconvert', None)) plugin_pipeline.append( Gst.ElementFactory.make('audioresample', None)) pipeline = plugin_pipeline + pipeline bufbin = Gst.Bin() for element in pipeline: assert element is not None, pipeline bufbin.add(element) if len(pipeline) > 1: if not link_many(pipeline): print_w("Linking the GStreamer pipeline failed") self._error( PlayerError(_("Could not create GStreamer pipeline"))) return False # see if the sink provides a volume property, if yes, use it sink_element = pipeline[-1] if isinstance(sink_element, Gst.Bin): sink_element = iter_to_list(sink_element.iterate_recurse)[-1] self._ext_vol_element = None if hasattr(sink_element.props, "volume"): self._ext_vol_element = sink_element # In case we use the sink volume directly we can increase buffering # without affecting the volume change delay too much and safe some # CPU time... (2x default for now). if hasattr(sink_element.props, "buffer_time"): sink_element.set_property("buffer-time", 400000) def ext_volume_notify(*args): # gets called from a thread GLib.idle_add(self.notify, "volume") self._ext_vol_element.connect("notify::volume", ext_volume_notify) self._ext_mute_element = None if hasattr(sink_element.props, "mute") and \ sink_element.get_factory().get_name() != "directsoundsink": # directsoundsink has a mute property but it doesn't work # https://bugzilla.gnome.org/show_bug.cgi?id=755106 self._ext_mute_element = sink_element def mute_notify(*args): # gets called from a thread GLib.idle_add(self.notify, "mute") self._ext_mute_element.connect("notify::mute", mute_notify) # Make the sink of the first element the sink of the bin gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink')) bufbin.add_pad(gpad) bin_ = Gst.ElementFactory.make('playbin', None) assert bin_ self.bin = BufferingWrapper(bin_, self) self._seeker = Seeker(self.bin, self) bus = bin_.get_bus() bus.add_signal_watch() self.__bus_id = bus.connect('message', self.__message, self._librarian) self.__atf_id = self.bin.connect('about-to-finish', self.__about_to_finish) # set buffer duration duration = config.getfloat("player", "gst_buffer") self._set_buffer_duration(int(duration * 1000)) # connect playbin to our pluing/volume/eq pipeline self.bin.set_property('audio-sink', bufbin) # by default playbin will render video -> suppress using fakesink fakesink = Gst.ElementFactory.make('fakesink', None) self.bin.set_property('video-sink', fakesink) # disable all video/text decoding in playbin GST_PLAY_FLAG_VIDEO = 1 << 0 GST_PLAY_FLAG_TEXT = 1 << 2 flags = self.bin.get_property("flags") flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT) self.bin.set_property("flags", flags) if not self.has_external_volume: # Restore volume/ReplayGain and mute state self.volume = self._volume self.mute = self._mute # ReplayGain information gets lost when destroying self._reset_replaygain() if self.song: self.bin.set_property('uri', self.song("~uri")) return True def __destroy_pipeline(self): self._remove_plugin_elements() if self.__bus_id: bus = self.bin.get_bus() bus.disconnect(self.__bus_id) bus.remove_signal_watch() self.__bus_id = None if self.__atf_id: self.bin.disconnect(self.__atf_id) self.__atf_id = None if self._seeker is not None: self._seeker.destroy() self._seeker = None self.notify("seekable") if self.bin: self.bin.set_state(Gst.State.NULL) self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT) # BufferingWrapper cleanup self.bin.destroy() self.bin = None self._in_gapless_transition = False self._ext_vol_element = None self._int_vol_element = None self._ext_mute_element = None self._eq_element = None def _rebuild_pipeline(self): """If a pipeline is active, rebuild it and restore vol, position etc""" if not self.bin: return paused = self.paused pos = self.get_position() self.__destroy_pipeline() self.paused = True self.__init_pipeline() self.paused = paused self.seek(pos) def __message(self, bus, message, librarian): if message.type == Gst.MessageType.EOS: print_d("Stream EOS") if not self._in_gapless_transition: self._source.next_ended() self._end(False) elif message.type == Gst.MessageType.TAG: self.__tag(message.parse_tag(), librarian) elif message.type == Gst.MessageType.ERROR: gerror, debug_info = message.parse_error() message = u"" if gerror: message = gerror.message.rstrip(".") details = None if debug_info: # strip the first line, not user friendly debug_info = "\n".join(debug_info.splitlines()[1:]) # can contain paths, so not sure if utf-8 in all cases details = debug_info self._error(PlayerError(message, details)) elif message.type == Gst.MessageType.STATE_CHANGED: # pulsesink doesn't notify a volume change on startup # and the volume is only valid in > paused states. if message.src is self._ext_vol_element: self.notify("volume") if message.src is self._ext_mute_element: self.notify("mute") elif message.type == Gst.MessageType.STREAM_START: if self._in_gapless_transition: print_d("Stream changed") self._end(False) elif message.type == Gst.MessageType.ELEMENT: message_name = message.get_structure().get_name() if message_name == "missing-plugin": self.__handle_missing_plugin(message) elif message.type == Gst.MessageType.CLOCK_LOST: print_d("Clock lost") self.bin.set_state(Gst.State.PAUSED) self.bin.set_state(Gst.State.PLAYING) elif message.type == Gst.MessageType.LATENCY: print_d("Recalculate latency") self.bin.recalculate_latency() elif message.type == Gst.MessageType.REQUEST_STATE: state = message.parse_request_state() print_d("State requested: %s" % Gst.Element.state_get_name(state)) self.bin.set_state(state) elif message.type == Gst.MessageType.DURATION_CHANGED: if self.song.fill_length: ok, p = self.bin.query_duration(Gst.Format.TIME) if ok: p /= float(Gst.SECOND) self.song["~#length"] = p librarian.changed([self.song]) def __handle_missing_plugin(self, message): get_installer_detail = \ GstPbutils.missing_plugin_message_get_installer_detail get_description = GstPbutils.missing_plugin_message_get_description details = get_installer_detail(message) if details is None: return self.stop() format_desc = get_description(message) title = _(u"No GStreamer element found to handle media format") error_details = _(u"Media format: %(format-description)s") % { "format-description": format_desc} def install_done_cb(plugins_return, *args): print_d("Gstreamer plugin install return: %r" % plugins_return) Gst.update_registry() context = GstPbutils.InstallPluginsContext.new() # new in 1.6 if hasattr(context, "set_desktop_id"): from gi.repository import Gtk context.set_desktop_id(app.id) # new in 1.6 if hasattr(context, "set_startup_notification_id"): current_time = Gtk.get_current_event_time() context.set_startup_notification_id("_TIME%d" % current_time) gdk_window = app.window.get_window() if gdk_window: try: xid = gdk_window.get_xid() except AttributeError: # non X11 pass else: context.set_xid(xid) res = GstPbutils.install_plugins_async( [details], context, install_done_cb, None) print_d("Gstreamer plugin install result: %r" % res) if res in (GstPbutils.InstallPluginsReturn.HELPER_MISSING, GstPbutils.InstallPluginsReturn.INTERNAL_FAILURE): self._error(PlayerError(title, error_details)) def __about_to_finish_sync(self): """Returns the next song uri to play or None""" print_d("About to finish (sync)") # Chained oggs falsely trigger a gapless transition. # At least for radio streams we can safely ignore it because # transitions don't occur there. # https://github.com/quodlibet/quodlibet/issues/1454 # https://bugzilla.gnome.org/show_bug.cgi?id=695474 if self.song.multisong: print_d("multisong: ignore about to finish") return # mod + gapless deadlocks # https://github.com/quodlibet/quodlibet/issues/2780 if isinstance(self.song, ModFile): return if config.getboolean("player", "gst_disable_gapless"): print_d("Gapless disabled") return # this can trigger twice, see issue 987 if self._in_gapless_transition: return self._in_gapless_transition = True print_d("Select next song in mainloop..") self._source.next_ended() print_d("..done.") song = self._source.current if song is not None: return song("~uri") def __about_to_finish(self, playbin): print_d("About to finish (async)") try: uri = self._runner.call(self.__about_to_finish_sync, priority=GLib.PRIORITY_HIGH, timeout=0.5) except MainRunnerTimeoutError as e: # Due to some locks being held during this signal we can get # into a deadlock when a seek or state change event happens # in the mainloop before our function gets scheduled. # In this case abort and do nothing, which results # in a non-gapless transition. print_d("About to finish (async): %s" % e) return except MainRunnerAbortedError as e: print_d("About to finish (async): %s" % e) return except MainRunnerError: util.print_exc() return if uri is not None: print_d("About to finish (async): setting uri") playbin.set_property('uri', uri) print_d("About to finish (async): done") def stop(self): super(GStreamerPlayer, self).stop() self.__destroy_pipeline() def do_get_property(self, property): if property.name == 'volume': if self._ext_vol_element is not None and \ sink_has_external_state(self._ext_vol_element) and \ sink_state_is_valid(self._ext_vol_element): # never read back the volume if we don't have to, e.g. # directsoundsink maps volume to an int which makes UI # sliders jump if we read the value back self._volume = self._ext_vol_element.get_property("volume") return self._volume elif property.name == "mute": if self._ext_mute_element is not None and \ sink_has_external_state(self._ext_mute_element) and \ sink_state_is_valid(self._ext_mute_element): self._mute = self._ext_mute_element.get_property("mute") return self._mute elif property.name == "seekable": if self._seeker is not None: return self._seeker.seekable return False else: raise AttributeError def _reset_replaygain(self): if not self.bin: return v = 1.0 if self._ext_vol_element is not None else self._volume v = self.calc_replaygain_volume(v) v = min(10.0, max(0.0, v)) self._int_vol_element.set_property('volume', v) def do_set_property(self, property, v): if property.name == 'volume': self._volume = v if self._ext_vol_element: v = min(10.0, max(0.0, v)) self._ext_vol_element.set_property("volume", v) else: v = self.calc_replaygain_volume(v) if self.bin: v = min(10.0, max(0.0, v)) self._int_vol_element.set_property('volume', v) elif property.name == 'mute': self._mute = v if self._ext_mute_element is not None: self._ext_mute_element.set_property("mute", v) else: if self.bin: self._int_vol_element.set_property("mute", v) else: raise AttributeError def get_position(self): """Return the current playback position in milliseconds, or 0 if no song is playing.""" if self._seeker: return self._seeker.get_position() return 0 @property def paused(self): return self._paused @paused.setter def paused(self, paused): if paused == self._paused: return self._paused = paused self.emit((paused and 'paused') or 'unpaused') # in case a signal handler changed the paused state, abort this if self._paused != paused: return if paused: if self.bin: if not self.song: # Something wants us to pause between songs, or when # we've got no song playing (probably StopAfterMenu). self.__destroy_pipeline() elif self.seekable: self.bin.set_state(Gst.State.PAUSED) else: q = Gst.Query.new_buffering(Gst.Format.DEFAULT) if self.bin.query(q): # destroy so that we rebuffer on resume i.e. we don't # want to continue unseekable streams from where we # paused but from where we unpaused. self.__destroy_pipeline() else: self.bin.set_state(Gst.State.PAUSED) else: if self.song and self.__init_pipeline(): self.bin.set_state(Gst.State.PLAYING) def _error(self, player_error): """Destroy the pipeline and set the error state. The passed PlayerError will be emitted through the 'error' signal. """ # prevent recursive errors if self._active_error: return self._active_error = True self.__destroy_pipeline() self.error = True self.paused = True print_w(player_error) self.emit('error', self.song, player_error) self._active_error = False def seek(self, pos): """Seek to a position in the song, in milliseconds.""" # Don't allow seeking during gapless. We can't go back to the old song. if not self.song or self._in_gapless_transition: return if self.__init_pipeline(): self._seeker.set_position(pos) def sync(self, timeout): if self.bin is not None: # XXX: This is flaky, try multiple times for i in range(5): self.bin.get_state(Gst.SECOND * timeout) # we have some logic in the main loop, so iterate there while GLib.MainContext.default().iteration(False): pass def _end(self, stopped, next_song=None): print_d("End song") song, info = self.song, self.info # set the new volume before the signals to avoid delays if self._in_gapless_transition: self.song = self._source.current self._reset_replaygain() # We need to set self.song to None before calling our signal # handlers. Otherwise, if they try to end the song they're given # (e.g. by removing it), then we get in an infinite loop. self.__info_buffer = self.song = self.info = None if song is not info: self.emit('song-ended', info, stopped) self.emit('song-ended', song, stopped) current = self._source.current if next_song is None else next_song # Then, set up the next song. self.song = self.info = current print_d("Next song") if self.song is not None: if not self._in_gapless_transition: # Due to extensive problems with playbin2, we destroy the # entire pipeline and recreate it each time we're not in # a gapless transition. self.__destroy_pipeline() self.__init_pipeline() if self.bin: if self.paused: self.bin.set_state(Gst.State.PAUSED) else: # something unpaused while no song was active if song is None: self.emit("unpaused") self.bin.set_state(Gst.State.PLAYING) else: self.__destroy_pipeline() self._in_gapless_transition = False if self._seeker is not None: # we could have a gapless transition to a non-seekable -> update self._seeker.reset() self.emit('song-started', self.song) if self.song is None: self.paused = True def __tag(self, tags, librarian): if self.song and self.song.multisong: self._fill_stream(tags, librarian) elif self.song and self.song.fill_metadata: pass def _fill_stream(self, tags, librarian): # get a new remote file new_info = self.__info_buffer if not new_info: new_info = type(self.song)(self.song["~filename"]) new_info.multisong = False new_info.streamsong = True # copy from the old songs # we should probably listen to the library for self.song changes new_info.update(self.song) new_info.update(self.info) changed = False info_changed = False tags = TagListWrapper(tags, merge=True) tags = parse_gstreamer_taglist(tags) for key, value in sanitize_tags(tags, stream=False).items(): if self.song.get(key) != value: changed = True self.song[key] = value for key, value in sanitize_tags(tags, stream=True).items(): if new_info.get(key) != value: info_changed = True new_info[key] = value if info_changed: # in case the title changed, make self.info a new instance # and emit ended/started for the the old/new one if self.info.get("title") != new_info.get("title"): if self.info is not self.song: self.emit('song-ended', self.info, False) self.info = new_info self.__info_buffer = None self.emit('song-started', self.info) else: # in case title didn't changed, update the values of the # old instance if there is one and tell the library. if self.info is not self.song: self.info.update(new_info) librarian.changed([self.info]) else: # So we don't loose all tags before the first title # save it for later self.__info_buffer = new_info if changed: librarian.changed([self.song]) @property def eq_bands(self): if Gst.ElementFactory.find('equalizer-10bands'): return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011] else: return [] def update_eq_values(self): need_eq = any(self._eq_values) if need_eq != self._use_eq: self._use_eq = need_eq self._rebuild_pipeline() if self._eq_element: for band, val in enumerate(self._eq_values): self._eq_element.set_property('band%d' % band, val) def can_play_uri(self, uri): if not Gst.uri_is_valid(uri): return False try: Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '') except GLib.GError: return False return True
class GStreamerPlayer(BasePlayer, GStreamerPluginHandler): def PlayerPreferences(self): return GstPlayerPreferences(self, const.DEBUG) def __init__(self, librarian=None): GStreamerPluginHandler.__init__(self) BasePlayer.__init__(self) self._librarian = librarian self.version_info = "GStreamer: %s" % fver(Gst.version()) self._pipeline_desc = None self._volume = 1.0 self._paused = True self._mute = False self._in_gapless_transition = False self._active_error = False self.bin = None self._seeker = None self._int_vol_element = None self._ext_vol_element = None self._ext_mute_element = None self._use_eq = False self._eq_element = None self.__info_buffer = None self._lib_id = librarian.connect("changed", self.__songs_changed) self.__atf_id = None self.__bus_id = None self._runner = MainRunner() def __songs_changed(self, librarian, songs): # replaygain values might have changed, recalc volume if self.song and self.song in songs: self._reset_replaygain() def _destroy(self): self._librarian.disconnect(self._lib_id) self._runner.abort() self.__destroy_pipeline() @property def name(self): name = "GStreamer" if self._pipeline_desc: name += " (%s)" % self._pipeline_desc return name @property def has_external_volume(self): ext = self._ext_vol_element if ext is None or not sink_has_external_state(ext): return False return True def _set_buffer_duration(self, duration): """Set the stream buffer duration in msecs""" config.set("player", "gst_buffer", float(duration) / 1000) if self.bin: value = duration * Gst.MSECOND self.bin.set_property('buffer-duration', value) def _print_pipeline(self): """Print debug information for the active pipeline to stdout (elements, formats, ...) """ if self.bin: # self.bin is just a wrapper, so get the real one for line in bin_debug([self.bin.bin]): print_(line) else: print_e("No active pipeline.") def __init_pipeline(self): """Creates a gstreamer pipeline. Returns True on success.""" if self.bin: return True # reset error state self.error = False pipeline = config.get("player", "gst_pipeline") try: pipeline, self._pipeline_desc = GStreamerSink(pipeline) except PlayerError as e: self._error(e) return False if self._use_eq and Gst.ElementFactory.find('equalizer-10bands'): # The equalizer only operates on 16-bit ints or floats, and # will only pass these types through even when inactive. # We push floats through to this point, then let the second # audioconvert handle pushing to whatever the rest of the # pipeline supports. As a bonus, this seems to automatically # select the highest-precision format supported by the # rest of the chain. filt = Gst.ElementFactory.make('capsfilter', None) filt.set_property('caps', Gst.Caps.from_string('audio/x-raw,format=F32LE')) eq = Gst.ElementFactory.make('equalizer-10bands', None) self._eq_element = eq self.update_eq_values() conv = Gst.ElementFactory.make('audioconvert', None) resample = Gst.ElementFactory.make('audioresample', None) pipeline = [filt, eq, conv, resample] + pipeline # playbin2 has started to control the volume through pulseaudio, # which means the volume property can change without us noticing. # Use our own volume element for now until this works with PA. self._int_vol_element = Gst.ElementFactory.make('volume', None) pipeline.insert(0, self._int_vol_element) # Get all plugin elements and append audio converters. # playbin already includes one at the end plugin_pipeline = [] for plugin in self._get_plugin_elements(): plugin_pipeline.append(plugin) plugin_pipeline.append( Gst.ElementFactory.make('audioconvert', None)) plugin_pipeline.append( Gst.ElementFactory.make('audioresample', None)) pipeline = plugin_pipeline + pipeline bufbin = Gst.Bin() for element in pipeline: assert element is not None, pipeline bufbin.add(element) if len(pipeline) > 1: if not link_many(pipeline): print_w("Linking the GStreamer pipeline failed") self._error( PlayerError(_("Could not create GStreamer pipeline"))) return False # see if the sink provides a volume property, if yes, use it sink_element = pipeline[-1] if isinstance(sink_element, Gst.Bin): sink_element = iter_to_list(sink_element.iterate_recurse)[-1] self._ext_vol_element = None if hasattr(sink_element.props, "volume"): self._ext_vol_element = sink_element # In case we use the sink volume directly we can increase buffering # without affecting the volume change delay too much and safe some # CPU time... (2x default for now). if hasattr(sink_element.props, "buffer_time"): sink_element.set_property("buffer-time", 400000) def ext_volume_notify(*args): # gets called from a thread GLib.idle_add(self.notify, "volume") self._ext_vol_element.connect("notify::volume", ext_volume_notify) self._ext_mute_element = None if hasattr(sink_element.props, "mute") and \ sink_element.get_factory().get_name() != "directsoundsink": # directsoundsink has a mute property but it doesn't work # https://bugzilla.gnome.org/show_bug.cgi?id=755106 self._ext_mute_element = sink_element def mute_notify(*args): # gets called from a thread GLib.idle_add(self.notify, "mute") self._ext_mute_element.connect("notify::mute", mute_notify) # Make the sink of the first element the sink of the bin gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink')) bufbin.add_pad(gpad) bin_ = Gst.ElementFactory.make('playbin', None) assert bin_ self.bin = BufferingWrapper(bin_, self) self._seeker = Seeker(self.bin, self) bus = bin_.get_bus() bus.add_signal_watch() self.__bus_id = bus.connect('message', self.__message, self._librarian) self.__atf_id = self.bin.connect('about-to-finish', self.__about_to_finish) # set buffer duration duration = config.getfloat("player", "gst_buffer") self._set_buffer_duration(int(duration * 1000)) # connect playbin to our pluing/volume/eq pipeline self.bin.set_property('audio-sink', bufbin) # by default playbin will render video -> suppress using fakesink fakesink = Gst.ElementFactory.make('fakesink', None) self.bin.set_property('video-sink', fakesink) # disable all video/text decoding in playbin GST_PLAY_FLAG_VIDEO = 1 << 0 GST_PLAY_FLAG_TEXT = 1 << 2 flags = self.bin.get_property("flags") flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT) self.bin.set_property("flags", flags) # find the (uri)decodebin after setup and use autoplug-sort # to sort elements like decoders def source_setup(*args): def autoplug_sort(decode, pad, caps, factories): def set_prio(x): i, f = x i = {"mad": -1, "mpg123audiodec": -2}.get(f.get_name(), i) return (i, f) return list( zip(*sorted(map(set_prio, enumerate(factories)))))[1] for e in iter_to_list(self.bin.iterate_recurse): try: e.connect("autoplug-sort", autoplug_sort) except TypeError: pass else: break self.bin.connect("source-setup", source_setup) if not self.has_external_volume: # Restore volume/ReplayGain and mute state self.volume = self._volume self.mute = self._mute # ReplayGain information gets lost when destroying self._reset_replaygain() if self.song: self.bin.set_property('uri', self.song("~uri")) return True def __destroy_pipeline(self): self._remove_plugin_elements() if self.__bus_id: bus = self.bin.get_bus() bus.disconnect(self.__bus_id) bus.remove_signal_watch() self.__bus_id = None if self.__atf_id: self.bin.disconnect(self.__atf_id) self.__atf_id = None if self._seeker is not None: self._seeker.destroy() self._seeker = None self.notify("seekable") if self.bin: self.bin.set_state(Gst.State.NULL) self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT) # BufferingWrapper cleanup self.bin.destroy() self.bin = None self._in_gapless_transition = False self._ext_vol_element = None self._int_vol_element = None self._ext_mute_element = None self._eq_element = None def _rebuild_pipeline(self): """If a pipeline is active, rebuild it and restore vol, position etc""" if not self.bin: return paused = self.paused pos = self.get_position() self.__destroy_pipeline() self.paused = True self.__init_pipeline() self.paused = paused self.seek(pos) def __message(self, bus, message, librarian): if message.type == Gst.MessageType.EOS: print_d("Stream EOS") if not self._in_gapless_transition: self._source.next_ended() self._end(False) elif message.type == Gst.MessageType.TAG: self.__tag(message.parse_tag(), librarian) elif message.type == Gst.MessageType.ERROR: gerror, debug_info = message.parse_error() message = u"" if gerror: message = util.gdecode(gerror.message).rstrip(".") details = None if debug_info: # strip the first line, not user friendly debug_info = "\n".join(debug_info.splitlines()[1:]) # can contain paths, so not sure if utf-8 in all cases details = util.gdecode(debug_info) self._error(PlayerError(message, details)) elif message.type == Gst.MessageType.STATE_CHANGED: # pulsesink doesn't notify a volume change on startup # and the volume is only valid in > paused states. if message.src is self._ext_vol_element: self.notify("volume") if message.src is self._ext_mute_element: self.notify("mute") elif message.type == Gst.MessageType.STREAM_START: if self._in_gapless_transition: print_d("Stream changed") self._end(False) elif message.type == Gst.MessageType.ELEMENT: message_name = message.get_structure().get_name() if message_name == "missing-plugin": self.__handle_missing_plugin(message) elif message.type == Gst.MessageType.CLOCK_LOST: print_d("Clock lost") self.bin.set_state(Gst.State.PAUSED) self.bin.set_state(Gst.State.PLAYING) elif message.type == Gst.MessageType.LATENCY: print_d("Recalculate latency") self.bin.recalculate_latency() elif message.type == Gst.MessageType.REQUEST_STATE: state = message.parse_request_state() print_d("State requested: %s" % Gst.Element.state_get_name(state)) self.bin.set_state(state) elif message.type == Gst.MessageType.DURATION_CHANGED: if self.song.fill_length: ok, p = self.bin.query_duration(Gst.Format.TIME) if ok: p /= float(Gst.SECOND) self.song["~#length"] = p librarian.changed([self.song]) def __handle_missing_plugin(self, message): get_installer_detail = \ GstPbutils.missing_plugin_message_get_installer_detail get_description = GstPbutils.missing_plugin_message_get_description details = get_installer_detail(message) if details is None: return self.stop() format_desc = get_description(message) title = _(u"No GStreamer element found to handle media format") error_details = _(u"Media format: %(format-description)s") % { "format-description": util.gdecode(format_desc) } def install_done_cb(plugins_return, *args): print_d("Gstreamer plugin install return: %r" % plugins_return) Gst.update_registry() context = GstPbutils.InstallPluginsContext.new() # new in 1.6 if hasattr(context, "set_desktop_id"): from gi.repository import Gtk context.set_desktop_id(app.id) # new in 1.6 if hasattr(context, "set_startup_notification_id"): current_time = Gtk.get_current_event_time() context.set_startup_notification_id("_TIME%d" % current_time) gdk_window = app.window.get_window() if gdk_window: try: xid = gdk_window.get_xid() except AttributeError: # non X11 pass else: context.set_xid(xid) res = GstPbutils.install_plugins_async([details], context, install_done_cb, None) print_d("Gstreamer plugin install result: %r" % res) if res in (GstPbutils.InstallPluginsReturn.HELPER_MISSING, GstPbutils.InstallPluginsReturn.INTERNAL_FAILURE): self._error(PlayerError(title, error_details)) def __about_to_finish_sync(self): """Returns the next song uri to play or None""" print_d("About to finish (sync)") # Chained oggs falsely trigger a gapless transition. # At least for radio streams we can safely ignore it because # transitions don't occur there. # https://github.com/quodlibet/quodlibet/issues/1454 # https://bugzilla.gnome.org/show_bug.cgi?id=695474 if self.song.multisong: print_d("multisong: ignore about to finish") return if config.getboolean("player", "gst_disable_gapless"): print_d("Gapless disabled") return # this can trigger twice, see issue 987 if self._in_gapless_transition: return self._in_gapless_transition = True print_d("Select next song in mainloop..") self._source.next_ended() print_d("..done.") song = self._source.current if song is not None: return song("~uri") def __about_to_finish(self, playbin): print_d("About to finish (async)") try: uri = self._runner.call(self.__about_to_finish_sync, priority=GLib.PRIORITY_HIGH, timeout=0.5) except MainRunnerTimeoutError as e: # Due to some locks being held during this signal we can get # into a deadlock when a seek or state change event happens # in the mainloop before our function gets scheduled. # In this case abort and do nothing, which results # in a non-gapless transition. print_d("About to finish (async): %s" % e) return except MainRunnerAbortedError as e: print_d("About to finish (async): %s" % e) return except MainRunnerError: util.print_exc() return if uri is not None: print_d("About to finish (async): setting uri") playbin.set_property('uri', uri) print_d("About to finish (async): done") def stop(self): super(GStreamerPlayer, self).stop() self.__destroy_pipeline() def do_get_property(self, property): if property.name == 'volume': if self._ext_vol_element is not None and \ sink_has_external_state(self._ext_vol_element) and \ sink_state_is_valid(self._ext_vol_element): # never read back the volume if we don't have to, e.g. # directsoundsink maps volume to an int which makes UI # sliders jump if we read the value back self._volume = self._ext_vol_element.get_property("volume") return self._volume elif property.name == "mute": if self._ext_mute_element is not None and \ sink_has_external_state(self._ext_mute_element) and \ sink_state_is_valid(self._ext_mute_element): self._mute = self._ext_mute_element.get_property("mute") return self._mute elif property.name == "seekable": if self._seeker is not None: return self._seeker.seekable return False else: raise AttributeError def _reset_replaygain(self): if not self.bin: return v = 1.0 if self._ext_vol_element is not None else self._volume v = self.calc_replaygain_volume(v) v = min(10.0, max(0.0, v)) self._int_vol_element.set_property('volume', v) def do_set_property(self, property, v): if property.name == 'volume': self._volume = v if self._ext_vol_element: v = min(10.0, max(0.0, v)) self._ext_vol_element.set_property("volume", v) else: v = self.calc_replaygain_volume(v) if self.bin: v = min(10.0, max(0.0, v)) self._int_vol_element.set_property('volume', v) elif property.name == 'mute': self._mute = v if self._ext_mute_element is not None: self._ext_mute_element.set_property("mute", v) else: if self.bin: self._int_vol_element.set_property("mute", v) else: raise AttributeError def get_position(self): """Return the current playback position in milliseconds, or 0 if no song is playing.""" if self._seeker: return self._seeker.get_position() return 0 @property def paused(self): return self._paused @paused.setter def paused(self, paused): if paused == self._paused: return self._paused = paused self.emit((paused and 'paused') or 'unpaused') # in case a signal handler changed the paused state, abort this if self._paused != paused: return if paused: if self.bin: if not self.song: # Something wants us to pause between songs, or when # we've got no song playing (probably StopAfterMenu). self.__destroy_pipeline() elif self.seekable: self.bin.set_state(Gst.State.PAUSED) else: q = Gst.Query.new_buffering(Gst.Format.DEFAULT) if self.bin.query(q): # destroy so that we rebuffer on resume i.e. we don't # want to continue unseekable streams from where we # paused but from where we unpaused. self.__destroy_pipeline() else: self.bin.set_state(Gst.State.PAUSED) else: if self.song and self.__init_pipeline(): self.bin.set_state(Gst.State.PLAYING) def _error(self, player_error): """Destroy the pipeline and set the error state. The passed PlayerError will be emitted through the 'error' signal. """ # prevent recursive errors if self._active_error: return self._active_error = True self.__destroy_pipeline() self.error = True self.paused = True print_w(player_error) self.emit('error', self.song, player_error) self._active_error = False def seek(self, pos): """Seek to a position in the song, in milliseconds.""" # Don't allow seeking during gapless. We can't go back to the old song. if not self.song or self._in_gapless_transition: return if self.__init_pipeline(): self._seeker.set_position(pos) def sync(self, timeout): if self.bin is not None: # XXX: This is flaky, try multiple times for i in range(5): self.bin.get_state(Gst.SECOND * timeout) # we have some logic in the main loop, so iterate there while GLib.MainContext.default().iteration(False): pass def _end(self, stopped, next_song=None): print_d("End song") song, info = self.song, self.info # set the new volume before the signals to avoid delays if self._in_gapless_transition: self.song = self._source.current self._reset_replaygain() # We need to set self.song to None before calling our signal # handlers. Otherwise, if they try to end the song they're given # (e.g. by removing it), then we get in an infinite loop. self.__info_buffer = self.song = self.info = None if song is not info: self.emit('song-ended', info, stopped) self.emit('song-ended', song, stopped) current = self._source.current if next_song is None else next_song # Then, set up the next song. self.song = self.info = current print_d("Next song") if self.song is not None: if not self._in_gapless_transition: # Due to extensive problems with playbin2, we destroy the # entire pipeline and recreate it each time we're not in # a gapless transition. self.__destroy_pipeline() self.__init_pipeline() if self.bin: if self.paused: self.bin.set_state(Gst.State.PAUSED) else: # something unpaused while no song was active if song is None: self.emit("unpaused") self.bin.set_state(Gst.State.PLAYING) else: self.__destroy_pipeline() self.paused = True self._in_gapless_transition = False if self._seeker is not None: # we could have a gapless transition to a non-seekable -> update self._seeker.reset() self.emit('song-started', self.song) def __tag(self, tags, librarian): if self.song and self.song.multisong: self._fill_stream(tags, librarian) elif self.song and self.song.fill_metadata: pass def _fill_stream(self, tags, librarian): # get a new remote file new_info = self.__info_buffer if not new_info: new_info = type(self.song)(self.song["~filename"]) new_info.multisong = False new_info.streamsong = True # copy from the old songs # we should probably listen to the library for self.song changes new_info.update(self.song) new_info.update(self.info) changed = False info_changed = False tags = TagListWrapper(tags, merge=True) tags = parse_gstreamer_taglist(tags) for key, value in iteritems(sanitize_tags(tags, stream=False)): if self.song.get(key) != value: changed = True self.song[key] = value for key, value in iteritems(sanitize_tags(tags, stream=True)): if new_info.get(key) != value: info_changed = True new_info[key] = value if info_changed: # in case the title changed, make self.info a new instance # and emit ended/started for the the old/new one if self.info.get("title") != new_info.get("title"): if self.info is not self.song: self.emit('song-ended', self.info, False) self.info = new_info self.__info_buffer = None self.emit('song-started', self.info) else: # in case title didn't changed, update the values of the # old instance if there is one and tell the library. if self.info is not self.song: self.info.update(new_info) librarian.changed([self.info]) else: # So we don't loose all tags before the first title # save it for later self.__info_buffer = new_info if changed: librarian.changed([self.song]) @property def eq_bands(self): if Gst.ElementFactory.find('equalizer-10bands'): return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011] else: return [] def update_eq_values(self): need_eq = any(self._eq_values) if need_eq != self._use_eq: self._use_eq = need_eq self._rebuild_pipeline() if self._eq_element: for band, val in enumerate(self._eq_values): self._eq_element.set_property('band%d' % band, val) def can_play_uri(self, uri): if not Gst.uri_is_valid(uri): return False try: Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '') except GLib.GError: return False return True
class GStreamerPlayer(BasePlayer, GStreamerPluginHandler): __gproperties__ = BasePlayer._gproperties_ __gsignals__ = BasePlayer._gsignals_ _paused = True _in_gapless_transition = False _last_position = 0 bin = None _vol_element = None _use_eq = False _eq_element = None __atf_id = None __bus_id = None __source_setup_id = None __info_buffer = None def PlayerPreferences(self): return GstPlayerPreferences(self, const.DEBUG) def __init__(self, librarian=None): GStreamerPluginHandler.__init__(self) super(GStreamerPlayer, self).__init__() self.version_info = "GStreamer: %s" % fver(Gst.version()) self._librarian = librarian self._pipeline_desc = None self._lib_id = librarian.connect("changed", self.__songs_changed) self._active_seeks = [] self._active_error = False self._runner = MainRunner() def __songs_changed(self, librarian, songs): # replaygain values might have changed, recalc volume if self.song and self.song in songs: self.volume = self.volume def _destroy(self): self._librarian.disconnect(self._lib_id) self._runner.abort() self.__destroy_pipeline() @property def name(self): name = "GStreamer" if self._pipeline_desc: name += " (%s)" % self._pipeline_desc return name def _set_buffer_duration(self, duration): """Set the stream buffer duration in msecs""" config.set("player", "gst_buffer", float(duration) / 1000) if self.bin: value = duration * Gst.MSECOND self.bin.set_property('buffer-duration', value) def _print_pipeline(self): """Print debug information for the active pipeline to stdout (elements, formats, ...) """ if self.bin: # self.bin is just a wrapper, so get the real one for line in bin_debug([self.bin.bin]): print_(line) else: print_e("No active pipeline.") def __init_pipeline(self): """Creates a gstreamer pipeline. Returns True on success.""" if self.bin: return True # reset error state self.error = False pipeline = config.get("player", "gst_pipeline") try: pipeline, self._pipeline_desc = GStreamerSink(pipeline) except PlayerError as e: self._error(e) return False if self._use_eq and Gst.ElementFactory.find('equalizer-10bands'): # The equalizer only operates on 16-bit ints or floats, and # will only pass these types through even when inactive. # We push floats through to this point, then let the second # audioconvert handle pushing to whatever the rest of the # pipeline supports. As a bonus, this seems to automatically # select the highest-precision format supported by the # rest of the chain. filt = Gst.ElementFactory.make('capsfilter', None) filt.set_property('caps', Gst.Caps.from_string('audio/x-raw,format=F32LE')) eq = Gst.ElementFactory.make('equalizer-10bands', None) self._eq_element = eq self.update_eq_values() conv = Gst.ElementFactory.make('audioconvert', None) resample = Gst.ElementFactory.make('audioresample', None) pipeline = [filt, eq, conv, resample] + pipeline # playbin2 has started to control the volume through pulseaudio, # which means the volume property can change without us noticing. # Use our own volume element for now until this works with PA. self._vol_element = Gst.ElementFactory.make('volume', None) pipeline.insert(0, self._vol_element) # Get all plugin elements and append audio converters. # playbin already includes one at the end plugin_pipeline = [] for plugin in self._get_plugin_elements(): plugin_pipeline.append(plugin) plugin_pipeline.append( Gst.ElementFactory.make('audioconvert', None)) plugin_pipeline.append( Gst.ElementFactory.make('audioresample', None)) pipeline = plugin_pipeline + pipeline bufbin = Gst.Bin() for element in pipeline: assert element is not None, pipeline bufbin.add(element) PIPELINE_ERROR = PlayerError(_("Could not create GStreamer pipeline")) if len(pipeline) > 1: if not link_many(pipeline): print_w("Linking the GStreamer pipeline failed") self._error(PIPELINE_ERROR) return False # Test to ensure output pipeline can preroll bufbin.set_state(Gst.State.READY) result, state, pending = bufbin.get_state(timeout=STATE_CHANGE_TIMEOUT) if result == Gst.StateChangeReturn.FAILURE: bufbin.set_state(Gst.State.NULL) print_w("Prerolling the GStreamer pipeline failed") self._error(PIPELINE_ERROR) return False # Make the sink of the first element the sink of the bin gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink')) bufbin.add_pad(gpad) self.bin = Gst.ElementFactory.make('playbin', None) assert self.bin bus = self.bin.get_bus() bus.add_signal_watch() self.__bus_id = bus.connect('message', self.__message, self._librarian) self.bin = BufferingWrapper(self.bin, self) self.__atf_id = self.bin.connect('about-to-finish', self.__about_to_finish) # set buffer duration duration = config.getfloat("player", "gst_buffer") self._set_buffer_duration(int(duration * 1000)) # connect playbin to our pluing/volume/eq pipeline self.bin.set_property('audio-sink', bufbin) # by default playbin will render video -> suppress using fakesink fakesink = Gst.ElementFactory.make('fakesink', None) self.bin.set_property('video-sink', fakesink) # disable all video/text decoding in playbin GST_PLAY_FLAG_VIDEO = 1 << 0 GST_PLAY_FLAG_TEXT = 1 << 2 flags = self.bin.get_property("flags") flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT) self.bin.set_property("flags", flags) # find the (uri)decodebin after setup and use autoplug-sort # to sort elements like decoders def source_setup(*args): def autoplug_sort(decode, pad, caps, factories): def set_prio(x): i, f = x i = {"mad": -1, "mpg123audiodec": -2}.get(f.get_name(), i) return (i, f) return zip(*sorted(map(set_prio, enumerate(factories))))[1] for e in iter_to_list(self.bin.iterate_recurse): try: e.connect("autoplug-sort", autoplug_sort) except TypeError: pass else: break self.__source_setup_id = self.bin.connect("source-setup", source_setup) # ReplayGain information gets lost when destroying self.volume = self.volume if self.song: self.bin.set_property('uri', self.song("~uri")) return True def __destroy_pipeline(self): self._remove_plugin_elements() if self.__bus_id: bus = self.bin.get_bus() bus.disconnect(self.__bus_id) bus.remove_signal_watch() self.__bus_id = None if self.__atf_id: self.bin.disconnect(self.__atf_id) self.__atf_id = None if self.__source_setup_id: self.bin.disconnect(self.__source_setup_id) self.__source_setup_id = None if self.bin: self.bin.set_state(Gst.State.NULL) self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT) self.bin.set_property('audio-sink', None) self.bin.set_property('video-sink', None) # BufferingWrapper cleanup self.bin.destroy() self.bin = None self._in_gapless_transition = False self._last_position = 0 self._active_seeks = [] self._vol_element = None self._eq_element = None def _rebuild_pipeline(self): """If a pipeline is active, rebuild it and restore vol, position etc""" if not self.bin: return paused = self.paused pos = self.get_position() self.__destroy_pipeline() self.paused = True self.__init_pipeline() self.paused = paused self.seek(pos) def __message(self, bus, message, librarian): if message.type == Gst.MessageType.EOS: print_d("Stream EOS") if not self._in_gapless_transition: self._source.next_ended() self._end(False) elif message.type == Gst.MessageType.TAG: self.__tag(message.parse_tag(), librarian) elif message.type == Gst.MessageType.ERROR: gerror, debug_info = message.parse_error() message = u"" if gerror: message = gerror.message.decode("utf-8").rstrip(".") details = None if debug_info: # strip the first line, not user friendly debug_info = "\n".join(debug_info.splitlines()[1:]) # can contain paths, so not sure if utf-8 in all cases details = debug_info.decode("utf-8", errors="replace") self._error(PlayerError(message, details)) elif message.type == Gst.MessageType.STREAM_START: if self._in_gapless_transition: print_d("Stream changed") self._end(False) elif message.type == Gst.MessageType.ASYNC_DONE: if self._active_seeks: song, pos = self._active_seeks.pop(0) if song is self.song: self.emit("seek", song, pos) elif message.type == Gst.MessageType.ELEMENT: message_name = message.get_structure().get_name() if message_name == "missing-plugin": self.__handle_missing_plugin(message) elif message.type == Gst.MessageType.CLOCK_LOST: print_d("Clock lost") self.bin.set_state(Gst.State.PAUSED) self.bin.set_state(Gst.State.PLAYING) elif message.type == Gst.MessageType.LATENCY: print_d("Recalculate latency") self.bin.recalculate_latency() elif message.type == Gst.MessageType.REQUEST_STATE: state = message.parse_request_state() print_d("State requested: %s" % Gst.Element.state_get_name(state)) self.bin.set_state(state) elif message.type == Gst.MessageType.DURATION_CHANGED: if self.song.fill_length: ok, p = self.bin.query_duration(Gst.Format.TIME) if ok: p /= float(Gst.SECOND) self.song["~#length"] = p librarian.changed([self.song]) def __handle_missing_plugin(self, message): get_installer_detail = \ GstPbutils.missing_plugin_message_get_installer_detail get_description = GstPbutils.missing_plugin_message_get_description details = get_installer_detail(message) if details is None: return self.stop() format_desc = get_description(message) title = _(u"No GStreamer element found to handle media format") error_details = _(u"Media format: %(format-description)s") % { "format-description": format_desc.decode("utf-8") } def install_done_cb(plugins_return, *args): print_d("Gstreamer plugin install return: %r" % plugins_return) Gst.update_registry() context = GstPbutils.InstallPluginsContext.new() res = GstPbutils.install_plugins_async([details], context, install_done_cb, None) print_d("Gstreamer plugin install result: %r" % res) if res in (GstPbutils.InstallPluginsReturn.HELPER_MISSING, GstPbutils.InstallPluginsReturn.INTERNAL_FAILURE): self._error(PlayerError(title, error_details)) def __about_to_finish_sync(self): """Returns a tuple (ok, next_song). ok is True if the next song should be set. """ print_d("About to finish (sync)") # Chained oggs falsely trigger a gapless transition. # At least for radio streams we can safely ignore it because # transitions don't occur there. # https://github.com/quodlibet/quodlibet/issues/1454 # https://bugzilla.gnome.org/show_bug.cgi?id=695474 if self.song.multisong: print_d("multisong: ignore about to finish") return (False, None) if config.getboolean("player", "gst_disable_gapless"): print_d("Gapless disabled") return (False, None) # this can trigger twice, see issue 987 if self._in_gapless_transition: return (False, None) self._in_gapless_transition = True print_d("Select next song in mainloop..") self._source.next_ended() print_d("..done.") return (True, self._source.current) def __about_to_finish(self, playbin): print_d("About to finish (async)") try: ok, song = self._runner.call(self.__about_to_finish_sync, priority=GLib.PRIORITY_HIGH, timeout=0.5) except MainRunnerTimeoutError as e: # Due to some locks being held during this signal we can get # into a deadlock when a seek or state change event happens # in the mainloop before our function gets scheduled. # In this case abort and do nothing, which results # in a non-gapless transition. print_d("About to finish (async): %s" % e) return except MainRunnerAbortedError: print_d("About to finish (async): %s" % e) return except MainRunnerError: util.print_exc() return if ok: print_d("About to finish (async): setting uri") uri = song("~uri") if song is not None else None playbin.set_property('uri', uri) print_d("About to finish (async): done") def stop(self): super(GStreamerPlayer, self).stop() self.__destroy_pipeline() def do_set_property(self, property, v): if property.name == 'volume': self._volume = v if self.song and config.getboolean("player", "replaygain"): profiles = filter(None, self.replaygain_profiles)[0] fb_gain = config.getfloat("player", "fallback_gain") pa_gain = config.getfloat("player", "pre_amp_gain") scale = self.song.replay_gain(profiles, pa_gain, fb_gain) v = min(10.0, max(0.0, v * scale)) # volume supports 0..10 if self.bin: self._vol_element.set_property('volume', v) else: raise AttributeError def get_position(self): """Return the current playback position in milliseconds, or 0 if no song is playing.""" p = self._last_position if self.song and self.bin: # While we are actively seeking return the last wanted position. # query_position() returns 0 while in this state if self._active_seeks: return self._active_seeks[-1][1] ok, p = self.bin.query_position(Gst.Format.TIME) if ok: p //= Gst.MSECOND # During stream seeking querying the position fails. # Better return the last valid one instead of 0. self._last_position = p return p @property def paused(self): return self._paused @paused.setter def paused(self, paused): if paused == self._paused: return self._paused = paused self.emit((paused and 'paused') or 'unpaused') # in case a signal handler changed the paused state, abort this if self._paused != paused: return if paused: if self.bin: if not self.song: # Something wants us to pause between songs, or when # we've got no song playing (probably StopAfterMenu). self.__destroy_pipeline() elif self.song.is_file: # fast path self.bin.set_state(Gst.State.PAUSED) else: # seekable streams (seem to) have a duration >= 0 ok, d = self.bin.query_duration(Gst.Format.TIME) if not ok: d = -1 if d >= 0: self.bin.set_state(Gst.State.PAUSED) else: # destroy so that we rebuffer on resume self.__destroy_pipeline() else: if self.song and self.__init_pipeline(): self.bin.set_state(Gst.State.PLAYING) def _error(self, player_error): """Destroy the pipeline and set the error state. The passed PlayerError will be emitted through the 'error' signal. """ # prevent recursive errors if self._active_error: return self._active_error = True self.__destroy_pipeline() self.error = True self.paused = True print_w(unicode(player_error)) self.emit('error', self.song, player_error) self._active_error = False def seek(self, pos): """Seek to a position in the song, in milliseconds.""" # Don't allow seeking during gapless. We can't go back to the old song. if not self.song or self._in_gapless_transition: return if self.__init_pipeline(): # ensure any pending state changes have completed and we have # at least paused state, so we can seek state = self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)[1] if state < Gst.State.PAUSED: self.bin.set_state(Gst.State.PAUSED) self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT) pos = max(0, int(pos)) gst_time = pos * Gst.MSECOND event = Gst.Event.new_seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH, Gst.SeekType.SET, gst_time, Gst.SeekType.NONE, 0) if self.bin.send_event(event): # to get a good estimate for when get_position fails self._last_position = pos # For cases where we get the position directly after # a seek and the seek is not done, GStreamer returns # a valid 0 position. To prevent this we try to emit seek only # after it is done. Every flushing seek will trigger # an async_done message on the bus, so we queue the seek # event here and emit in the bus message callback. self._active_seeks.append((self.song, pos)) def _end(self, stopped, next_song=None): print_d("End song") song, info = self.song, self.info # set the new volume before the signals to avoid delays if self._in_gapless_transition: self.song = self._source.current self.volume = self.volume # We need to set self.song to None before calling our signal # handlers. Otherwise, if they try to end the song they're given # (e.g. by removing it), then we get in an infinite loop. self.__info_buffer = self.song = self.info = None if song is not info: self.emit('song-ended', info, stopped) self.emit('song-ended', song, stopped) current = self._source.current if next_song is None else next_song # Then, set up the next song. self.song = self.info = current self.emit('song-started', self.song) print_d("Next song") if self.song is not None: if not self._in_gapless_transition: self.volume = self.volume # Due to extensive problems with playbin2, we destroy the # entire pipeline and recreate it each time we're not in # a gapless transition. self.__destroy_pipeline() self.__init_pipeline() if self.bin: if self.paused: self.bin.set_state(Gst.State.PAUSED) else: # something unpaused while no song was active if song is None: self.emit("unpaused") self.bin.set_state(Gst.State.PLAYING) else: self.__destroy_pipeline() self.paused = True self._in_gapless_transition = False def __tag(self, tags, librarian): if self.song and self.song.multisong: self._fill_stream(tags, librarian) elif self.song and self.song.fill_metadata: pass def _fill_stream(self, tags, librarian): # get a new remote file new_info = self.__info_buffer if not new_info: new_info = type(self.song)(self.song["~filename"]) new_info.multisong = False new_info.streamsong = True # copy from the old songs # we should probably listen to the library for self.song changes new_info.update(self.song) new_info.update(self.info) changed = False info_changed = False tags = TagListWrapper(tags, merge=True) tags = parse_gstreamer_taglist(tags) for key, value in sanitize_tags(tags, stream=False).iteritems(): if self.song.get(key) != value: changed = True self.song[key] = value for key, value in sanitize_tags(tags, stream=True).iteritems(): if new_info.get(key) != value: info_changed = True new_info[key] = value if info_changed: # in case the title changed, make self.info a new instance # and emit ended/started for the the old/new one if self.info.get("title") != new_info.get("title"): if self.info is not self.song: self.emit('song-ended', self.info, False) self.info = new_info self.__info_buffer = None self.emit('song-started', self.info) else: # in case title didn't changed, update the values of the # old instance if there is one and tell the library. if self.info is not self.song: self.info.update(new_info) librarian.changed([self.info]) else: # So we don't loose all tags before the first title # save it for later self.__info_buffer = new_info if changed: librarian.changed([self.song]) @property def eq_bands(self): if Gst.ElementFactory.find('equalizer-10bands'): return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011] else: return [] def update_eq_values(self): need_eq = any(self._eq_values) if need_eq != self._use_eq: self._use_eq = need_eq self._rebuild_pipeline() if self._eq_element: for band, val in enumerate(self._eq_values): self._eq_element.set_property('band%d' % band, val) def can_play_uri(self, uri): if not Gst.uri_is_valid(uri): return False try: Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '') except GLib.GError: return False return True
class GStreamerPlayer(BasePlayer, GStreamerPluginHandler): __gproperties__ = BasePlayer._gproperties_ __gsignals__ = BasePlayer._gsignals_ _paused = True _in_gapless_transition = False _last_position = 0 bin = None _vol_element = None _use_eq = False _eq_element = None __atf_id = None __bus_id = None __source_setup_id = None __info_buffer = None def PlayerPreferences(self): return GstPlayerPreferences(self, const.DEBUG) def __init__(self, librarian=None): GStreamerPluginHandler.__init__(self) super(GStreamerPlayer, self).__init__() self.version_info = "GStreamer: %s" % fver(Gst.version()) self._librarian = librarian self._pipeline_desc = None self._lib_id = librarian.connect("changed", self.__songs_changed) self._active_seeks = [] self._active_error = False self._runner = MainRunner() def __songs_changed(self, librarian, songs): # replaygain values might have changed, recalc volume if self.song and self.song in songs: self.volume = self.volume def _destroy(self): self._librarian.disconnect(self._lib_id) self._runner.abort() self.__destroy_pipeline() @property def name(self): name = "GStreamer" if self._pipeline_desc: name += " (%s)" % self._pipeline_desc return name def _set_buffer_duration(self, duration): """Set the stream buffer duration in msecs""" config.set("player", "gst_buffer", float(duration) / 1000) if self.bin: value = duration * Gst.MSECOND self.bin.set_property('buffer-duration', value) def _print_pipeline(self): """Print debug information for the active pipeline to stdout (elements, formats, ...) """ if self.bin: # self.bin is just a wrapper, so get the real one for line in bin_debug([self.bin.bin]): print_(line) else: print_e("No active pipeline.") def __init_pipeline(self): """Creates a gstreamer pipeline. Returns True on success.""" if self.bin: return True # reset error state self.error = False pipeline = config.get("player", "gst_pipeline") try: pipeline, self._pipeline_desc = GStreamerSink(pipeline) except PlayerError as e: self._error(e) return False if self._use_eq and Gst.ElementFactory.find('equalizer-10bands'): # The equalizer only operates on 16-bit ints or floats, and # will only pass these types through even when inactive. # We push floats through to this point, then let the second # audioconvert handle pushing to whatever the rest of the # pipeline supports. As a bonus, this seems to automatically # select the highest-precision format supported by the # rest of the chain. filt = Gst.ElementFactory.make('capsfilter', None) filt.set_property('caps', Gst.Caps.from_string('audio/x-raw,format=F32LE')) eq = Gst.ElementFactory.make('equalizer-10bands', None) self._eq_element = eq self.update_eq_values() conv = Gst.ElementFactory.make('audioconvert', None) resample = Gst.ElementFactory.make('audioresample', None) pipeline = [filt, eq, conv, resample] + pipeline # playbin2 has started to control the volume through pulseaudio, # which means the volume property can change without us noticing. # Use our own volume element for now until this works with PA. self._vol_element = Gst.ElementFactory.make('volume', None) pipeline.insert(0, self._vol_element) # Get all plugin elements and append audio converters. # playbin already includes one at the end plugin_pipeline = [] for plugin in self._get_plugin_elements(): plugin_pipeline.append(plugin) plugin_pipeline.append( Gst.ElementFactory.make('audioconvert', None)) plugin_pipeline.append( Gst.ElementFactory.make('audioresample', None)) pipeline = plugin_pipeline + pipeline bufbin = Gst.Bin() for element in pipeline: assert element is not None, pipeline bufbin.add(element) PIPELINE_ERROR = PlayerError(_("Could not create GStreamer pipeline")) if len(pipeline) > 1: if not link_many(pipeline): print_w("Linking the GStreamer pipeline failed") self._error(PIPELINE_ERROR) return False # Test to ensure output pipeline can preroll bufbin.set_state(Gst.State.READY) result, state, pending = bufbin.get_state(timeout=STATE_CHANGE_TIMEOUT) if result == Gst.StateChangeReturn.FAILURE: bufbin.set_state(Gst.State.NULL) print_w("Prerolling the GStreamer pipeline failed") self._error(PIPELINE_ERROR) return False # Make the sink of the first element the sink of the bin gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink')) bufbin.add_pad(gpad) self.bin = Gst.ElementFactory.make('playbin', None) assert self.bin bus = self.bin.get_bus() bus.add_signal_watch() self.__bus_id = bus.connect('message', self.__message, self._librarian) self.bin = BufferingWrapper(self.bin, self) self.__atf_id = self.bin.connect('about-to-finish', self.__about_to_finish) # set buffer duration duration = config.getfloat("player", "gst_buffer") self._set_buffer_duration(int(duration * 1000)) # connect playbin to our pluing/volume/eq pipeline self.bin.set_property('audio-sink', bufbin) # by default playbin will render video -> suppress using fakesink fakesink = Gst.ElementFactory.make('fakesink', None) self.bin.set_property('video-sink', fakesink) # disable all video/text decoding in playbin GST_PLAY_FLAG_VIDEO = 1 << 0 GST_PLAY_FLAG_TEXT = 1 << 2 flags = self.bin.get_property("flags") flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT) self.bin.set_property("flags", flags) # find the (uri)decodebin after setup and use autoplug-sort # to sort elements like decoders def source_setup(*args): def autoplug_sort(decode, pad, caps, factories): def set_prio(x): i, f = x i = { "mad": -1, "mpg123audiodec": -2 }.get(f.get_name(), i) return (i, f) return zip(*sorted(map(set_prio, enumerate(factories))))[1] for e in iter_to_list(self.bin.iterate_recurse): try: e.connect("autoplug-sort", autoplug_sort) except TypeError: pass else: break self.__source_setup_id = self.bin.connect("source-setup", source_setup) # ReplayGain information gets lost when destroying self.volume = self.volume if self.song: self.bin.set_property('uri', self.song("~uri")) return True def __destroy_pipeline(self): self._remove_plugin_elements() if self.__bus_id: bus = self.bin.get_bus() bus.disconnect(self.__bus_id) bus.remove_signal_watch() self.__bus_id = None if self.__atf_id: self.bin.disconnect(self.__atf_id) self.__atf_id = None if self.__source_setup_id: self.bin.disconnect(self.__source_setup_id) self.__source_setup_id = None if self.bin: self.bin.set_state(Gst.State.NULL) self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT) self.bin.set_property('audio-sink', None) self.bin.set_property('video-sink', None) # BufferingWrapper cleanup self.bin.destroy() self.bin = None self._in_gapless_transition = False self._last_position = 0 self._active_seeks = [] self._vol_element = None self._eq_element = None def _rebuild_pipeline(self): """If a pipeline is active, rebuild it and restore vol, position etc""" if not self.bin: return paused = self.paused pos = self.get_position() self.__destroy_pipeline() self.paused = True self.__init_pipeline() self.paused = paused self.seek(pos) def __message(self, bus, message, librarian): if message.type == Gst.MessageType.EOS: print_d("Stream EOS") if not self._in_gapless_transition: self._source.next_ended() self._end(False) elif message.type == Gst.MessageType.TAG: self.__tag(message.parse_tag(), librarian) elif message.type == Gst.MessageType.ERROR: gerror, debug_info = message.parse_error() message = u"" if gerror: message = gerror.message.decode("utf-8").rstrip(".") details = None if debug_info: # strip the first line, not user friendly debug_info = "\n".join(debug_info.splitlines()[1:]) # can contain paths, so not sure if utf-8 in all cases details = debug_info.decode("utf-8", errors="replace") self._error(PlayerError(message, details)) elif message.type == Gst.MessageType.STREAM_START: if self._in_gapless_transition: print_d("Stream changed") self._end(False) elif message.type == Gst.MessageType.ASYNC_DONE: if self._active_seeks: song, pos = self._active_seeks.pop(0) if song is self.song: self.emit("seek", song, pos) elif message.type == Gst.MessageType.ELEMENT: message_name = message.get_structure().get_name() if message_name == "missing-plugin": self.__handle_missing_plugin(message) elif message.type == Gst.MessageType.CLOCK_LOST: print_d("Clock lost") self.bin.set_state(Gst.State.PAUSED) self.bin.set_state(Gst.State.PLAYING) elif message.type == Gst.MessageType.LATENCY: print_d("Recalculate latency") self.bin.recalculate_latency() elif message.type == Gst.MessageType.REQUEST_STATE: state = message.parse_request_state() print_d("State requested: %s" % Gst.Element.state_get_name(state)) self.bin.set_state(state) elif message.type == Gst.MessageType.DURATION_CHANGED: if self.song.fill_length: ok, p = self.bin.query_duration(Gst.Format.TIME) if ok: p /= float(Gst.SECOND) self.song["~#length"] = p librarian.changed([self.song]) def __handle_missing_plugin(self, message): get_installer_detail = \ GstPbutils.missing_plugin_message_get_installer_detail get_description = GstPbutils.missing_plugin_message_get_description details = get_installer_detail(message) if details is None: return self.stop() format_desc = get_description(message) title = _(u"No GStreamer element found to handle media format") error_details = _(u"Media format: %(format-description)s") % { "format-description": format_desc.decode("utf-8")} def install_done_cb(plugins_return, *args): print_d("Gstreamer plugin install return: %r" % plugins_return) Gst.update_registry() context = GstPbutils.InstallPluginsContext.new() res = GstPbutils.install_plugins_async( [details], context, install_done_cb, None) print_d("Gstreamer plugin install result: %r" % res) if res in (GstPbutils.InstallPluginsReturn.HELPER_MISSING, GstPbutils.InstallPluginsReturn.INTERNAL_FAILURE): self._error(PlayerError(title, error_details)) def __about_to_finish_sync(self): """Returns a tuple (ok, next_song). ok is True if the next song should be set. """ print_d("About to finish (sync)") # Chained oggs falsely trigger a gapless transition. # At least for radio streams we can safely ignore it because # transitions don't occur there. # https://github.com/quodlibet/quodlibet/issues/1454 # https://bugzilla.gnome.org/show_bug.cgi?id=695474 if self.song.multisong: print_d("multisong: ignore about to finish") return (False, None) if config.getboolean("player", "gst_disable_gapless"): print_d("Gapless disabled") return (False, None) # this can trigger twice, see issue 987 if self._in_gapless_transition: return (False, None) self._in_gapless_transition = True print_d("Select next song in mainloop..") self._source.next_ended() print_d("..done.") return (True, self._source.current) def __about_to_finish(self, playbin): print_d("About to finish (async)") try: ok, song = self._runner.call(self.__about_to_finish_sync, priority=GLib.PRIORITY_HIGH, timeout=0.5) except MainRunnerTimeoutError as e: # Due to some locks being held during this signal we can get # into a deadlock when a seek or state change event happens # in the mainloop before our function gets scheduled. # In this case abort and do nothing, which results # in a non-gapless transition. print_d("About to finish (async): %s" % e) return except MainRunnerAbortedError: print_d("About to finish (async): %s" % e) return except MainRunnerError: util.print_exc() return if ok: print_d("About to finish (async): setting uri") uri = song("~uri") if song is not None else None playbin.set_property('uri', uri) print_d("About to finish (async): done") def stop(self): super(GStreamerPlayer, self).stop() self.__destroy_pipeline() def do_set_property(self, property, v): if property.name == 'volume': self._volume = v if self.song and config.getboolean("player", "replaygain"): profiles = filter(None, self.replaygain_profiles)[0] fb_gain = config.getfloat("player", "fallback_gain") pa_gain = config.getfloat("player", "pre_amp_gain") scale = self.song.replay_gain(profiles, pa_gain, fb_gain) v = min(10.0, max(0.0, v * scale)) # volume supports 0..10 if self.bin: self._vol_element.set_property('volume', v) else: raise AttributeError def get_position(self): """Return the current playback position in milliseconds, or 0 if no song is playing.""" p = self._last_position if self.song and self.bin: # While we are actively seeking return the last wanted position. # query_position() returns 0 while in this state if self._active_seeks: return self._active_seeks[-1][1] ok, p = self.bin.query_position(Gst.Format.TIME) if ok: p //= Gst.MSECOND # During stream seeking querying the position fails. # Better return the last valid one instead of 0. self._last_position = p return p @property def paused(self): return self._paused @paused.setter def paused(self, paused): if paused == self._paused: return self._paused = paused self.emit((paused and 'paused') or 'unpaused') # in case a signal handler changed the paused state, abort this if self._paused != paused: return if paused: if self.bin: if not self.song: # Something wants us to pause between songs, or when # we've got no song playing (probably StopAfterMenu). self.__destroy_pipeline() elif self.song.is_file: # fast path self.bin.set_state(Gst.State.PAUSED) else: # seekable streams (seem to) have a duration >= 0 ok, d = self.bin.query_duration(Gst.Format.TIME) if not ok: d = -1 if d >= 0: self.bin.set_state(Gst.State.PAUSED) else: # destroy so that we rebuffer on resume self.__destroy_pipeline() else: if self.song and self.__init_pipeline(): self.bin.set_state(Gst.State.PLAYING) def _error(self, player_error): """Destroy the pipeline and set the error state. The passed PlayerError will be emitted through the 'error' signal. """ # prevent recursive errors if self._active_error: return self._active_error = True self.__destroy_pipeline() self.error = True self.paused = True print_w(unicode(player_error)) self.emit('error', self.song, player_error) self._active_error = False def seek(self, pos): """Seek to a position in the song, in milliseconds.""" # Don't allow seeking during gapless. We can't go back to the old song. if not self.song or self._in_gapless_transition: return if self.__init_pipeline(): # ensure any pending state changes have completed and we have # at least paused state, so we can seek state = self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)[1] if state < Gst.State.PAUSED: self.bin.set_state(Gst.State.PAUSED) self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT) pos = max(0, int(pos)) gst_time = pos * Gst.MSECOND event = Gst.Event.new_seek( 1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH, Gst.SeekType.SET, gst_time, Gst.SeekType.NONE, 0) if self.bin.send_event(event): # to get a good estimate for when get_position fails self._last_position = pos # For cases where we get the position directly after # a seek and the seek is not done, GStreamer returns # a valid 0 position. To prevent this we try to emit seek only # after it is done. Every flushing seek will trigger # an async_done message on the bus, so we queue the seek # event here and emit in the bus message callback. self._active_seeks.append((self.song, pos)) def _end(self, stopped, next_song=None): print_d("End song") song, info = self.song, self.info # set the new volume before the signals to avoid delays if self._in_gapless_transition: self.song = self._source.current self.volume = self.volume # We need to set self.song to None before calling our signal # handlers. Otherwise, if they try to end the song they're given # (e.g. by removing it), then we get in an infinite loop. self.__info_buffer = self.song = self.info = None if song is not info: self.emit('song-ended', info, stopped) self.emit('song-ended', song, stopped) current = self._source.current if next_song is None else next_song # Then, set up the next song. self.song = self.info = current self.emit('song-started', self.song) print_d("Next song") if self.song is not None: if not self._in_gapless_transition: self.volume = self.volume # Due to extensive problems with playbin2, we destroy the # entire pipeline and recreate it each time we're not in # a gapless transition. self.__destroy_pipeline() self.__init_pipeline() if self.bin: if self.paused: self.bin.set_state(Gst.State.PAUSED) else: # something unpaused while no song was active if song is None: self.emit("unpaused") self.bin.set_state(Gst.State.PLAYING) else: self.__destroy_pipeline() self.paused = True self._in_gapless_transition = False def __tag(self, tags, librarian): if self.song and self.song.multisong: self._fill_stream(tags, librarian) elif self.song and self.song.fill_metadata: pass def _fill_stream(self, tags, librarian): # get a new remote file new_info = self.__info_buffer if not new_info: new_info = type(self.song)(self.song["~filename"]) new_info.multisong = False new_info.streamsong = True # copy from the old songs # we should probably listen to the library for self.song changes new_info.update(self.song) new_info.update(self.info) changed = False info_changed = False tags = TagListWrapper(tags, merge=True) tags = parse_gstreamer_taglist(tags) for key, value in sanitize_tags(tags, stream=False).iteritems(): if self.song.get(key) != value: changed = True self.song[key] = value for key, value in sanitize_tags(tags, stream=True).iteritems(): if new_info.get(key) != value: info_changed = True new_info[key] = value if info_changed: # in case the title changed, make self.info a new instance # and emit ended/started for the the old/new one if self.info.get("title") != new_info.get("title"): if self.info is not self.song: self.emit('song-ended', self.info, False) self.info = new_info self.__info_buffer = None self.emit('song-started', self.info) else: # in case title didn't changed, update the values of the # old instance if there is one and tell the library. if self.info is not self.song: self.info.update(new_info) librarian.changed([self.info]) else: # So we don't loose all tags before the first title # save it for later self.__info_buffer = new_info if changed: librarian.changed([self.song]) @property def eq_bands(self): if Gst.ElementFactory.find('equalizer-10bands'): return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011] else: return [] def update_eq_values(self): need_eq = any(self._eq_values) if need_eq != self._use_eq: self._use_eq = need_eq self._rebuild_pipeline() if self._eq_element: for band, val in enumerate(self._eq_values): self._eq_element.set_property('band%d' % band, val) def can_play_uri(self, uri): if not Gst.uri_is_valid(uri): return False try: Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '') except GLib.GError: return False return True