def __init__(self, engine): AudioStream.idx += 1 self.name = '%s-audiostream-%s' % (engine.name, self.idx) self.engine = engine self.logger = logging.getLogger('%s [%s-a%s]' % (__name__, engine.name, self.idx)) # track being played by this stream self.current_track = None self.buffered_track = None # This exists because if there is a sink error, it doesn't # really make sense to recreate the sink -- it'll just fail # again. Instead, wait for the user to try to play a track, # and maybe the issue has resolved itself (plugged device in?) self.needs_sink = True self.last_position = 0 self.audio_filters = gst_utils.ProviderBin('gst_audio_filter', '%s-filters' % self.name) self.playbin = Gst.ElementFactory.make("playbin", "%s-playbin" % self.name) if self.playbin is None: raise TypeError("gstreamer 1.x base plugins not installed!") gst_utils.disable_video_text(self.playbin) self.playbin.connect("about-to-finish", self.on_about_to_finish) video = Gst.ElementFactory.make("fakesink", '%s-fakevideo' % self.name) video.set_property('sync', True) self.playbin.set_property('video-sink', video) self.audio_sink = DynamicAudioSink('%s-sink' % self.name) self.playbin.set_property('audio-sink', self.audio_sink) # Setup the bus bus = self.playbin.get_bus() bus.add_signal_watch() bus.connect('message', self.on_message) # priority boost hack if needed priority_boost(self.playbin) # Pulsesink changes volume behind our back, track it self.playbin.connect('notify::volume', self.on_volume_change) self.fader = TrackFader(self, self.on_fade_out_begin, '%s-fade-%s' % (engine.name, self.idx))
def test_calculate_fades(): fader = TrackFader(None, None, None) # fin, fout, start_off, stop_off, tracklen; # start, start+fade, end-fade, end calcs = [ # fmt: off # one is zero/none (0, 4, 0, 0, 10, 0, 0, 6, 10), (None, 4, 0, 0, 10, 0, 0, 6, 10), # other is zero/none (4, 0, 0, 0, 10, 0, 4, 10, 10), (4, None, 0, 0, 10, 0, 4, 10, 10), # both are equal (4, 4, 0, 0, 10, 0, 4, 6, 10), # both are none (0, 0, 0, 0, 10, 0, 0, 10, 10), (None, None, 0, 0, 10, 0, 0, 10, 10), # Bigger than playlen: all three cases (0, 4, 0, 0, 2, 0, 0, 0, 2), (4, 0, 0, 0, 2, 0, 2, 2, 2), (4, 4, 0, 0, 2, 0, 1, 1, 2), # With start offset (4, 4, 1, 0, 10, 1, 5, 6, 10), # With stop offset (4, 4, 0, 9, 10, 0, 4, 5, 9), # With both (2, 2, 1, 9, 10, 1, 3, 7, 9), # With both, constrained (4, 4, 4, 8, 10, 4, 6, 6, 8), (2, 4, 4, 7, 10, 4, 5, 5, 7), (4, 2, 4, 7, 10, 4, 6, 6, 7), # fmt: on ] i = 0 for fin, fout, start, stop, tlen, t0, t1, t2, t3 in calcs: print('%2d: Fade In: %s; Fade Out: %s; start: %s; stop: %s; Len: %s' % (i, fin, fout, start, stop, tlen)) track = FakeTrack(start, stop, tlen) assert fader.calculate_fades(track, fin, fout) == (t0, t1, t2, t3) i += 1
def __init__(self, engine): AudioStream.idx += 1 self.name = '%s-audiostream-%s' % (engine.name, self.idx) self.engine = engine self.logger = logging.getLogger( '%s [%s-a%s]' % (__name__, engine.name, self.idx) ) # track being played by this stream self.current_track = None self.buffered_track = None # This exists because if there is a sink error, it doesn't # really make sense to recreate the sink -- it'll just fail # again. Instead, wait for the user to try to play a track, # and maybe the issue has resolved itself (plugged device in?) self.needs_sink = True self.last_position = 0 self.audio_filters = gst_utils.ProviderBin( 'gst_audio_filter', '%s-filters' % self.name ) self.playbin = Gst.ElementFactory.make("playbin", "%s-playbin" % self.name) if self.playbin is None: raise TypeError("gstreamer 1.x base plugins not installed!") gst_utils.disable_video_text(self.playbin) self.playbin.connect("about-to-finish", self.on_about_to_finish) video = Gst.ElementFactory.make("fakesink", '%s-fakevideo' % self.name) video.set_property('sync', True) self.playbin.set_property('video-sink', video) self.audio_sink = DynamicAudioSink('%s-sink' % self.name) self.playbin.set_property('audio-sink', self.audio_sink) # Setup the bus bus = self.playbin.get_bus() bus.add_signal_watch() bus.connect('message', self.on_message) # priority boost hack if needed priority_boost(self.playbin) # Pulsesink changes volume behind our back, track it self.playbin.connect('notify::volume', self.on_volume_change) self.fader = TrackFader( self, self.on_fade_out_begin, '%s-fade-%s' % (engine.name, self.idx) )
def check_fader(test): stream = FakeStream() fader = TrackFader(stream, stream.on_fade_out, 'test') for data in test: print(data) now = data[0] stream.position = int(now * TrackFader.SECOND) print(stream.position) volume = data[1] state = data[2] timer_id = data[3] if len(data) > 4: action = data[4] args = data[5:] if len(data) > 5 else () if action == 'start': action = '_on_fade_start' elif action == 'execute': action = '_execute_fade' args = timeout_args[0] fader.now = now - 0.010 # Call the function getattr(fader, action)(*args) # Check to see if timer id exists if timer_id is None: assert fader.timer_id is None elif timer_id == TmSt: assert fader.timer_id == fader._on_fade_start elif timer_id == TmEx: assert fader.timer_id == fader._execute_fade else: assert False assert fader.state == state assert stream.volume == volume
def check_fader(test): stream = FakeStream() fader = TrackFader(stream, stream.on_fade_out, 'test') for data in test: print data now = data[0] stream.position = int(now*TrackFader.SECOND) print stream.position volume = data[1] state = data[2] timer_id = data[3] if len(data) > 4: action = data[4] args = data[5:] if len(data) > 5 else () if action == 'start': action = '_on_fade_start' elif action == 'execute': action = '_execute_fade' args = timeout_args[0] fader.now = now - 0.010 # Call the function getattr(fader, action)(*args) # Check to see if timer id exists if timer_id is None: assert fader.timer_id is None elif timer_id == TmSt: assert fader.timer_id == fader._on_fade_start elif timer_id == TmEx: assert fader.timer_id == fader._execute_fade else: assert False assert fader.state == state assert stream.volume == volume
class AudioStream(object): ''' An object that can play one or more tracks ''' idx = 0 def __init__(self, engine): AudioStream.idx += 1 self.name = '%s-audiostream-%s' % (engine.name, self.idx) self.engine = engine self.logger = logging.getLogger('%s [%s-a%s]' % (__name__, engine.name, self.idx)) # track being played by this stream self.current_track = None self.buffered_track = None # This exists because if there is a sink error, it doesn't # really make sense to recreate the sink -- it'll just fail # again. Instead, wait for the user to try to play a track, # and maybe the issue has resolved itself (plugged device in?) self.needs_sink = True self.last_position = 0 self.audio_filters = gst_utils.ProviderBin('gst_audio_filter', '%s-filters' % self.name) self.playbin = Gst.ElementFactory.make("playbin", "%s-playbin" % self.name) if self.playbin is None: raise TypeError("gstreamer 1.x base plugins not installed!") gst_utils.disable_video_text(self.playbin) self.playbin.connect("about-to-finish", self.on_about_to_finish) video = Gst.ElementFactory.make("fakesink", '%s-fakevideo' % self.name) video.set_property('sync', True) self.playbin.set_property('video-sink', video) self.audio_sink = DynamicAudioSink('%s-sink' % self.name) self.playbin.set_property('audio-sink', self.audio_sink) # Setup the bus bus = self.playbin.get_bus() bus.add_signal_watch() bus.connect('message', self.on_message) # Pulsesink changes volume behind our back, track it self.playbin.connect('notify::volume', self.on_volume_change) self.fader = TrackFader(self, self.on_fade_out_begin, '%s-fade-%s' %(engine.name, self.idx)) def destroy(self): self.fader.stop() self.playbin.set_state(Gst.State.NULL) self.playbin.get_bus().remove_signal_watch() def reconfigure_sink(self): self.needs_sink = False sink = create_device(self.engine.name) self.audio_sink.reconfigure(sink) def reconfigure_fader(self, fade_in_duration, fade_out_duration): if self.get_gst_state() != Gst.State.NULL: self.fader.setup_track(self.current_track, fade_in_duration, fade_out_duration, is_update=True) def get_gst_state(self): return self.playbin.get_state(timeout=50*Gst.MSECOND)[1] def get_position(self): # TODO: This only works when pipeline is prerolled/ready? if not self.get_gst_state() == Gst.State.PAUSED: res, self.last_position = \ self.playbin.query_position(Gst.Format.TIME) if res is False: self.last_position = 0 return self.last_position def get_volume(self): return self.playbin.props.volume def get_user_volume(self): return self.fader.get_user_volume() def pause(self): # This caches the current last position before pausing self.get_position() self.playbin.set_state(Gst.State.PAUSED) self.fader.pause() def play(self, track, start_at, paused, already_queued, fade_in_duration=None, fade_out_duration=None): '''fade duration is in seconds''' if not already_queued: self.stop(emit_eos=False) # For the moment, the only safe time to add/remove elements # is when the playbin is NULL, so do that here.. if self.audio_filters.setup_elements(): self.logger.debug("Applying audio filters") self.playbin.props.audio_filter = self.audio_filters else: self.logger.debug("Not applying audio filters") self.playbin.props.audio_filter = None if self.needs_sink: self.reconfigure_sink() self.current_track = track self.last_position = 0 self.buffered_track = None uri = track.get_loc_for_io() self.logger.info("Playing %s", common.sanitize_url(uri)) # This is only set for gapless playback if not already_queued: self.playbin.set_property("uri", uri) if urlparse.urlsplit(uri)[0] == "cdda": self.notify_id = self.playbin.connect('source-setup', self.on_source_setup, track) # Start in paused mode if we need to seek if paused or start_at is not None: self.playbin.set_state(Gst.State.PAUSED) elif not already_queued: self.playbin.set_state(Gst.State.PLAYING) self.fader.setup_track(track, fade_in_duration, fade_out_duration, now=0) if start_at is not None: self.seek(start_at) if not paused: self.playbin.set_state(Gst.State.PLAYING) if paused: self.fader.pause() def seek(self, value): '''value is in seconds''' # TODO: Make sure that we're in a valid seekable state before seeking? # wait up to 1s for the state to switch, else this fails if self.playbin.get_state(timeout=1000*Gst.MSECOND)[0] != Gst.StateChangeReturn.SUCCESS: # TODO: This error message is misleading, when does this ever happen? # TODO: if the sink is incorrectly specified, this error happens first. #self.engine._error_func(self, "Could not start at specified offset") self.logger.warning("Error seeking to specified offset") return False new_position = int(Gst.SECOND * value) seek_event = Gst.Event.new_seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH, Gst.SeekType.SET, new_position, Gst.SeekType.NONE, 0) self.last_position = new_position self.fader.seek(value) return self.playbin.send_event(seek_event) def set_volume(self, volume): #self.logger.debug("Set playbin volume: %.2f", volume) # TODO: strange issue where pulse sets the system audio volume # when exaile starts up... self.playbin.props.volume = volume def set_user_volume(self, volume): self.logger.debug("Set user volume: %.2f", volume) self.fader.set_user_volume(volume) def stop(self, emit_eos=True): prior_track = self.current_track self.current_track = None self.playbin.set_state(Gst.State.NULL) self.fader.stop() if emit_eos: self.engine._eos_func(self) return prior_track def unpause(self): # gstreamer does not buffer paused network streams, so if the user # is unpausing a stream, just restart playback current = self.current_track if not (current.is_local() or current.get_tag_raw('__length')): self.playbin.set_state(Gst.State.READY) self.playbin.set_state(Gst.State.PLAYING) self.fader.unpause() # # Events # def on_about_to_finish(self, *args): ''' This function exists solely to allow gapless playback for audio formats that support it. Setting the URI property of the playbin will queue the track for playback immediately after the previous track. .. note:: This is called from the gstreamer thread ''' if self.engine.crossfade_enabled: return track = self.engine.player.engine_autoadvance_get_next_track(gapless=True) if track: uri = track.get_loc_for_io() self.playbin.set_property('uri', uri) self.buffered_track = track self.logger.debug("Gapless transition: queuing %s", common.sanitize_url(uri)) def on_fade_out_begin(self): if self.engine.crossfade_enabled: self.engine._autoadvance_track(still_fading=True) def on_message(self, bus, message): ''' This is called on the main thread ''' if message.type == Gst.MessageType.BUFFERING: percent = message.parse_buffering() if not percent < 100: self.logger.info('Buffering complete') if percent % 5 == 0: event.log_event('playback_buffering', self.engine.player, percent) elif message.type == Gst.MessageType.TAG: """ Update track length and optionally metadata from gstreamer's parser. Useful for streams and files mutagen doesn't understand. """ current = self.current_track if not current.is_local(): gst_utils.parse_stream_tags(current, message.parse_tag()) if current and not current.get_tag_raw('__length'): res, raw_duration = self.playbin.query_duration(Gst.Format.TIME) if not res: self.logger.error("Couldn't query duration") raw_duration = 0 duration = float(raw_duration)/Gst.SECOND if duration > 0: current.set_tag_raw('__length', duration) elif message.type == Gst.MessageType.EOS and \ not self.get_gst_state() == Gst.State.PAUSED: self.engine._eos_func(self) elif message.type == Gst.MessageType.STREAM_START and \ message.src == self.playbin and \ self.buffered_track is not None: # This handles starting the next track during gapless transition buffered_track = self.buffered_track self.buffered_track = None play_args = self.engine.player.engine_autoadvance_notify_next(buffered_track) + (True, True) self.engine._next_track(*play_args) elif message.type == Gst.MessageType.STATE_CHANGED: # This idea from quodlibet: pulsesink will not notify us when # volume changes if the stream is paused, so do it when the # state changes. if message.src == self.audio_sink: self.playbin.notify("volume") elif message.type == Gst.MessageType.ERROR: # Error handling code is from quodlibet gerror, debug_info = message.parse_error() message_text = "" if gerror: message_text = gerror.message.rstrip(".") if message_text == "": # The most readable part is always the last.. message_text = debug_info[debug_info.rfind(':') + 1:] # .. unless there's nothing in it. if ' ' not in message_text: if debug_info.startswith('playsink'): message_text += _(': Possible audio device error, is it plugged in?') self.logger.error("Playback error: %s", message_text) self.logger.debug("- Extra error info: %s", debug_info) envname = 'GST_DEBUG_DUMP_DOT_DIR' if envname not in os.environ: import xl.xdg os.environ[envname] = xl.xdg.get_logs_dir() Gst.debug_bin_to_dot_file(self.playbin, Gst.DebugGraphDetails.ALL, self.name) self.logger.debug("- Pipeline debug info written to file '%s/%s.dot'", os.environ[envname], self.name) self.engine._error_func(self, message_text) # TODO: Missing plugin error handling from quod libet # -- http://cgit.freedesktop.org/gstreamer/gstreamer/tree/docs/design/part-missing-plugins.txt return True def on_source_setup(self, playbin, source, track): # this is for handling multiple CD devices properly device = track.get_loc_for_io().split("#")[-1] source.props.device = device playbin.disconnect(self.notify_id) def on_volume_change(self, e, p): real = self.playbin.props.volume vol, is_same = self.fader.calculate_user_volume(real) if not is_same: GLib.idle_add(self.engine.player.engine_notify_user_volume_change, vol)
class AudioStream(object): ''' An object that can play one or more tracks ''' idx = 0 def __init__(self, engine): AudioStream.idx += 1 self.name = '%s-audiostream-%s' % (engine.name, self.idx) self.engine = engine self.logger = logging.getLogger('%s [%s-a%s]' % (__name__, engine.name, self.idx)) # track being played by this stream self.current_track = None self.buffered_track = None # This exists because if there is a sink error, it doesn't # really make sense to recreate the sink -- it'll just fail # again. Instead, wait for the user to try to play a track, # and maybe the issue has resolved itself (plugged device in?) self.needs_sink = True self.last_position = 0 self.audio_filters = gst_utils.ProviderBin('gst_audio_filter', '%s-filters' % self.name) self.playbin = Gst.ElementFactory.make("playbin", "%s-playbin" % self.name) if self.playbin is None: raise TypeError("gstreamer 1.x base plugins not installed!") gst_utils.disable_video_text(self.playbin) self.playbin.connect("about-to-finish", self.on_about_to_finish) video = Gst.ElementFactory.make("fakesink", '%s-fakevideo' % self.name) video.set_property('sync', True) self.playbin.set_property('video-sink', video) self.audio_sink = DynamicAudioSink('%s-sink' % self.name) self.playbin.set_property('audio-sink', self.audio_sink) # Setup the bus bus = self.playbin.get_bus() bus.add_signal_watch() bus.connect('message', self.on_message) # priority boost hack if needed priority_boost(self.playbin) # Pulsesink changes volume behind our back, track it self.playbin.connect('notify::volume', self.on_volume_change) self.fader = TrackFader(self, self.on_fade_out_begin, '%s-fade-%s' % (engine.name, self.idx)) def destroy(self): self.fader.stop() self.playbin.set_state(Gst.State.NULL) self.playbin.get_bus().remove_signal_watch() def reconfigure_sink(self): self.needs_sink = False sink = create_device(self.engine.name) # Works for pulsesink, but not other sinks # -> Not a perfect solution, still some audio blip is heard. Unfortunately, # can't do better without direct support from gstreamer if self.engine.disable_autoswitch and hasattr(sink.props, 'current_device'): self.selected_sink = sink.props.device sink.connect('notify::current-device', self._on_sink_change_notify) self.audio_sink.reconfigure(sink) def _on_sink_change_notify(self, sink, param): if self.selected_sink != sink.props.current_device: domain = GLib.quark_from_string("g-exaile-error") err = GLib.Error.new_literal(domain, "Audio device disconnected", 0) self.playbin.get_bus().post( Gst.Message.new_error(None, err, "Disconnected")) self.logger.info("Detected device disconnect, stopping playback") def reconfigure_fader(self, fade_in_duration, fade_out_duration): if self.get_gst_state() != Gst.State.NULL: self.fader.setup_track(self.current_track, fade_in_duration, fade_out_duration, is_update=True) def get_gst_state(self): return self.playbin.get_state(timeout=50 * Gst.MSECOND)[1] def get_position(self): # TODO: This only works when pipeline is prerolled/ready? if not self.get_gst_state() == Gst.State.PAUSED: res, self.last_position = self.playbin.query_position( Gst.Format.TIME) if res is False: self.last_position = 0 return self.last_position def get_volume(self): return self.playbin.props.volume def get_user_volume(self): return self.fader.get_user_volume() def pause(self): # This caches the current last position before pausing self.get_position() self.playbin.set_state(Gst.State.PAUSED) self.fader.pause() def play( self, track, start_at, paused, already_queued, fade_in_duration=None, fade_out_duration=None, ): '''fade duration is in seconds''' if not already_queued: self.stop(emit_eos=False) # For the moment, the only safe time to add/remove elements # is when the playbin is NULL, so do that here.. if self.audio_filters.setup_elements(): self.logger.debug("Applying audio filters") self.playbin.props.audio_filter = self.audio_filters else: self.logger.debug("Not applying audio filters") self.playbin.props.audio_filter = None if self.needs_sink: self.reconfigure_sink() self.current_track = track self.last_position = 0 self.buffered_track = None uri = track.get_loc_for_io() self.logger.info("Playing %s", common.sanitize_url(uri)) # This is only set for gapless playback if not already_queued: self.playbin.set_property("uri", uri) if urlparse.urlsplit(uri)[0] == "cdda": self.notify_id = self.playbin.connect('source-setup', self.on_source_setup, track) # Start in paused mode if we need to seek if paused or start_at is not None: self.playbin.set_state(Gst.State.PAUSED) elif not already_queued: self.playbin.set_state(Gst.State.PLAYING) self.fader.setup_track(track, fade_in_duration, fade_out_duration, now=0) if start_at is not None: self.seek(start_at) if not paused: self.playbin.set_state(Gst.State.PLAYING) if paused: self.fader.pause() def seek(self, value): '''value is in seconds''' # TODO: Make sure that we're in a valid seekable state before seeking? # wait up to 1s for the state to switch, else this fails if (self.playbin.get_state(timeout=1000 * Gst.MSECOND)[0] != Gst.StateChangeReturn.SUCCESS): # TODO: This error message is misleading, when does this ever happen? # TODO: if the sink is incorrectly specified, this error happens first. # self.engine._error_func(self, "Could not start at specified offset") self.logger.warning("Error seeking to specified offset") return False new_position = int(Gst.SECOND * value) seek_event = Gst.Event.new_seek( 1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH, Gst.SeekType.SET, new_position, Gst.SeekType.NONE, 0, ) self.last_position = new_position self.fader.seek(value) return self.playbin.send_event(seek_event) def set_volume(self, volume): # self.logger.debug("Set playbin volume: %.2f", volume) # TODO: strange issue where pulse sets the system audio volume # when exaile starts up... self.playbin.props.volume = volume def set_user_volume(self, volume): self.logger.debug("Set user volume: %.2f", volume) self.fader.set_user_volume(volume) def stop(self, emit_eos=True): prior_track = self.current_track self.current_track = None self.playbin.set_state(Gst.State.NULL) self.fader.stop() if emit_eos: self.engine._eos_func(self) return prior_track def unpause(self): # gstreamer does not buffer paused network streams, so if the user # is unpausing a stream, just restart playback current = self.current_track if not (current.is_local() or current.get_tag_raw('__length')): self.playbin.set_state(Gst.State.READY) self.playbin.set_state(Gst.State.PLAYING) self.fader.unpause() # # Events # def on_about_to_finish(self, *args): ''' This function exists solely to allow gapless playback for audio formats that support it. Setting the URI property of the playbin will queue the track for playback immediately after the previous track. .. note:: This is called from the gstreamer thread ''' if self.engine.crossfade_enabled: return track = self.engine.player.engine_autoadvance_get_next_track( gapless=True) if track: uri = track.get_loc_for_io() self.playbin.set_property('uri', uri) self.buffered_track = track self.logger.debug("Gapless transition: queuing %s", common.sanitize_url(uri)) def on_fade_out_begin(self): if self.engine.crossfade_enabled: self.engine._autoadvance_track(still_fading=True) def on_message(self, bus, message): ''' This is called on the main thread ''' if message.type == Gst.MessageType.BUFFERING: percent = message.parse_buffering() if not percent < 100: self.logger.info('Buffering complete') if percent % 5 == 0: event.log_event('playback_buffering', self.engine.player, percent) elif message.type == Gst.MessageType.TAG: """ Update track length and optionally metadata from gstreamer's parser. Useful for streams and files mutagen doesn't understand. """ current = self.current_track if not current.is_local(): gst_utils.parse_stream_tags(current, message.parse_tag()) if current and not current.get_tag_raw('__length'): res, raw_duration = self.playbin.query_duration( Gst.Format.TIME) if not res: self.logger.error("Couldn't query duration") raw_duration = 0 duration = float(raw_duration) / Gst.SECOND if duration > 0: current.set_tag_raw('__length', duration) elif (message.type == Gst.MessageType.EOS and not self.get_gst_state() == Gst.State.PAUSED): self.engine._eos_func(self) elif (message.type == Gst.MessageType.STREAM_START and message.src == self.playbin and self.buffered_track is not None): # This handles starting the next track during gapless transition buffered_track = self.buffered_track self.buffered_track = None play_args = self.engine.player.engine_autoadvance_notify_next( buffered_track) + (True, True) self.engine._next_track(*play_args) elif message.type == Gst.MessageType.STATE_CHANGED: # This idea from quodlibet: pulsesink will not notify us when # volume changes if the stream is paused, so do it when the # state changes. if message.src == self.audio_sink: self.playbin.notify("volume") elif message.type == Gst.MessageType.ERROR: self.__handle_error_message(message) elif message.type == Gst.MessageType.ELEMENT: if not missing_plugin.handle_message(message, self.engine): logger.debug( "Unexpected element-specific GstMessage received from %s: %s", message.src, message, ) elif message.type == Gst.MessageType.WARNING: # TODO there might be some useful warnings we ignore for now. gerror, debug_text = Gst.Message.parse_warning(message) logger.warn( "Unhandled GStreamer warning received:\n\tGError: %s\n\tDebug text: %s", gerror, debug_text, ) else: # TODO there might be some useful messages we ignore for now. logger.debug("Unhandled GstMessage of type %s received: %s", message.type, message) def __handle_error_message(self, message): # Error handling code is from quodlibet gerror, debug_info = message.parse_error() message_text = "" if gerror: message_text = gerror.message.rstrip(".") if message_text == "": # The most readable part is always the last.. message_text = debug_info[debug_info.rfind(':') + 1:] # .. unless there's nothing in it. if ' ' not in message_text: if debug_info.startswith('playsink'): message_text += _( ': Possible audio device error, is it plugged in?') self.logger.error("Playback error: %s", message_text) self.logger.debug("- Extra error info: %s", debug_info) envname = 'GST_DEBUG_DUMP_DOT_DIR' if envname not in os.environ: import xl.xdg os.environ[envname] = xl.xdg.get_logs_dir() Gst.debug_bin_to_dot_file(self.playbin, Gst.DebugGraphDetails.ALL, self.name) self.logger.debug( "- Pipeline debug info written to file '%s/%s.dot'", os.environ[envname], self.name, ) self.engine._error_func(self, message_text) def on_source_setup(self, playbin, source, track): # this is for handling multiple CD devices properly device = track.get_loc_for_io().split("#")[-1] source.props.device = device playbin.disconnect(self.notify_id) def on_volume_change(self, e, p): real = self.playbin.props.volume vol, is_same = self.fader.calculate_user_volume(real) if not is_same: GLib.idle_add(self.engine.player.engine_notify_user_volume_change, vol)