[banshee/gapless] Patch 147845 from bgo#440952



commit a1164904a0c1e13efd1e3306f8eeaea7dd71532c
Author: Christopher Halse Rogers <chalserogers gmail com>
Date:   Wed Nov 18 11:30:55 2009 +1100

    Patch 147845 from bgo#440952
    
    Signed-off-by: Alexander Kojevnikov <alexander kojevnikov com>

 libbanshee/banshee-player-pipeline.c               |  107 ++++++++++++++++----
 libbanshee/banshee-player-private.h                |   14 +++
 libbanshee/banshee-player-video.c                  |   16 ++--
 libbanshee/banshee-player.c                        |   40 +++++++
 .../Banshee.GStreamer/PlayerEngine.cs              |  104 +++++++++++++++++++
 .../Banshee.MediaEngine/PlayerEngine.cs            |   50 +++++++++
 .../Banshee.MediaEngine/PlayerEngineService.cs     |   85 ++++++++++++----
 .../Banshee.MediaEngine/PlayerEvent.cs             |    3 +-
 .../IBasicPlaybackController.cs                    |    2 +-
 .../PlaybackControllerService.cs                   |   84 +++++++++-------
 .../Banshee.InternetRadio/InternetRadioSource.cs   |   14 +++-
 .../Banshee.Lastfm.Radio/StationSource.cs          |   21 +++--
 .../Banshee.PlayQueue/PlayQueueSource.cs           |   15 ++-
 13 files changed, 460 insertions(+), 95 deletions(-)
---
diff --git a/libbanshee/banshee-player-pipeline.c b/libbanshee/banshee-player-pipeline.c
index d246c92..9014a55 100644
--- a/libbanshee/banshee-player-pipeline.c
+++ b/libbanshee/banshee-player-pipeline.c
@@ -62,6 +62,38 @@ bp_pipeline_process_tag (const GstTagList *tag_list, const gchar *tag_name, Bans
     }
 }
 
