[gnome-music/wip/mschraal/gapless-v3: 27/29] gstplayer: Enable gapless playback



commit 1eaff88b6dc4a418efcb9092419096040b123102
Author: Marinus Schraal <mschraal gnome org>
Date:   Sat May 4 12:18:15 2019 +0200

    gstplayer: Enable gapless playback
    
    Enable gapless playback in GstPlayer by listening to the 'about-to-finish'
    signal of playbin and handling it in Player. The active playlist is now
    played as one continous stream, so start listening to 'stream-start' on the
    playbin bus as well as an indicator of a new song starting.
    
    A few workarounds are in place to ensure a smooth experience.
    
    1. The 'eos' signal is in theory only sent at the end of the stream
    (playlist). In practice it is possible to trigger both 'about-to-finish'
    and 'eos' if seeking to the end of a song. Add _gapless_set to track if
    'about-to-finish' has fired.
    
    2. On 'stream-start' the new duration is not yet set. Introduce a slight
    delay before processing further to have GStreamer update the new duration.
    
    3. GstPlayer "clock-tick" is reset on 'stream-start', as the pipeline clock
    is now ticking for the full duration of the pipeline.

 gnomemusic/gstplayer.py | 32 +++++++++++++++++++++++++++-----
 gnomemusic/player.py    | 37 +++++++++++++++++++++++++++++++++----
 2 files changed, 60 insertions(+), 9 deletions(-)
---
diff --git a/gnomemusic/gstplayer.py b/gnomemusic/gstplayer.py
index 2265ae7a..f53cc142 100644
--- a/gnomemusic/gstplayer.py
+++ b/gnomemusic/gstplayer.py
@@ -29,7 +29,8 @@ from gettext import gettext as _, ngettext
 import gi
 gi.require_version('Gst', '1.0')
 gi.require_version('GstPbutils', '1.0')
-from gi.repository import Gtk, Gio, GObject, Gst, GstPbutils
+from gi.repository import GLib, Gtk, Gio, GObject, Gst, GstPbutils
+
 
 from gnomemusic import log
 
@@ -50,9 +51,11 @@ class GstPlayer(GObject.GObject):
     Handles GStreamer interaction for Player and SmoothScale.
     """
     __gsignals__ = {
+        "about-to-finish": (GObject.SignalFlags.RUN_FIRST, None, ()),
+        "clock-tick": (GObject.SignalFlags.RUN_FIRST, None, (int, )),
         'eos': (GObject.SignalFlags.RUN_FIRST, None, ()),
-        'clock-tick': (GObject.SignalFlags.RUN_FIRST, None, (int, )),
-        'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, ())
+        'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, ()),
+        "stream-start": (GObject.SignalFlags.RUN_FIRST, None, ())
     }
 
     def __repr__(self):
@@ -70,6 +73,7 @@ class GstPlayer(GObject.GObject):
 
         self._application = application
         self._duration = -1.
+        self._tick = 0
 
         self._missing_plugin_messages = []
         self._settings = application.props.settings
@@ -90,6 +94,9 @@ class GstPlayer(GObject.GObject):
         self._bus.connect('message::reset-time', self._on_reset_time)
         self._bus.connect('message::eos', self._on_bus_eos)
         self._bus.connect('message::new-clock', self._on_new_clock)
+        self._bus.connect("message::stream-start", self._on_bus_stream_start)
+
+        self._player.connect("about-to-finish", self._on_about_to_finish)
 
         self.props.state = Playback.STOPPED
 
@@ -125,6 +132,10 @@ class GstPlayer(GObject.GObject):
         else:
             self._player.set_property("audio-filter", None)
 
+    @log
+    def _on_about_to_finish(self, klass):
+        self.emit("about-to-finish")
+
     @log
     def _on_async_done(self, bus, message):
         success, duration = self._player.query_duration(
@@ -149,14 +160,25 @@ class GstPlayer(GObject.GObject):
 
     @log
     def _on_clock_tick(self, clock, time, id, data):
-        tick = time / Gst.SECOND
-        self.emit('clock-tick', tick)
+        self.emit("clock-tick", self._tick)
+        self._tick += 1
 
     @log
     def _on_bus_element(self, bus, message):
         if GstPbutils.is_missing_plugin_message(message):
             self._missing_plugin_messages.append(message)
 
+    @log
+    def _on_bus_stream_start(self, bus, message):
+        def delayed_query():
+            self._on_async_done(None, None)
+            self._tick = 0
+            self.emit("stream-start")
+
+        # Delay the signalling slightly or the new duration will not
+        # have been set yet.
+        GLib.timeout_add(1, delayed_query)
+
     @log
     def _on_bus_error(self, bus, message):
         if self._is_missing_plugin_message(message):
diff --git a/gnomemusic/player.py b/gnomemusic/player.py
index 3e42d2aa..4dc6f49a 100644
--- a/gnomemusic/player.py
+++ b/gnomemusic/player.py
@@ -562,6 +562,14 @@ class Player(GObject.GObject):
         """
         super().__init__()
 
+        # In the case of gapless playback, both 'about-to-finish'
+        # and 'eos' can occur during the same stream. 'about-to-finish'
+        # already sets self._playlist to the next song, so doing it
+        # again on eos would skip a song.
+        # TODO: Improve playlist handling so this hack is no longer
+        # needed.
+        self._gapless_set = False
+
         self._playlist = PlayerPlaylist()
         self._playlist.connect('song-validated', self._on_song_validated)
 
@@ -577,9 +585,11 @@ class Player(GObject.GObject):
         self._new_clock = True
 
         self._gst_player = GstPlayer(application)
+        self._gst_player.connect("about-to-finish", self._on_about_to_finish)
         self._gst_player.connect('clock-tick', self._on_clock_tick)
         self._gst_player.connect('eos', self._on_eos)
         self._gst_player.connect('seek-finished', self._on_seek_finished)
+        self._gst_player.connect("stream-start", self._on_stream_start)
         self._gst_player.bind_property(
             'duration', self, 'duration', GObject.BindingFlags.SYNC_CREATE)
         self._gst_player.bind_property(
@@ -624,15 +634,33 @@ class Player(GObject.GObject):
 
         self._gst_player.props.url = song.get_url()
 
-        self.emit('song-changed')
+    @log
+    def _on_about_to_finish(self, klass):
+        if self.props.has_next:
+            self._playlist.next()
+
+            new_url = self._playlist.props.current_song.get_url()
+            self._gst_player.props.url = new_url
+            self._gapless_set = True
 
     @log
     def _on_eos(self, klass):
-        if self.props.has_next:
-            self.next()
+        if self._gapless_set:
+            # After 'eos' in the gapless case, the pipeline needs to be
+            # hard reset.
+            self.stop()
+            self.play()
         else:
             self.stop()
 
+        self._gapless_set = False
+
+    def _on_stream_start(self, klass):
+        self._gapless_set = False
+        self._time_stamp = int(time.time())
+
+        self.emit("song-changed")
+
     @log
     def play(self, song_changed=True, song_offset=None):
         """Play a song.
@@ -650,7 +678,8 @@ class Player(GObject.GObject):
                 and not self._playlist.set_song(song_offset)):
             return False
 
-        if song_changed is True:
+        if (song_changed
+                or self._gapless_set):
             self._load(self._playlist.props.current_song)
 
         self._gst_player.props.state = Playback.PLAYING


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]