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 GStreamerSink(pipeline_desc): """Returns a list of unlinked gstreamer elements ending with an audio sink and a textual description of the pipeline. `pipeline_desc` can be gst-launch syntax for multiple elements with or without an audiosink. In case of an error, raises PlayerError """ pipe = None if pipeline_desc: try: pipe = [Gst.parse_launch(e) for e in pipeline_desc.split('!')] except GLib.GError as e: message = e.message.decode("utf-8") raise PlayerError(_("Invalid GStreamer output pipeline"), message) if pipe: # In case the last element is linkable with a fakesink # it is not an audiosink, so we append the default one fake = Gst.ElementFactory.make('fakesink', None) if link_many([pipe[-1], fake]): unlink_many([pipe[-1], fake]) default_elm, default_desc = find_audio_sink() pipe += [default_elm] pipeline_desc += " ! " + default_desc else: elm, pipeline_desc = find_audio_sink() pipe = [elm] return pipe, pipeline_desc
def find_audio_sink(): """Get the best audio sink available. Returns (element, description) or raises PlayerError. """ if is_windows(): sinks = [ "directsoundsink", "autoaudiosink", ] elif is_linux() and pulse_is_running(): sinks = [ "pulsesink", ] else: sinks = [ "autoaudiosink", # plugins-good "pulsesink", # plugins-good "alsasink", # plugins-base ] for name in sinks: element = Gst.ElementFactory.make(name, None) if element is not None: return (element, name) else: details = " (%s)" % ", ".join(sinks) if sinks else "" raise PlayerError(_("No GStreamer audio sink found") + details)
def find_audio_sink() -> Tuple[Gst.Element, str]: """Get the best audio sink available. Returns (element, description) or raises PlayerError. """ def sink_options(): # People with Jack running probably want it more than any other options if config.getboolean("player", "gst_use_jack") and jack_is_running(): print_d("Using JACK output via Gstreamer") return [AudioSinks.JACK] elif is_windows(): return [AudioSinks.DIRECTSOUND] elif is_linux() and pulse_is_running(): return [AudioSinks.PULSE] else: return [ AudioSinks.AUTO, AudioSinks.PULSE, AudioSinks.ALSA, ] options = sink_options() for sink in options: element = Gst.ElementFactory.make(sink.value, "player") if (sink == AudioSinks.JACK and not config.getboolean("player", "gst_jack_auto_connect")): # Disable the auto-connection to outputs (e.g. maybe there's scripting) element.set_property("connect", "none") if element is not None: return element, sink.value else: details = ', '.join(s.value for s in options) if options else "[]" raise PlayerError( _("No GStreamer audio sink found. Tried: %s") % details)
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.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") 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 init(librarian): # Enable error messages by default if Gst.debug_get_default_threshold() == Gst.DebugLevel.NONE: Gst.debug_set_default_threshold(Gst.DebugLevel.ERROR) if Gst.Element.make_from_uri(Gst.URIType.SRC, "file:///fake/path/for/gst", ""): return GStreamerPlayer(librarian) else: raise PlayerError( _("Unable to open input files"), _("GStreamer has no element to handle reading files. Check " "your GStreamer installation settings."))
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 _event_listener(self, user_data, event): event = event.contents if event.type == XINE_EVENT_UI_PLAYBACK_FINISHED: GLib.idle_add(self._playback_finished, priority=GLib.PRIORITY_HIGH) elif event.type == XINE_EVENT_UI_SET_TITLE: GLib.idle_add(self._update_metadata, priority=GLib.PRIORITY_HIGH) elif event.type == XINE_EVENT_UI_MESSAGE: from ctypes import POINTER, cast, string_at, addressof msg = cast(event.data, POINTER(xine_ui_message_data_t)).contents if msg.type != XINE_MSG_NO_ERROR: if msg.explanation: message = string_at(addressof(msg) + msg.explanation) else: message = "xine error %s" % msg.type message = message.decode("utf-8", errors="replace") GLib.idle_add(self._error, PlayerError(message)) return True
def _new_stream(self, driver): self._audio_port = self._handle.open_audio_driver(driver, None) if not self._audio_port: raise PlayerError( _("Unable to create audio output"), _("The audio device %r was not found. Check your Xine " "settings in ~/.quodlibet/config.") % driver) self._stream = self._handle.stream_new(self._audio_port, None) xine_set_param(self._stream, XINE_PARAM_IGNORE_VIDEO, 1) xine_set_param(self._stream, XINE_PARAM_IGNORE_SPU, 1) self.update_eq_values() if self._supports_gapless: xine_set_param(self._stream, XINE_PARAM_EARLY_FINISHED_EVENT, 1) if self._event_queue: xine_event_dispose_queue(self._event_queue) self._event_queue = xine_event_new_queue(self._stream) xine_event_create_listener_thread(self._event_queue, self._event_listener, None)
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) return True
def find_audio_sink(): """Get the best audio sink available. Returns (element, description) or raises PlayerError. """ if os.name == "nt": sinks = [ "directsoundsink", "autoaudiosink", ] else: sinks = [ "autoaudiosink", # plugins-good "pulsesink", # plugins-good "alsasink", # plugins-base ] for name in sinks: element = Gst.ElementFactory.make(name, None) if element is not None: return (element, name) else: raise PlayerError(_("No GStreamer audio sink found"))
def _error(self, message): self.paused = True self.emit('error', self.song, PlayerError(message))
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 __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