+void bp_try_set_audiosink_buffer (GstElement *element, guint64 *buffer_len)
+{
+    if (g_object_class_find_property (G_OBJECT_GET_CLASS (element), "buffer-time")) {
+        g_object_set (G_OBJECT (element), "buffer-time", *buffer_len, NULL);
+    }
+    gst_object_unref (G_OBJECT (element));
+}
+
+static void
+bp_set_audiosink_buffer_length (BansheePlayer *player, guint64 buffer_len)
+{
+    GstIterator *sink_iterator;
+    GstIteratorResult iter_result;
+    GstElement *audiosink;
+
+    g_return_if_fail (IS_BANSHEE_PLAYER (player));
+    audiosink = player->audiosink;
+    // If we've directly selected alsasink or directsoundsink then we'll have a
+    // buffer-time property on audiosink.  If so, just set this and return.
+    if (g_object_class_find_property (G_OBJECT_GET_CLASS (audiosink), "buffer-time")) {
+        g_object_set (G_OBJECT (audiosink), "buffer-time", buffer_len, NULL);
+        return;
+    }
+    // If we've selected an auto audio sink, then the real audio sink is a child of the bin.
+    sink_iterator = gst_bin_iterate_recurse (GST_BIN (audiosink));
+    iter_result = gst_iterator_foreach (sink_iterator, (GFunc)&bp_try_set_audiosink_buffer, &buffer_len);
+    if (iter_result == GST_ITERATOR_ERROR) {
+        bp_debug ("Failed to set audio buffer length: failed to find sink");
+    }
+    gst_iterator_free (sink_iterator);
+}
+
 static gboolean
 bp_pipeline_bus_callback (GstBus *bus, GstMessage *message, gpointer userdata)
 {
@@ -81,7 +113,16 @@ bp_pipeline_bus_callback (GstBus *bus, GstMessage *message, gpointer userdata)
         case GST_MESSAGE_STATE_CHANGED: {
             GstState old, new, pending;
             gst_message_parse_state_changed (message, &old, &new, &pending);
-            
+
+            if (new == GST_STATE_READY) {
+                // Explicitly set the buffer length.
+                // We need to know the buffer length so that we can emit a fake track-changed signal at something
+                // approximating the right time, at least until playbin2 can do this for us.
+                // The easiest way to do this is to set the buffer to a known length.
+                //
+                // gconfaudiosink only has a real audiosink in it once it's made a transition to READY.
+                bp_set_audiosink_buffer_length (player, BP_BUFFER_LEN_MICROSECONDS);
+            }
             _bp_missing_elements_handle_state_changed (player, old, new);
             _bp_replaygain_handle_state_changed (player, old, new, pending);
             
@@ -171,6 +212,33 @@ bp_pipeline_bus_callback (GstBus *bus, GstMessage *message, gpointer userdata)
     return TRUE;
 }
 
+static gboolean
+bp_next_track_starting (gpointer player)
+{
+    g_return_val_if_fail (IS_BANSHEE_PLAYER (player), FALSE);
+
+    bp_debug ("[gapless] Triggering track-change signal");
+    if (((BansheePlayer *)player)->next_track_starting_cb != NULL) {
+        ((BansheePlayer *)player)->next_track_starting_cb (player);
+    }
+    ((BansheePlayer *)player)->next_track_starting_timer_id = 0;
+    return FALSE;
+}
+
+static void bp_about_to_finish_callback (GstElement *playbin, BansheePlayer *player)
+{
+    g_return_if_fail (IS_BANSHEE_PLAYER (player));
+    // Playbin2 doesn't (yet) have any way to notify us when the current track has actually finished playing on the
+    // hardware.  Fake this for now by adding a timer with length equal to the hardware buffer.
+    player->next_track_starting_timer_id = g_timeout_add (((guint64)BP_BUFFER_LEN_MICROSECONDS) / 1000,
+                                                          &bp_next_track_starting,
+                                                          player);
+
+    if (player->about_to_finish_cb != NULL) {
+        player->about_to_finish_cb (player);
+    }
+}
+
 // ---------------------------------------------------------------------------
 // Internal Functions
 // ---------------------------------------------------------------------------
@@ -180,7 +248,6 @@ _bp_pipeline_construct (BansheePlayer *player)
 {
     GstBus *bus;
     GstPad *teepad;
-    GstElement *audiosink;
     GstElement *audiosinkqueue;
     GstElement *eq_audioconvert = NULL;
     GstElement *eq_audioconvert2 = NULL;
@@ -189,31 +256,35 @@ _bp_pipeline_construct (BansheePlayer *player)
     
     // Playbin is the core element that handles autoplugging (finding the right
     // source and decoder elements) based on source URI and stream content
-    player->playbin = gst_element_factory_make ("playbin", "playbin");
+    player->playbin = gst_element_factory_make ("playbin2", "playbin");
     g_return_val_if_fail (player->playbin != NULL, FALSE);
 
+    // Connect a proxy about-to-finish callback that will generate a next-track-starting callback.
+    // This can be removed once playbin2 generates its own next-track signal.
+    g_signal_connect (player->playbin, "about-to-finish", G_CALLBACK (bp_about_to_finish_callback), player);
+
     // Try to find an audio sink, prefer gconf, which typically is set to auto these days,
     // fall back on auto, which should work on windows, and as a last ditch, try alsa
-    audiosink = gst_element_factory_make ("gconfaudiosink", "audiosink");
-    if (audiosink == NULL) {
-        audiosink = gst_element_factory_make ("directsoundsink", "audiosink");
-        if (audiosink != NULL) {
-            g_object_set (G_OBJECT (audiosink), "volume", 1.0, NULL);
+    player->audiosink = gst_element_factory_make ("gconfaudiosink", "audiosink");
+    if (player->audiosink == NULL) {
+        player->audiosink = gst_element_factory_make ("directsoundsink", "audiosink");
+        if (player->audiosink != NULL) {
+            g_object_set (G_OBJECT (player->audiosink), "volume", 1.0, NULL);
         } else {
-            audiosink = gst_element_factory_make ("autoaudiosink", "audiosink");
-            if (audiosink == NULL) {
-                audiosink = gst_element_factory_make ("alsasink", "audiosink");
+            player->audiosink = gst_element_factory_make ("autoaudiosink", "audiosink");
+            if (player->audiosink == NULL) {
+                player->audiosink = gst_element_factory_make ("alsasink", "audiosink");
             }
         }
     }
     
-    g_return_val_if_fail (audiosink != NULL, FALSE);
+    g_return_val_if_fail (player->audiosink != NULL, FALSE);
         
     // Set the profile to "music and movies" (gst-plugins-good 0.10.3)
-    if (g_object_class_find_property (G_OBJECT_GET_CLASS (audiosink), "profile")) {
-        g_object_set (G_OBJECT (audiosink), "profile", 1, NULL);
+    if (g_object_class_find_property (G_OBJECT_GET_CLASS (player->audiosink), "profile")) {
+        g_object_set (G_OBJECT (player->audiosink), "profile", 1, NULL);
     }
-    
+
     // Create a custom audio sink bin that will hold the real primary sink
     player->audiobin = gst_bin_new ("audiobin");
     g_return_val_if_fail (player->audiobin != NULL, FALSE);
@@ -244,7 +315,7 @@ _bp_pipeline_construct (BansheePlayer *player)
     }
     
     gst_bin_add (GST_BIN (player->audiobin), audiosinkqueue);
-    gst_bin_add (GST_BIN (player->audiobin), audiosink);
+    gst_bin_add (GST_BIN (player->audiobin), player->audiosink);
    
     // Ghost pad the audio bin so audio is passed from the bin into the tee
     teepad = gst_element_get_pad (player->audiotee, "sink");
@@ -255,10 +326,10 @@ _bp_pipeline_construct (BansheePlayer *player)
     if (player->equalizer != NULL) {
         // link in equalizer, preamp and audioconvert.
         gst_element_link_many (audiosinkqueue, eq_audioconvert, player->preamp, 
-            player->equalizer, eq_audioconvert2, audiosink, NULL);
+            player->equalizer, eq_audioconvert2, player->audiosink, NULL);
     } else {
         // link the queue with the real audio sink
-        gst_element_link (audiosinkqueue, audiosink);
+        gst_element_link (audiosinkqueue, player->audiosink);
     }
     
     _bp_vis_pipeline_setup (player);
diff --git a/libbanshee/banshee-player-private.h b/libbanshee/banshee-player-private.h
index 5cbaa40..58cdb8d 100644
--- a/libbanshee/banshee-player-private.h
+++ b/libbanshee/banshee-player-private.h
@@ -68,6 +68,8 @@
 #define bp_debug(x...) banshee_log_debug ("player", x)
 #endif
 
+#define BP_BUFFER_LEN_MICROSECONDS 1000000
+
 typedef struct BansheePlayer BansheePlayer;
 
 typedef void (* BansheePlayerEosCallback)          (BansheePlayer *player);
@@ -79,6 +81,8 @@ typedef void (* BansheePlayerIterateCallback)      (BansheePlayer *player);
 typedef void (* BansheePlayerBufferingCallback)    (BansheePlayer *player, gint buffering_progress);
 typedef void (* BansheePlayerTagFoundCallback)     (BansheePlayer *player, const gchar *tag, const GValue *value);
 typedef void (* BansheePlayerVisDataCallback)      (BansheePlayer *player, gint channels, gint samples, gfloat *data, gint bands, gfloat *spectrum);
+typedef void (* BansheePlayerNextTrackStartingCallback)     (BansheePlayer *player);
+typedef void (* BansheePlayerAboutToFinishCallback)         (BansheePlayer *player);
 typedef GstElement * (* BansheePlayerVideoPipelineSetupCallback) (BansheePlayer *player, GstBus *bus);
 
 typedef enum {
@@ -96,14 +100,18 @@ struct BansheePlayer {
     BansheePlayerBufferingCallback buffering_cb;
     BansheePlayerTagFoundCallback tag_found_cb;
     BansheePlayerVisDataCallback vis_data_cb;
+    BansheePlayerNextTrackStartingCallback next_track_starting_cb;
+    BansheePlayerAboutToFinishCallback about_to_finish_cb;
     BansheePlayerVideoPipelineSetupCallback video_pipeline_setup_cb;
 
     // Pipeline Elements
     GstElement *playbin;
+    GstElement *audiosink;
     GstElement *audiotee;
     GstElement *audiobin;
     GstElement *equalizer;
     GstElement *preamp;
+    GstElement *videosink;
     gint equalizer_status;
     gdouble current_volume;
     
@@ -156,6 +164,12 @@ struct BansheePlayer {
     gdouble album_peak;
     gdouble track_gain;
     gdouble track_peak;
+
+    // Work around playbin2 not giving any notification about when a
+    // track changes.  We know how long playbin's buffer is, so we know
+    // how long after about-to-finish is raised the track will end.
+    // Use this timer to fire a signal when that happens.
+    guint next_track_starting_timer_id;
 };
 
 #endif /* _BANSHEE_PLAYER_PRIVATE_H */
diff --git a/libbanshee/banshee-player-video.c b/libbanshee/banshee-player-video.c
index 21bc0ce..31a4b4d 100644
--- a/libbanshee/banshee-player-video.c
+++ b/libbanshee/banshee-player-video.c
@@ -37,15 +37,14 @@
 static gboolean
 bp_video_find_xoverlay (BansheePlayer *player)
 {
-    GstElement *video_sink = NULL;
     GstElement *xoverlay;
     GstXOverlay *previous_xoverlay;
 
+    g_return_val_if_fail (IS_BANSHEE_PLAYER (player), FALSE);
+
     previous_xoverlay = player->xoverlay;
     
-    g_object_get (player->playbin, "video-sink", &video_sink, NULL);
-    
-    if (video_sink == NULL) {
+    if (player->videosink == NULL) {
         player->xoverlay = NULL;
         if (previous_xoverlay != NULL) {
             gst_object_unref (previous_xoverlay);
@@ -54,9 +53,9 @@ bp_video_find_xoverlay (BansheePlayer *player)
         return FALSE;
     }
     
-    xoverlay = GST_IS_BIN (video_sink)
-        ? gst_bin_get_by_interface (GST_BIN (video_sink), GST_TYPE_X_OVERLAY)
-        : video_sink;
+    xoverlay = GST_IS_BIN (player->videosink)
+        ? gst_bin_get_by_interface (GST_BIN (player->videosink), GST_TYPE_X_OVERLAY)
+        : player->videosink;
     
     player->xoverlay = GST_IS_X_OVERLAY (xoverlay) ? GST_X_OVERLAY (xoverlay) : NULL;
     
@@ -74,8 +73,6 @@ bp_video_find_xoverlay (BansheePlayer *player)
         g_object_set (G_OBJECT (player->xoverlay), "handle-events", FALSE, NULL);
     }
 
-    gst_object_unref (video_sink);
-
     return player->xoverlay != NULL;
 }
 
@@ -178,6 +175,7 @@ _bp_video_pipeline_setup (BansheePlayer *player, GstBus *bus)
     #endif
     
     #endif
+    player->videosink = videosink;
 }
 
 P_INVOKE void
diff --git a/libbanshee/banshee-player.c b/libbanshee/banshee-player.c
index 4052d06..f2253ba 100644
--- a/libbanshee/banshee-player.c
+++ b/libbanshee/banshee-player.c
@@ -180,6 +180,11 @@ bp_stop (BansheePlayer *player, gboolean nullstate)
         state = GST_STATE_NULL;
     }
     
+    if (player->next_track_starting_timer_id != 0) {
+        g_source_remove (player->next_track_starting_timer_id);
+        player->next_track_starting_timer_id = 0;
+    }
+
     bp_debug ("bp_stop: setting state to %s", 
         state == GST_STATE_NULL ? "GST_STATE_NULL" : "GST_STATE_PAUSED");
     
@@ -195,10 +200,33 @@ bp_pause (BansheePlayer *player)
 P_INVOKE void
 bp_play (BansheePlayer *player)
 {
+    g_return_if_fail (IS_BANSHEE_PLAYER (player));
+    if (player->next_track_starting_timer_id != 0) {
+        g_source_remove (player->next_track_starting_timer_id);
+        player->next_track_starting_timer_id = 0;
+    }
     bp_pipeline_set_state (player, GST_STATE_PLAYING);
 }
 
 P_INVOKE gboolean
+bp_set_next_track (BansheePlayer *player, const gchar *uri)
+{
+    g_return_val_if_fail (IS_BANSHEE_PLAYER (player), FALSE);
+    g_return_val_if_fail (player->playbin != NULL, FALSE);
+    if (uri == NULL && player->next_track_starting_timer_id != 0) {
+        // URI == NULL indicates that there is not a next track to play.
+        // This means that there will not be a next track *starting*, so
+        // we have to disable the timer so that we don't fire a spurious
+        // next-track-starting signal.
+        g_source_remove (player->next_track_starting_timer_id);
+        player->next_track_starting_timer_id = 0;
+        return TRUE;
+    }
+    g_object_set (G_OBJECT (player->playbin), "uri", uri, NULL);
+    return TRUE;
+}
+
+P_INVOKE gboolean
 bp_set_position (BansheePlayer *player, guint64 time_ms)
 {
     g_return_val_if_fail (IS_BANSHEE_PLAYER (player), FALSE);
@@ -348,3 +376,15 @@ bp_get_error_quarks (GQuark *core, GQuark *library, GQuark *resource, GQuark *st
     *resource = GST_RESOURCE_ERROR;
     *stream = GST_STREAM_ERROR;
 }
+
+P_INVOKE void
+bp_set_next_track_starting_callback (BansheePlayer *player, BansheePlayerNextTrackStartingCallback cb)
+{
+    SET_CALLBACK (next_track_starting_cb);
+}
+
+P_INVOKE void
+bp_set_about_to_finish_callback (BansheePlayer *player, BansheePlayerAboutToFinishCallback cb)
+{
+    SET_CALLBACK (about_to_finish_cb);
+}
diff --git a/src/Backends/Banshee.GStreamer/Banshee.GStreamer/PlayerEngine.cs b/src/Backends/Banshee.GStreamer/Banshee.GStreamer/PlayerEngine.cs
index 3193187..6350405 100644
--- a/src/Backends/Banshee.GStreamer/Banshee.GStreamer/PlayerEngine.cs
+++ b/src/Backends/Banshee.GStreamer/Banshee.GStreamer/PlayerEngine.cs
@@ -29,6 +29,7 @@
 using System;
 using System.Collections;
 using System.Runtime.InteropServices;
+using System.Threading;
 using Mono.Unix;
 using Hyena;
 using Hyena.Data;
@@ -57,6 +58,8 @@ namespace Banshee.GStreamer
     internal delegate void BansheePlayerIterateCallback (IntPtr player);
     internal delegate void BansheePlayerBufferingCallback (IntPtr player, int buffering_progress);
     internal delegate void BansheePlayerVisDataCallback (IntPtr player, int channels, int samples, IntPtr data, int bands, IntPtr spectrum);
+    internal delegate void BansheePlayerNextTrackStartingCallback (IntPtr player);
+    internal delegate void BansheePlayerAboutToFinishCallback (IntPtr player);
     internal delegate IntPtr VideoPipelineSetupHandler (IntPtr player, IntPtr bus);
 
     internal delegate void GstTaggerTagFoundCallback (IntPtr player, string tagName, ref GLib.Value value);
@@ -79,11 +82,16 @@ namespace Banshee.GStreamer
         private BansheePlayerVisDataCallback vis_data_callback;
         private VideoPipelineSetupHandler video_pipeline_setup_callback;
         private GstTaggerTagFoundCallback tag_found_callback;
+        private BansheePlayerNextTrackStartingCallback next_track_starting_callback;
+        private BansheePlayerAboutToFinishCallback about_to_finish_callback;
 
         private bool buffering_finished;
         private int pending_volume = -1;
         private bool xid_is_set = false;
 
+        private bool gapless_enabled;
+        private EventWaitHandle next_track_set;
+
         private event VisualizationDataHandler data_available = null;
         public event VisualizationDataHandler DataAvailable {
             add {
@@ -135,6 +143,8 @@ namespace Banshee.GStreamer
             vis_data_callback = new BansheePlayerVisDataCallback (OnVisualizationData);
             video_pipeline_setup_callback = new VideoPipelineSetupHandler (OnVideoPipelineSetup);
             tag_found_callback = new GstTaggerTagFoundCallback (OnTagFound);
+            next_track_starting_callback = new BansheePlayerNextTrackStartingCallback (OnNextTrackStarting);
+            about_to_finish_callback = new BansheePlayerAboutToFinishCallback (OnAboutToFinish);
 
             bp_set_eos_callback (handle, eos_callback);
             bp_set_iterate_callback (handle, iterate_callback);
@@ -142,7 +152,10 @@ namespace Banshee.GStreamer
             bp_set_state_changed_callback (handle, state_changed_callback);
             bp_set_buffering_callback (handle, buffering_callback);
             bp_set_tag_found_callback (handle, tag_found_callback);
+            bp_set_next_track_starting_callback (handle, next_track_starting_callback);
             bp_set_video_pipeline_setup_callback (handle, video_pipeline_setup_callback);
+
+            next_track_set = new EventWaitHandle (false, EventResetMode.AutoReset);
         }
 
         protected override void Initialize ()
@@ -161,6 +174,7 @@ namespace Banshee.GStreamer
 
             InstallPreferences ();
             ReplayGainEnabled = ReplayGainEnabledSchema.Get ();
+            GaplessEnabled = GaplessEnabledSchema.Get ();
         }
 
         public override void Dispose ()
@@ -210,6 +224,23 @@ namespace Banshee.GStreamer
             bp_pause (handle);
         }
 
+        public override void SetNextTrackUri (SafeUri uri)
+        {
+            // If there isn't a next track for us, release the block on the about-to-finish callback.
+            if (uri == null) {
+                next_track_set.Set ();
+                bp_set_next_track (handle, IntPtr.Zero);
+                return;
+            }
+            IntPtr uri_ptr = GLib.Marshaller.StringToPtrGStrdup (uri.AbsoluteUri);
+            try {
+                bp_set_next_track (handle, uri_ptr);
+            } finally {
+                GLib.Marshaller.Free (uri_ptr);
+            }
+            next_track_set.Set ();
+        }
+
         public override void VideoExpose (IntPtr window, bool direct)
         {
             bp_video_window_expose (handle, window, direct);
@@ -230,6 +261,37 @@ namespace Banshee.GStreamer
         {
             Close (false);
             OnEventChanged (PlayerEvent.EndOfStream);
+            if (!GaplessEnabled) {
+                OnEventChanged (PlayerEvent.RequestNextTrack);
+            }
+        }
+
+        private void OnNextTrackStarting (IntPtr player)
+        {
+            if (GaplessEnabled) {
+                OnEventChanged (PlayerEvent.EndOfStream);
+                OnEventChanged (PlayerEvent.StartOfStream);
+            }
+        }
+
+        private void OnAboutToFinish (IntPtr player)
+        {
+            // This is needed to make Shuffle-by-* work.
+            // Shuffle-by-* uses the LastPlayed field to determine what track in the grouping to play next.
+            // Therefore, we need to update this before requesting the next track.
+            //
+            // This will be overridden by IncrementLastPlayed () called by
+            // PlaybackControllerService's EndOfStream handler.
+            CurrentTrack.LastPlayed = DateTime.Now;
+            CurrentTrack.Save ();
+
+            OnEventChanged (PlayerEvent.RequestNextTrack);
+            // Gapless playback with Playbin2 requires that the about-to-finish callback does not return until
+            // the next uri has been set.  Block here for a second or until the RequestNextTrack event has
+            // finished triggering.
+            if (!next_track_set.WaitOne (1000, false)) {
+                Log.Debug ("[Gapless] Timed out while waiting for next_track_set to be raised");
+            }
         }
 
         private void OnIterate (IntPtr player)
@@ -489,6 +551,20 @@ namespace Banshee.GStreamer
             set { bp_replaygain_set_enabled (handle, value); }
         }
 
+        private bool GaplessEnabled {
+            get { return gapless_enabled; }
+            set
+            {
+                gapless_enabled = value;
+                if (value) {
+                    bp_set_about_to_finish_callback (handle, about_to_finish_callback);
+                } else {
+                    bp_set_about_to_finish_callback (handle, null);
+                }
+            }
+        }
+
+
 #region ISupportClutter
 
         private IntPtr clutter_video_sink;
@@ -515,6 +591,7 @@ namespace Banshee.GStreamer
             }
         }
 
+
         private IntPtr OnVideoPipelineSetup (IntPtr player, IntPtr bus)
         {
             try {
@@ -543,6 +620,7 @@ namespace Banshee.GStreamer
 #region Preferences
 
         private PreferenceBase replaygain_preference;
+        private PreferenceBase gapless_preference;
 
         private void InstallPreferences ()
         {
@@ -556,6 +634,11 @@ namespace Banshee.GStreamer
                 Catalog.GetString ("For tracks that have ReplayGain data, automatically scale (normalize) playback volume"),
                 delegate { ReplayGainEnabled = ReplayGainEnabledSchema.Get (); }
             ));
+            gapless_preference = service["general"]["misc"].Add (new SchemaPreference<bool> (GaplessEnabledSchema,
+                Catalog.GetString ("Enable _gapless playback"),
+                Catalog.GetString ("Eliminate the small playback gap on track change.  Useful for concept albums and classical music."),
+                delegate { GaplessEnabled = GaplessEnabledSchema.Get (); }
+            ));
         }
 
         private void UninstallPreferences ()
@@ -566,7 +649,9 @@ namespace Banshee.GStreamer
             }
 
             service["general"]["misc"].Remove (replaygain_preference);
+            service["general"]["misc"].Remove (gapless_preference);
             replaygain_preference = null;
+            gapless_preference = null;
         }
 
         public static readonly SchemaEntry<bool> ReplayGainEnabledSchema = new SchemaEntry<bool> (
@@ -576,6 +661,14 @@ namespace Banshee.GStreamer
             "If ReplayGain data is present on tracks when playing, allow volume scaling"
         );
 
+        public static readonly SchemaEntry<bool> GaplessEnabledSchema = new SchemaEntry<bool> (
+            "player_engine", "gapless_playback_enabled",
+            true,
+            "Enable gapless playback",
+            "Eliminate the small playback gap on track change.  Useful for concept albums & classical music.  EXPERIMENTAL"
+        );
+
+
 #endregion
 
         [DllImport ("libbanshee.dll")]
@@ -617,6 +710,14 @@ namespace Banshee.GStreamer
             GstTaggerTagFoundCallback cb);
 
         [DllImport ("libbanshee.dll")]
