Esempio n. 1
0
    class GPlayer:
        def __init__(self):

            # This is used to keep track of time between callbacks to progress the seek bar
            self.player_timer = Timer()

            # This is used to keep note of what state of playing we should be in
            self.play_state = 0  # 0 is stopped, 1 is playing, 2 is paused

            # Initiate GSteamer
            Gst.init([])
            self.mainloop = GLib.MainLoop()

            # Get list of available audio device
            pctl.gst_devices = ["Auto", "PulseAudio", "ALSA", "JACK"]
            if tauon.snap_mode:
                pctl.gst_devices.remove("JACK")
                pctl.gst_devices.remove("ALSA")
            pctl.gst_outputs.clear()
            dm = Gst.DeviceMonitor()
            dm.start()
            for device in dm.get_devices():
                if device.get_device_class() == "Audio/Sink":
                    element = device.create_element(None)
                    type_name = element.get_factory().get_name()
                    device_name = element.props.device
                    display_name = device.get_display_name()

                    # This is used by the UI to present list of options to the user in audio settings
                    pctl.gst_outputs[display_name] = (type_name, device_name)
                    pctl.gst_devices.append(display_name)

            dm.stop()

            # Create main "playbin" pipeline for playback
            self.playbin = Gst.ElementFactory.make("playbin", "player")

            # Create output bin
            if not prefs.gst_use_custom_output:
                prefs.gst_output = prefs.gen_gst_out()

            self._output = Gst.parse_bin_from_description(
                prefs.gst_output, ghost_unlinked_pads=True)

            # Create a bin for the audio pipeline
            self._sink = Gst.ElementFactory.make("bin", "sink")
            self._sink.add(self._output)

            # # Spectrum -------------------------
            # # This kind of works, but is a different result to that of the bass backend.
            # # This seems linear and also less visually appealing.
            #
            # self.spectrum = Gst.ElementFactory.make("spectrum", "spectrum")
            # self.spectrum.set_property('bands', 280)
            # self.spectrum.set_property('interval', 10000000)
            # self.spectrum.set_property('post-messages', True)
            # self.spectrum.set_property('message-magnitude', True)
            #
            # self.playbin.set_property('audio-filter', self.spectrum)
            # # ------------------------------------

            # Create volume element
            self._vol = Gst.ElementFactory.make("volume", "volume")
            self._sink.add(self._vol)
            self._vol.link(self._output)

            # Set up sink pad for the intermediate bin via the
            #  first element (volume)
            ghost = Gst.GhostPad.new("sink", self._vol.get_static_pad("sink"))

            self._sink.add_pad(ghost)

            # Connect the playback bin to to the intermediate bin sink pad
            self.playbin.set_property("audio-sink", self._sink)

            # The pipeline should look something like this -
            # (player) -> [(volume) -> (output)]

            # Set callback for the main callback loop
            GLib.timeout_add(50, self.main_callback)

            # self.playbin.connect("about-to-finish", self.about_to_finish)  # Not used by anything

            # # Enable bus to get spectrum messages
            bus = self.playbin.get_bus()
            bus.add_signal_watch()
            bus.connect('message::element', self.on_message)
            bus.connect('message::buffering', self.on_message)
            bus.connect('message::error', self.on_message)
            bus.connect('message::tag', self.on_message)
            # bus.connect('message::warning', self.on_message)
            # bus.connect('message::eos', self.on_message)

            # Variables used with network downloading
            self.temp_id = "a"
            self.url = None
            self.dl_ready = False
            self.temp_path = ""  # Full path + filename

            # # Broadcasting pipeline ------------
            #
            # # This works, but only for one track, switching tracks seems to be a more complicated process.
            #
            # self.b_playbin = Gst.ElementFactory.make("playbin", "player")
            #
            # # Create output bin
            # # Using tcpserversink seems to mostly work with the html5 player, though an HTTP server may be preferred.
            # self._b_output = Gst.parse_bin_from_description(
            #    "audioconvert ! vorbisenc ! oggmux ! tcpserversink port=8000", ghost_unlinked_pads=True)
            #    #"autoaudiosink", ghost_unlinked_pads=True)
            #
            # # Connect the playback bin to to the output bin
            # self.b_playbin.set_property("audio-sink", self._b_output)
            # # ----------------------------------------

            # Start GLib mainloop
            self.mainloop.run()

        # # Used to get spectrum data and pass onto UI
        def on_message(self, bus, msg):
            struct = msg.get_structure()
            # print(struct.get_name())
            # print(struct.to_string())

            if self.play_state == 3 and struct.get_name() == "GstMessageTag":
                data = struct.get_value("taglist").get_string("title")
                if data[0]:
                    pctl.tag_meta = data[1]

            elif struct.get_name() == "GstMessageError":
                if "Connection" in struct.get_value("debug"):
                    gui.show_message("Connection error", mode="info")
            elif struct.get_name() == 'GstMessageBuffering':
                buff_percent = struct.get_value("buffer-percent")

                if buff_percent < 100 and (self.play_state == 1
                                           or self.play_state == 3):
                    self.playbin.set_state(Gst.State.PAUSED)

                elif buff_percent == 100 and (self.play_state == 1
                                              or self.play_state == 3):
                    self.playbin.set_state(Gst.State.PLAYING)

            # if struct.get_name() == 'spectrum':

        #         struct_str = struct.to_string()
        #         magnitude_str = re.match(r'.*magnitude=\(float\){(.*)}.*', struct_str)
        #         if magnitude_str:
        #             magnitude = map(float, magnitude_str.group(1).split(','))
        #
        #             l = list(magnitude)
        #             k = []
        #             for a in l[:23]:
        #                 k.append(a + 60)
        #             gui.spec = k
        #             #print(k)
        #             gui.level_update = True

        def check_duration(self):

            # This function is to be called when loading a track to query for a duration of track
            # in case the tagger failed to calculate a length for the track when imported.

            # Get current playing track object from player
            current_track = pctl.playing_object()

            if current_track is not None and current_track.length < 1:

                result = self.playbin.query_duration(Gst.Format.TIME)

                if result[0] is True:
                    current_track.length = result[1] / Gst.SECOND

                else:  # still loading? I guess we wait and try again.
                    time.sleep(1.5)
                    result = self.playbin.query_duration(Gst.Format.TIME)

                    if result[0] is True:
                        current_track.length = result[1] / Gst.SECOND

        def main_callback(self):

            # This is the main callback function to be triggered continuously as long as application is running
            if self.play_state == 1:
                pctl.test_progress(
                )  # This function triggers an advance if we are near end of track

            if self.play_state == 3:
                pctl.radio_progress()

            if pctl.playerCommandReady:
                pctl.playerCommandReady = False

                # Here we process commands from the main thread/module

                # Possible commands:

                # open: Start playback of a file
                #  (Path given by pctl.target_open at position pctl.start_time_target + pctl.jump_time)
                # stop: Stop playback (OK to unload file from memory)
                # runstop: Stop playback but let finish if we are near the end of the file
                # pauseon: Pause playback (be ready to resume)
                # pauseoff: Resume playback if paused
                # volume: Set (and remember) the volume specified by pctl.player_volume (0 to 100)
                # seek: Seek to position given by pctl.new_time + pctl.start_time (don't resume playback if paused)
                # url: Start playback of a shoutcast/icecast stream. URL specified by pctl.url (todo)
                # suspend: Pause and disconnect from output device (not used, playbin automatically does this)
                # unload: Cleanup and exit
                # done: Tell the main thread we finished doing a special request it was waiting for (such as unload)
                # encstart: Start broadcasting given track at start time (same way as open)
                # cast-next: Switch broadcasting to given track at start time (same way as open)

                # Note that functions such as gapless playback are entirely implemented on this side.
                # We wont be told, we just guess when we need to do them and hold loop until we are done.
                # Advance will be called early for gapless, currently allotted 5 seconds (can we reduce this somehow?)
                # Concepts such as advance and back are not used on this side.

                # Todo: Visualisers
                # Uhhh, this is a bit of a can of worms. What we want to do is constantly get binned spectrum data
                # and pass it to the UI (in certain formats).
                # Specifically, current format used with BASS module is:
                # - An FFT of (a current segment of?) raw sample data
                # - Non-complex (magnitudes of the first half of the FFT are returned)
                # - 1024 samples (returns 512 values)
                # - Combined left and right channels (mono)
                # - Binned to particular numbers of bins and passed onto UI after some scaling and truncating
                # There's also a level meter which just takes peak "level" (scaled in someway perhaps)

                # Todo: User settings
                # prefs.use_transition_crossfade (if true, fade rather than transition gaplessly at end of file) todo
                # prefs.use_jump_crossfade (if true and not end of file, fade rather than switch instantly) todo
                # prefs.use_pause_fade (if true, fade when pausing, rather than pausing instantly) todo
                url = None
                if pctl.playerCommand == 'open' and pctl.target_object:

                    # Check if the file exists, mark it as missing if not
                    if pctl.target_object.is_network:
                        try:
                            url, params = pctl.get_url(pctl.target_object)
                        except:
                            time.sleep(0.1)
                            gui.show_message("Connection error",
                                             "Bad login? Server offline?",
                                             mode='info')
                            pctl.stop()
                            pctl.playerCommand = ""
                            self.main_callback()
                            return

                    elif os.path.isfile(pctl.target_object.fullpath):
                        # File exists so continue
                        pctl.target_object.found = True
                    else:
                        # File does not exist, trigger an advance
                        pctl.target_object.found = False
                        tauon.console.print(
                            "Missing File: " + pctl.target_object.fullpath, 2)
                        pctl.playing_state = 0
                        pctl.jump_time = 0
                        pctl.advance(inplace=True, nolock=True)
                        GLib.timeout_add(19, self.main_callback)
                        return

                    gapless = False
                    current_time = 0
                    current_duration = 0

                    if pctl.target_object.is_network:

                        if params:
                            self.url = url + ".view?" + urllib.parse.urlencode(
                                params)
                        else:
                            self.url = url

                    if self.play_state != 0:
                        # Determine time position of currently playing track
                        current_time = self.playbin.query_position(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        current_duration = self.playbin.query_duration(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        #print("We are " + str(current_duration - current_time) + " seconds from end.")

                    # If we are close to the end of the track, try transition gaplessly
                    if self.play_state == 1 and pctl.start_time_target == 0 and pctl.jump_time == 0 and \
                            0.2 < current_duration - current_time < 5.5 and not pctl.playerSubCommand == 'now':
                        #print("Use GStreamer Gapless transition")
                        gapless = True

                    # If we are not supposed to be playing, stop (crossfade todo)
                    else:
                        self.playbin.set_state(Gst.State.READY)

                    pctl.playerSubCommand = ""
                    self.play_state = 1

                    if url:
                        self.playbin.set_property('uri', self.url)
                    else:
                        # Play file on disk
                        self.playbin.set_property(
                            'uri', 'file://' + urllib.parse.quote(
                                os.path.abspath(pctl.target_open)))
                    self._vol.set_property('volume', pctl.player_volume / 100)
                    self.playbin.set_state(Gst.State.PLAYING)
                    if pctl.jump_time == 0:
                        pctl.playing_time = 0

                    time.sleep(
                        0.1
                    )  # Setting and querying position right away seems to fail, so wait a small moment

                    # The position to start is not always the beginning of the file, so seek to position
                    if pctl.start_time_target > 0 or pctl.jump_time > 0:
                        self.playbin.seek_simple(
                            Gst.Format.TIME,
                            Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                            (pctl.start_time_target + pctl.jump_time) *
                            Gst.SECOND)
                        pctl.playing_time = 0
                        gui.update = 1

                    if gapless:  # Hold thread while a gapless transition is in progress
                        t = 0
                        while self.playbin.query_position(Gst.Format.TIME)[
                                1] / Gst.SECOND >= current_time > 0:

                            time.sleep(0.1)
                            t += 1

                            if self.playbin.get_state(
                                    0).state != Gst.State.PLAYING:
                                break

                            if t > 50:
                                print("Gonna stop waiting..."
                                      )  # Cant wait forever
                                break

                            if pctl.playerCommand == 'open' and pctl.playerCommandReady:
                                # Cancel the gapless transition
                                self.playbin.set_state(Gst.State.READY)
                                time.sleep(0.1)
                                GLib.timeout_add(19, self.main_callback)
                                return

                    pctl.jump_time = 0
                    time.sleep(0.15)
                    self.check_duration()
                    self.player_timer.hit()

                # elif pctl.playerCommand == 'encstart':
                #     print("Start Gstreamer broadcast")
                #     self.b_playbin.set_property('uri', 'file://' + urllib.parse.quote(os.path.abspath(pctl.target_open)))
                #     self.b_playbin.set_state(Gst.State.PLAYING)
                #     pctl.broadcast_active = True
                #
                # elif pctl.playerCommand == 'cast-next':
                #     print("castt next")
                #     self.playbin.set_state(Gst.State.READY)
                #     time.sleep(0.15)
                #     self.b_playbin.set_property('uri', 'file://' + urllib.parse.quote(os.path.abspath(pctl.target_open)))
                #     self.b_playbin.set_state(Gst.State.PLAYING)

                elif pctl.playerCommand == 'url':

                    # Stop if playing or paused
                    if self.play_state == 1 or self.play_state == 2 or self.play_state == 3:
                        self.playbin.set_state(Gst.State.READY)
                        time.sleep(0.1)

                    # Open URL stream
                    self.playbin.set_property('uri', pctl.url)
                    self.playbin.set_property('volume',
                                              pctl.player_volume / 100)
                    time.sleep(0.1)
                    self.playbin.set_state(Gst.State.PLAYING)
                    self.play_state = 3
                    self.player_timer.hit()

                elif pctl.playerCommand == 'volume':
                    if self.play_state == 1 or self.play_state == 3:
                        self.playbin.set_property('volume',
                                                  pctl.player_volume / 100)

                elif pctl.playerCommand == 'runstop':

                    if self.play_state != 0:
                        # Determine time position of currently playing track
                        current_time = self.playbin.query_position(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        current_duration = self.playbin.query_duration(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        if current_duration - current_time < 5.5:
                            pass
                        else:
                            self.playbin.set_state(Gst.State.READY)
                    else:
                        self.playbin.set_state(Gst.State.READY)
                    self.play_state = 0
                    pctl.playerCommand = "stopped"

                elif pctl.playerCommand == 'stop':

                    if self.play_state > 0:
                        self.playbin.set_state(Gst.State.READY)
                    self.play_state = 0
                    pctl.playerCommand = "stopped"

                elif pctl.playerCommand == 'seek':
                    if self.play_state > 0:
                        self.playbin.seek_simple(
                            Gst.Format.TIME,
                            Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                            (pctl.new_time + pctl.start_time_target) *
                            Gst.SECOND)

                    # It may take a moment for seeking to update when streaming, so for better UI feedback we'll
                    # update the seek indicator immediately and hold the thread for a moment
                    if pctl.target_object.is_network:
                        pctl.playing_time = pctl.new_time + pctl.start_time_target
                        pctl.decode_time = pctl.playing_time
                        time.sleep(0.2)

                elif pctl.playerCommand == 'pauseon':
                    self.player_timer.hit()
                    self.play_state = 2
                    self.playbin.set_state(Gst.State.PAUSED)

                elif pctl.playerCommand == 'pauseoff':
                    self.player_timer.hit()
                    self.playbin.set_state(Gst.State.PLAYING)
                    self.play_state = 1

                elif pctl.playerCommand == 'unload':
                    if self.play_state > 0:
                        self.playbin.set_state(Gst.State.NULL)
                        time.sleep(0.5)

                    self.mainloop.quit()
                    pctl.playerCommand = 'done'
                    return

            if self.play_state == 3:
                if self.playbin.get_state(0).state == Gst.State.PLAYING:
                    add_time = self.player_timer.hit()
                    if add_time > 2:
                        add_time = 2
                    if add_time < 0:
                        add_time = 0
                    pctl.playing_time += add_time
                    pctl.decode_time = pctl.playing_time

            if self.play_state == 1:

                # Get jump in time since last call
                add_time = self.player_timer.hit()

                # Limit the jump. Timer is monotonic, but we'll double check, just in case.
                if add_time > 2:
                    add_time = 2
                if add_time < 0:
                    add_time = 0

                # Progress main seek head
                if self.playbin.get_state(0).state == Gst.State.PLAYING:
                    pctl.playing_time = max(
                        0, (self.playbin.query_position(Gst.Format.TIME)[1] /
                            Gst.SECOND) - pctl.start_time_target)
                    pctl.decode_time = pctl.playing_time  # A difference isn't discerned in this module

                else:
                    # We're supposed to be playing but it's not? Give it a push I guess.
                    self.playbin.set_state(Gst.State.PLAYING)
                    pctl.playing_time += add_time
                    pctl.decode_time = pctl.playing_time

                # Other things we need to progress such as scrobbling
                if pctl.playing_time < 3 and pctl.a_time < 3:
                    pctl.a_time = pctl.playing_time
                else:
                    pctl.a_time += add_time

                pctl.total_playtime += add_time
                lfm_scrobbler.update(
                    add_time
                )  # This handles other scrobblers such as listenbrainz also

                # Update track total playtime
                if len(pctl.track_queue) > 0 and 2 > add_time > 0:
                    star_store.add(pctl.track_queue[pctl.queue_step], add_time)

            if not pctl.running:
                # print("unloading gstreamer")
                if self.play_state > 0:
                    self.playbin.set_state(Gst.State.NULL)
                    time.sleep(0.5)

                self.mainloop.quit()
                pctl.playerCommand = 'done'

            else:
                GLib.timeout_add(19, self.main_callback)

        def exit(self):
            pctl.playerCommand = 'done'
Esempio n. 2
0
    class GPlayer:
        def __init__(self):

            print("Init GStreamer...")

            # This is used to keep track of time between callbacks.
            self.player_timer = Timer()

            # Store the track object that is currently playing
            self.loaded_track = None

            # This is used to keep note of what state of playing we should be in
            self.play_state = 0  # 0 is stopped, 1 is playing, 2 is paused

            # Initiate GSteamer
            Gst.init([])
            self.mainloop = GLib.MainLoop()

            # Populate list of output devices with defaults
            outputs = {}
            devices = [
                "PulseAudio",
                "ALSA",
                "JACK",
            ]
            if tauon.snap_mode:  # Snap permissions don't support these by default
                devices.remove("JACK")
                devices.remove("ALSA")

            # Get list of available audio device
            self.dm = Gst.DeviceMonitor()
            self.dm.start()
            for device in self.dm.get_devices():
                if device.get_device_class() == "Audio/Sink":
                    element = device.create_element(None)
                    type_name = element.get_factory().get_name()
                    if hasattr(element.props, "device"):
                        device_name = element.props.device
                        display_name = device.get_display_name()

                        # This is used by the UI to present list of options to the user in audio settings
                        outputs[display_name] = (type_name, device_name)
                        devices.append(display_name)

            # dm.stop()  # Causes a segfault sometimes
            pctl.gst_outputs = outputs
            pctl.gst_devices = devices

            # Create main "playbin" pipeline for playback
            self.playbin = Gst.ElementFactory.make("playbin", "player")

            # Create custom output bin from user preferences
            if not prefs.gst_use_custom_output:
                prefs.gst_output = prefs.gen_gst_out()

            self._output = Gst.parse_bin_from_description(
                prefs.gst_output, ghost_unlinked_pads=True)

            # Create a bin for the audio pipeline
            self._sink = Gst.ElementFactory.make("bin", "sink")
            self._sink.add(self._output)

            # Spectrum -------------------------
            # Cant seem to figure out how to process these magnitudes in a way that looks good

            # self.spectrum = Gst.ElementFactory.make("spectrum", "spectrum")
            # self.spectrum.set_property('bands', 20)
            # self.spectrum.set_property('interval', 10000000)
            # self.spectrum.set_property('threshold', -100)
            # self.spectrum.set_property('post-messages', True)
            # self.spectrum.set_property('message-magnitude', True)
            #
            # self.playbin.set_property('audio-filter', self.spectrum)
            # # ------------------------------------

            # # Level Meter -------------------------
            self.level = Gst.ElementFactory.make("level", "level")
            self.level.set_property('interval', 20000000)
            self.playbin.set_property('audio-filter', self.level)
            # # ------------------------------------

            self._eq = Gst.ElementFactory.make("equalizer-nbands", "eq")
            self._vol = Gst.ElementFactory.make("volume", "volume")

            self._sink.add(self._eq)
            self._sink.add(self._vol)

            self._eq.link(self._vol)
            self._vol.link(self._output)

            self._eq.set_property("num-bands", 10 + 2)

            # Set the equalizer based on user preferences

            # Using workaround for "inverted slider" bug.
            # Thanks to Lollypop and Clementine for discovering this.
            # Ref https://github.com/Taiko2k/TauonMusicBox/issues/414

            for i in range(10 + 2):
                band = self._eq.get_child_by_index(i)
                band.set_property("freq", 0)
                band.set_property("bandwidth", 0)
                band.set_property("gain", 0)

            self.set_eq()
            # if prefs.use_eq:
            #     self._eq.set_property("band" + str(i), level * -1)
            # else:
            #     self._eq.set_property("band" + str(i), 0.0)

            # Set up sink pad for the intermediate bin via the
            # first element (volume)
            ghost = Gst.GhostPad.new("sink", self._eq.get_static_pad("sink"))
            self._sink.add_pad(ghost)

            # Connect the playback bin to to the intermediate bin sink pad
            self.playbin.set_property("audio-sink", self._sink)

            # The pipeline should look something like this -
            # (player) -> [(eq) -> (volume) -> (output)]

            # Create controller for pause/resume volume fading
            self.c_source = GstController.InterpolationControlSource()
            self.c_source.set_property('mode',
                                       GstController.InterpolationMode.LINEAR)
            self.c_binding = GstController.DirectControlBinding.new(
                self._vol, "volume", self.c_source)
            self._vol.add_control_binding(self.c_binding)

            # Set callback for the main callback loop
            GLib.timeout_add(50, self.main_callback)

            self.playbin.connect("about-to-finish",
                                 self.about_to_finish)  # Not used

            # Setup bus and select what types of messages we want to listen for
            bus = self.playbin.get_bus()
            bus.add_signal_watch()
            bus.connect('message::element', self.on_message)
            bus.connect('message::buffering', self.on_message)
            bus.connect('message::error', self.on_message)
            bus.connect('message::tag', self.on_message)
            bus.connect('message::warning', self.on_message)
            # bus.connect('message::eos', self.on_message)

            # Variables used with network downloading
            self.temp_id = "a"
            self.url = None
            self.dl_ready = True
            self.using_cache = False
            self.temp_path = ""  # Full path + filename
            # self.level_train = []
            self.seek_timer = Timer()
            self.seek_timer.force_set(10)
            self.buffering = False
            # Other
            self.end_timer = Timer()

            # Start GLib mainloop
            self.mainloop.run()

        def set_eq(self):
            last = 0
            for i, level in enumerate(prefs.eq):
                band = self._eq.get_child_by_index(i + 1)
                if prefs.use_eq:
                    band = self._eq.get_child_by_index(i + 1)
                    freq = 31.25 * (2**i)
                    band.set_property("freq", freq)
                    band.set_property("bandwidth", freq - last)
                    last = freq
                    band.set_property("gain", level * -1)
                else:
                    band.set_property("freq", 0)
                    band.set_property("bandwidth", 0)
                    band.set_property("gain", 0)

        def about_to_finish(self, bin):
            self.end_timer.set()

        def on_message(self, bus, msg):
            struct = msg.get_structure()
            #print(struct.get_name())
            #print(struct.to_string())

            name = struct.get_name()

            if name == "GstMessageError":

                if "_is_dead" in struct.to_string():
                    # Looks like PulseAudio was reset. Need to restart playback.

                    self.playbin.set_state(Gst.State.NULL)

                    if tauon.stream_proxy.download_running:
                        tauon.stream_proxy.stop()
                    else:
                        self.playbin.set_state(Gst.State.PLAYING)
                        tries = 0
                        while tries < 10:
                            time.sleep(0.03)
                            r = self.playbin.seek_simple(
                                Gst.Format.TIME,
                                Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                                (pctl.start_time_target + pctl.playing_time) *
                                Gst.SECOND)
                            if r:
                                break
                            tries += 1

            if self.play_state == 3 and name == "GstMessageTag":

                if not tauon.radiobox.parse_vorbis_okay():
                    return

                data = struct.get_value("taglist").get_string("title")
                data2 = struct.get_value("taglist").get_string("artist")
                data3 = struct.get_value("taglist").get_string("year")
                data4 = struct.get_value("taglist").get_string("album")
                # print(struct.to_string())
                if data[0]:
                    pctl.tag_meta = ""
                    line = ""
                    line = data[1]
                    if data2[0]:
                        line = data2[1] + " - " + line

                    pctl.found_tags = {}

                    pctl.found_tags["title"] = data[1]
                    if data2[0]:
                        pctl.found_tags["artist"] = data2[1]
                    if data3[0]:
                        pctl.found_tags["year"] = str(data3[1])
                    if data4[0]:
                        pctl.found_tags["album"] = data4[1]

                    pctl.tag_meta = line
                    print("Found tag: " + line)

            elif name == "GstMessageError":
                if "Connection" in struct.get_value("debug"):
                    gui.show_message("Connection error", mode="info")
            elif name == 'GstMessageBuffering':

                if pctl.playing_state == 3:
                    buff_percent = struct.get_value("buffer-percent")

                    if buff_percent == 0 and (self.play_state == 1
                                              or self.play_state == 3):
                        self.playbin.set_state(Gst.State.PAUSED)
                        self.buffering = True
                        print("Buffering...")

                    elif self.buffering and buff_percent == 100 and (
                            self.play_state == 1 or self.play_state == 3):
                        self.playbin.set_state(Gst.State.PLAYING)
                        self.buffering = False
                        print("Buffered")

            if gui.vis == 1 and name == 'level':
                # print(struct.to_string())
                data = struct.get_value("peak")
                ts = struct.get_value("timestamp")
                # print(data)
                l = (10**(data[0] / 20)) * 11.6
                if len(data) == 1:
                    r = l
                else:
                    r = (10**(data[1] / 20)) * 11.6

                td = (ts / 1000000000) - (self.playbin.query_position(
                    Gst.Format.TIME)[1] / Gst.SECOND)
                t = time.time()
                rt = t + td
                if td > 0:
                    for item in tauon.level_train:
                        if rt < item[0]:
                            tauon.level_train.clear()
                            # print("FF")
                            break
                    tauon.level_train.append((rt, l, r))

            # if name == 'spectrum':
            #     struct_str = struct.to_string()
            #     magnitude_str = re.match(r'.*magnitude=\(float\){(.*)}.*', struct_str)
            #     if magnitude_str:
            #         magnitude = map(float, magnitude_str.group(1).split(','))
            #
            #         l = list(magnitude)
            #         k = []
            #         #print(l)
            #         for a in l[0:20]:
            #             #v = ??
            #             k.append()
            #         print(k)
            #         gui.spec = k
            #         #print(k)
            #         gui.level_update = True
            #
            # return True

        def check_duration(self):

            # This function is to be called when loading a track to query for a duration of track
            # in case the tagger failed to calculate a length for the track when imported.

            # Get current playing track object from player
            current_track = pctl.playing_object()

            if current_track is not None and (
                    current_track.length < 1
                    or current_track.file_ext.lower() in tauon.mod_formats):

                time.sleep(0.25)

                result = self.playbin.query_duration(Gst.Format.TIME)

                if result[0] is True:
                    current_track.length = result[1] / Gst.SECOND
                    pctl.playing_length = current_track.length
                    gui.pl_update += 1

                else:  # still loading? I guess we wait and try again.
                    time.sleep(1.5)
                    result = self.playbin.query_duration(Gst.Format.TIME)

                    if result[0] is True:
                        current_track.length = result[1] / Gst.SECOND

        def download_part(self, url, target, params, id):

            #   GStreamer can't seek some types of HTTP sources.
            #
            #   To work around this, when a seek is requested by the user, this
            #   function facilitates the download of the URL in whole, then loaded
            #   into GStreamer as a complete file to provide at least some manner of seeking
            #   ability for the user. (User must wait for full download)
            #
            #   (Koel and Airsonic MP3 sources are exempt from this as seeking does work with them)
            #
            #   A better solution might be to download file externally then feed the audio data
            #   into GStreamer as it downloads. This still would have the issue that the whole file
            #   must have been downloaded before a seek could begin.
            #
            #   With the old BASS backend, this was done with the file on disk being constantly
            #   appended to. Unfortunately GStreamer doesn't support playing files in this manner.

            try:
                part = requests.get(url, stream=True, params=params)
            except:
                gui.show_message("Could not connect to server", mode="error")
                self.dl_ready = True
                return

            bitrate = 0

            a = 0
            z = 0
            # print(target)
            f = open(target, "wb")
            for chunk in part.iter_content(chunk_size=1024):
                if chunk:  # filter out keep-alive new chunks
                    a += 1
                    # if a == 300:  # kilobyes~
                    #     self.dl_ready = True

                    if id != self.id:
                        part.close()
                        f.close()
                        os.remove(target)
                        return
                        # break

                    f.write(chunk)

                    # Periodically update download the progress indicator
                    z += 1
                    if id == self.id:
                        if z == 60:
                            z = 0
                            if bitrate == 0:
                                audio = auto.File(target)
                                bitrate = audio.bitrate

                            if bitrate > 0:
                                gui.update += 1
                                pctl.download_time = a * 1024 / (bitrate /
                                                                 8) / 1000
            f.close()
            pctl.download_time = -1

            self.dl_ready = True

        def main_callback(self):

            if not pctl.playerCommandReady and pctl.playing_state == 0 and not tauon.spot_ctl.playing and not tauon.spot_ctl.coasting:
                # tauon.tm.player_lock.acquire()  # Blocking the GLib thread blocks tray processing
                GLib.timeout_add(50, self.main_callback)
                return

            if gui.vis == 1:
                if pctl.playing_state == 1:
                    gui.level_update = True

            # This is the main callback function to be triggered continuously as long as application is running
            if self.play_state == 1 and pctl.playing_time > 1 and not pctl.playerCommandReady:

                pctl.test_progress(
                )  # This function triggers an advance if we are near end of track

                success, state, pending = self.playbin.get_state(3 *
                                                                 Gst.SECOND)
                if state != Gst.State.PLAYING:
                    time.sleep(0.5)

                    print("Stall...")

            if self.play_state == 3:
                pctl.radio_progress()

            if not pctl.playerCommandReady:
                pctl.spot_test_progress()

            if pctl.playerCommandReady:
                command = pctl.playerCommand
                pctl.playerCommand = ""
                pctl.playerCommandReady = False

                # print("RUN COMMAND")
                # print(command)

                # Here we process commands from the main thread/module

                # Possible commands:

                # open:     Start playback of a file
                #           Path given by pctl.target_open at position pctl.start_time_target + pctl.jump_time
                # stop:     Stop playback (Implies release file)
                # runstop:  Stop playback but let finish if we are near the end of the file
                # pauseon:  Pause playback (be ready to resume)
                # pauseoff: Resume playback if paused
                # volume:   Set to the volume specified by pctl.player_volume (0 to 100)
                # seek:     Seek to position given by pctl.new_time + pctl.start_time (don't resume playback if paused)
                # url:      Start playback of a shoutcast/icecast stream. URL specified by pctl.url
                #           todo: start recording if pctl.record_stream  (rec button is current disabled for GST in UI)
                #                 encode to OGG and output file to prefs.encoder_output
                #                 automatically name files and split on metadata change
                # unload:   Stop, cleanup and exit thread
                # done:     Tell the main thread we finished doing a special request it was waiting for (such as unload)

                # Todo: Visualisers (Hard)
                # Ideally we would want the same visual effect as the old BASS based visualisers.
                # What we want to do is constantly get binned spectrum data and pass it to the UI.
                # Specifically, format used with BASS module is:
                # - An FFT of sample data with Hanning window applied
                # - 1024 samples (returns first half, 512 values)
                # - Non-complex (magnitudes)
                # - Combined left and right channels
                # - Binned to particular numbers of bars and passed onto UI after some scaling and truncating

                pctl.download_time = 0
                url = None
                if command == 'open' and pctl.target_object:
                    # print("Start track")
                    track = pctl.target_object

                    if (tauon.spot_ctl.playing or tauon.spot_ctl.coasting
                        ) and not track.file_ext == "SPTY":
                        tauon.spot_ctl.control("stop")

                    if tauon.stream_proxy.download_running:
                        tauon.stream_proxy.stop()

                    # Check if the file exists, mark it as missing if not
                    if track.is_network:

                        if track.file_ext == "SPTY":
                            tauon.level_train.clear()
                            if self.play_state > 0:
                                self.playbin.set_state(Gst.State.READY)
                            self.play_state = 0
                            try:
                                tauon.spot_ctl.play_target(track.url_key)
                            except:
                                print("Failed to start Spotify track")
                                pctl.playerCommand = "stop"
                                pctl.playerCommandReady = True

                            GLib.timeout_add(19, self.main_callback)
                            return

                        try:
                            url, params = pctl.get_url(track)
                            self.urlparams = url, params
                        except:
                            time.sleep(0.1)
                            gui.show_message("Connection error",
                                             "Bad login? Server offline?",
                                             mode='info')
                            pctl.stop()
                            pctl.playerCommand = ""
                            self.main_callback()
                            return

                    # If the target is a file, check that is exists
                    elif os.path.isfile(track.fullpath):
                        if not track.found:
                            pctl.reset_missing_flags()
                    else:
                        # File does not exist, force trigger an advance
                        track.found = False
                        tauon.console.print("Missing File: " + track.fullpath,
                                            2)
                        pctl.playing_state = 0
                        pctl.jump_time = 0
                        # print("FORCE JUMP")
                        pctl.advance(inplace=True, play=True)
                        GLib.timeout_add(19, self.main_callback)
                        return

                    gapless = False
                    current_time = 0
                    current_duration = 0

                    if track.is_network:
                        if params:
                            self.url = url + ".view?" + urllib.parse.urlencode(
                                params)
                        else:
                            self.url = url

                    if self.play_state != 0:
                        # Determine time position of currently playing track
                        current_time = self.playbin.query_position(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        current_duration = self.playbin.query_duration(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        # print("We are " + str(current_duration - current_time) + " seconds from end.")

                    if self.play_state != 1:
                        self.loaded_track = None

                    # If we are close to the end of the track, try transition gaplessly
                    if self.play_state == 1 and pctl.start_time_target == 0 and pctl.jump_time == 0 and \
                            current_duration - current_time < 5.5 and not pctl.playerSubCommand == 'now' \
                            and self.end_timer.get() > 3:

                        gapless = True

                        # if self.play_state == 1 and self.loaded_track and self.loaded_track.is_network:
                        #     # Gst may report wrong length for network tracks, use known length instead
                        #     if pctl.playing_time < self.loaded_track.length - 4:
                        #         gapless = False

                    # We're not at the end of the last track so reset the pipeline
                    if not gapless:
                        self.playbin.set_state(Gst.State.NULL)
                        tauon.level_train.clear()

                    pctl.playerSubCommand = ""
                    self.play_state = 1

                    self.save_temp = tauon.temp_audio + "/" + str(
                        track.index) + "-audio"
                    # shoot_dl = threading.Thread(target=self.download_part,
                    #                             args=([url, self.save_temp, params, track.url_key]))
                    # shoot_dl.daemon = True
                    # shoot_dl.start()
                    self.using_cache = False

                    self.id = track.url_key
                    if url:
                        # self.playbin.set_property('uri',
                        #                           'file://' + urllib.parse.quote(os.path.abspath(self.save_temp)))

                        if self.dl_ready and os.path.exists(self.save_temp):
                            self.using_cache = True
                            self.playbin.set_property(
                                'uri', 'file://' + urllib.parse.quote(
                                    os.path.abspath(self.save_temp)))
                        else:
                            self.playbin.set_property('uri', self.url)
                    else:
                        # Play file on disk
                        self.playbin.set_property(
                            'uri', 'file://' + urllib.parse.quote(
                                os.path.abspath(track.fullpath)))

                    if pctl.start_time_target > 0:
                        self.playbin.set_property('volume', 0.0)
                    else:
                        self.playbin.set_property('volume',
                                                  pctl.player_volume / 100)

                    self.playbin.set_state(Gst.State.PLAYING)
                    if pctl.jump_time == 0 and not pctl.playerCommand == "seek":
                        pctl.playing_time = 0

                    # The position to start is not always the beginning of the file, so seek to position
                    if pctl.start_time_target > 0 or pctl.jump_time > 0:

                        tries = 0
                        while tries < 150:
                            time.sleep(0.03)
                            r = self.playbin.seek_simple(
                                Gst.Format.TIME,
                                Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                                (pctl.start_time_target + pctl.jump_time) *
                                Gst.SECOND)
                            if r:
                                break
                            tries += 1
                            if tries > 2:
                                print("Seek failed, retrying...")
                                print(tries)

                        pctl.playing_time = 0
                        gui.update = 1

                        self.playbin.set_property('volume',
                                                  pctl.player_volume / 100)

                    if gapless:  # Hold thread while a gapless transition is in progress
                        t = 0
                        # print("Gapless go")
                        while self.playbin.query_position(Gst.Format.TIME)[
                                1] / Gst.SECOND >= current_time > 0:

                            time.sleep(0.02)
                            t += 1

                            if self.playbin.get_state(
                                    0).state != Gst.State.PLAYING:
                                break

                            if t > 250:
                                print("Gonna stop waiting..."
                                      )  # Cant wait forever
                                break

                            if pctl.playerCommand == 'open' and pctl.playerCommandReady:
                                # Cancel the gapless transition
                                print("Cancel transition")
                                self.playbin.set_state(Gst.State.NULL)
                                time.sleep(0.5)
                                GLib.timeout_add(19, self.main_callback)
                                return

                    add_time = max(min(self.player_timer.hit(), 3), 0)
                    if self.loaded_track and self.loaded_track in pctl.master_library.values(
                    ):
                        star_store.add(self.loaded_track.index, add_time)

                    self.loaded_track = track

                    pctl.jump_time = 0
                    #time.sleep(1)
                    add_time = self.player_timer.hit()
                    if add_time > 2:
                        add_time = 2
                    if add_time < 0:
                        add_time = 0
                    pctl.playing_time += add_time

                    t = self.playbin.query_position(Gst.Format.TIME)
                    if t[0]:
                        pctl.decode_time = (
                            t[1] / Gst.SECOND) - self.loaded_track.start_time
                    else:
                        pctl.decode_time = pctl.playing_time

                    if self.loaded_track:
                        star_store.add(self.loaded_track.index, add_time)

                    self.check_duration()
                    self.player_timer.hit()
                    time.sleep(0.5)

                elif command == 'url':

                    # Stop if playing or paused
                    if self.play_state == 1 or self.play_state == 2 or self.play_state == 3:
                        self.playbin.set_state(Gst.State.NULL)
                        time.sleep(0.1)

                    w = 0
                    while len(tauon.stream_proxy.chunks) < 50:
                        time.sleep(0.01)
                        w += 1
                        if w > 500:
                            print("Taking too long!")
                            tauon.stream_proxy.stop()
                            pctl.playerCommand = 'stop'
                            pctl.playerCommandReady = True
                            break
                    else:
                        # Open URL stream
                        self.playbin.set_property('uri', pctl.url)
                        self.playbin.set_property('volume',
                                                  pctl.player_volume / 100)
                        self.buffering = False
                        self.playbin.set_state(Gst.State.PLAYING)
                        self.play_state = 3
                        self.player_timer.hit()

                elif command == 'seteq':
                    self.set_eq()

                    # for i, level in enumerate(prefs.eq):
                    #     if prefs.use_eq:
                    #         self._eq.set_property("band" + str(i), level * -1)
                    #     else:
                    #         self._eq.set_property("band" + str(i), 0.0)

                elif command == 'volume':

                    if tauon.spot_ctl.coasting or tauon.spot_ctl.playing:
                        tauon.spot_ctl.control("volume",
                                               int(pctl.player_volume))

                    elif self.play_state == 1 or self.play_state == 3:

                        success, current_time = self.playbin.query_position(
                            Gst.Format.TIME)
                        self.playbin.set_state(Gst.State.PLAYING)

                        if success and False:
                            start = current_time + ((150 / 1000) * Gst.SECOND)
                            end = current_time + ((200 / 1000) * Gst.SECOND)
                            self.c_source.set(
                                start,
                                self._vol.get_property('volume') / 10)
                            self.c_source.set(end,
                                              (pctl.player_volume / 100) / 10)
                            time.sleep(0.6)
                            self.c_source.unset_all()
                        else:
                            self.playbin.set_property('volume',
                                                      pctl.player_volume / 100)

                elif command == 'runstop':

                    if self.play_state != 0:
                        # Determine time position of currently playing track
                        current_time = self.playbin.query_position(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        current_duration = self.playbin.query_duration(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        if current_duration - current_time < 5.5:
                            time.sleep((current_duration - current_time) + 1)
                            self.playbin.set_state(Gst.State.READY)
                        else:
                            self.playbin.set_state(Gst.State.READY)
                    else:
                        self.playbin.set_state(Gst.State.READY)
                    self.play_state = 0
                    pctl.playerSubCommand = "stopped"

                elif command == 'stop':
                    if self.play_state > 0:

                        if prefs.use_pause_fade:
                            success, current_time = self.playbin.query_position(
                                Gst.Format.TIME)
                            if success:
                                start = current_time + (150 / 1000 *
                                                        Gst.SECOND)
                                end = current_time + (prefs.pause_fade_time /
                                                      1000 * Gst.SECOND)
                                self.c_source.set(
                                    start, (pctl.player_volume / 100) / 10)
                                self.c_source.set(end, 0.0)
                                time.sleep(prefs.pause_fade_time / 1000)
                                time.sleep(0.05)
                                self.c_source.unset_all()

                        self.playbin.set_state(Gst.State.NULL)
                        time.sleep(0.1)
                        self._vol.set_property("volume",
                                               pctl.player_volume / 100)

                    self.play_state = 0
                    pctl.playerSubCommand = "stopped"

                elif command == 'seek':
                    self.seek_timer.set()
                    if tauon.spot_ctl.coasting or tauon.spot_ctl.playing:
                        tauon.spot_ctl.control("seek",
                                               int(pctl.new_time * 1000))
                        pctl.playing_time = pctl.new_time

                    elif self.play_state > 0:
                        if not self.using_cache and pctl.target_object.is_network and \
                                not pctl.target_object.file_ext == "KOEL" and \
                                not pctl.target_object.file_ext == "SUB" and \
                                not pctl.target_object.file_ext == "JELY":

                            if not os.path.exists(tauon.temp_audio):
                                os.makedirs(tauon.temp_audio)

                            listing = os.listdir(tauon.temp_audio)
                            full = [
                                os.path.join(tauon.temp_audio, x)
                                for x in listing
                            ]
                            size = get_folder_size(tauon.temp_audio) / 1000000
                            print(f"Audio cache size is {size}MB")
                            if size > 120:
                                oldest_file = min(full, key=os.path.getctime)
                                print("Cache full, delete oldest cached file")
                                os.remove(oldest_file)

                            pctl.playing_time = 0
                            self.playbin.set_state(Gst.State.NULL)
                            self.dl_ready = False
                            url, params = self.urlparams
                            shoot_dl = threading.Thread(
                                target=self.download_part,
                                args=([
                                    url, self.save_temp, params,
                                    pctl.target_object.url_key
                                ]))
                            shoot_dl.daemon = True
                            shoot_dl.start()
                            pctl.playerCommand = ""
                            while not self.dl_ready:
                                # print("waiting...")
                                time.sleep(0.25)
                                if pctl.playerCommandReady and pctl.playerCommand != "seek":
                                    print("BREAK!")
                                    self.main_callback()
                                    return

                            self.playbin.set_property(
                                'uri', 'file://' + urllib.parse.quote(
                                    os.path.abspath(self.save_temp)))
                            #time.sleep(0.05)
                            self.playbin.set_state(Gst.State.PLAYING)
                            self.using_cache = True
                            time.sleep(0.1)

                        self.playbin.seek_simple(
                            Gst.Format.TIME,
                            Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                            (pctl.new_time + pctl.start_time_target) *
                            Gst.SECOND)

                        # It may take a moment for seeking to update when streaming, so for better UI feedback we'll
                        # update the seek indicator immediately and hold the thread for a moment
                        if pctl.target_object.is_network:
                            pctl.playing_time = pctl.new_time + pctl.start_time_target
                            pctl.decode_time = pctl.playing_time
                            time.sleep(0.25)

                elif command == 'pauseon':
                    self.player_timer.hit()
                    self.play_state = 2

                    if prefs.use_pause_fade:
                        success, current_time = self.playbin.query_position(
                            Gst.Format.TIME)
                        if success:
                            start = current_time + (150 / 1000 * Gst.SECOND)
                            end = current_time + (prefs.pause_fade_time /
                                                  1000 * Gst.SECOND)
                            self.c_source.set(start,
                                              (pctl.player_volume / 100) / 10)
                            self.c_source.set(end, 0.0)
                            time.sleep(prefs.pause_fade_time / 1000)
                            self.c_source.unset_all()

                    self.playbin.set_state(Gst.State.PAUSED)

                elif command == 'pauseoff':
                    self.player_timer.hit()

                    if not prefs.use_pause_fade:
                        self.playbin.set_state(Gst.State.PLAYING)

                    else:
                        self._vol.set_property("volume", 0.0)
                        success, current_time = self.playbin.query_position(
                            Gst.Format.TIME)
                        self.playbin.set_state(Gst.State.PLAYING)
                        if success:
                            start = current_time + (150 / 1000 * Gst.SECOND)
                            end = current_time + (
                                (prefs.pause_fade_time / 1000) * Gst.SECOND)
                            self.c_source.set(start, 0.0)
                            self.c_source.set(end,
                                              (pctl.player_volume / 100) / 10)
                            time.sleep(prefs.pause_fade_time / 1000)
                            time.sleep(0.05)
                            self.c_source.unset_all()

                    self._vol.set_property("volume", pctl.player_volume / 100)

                    self.play_state = 1

                elif command == 'unload':
                    if self.play_state > 0:
                        self.playbin.set_state(Gst.State.NULL)
                        time.sleep(0.05)
                    print("Unload GStreamer")
                    self.mainloop.quit()
                    pctl.playerCommand = 'done'
                    return

            if self.play_state == 3:
                if self.playbin.get_state(0).state == Gst.State.PLAYING:
                    add_time = self.player_timer.hit()
                    if add_time > 2:
                        add_time = 2
                    if add_time < 0:
                        add_time = 0
                    pctl.playing_time += add_time
                    pctl.decode_time = pctl.playing_time

            if self.play_state == 1:

                # Get jump in time since last call
                add_time = self.player_timer.hit()

                # Limit the jump.
                if add_time > 2:
                    add_time = 2
                if add_time < 0:
                    add_time = 0

                # # Progress main seek head
                if self.playbin.get_state(
                        0).state == Gst.State.PLAYING and self.seek_timer.get(
                        ) > 1 and not pctl.playerCommandReady:

                    pctl.playing_time += add_time

                    p = max(0,
                            (self.playbin.query_position(Gst.Format.TIME)[1] /
                             Gst.SECOND) - pctl.start_time_target)

                    if abs(pctl.playing_time - p) > 0.2:
                        pctl.playing_time = p

                    pctl.decode_time = p

                else:
                    # We're supposed to be playing but it's not? Give it a push I guess.
                    #self.playbin.set_state(Gst.State.PLAYING)
                    pctl.playing_time += add_time
                    pctl.decode_time = pctl.playing_time

                # Other things we need to progress such as scrobbling
                if pctl.playing_time < 3 and pctl.a_time < 3:
                    pctl.a_time = pctl.playing_time
                else:
                    pctl.a_time += add_time

                pctl.total_playtime += add_time
                lfm_scrobbler.update(
                    add_time
                )  # This handles other scrobblers such as listenbrainz also

                # Update track total playtime
                if len(pctl.track_queue) > 0 and 2 > add_time > 0:
                    star_store.add(pctl.track_queue[pctl.queue_step], add_time)

            if not pctl.running:
                # print("unloading gstreamer")
                if self.play_state > 0:
                    self.playbin.set_state(Gst.State.NULL)
                    time.sleep(0.5)

                self.mainloop.quit()
                pctl.playerCommand = 'done'

            else:
                if gui.vis == 1:
                    GLib.timeout_add(19, self.main_callback)
                else:
                    GLib.timeout_add(50, self.main_callback)

        def exit(self):
            print("GStreamer unloaded")
            pctl.playerCommand = 'done'
Esempio n. 3
0
    class GPlayer:
        def __init__(self):

            # This is used to keep track of time between callbacks to progress the seek bar
            self.player_timer = Timer()

            # This is used to keep note of what state of playing we should be in
            self.play_state = 0  # 0 is stopped, 1 is playing, 2 is paused

            # Initiate GSteamer
            Gst.init([])
            self.mainloop = GLib.MainLoop()

            # Create main "playbin" pipeline thingy for simple playback
            self.pl = Gst.ElementFactory.make("playbin", "player")

            # Set callback for the main callback loop
            GLib.timeout_add(500, self.main_callback)

            # self.pl.connect("about-to-finish", self.about_to_finish)

            self.mainloop.run()

        def check_duration(self):

            # This function is to be called when loading a track to query for a duration of track
            # in case the tagger failed to calculate a length for the track when imported.

            # Get current playing track object from player
            current_track = pctl.playing_object()

            if current_track is not None and current_track.length < 1:

                result = self.pl.query_duration(Gst.Format.TIME)

                if result[0] is True:
                    current_track.length = result[1] / Gst.SECOND

                else:  # still loading? I guess we wait and try again.
                    time.sleep(1.5)
                    result = self.pl.query_duration(Gst.Format.TIME)

                    if result[0] is True:
                        current_track.length = result[1] / Gst.SECOND

        def main_callback(self):

            # This is the main callback function to be triggered continuously as long as application is running

            pctl.test_progress(
            )  # This function triggers an advance if we are near end of track

            if pctl.playerCommandReady:
                pctl.playerCommandReady = False

                # Here we process commands from the main thread/module

                # Possible commands:

                # open: Start playback of a file
                #  (Path given by pctl.target_open at position pctl.start_time_target + pctl.jump_time)
                # stop: Stop playback (OK to unload file from memory)
                # runstop: Stop playback but let finish if we are near the end of the file (todo)
                # pauseon: Pause playback (be ready to resume)
                # pauseoff: Resume playback if paused
                # volume: Set (and remember) the volume specified by pctl.player_volume (0 to 100)
                # seek: Seek to position given by pctl.new_time + pctl.start_time (don't resume playback if paused)
                # url: Start playback of a shoutcast/icecast stream. URL specified by pctl.url (todo)
                # suspend: Pause and disconnect from output device (not used, playbin automatically does this)
                # unload: Cleanup and exit
                # done: Tell the main thread we finished doing a special request it was waiting for (such as unload)

                # Note that functions such as gapless playback are entirely implemented on this side.
                # We wont be told, we just guess when we need to do them and hold loop until we are done.
                # Advance will be called early for gapless, currently allotted 5 seconds (can we reduce this somehow?)
                # Concepts such as advance and back are not used on this side.

                # Todo: Visualisers
                # Uhhh, this is a bit of a can of worms. What we want to do is constantly get binned spectrum data
                # and pass it to the UI (in certain formats).
                # Specifically, current format used with BASS module is:
                # - An FFT of (a current segment of?) raw sample data
                # - Non-complex (magnitudes of the first half of the FFT are returned)
                # - 1024 samples (returns 512 values)
                # - Combined left and right channels (mono)
                # - Binned to particular numbers of bins and passed onto UI after some scaling and truncating
                # There's also a level meter which just takes peak "level" (scaled in someway perhaps)

                # Todo: User settings
                # prefs.use_transition_crossfade (if true, fade rather than transition gaplessly at end of file) todo
                # prefs.use_jump_crossfade (if true and not end of file, fade rather than switch instantly) todo
                # prefs.use_pause_fade (if true, fade when pausing, rather than pausing instantly) todo

                if pctl.playerCommand == 'open' and pctl.target_open != '':

                    # Check if the file exists, mark it as missing if not
                    if os.path.isfile(pctl.target_object.fullpath):
                        # File exists so continue
                        pctl.target_object.found = True
                    else:
                        # File does not exist, trigger an advance
                        pctl.target_object.found = False
                        print("Missing File: " + pctl.target_object.fullpath)
                        pctl.playing_state = 0
                        pctl.jump_time = 0
                        pctl.advance(inplace=True, nolock=True)
                        GLib.timeout_add(19, self.main_callback)
                        return

                    gapless = False
                    current_time = 0
                    current_duration = 0

                    if self.play_state != 0:
                        # Determine time position of currently playing track
                        current_time = self.pl.query_position(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        current_duration = self.pl.query_duration(
                            Gst.Format.TIME)[1] / Gst.SECOND
                        print("We are " +
                              str(current_duration - current_time) +
                              " seconds from end.")

                    # If we are close to the end of the track, try transition gaplessly
                    if self.play_state == 1 and pctl.start_time_target == 0 and pctl.jump_time == 0 and \
                            0.2 < current_duration - current_time < 5.5:
                        print("Use GStreamer Gapless transition")
                        gapless = True

                    # If we are not supposed to be playing, stop (crossfade todo)
                    else:
                        self.pl.set_state(Gst.State.READY)

                    self.play_state = 1
                    self.pl.set_property(
                        'uri', 'file://' +
                        urllib.parse.quote(os.path.abspath(pctl.target_open)))
                    self.pl.set_property('volume', pctl.player_volume / 100)
                    self.pl.set_state(Gst.State.PLAYING)
                    if pctl.jump_time == 0:
                        pctl.playing_time = 0

                    time.sleep(
                        0.1
                    )  # Setting and querying position right away seems to fail, so wait a small moment

                    # The position to start is not always the beginning of the file, so seek to position
                    if pctl.start_time_target > 0 or pctl.jump_time > 0:
                        self.pl.seek_simple(
                            Gst.Format.TIME,
                            Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                            (pctl.start_time_target + pctl.jump_time) *
                            Gst.SECOND)
                        pctl.playing_time = 0
                        gui.update = 1

                    if gapless:  # Hold thread while a gapless transition is in progress
                        t = 0
                        while self.pl.query_position(Gst.Format.TIME)[
                                1] / Gst.SECOND >= current_time > 0:
                            time.sleep(0.1)
                            t += 1

                            if self.pl.get_state(0).state != Gst.State.PLAYING:
                                break

                            if t > 40:
                                print("Gonna stop waiting..."
                                      )  # Cant wait forever
                                break

                    pctl.jump_time = 0
                    time.sleep(0.15)
                    self.check_duration()

                    self.player_timer.hit()

                # elif pctl.playerCommand == 'url': (todo)
                #
                #    # Stop if playing or paused
                #    if self.play_state == 1 or self.play_state == 2:
                #        self.pl.set_state(Gst.State.NULL)
                #
                #    # Open URL stream
                #    self.pl.set_property('uri', pctl.url)
                #    self.pl.set_property('volume', pctl.player_volume / 100)
                #    self.pl.set_state(Gst.State.PLAYING)
                #    self.play_state = 3
                #    self.player_timer.hit()

                elif pctl.playerCommand == 'volume':
                    if self.play_state == 1:
                        self.pl.set_property('volume',
                                             pctl.player_volume / 100)

                elif pctl.playerCommand == 'stop' or pctl.playerCommand == 'runstop':
                    if self.play_state > 0:
                        self.pl.set_state(Gst.State.READY)
                    self.play_state = 0
                    pctl.playerCommand = "stopped"

                elif pctl.playerCommand == 'seek':
                    if self.play_state > 0:
                        self.pl.seek_simple(
                            Gst.Format.TIME,
                            Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                            (pctl.new_time + pctl.start_time_target) *
                            Gst.SECOND)

                elif pctl.playerCommand == 'pauseon':
                    self.player_timer.hit()
                    self.play_state = 2
                    self.pl.set_state(Gst.State.PAUSED)

                elif pctl.playerCommand == 'pauseoff':
                    self.player_timer.hit()
                    self.pl.set_state(Gst.State.PLAYING)
                    self.play_state = 1

                elif pctl.playerCommand == 'unload':
                    if self.play_state > 0:
                        self.pl.set_state(Gst.State.NULL)
                        time.sleep(0.5)

                    self.mainloop.quit()
                    pctl.playerCommand = 'done'
                    return

            if self.play_state == 1:

                # Get jump in time since last call
                add_time = self.player_timer.hit()

                # Limit the jump. Timer is monotonic, but we'll double check, just in case.
                if add_time > 2:
                    add_time = 2
                if add_time < 0:
                    add_time = 0

                # Progress main seek head
                if self.pl.get_state(0).state == Gst.State.PLAYING:
                    pctl.playing_time = max(
                        0, (self.pl.query_position(Gst.Format.TIME)[1] /
                            Gst.SECOND) - pctl.start_time_target)
                    pctl.decode_time = pctl.playing_time  # A difference isn't discerned in this module

                else:
                    # We're supposed to be playing but it's not? Give it a push I guess.
                    self.pl.set_state(Gst.State.PLAYING)
                    pctl.playing_time += add_time
                    pctl.decode_time = pctl.playing_time

                # Other things we need to progress such as scrobbling
                if pctl.playing_time < 3 and pctl.a_time < 3:
                    pctl.a_time = pctl.playing_time
                else:
                    pctl.a_time += add_time

                pctl.total_playtime += add_time
                lfm_scrobbler.update(
                    add_time
                )  # This handles other scrobblers such as listenbrainz also

                # Update track total playtime
                if len(pctl.track_queue) > 0 and 2 > add_time > 0:
                    star_store.add(pctl.track_queue[pctl.queue_step], add_time)

            if not pctl.running:
                print("unloading gstreamer")
                if self.play_state > 0:
                    self.pl.set_state(Gst.State.NULL)
                    time.sleep(0.5)

                self.mainloop.quit()
                pctl.playerCommand = 'done'

            else:
                GLib.timeout_add(19, self.main_callback)

        def exit(self):
            pctl.playerCommand = 'done'