1# Copyright 2004-2011 Joe Wreschnig, Michael Urman, Steven Robertson,
2#           2011-2014 Christoph Reiter
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9import gi
10try:
11    gi.require_version("Gst", "1.0")
12    gi.require_version("GstPbutils", "1.0")
13except ValueError as e:
14    raise ImportError(e)
15
16from gi.repository import Gst, GLib, GstPbutils
17
18from quodlibet import const
19from quodlibet import config
20from quodlibet import util
21from quodlibet import app
22from quodlibet import _
23
24from quodlibet.util import fver, sanitize_tags, MainRunner, MainRunnerError, \
25    MainRunnerAbortedError, MainRunnerTimeoutError, print_w, print_d, \
26    print_e, print_
27from quodlibet.player import PlayerError
28from quodlibet.player._base import BasePlayer
29from quodlibet.qltk.notif import Task
30from quodlibet.formats.mod import ModFile
31
32from .util import (parse_gstreamer_taglist, TagListWrapper, iter_to_list,
33    GStreamerSink, link_many, bin_debug)
34from .plugins import GStreamerPluginHandler
35from .prefs import GstPlayerPreferences
36
37STATE_CHANGE_TIMEOUT = Gst.SECOND * 4
38
39
40const.MinVersions.GSTREAMER.check(Gst.version())
41
42
43class BufferingWrapper(object):
44    """A wrapper for a Gst.Element (usually GstPlayBin) which pauses the
45    elmement in case buffering is needed, but hides the fact that it does.
46
47    Probably not perfect...
48
49    You have to call destroy() before it gets gc'd
50    """
51
52    def __init__(self, bin_, player):
53        """
54        bin_ -- the GstPlaybin instance to wrap
55        player -- the GStreamerPlayer instance used
56                  for aborting the buffer process
57        """
58
59        self.bin = bin_
60        self._wanted_state = None
61        self._task = None
62        self._inhibit_play = False
63        self._player = player
64
65        bus = self.bin.get_bus()
66        bus.add_signal_watch()
67        self.__bus_id = bus.connect('message', self.__message)
68
69    def __message(self, bus, message):
70        if message.type == Gst.MessageType.BUFFERING:
71            percent = message.parse_buffering()
72            self.__update_buffering(percent)
73
74    def __getattr__(self, name):
75        return getattr(self.bin, name)
76
77    def __update_buffering(self, percent):
78        """Call this with buffer percent values from the bus"""
79
80        if self._task:
81            self._task.update(percent / 100.0)
82
83        self.__set_inhibit_play(percent < 100)
84
85    def __set_inhibit_play(self, inhibit):
86        """Change the inhibit state"""
87
88        if inhibit == self._inhibit_play:
89            return
90        self._inhibit_play = inhibit
91
92        # task management
93        if inhibit:
94            if not self._task:
95                def stop_buf(*args):
96                    self._player.paused = True
97
98                self._task = Task(_("Stream"), _("Buffering"), stop=stop_buf)
99        elif self._task:
100            self._task.finish()
101            self._task = None
102
103        # state management
104        if inhibit:
105            # save the current state
106            status, state, pending = self.bin.get_state(
107                timeout=STATE_CHANGE_TIMEOUT)
108            if status == Gst.StateChangeReturn.SUCCESS and \
109                    state == Gst.State.PLAYING:
110                self._wanted_state = state
111            else:
112                # no idea, at least don't play
113                self._wanted_state = Gst.State.PAUSED
114
115            self.bin.set_state(Gst.State.PAUSED)
116        else:
117            # restore the old state
118            self.bin.set_state(self._wanted_state)
119            self._wanted_state = None
120
121    def set_state(self, state):
122        if self._inhibit_play:
123            # PLAYING, PAUSED change the state for after buffering is finished,
124            # everything else aborts buffering
125            if state not in (Gst.State.PLAYING, Gst.State.PAUSED):
126                # abort
127                self.__set_inhibit_play(False)
128                self.bin.set_state(state)
129                return
130            self._wanted_state = state
131        else:
132            self.bin.set_state(state)
133
134    def get_state(self, *args, **kwargs):
135        # get_state also is a barrier (seek/positioning),
136        # so call every time but ignore the result in the inhibit case
137        res = self.bin.get_state(*args, **kwargs)
138        if self._inhibit_play:
139            return (Gst.StateChangeReturn.SUCCESS,
140                    self._wanted_state, Gst.State.VOID_PENDING)
141        return res
142
143    def destroy(self):
144        if self.__bus_id:
145            bus = self.bin.get_bus()
146            bus.disconnect(self.__bus_id)
147            bus.remove_signal_watch()
148            self.__bus_id = None
149
150        self.__set_inhibit_play(False)
151        self.bin = None
152
153
154def sink_has_external_state(sink):
155    sink_name = sink.get_factory().get_name()
156
157    if sink_name == "wasapisink":
158        # https://bugzilla.gnome.org/show_bug.cgi?id=796386
159        return hasattr(sink.props, "volume")
160    else:
161        return sink_name == "pulsesink"
162
163
164def sink_state_is_valid(sink):
165    if not sink_has_external_state(sink):
166        return True
167
168    # pulsesink volume is only valid in PAUSED/PLAYING
169    # https://bugzilla.gnome.org/show_bug.cgi?id=748577
170    current_state = sink.get_state(0)[1]
171    return current_state >= Gst.State.PAUSED
172
173
174class Seeker(object):
175    """Manages async seeking and position reporting for a pipeline.
176
177    You have to call destroy() before it gets gc'd
178    """
179
180    def __init__(self, playbin, player):
181        self._player = player
182        self._playbin = playbin
183
184        self._last_position = 0
185        self._seek_requests = []
186        self._active_seeks = []
187        self._seekable = False
188
189        bus = playbin.get_bus()
190        bus.add_signal_watch()
191        self._bus_id = bus.connect('message', self._on_message)
192        player.notify("seekable")
193
194    @property
195    def seekable(self):
196        """If the current stream is seekable"""
197
198        return self._seekable
199
200    def destroy(self):
201        """This needs to be called before it gets GC'ed"""
202
203        del self._seek_requests[:]
204        del self._active_seeks[:]
205
206        if self._bus_id:
207            bus = self._playbin.get_bus()
208            bus.disconnect(self._bus_id)
209            bus.remove_signal_watch()
210            self._bus_id = None
211
212        self._player = None
213        self._playbin = None
214
215    def set_position(self, pos):
216        """Set the position. Async and might not succeed.
217
218        Args:
219            pos (int): position in milliseconds
220        """
221
222        pos = max(0, int(pos))
223
224        # We need at least a paused state to seek, if there is non active
225        # or pending, request one async.
226        res, next_state, pending = self._playbin.get_state(timeout=0)
227        if pending != Gst.State.VOID_PENDING:
228            next_state = pending
229        if next_state < Gst.State.PAUSED:
230            self._playbin.set_state(Gst.State.PAUSED)
231
232        self._set_position(self._player.song, pos)
233
234    def get_position(self):
235        """Get the position
236
237        Returns:
238            int: the position in milliseconds
239        """
240
241        if self._seek_requests:
242            self._last_position = self._seek_requests[-1][1]
243        elif self._active_seeks:
244            self._last_position = self._active_seeks[-1][1]
245        else:
246            # While we are actively seeking return the last wanted position.
247            # query_position() returns 0 while in this state
248            ok, p = self._playbin.query_position(Gst.Format.TIME)
249            if ok:
250                p //= Gst.MSECOND
251                # During stream seeking querying the position fails.
252                # Better return the last valid one instead of 0.
253                self._last_position = p
254
255        return self._last_position
256
257    def reset(self):
258        """In case the underlying stream has changed, call this to
259        abort any pending seeking actions and update the seekable state
260        """
261
262        self._last_position = 0
263        del self._seek_requests[:]
264        del self._active_seeks[:]
265        self._refresh_seekable()
266
267    def _refresh_seekable(self):
268        query = Gst.Query.new_seeking(Gst.Format.TIME)
269        if self._playbin.query(query):
270            seekable = query.parse_seeking()[1]
271        elif self._player.song is None:
272            seekable = False
273        else:
274            seekable = True
275
276        if seekable != self._seekable:
277            self._seekable = seekable
278            self._player.notify("seekable")
279
280    def _on_message(self, bus, message):
281        if message.type == Gst.MessageType.ASYNC_DONE:
282            # we only get one ASYNC_DONE for multiple seeks, so flush all
283
284            if self._active_seeks:
285                song, pos = self._active_seeks[-1]
286                if song is self._player.song:
287                    self._player.emit("seek", song, pos)
288                del self._active_seeks[:]
289        elif message.type == Gst.MessageType.STATE_CHANGED:
290            if message.src is self._playbin.bin:
291                new_state = message.parse_state_changed()[1]
292                if new_state >= Gst.State.PAUSED:
293                    self._refresh_seekable()
294
295                    if self._seek_requests:
296                        song, pos = self._seek_requests[-1]
297                        if song is self._player.song:
298                            self._set_position(song, pos)
299                        del self._seek_requests[:]
300
301    def _set_position(self, song, pos):
302        event = Gst.Event.new_seek(
303            1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH,
304            Gst.SeekType.SET, pos * Gst.MSECOND, Gst.SeekType.NONE, 0)
305
306        if self._playbin.send_event(event):
307            self._active_seeks.append((song, pos))
308        else:
309            self._seek_requests.append((song, pos))
310
311
312class GStreamerPlayer(BasePlayer, GStreamerPluginHandler):
313
314    def PlayerPreferences(self):
315        return GstPlayerPreferences(self, const.DEBUG)
316
317    def __init__(self, librarian=None):
318        GStreamerPluginHandler.__init__(self)
319        BasePlayer.__init__(self)
320
321        self._librarian = librarian
322
323        self.version_info = "GStreamer: %s" % fver(Gst.version())
324        self._pipeline_desc = None
325
326        self._volume = 1.0
327        self._paused = True
328        self._mute = False
329
330        self._in_gapless_transition = False
331        self._active_error = False
332
333        self.bin = None
334        self._seeker = None
335        self._int_vol_element = None
336        self._ext_vol_element = None
337        self._ext_mute_element = None
338        self._use_eq = False
339        self._eq_element = None
340        self.__info_buffer = None
341
342        self._lib_id = librarian.connect("changed", self.__songs_changed)
343        self.__atf_id = None
344        self.__bus_id = None
345        self._runner = MainRunner()
346
347    def __songs_changed(self, librarian, songs):
348        # replaygain values might have changed, recalc volume
349        if self.song and self.song in songs:
350            self._reset_replaygain()
351
352    def _destroy(self):
353        self._librarian.disconnect(self._lib_id)
354        self._runner.abort()
355        self.__destroy_pipeline()
356
357    @property
358    def name(self):
359        name = "GStreamer"
360        if self._pipeline_desc:
361            name += " (%s)" % self._pipeline_desc
362        return name
363
364    @property
365    def has_external_volume(self):
366        ext = self._ext_vol_element
367        if ext is None or not sink_has_external_state(ext):
368            return False
369        return True
370
371    def _set_buffer_duration(self, duration):
372        """Set the stream buffer duration in msecs"""
373
374        config.set("player", "gst_buffer", float(duration) / 1000)
375
376        if self.bin:
377            value = duration * Gst.MSECOND
378            self.bin.set_property('buffer-duration', value)
379
380    def _print_pipeline(self):
381        """Print debug information for the active pipeline to stdout
382        (elements, formats, ...)
383        """
384
385        if self.bin:
386            # self.bin is just a wrapper, so get the real one
387            for line in bin_debug([self.bin.bin]):
388                print_(line)
389        else:
390            print_e("No active pipeline.")
391
392    def __init_pipeline(self):
393        """Creates a gstreamer pipeline. Returns True on success."""
394
395        if self.bin:
396            return True
397
398        # reset error state
399        self.error = False
400
401        pipeline = config.get("player", "gst_pipeline")
402        try:
403            pipeline, self._pipeline_desc = GStreamerSink(pipeline)
404        except PlayerError as e:
405            self._error(e)
406            return False
407
408        if self._use_eq and Gst.ElementFactory.find('equalizer-10bands'):
409            # The equalizer only operates on 16-bit ints or floats, and
410            # will only pass these types through even when inactive.
411            # We push floats through to this point, then let the second
412            # audioconvert handle pushing to whatever the rest of the
413            # pipeline supports. As a bonus, this seems to automatically
414            # select the highest-precision format supported by the
415            # rest of the chain.
416            filt = Gst.ElementFactory.make('capsfilter', None)
417            filt.set_property('caps',
418                              Gst.Caps.from_string('audio/x-raw,format=F32LE'))
419            eq = Gst.ElementFactory.make('equalizer-10bands', None)
420            self._eq_element = eq
421            self.update_eq_values()
422            conv = Gst.ElementFactory.make('audioconvert', None)
423            resample = Gst.ElementFactory.make('audioresample', None)
424            pipeline = [filt, eq, conv, resample] + pipeline
425
426        # playbin2 has started to control the volume through pulseaudio,
427        # which means the volume property can change without us noticing.
428        # Use our own volume element for now until this works with PA.
429        self._int_vol_element = Gst.ElementFactory.make('volume', None)
430        pipeline.insert(0, self._int_vol_element)
431
432        # Get all plugin elements and append audio converters.
433        # playbin already includes one at the end
434        plugin_pipeline = []
435        for plugin in self._get_plugin_elements():
436            plugin_pipeline.append(plugin)
437            plugin_pipeline.append(
438                Gst.ElementFactory.make('audioconvert', None))
439            plugin_pipeline.append(
440                Gst.ElementFactory.make('audioresample', None))
441        pipeline = plugin_pipeline + pipeline
442
443        bufbin = Gst.Bin()
444        for element in pipeline:
445            assert element is not None, pipeline
446            bufbin.add(element)
447
448        if len(pipeline) > 1:
449            if not link_many(pipeline):
450                print_w("Linking the GStreamer pipeline failed")
451                self._error(
452                    PlayerError(_("Could not create GStreamer pipeline")))
453                return False
454
455        # see if the sink provides a volume property, if yes, use it
456        sink_element = pipeline[-1]
457        if isinstance(sink_element, Gst.Bin):
458            sink_element = iter_to_list(sink_element.iterate_recurse)[-1]
459
460        self._ext_vol_element = None
461        if hasattr(sink_element.props, "volume"):
462            self._ext_vol_element = sink_element
463
464            # In case we use the sink volume directly we can increase buffering
465            # without affecting the volume change delay too much and safe some
466            # CPU time... (2x default for now).
467            if hasattr(sink_element.props, "buffer_time"):
468                sink_element.set_property("buffer-time", 400000)
469
470            def ext_volume_notify(*args):
471                # gets called from a thread
472                GLib.idle_add(self.notify, "volume")
473
474            self._ext_vol_element.connect("notify::volume", ext_volume_notify)
475
476        self._ext_mute_element = None
477        if hasattr(sink_element.props, "mute") and \
478                sink_element.get_factory().get_name() != "directsoundsink":
479            # directsoundsink has a mute property but it doesn't work
480            # https://bugzilla.gnome.org/show_bug.cgi?id=755106
481            self._ext_mute_element = sink_element
482
483            def mute_notify(*args):
484                # gets called from a thread
485                GLib.idle_add(self.notify, "mute")
486
487            self._ext_mute_element.connect("notify::mute", mute_notify)
488
489        # Make the sink of the first element the sink of the bin
490        gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink'))
491        bufbin.add_pad(gpad)
492
493        bin_ = Gst.ElementFactory.make('playbin', None)
494        assert bin_
495
496        self.bin = BufferingWrapper(bin_, self)
497        self._seeker = Seeker(self.bin, self)
498
499        bus = bin_.get_bus()
500        bus.add_signal_watch()
501        self.__bus_id = bus.connect('message', self.__message, self._librarian)
502
503        self.__atf_id = self.bin.connect('about-to-finish',
504            self.__about_to_finish)
505
506        # set buffer duration
507        duration = config.getfloat("player", "gst_buffer")
508        self._set_buffer_duration(int(duration * 1000))
509
510        # connect playbin to our pluing/volume/eq pipeline
511        self.bin.set_property('audio-sink', bufbin)
512
513        # by default playbin will render video -> suppress using fakesink
514        fakesink = Gst.ElementFactory.make('fakesink', None)
515        self.bin.set_property('video-sink', fakesink)
516
517        # disable all video/text decoding in playbin
518        GST_PLAY_FLAG_VIDEO = 1 << 0
519        GST_PLAY_FLAG_TEXT = 1 << 2
520        flags = self.bin.get_property("flags")
521        flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT)
522        self.bin.set_property("flags", flags)
523
524        if not self.has_external_volume:
525            # Restore volume/ReplayGain and mute state
526            self.props.volume = self._volume
527            self.mute = self._mute
528
529        # ReplayGain information gets lost when destroying
530        self._reset_replaygain()
531
532        if self.song:
533            self.bin.set_property('uri', self.song("~uri"))
534
535        return True
536
537    def __destroy_pipeline(self):
538        self._remove_plugin_elements()
539
540        if self.__bus_id:
541            bus = self.bin.get_bus()
542            bus.disconnect(self.__bus_id)
543            bus.remove_signal_watch()
544            self.__bus_id = None
545
546        if self.__atf_id:
547            self.bin.disconnect(self.__atf_id)
548            self.__atf_id = None
549
550        if self._seeker is not None:
551            self._seeker.destroy()
552            self._seeker = None
553            self.notify("seekable")
554
555        if self.bin:
556            self.bin.set_state(Gst.State.NULL)
557            self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT)
558            # BufferingWrapper cleanup
559            self.bin.destroy()
560            self.bin = None
561
562        self._in_gapless_transition = False
563
564        self._ext_vol_element = None
565        self._int_vol_element = None
566        self._ext_mute_element = None
567        self._eq_element = None
568
569    def _rebuild_pipeline(self):
570        """If a pipeline is active, rebuild it and restore vol, position etc"""
571
572        if not self.bin:
573            return
574
575        paused = self.paused
576        pos = self.get_position()
577
578        self.__destroy_pipeline()
579        self.paused = True
580        self.__init_pipeline()
581        self.paused = paused
582        self.seek(pos)
583
584    def __message(self, bus, message, librarian):
585        if message.type == Gst.MessageType.EOS:
586            print_d("Stream EOS")
587            if not self._in_gapless_transition:
588                self._source.next_ended()
589            self._end(False)
590        elif message.type == Gst.MessageType.TAG:
591            self.__tag(message.parse_tag(), librarian)
592        elif message.type == Gst.MessageType.ERROR:
593            gerror, debug_info = message.parse_error()
594            message = u""
595            if gerror:
596                message = gerror.message.rstrip(".")
597            details = None
598            if debug_info:
599                # strip the first line, not user friendly
600                debug_info = "\n".join(debug_info.splitlines()[1:])
601                # can contain paths, so not sure if utf-8 in all cases
602                details = debug_info
603            self._error(PlayerError(message, details))
604        elif message.type == Gst.MessageType.STATE_CHANGED:
605            # pulsesink doesn't notify a volume change on startup
606            # and the volume is only valid in > paused states.
607            if message.src is self._ext_vol_element:
608                self.notify("volume")
609            if message.src is self._ext_mute_element:
610                self.notify("mute")
611        elif message.type == Gst.MessageType.STREAM_START:
612            if self._in_gapless_transition:
613                print_d("Stream changed")
614                self._end(False)
615        elif message.type == Gst.MessageType.ELEMENT:
616            message_name = message.get_structure().get_name()
617
618            if message_name == "missing-plugin":
619                self.__handle_missing_plugin(message)
620        elif message.type == Gst.MessageType.CLOCK_LOST:
621            print_d("Clock lost")
622            self.bin.set_state(Gst.State.PAUSED)
623            self.bin.set_state(Gst.State.PLAYING)
624        elif message.type == Gst.MessageType.LATENCY:
625            print_d("Recalculate latency")
626            self.bin.recalculate_latency()
627        elif message.type == Gst.MessageType.REQUEST_STATE:
628            state = message.parse_request_state()
629            print_d("State requested: %s" % Gst.Element.state_get_name(state))
630            self.bin.set_state(state)
631        elif message.type == Gst.MessageType.DURATION_CHANGED:
632            if self.song.fill_length:
633                ok, p = self.bin.query_duration(Gst.Format.TIME)
634                if ok:
635                    p /= float(Gst.SECOND)
636                    self.song["~#length"] = p
637                    librarian.changed([self.song])
638
639    def __handle_missing_plugin(self, message):
640        get_installer_detail = \
641            GstPbutils.missing_plugin_message_get_installer_detail
642        get_description = GstPbutils.missing_plugin_message_get_description
643
644        details = get_installer_detail(message)
645        if details is None:
646            return
647
648        self.stop()
649
650        format_desc = get_description(message)
651        title = _(u"No GStreamer element found to handle media format")
652        error_details = _(u"Media format: %(format-description)s") % {
653            "format-description": format_desc}
654
655        def install_done_cb(plugins_return, *args):
656            print_d("Gstreamer plugin install return: %r" % plugins_return)
657            Gst.update_registry()
658
659        context = GstPbutils.InstallPluginsContext.new()
660
661        # new in 1.6
662        if hasattr(context, "set_desktop_id"):
663            from gi.repository import Gtk
664            context.set_desktop_id(app.id)
665
666        # new in 1.6
667        if hasattr(context, "set_startup_notification_id"):
668            current_time = Gtk.get_current_event_time()
669            context.set_startup_notification_id("_TIME%d" % current_time)
670
671        gdk_window = app.window.get_window()
672        if gdk_window:
673            try:
674                xid = gdk_window.get_xid()
675            except AttributeError:  # non X11
676                pass
677            else:
678                context.set_xid(xid)
679
680        res = GstPbutils.install_plugins_async(
681            [details], context, install_done_cb, None)
682        print_d("Gstreamer plugin install result: %r" % res)
683
684        if res in (GstPbutils.InstallPluginsReturn.HELPER_MISSING,
685                GstPbutils.InstallPluginsReturn.INTERNAL_FAILURE):
686            self._error(PlayerError(title, error_details))
687
688    def __about_to_finish_sync(self):
689        """Returns the next song uri to play or None"""
690
691        print_d("About to finish (sync)")
692
693        # Chained oggs falsely trigger a gapless transition.
694        # At least for radio streams we can safely ignore it because
695        # transitions don't occur there.
696        # https://github.com/quodlibet/quodlibet/issues/1454
697        # https://bugzilla.gnome.org/show_bug.cgi?id=695474
698        if self.song.multisong:
699            print_d("multisong: ignore about to finish")
700            return
701
702        # mod + gapless deadlocks
703        # https://github.com/quodlibet/quodlibet/issues/2780
704        if isinstance(self.song, ModFile):
705            return
706
707        if config.getboolean("player", "gst_disable_gapless"):
708            print_d("Gapless disabled")
709            return
710
711        # this can trigger twice, see issue 987
712        if self._in_gapless_transition:
713            return
714        self._in_gapless_transition = True
715
716        print_d("Select next song in mainloop..")
717        self._source.next_ended()
718        print_d("..done.")
719
720        song = self._source.current
721        if song is not None:
722            return song("~uri")
723
724    def __about_to_finish(self, playbin):
725        print_d("About to finish (async)")
726
727        try:
728            uri = self._runner.call(self.__about_to_finish_sync,
729                                    priority=GLib.PRIORITY_HIGH,
730                                    timeout=0.5)
731        except MainRunnerTimeoutError as e:
732            # Due to some locks being held during this signal we can get
733            # into a deadlock when a seek or state change event happens
734            # in the mainloop before our function gets scheduled.
735            # In this case abort and do nothing, which results
736            # in a non-gapless transition.
737            print_d("About to finish (async): %s" % e)
738            return
739        except MainRunnerAbortedError as e:
740            print_d("About to finish (async): %s" % e)
741            return
742        except MainRunnerError:
743            util.print_exc()
744            return
745
746        if uri is not None:
747            print_d("About to finish (async): setting uri")
748            playbin.set_property('uri', uri)
749        print_d("About to finish (async): done")
750
751    def stop(self):
752        super(GStreamerPlayer, self).stop()
753        self.__destroy_pipeline()
754
755    def do_get_property(self, property):
756        if property.name == 'volume':
757            if self._ext_vol_element is not None and \
758                    sink_has_external_state(self._ext_vol_element) and \
759                    sink_state_is_valid(self._ext_vol_element):
760                # never read back the volume if we don't have to, e.g.
761                # directsoundsink maps volume to an int which makes UI
762                # sliders jump if we read the value back
763                self._volume = self._ext_vol_element.get_property("volume")
764            return self._volume
765        elif property.name == "mute":
766            if self._ext_mute_element is not None and \
767                    sink_has_external_state(self._ext_mute_element) and \
768                    sink_state_is_valid(self._ext_mute_element):
769                self._mute = self._ext_mute_element.get_property("mute")
770            return self._mute
771        elif property.name == "seekable":
772            if self._seeker is not None:
773                return self._seeker.seekable
774            return False
775        else:
776            raise AttributeError
777
778    def _reset_replaygain(self):
779        if not self.bin:
780            return
781
782        v = 1.0 if self._ext_vol_element is not None else self._volume
783        v = self.calc_replaygain_volume(v)
784        v = min(10.0, max(0.0, v))
785        self._int_vol_element.set_property('volume', v)
786
787    def do_set_property(self, property, v):
788        if property.name == 'volume':
789            self._volume = v
790            if self._ext_vol_element:
791                v = min(10.0, max(0.0, v))
792                self._ext_vol_element.set_property("volume", v)
793            else:
794                v = self.calc_replaygain_volume(v)
795                if self.bin:
796                    v = min(10.0, max(0.0, v))
797                    self._int_vol_element.set_property('volume', v)
798        elif property.name == 'mute':
799            self._mute = v
800            if self._ext_mute_element is not None:
801                self._ext_mute_element.set_property("mute", v)
802            else:
803                if self.bin:
804                    self._int_vol_element.set_property("mute", v)
805        else:
806            raise AttributeError
807
808    def get_position(self):
809        """Return the current playback position in milliseconds,
810        or 0 if no song is playing."""
811
812        if self._seeker:
813            return self._seeker.get_position()
814        return 0
815
816    @property
817    def paused(self):
818        return self._paused
819
820    @paused.setter
821    def paused(self, paused):
822        if paused == self._paused:
823            return
824
825        self._paused = paused
826        self.emit((paused and 'paused') or 'unpaused')
827        # in case a signal handler changed the paused state, abort this
828        if self._paused != paused:
829            return
830
831        if paused:
832            if self.bin:
833                if not self.song:
834                    # Something wants us to pause between songs, or when
835                    # we've got no song playing (probably StopAfterMenu).
836                    self.__destroy_pipeline()
837                elif self.seekable:
838                    self.bin.set_state(Gst.State.PAUSED)
839                else:
840                    q = Gst.Query.new_buffering(Gst.Format.DEFAULT)
841                    if self.bin.query(q):
842                        # destroy so that we rebuffer on resume i.e. we don't
843                        # want to continue unseekable streams from where we
844                        # paused but from where we unpaused.
845                        self.__destroy_pipeline()
846                    else:
847                        self.bin.set_state(Gst.State.PAUSED)
848        else:
849            if self.song and self.__init_pipeline():
850                self.bin.set_state(Gst.State.PLAYING)
851
852    def _error(self, player_error):
853        """Destroy the pipeline and set the error state.
854
855        The passed PlayerError will be emitted through the 'error' signal.
856        """
857
858        # prevent recursive errors
859        if self._active_error:
860            return
861        self._active_error = True
862
863        self.__destroy_pipeline()
864        self.error = True
865        self.paused = True
866
867        print_w(player_error)
868        self.emit('error', self.song, player_error)
869        self._active_error = False
870
871    def seek(self, pos):
872        """Seek to a position in the song, in milliseconds."""
873
874        # Don't allow seeking during gapless. We can't go back to the old song.
875        if not self.song or self._in_gapless_transition:
876            return
877
878        if self.__init_pipeline():
879            self._seeker.set_position(pos)
880
881    def sync(self, timeout):
882        if self.bin is not None:
883            # XXX: This is flaky, try multiple times
884            for i in range(5):
885                self.bin.get_state(Gst.SECOND * timeout)
886                # we have some logic in the main loop, so iterate there
887                while GLib.MainContext.default().iteration(False):
888                    pass
889
890    def _end(self, stopped, next_song=None):
891        print_d("End song")
892        song, info = self.song, self.info
893
894        # set the new volume before the signals to avoid delays
895        if self._in_gapless_transition:
896            self.song = self._source.current
897            self._reset_replaygain()
898
899        # We need to set self.song to None before calling our signal
900        # handlers. Otherwise, if they try to end the song they're given
901        # (e.g. by removing it), then we get in an infinite loop.
902        self.__info_buffer = self.song = self.info = None
903        if song is not info:
904            self.emit('song-ended', info, stopped)
905        self.emit('song-ended', song, stopped)
906
907        current = self._source.current if next_song is None else next_song
908
909        # Then, set up the next song.
910        self.song = self.info = current
911
912        print_d("Next song")
913        if self.song is not None:
914            if not self._in_gapless_transition:
915                # Due to extensive problems with playbin2, we destroy the
916                # entire pipeline and recreate it each time we're not in
917                # a gapless transition.
918                self.__destroy_pipeline()
919                self.__init_pipeline()
920            if self.bin:
921                if self.paused:
922                    self.bin.set_state(Gst.State.PAUSED)
923                else:
924                    # something unpaused while no song was active
925                    if song is None:
926                        self.emit("unpaused")
927                    self.bin.set_state(Gst.State.PLAYING)
928        else:
929            self.__destroy_pipeline()
930
931        self._in_gapless_transition = False
932
933        if self._seeker is not None:
934            # we could have a gapless transition to a non-seekable -> update
935            self._seeker.reset()
936
937        self.emit('song-started', self.song)
938
939        if self.song is None:
940            self.paused = True
941
942    def __tag(self, tags, librarian):
943        if self.song and self.song.multisong:
944            self._fill_stream(tags, librarian)
945        elif self.song and self.song.fill_metadata:
946            pass
947
948    def _fill_stream(self, tags, librarian):
949        # get a new remote file
950        new_info = self.__info_buffer
951        if not new_info:
952            new_info = type(self.song)(self.song["~filename"])
953            new_info.multisong = False
954            new_info.streamsong = True
955
956            # copy from the old songs
957            # we should probably listen to the library for self.song changes
958            new_info.update(self.song)
959            new_info.update(self.info)
960
961        changed = False
962        info_changed = False
963
964        tags = TagListWrapper(tags, merge=True)
965        tags = parse_gstreamer_taglist(tags)
966
967        for key, value in sanitize_tags(tags, stream=False).items():
968            if self.song.get(key) != value:
969                changed = True
970                self.song[key] = value
971
972        for key, value in sanitize_tags(tags, stream=True).items():
973            if new_info.get(key) != value:
974                info_changed = True
975                new_info[key] = value
976
977        if info_changed:
978            # in case the title changed, make self.info a new instance
979            # and emit ended/started for the the old/new one
980            if self.info.get("title") != new_info.get("title"):
981                if self.info is not self.song:
982                    self.emit('song-ended', self.info, False)
983                self.info = new_info
984                self.__info_buffer = None
985                self.emit('song-started', self.info)
986            else:
987                # in case title didn't changed, update the values of the
988                # old instance if there is one and tell the library.
989                if self.info is not self.song:
990                    self.info.update(new_info)
991                    librarian.changed([self.info])
992                else:
993                    # So we don't loose all tags before the first title
994                    # save it for later
995                    self.__info_buffer = new_info
996
997        if changed:
998            librarian.changed([self.song])
999
1000    @property
1001    def eq_bands(self):
1002        if Gst.ElementFactory.find('equalizer-10bands'):
1003            return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011]
1004        else:
1005            return []
1006
1007    def update_eq_values(self):
1008        need_eq = any(self._eq_values)
1009        if need_eq != self._use_eq:
1010            self._use_eq = need_eq
1011            self._rebuild_pipeline()
1012
1013        if self._eq_element:
1014            for band, val in enumerate(self._eq_values):
1015                self._eq_element.set_property('band%d' % band, val)
1016
1017    def can_play_uri(self, uri):
1018        if not Gst.uri_is_valid(uri):
1019            return False
1020        try:
1021            Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '')
1022        except GLib.GError:
1023            return False
1024        return True
1025
1026
1027def init(librarian):
1028    # Enable error messages by default
1029    if Gst.debug_get_default_threshold() == Gst.DebugLevel.NONE:
1030        Gst.debug_set_default_threshold(Gst.DebugLevel.ERROR)
1031
1032    return GStreamerPlayer(librarian)
1033