+        private static extern void bp_set_next_track_starting_callback (HandleRef player,
+            BansheePlayerNextTrackStartingCallback cb);
+
+        [DllImport ("libbanshee.dll")]
+        private static extern void bp_set_about_to_finish_callback (HandleRef player,
+            BansheePlayerAboutToFinishCallback cb);
+
+        [DllImport ("libbanshee.dll")]
         private static extern bool bp_open (HandleRef player, IntPtr uri);
 
         [DllImport ("libbanshee.dll")]
@@ -629,6 +730,9 @@ namespace Banshee.GStreamer
         private static extern void bp_play (HandleRef player);
 
         [DllImport ("libbanshee.dll")]
+        private static extern bool bp_set_next_track (HandleRef player, IntPtr uri);
+
+        [DllImport ("libbanshee.dll")]
         private static extern void bp_set_volume (HandleRef player, double volume);
 
         [DllImport("libbanshee.dll")]
diff --git a/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEngine.cs b/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEngine.cs
index b074d2b..e5f31b0 100644
--- a/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEngine.cs
+++ b/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEngine.cs
@@ -47,6 +47,8 @@ namespace Banshee.MediaEngine
 
         private TrackInfo current_track;
         private SafeUri current_uri;
+        private TrackInfo pending_track;
+        private SafeUri pending_uri;
         private PlayerState current_state = PlayerState.NotReady;
         private PlayerState last_state = PlayerState.NotReady;
 
@@ -98,6 +100,40 @@ namespace Banshee.MediaEngine
             HandleOpen (uri);
         }
 
+        public void SetNextTrack (TrackInfo track)
+        {
+            pending_track = track;
+            pending_uri = track != null ? track.Uri : null;
+
+            HandleNextTrack (pending_uri);
+        }
+
+        public void SetNextTrack (SafeUri uri)
+        {
+            pending_uri = uri;
+            pending_track = new UnknownTrackInfo (uri);
+
+            HandleNextTrack (uri);
+        }
+
+        private void HandleNextTrack (SafeUri uri)
+        {
+            if (current_state != PlayerState.Playing) {
+                // Pre-buffering the next track only makes sense when we're currently playing
+                // Instead, just open.
+                HandleOpen (uri);
+                Play ();
+                return;
+            }
+
+            try {
+                // Setting the next track doesn't change the player state.
+                SetNextTrackUri (uri);
+            } catch (Exception e) {
+                Log.Exception ("Failed to pre-buffer next track", e);
+            }
+        }
+
         private void HandleOpen (SafeUri uri)
         {
             if (current_state != PlayerState.Idle && current_state != PlayerState.NotReady && current_state != PlayerState.Contacting) {
@@ -117,6 +153,13 @@ namespace Banshee.MediaEngine
 
         public abstract void Pause ();
 
+        public virtual void SetNextTrackUri (SafeUri uri)
+        {
+            // Opening files on SetNextTrack is a sane default behaviour.
+            // This only wants to be overridden if the PlayerEngine sends out RequestNextTrack signals before EoS
+            OpenUri (uri);
+        }
+
         public virtual void VideoExpose (IntPtr displayContext, bool direct)
         {
             throw new NotImplementedException ("Engine must implement VideoExpose since this method only gets called when SupportsVideo is true");
@@ -163,6 +206,13 @@ namespace Banshee.MediaEngine
 
         protected virtual void OnEventChanged (PlayerEventArgs args)
         {
+            if (args.Event == PlayerEvent.StartOfStream && pending_track != null) {
+                Log.DebugFormat ("OnEventChanged called with StartOfStream.  Replacing current_track: \"{0}\" with pending_track: \"{1}\"", current_track.DisplayTrackTitle, pending_track.DisplayTrackTitle);
+                current_track = pending_track;
+                current_uri = pending_uri;
+                pending_track = null;
+                pending_uri = null;
+            }
             if (ThreadAssist.InMainThread) {
                 RaiseEventChanged (args);
             } else {
diff --git a/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEngineService.cs b/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEngineService.cs
index 2ad3f74..94c70fb 100644
--- a/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEngineService.cs
+++ b/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEngineService.cs
@@ -246,6 +246,10 @@ namespace Banshee.MediaEngine
                 }
             }
 
+            if (args.Event == PlayerEvent.StartOfStream) {
+                incremented_last_played = false;
+            }
+
             RaiseEvent (args);
 
             // Do not raise iterate across DBus to avoid so many calls;
@@ -303,6 +307,46 @@ namespace Banshee.MediaEngine
             OpenCheck (new SafeUri (uri));
         }
 
+        public void SetNextTrack (TrackInfo track)
+        {
+            if (track != null && EnsureActiveEngineCanPlay (track.Uri)) {
+                active_engine.SetNextTrack (track);
+            } else {
+                active_engine.SetNextTrack ((TrackInfo) null);
+            }
+        }
+
+        public void SetNextTrack (SafeUri uri)
+        {
+            if (EnsureActiveEngineCanPlay (uri)) {
+                active_engine.SetNextTrack (uri);
+            } else {
+                active_engine.SetNextTrack ((SafeUri) null);
+            }
+        }
+
+        private bool EnsureActiveEngineCanPlay (SafeUri uri)
+        {
+            if (uri == null) {
+                // No engine can play the null URI.
+                return false;
+            }
+            if (active_engine != FindSupportingEngine (uri)) {
+                if (active_engine.CurrentState == PlayerState.Playing) {
+                    // If we're currently playing then we can't switch engines now.
+                    // We can't ensure the active engine can play this URI.
+                    return false;
+                } else {
+                    // If we're not playing, we can switch the active engine to
+                    // something that will play this URI.
+                    SwitchToEngine (FindSupportingEngine (uri));
+                    CheckPending ();
+                    return true;
+                }
+            }
+            return true;
+        }
+
         public void OpenPlay (TrackInfo track)
         {
             OpenPlay (track, true);
@@ -349,17 +393,14 @@ namespace Banshee.MediaEngine
                 return;
             }
 
-            IncrementLastPlayed ();
-
-            FindSupportingEngine (uri);
+            PlayerEngine supportingEngine = FindSupportingEngine (uri);
+            SwitchToEngine (supportingEngine);
             CheckPending ();
 
             if (track != null) {
                 active_engine.Open (track);
-                incremented_last_played = false;
             } else if (uri != null) {
                 active_engine.Open (uri);
-                incremented_last_played = false;
             }
 
             if (play) {
@@ -381,34 +422,39 @@ namespace Banshee.MediaEngine
             }
         }
 
-        private void FindSupportingEngine (SafeUri uri)
+        private PlayerEngine FindSupportingEngine (SafeUri uri)
         {
             foreach (PlayerEngine engine in engines) {
                 foreach (string extension in engine.ExplicitDecoderCapabilities) {
                     if (!uri.AbsoluteUri.EndsWith (extension)) {
                         continue;
-                    } else if (active_engine != engine) {
-                        Close ();
-                        pending_engine = engine;
-                        Log.DebugFormat ("Switching engine to: {0}", engine.GetType ());
                     }
-                    return;
+                    return engine;
                 }
             }
 
             foreach (PlayerEngine engine in engines) {
                 foreach (string scheme in engine.SourceCapabilities) {
                     bool supported = scheme == uri.Scheme;
-                    if (supported && active_engine != engine) {
-                        Close ();
-                        pending_engine = engine;
-                        Log.DebugFormat ("Switching engine to: {0}", engine.GetType ());
-                        return;
-                    } else if (supported) {
-                        return;
+                    if (supported) {
+                        return engine;
                     }
                 }
             }
+            // If none of our engines support this URI, return the currently active one.
+            // There doesn't seem to be anything better to do.
+            return active_engine;
+        }
+
+        private bool SwitchToEngine (PlayerEngine switchTo)
+        {
+            if (active_engine != switchTo) {
+                Close ();
+                pending_engine = switchTo;
+                Log.DebugFormat ("Switching engine to: {0}", switchTo.GetType ());
+                return true;
+            }
+            return false;
         }
 
         public void Close ()
@@ -644,7 +690,8 @@ namespace Banshee.MediaEngine
             | PlayerEvent.Error
             | PlayerEvent.Volume
             | PlayerEvent.Metadata
-            | PlayerEvent.TrackInfoUpdated;
+            | PlayerEvent.TrackInfoUpdated
+            | PlayerEvent.RequestNextTrack;
 
         private const PlayerEvent event_default_mask = event_all_mask & ~PlayerEvent.Iterate;
 
diff --git a/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEvent.cs b/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEvent.cs
index 96b1b79..a5c714f 100644
--- a/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEvent.cs
+++ b/src/Core/Banshee.Services/Banshee.MediaEngine/PlayerEvent.cs
@@ -108,7 +108,8 @@ namespace Banshee.MediaEngine
         Error = 64,
         Volume = 128,
         Metadata = 256,
-        TrackInfoUpdated = 512
+        TrackInfoUpdated = 512,
+        RequestNextTrack = 1024
     }
 
     public enum PlayerState
diff --git a/src/Core/Banshee.Services/Banshee.PlaybackController/IBasicPlaybackController.cs b/src/Core/Banshee.Services/Banshee.PlaybackController/IBasicPlaybackController.cs
index 4d793c7..9825f8a 100644
--- a/src/Core/Banshee.Services/Banshee.PlaybackController/IBasicPlaybackController.cs
+++ b/src/Core/Banshee.Services/Banshee.PlaybackController/IBasicPlaybackController.cs
@@ -31,7 +31,7 @@ namespace Banshee.PlaybackController
     public interface IBasicPlaybackController
     {
         bool First ();
-        bool Next (bool restart);
+        bool Next (bool restart, bool userRequested);
         bool Previous (bool restart);
     }
 }
diff --git a/src/Core/Banshee.Services/Banshee.PlaybackController/PlaybackControllerService.cs b/src/Core/Banshee.Services/Banshee.PlaybackController/PlaybackControllerService.cs
index 6625cde..fa30d5a 100644
--- a/src/Core/Banshee.Services/Banshee.PlaybackController/PlaybackControllerService.cs
+++ b/src/Core/Banshee.Services/Banshee.PlaybackController/PlaybackControllerService.cs
@@ -90,6 +90,7 @@ namespace Banshee.PlaybackController
             player_engine = ServiceManager.PlayerEngine;
             player_engine.PlayWhenIdleRequest += OnPlayerEnginePlayWhenIdleRequest;
             player_engine.ConnectEvent (OnPlayerEvent,
+                PlayerEvent.RequestNextTrack |
                 PlayerEvent.EndOfStream |
                 PlayerEvent.StartOfStream |
                 PlayerEvent.StateChange |
@@ -164,6 +165,9 @@ namespace Banshee.PlaybackController
                         transition_track_started = true;
                     }
                     break;
+                case PlayerEvent.RequestNextTrack:
+                    RequestTrackHandler ();
+                    break;
             }
         }
 
@@ -177,12 +181,18 @@ namespace Banshee.PlaybackController
 
         private bool EosTransition ()
         {
+            player_engine.IncrementLastPlayed ();
+            return true;
+        }
+
+        private bool RequestTrackHandler ()
+        {
             if (!StopWhenFinished) {
                 if (RepeatMode == PlaybackRepeatMode.RepeatSingle) {
                     QueuePlayTrack ();
                 } else {
                     last_was_skipped = false;
-                    Next ();
+                    Next (RepeatMode == PlaybackRepeatMode.RepeatAll, false);
                 }
             } else {
                 OnStopped ();
@@ -211,21 +221,28 @@ namespace Banshee.PlaybackController
 
         public void Next ()
         {
-            Next (RepeatMode == PlaybackRepeatMode.RepeatAll);
+            Next (RepeatMode == PlaybackRepeatMode.RepeatAll, true);
         }
 
         public void Next (bool restart)
         {
+            Next (restart, true);
+        }
+
+        public void Next (bool restart, bool userRequested)
+        {
             CancelErrorTransition ();
 
             Source = NextSource;
             raise_started_after_transition = true;
 
-            player_engine.IncrementLastPlayed ();
+            if (userRequested) {
+                player_engine.IncrementLastPlayed ();
+            }
 
-            if (Source is IBasicPlaybackController && ((IBasicPlaybackController)Source).Next (restart)) {
+            if (Source is IBasicPlaybackController && ((IBasicPlaybackController)Source).Next (restart, userRequested)) {
             } else {
-                ((IBasicPlaybackController)this).Next (restart);
+                ((IBasicPlaybackController)this).Next (restart, userRequested);
             }
 
             OnTransition ();
@@ -261,29 +278,19 @@ namespace Banshee.PlaybackController
             return true;
         }
 
-        bool IBasicPlaybackController.Next (bool restart)
+        bool IBasicPlaybackController.Next (bool restart, bool userRequested)
         {
-            TrackInfo tmp_track = CurrentTrack;
-
-            if (next_stack.Count > 0) {
-                CurrentTrack = next_stack.Pop ();
-                if (tmp_track != null) {
-                    previous_stack.Push (tmp_track);
-                }
-            } else {
-                TrackInfo next_track = QueryTrack (Direction.Next, restart);
-                if (next_track != null) {
-                    if (tmp_track != null) {
-                        previous_stack.Push (tmp_track);
-                    }
-                } else {
-                    return true;
-                }
-
-                CurrentTrack = next_track;
+            if (CurrentTrack != null) {
+                previous_stack.Push (CurrentTrack);
             }
 
-            QueuePlayTrack ();
+            CurrentTrack = CalcNextTrack (Direction.Next, restart);
+            if (!userRequested) {
+                // A RequestNextTrack event should always result in SetNextTrack being called.  null is acceptable.
+                player_engine.SetNextTrack (CurrentTrack);
+            } else if (CurrentTrack != null) {
+                QueuePlayTrack ();
+            }
             return true;
         }
 
@@ -293,21 +300,28 @@ namespace Banshee.PlaybackController
                 next_stack.Push (current_track);
             }
 
-            if (previous_stack.Count > 0) {
-                CurrentTrack = previous_stack.Pop ();
-            } else {
-                TrackInfo track = CurrentTrack = QueryTrack (Direction.Previous, restart);
-                if (track != null) {
-                    CurrentTrack = track;
-                } else {
-                    return true;
-                }
+            CurrentTrack = CalcNextTrack (Direction.Previous, restart);
+            if (CurrentTrack != null) {
+                QueuePlayTrack ();
             }
 
-            QueuePlayTrack ();
             return true;
         }
 
+        private TrackInfo CalcNextTrack (Direction direction, bool restart)
+        {
+            if (direction == Direction.Previous) {
+                if (previous_stack.Count > 0) {
+                    return previous_stack.Pop ();
+                }
+            } else if (direction == Direction.Next) {
+                if (next_stack.Count > 0) {
+                    return next_stack.Pop ();
+                }
+            }
+            return QueryTrack (direction, restart);
+        }
+
         private TrackInfo QueryTrack (Direction direction, bool restart)
         {
             Log.DebugFormat ("Querying model for track to play in {0}:{1} mode", ShuffleMode, direction);
diff --git a/src/Extensions/Banshee.InternetRadio/Banshee.InternetRadio/InternetRadioSource.cs b/src/Extensions/Banshee.InternetRadio/Banshee.InternetRadio/InternetRadioSource.cs
index 7447515..5d700b7 100644
--- a/src/Extensions/Banshee.InternetRadio/Banshee.InternetRadio/InternetRadioSource.cs
+++ b/src/Extensions/Banshee.InternetRadio/Banshee.InternetRadio/InternetRadioSource.cs
@@ -279,8 +279,20 @@ namespace Banshee.InternetRadio
             return false;
         }
 
-        public bool Next (bool restart)
+        public bool Next (bool restart, bool userRequested)
         {
+            /*
+             * TODO: It should be technically possible to handle userRequested=False
+             * correctly here, but the current implementation is quite hostile.
+             * For the moment, just SetNextTrack (null), and go on to OpenPlay if
+             * the engine isn't currently playing.
+             */
+            if (!userRequested) {
+                ServiceManager.PlayerEngine.SetNextTrack ((SafeUri)null);
+                if (ServiceManager.PlayerEngine.IsPlaying ()) {
+                    return true;
+                }
+            }
             RadioTrackInfo radio_track = ServiceManager.PlaybackController.CurrentTrack as RadioTrackInfo;
             if (radio_track != null && radio_track.PlayNextStream ()) {
                 return true;
diff --git a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/StationSource.cs b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/StationSource.cs
index 2aeffce..746349b 100644
--- a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/StationSource.cs
+++ b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/StationSource.cs
@@ -294,17 +294,24 @@ namespace Banshee.Lastfm.Radio
 
         bool IBasicPlaybackController.First ()
         {
-            return ((IBasicPlaybackController)this).Next (false);
+            return ((IBasicPlaybackController)this).Next (false, true);
         }
 
         private bool playback_requested;
-        bool IBasicPlaybackController.Next (bool restart)
+        bool IBasicPlaybackController.Next (bool restart, bool userRequested)
         {
             TrackInfo next = NextTrack;
-            if (next != null) {
-                ServiceManager.PlayerEngine.OpenPlay (next);
-            }  else {
-                playback_requested = true;
+            if (userRequested) {
+                if (next != null) {
+                    ServiceManager.PlayerEngine.OpenPlay (next);
+                }  else {
+                    playback_requested = true;
+                }
+            } else {
+                // We want to unconditionally SetNextTrack.
+                // Passing null is OK.
+                ServiceManager.PlayerEngine.SetNextTrack (next);
+                playback_requested = next == null;
             }
             return true;
         }
@@ -372,7 +379,7 @@ namespace Banshee.Lastfm.Radio
 
                             if (playback_requested) {
                                 if (this == ServiceManager.PlaybackController.Source ) {
-                                    ((IBasicPlaybackController)this).Next (false);
+                                    ((IBasicPlaybackController)this).Next (false, true);
                                 }
                                 playback_requested = false;
                             }
diff --git a/src/Extensions/Banshee.PlayQueue/Banshee.PlayQueue/PlayQueueSource.cs b/src/Extensions/Banshee.PlayQueue/Banshee.PlayQueue/PlayQueueSource.cs
index 85013fe..22b21fe 100644
--- a/src/Extensions/Banshee.PlayQueue/Banshee.PlayQueue/PlayQueueSource.cs
+++ b/src/Extensions/Banshee.PlayQueue/Banshee.PlayQueue/PlayQueueSource.cs
@@ -519,10 +519,10 @@ namespace Banshee.PlayQueue
 
         bool IBasicPlaybackController.First ()
         {
-            return ((IBasicPlaybackController)this).Next (false);
+            return ((IBasicPlaybackController)this).Next (false, true);
         }
 
-        bool IBasicPlaybackController.Next (bool restart)
+        bool IBasicPlaybackController.Next (bool restart, bool userRequested)
         {
             if (current_track != null && ServiceManager.PlayerEngine.CurrentTrack == current_track) {
                 int index = TrackModel.IndexOf (current_track) + 1;
@@ -533,14 +533,21 @@ namespace Banshee.PlayQueue
                 ServiceManager.PlaybackController.Source = PriorSource;
                 if (was_playing) {
                     ServiceManager.PlaybackController.PriorTrack = prior_playback_track;
-                    ServiceManager.PlaybackController.Next (restart);
+                    ServiceManager.PlaybackController.Next (restart, userRequested);
                 } else {
+                    if (!userRequested) {
+                        ServiceManager.PlayerEngine.SetNextTrack ((TrackInfo)null);
+                    }
                     ServiceManager.PlayerEngine.Close ();
                 }
                 return true;
             }
 
-            ServiceManager.PlayerEngine.OpenPlay (current_track);
+            if (userRequested) {
+                ServiceManager.PlayerEngine.OpenPlay (current_track);
+            } else {
+                ServiceManager.PlayerEngine.SetNextTrack (current_track);
+            }
             return true;
         }
 



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