Sun Mar 18 18:34:16 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: implement visualisation using RBPlayerGstTee
Sun Mar 18 18:32:27 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: implement tee and filter interfaces (mostly successfully)
Sun Mar 18 18:31:55 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: probably support replaygain
Sun Mar 18 18:30:01 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: fix fading in of mp3s, some other things
Wed Mar 14 07:27:11 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: improve error reporting a tiny bit
Wed Mar 14 07:12:10 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: update audiocd plugin changes
Mon Mar  5 19:14:30 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: update audiocd plugin for stream reuse signal changes
Mon Mar  5 19:11:41 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: split reuse-stream into can-reuse-stream and reuse-stream; unlink and block while reusing; track seeking vs paused state better; update python bindings
Tue Feb 27 22:59:03 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: fix some UI+dbus signal emission
Tue Feb 27 08:53:56 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: fix network buffer size config, try to stop it deadlocking at ~256kB
Tue Feb 27 07:42:11 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: fix fade in, add hook for cd audio track changes (broken?)
Fri Feb 23 08:14:05 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: implement seeking properly, buffering, buffer size control, GObjectise RBXFadeStream
Sun Feb 18 15:54:59 EST 2007  jonathan@kaolin.wh9.net
  * GLXF: mostly working gapless/crossfade backend, StreamData needs gobjectifying
diff -rN -u -p old-npb/backends/gstreamer/Makefile.am new-npb/backends/gstreamer/Makefile.am
--- old-npb/backends/gstreamer/Makefile.am	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/backends/gstreamer/Makefile.am	2007-03-18 18:49:31.000000000 +1000
@@ -30,3 +30,8 @@ librbbackendsgstreamer_la_SOURCES +=	\
 	rb-encoder-gst.c
 endif
 
+if USE_GSTREAMER_0_10
+librbbackendsgstreamer_la_SOURCES +=	\
+	rb-player-gst-xfade.h		\
+	rb-player-gst-xfade.c
+endif
diff -rN -u -p old-npb/backends/gstreamer/rb-player-gst.c new-npb/backends/gstreamer/rb-player-gst.c
--- old-npb/backends/gstreamer/rb-player-gst.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/backends/gstreamer/rb-player-gst.c	2007-03-18 18:49:31.000000000 +1000
@@ -37,6 +37,7 @@
 
 #include "rb-debug.h"
 #include "rb-marshal.h"
+#include "rb-util.h"
 
 #include "rb-player.h"
 #include "rb-player-gst-filter.h"
@@ -44,7 +45,6 @@
 /*#include "rb-player-gst-data-tee.h"*/
 #include "rb-player-gst.h"
 
-
 static void rb_player_init (RBPlayerIface *iface);
 static void rb_player_gst_filter_init (RBPlayerGstFilterIface *iface);
 static void rb_player_gst_tee_init (RBPlayerGstTeeIface *iface);
@@ -57,17 +57,22 @@ static void rb_player_gst_get_property (
 
 static gboolean rb_player_gst_open (RBPlayer *player,
 				    const char *uri,
+				    gpointer stream_data,
+				    GDestroyNotify stream_data_destroy,
 				    GError **error);
 static gboolean rb_player_gst_opened (RBPlayer *player);
-static gboolean rb_player_gst_close (RBPlayer *player, GError **error);
-static gboolean rb_player_gst_play (RBPlayer *player, GError **error);
+static gboolean rb_player_gst_close (RBPlayer *player,
+				     const char *uri,
+				     GError **error);
+static gboolean rb_player_gst_play (RBPlayer *player, gint crossfade, GError **error);
 static void rb_player_gst_pause (RBPlayer *player);
 static gboolean rb_player_gst_playing (RBPlayer *player);
 static void rb_player_gst_set_volume (RBPlayer *player, float volume);
 static float rb_player_gst_get_volume (RBPlayer *player);
 static void rb_player_gst_set_replaygain (RBPlayer *player,
-				      double track_gain, double track_peak,
-				      double album_gain, double album_peak);
+					  const char *uri,
+					  double track_gain, double track_peak,
+					  double album_gain, double album_peak);
 static gboolean rb_player_gst_seekable (RBPlayer *player);
 static void rb_player_gst_set_time (RBPlayer *player, long time);
 static long rb_player_gst_get_time (RBPlayer *player);
@@ -93,6 +98,8 @@ enum
 struct _RBPlayerGstPrivate
 {
 	char *uri;
+	gpointer stream_data;
+	GDestroyNotify stream_data_destroy;
 
 	GstElement *playbin;
 
@@ -183,6 +190,7 @@ rb_player_init (RBPlayerIface *iface)
 	iface->seekable = rb_player_gst_seekable;
 	iface->set_time = rb_player_gst_set_time;
 	iface->get_time = rb_player_gst_get_time;
+	iface->multiple_open = (RBPlayerFeatureFunc) rb_false_function;
 }
 
 static gboolean
@@ -191,7 +199,7 @@ tick_timeout (RBPlayerGst *mp)
 	if (mp->priv->playing == FALSE)
 		return TRUE;
 
-	_rb_player_emit_tick (RB_PLAYER (mp), rb_player_get_time (RB_PLAYER (mp)));
+	_rb_player_emit_tick (RB_PLAYER (mp), mp->priv->uri, rb_player_get_time (RB_PLAYER (mp)), -1);
 
 	return TRUE;
 }
@@ -295,29 +303,40 @@ emit_signal_idle (RBPlayerGstSignal *sig
 {
 	switch (signal->type) {
 	case ERROR:
-		_rb_player_emit_error (RB_PLAYER (signal->object), signal->error);
+		_rb_player_emit_error (RB_PLAYER (signal->object),
+				       signal->object->priv->stream_data,
+				       signal->error);
 
 		/* close if not already closing */
 		if (signal->object->priv->uri != NULL)
-			rb_player_close (RB_PLAYER (signal->object), NULL);
+			rb_player_close (RB_PLAYER (signal->object), NULL, NULL);
 
 		break;
 
 	case EOS:
-		_rb_player_emit_eos (RB_PLAYER (signal->object));
+		_rb_player_emit_eos (RB_PLAYER (signal->object),
+				     signal->object->priv->stream_data);
 		signal->object->priv->idle_eos_id = 0;
 		break;
 
 	case INFO:
-		_rb_player_emit_info (RB_PLAYER (signal->object), signal->info_field, signal->info);
+		_rb_player_emit_info (RB_PLAYER (signal->object),
+				      signal->object->priv->stream_data,
+				      signal->info_field,
+				      signal->info);
 		break;
 
 	case BUFFERING:
-		_rb_player_emit_buffering (RB_PLAYER (signal->object), g_value_get_uint (signal->info));
+		_rb_player_emit_buffering (RB_PLAYER (signal->object),
+					   signal->object->priv->stream_data,
+					   g_value_get_uint (signal->info));
 		signal->object->priv->idle_buffering_id = 0;
 		break;
 	case EVENT:
-		_rb_player_emit_event (RB_PLAYER (signal->object), g_value_get_string (signal->info), NULL);
+		_rb_player_emit_event (RB_PLAYER (signal->object),
+				       signal->object->priv->stream_data,
+				       g_value_get_string (signal->info),
+				       NULL);
 		break;
 	}
 
@@ -535,19 +554,19 @@ rb_player_gst_bus_cb (GstBus * bus, GstM
 							 code,
 							 error->message);
 			mp->priv->emitted_error = TRUE;
-			_rb_player_emit_error (RB_PLAYER (mp), sig_error);
+			_rb_player_emit_error (RB_PLAYER (mp), mp->priv->stream_data, sig_error);
 		}
 
 		/* close if not already closing */
 		if (mp->priv->uri != NULL)
-			rb_player_close (RB_PLAYER (mp), NULL);
+			rb_player_close (RB_PLAYER (mp), NULL, NULL);
 
 		g_error_free (error);
 		g_free (debug);
 		break;
 	}
 	case GST_MESSAGE_EOS:
-		_rb_player_emit_eos (RB_PLAYER (mp));
+		_rb_player_emit_eos (RB_PLAYER (mp), mp->priv->stream_data);
 		break;
 	case GST_MESSAGE_TAG: {
 		GstTagList *tags;
@@ -872,9 +891,21 @@ cdda_got_source_cb (GObject *object, GPa
 	}
 }
 
+static void
+_destroy_stream_data (RBPlayerGst *player)
+{
+	if (player->priv->stream_data && player->priv->stream_data_destroy) {
+		player->priv->stream_data_destroy (player->priv->stream_data);
+	}
+	player->priv->stream_data = NULL;
+	player->priv->stream_data_destroy = NULL;
+}
+
 static gboolean
 rb_player_gst_open (RBPlayer *player,
 		const char *uri,
+		gpointer stream_data,
+		GDestroyNotify stream_data_destroy,
 		GError **error)
 {
 	RBPlayerGst *mp = RB_PLAYER_GST (player);
@@ -884,13 +915,14 @@ rb_player_gst_open (RBPlayer *player,
 		if (!rb_player_gst_construct (mp, error))
 			return FALSE;
 	} else {
-		if (!rb_player_close (player, error))
+		if (!rb_player_close (player, NULL, error))
 			return FALSE;
 	}
 
 	g_assert (mp->priv->playbin != NULL);
 
 	if (uri == NULL) {
+		_destroy_stream_data (mp);
 		g_free (mp->priv->uri);
 		mp->priv->uri = NULL;
 		mp->priv->playing = FALSE;
@@ -950,8 +982,11 @@ rb_player_gst_open (RBPlayer *player,
 	}
 
 	begin_gstreamer_operation (mp);
+	_destroy_stream_data (mp);
 	g_free (mp->priv->uri);
 	mp->priv->uri = g_strdup (uri);
+	mp->priv->stream_data = stream_data;
+	mp->priv->stream_data_destroy = stream_data_destroy;
 	mp->priv->emitted_error = FALSE;
 
 	if (!cdda_seek) {
@@ -960,7 +995,7 @@ rb_player_gst_open (RBPlayer *player,
 
 	if (!rb_player_gst_sync_pipeline (mp)) {
 		end_gstreamer_operation (mp, TRUE, error);
-		rb_player_gst_close (player, NULL);
+		rb_player_gst_close (player, uri, NULL);
 		return FALSE;
 	}
 
@@ -980,7 +1015,7 @@ remove_idle_source (gpointer key, gpoint
 }
 
 static gboolean
-rb_player_gst_close (RBPlayer *player, GError **error)
+rb_player_gst_close (RBPlayer *player, const char *uri, GError **error)
 {
 	RBPlayerGst *mp = RB_PLAYER_GST (player);
 	gboolean ret;
@@ -988,6 +1023,12 @@ rb_player_gst_close (RBPlayer *player, G
 	mp->priv->playing = FALSE;
 	mp->priv->buffering = FALSE;
 
+	if (uri != NULL && strcmp (mp->priv->uri, uri) == 0) {
+		rb_debug ("URI doesn't match current playing URI; ignoring");
+		return TRUE;
+	}
+
+	_destroy_stream_data (mp);
 	g_free (mp->priv->uri);
 	mp->priv->uri = NULL;
 
@@ -1032,7 +1073,7 @@ rb_player_gst_opened (RBPlayer *player)
 }
 
 static gboolean
-rb_player_gst_play (RBPlayer *player, GError **error)
+rb_player_gst_play (RBPlayer *player, gint crossfade, GError **error)
 {
 	RBPlayerGst *mp = RB_PLAYER_GST (player);
 	gboolean ret;
@@ -1046,6 +1087,8 @@ rb_player_gst_play (RBPlayer *player, GE
 	ret = rb_player_gst_sync_pipeline (mp);
 	end_gstreamer_operation (mp, !ret, error);
 
+	_rb_player_emit_playing_stream (RB_PLAYER (mp), mp->priv->stream_data);
+
 	if (mp->priv->tick_timeout_id == 0)
 		mp->priv->tick_timeout_id = g_timeout_add (1000 / RB_PLAYER_GST_TICK_HZ, (GSourceFunc) tick_timeout, mp);
 
@@ -1082,6 +1125,7 @@ rb_player_gst_playing (RBPlayer *player)
 
 static void
 rb_player_gst_set_replaygain (RBPlayer *player,
+			      const char *uri,
 			      double track_gain, double track_peak,
 			      double album_gain, double album_peak)
 {
diff -rN -u -p old-npb/backends/gstreamer/rb-player-gst-xfade.c new-npb/backends/gstreamer/rb-player-gst-xfade.c
--- old-npb/backends/gstreamer/rb-player-gst-xfade.c	1970-01-01 10:00:00.000000000 +1000
+++ new-npb/backends/gstreamer/rb-player-gst-xfade.c	2007-03-18 18:49:31.000000000 +1000
@@ -0,0 +1,3372 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2006   Jonathan Matthew  <jonathan@kaolin.wh9.net>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+/*
+ * GStreamer player backend with crossfading and gaplessness and trees and
+ * flowers and bunnies.
+ */
+
+/*
+ * not yet implemented:
+ * - replaygain (need to figure out what to do if we set_replaygain gets
+ *               called while fading in)
+ * - implement RBPlayerGstTee (maybe not entirely working?)
+ * - implement RBPlayerGstFilter (sort of works?)
+ *
+ * things that need to be fixed:
+ * - error reporting is abysmal
+ * - shell-player still thinks it's paused when playing the first stream
+ *    (playing/paused status is generally screwy) (maybe fixed?)
+ * - deadlocks when trying to close a fading stream (on shutdown, for example)
+ *
+ * crack:
+ * - use more interesting transition effects - filter sweeps, reverb, etc.
+ */
+
+/*
+ * basic design:
+ *
+ * we have a single output bin, beginning with an adder.
+ * connected to this are a number of stream bins, consisting of a
+ * source, decodebin, audio convert/resample, and a volume element used
+ * for fading in and out.  (might be interesting to replace those with
+ * high/low pass filter elements?)
+ *
+ * stream bins only stay connected to the adder while actually playing.
+ * when not playing (prerolling or paused), the stream bin's source pad
+ * is blocked so no data can flow.
+ *
+ * streams go through a number of states:
+ *
+ * when a stream is created (in rb_player_open()), it starts in PREROLLING
+ * state.  from there:
+ *
+ * - rb_player_play():  -> PREROLL_PLAY
+ * - preroll finishes:  -> WAITING
+ *
+ * from WAITING:
+ *
+ * - rb_player_play(), crossfade == 0, other stream playing:  -> WAITING_EOS
+ * - rb_player_play(), crossfade > 0, other stream playing:   -> FADING IN, link to adder, unblock
+ *      + fade out existing stream
+ * - rb_player_play(), crossfade < 0, other stream playing:   -> PLAYING, link to adder, unblock
+ *      + stop existing stream
+ * - rb_player_play(), existing stream paused:  -> PLAYING, link to adder, unblock
+ *      + stop existing stream
+ * - rb_player_play(), nothing already playing:  -> PLAYING, link to adder, unblock
+ *
+ * from PREROLL_PLAY:
+ *
+ * - preroll finishes, crossfade == 0, other stream playing:  -> WAITING_EOS
+ * - preroll finishes, crossfade > 0, other stream playing:  -> FADING_IN, link to adder, unblock
+ *   	+ fade out existing stream
+ * - preroll finishes, crossfade < 0, other stream playing:  -> PLAYING, link to adder, unblock
+ *      + stop existing stream
+ * - preroll finishes, existing stream paused:  -> PLAYING, link to adder, unblock
+ *      + stop existing stream
+ * - preroll finishes, nothing already playing:  -> PLAYING, link to adder, unblock
+ *
+ * from WAITING_EOS:
+ *
+ * - EOS received for another stream:  -> PLAYING, link to adder, unblock
+ *
+ * from FADING_IN:
+ *
+ * - fade in completes:  -> PLAYING
+ * - another stream starts fading in:  -> FADING_OUT
+ * - rb_player_pause():  -> PAUSED, block, unlink from adder
+ * - stopped for another stream:  -> PENDING_REMOVE
+ * - rb_player_set_time():  -> SEEKING, block, unlink
+ * - reused for another stream:  -> REUSING; block, unlink
+ *
+ * from PLAYING:
+ *
+ * - rb_player_pause():  -> PAUSED, block, unlink from adder
+ * - EOS:  -> PENDING_REMOVE
+ * - another stream starts fading in:  -> FADING_OUT
+ * - stopped for another stream:  -> PENDING_REMOVE
+ * - rb_player_set_time():  -> SEEKING, block, unlink
+ * - reused for another stream:  -> REUSING; block, unlink
+ *
+ * from SEEKING:
+ * - rb_player_pause():  -> SEEKING_PAUSED
+ * - blocked:  perform seek, link, unblock -> PLAYING | FADING_IN
+ *
+ * from SEEKING_PAUSED:
+ * - blocked:  perform seek, -> PAUSED
+ * - rb_player_play():   -> SEEKING
+ *
+ * from PAUSED:
+ *
+ * - rb_player_play():  -> PLAYING, link to adder, unblock
+ * - stopped for another stream:  -> PENDING_REMOVE
+ * - rb_player_set_time(): -> perform seek
+ *
+ * from FADING_OUT:
+ *
+ * - fade out finishes:  -> PENDING_REMOVE
+ * - EOS:  -> PENDING_REMOVE
+ * - reused for another stream:  -> REUSING; block, unlink
+ *
+ * from PENDING_REMOVE:
+ *
+ * - reap_streams idle handler called:  -> unlink from adder, stream destroyed
+ *
+ * from REUSING:
+ *
+ *  - blocked:  emit reuse-stream, -> PLAYING
+ */
+
+#include "config.h"
+#include <math.h>
+
+#include <glib/gi18n.h>
+#include <gst/gst.h>
+#include <gst/controller/gstcontroller.h>
+#include <gst/base/gstbasetransform.h>
+
+#include "rb-player.h"
+#include "rb-player-gst-xfade.h"
+#include "rb-debug.h"
+#include "rb-file-helpers.h"
+#include "rb-util.h"
+#include "rb-marshal.h"
+#include "rb-player-gst-tee.h"
+#include "rb-player-gst-filter.h"
+
+static void rb_player_init (RBPlayerIface *iface);
+static void rb_player_gst_tee_init (RBPlayerGstTeeIface *iface);
+static void rb_player_gst_filter_init (RBPlayerGstFilterIface *iface);
+static void rb_player_gst_xfade_dispose (GObject *object);
+static void rb_player_gst_xfade_finalize (GObject *object);
+
+static gboolean rb_player_gst_xfade_open (RBPlayer *player,
+					  const char *uri,
+					  gpointer stream_data,
+					  GDestroyNotify stream_data_destroy,
+					  GError **error);
+static gboolean rb_player_gst_xfade_opened (RBPlayer *player);
+static gboolean rb_player_gst_xfade_close (RBPlayer *player, const char *uri, GError **error);
+static gboolean rb_player_gst_xfade_play (RBPlayer *player, gint crossfade, GError **error);
+static void rb_player_gst_xfade_pause (RBPlayer *player);
+static gboolean rb_player_gst_xfade_playing (RBPlayer *player);
+static gboolean rb_player_gst_xfade_seekable (RBPlayer *player);
+static void rb_player_gst_xfade_set_time (RBPlayer *player, long time);
+static long rb_player_gst_xfade_get_time (RBPlayer *player);
+static void rb_player_gst_xfade_set_volume (RBPlayer *player, float volume);
+static float rb_player_gst_xfade_get_volume (RBPlayer *player);
+static void rb_player_gst_xfade_set_replaygain (RBPlayer *player,
+						const char *uri,
+						double track_gain, double track_peak,
+						double album_gain, double album_peak);
+static gboolean rb_player_gst_xfade_add_tee (RBPlayerGstTee *player, GstElement *element);
+static gboolean rb_player_gst_xfade_add_filter (RBPlayerGstFilter *player, GstElement *element);
+static gboolean rb_player_gst_xfade_remove_tee (RBPlayerGstTee *player, GstElement *element);
+static gboolean rb_player_gst_xfade_remove_filter (RBPlayerGstFilter *player, GstElement *element);
+
+static gboolean create_sink (RBPlayerGstXFade *player, GError **error);
+static gboolean start_sink (RBPlayerGstXFade *player);
+static gboolean stop_sink (RBPlayerGstXFade *player);
+
+GType rb_xfade_stream_get_type (void);
+
+G_DEFINE_TYPE_WITH_CODE(RBPlayerGstXFade, rb_player_gst_xfade, G_TYPE_OBJECT,
+			G_IMPLEMENT_INTERFACE(RB_TYPE_PLAYER,
+					      rb_player_init)
+			G_IMPLEMENT_INTERFACE(RB_TYPE_PLAYER_GST_TEE,
+					      rb_player_gst_tee_init)
+			G_IMPLEMENT_INTERFACE(RB_TYPE_PLAYER_GST_FILTER,
+					      rb_player_gst_filter_init))
+
+#define GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_PLAYER_GST_XFADE, RBPlayerGstXFadePrivate))
+
+#define RB_PLAYER_GST_XFADE_TICK_HZ 5
+
+#define EPSILON			(0.001)
+#define STREAM_PLAYING_MESSAGE	"rb-stream-playing"
+#define FADE_OUT_DONE_MESSAGE	"rb-fade-out-done"
+#define FADE_IN_DONE_MESSAGE	"rb-fade-in-done"
+#define STREAM_EOS_MESSAGE	"rb-stream-eos"
+
+#define MAX_NETWORK_BUFFER_SIZE		(2048)
+
+enum
+{
+	PROP_0,
+	PROP_BUFFER_SIZE
+};
+
+enum
+{
+	CAN_REUSE_STREAM,
+	REUSE_STREAM,
+	LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+/* these aren't actually used to construct bitmasks,
+ * but we search the list that way.
+ */
+typedef enum
+{
+	/* stable states */
+	WAITING = 1,
+	PLAYING = 2,
+	PAUSED = 4,
+
+	/* transition states */
+	REUSING = 8,
+	PREROLLING = 16,
+	PREROLL_PLAY = 32,
+	FADING_IN = 64,
+	SEEKING = 128,
+	SEEKING_PAUSED = 256,
+	WAITING_EOS = 512,
+	FADING_OUT = 1024,
+	PENDING_REMOVE = 2048
+} StreamState;
+
+typedef struct
+{
+	GObject parent;
+	RBPlayerGstXFade *player;
+
+	GStaticMutex lock;
+
+	char *uri;
+	gpointer stream_data;
+	GDestroyNotify stream_data_destroy;
+
+	/* stream reuse data */
+	char *new_uri;
+	gpointer new_stream_data;
+	GDestroyNotify new_stream_data_destroy;
+
+	/* probably don't need to store pointers to all of these.. */
+	GstElement *bin;
+	GstElement *source;
+	GstElement *queue;
+	GstElement *decoder;
+	GstElement *volume;
+	GstElement *audioconvert;
+	GstElement *audioresample;
+	GstElement *capsfilter;
+	GstElement *preroll;
+	gboolean decoder_linked;
+	gboolean emitted_playing;
+
+	GstPad *src_pad;
+	GstPad *ghost_pad;
+	GstPad *adder_pad;
+	gboolean src_blocked;
+	GstClockTime base_time;
+
+	gint64 seek_target;
+
+	GstController *fader;
+	StreamState state;
+	gint crossfade;
+	gboolean fading;
+
+	guint64 queue_threshold;
+	gulong queue_threshold_id;
+	gulong underrun_id;
+	gulong queue_probe_id;
+
+	float replaygain_scale;
+	double fade_end;
+
+	gulong error_idle_id;
+	GError *error;
+} RBXFadeStream;
+
+#define RB_TYPE_XFADE_STREAM 	(rb_xfade_stream_get_type ())
+#define RB_XFADE_STREAM(obj)	(G_TYPE_CHECK_INSTANCE_CAST ((obj), RB_TYPE_XFADE_STREAM, RBXFadeStream))
+#define RB_IS_XFADE_STREAM(obj)	(G_TYPE_CHECK_INSTANCE_TYPE ((obj), RB_TYPE_XFADE_STREAM))
+
+typedef struct {
+	GObjectClass obj_class;
+} RBXFadeStreamClass;
+
+static void rb_xfade_stream_class_init (RBXFadeStreamClass *klass);
+
+G_DEFINE_TYPE(RBXFadeStream, rb_xfade_stream, G_TYPE_OBJECT)
+
+
+struct _RBPlayerGstXFadePrivate
+{
+	/* probably don't need to store pointers to these either */
+	GstElement *pipeline;
+	GstElement *outputbin;
+	GstElement *silencebin;
+	GstElement *adder;
+	GstElement *capsfilter;
+	GstElement *volume;
+	GstElement *sink;
+	GstElement *tee;
+	GstElement *filterbin;
+	gboolean sink_playing;
+
+	GList *waiting_tees;
+	GList *waiting_filters;
+
+	GStaticRecMutex stream_list_lock;
+	GList *streams;
+
+	gboolean can_signal_direct_error;
+	GError *error;
+	gboolean emitted_error;
+
+	gboolean playing;
+
+	float cur_volume;
+	guint buffer_size;	/* kB */
+
+	guint tick_timeout_id;
+
+	guint stream_reap_id;
+};
+
+
+static void
+rb_xfade_stream_init (RBXFadeStream *stream)
+{
+	stream->replaygain_scale = 1.0;
+	g_static_mutex_init (&stream->lock);
+}
+
+static void
+rb_xfade_stream_dispose_stream_data (RBXFadeStream *stream)
+{
+	if (stream->stream_data && stream->stream_data_destroy) {
+		stream->stream_data_destroy (stream->stream_data);
+	}
+	stream->stream_data = NULL;
+	stream->stream_data_destroy = NULL;
+}
+
+static void
+rb_xfade_stream_dispose (GObject *object)
+{
+	RBXFadeStream *sd = RB_XFADE_STREAM (object);
+
+	if (sd->bin != NULL) {
+		gst_object_unref (sd->bin);
+		sd->bin = NULL;
+	}
+
+	if (sd->source != NULL) {
+		gst_object_unref (sd->source);
+		sd->source = NULL;
+	}
+
+	if (sd->queue != NULL) {
+		if (sd->queue_threshold_id != 0)
+			g_signal_handler_disconnect (sd->queue, sd->queue_threshold_id);
+		if (sd->underrun_id != 0)
+			g_signal_handler_disconnect (sd->queue, sd->underrun_id);
+		if (sd->queue_probe_id != 0) {
+			GstPad *sinkpad;
+
+			sinkpad = gst_element_get_pad (sd->queue, "sink");
+			gst_pad_remove_buffer_probe (sinkpad, sd->queue_probe_id);
+			gst_object_unref (sinkpad);
+		}
+		gst_object_unref (sd->queue);
+		sd->queue = NULL;
+	}
+
+	if (sd->decoder != NULL) {
+		gst_object_unref (sd->decoder);
+		sd->decoder = NULL;
+	}
+
+	if (sd->volume != NULL) {
+		gst_object_unref (sd->volume);
+		sd->volume = NULL;
+	}
+
+	if (sd->fader != NULL) {
+		gst_object_unref (sd->fader);
+		sd->fader = NULL;
+	}
+
+	if (sd->audioconvert != NULL) {
+		gst_object_unref (sd->audioconvert);
+		sd->audioconvert = NULL;
+	}
+
+	if (sd->audioresample != NULL) {
+		gst_object_unref (sd->audioresample);
+		sd->audioresample = NULL;
+	}
+
+	if (sd->player != NULL) {
+		g_object_unref (sd->player);
+		sd->player = NULL;
+	}
+
+
+	rb_xfade_stream_dispose_stream_data (sd);
+
+	G_OBJECT_CLASS (rb_xfade_stream_parent_class)->dispose (object);
+}
+
+static void
+rb_xfade_stream_finalize (GObject *object)
+{
+	RBXFadeStream *sd = RB_XFADE_STREAM (object);
+
+	g_static_mutex_free (&sd->lock);
+	g_free (sd->uri);
+
+	if (sd->error != NULL) {
+		g_error_free (sd->error);
+	}
+
+	G_OBJECT_CLASS (rb_xfade_stream_parent_class)->finalize (object);
+}
+
+static void
+rb_xfade_stream_class_init (RBXFadeStreamClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->dispose = rb_xfade_stream_dispose;
+	object_class->finalize = rb_xfade_stream_finalize;
+}
+
+/* caller must hold stream list lock */
+static void
+dump_stream_list (RBPlayerGstXFade *player)
+{
+	GList *l;
+	if (player->priv->streams == NULL) {
+		rb_debug ("stream list is empty");
+	} else {
+		rb_debug ("current stream list:");
+		for (l = player->priv->streams; l != NULL; l = l->next) {
+			RBXFadeStream *stream = (RBXFadeStream *)l->data;
+			const char *statename = "<wtf>";
+			switch (stream->state) {
+			case WAITING:	 	statename = "waiting";		break;
+			case PLAYING:	 	statename = "playing";		break;
+			case PAUSED:	 	statename = "paused";		break;
+
+			case REUSING:		statename = "reusing";		break;
+			case PREROLLING: 	statename = "prerolling"; 	break;
+			case PREROLL_PLAY: 	statename = "preroll->play"; 	break;
+			case FADING_IN: 	statename = "fading in"; 	break;
+			case SEEKING:		statename = "seeking";		break;
+			case SEEKING_PAUSED:	statename = "seeking->paused";	break;
+			case WAITING_EOS: 	statename = "waiting for EOS"; 	break;
+			case FADING_OUT: 	statename = "fading out"; 	break;
+
+			case PENDING_REMOVE:	statename = "pending remove";	break;
+			}
+
+			rb_debug ("[%s] %s", statename, stream->uri);
+		}
+	}
+}
+
+static void
+dump_stream_list_lock (RBPlayerGstXFade *player)
+{
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	dump_stream_list (player);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+}
+
+/* caller must hold stream list lock */
+static RBXFadeStream *
+find_stream_by_uri (RBPlayerGstXFade *player, const char *uri)
+{
+	GList *i;
+	if (uri == NULL)
+		return NULL;
+
+	for (i = player->priv->streams; i != NULL; i = i->next) {
+		RBXFadeStream *stream = (RBXFadeStream *)i->data;
+		if (strcmp (uri, stream->uri) == 0)
+			return g_object_ref (stream);
+	}
+	return NULL;
+}
+
+/* caller must hold stream list lock */
+static RBXFadeStream *
+find_stream_by_element (RBPlayerGstXFade *player, GstElement *element)
+{
+	GList *i;
+
+	for (i = player->priv->streams; i != NULL; i = i->next) {
+		RBXFadeStream *stream;
+		GstElement *e;
+
+		stream = (RBXFadeStream *)i->data;
+		e = element;
+		while (e != NULL) {
+			if (e == stream->bin)
+				return g_object_ref (stream);
+
+			e = GST_ELEMENT_PARENT (e);
+		}
+	}
+
+	return NULL;
+}
+
+/* caller must hold stream list lock */
+static RBXFadeStream *
+find_stream_by_state (RBPlayerGstXFade *player, gint state_mask)
+{
+	GList *i;
+
+	for (i = player->priv->streams; i != NULL; i = i->next) {
+		RBXFadeStream *stream;
+
+		stream = (RBXFadeStream *)i->data;
+		if ((stream->state & state_mask) != 0) {
+			return g_object_ref (stream);
+		}
+	}
+
+	return NULL;
+}
+
+static void
+rb_player_gst_xfade_get_property (GObject *object,
+				  guint prop_id,
+				  GValue *value,
+				  GParamSpec *pspec)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (object);
+
+	switch (prop_id) {
+	case PROP_BUFFER_SIZE:
+		g_value_set_uint (value, player->priv->buffer_size);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+rb_player_gst_xfade_set_property (GObject *object,
+				  guint prop_id,
+				  const GValue *value,
+				  GParamSpec *pspec)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (object);
+
+	switch (prop_id) {
+	case PROP_BUFFER_SIZE:
+		player->priv->buffer_size = g_value_get_uint (value);
+		/* try to adjust any playing streams? */
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+rb_player_gst_xfade_class_init (RBPlayerGstXFadeClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->dispose = rb_player_gst_xfade_dispose;
+	object_class->finalize = rb_player_gst_xfade_finalize;
+	object_class->set_property = rb_player_gst_xfade_set_property;
+	object_class->get_property = rb_player_gst_xfade_get_property;
+
+	g_object_class_install_property (object_class,
+					 PROP_BUFFER_SIZE,
+					 g_param_spec_uint ("buffer-size",
+						 	    "buffer size",
+							    "Buffer size for network streams, in kB",
+							    64, MAX_NETWORK_BUFFER_SIZE, 128,
+							    G_PARAM_READWRITE));
+
+	signals[CAN_REUSE_STREAM] =
+		g_signal_new ("can-reuse-stream",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBPlayerGstXFadeClass, can_reuse_stream),
+			      NULL, NULL,
+			      rb_marshal_BOOLEAN__STRING_STRING_OBJECT,
+			      G_TYPE_BOOLEAN,
+			      3,
+			      G_TYPE_STRING, G_TYPE_STRING, GST_TYPE_ELEMENT);
+	signals[REUSE_STREAM] =
+		g_signal_new ("reuse-stream",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBPlayerGstXFadeClass, reuse_stream),
+			      NULL, NULL,
+			      rb_marshal_VOID__STRING_STRING_OBJECT,
+			      G_TYPE_NONE,
+			      3,
+			      G_TYPE_STRING, G_TYPE_STRING, GST_TYPE_ELEMENT);
+
+	g_type_class_add_private (klass, sizeof (RBPlayerGstXFadePrivate));
+}
+
+static void
+rb_player_init (RBPlayerIface *iface)
+{
+	iface->open = rb_player_gst_xfade_open;
+	iface->opened = rb_player_gst_xfade_opened;
+	iface->close = rb_player_gst_xfade_close;
+	iface->play = rb_player_gst_xfade_play;
+	iface->pause = rb_player_gst_xfade_pause;
+	iface->playing = rb_player_gst_xfade_playing;
+	iface->set_volume = rb_player_gst_xfade_set_volume;
+	iface->get_volume = rb_player_gst_xfade_get_volume;
+	iface->set_replaygain = rb_player_gst_xfade_set_replaygain;
+	iface->seekable = rb_player_gst_xfade_seekable;
+	iface->set_time = rb_player_gst_xfade_set_time;
+	iface->get_time = rb_player_gst_xfade_get_time;
+	iface->multiple_open = (RBPlayerFeatureFunc) rb_true_function;
+}
+
+static void
+rb_player_gst_tee_init (RBPlayerGstTeeIface *iface)
+{
+	iface->add_tee = rb_player_gst_xfade_add_tee;
+	iface->remove_tee = rb_player_gst_xfade_remove_tee;
+}
+
+static void
+rb_player_gst_filter_init (RBPlayerGstFilterIface *iface)
+{
+	iface->add_filter = rb_player_gst_xfade_add_filter;
+	iface->remove_filter = rb_player_gst_xfade_remove_filter;
+}
+
+
+static void
+rb_player_gst_xfade_init (RBPlayerGstXFade *player)
+{
+	player->priv = GET_PRIVATE (player);
+
+	g_static_rec_mutex_init (&player->priv->stream_list_lock);
+	player->priv->cur_volume = 1.0f;
+}
+
+static void
+rb_player_gst_xfade_dispose (GObject *object)
+{
+	RBPlayerGstXFade *player;
+	GList *l;
+
+	g_return_if_fail (RB_IS_PLAYER_GST_XFADE (object));
+	player = RB_PLAYER_GST_XFADE (object);
+
+	stop_sink (player);
+
+	/* clean up streams too */
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	for (l = player->priv->streams; l != NULL; l = l->next) {
+		RBXFadeStream *stream = (RBXFadeStream *)l->data;
+
+		/* unlink instead? */
+		gst_element_set_state (stream->bin, GST_STATE_NULL);
+
+		g_object_unref (stream);
+	}
+	g_list_free (player->priv->streams);
+	player->priv->streams = NULL;
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	if (player->priv->pipeline != NULL) {
+		/* maybe we should keep references to the adder, sink, etc.? */
+		gst_element_set_state (player->priv->pipeline, GST_STATE_NULL);
+		g_object_unref (player->priv->pipeline);
+		player->priv->pipeline = NULL;
+	}
+
+	G_OBJECT_CLASS (rb_player_gst_xfade_parent_class)->dispose (object);
+}
+
+static void
+rb_player_gst_xfade_finalize (GObject *object)
+{
+	RBPlayerGstXFade *player;
+
+	player = RB_PLAYER_GST_XFADE (object);
+
+	if (player->priv->waiting_tees) {
+		g_list_foreach (player->priv->waiting_tees, (GFunc)gst_object_sink, NULL);
+	}
+	g_list_free (player->priv->waiting_tees);
+
+	G_OBJECT_CLASS (rb_player_gst_xfade_parent_class)->finalize (object);
+}
+
+RBPlayer *
+rb_player_gst_xfade_new (GError **error)
+{
+	RBPlayer *mp;
+
+	mp = RB_PLAYER (g_object_new (RB_TYPE_PLAYER_GST_XFADE, NULL, NULL));
+
+	return mp;
+}
+
+/*
+static gboolean
+emit_stream_error (RBXFadeStream *stream)
+{
+	stream->error_idle_id = 0;
+	_rb_player_emit_error (RB_PLAYER (stream->player),
+			       stream->stream_data,
+			       stream->error);
+	return FALSE;
+}
+*/
+
+static void
+post_stream_playing_message (RBXFadeStream *stream)
+{
+	GstMessage *msg;
+	GstStructure *s;
+
+	if (stream->emitted_playing)
+		return;
+
+	rb_debug ("posting " STREAM_PLAYING_MESSAGE " message for stream %s", stream->uri);
+	s = gst_structure_new (STREAM_PLAYING_MESSAGE, NULL);
+	msg = gst_message_new_application (GST_OBJECT (stream->bin), s);
+	gst_element_post_message (stream->bin, msg);
+
+	stream->emitted_playing = TRUE;
+}
+
+static void
+post_buffering_message (RBXFadeStream *stream, guint64 level)
+{
+	GstMessage *message;
+
+	/* somewhat hackish: pretend the stream is already playing
+	 * so that everything above handles the buffering messages
+	 * correctly.
+	 */
+	post_stream_playing_message (stream);
+
+	message = gst_message_new_buffering (GST_OBJECT_CAST (stream->queue), level);
+	gst_element_post_message (stream->queue, message);
+}
+
+/* updates a stream's base time so its position is reported correctly */
+static void
+adjust_stream_base_time (RBXFadeStream *stream)
+{
+	GstFormat format;
+	gint64 output_pos = -1;
+	gint64 stream_pos = -1;
+
+	format = GST_FORMAT_TIME;
+	gst_element_query_position (GST_PAD_PARENT (stream->adder_pad), &format, &output_pos);
+	if (output_pos != -1) {
+		stream->base_time = output_pos;
+	}
+
+	/* offset the base position to account for the current stream position */
+	format = GST_FORMAT_TIME;
+	gst_element_query_position (stream->volume, &format, &stream_pos);
+	if (stream_pos != -1) {
+		rb_debug ("adjusting base time: %lld - %lld => %lld", stream->base_time, stream_pos, stream->base_time - stream_pos);
+		stream->base_time -= stream_pos;
+	}
+}
+
+/* called on a streaming thread when the volume level for a stream changes. */
+static void
+volume_changed_cb (GObject *object, GParamSpec *pspec, RBPlayerGstXFade *player)
+{
+	RBXFadeStream *stream;
+	gdouble vol;
+	char *message = NULL;
+
+	/* post app messages on the bus when fades complete.
+	 * our bus callback will handle them on the main thread.
+	 */
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	stream = find_stream_by_element (player, GST_ELEMENT (object));
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	if (stream == NULL) {
+		rb_debug ("got volume change for unknown stream");
+		return;
+	}
+
+	/* check if the fade is complete */
+	g_object_get (stream->volume, "volume", &vol, NULL);
+	switch (stream->state) {
+	case FADING_IN:
+		if (vol > (stream->fade_end - EPSILON)) {
+			rb_debug ("stream %s fully faded in -> PLAYING state", stream->uri);
+			/*message = FADE_IN_DONE_MESSAGE;*/		/* not actually used */
+			stream->fading = FALSE;
+			stream->state = PLAYING;
+		/*} else {
+			rb_debug ("fading %s in: %f", stream->uri, (float)vol);*/
+		}
+		break;
+	case FADING_OUT:
+		if (vol < (stream->fade_end + EPSILON)) {
+			rb_debug ("stream %s fully faded out", stream->uri);
+			message = FADE_OUT_DONE_MESSAGE;
+			stream->fading = FALSE;
+		} else {
+			/*rb_debug ("fading %s out: %f", stream->uri, (float)vol);*/
+			/* force the volume element out of passthrough mode so it
+			 * continues to update the controller (otherwise, if the
+			 * fade out starts at 1.0, it never gets anywhere)
+			 */
+			gst_base_transform_set_passthrough (GST_BASE_TRANSFORM (stream->volume), FALSE);
+		}
+		break;
+	default:
+		rb_debug ("unexpectedly got a volume change for stream %s to %f (not fading)", stream->uri, (float)vol);
+		break;
+	}
+
+	if (message != NULL) {
+		GstMessage *msg;
+		GstStructure *s;
+
+		rb_debug ("posting %s message for stream %s", message, stream->uri);
+		s = gst_structure_new (message, NULL);
+		msg = gst_message_new_application (GST_OBJECT (object), s);
+		gst_element_post_message (GST_ELEMENT (object), msg);
+	}
+
+	g_object_unref (stream);
+}
+
+/* starts a volume slide on a stream.
+ * volume_changed_cb watches the volume change
+ * and posts a message on the bus when the slide
+ * is done.
+ */
+static void
+start_stream_fade (RBXFadeStream *stream, double start, double end, gint64 time)
+{
+	GValue v = {0,};
+	gint64 pos = -1;
+	GstFormat format = GST_FORMAT_TIME;
+	RBPlayerGstXFade *player = stream->player;
+
+	/* should this take replaygain scaling into account? */
+
+	gst_element_query_position (stream->volume, &format, &pos);
+	if (pos < 0) {
+		/* probably means we haven't actually started the stream yet.
+		 * we also get (weird) negative results with some decoders
+		 * (mad but not flump3dec, for instance) immediately after prerolling.
+		 * the controller doesn't seem to work if we give it a 0 timestamp
+		 * here, but something unnoticeably later does work.
+		 */
+		pos = 100000;
+	}
+	if (format != GST_FORMAT_TIME) {
+		rb_debug ("got position query results in some other format: %s", gst_format_get_name (format));
+		pos = 0;
+	}
+
+	/* apply replaygain scaling */
+	start *= stream->replaygain_scale;
+	end *= stream->replaygain_scale;
+	rb_debug ("fading stream %s: [%f, %" G_GINT64_FORMAT "] to [%f, %" G_GINT64_FORMAT "]", stream->uri, (float)start, pos, (float)end, pos + time);
+
+	/* drop the stream list lock before doing anything to the controller.
+	 * volume changes result in volume_changed_cb being called with the
+	 * controller lock held; volume_changed_cb acquires the stream list lock.
+	 * therefore we can't ever touch the controller with the stream list lock
+	 * held.
+	 */
+
+	g_object_ref (stream);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	/* apparently we need to set the starting volume, otherwise fading in doesn't work. */
+	g_object_set (stream->volume, "volume", start, NULL);
+
+	gst_controller_unset_all (stream->fader, "volume");
+
+	g_value_init (&v, G_TYPE_DOUBLE);
+	g_value_set_double (&v, start);
+	if (gst_controller_set (stream->fader, "volume", pos, &v) == FALSE) {
+		rb_debug ("controller didn't like our start point");
+	}
+	g_value_unset (&v);
+
+	g_value_init (&v, G_TYPE_DOUBLE);
+	g_value_set_double (&v, end);
+	if (gst_controller_set (stream->fader, "volume", pos + time, &v) == FALSE) {
+		rb_debug ("controller didn't like our end point");
+	}
+	g_value_unset (&v);
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	g_object_unref (stream);
+
+	stream->fade_end = end;
+	stream->fading = TRUE;
+
+	/* tiny hack:  if the controlled element is in passthrough mode, the
+	 * controller won't get updated.
+	 */
+	gst_base_transform_set_passthrough (GST_BASE_TRANSFORM (stream->volume), FALSE);
+}
+
+
+static void
+link_unblocked_cb (GstPad *pad, gboolean blocked, RBXFadeStream *stream)
+{
+	rb_debug ("stream %s is unblocked -> FADING_IN | PLAYING", stream->uri);
+	g_static_rec_mutex_lock (&stream->player->priv->stream_list_lock);
+	stream->src_blocked = FALSE;
+	if (stream->fading)
+		stream->state = FADING_IN;
+	else
+		stream->state = PLAYING;
+
+	adjust_stream_base_time (stream);
+
+	/* should handle state change failures here.. */
+	gst_element_set_state (stream->bin, GST_STATE_PLAYING);
+
+	post_stream_playing_message (stream);
+	g_object_unref (stream);
+	g_static_rec_mutex_unlock (&stream->player->priv->stream_list_lock);
+}
+
+/* links a stream bin to the adder
+ * - adds the bin to the pipeline
+ * - links to a new adder pad
+ * - unblocks the stream if it's blocked
+ *
+ * how should this signal errors?  post bus messages?
+ */
+static gboolean
+link_and_unblock_stream (RBXFadeStream *stream)
+{
+	GstPadLinkReturn plr;
+	GstStateChangeReturn scr;
+	RBPlayerGstXFade *player = stream->player;
+
+	if (stream->adder_pad != NULL) {
+		rb_debug ("stream %s is already linked", stream->uri);
+		return TRUE;
+	}
+
+	rb_debug ("linking stream %s", stream->uri);
+
+	start_sink (player);
+
+	if (GST_ELEMENT_PARENT (stream->bin) == NULL)
+		gst_bin_add (GST_BIN (player->priv->pipeline), stream->bin);
+
+	stream->adder_pad = gst_element_get_request_pad (player->priv->adder, "sink%d");
+	if (stream->adder_pad == NULL) {
+		/* er */
+		rb_debug ("couldn't get adder pad to link in new stream");
+		return FALSE;
+	}
+
+	plr = gst_pad_link (stream->ghost_pad, stream->adder_pad);
+	if (GST_PAD_LINK_FAILED (plr)) {
+		/* um */
+		rb_debug ("linking stream pad to adder pad failed: %d", plr);
+		gst_element_release_request_pad (player->priv->adder, stream->adder_pad);
+		stream->adder_pad = NULL;
+		return FALSE;
+	}
+
+	if (stream->src_blocked) {
+		g_object_ref (stream);
+		gst_pad_set_blocked_async (stream->src_pad,
+					   FALSE,
+					   (GstPadBlockCallback) link_unblocked_cb,
+					   stream);
+		return TRUE;
+	} else {
+		rb_debug ("??? stream %s is already unblocked -> PLAYING", stream->uri);
+		stream->state = PLAYING;
+		adjust_stream_base_time (stream);
+
+		scr = gst_element_set_state (stream->bin, GST_STATE_PLAYING);
+
+		post_stream_playing_message (stream);
+
+		return (scr != GST_STATE_CHANGE_FAILURE);
+	}
+}
+
+/*
+ * reuses a stream.  the stream reuse signal is handled by some
+ * external code somewhere.
+ */
+static void
+reuse_stream (RBXFadeStream *stream)
+{
+	g_signal_emit (stream->player,
+		       signals[REUSE_STREAM], 0,
+		       stream->new_uri, stream->uri, stream->bin);
+
+	/* replace URI and stream data */
+	g_free (stream->uri);
+	stream->uri = stream->new_uri;
+
+	rb_xfade_stream_dispose_stream_data (stream);
+	stream->stream_data = stream->new_stream_data;
+	stream->stream_data_destroy = stream->new_stream_data_destroy;
+
+	stream->new_uri = NULL;
+	stream->new_stream_data = NULL;
+	stream->new_stream_data_destroy = NULL;
+
+	link_and_unblock_stream (stream);
+}
+
+
+/*
+ * performs a seek on an unlinked and blocked stream.
+ * if the stream is still in FADING_IN or PLAYING state,
+ * relinks and unblocks the stream.
+ */
+static void
+perform_seek (RBXFadeStream *stream)
+{
+	rb_debug ("sending seek event..");
+	gst_element_seek (stream->volume, 1.0,
+			  GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
+			  GST_SEEK_TYPE_SET, stream->seek_target,
+			  GST_SEEK_TYPE_NONE, -1);
+
+	switch (stream->state) {
+	case SEEKING:
+		link_and_unblock_stream (stream);
+		/* state will be set to PLAYING or FADING_IN when the
+		 * stream is relinked.
+		 */
+		break;
+	case SEEKING_PAUSED:
+		rb_debug ("leaving paused stream %s unlinked", stream->uri);
+		stream->state = PAUSED;
+		break;
+	default:
+		break;
+	}
+}
+
+/* called when a stream's source pad is blocked, so it can be unlinked
+ * from the pipeline.
+ */
+static void
+unlink_blocked_cb (GstPad *pad, gboolean blocked, RBXFadeStream *stream)
+{
+	if (stream->adder_pad == NULL) {
+		rb_debug ("stream %s is already unlinked.  huh?", stream->uri);
+		return;
+	}
+
+	rb_debug ("stream %s is blocked; unlinking", stream->uri);
+
+	if (gst_pad_unlink (stream->ghost_pad, stream->adder_pad) == FALSE) {
+		g_warning ("Couldn't unlink stream %s: things will probably go quite badly from here on", stream->uri);
+	}
+
+	gst_element_release_request_pad (GST_PAD_PARENT (stream->adder_pad), stream->adder_pad);
+	stream->adder_pad = NULL;
+
+	stream->src_blocked = TRUE;
+	stream->emitted_playing = FALSE;
+
+	/* might want a stream-paused signal here? */
+
+	/* handle unlinks for seeking and stream reuse */
+	switch (stream->state) {
+	case SEEKING:
+		perform_seek (stream);
+		break;
+	case REUSING:
+		reuse_stream (stream);
+		break;
+	default:
+		break;
+	}
+}
+
+/*
+ * blocks and unlinks a stream.  this is the only way we can pause a stream -
+ * if the stream is linked to the adder and the audio sink is in PLAYING, the
+ * stream will play.
+ */
+static void
+unlink_and_block_stream (RBXFadeStream *stream)
+{
+	if (stream->adder_pad == NULL) {
+		rb_debug ("stream %s is not linked", stream->uri);
+	} else if (stream->src_blocked) {
+		/* probably shouldn't happen, but we'll handle it anyway */
+		unlink_blocked_cb (stream->src_pad, TRUE, stream);
+	} else {
+		gst_pad_set_blocked_async (stream->src_pad,
+					   TRUE,
+					   (GstPadBlockCallback) unlink_blocked_cb,
+					   stream);
+	}
+}
+
+/*
+ * sets a stream to NULL state, unlinks it from the adder,
+ * removes it from the pipeline, removes it from the
+ * stream list, and frees it (hopefully).
+ *
+ * must not be called on a streaming thread.
+ */
+static void
+unlink_and_dispose_stream (RBPlayerGstXFade *player, RBXFadeStream *stream)
+{
+	GstStateChangeReturn sr;
+
+	rb_debug ("stopping stream %s", stream->uri);
+	sr = gst_element_set_state (stream->bin, GST_STATE_NULL);
+	if (sr == GST_STATE_CHANGE_ASYNC) {
+		/* downward state transitions aren't supposed to return ASYNC.. */
+		rb_debug ("!!! stream %s isn't cooperating", stream->uri);
+		gst_element_get_state (stream->bin, NULL, NULL, GST_CLOCK_TIME_NONE);
+	}
+
+	if (stream->adder_pad != NULL) {
+		rb_debug ("unlinking stream %s", stream->uri);
+		if (gst_pad_unlink (stream->ghost_pad, stream->adder_pad) == FALSE) {
+			g_warning ("Couldn't unlink stream %s: things will probably go quite badly from here on", stream->uri);
+		}
+
+		gst_element_release_request_pad (GST_PAD_PARENT (stream->adder_pad), stream->adder_pad);
+		stream->adder_pad = NULL;
+	}
+
+	gst_bin_remove (GST_BIN (player->priv->pipeline), stream->bin);
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	player->priv->streams = g_list_remove (player->priv->streams, stream);
+	dump_stream_list (player);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	g_object_unref (stream);
+}
+
+/* idle handler used to clean up finished streams */
+static gboolean
+reap_streams (RBPlayerGstXFade *player)
+{
+	GList *t;
+	GList *reap = NULL;
+
+	player->priv->stream_reap_id = 0;
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	dump_stream_list (player);
+	for (t = player->priv->streams; t != NULL; t = t->next) {
+		RBXFadeStream *stream = (RBXFadeStream *)t->data;
+
+		if (stream->state == PENDING_REMOVE) {
+			reap = g_list_prepend (reap, stream);
+		}
+	}
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	for (t = reap; t != NULL; t = t->next) {
+		RBXFadeStream *stream = (RBXFadeStream *)t->data;
+		rb_debug ("reaping stream %s", stream->uri);
+		unlink_and_dispose_stream (player, stream);
+	}
+	g_list_free (reap);
+
+	return FALSE;
+}
+
+/* schedules a call to reap_streams */
+static void
+schedule_stream_reap (RBPlayerGstXFade *player)
+{
+	if (player->priv->stream_reap_id != 0)
+		return;
+
+	dump_stream_list_lock (player);
+	player->priv->stream_reap_id = g_idle_add ((GSourceFunc) reap_streams, player);
+}
+
+/* emits a tag signal from the player, maybe */
+static void
+process_tag (const GstTagList *list, const gchar *tag, RBXFadeStream *stream)
+{
+	int count;
+	RBMetaDataField field;
+	const GValue *val;
+	GValue newval = {0,};
+
+	count = gst_tag_list_get_tag_size (list, tag);
+	if (count < 1)
+		return;
+
+	rb_debug ("got tag %s for stream %s", tag, stream->uri);
+	/* only handle the subset of fields we use for iradio */
+	if (!strcmp (tag, GST_TAG_TITLE))
+		field = RB_METADATA_FIELD_TITLE;
+	else if (!strcmp (tag, GST_TAG_GENRE))
+		field = RB_METADATA_FIELD_GENRE;
+	else if (!strcmp (tag, GST_TAG_COMMENT))
+		field = RB_METADATA_FIELD_COMMENT;
+	else if (!strcmp (tag, GST_TAG_BITRATE))
+		field = RB_METADATA_FIELD_BITRATE;
+#ifdef GST_TAG_MUSICBRAINZ_TRACKID
+	else if (!strcmp (tag, GST_TAG_MUSICBRAINZ_TRACKID))
+		field = RB_METADATA_FIELD_MUSICBRAINZ_TRACKID;
+#endif
+	else
+		return;
+
+	/* of those, all except bitrate are strings */
+	switch (field) {
+	case RB_METADATA_FIELD_BITRATE:
+		g_value_init (&newval, G_TYPE_ULONG);
+		break;
+
+	case RB_METADATA_FIELD_TITLE:
+	case RB_METADATA_FIELD_GENRE:
+	case RB_METADATA_FIELD_COMMENT:
+	case RB_METADATA_FIELD_MUSICBRAINZ_TRACKID:
+	default:
+		g_value_init (&newval, G_TYPE_STRING);
+		break;
+	}
+	val = gst_tag_list_get_value_index (list, tag, 0);
+	if (!g_value_transform (val, &newval)) {
+		rb_debug ("Could not transform tag value type %s into %s",
+			  g_type_name (G_VALUE_TYPE (val)),
+			  g_type_name (G_VALUE_TYPE (&newval)));
+		return;
+	}
+
+	rb_debug ("emitting info field %d for uri %s", field, stream->uri);
+	_rb_player_emit_info (RB_PLAYER (stream->player),
+			      stream->stream_data,
+			      field,
+			      &newval);
+
+	g_value_unset (&newval);
+}
+
+/* gstreamer message bus callback */
+static gboolean
+rb_player_gst_xfade_bus_cb (GstBus *bus, GstMessage *message, RBPlayerGstXFade *player)
+{
+	RBXFadeStream *stream;
+	GstObject *message_src;
+
+	g_return_val_if_fail (player != NULL, FALSE);
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+
+	message_src = GST_MESSAGE_SRC (message);
+	stream = find_stream_by_element (player, GST_ELEMENT (message_src));
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	switch (GST_MESSAGE_TYPE (message)) {
+	case GST_MESSAGE_ERROR:
+	{
+		char *debug;
+		GError *error, *sig_error;
+		int code;
+		gboolean emit = TRUE;
+
+		if (stream == NULL) {
+			rb_debug ("Couldn't find stream for error \"%s\"", error->message);
+			g_error_free (error);
+			break;
+		}
+
+		gst_message_parse_error (message, &error, &debug);
+
+		/* If we've already got an error, ignore 'internal data flow error'
+		 * type messages, as they're too generic to be helpful.
+		 */
+		if (player->priv->emitted_error &&
+		    error->domain == GST_STREAM_ERROR &&
+		    error->code == GST_STREAM_ERROR_FAILED) {
+			rb_debug ("Ignoring generic error \"%s\"", error->message);
+			emit = FALSE;
+		}
+
+		if ((error->domain == GST_CORE_ERROR)
+			|| (error->domain == GST_LIBRARY_ERROR)
+			|| (error->domain == GST_RESOURCE_ERROR && error->code == GST_RESOURCE_ERROR_BUSY)) {
+			code = RB_PLAYER_ERROR_NO_AUDIO;
+		} else {
+			code = RB_PLAYER_ERROR_GENERAL;
+		}
+
+		if (emit) {
+			sig_error = g_error_new_literal (RB_PLAYER_ERROR,
+							 code,
+							 error->message);
+			player->priv->emitted_error = TRUE;
+			_rb_player_emit_error (RB_PLAYER (player), stream->stream_data, sig_error);
+		}
+
+		/* kill the stream? */
+		unlink_and_dispose_stream (player, stream);
+
+		g_error_free (error);
+		g_free (debug);
+		break;
+	}
+	case GST_MESSAGE_TAG:
+		if (stream == NULL) {
+			rb_debug ("got tag message for unknown stream");
+		} else {
+			GstTagList *tags;
+			gst_message_parse_tag (message, &tags);
+
+			gst_tag_list_foreach (tags, (GstTagForeachFunc) process_tag, stream);
+			gst_tag_list_free (tags);
+		}
+		break;
+
+	case GST_MESSAGE_DURATION:
+		if (stream == NULL) {
+			rb_debug ("got duration message for unknown stream");
+		} else {
+			gint64 duration;
+			GstFormat format;
+			gst_message_parse_duration (message, &format, &duration);
+			rb_debug ("got duration %lld for stream %s", duration, stream->uri);
+		}
+		break;
+
+	case GST_MESSAGE_APPLICATION:
+	{
+		/* process fade messages and emit signals for
+		 * other stuff.
+		 */
+		const GstStructure *structure;
+		const char *name;
+
+		structure = gst_message_get_structure (message);
+		name = gst_structure_get_name (structure);
+		if (stream == NULL) {
+			rb_debug ("got application message %s for unknown stream", name);
+		} else if (strcmp (name, STREAM_PLAYING_MESSAGE) == 0) {
+			rb_debug ("got stream playing message for %s", stream->uri);
+			_rb_player_emit_playing_stream (RB_PLAYER (player), stream->stream_data);
+		} else if (strcmp (name, FADE_IN_DONE_MESSAGE) == 0) {
+			/* do something? */
+		} else if (strcmp (name, FADE_OUT_DONE_MESSAGE) == 0) {
+			/* stop the stream and dispose of it */
+			rb_debug ("got fade-out-done for stream %s -> PENDING_REMOVE", stream->uri);
+			stream->state = PENDING_REMOVE;
+			schedule_stream_reap (player);
+		} else if (strcmp (name, STREAM_EOS_MESSAGE) == 0) {
+			/* emit EOS, dispose of the stream, and start any
+			 * streams we had waiting for an EOS.
+			 */
+			rb_debug ("got EOS message for stream %s -> PENDING_REMOVE", stream->uri);
+			_rb_player_emit_eos (RB_PLAYER (player), stream->stream_data);
+			stream->state = PENDING_REMOVE;
+			schedule_stream_reap (player);
+		} else {
+			_rb_player_emit_event (RB_PLAYER (player), stream->stream_data, name, NULL);
+		}
+
+		break;
+	}
+	case GST_MESSAGE_BUFFERING:
+	{
+		const GstStructure *s;
+		gint progress;
+
+		s = gst_message_get_structure (message);
+		if (!gst_structure_get_int (s, "buffer-percent", &progress)) {
+			g_warning ("Could not get value from BUFFERING message");
+			break;
+		}
+
+		if (stream == NULL) {
+			rb_debug ("got buffering message for unknown stream (%d)", progress);
+		} else {
+			_rb_player_emit_buffering (RB_PLAYER (player), stream->stream_data, progress);
+		}
+		break;
+	}
+	default:
+		break;
+	}
+
+	if (stream != NULL)
+		g_object_unref (stream);
+
+	return TRUE;
+}
+
+
+/* queue buffering signal handlers */
+
+
+static gboolean
+stream_queue_probe_cb (GstPad *pad, GstBuffer *data, RBXFadeStream *stream)
+{
+	guint64 level = 0;
+	guint64 progress = 0;
+
+	g_object_get (stream->queue, "current-level-bytes", &level, NULL);
+	if (stream->queue_threshold > 0) {
+		progress = (level * 99) / stream->queue_threshold;
+		if (progress > 99)
+			progress = 99;
+	} else {
+		progress = 99;
+	}
+	rb_debug ("current network buffer level: %" G_GUINT64_FORMAT
+		  "; threshold %" G_GUINT64_FORMAT
+		  " - %" G_GUINT64_FORMAT "%%",
+		  level, stream->queue_threshold, progress);
+
+	post_buffering_message (stream, progress);
+
+	return TRUE;
+}
+
+static void
+stream_queue_threshold_cb (GstElement *queue, RBXFadeStream *stream)
+{
+	GstPad *sinkpad;
+
+	rb_debug ("queue running - removing pad probe, running signal handler");
+
+	/* detach pad probe */
+	sinkpad = gst_element_get_pad (stream->queue, "sink");
+	gst_pad_remove_buffer_probe (sinkpad, stream->queue_probe_id);
+	stream->queue_probe_id = 0;
+	gst_object_unref (sinkpad);
+
+	g_object_set (stream->queue, "min-threshold-bytes", 0, NULL);
+
+	/* detach self */
+	g_signal_handler_disconnect (stream->queue,
+				     stream->queue_threshold_id);
+	stream->queue_threshold_id = 0;
+
+	post_buffering_message (stream, 100);
+}
+
+static void
+stream_queue_underrun_cb (GstElement *queue, RBXFadeStream *stream)
+{
+	rb_debug ("queue underrun - attaching pad probe, running signal handler");
+	GstPad *sinkpad;
+
+	g_object_set (stream->queue, "min-threshold-bytes", stream->queue_threshold, NULL);
+
+	/* attach pad probe to get buffering progress while refilling */
+	if (stream->queue_probe_id == 0) {
+		sinkpad = gst_element_get_pad (stream->queue, "sink");
+		stream->queue_probe_id = gst_pad_add_buffer_probe (sinkpad,
+								   G_CALLBACK (stream_queue_probe_cb),
+								   stream);
+		/* need an event probe for eos/flush events too? */
+		gst_object_unref (sinkpad);
+	}
+
+	if (stream->queue_threshold_id == 0) {
+		stream->queue_threshold_id =
+			g_signal_connect_object (stream->queue,
+						 "running",
+						 G_CALLBACK (stream_queue_threshold_cb),
+						 stream,
+						 0);
+
+		post_buffering_message (stream, 0);
+	}
+
+}
+
+/* links decodebin src pads to the rest of the output pipeline */
+static void
+stream_new_decoded_pad_cb (GstElement *decoder, GstPad *pad, gboolean last, RBXFadeStream *stream)
+{
+	GstCaps *caps;
+	GstStructure *structure;
+	const char *mediatype;
+	GstPad *vpad;
+
+	/* make sure this is an audio pad */
+	caps = gst_pad_get_caps (pad);
+	if (gst_caps_is_empty (caps) || gst_caps_is_any (caps)) {
+		rb_debug ("got empty/any decoded caps.  hmm?");
+		gst_caps_unref (caps);
+		return;
+	}
+
+	structure = gst_caps_get_structure (caps, 0);
+	mediatype = gst_structure_get_name (structure);
+	if (g_str_has_prefix (mediatype, "audio/x-raw") == FALSE) {
+		rb_debug ("got non-audio decoded caps: %s", mediatype);
+		gst_caps_unref (caps);
+		return;
+	}
+
+	/* link to the audioconvert element for the stream */
+	if (stream->decoder_linked) {
+		/* probably should never happen */
+		rb_debug ("hmm, decoder is already linked");
+	} else {
+		rb_debug ("got decoded audio pad for stream %s", stream->uri);
+		vpad = gst_element_get_pad (stream->audioconvert, "sink");
+		gst_pad_link (pad, vpad);
+		gst_object_unref (vpad);
+		stream->decoder_linked = TRUE;
+	}
+}
+
+/* handles EOS events on stream bins.  since the pipeline as a whole
+ * never goes EOS, we don't get an EOS bus message, so we have to handle
+ * it here.
+ *
+ * when an EOS event is received, a bus message is posted, and any streams
+ * in the WAITING_EOS state are started.
+ *
+ * when a new segment event is received, the stream base time is updated
+ * (mostly for seeking)
+ */
+static gboolean
+stream_src_event_cb (GstPad *pad, GstEvent *event, RBXFadeStream *stream)
+{
+	GstMessage *msg;
+	GstStructure *s;
+	RBPlayerGstXFade *player;
+	GList *l;
+
+	switch (GST_EVENT_TYPE (event)) {
+	case GST_EVENT_EOS:
+		rb_debug ("posting EOS message for stream %s", stream->uri);
+		s = gst_structure_new (STREAM_EOS_MESSAGE, NULL);
+		msg = gst_message_new_application (GST_OBJECT (stream->bin), s);
+		gst_element_post_message (stream->bin, msg);
+
+		/* start playing any streams that were waiting on an EOS
+		 * (are we really allowed to do this on a stream thread?)
+		 */
+		player = stream->player;
+		g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+		for (l = player->priv->streams; l != NULL; l = l->next) {
+			RBXFadeStream *pstream = l->data;
+			if (pstream->state == WAITING_EOS) {
+				rb_debug ("starting stream %s on EOS from previous", pstream->uri);
+				if (link_and_unblock_stream (pstream) == FALSE) {
+					/* uh, now what?  errorise? */
+				}
+			}
+		}
+		g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+		break;
+
+	case GST_EVENT_NEWSEGMENT:
+		rb_debug ("got new segment for stream %s", stream->uri);
+		adjust_stream_base_time (stream);
+		break;
+	default:
+		break;
+	}
+
+	return TRUE;
+}
+
+/*
+ * stream playback bin:
+ *
+ * src [ ! queue ] ! decodebin ! audioconvert ! audioresample ! caps ! queue ! volume
+ *
+ * the first queue is only added for non-local streams.  the thresholds
+ * and such are probably going to be configurable at some point,
+ * since people seem to get all whiny if they don't have a buffer
+ * size slider to play with.
+ *
+ * the volume element is used for crossfading and probably replaygain
+ * somehow.
+ */
+static RBXFadeStream *
+create_stream (RBPlayerGstXFade *player, const char *uri, gpointer stream_data, GDestroyNotify stream_data_destroy)
+{
+	RBXFadeStream *stream;
+	GstCaps *caps;
+
+	rb_debug ("creating new stream for %s (stream data %p)", uri, stream_data);
+	stream = g_object_new (RB_TYPE_XFADE_STREAM, NULL, NULL);
+	stream->player = g_object_ref (player);
+	stream->stream_data = stream_data;
+	stream->stream_data_destroy = stream_data_destroy;
+	stream->uri = g_strdup (uri);
+	stream->bin = gst_bin_new (NULL);
+	stream->state = WAITING;
+	if (stream->bin == NULL) {
+		rb_debug ("unable to create new bin");
+		g_object_unref (stream);
+		return NULL;
+	}
+	gst_object_ref (stream->bin);
+
+	stream->source = gst_element_make_from_uri (GST_URI_SRC, stream->uri, NULL);
+	if (stream->source == NULL) {
+		rb_debug ("unable to create source for %s", uri);
+		g_object_unref (stream);
+		return NULL;
+	}
+	gst_object_ref (stream->source);
+
+	/* if the source looks like it might support shoutcast/icecast metadata
+	 * extraction, ask it to do so.
+	 */
+	if (g_str_has_prefix (uri, "http://") &&
+	    g_object_class_find_property (G_OBJECT_GET_CLASS (stream->source),
+		    			  "iradio-mode")) {
+		g_object_set (stream->source, "iradio-mode", TRUE, NULL);
+	}
+
+	if (g_getenv ("USE_DECODEBIN2") != NULL) {
+		stream->decoder = gst_element_factory_make ("decodebin2", NULL);
+	}
+
+	if (stream->decoder == NULL) {
+		stream->decoder = gst_element_factory_make ("decodebin", NULL);
+	}
+
+	if (stream->decoder == NULL) {
+		rb_debug ("unable to create decodebin");
+		g_object_unref (stream);
+		return NULL;
+	}
+	gst_object_ref (stream->decoder);
+
+	/* connect decodebin to audioconvert when it creates its output pad */
+	g_signal_connect_object (stream->decoder,
+				 "new-decoded-pad",
+				 G_CALLBACK (stream_new_decoded_pad_cb),
+				 stream,
+				 0);
+
+	stream->audioconvert = gst_element_factory_make ("audioconvert", NULL);
+	if (stream->audioconvert == NULL) {
+		rb_debug ("unable to create audio converter");
+		g_object_unref (stream);
+		return NULL;
+	}
+	gst_object_ref (stream->audioconvert);
+
+	stream->audioresample = gst_element_factory_make ("audioresample", NULL);
+	if (stream->audioresample == NULL) {
+		rb_debug ("unable to create audio resample");
+		g_object_unref (stream);
+		return NULL;
+	}
+	gst_object_ref (stream->audioresample);
+
+	stream->capsfilter = gst_element_factory_make ("capsfilter", NULL);
+	if (stream->capsfilter == NULL) {
+		rb_debug ("unable to create capsfilter");
+		g_object_unref (stream);
+		return NULL;
+	}
+	gst_object_ref (stream->capsfilter);
+
+	caps = gst_caps_new_simple ("audio/x-raw-int",
+				    "channels", G_TYPE_INT, 2,
+				    "rate",	G_TYPE_INT, 44100,
+				    "width",	G_TYPE_INT, 16,
+				    "depth",	G_TYPE_INT, 16,
+				    NULL);
+	g_object_set (stream->capsfilter, "caps", caps, NULL);
+	gst_caps_unref (caps);
+
+	stream->volume = gst_element_factory_make ("volume", NULL);
+	if (stream->volume == NULL) {
+		rb_debug ("unable to create volume control");
+		g_object_unref (stream);
+		return NULL;
+	}
+	gst_object_ref (stream->volume);
+
+	g_signal_connect_object (stream->volume,
+				 "notify::volume",
+				 G_CALLBACK (volume_changed_cb),
+				 player, 0);
+
+	stream->fader = gst_object_control_properties (G_OBJECT (stream->volume), "volume", NULL);
+	if (stream->fader == NULL) {
+		rb_debug ("unable to create volume controller");
+		g_object_unref (stream);
+		return NULL;
+	}
+	gst_controller_set_interpolation_mode (stream->fader, "volume", GST_INTERPOLATE_LINEAR);
+
+	stream->preroll = gst_element_factory_make ("queue", NULL);
+	if (stream->preroll == NULL) {
+		rb_debug ("unable to create preroll queue");
+		g_object_unref (stream);
+		return NULL;
+	}
+	/* decode a couple of seconds during prerolling, to hopefully avoid underruns.
+	 * we clear this when prerolling is finished.
+	 */
+	g_object_set (stream->preroll, "min-threshold-buffers", 10, NULL);
+
+	/* probably could stand to make this check a bit smarter..
+	 */
+	if (rb_uri_is_local (stream->uri) == FALSE) {
+
+		stream->queue = gst_element_factory_make ("queue", NULL);
+		if (stream->queue == NULL) {
+			rb_debug ("unable to create queue");
+			g_object_unref (stream);
+			return NULL;
+		}
+		gst_object_ref (stream->queue);
+
+		/* set buffer size */
+		stream->queue_threshold = player->priv->buffer_size * 1024;
+		g_object_set (stream->queue,
+			      "min-threshold-bytes",
+			      stream->queue_threshold,
+			      "max-size-bytes",
+			      MAX_NETWORK_BUFFER_SIZE * 2 * 1024,
+			      "max-size-buffers", 0,
+			      "max-size-time", 0,
+			      NULL);
+
+		stream->underrun_id =
+			g_signal_connect_object (stream->queue,
+						 "underrun",
+						 G_CALLBACK (stream_queue_underrun_cb),
+						 stream, 0);
+
+		gst_bin_add_many (GST_BIN (stream->bin),
+				  stream->source,
+				  stream->queue,
+				  stream->decoder,
+				  stream->audioconvert,
+				  stream->audioresample,
+				  stream->capsfilter,
+				  stream->preroll,
+				  stream->volume,
+				  NULL);
+		gst_element_link_many (stream->source,
+				       stream->queue,
+				       stream->decoder,
+				       NULL);
+		gst_element_link_many (stream->audioconvert,
+				       stream->audioresample,
+				       stream->capsfilter,
+				       stream->preroll,
+				       stream->volume,
+				       NULL);
+	} else {
+		gst_bin_add_many (GST_BIN (stream->bin),
+				  stream->source,
+				  stream->decoder,
+				  stream->audioconvert,
+				  stream->audioresample,
+				  stream->capsfilter,
+				  stream->preroll,
+				  stream->volume,
+				  NULL);
+		gst_element_link_many (stream->source,
+				       stream->decoder,
+				       NULL);
+		gst_element_link_many (stream->audioconvert,
+				       stream->audioresample,
+				       stream->capsfilter,
+				       stream->preroll,
+				       stream->volume,
+				       NULL);
+	}
+
+	/* ghost the volume src pad up to the bin */
+	stream->src_pad = gst_element_get_pad (stream->volume, "src");
+	stream->ghost_pad = gst_ghost_pad_new ("src", stream->src_pad);
+	gst_element_add_pad (stream->bin, stream->ghost_pad);
+
+	/* watch for EOS events using a pad probe */
+	gst_pad_add_event_probe (stream->src_pad, (GCallback) stream_src_event_cb, stream);
+
+	/* use the pipeline bus even when not inside the pipeline (?) */
+	gst_element_set_bus (stream->bin, gst_element_get_bus (player->priv->pipeline));
+
+	return stream;
+}
+
+/* starts playback for a stream.
+ * - links to adder and unblocks
+ * - if crossfading:
+ *   - starts the fade in of the new stream
+ *   - starts the fade out of the old stream
+ *   - sets the stream to PLAYING state
+ * - if following (crossfade == 0)
+ *   - if something is playing, set the stream to wait-eos state
+ *   - otherwise, starts it
+ * - if replacing (crossfade == -1)
+ *   - stops any existing stream
+ *   - starts the new stream
+ */
+static gboolean
+actually_start_stream (RBXFadeStream *stream)
+{
+	RBPlayerGstXFade *player = stream->player;
+	gboolean ret = TRUE;
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+
+	rb_debug ("going to start playback for stream %s (crossfade %d) -> FADING_IN | PLAYING", stream->uri, stream->crossfade);
+	if (stream->crossfade > 0) {
+		GList *l;
+
+		for (l = player->priv->streams; l != NULL; l = l->next) {
+			RBXFadeStream *pstream = (RBXFadeStream *)l->data;
+
+			double fade_out_start = 1.0f;
+			gint64 fade_out_time = stream->crossfade * GST_SECOND;
+
+			if (pstream == stream)
+				continue;
+
+			switch (pstream->state) {
+			case FADING_IN:
+				/* fade out from where the fade in got up to */
+				g_object_get (pstream->volume, "volume", &fade_out_start, NULL);
+				fade_out_time = (gint64)(((double) stream->crossfade) * fade_out_start) * GST_SECOND;
+				/* fall through */
+			case PLAYING:
+				rb_debug ("stream %s is playing; crossfading -> FADING_OUT", pstream->uri);
+				start_stream_fade (pstream, fade_out_start, 0.0f, fade_out_time);
+				pstream->state = FADING_OUT;
+
+				start_stream_fade (stream, 0.0f, 1.0f, stream->crossfade * GST_SECOND);
+				break;
+			case PAUSED:
+				rb_debug ("stream %s is paused; replacing it", pstream->uri);
+				unlink_and_dispose_stream (player, pstream);
+				break;
+			default:
+				break;
+			}
+		}
+
+		if (stream->fading == FALSE) {
+			GValue v = {0,};
+
+			rb_debug ("stream isn't fading; setting volume to 1.0");
+			g_value_init (&v, G_TYPE_DOUBLE);
+			g_value_set_double (&v, 1.0);
+			if (gst_controller_set (stream->fader, "volume", 0, &v) == FALSE) {
+				rb_debug ("controller didn't like our start point");
+			}
+			g_value_unset (&v);
+		}
+
+		ret = link_and_unblock_stream (stream);
+	} else if (stream->crossfade == 0) {
+		GList *l;
+		gboolean playing = FALSE;
+		for (l = player->priv->streams; l != NULL; l = l->next) {
+			RBXFadeStream *pstream = (RBXFadeStream *)l->data;
+			if (pstream == stream)
+				continue;
+
+			switch (pstream->state) {
+			case PLAYING:
+			case FADING_IN:
+			case FADING_OUT:
+				rb_debug ("stream %s is already playing", pstream->uri);
+				playing = TRUE;
+				break;
+			case PAUSED:
+				rb_debug ("stream %s is paused; replacing it", pstream->uri);
+				unlink_and_dispose_stream (player, pstream);
+				break;
+			default:
+				break;
+			}
+		}
+
+		if (playing) {
+			/* wait for current stream's EOS */
+			rb_debug ("existing playing stream found; waiting for its EOS -> WAITING_EOS");
+			stream->state = WAITING_EOS;
+		} else {
+			rb_debug ("no playing stream found, so starting immediately");
+			ret = link_and_unblock_stream (stream);
+		}
+	} else {
+		/* replace any existing playing stream.
+		 * might need to use schedule_stream_reap instead of
+		 * doing it directly, since we can get in here on a streaming
+		 * thread..  hmm.
+		 */
+		GList *l;
+		for (l = player->priv->streams; l != NULL; l = l->next) {
+			RBXFadeStream *pstream = (RBXFadeStream *)l->data;
+			if (pstream == stream)
+				continue;
+
+			switch (pstream->state) {
+			case PLAYING:
+			case PAUSED:
+			case FADING_IN:
+				/* kill this one */
+				rb_debug ("stopping stream %s (replaced by new stream)", pstream->uri);
+				unlink_and_dispose_stream (player, pstream);
+				break;
+
+			default:
+				/* let it go */
+				break;
+			}
+		}
+
+		ret = link_and_unblock_stream (stream);
+	}
+
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	return ret;
+}
+
+/* called on a streaming thread when the stream src pad is blocked
+ * (that is, when prerolling is complete).  in some situations we
+ * start playback immediately, otherwise we wait for something else
+ * to happen.
+ */
+static void
+stream_src_blocked_cb (GstPad *pad, gboolean blocked, RBXFadeStream *stream)
+{
+	stream->src_blocked = TRUE;
+
+	g_object_set (stream->preroll, "min-threshold-buffers", 0, NULL);
+
+	/* update stream state */
+	switch (stream->state) {
+	case PREROLLING:
+		rb_debug ("stream %s is prerolled, not starting yet -> WAITING", stream->uri);
+		stream->state = WAITING;
+		break;
+	case PREROLL_PLAY:
+		/* not sure this is actually an acceptable thing to do on a streaming thread.. */
+		rb_debug ("stream %s is prerolled, need to start it", stream->uri);
+		actually_start_stream (stream);
+		break;
+	default:
+		rb_debug ("didn't expect to get preroll completion callback in this state (%d)", stream->state);
+		break;
+	}
+}
+
+/*
+ * starts prerolling for a stream.
+ * since the stream isn't linked to anything yet, we
+ * block the src pad.  when the pad block callback
+ * is called, prerolling is complete and the stream
+ * can be linked and played immediately if required.
+ *
+ * must be called *without* the stream list lock?
+ */
+static gboolean
+preroll_stream (RBPlayerGstXFade *player, RBXFadeStream *stream)
+{
+	GstStateChangeReturn state;
+	gboolean ret = TRUE;
+	gboolean unblock = FALSE;
+
+	gst_pad_set_blocked_async (stream->src_pad,
+				   TRUE,
+				   (GstPadBlockCallback) stream_src_blocked_cb,
+				   stream);
+
+	stream->emitted_playing = FALSE;
+	stream->state = PREROLLING;
+	state = gst_element_set_state (stream->bin, GST_STATE_PAUSED);
+	switch (state) {
+	case GST_STATE_CHANGE_FAILURE:
+		rb_debug ("preroll for stream %s failed (state change failed)", stream->uri);
+		ret = FALSE;
+		unblock = TRUE;
+		break;
+	case GST_STATE_CHANGE_NO_PREROLL:
+		rb_debug ("no preroll for stream %s -> WAITING", stream->uri);
+		unblock = TRUE;
+		stream->state = WAITING;
+		break;
+	case GST_STATE_CHANGE_SUCCESS:
+		rb_debug ("stream %s prerolled synchronously -> WAITING", stream->uri);
+		stream->state = WAITING;
+		/* expect pad block callback to have been called */
+		g_assert (stream->src_blocked);
+		break;
+	case GST_STATE_CHANGE_ASYNC:
+		stream->state = PREROLLING;
+		break;
+	default:
+		g_assert_not_reached();
+	}
+
+	if (unblock) {
+		rb_debug ("unblocking stream source pad");
+		gst_pad_set_blocked_async (stream->src_pad,
+					   FALSE,
+					   NULL,
+					   NULL);
+	}
+
+	return ret;
+}
+
+/*
+ * returns the RBXFadeStream, playback position, and duration of the current
+ * playing stream.
+ */
+static gboolean
+get_times_and_stream (RBPlayerGstXFade *player, RBXFadeStream **pstream, gint64 *pos, gint64 *duration)
+{
+	gboolean got_time = FALSE;
+	RBXFadeStream *stream;
+
+	if (player->priv->pipeline == NULL)
+		return FALSE;
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	stream = find_stream_by_state (player, FADING_IN | PLAYING | PAUSED);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	if (stream != NULL) {
+		if (pstream != NULL) {
+			*pstream = stream;
+		}
+
+		if (pos != NULL) {
+			if (stream->state == PAUSED) {
+				GstFormat format = GST_FORMAT_TIME;
+				*pos = -1;
+
+				gst_element_query_position (stream->volume, &format, pos);
+				if (*pos != -1) {
+					*pos /= GST_SECOND;
+				}
+			} else {
+				/* for playing streams, we subtract the current output position
+				 * (a running counter generated by the adder) from the position
+				 * at which we started playback.
+				 */
+				GstFormat format = GST_FORMAT_TIME;
+				*pos = -1;
+				gst_element_query_position (player->priv->pipeline, &format, pos);
+				if (*pos != -1) {
+					*pos -= stream->base_time;
+					*pos /= GST_SECOND;
+				} else {
+					rb_debug ("position query failed");
+				}
+			}
+		}
+
+		if (duration != NULL) {
+			GstFormat format = GST_FORMAT_TIME;
+			*duration = -1;
+			/* queries are supposed to go to sinks, but the closest thing we
+			 * have in the stream bin is the volume element, which is the last
+			 * linked element.
+			 */
+			gst_element_query_duration (stream->volume, &format, duration);
+			if (*duration != -1) {
+				*duration /= GST_SECOND;
+			}
+		}
+		got_time = TRUE;
+		g_object_unref (stream);
+	} else {
+		rb_debug ("not playing");
+	}
+
+	return got_time;
+}
+
+static gboolean
+tick_timeout (RBPlayerGstXFade *player)
+{
+	gint64 pos = -1;
+	gint64 duration = -1;
+	RBXFadeStream *stream = NULL;
+
+	if (get_times_and_stream (player, &stream, &pos, &duration)) {
+		_rb_player_emit_tick (RB_PLAYER (player), stream->stream_data, pos, duration);
+	}
+
+	return TRUE;
+}
+
+/*
+ * output sink + adder pipeline:
+ *
+ * outputcaps = audio/x-raw-int,channels=2,rate=44100,width=16,depth=16
+ * outputbin = outputcaps ! volume ! filterbin ! audioconvert ! audioresample ! tee ! queue ! gconfaudiosink
+ * silencebin = audiotestsrc wave=silence ! outputcaps
+ *
+ * pipeline = silencebin ! adder ! outputbin
+ *
+ * the tee in output bin has branches attached to it using the
+ * RBPlayerGstTee interface.  filterbin contains elements inserted
+ * using the RBPlayerGstFilter interface.
+ *
+ * silencebin is there for two reasons:
+ * - lets us start the sink without having any streams playing
+ * - probably helps keep things from falling over between streams
+ */
+
+
+static gboolean
+start_sink (RBPlayerGstXFade *player)
+{
+	GstStateChangeReturn sr;
+	GstState state;
+
+	if (player->priv->sink_playing)
+		return TRUE;
+
+	rb_debug ("starting sink");
+
+	/* first, start the output bin.
+	 * this won't preroll until we start the silence bin.
+	 */
+	sr = gst_element_set_state (player->priv->outputbin, GST_STATE_PLAYING);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("output bin state change failed");
+		return FALSE;
+	}
+
+	/* then the adder */
+	sr = gst_element_set_state (player->priv->adder, GST_STATE_PLAYING);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("adder state change failed");
+		return FALSE;
+	}
+
+	/* then the silence bin */
+	sr = gst_element_set_state (player->priv->silencebin, GST_STATE_PLAYING);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("silence bin state change failed");
+		return FALSE;
+	}
+
+	/* now wait for everything to finish */
+	sr = gst_element_get_state (player->priv->silencebin, &state, NULL, GST_CLOCK_TIME_NONE);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("silence bin state change failed (async)");
+		return FALSE;
+	}
+	rb_debug ("silence bin is now in state %s", gst_element_state_get_name (state));
+
+	sr = gst_element_get_state (player->priv->adder, &state, NULL, GST_CLOCK_TIME_NONE);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("adder state change failed (async)");
+		return FALSE;
+	}
+	rb_debug ("adder is now in state %s", gst_element_state_get_name (state));
+
+	sr = gst_element_get_state (player->priv->outputbin, &state, NULL, GST_CLOCK_TIME_NONE);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("output bin state change failed (async)");
+		return FALSE;
+	}
+	rb_debug ("output bin is now in state %s", gst_element_state_get_name (state));
+
+	player->priv->sink_playing = TRUE;
+
+	/* now that the sink is running, start polling for playing position.
+	 * might want to replace this with a complicated set of pad probes
+	 * to avoid polling, but duration queries on the sink are better
+	 * as they account for internal buffering etc.  maybe there's a way
+	 * to account for that in a pad probe callback on the sink's sink pad?
+	 */
+	{
+		gint ms_period = 1000 / RB_PLAYER_GST_XFADE_TICK_HZ;
+		player->priv->tick_timeout_id =
+			g_timeout_add (ms_period,
+				      (GSourceFunc) tick_timeout,
+				      player);
+	}
+
+	return TRUE;
+}
+
+static gboolean
+stop_sink (RBPlayerGstXFade *player)
+{
+	GstStateChangeReturn sr;
+
+	if (player->priv->sink_playing == FALSE)
+		return TRUE;
+
+	rb_debug ("stopping sink");
+	player->priv->sink_playing = FALSE;
+
+	if (player->priv->tick_timeout_id != 0) {
+		g_source_remove (player->priv->tick_timeout_id);
+		player->priv->tick_timeout_id = 0;
+	}
+
+	sr = gst_element_set_state (player->priv->silencebin, GST_STATE_READY);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("couldn't stop silence bin");
+		return FALSE;
+	}
+
+	sr = gst_element_set_state (player->priv->adder, GST_STATE_READY);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("couldn't stop silence bin");
+		return FALSE;
+	}
+
+	sr = gst_element_set_state (player->priv->outputbin, GST_STATE_READY);
+	if (sr == GST_STATE_CHANGE_FAILURE) {
+		rb_debug ("couldn't stop silence bin");
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static gboolean
+create_sink (RBPlayerGstXFade *player, GError **error)
+{
+	GstElement *audiotestsrc;
+	GstElement *audioconvert;
+	GstElement *audioconvert2;
+	GstElement *audioresample;
+	GstElement *capsfilter;
+	GstElement *queue;
+	GstCaps *caps;
+	GstPad *pad;
+	GstPad *filterpad;
+	GstPad *outputghostpad;
+	GstPad *ghostpad;
+	GstPad *reqpad;
+	GstPad *addersrcpad;
+	GstPadLinkReturn plr;
+	GList *l;
+
+	if (player->priv->pipeline != NULL)
+		return TRUE;
+
+	/* set filter caps.
+	 * 44100Hz is about the most reasonable thing to use;
+	 * we have audioconvert+audioresample afterwards in
+	 * case the output device doesn't actually support
+	 * that rate.
+	 */
+	caps = gst_caps_new_simple ("audio/x-raw-int",
+				    "channels", G_TYPE_INT, 2,
+				    "rate",	G_TYPE_INT, 44100,
+				    "width",	G_TYPE_INT, 16,
+				    "depth",	G_TYPE_INT, 16,
+				    NULL);
+
+	player->priv->pipeline = gst_pipeline_new ("rbplayer");
+	gst_bus_add_watch (gst_element_get_bus (GST_ELEMENT (player->priv->pipeline)),
+			     (GstBusFunc) rb_player_gst_xfade_bus_cb, player);
+
+	player->priv->outputbin = gst_bin_new ("outputbin");
+	player->priv->adder = gst_element_factory_make ("adder", "outputadder");
+	player->priv->capsfilter = gst_element_factory_make ("capsfilter", "outputcapsfilter");
+	audioconvert = gst_element_factory_make ("audioconvert", "outputconvert");
+	audioresample = gst_element_factory_make ("audioresample", "outputresample");
+	player->priv->tee = gst_element_factory_make ("tee", "outputtee");
+	queue = gst_element_factory_make ("queue", NULL);
+	player->priv->volume = gst_element_factory_make ("volume", "outputvolume");
+	player->priv->filterbin = gst_bin_new ("filterbin");
+	if (player->priv->pipeline == NULL ||
+	    player->priv->adder == NULL ||
+	    player->priv->capsfilter == NULL ||
+	    audioconvert == NULL ||
+	    audioresample == NULL ||
+	    player->priv->tee == NULL ||
+	    queue == NULL ||
+	    player->priv->volume == NULL ||
+	    player->priv->filterbin == NULL) {
+		/* we could include the element name in the error message,
+		 * but these are all fundamental elements that are always
+		 * available.
+		 */
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     _("Failed to create GStreamer element; check your installation"));
+		return FALSE;
+	}
+
+	g_object_set (player->priv->volume, "volume", player->priv->cur_volume, NULL);
+
+	player->priv->sink = gst_element_factory_make ("gconfaudiosink", NULL);
+	if (player->priv->sink == NULL) {
+		player->priv->sink = gst_element_factory_make ("autoaudiosink", NULL);
+		if (player->priv->sink == NULL) {
+			g_set_error (error,
+				     RB_PLAYER_ERROR,
+				     RB_PLAYER_ERROR_GENERAL,
+				     _("Failed to create audio output element; check your installation"));
+			return FALSE;
+		}
+	} else {
+		/* set the profile property on the gconfaudiosink to "music and movies" */
+		if (g_object_class_find_property (G_OBJECT_GET_CLASS (player->priv->sink), "profile"))
+			g_object_set (player->priv->sink, "profile", 1, NULL);
+	}
+
+	g_object_set (player->priv->capsfilter, "caps", caps, NULL);
+
+	/* set up initial filterbin contents */
+	audioconvert2 = gst_element_factory_make ("audioconvert", NULL);
+	gst_bin_add (GST_BIN (player->priv->filterbin), audioconvert2);
+
+	pad = gst_element_get_pad (audioconvert2, "sink");
+	gst_element_add_pad (player->priv->filterbin,
+			     gst_ghost_pad_new ("sink", pad));
+	gst_object_unref (pad);
+
+	pad = gst_element_get_pad (audioconvert2, "src");
+	gst_element_add_pad (player->priv->filterbin,
+			     gst_ghost_pad_new ("src", pad));
+	gst_object_unref (pad);
+
+	g_object_set (queue, "max-size-buffers", 10, NULL);
+
+	gst_bin_add_many (GST_BIN (player->priv->outputbin),
+			  player->priv->capsfilter,
+			  player->priv->volume,
+			  player->priv->filterbin,
+			  audioconvert,
+			  audioresample,
+			  player->priv->tee,
+			  queue,
+			  player->priv->sink,
+			  NULL);
+	if (gst_element_link_many (player->priv->capsfilter,
+			       player->priv->volume,
+			       player->priv->filterbin,
+			       audioconvert,
+			       audioresample,
+			       player->priv->tee,
+			       queue,
+			       player->priv->sink,
+			       NULL) == FALSE) {
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     _("Failed to link GStreamer pipeline; check your installation"));
+		return FALSE;
+	}
+
+	filterpad = gst_element_get_pad (player->priv->capsfilter, "sink");
+	outputghostpad = gst_ghost_pad_new ("sink", filterpad);
+	gst_element_add_pad (player->priv->outputbin, outputghostpad);
+	gst_object_unref (filterpad);
+
+	/* create silence bin */
+	player->priv->silencebin = gst_bin_new ("silencebin");
+	audiotestsrc = gst_element_factory_make ("audiotestsrc", "silence");
+	g_object_set (audiotestsrc, "wave", 4, NULL);
+
+	audioconvert = gst_element_factory_make ("audioconvert", "silenceconvert");
+
+	capsfilter = gst_element_factory_make ("capsfilter", "silencecapsfilter");
+	g_object_set (capsfilter, "caps", caps, NULL);
+	gst_caps_unref (caps);
+
+	if (audiotestsrc == NULL ||
+	    audioconvert == NULL ||
+	    capsfilter == NULL) {
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     _("Failed to create GStreamer element; check your installation"));
+		return FALSE;
+	}
+
+	gst_bin_add_many (GST_BIN (player->priv->silencebin),
+			  audiotestsrc,
+			  audioconvert,
+			  capsfilter,
+			  NULL);
+	if (gst_element_link_many (audiotestsrc,
+				   audioconvert,
+				   capsfilter,
+				   NULL) == FALSE) {
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     _("Failed to link GStreamer pipeline; check your installation"));
+		return FALSE;
+	}
+
+	filterpad = gst_element_get_pad (capsfilter, "src");
+	ghostpad = gst_ghost_pad_new (NULL, filterpad);
+	gst_element_add_pad (player->priv->silencebin, ghostpad);
+	gst_object_unref (filterpad);
+
+	/* assemble stuff:
+	 * - add everything to the pipeline
+	 * - link adder to output bin
+	 * - link silence bin to adder
+	 */
+	gst_bin_add_many (GST_BIN (player->priv->pipeline),
+			  player->priv->adder,
+			  player->priv->outputbin,
+			  player->priv->silencebin,
+			  NULL);
+
+	addersrcpad = gst_element_get_pad (player->priv->adder, "src");
+	plr = gst_pad_link (addersrcpad, outputghostpad);
+	if (plr != GST_PAD_LINK_OK) {
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     _("Failed to link GStreamer pipeline; check your installation"));
+		return FALSE;
+	}
+
+	reqpad = gst_element_get_request_pad (player->priv->adder, "sink%d");
+	if (reqpad == NULL) {
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     _("Failed to link GStreamer pipeline; check your installation"));
+		return FALSE;
+	}
+
+	plr = gst_pad_link (ghostpad, reqpad);
+	if (plr != GST_PAD_LINK_OK) {
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     _("Failed to link GStreamer pipeline; check your installation"));
+		return FALSE;
+	}
+
+	/* add any tees and filters that were waiting for us */
+	for (l = player->priv->waiting_tees; l != NULL; l = g_list_next (l)) {
+		rb_player_gst_tee_add_tee (RB_PLAYER_GST_TEE (player), GST_ELEMENT (l->data));
+	}
+	g_list_free (player->priv->waiting_tees);
+	player->priv->waiting_tees = NULL;
+
+	for (l = player->priv->waiting_filters; l != NULL; l = g_list_next (l)) {
+		rb_player_gst_filter_add_filter (RB_PLAYER_GST_FILTER (player), GST_ELEMENT (l->data));
+	}
+	g_list_free (player->priv->waiting_filters);
+	player->priv->waiting_filters = NULL;
+
+	return TRUE;
+}
+
+
+
+static gboolean
+rb_player_gst_xfade_open (RBPlayer *iplayer,
+			  const char *uri,
+			  gpointer stream_data,
+			  GDestroyNotify stream_data_destroy,
+			  GError **error)
+{
+	RBXFadeStream *stream;
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	gboolean reused = FALSE;
+	GList *t;
+
+	/* create sink if we don't already have one */
+	if (create_sink (player, error) == FALSE)
+		return FALSE;
+
+	/* see if anyone wants us to reuse an existing stream */
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	for (t = player->priv->streams; t != NULL; t = t->next) {
+		RBXFadeStream *stream = (RBXFadeStream *)t->data;
+
+		switch (stream->state) {
+		case WAITING:
+		case PENDING_REMOVE:
+		case REUSING:
+		case SEEKING:
+		case SEEKING_PAUSED:
+		case PREROLLING:
+		case PREROLL_PLAY:
+			break;
+
+		case PLAYING:
+		case FADING_IN:
+		case FADING_OUT:
+		case WAITING_EOS:
+		case PAUSED:
+			g_signal_emit (player,
+				       signals[CAN_REUSE_STREAM], 0,
+				       uri, stream->uri, stream->bin,
+				       &reused);
+			break;
+		}
+
+		if (reused) {
+			rb_debug ("reusing stream %s for new stream %s", stream->uri, uri);
+			stream->new_uri = g_strdup (uri);
+			stream->new_stream_data = stream_data;
+			stream->new_stream_data_destroy = stream_data_destroy;
+
+			if (stream->state == PAUSED) {
+				reuse_stream (stream);
+			} else {
+				stream->state = REUSING;
+				unlink_and_block_stream (stream);
+			}
+			break;
+		}
+	}
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+	if (reused) {
+		return TRUE;
+	}
+
+	/* construct new stream */
+	stream = create_stream (player, uri, stream_data, stream_data_destroy);
+	if (stream == NULL) {
+		char *err;
+		err = g_strdup_printf (_("Failed to create GStreamer pipeline to play %s"),
+				       uri);
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     err);
+		g_free (err);
+		return FALSE;
+	}
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	player->priv->streams = g_list_prepend (player->priv->streams, stream);
+	dump_stream_list (player);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	/* start prerolling it */
+	if (preroll_stream (player, stream) == FALSE) {
+		char *err;
+
+		/* need to set errors here etc. .. */
+		g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+		player->priv->streams = g_list_remove (player->priv->streams, stream);
+		g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+		g_object_unref (stream);
+
+		err = g_strdup_printf (_("Failed to start playback of %s"),
+				       uri);
+		g_set_error (error,
+			     RB_PLAYER_ERROR,
+			     RB_PLAYER_ERROR_GENERAL,
+			     err);
+		g_free (err);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+
+static gboolean
+rb_player_gst_xfade_close (RBPlayer *iplayer, const char *uri, GError **error)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	gboolean ret = TRUE;
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	if (uri == NULL) {
+		GList *list;
+		GList *l;
+
+		/* need to copy the list as unlink_and_dispose_stream modifies it */
+		list = g_list_copy (player->priv->streams);
+		for (l = list; l != NULL; l = l->next) {
+			RBXFadeStream *stream = (RBXFadeStream *)l->data;
+			unlink_and_dispose_stream (player, stream);
+		}
+		g_list_free (list);
+	} else {
+		/* just stop and close the stream for the specified uri */
+		RBXFadeStream *stream;
+
+		stream = find_stream_by_uri (player, uri);
+		if (stream != NULL) {
+			unlink_and_dispose_stream (player, stream);
+			g_object_unref (stream);
+		} else {
+			rb_debug ("can't find stream for %s", uri);
+			/* XXX set error ?*/
+			ret = FALSE;
+		}
+	}
+
+	if (player->priv->streams == NULL) {
+		ret = stop_sink (player);
+	}
+
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	return ret;
+}
+
+
+static gboolean
+rb_player_gst_xfade_opened (RBPlayer *iplayer)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	RBXFadeStream *stream;
+	gboolean opened = FALSE;
+
+	/* maybe replace this with just a flag somewhere? */
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+
+	stream = find_stream_by_state (player, PREROLLING | PREROLL_PLAY | WAITING_EOS | WAITING | FADING_IN | PLAYING | PAUSED);
+	if (stream != NULL) {
+		opened = TRUE;
+		g_object_unref (stream);
+	}
+
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	return opened;
+}
+
+static gboolean
+rb_player_gst_xfade_play (RBPlayer *iplayer, gint crossfade, GError **error)
+{
+	RBXFadeStream *stream;
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	gboolean ret = TRUE;
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+
+	/* is there anything to play? */
+	if (player->priv->streams == NULL) {
+		/* XXX set error */
+		g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+		return FALSE;
+	}
+
+	stream = g_list_first (player->priv->streams)->data;
+	g_object_ref (stream);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	rb_debug ("playing stream %s, crossfade %d", stream->uri, crossfade);
+
+	/* is the head stream already playing? */
+	switch (stream->state) {
+	case FADING_IN:
+	case FADING_OUT:
+	case PLAYING:
+	case SEEKING:
+		rb_debug ("stream %s is already playing", stream->uri);
+		break;
+
+	case PREROLLING:
+	case PREROLL_PLAY:
+		rb_debug ("stream %s is prerolling; will start playback once prerolling is complete -> PREROLL_PLAY", stream->uri);
+		stream->crossfade = crossfade;
+		stream->state = PREROLL_PLAY;
+		break;
+
+	case PAUSED:
+		rb_debug ("unpausing stream %s", stream->uri);
+		/* consider fading in here?  and/or seek back a bit? */
+		link_and_unblock_stream (stream);
+		break;
+
+	case SEEKING_PAUSED:
+		rb_debug ("unpausing seeking stream %s", stream->uri);
+		stream->state = SEEKING;
+		break;
+
+	case WAITING_EOS:
+	case WAITING:
+		stream->crossfade = crossfade;
+		ret = actually_start_stream (stream);
+		break;
+
+	case REUSING:
+		rb_debug ("currently reusing stream %s; will play when done", stream->uri);
+		break;
+
+	case PENDING_REMOVE:
+		rb_debug ("hmm, can't play streams in PENDING_REMOVE state..");
+		break;
+	}
+
+	g_object_unref (stream);
+
+	return TRUE;
+}
+
+static void
+rb_player_gst_xfade_pause (RBPlayer *iplayer)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	GList *l;
+	gboolean done = FALSE;
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+
+	for (l = player->priv->streams; l != NULL; l = l->next) {
+		RBXFadeStream *stream;
+		stream = (RBXFadeStream *)l->data;
+		switch (stream->state) {
+		case WAITING:
+		case WAITING_EOS:
+			rb_debug ("stream %s is not yet playing, can't pause", stream->uri);
+			break;
+
+		case PREROLLING:
+		case PREROLL_PLAY:
+			rb_debug ("stream %s is prerolling, can't pause", stream->uri);
+			break;
+
+		case REUSING:
+			rb_debug ("stream %s is being reused, can't pause", stream->uri);
+			break;
+
+		case PAUSED:
+		case SEEKING_PAUSED:
+			rb_debug ("stream %s is already paused", stream->uri);
+			done = TRUE;
+			break;
+
+		case PLAYING:
+		case FADING_IN:
+			rb_debug ("pausing stream %s -> PAUSED", stream->uri);
+			/* consider fading out here?  or just kill the volume? */
+			unlink_and_block_stream (stream);
+			stream->state = PAUSED;
+			done = TRUE;
+			break;
+
+		case SEEKING:
+			rb_debug ("pausing seeking stream %s -> SEEKING_PAUSED", stream->uri);
+			stream->state = SEEKING_PAUSED;
+			done = TRUE;
+			break;
+
+		case FADING_OUT:
+			rb_debug ("stream %s is fading out, can't be bothered pausing it", stream->uri);
+			break;
+
+		case PENDING_REMOVE:
+			rb_debug ("stream %s is done, can't pause", stream->uri);
+			break;
+		}
+
+		if (done)
+			break;
+	}
+
+	if (done == FALSE)
+		rb_debug ("couldn't find a stream to pause");
+
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+}
+
+static gboolean
+rb_player_gst_xfade_playing (RBPlayer *iplayer)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	gboolean playing = FALSE;
+	RBXFadeStream *stream;
+
+	/* XXX maybe replace with just a flag? */
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+
+	stream = find_stream_by_state (player, PLAYING | FADING_IN);
+	if (stream != NULL) {
+		playing = TRUE;
+		g_object_unref (stream);
+	}
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+	return playing;
+}
+
+
+static void
+rb_player_gst_xfade_set_replaygain (RBPlayer *iplayer,
+				    const char *uri,
+				    double track_gain, double track_peak,
+				    double album_gain, double album_peak)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	RBXFadeStream *stream;
+	double scale;
+	double gain = 0;
+	double peak = 0;
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	stream = find_stream_by_uri (player, uri);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	if (stream == NULL) {
+		rb_debug ("can't find stream for %s", uri);
+		return;
+	}
+
+	if (album_gain != 0)
+		gain = album_gain;
+	else
+		gain = track_gain;
+
+	if (gain == 0)
+		return;
+
+	scale = pow (10., gain / 20);
+
+	/* anti clip */
+	if (album_peak != 0)
+		peak = album_peak;
+	else
+		peak = track_peak;
+
+	if (peak != 0 && (scale * peak) > 1)
+		scale = 1.0 / peak;
+
+	/* For security */
+	if (scale > 15)
+		scale = 15;
+
+	stream->replaygain_scale = scale;
+
+	/* update the stream volume if we can */
+	switch (stream->state) {
+	case PLAYING:
+	case PAUSED:
+	case SEEKING:
+	case SEEKING_PAUSED:
+	case REUSING:
+	case WAITING:
+	case WAITING_EOS:
+	case PREROLLING:
+	case PREROLL_PLAY:
+		g_object_set (stream->volume, "volume", stream->replaygain_scale, NULL);
+		break;
+
+	case FADING_IN:
+		/* hmm.. need to reset the fade?
+		 * this probably shouldn't happen anyway..
+		 */
+		break;
+
+	case FADING_OUT:
+	case PENDING_REMOVE:
+		/* not much point doing anything here */
+		break;
+	}
+
+	g_object_unref (stream);
+}
+
+
+static void
+rb_player_gst_xfade_set_volume (RBPlayer *iplayer, float volume)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+
+	if (player->priv->volume != NULL) {
+		gdouble v = (gdouble)volume;
+
+		/* maybe use a controller here for smoother changes? */
+		g_object_set (player->priv->volume, "volume", v, NULL);
+	}
+	player->priv->cur_volume = volume;
+}
+
+
+static float
+rb_player_gst_xfade_get_volume (RBPlayer *iplayer)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+
+	return player->priv->cur_volume;
+}
+
+static gboolean
+rb_player_gst_xfade_seekable (RBPlayer *iplayer)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	gboolean can_seek = TRUE;
+	RBXFadeStream *stream;
+
+	/* is this supposed to query the most recently opened stream,
+	 * or the current playing stream?  I really don't know.
+	 */
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	stream = find_stream_by_state (player, FADING_IN | PAUSED | PLAYING);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	if (stream) {
+		GstQuery *query = NULL;
+		query = gst_query_new_seeking (GST_FORMAT_TIME);
+		if (gst_element_query (stream->volume, query)) {
+			gst_query_parse_seeking (query, NULL, &can_seek, NULL, NULL);
+		} else {
+			gst_query_unref (query);
+
+			query = gst_query_new_duration (GST_FORMAT_TIME);
+			can_seek = gst_element_query (stream->volume, query);
+		}
+		gst_query_unref (query);
+		g_object_unref (stream);
+	}
+
+	return can_seek;
+}
+
+static void
+rb_player_gst_xfade_set_time (RBPlayer *iplayer, long time)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	RBXFadeStream *stream;
+
+	g_static_rec_mutex_lock (&player->priv->stream_list_lock);
+	stream = find_stream_by_state (player, FADING_IN | PLAYING | PAUSED);
+	g_static_rec_mutex_unlock (&player->priv->stream_list_lock);
+
+	if (stream == NULL) {
+		rb_debug ("got seek while no playing streams exist");
+		return;
+	}
+
+	stream->seek_target = time * GST_SECOND;
+	if (stream->state == PAUSED) {
+		rb_debug ("seeking in paused stream %s; target %lld", stream->uri, stream->seek_target);
+		perform_seek (stream);
+	} else {
+		rb_debug ("unlinking playing stream %s to seek to %lld", stream->uri, stream->seek_target);
+		stream->state = SEEKING;
+		unlink_and_block_stream (stream);
+	}
+
+	g_object_unref (stream);
+}
+
+static long
+rb_player_gst_xfade_get_time (RBPlayer *iplayer)
+{
+	gint64 pos = -1;
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+
+	get_times_and_stream (player, NULL, &pos, NULL);
+	return pos;
+}
+
+/* RBPlayerGstTee implementation */
+
+typedef struct {
+	RBPlayerGstXFade *player;
+	GstElement *element;
+} RBPlayerGstXFadePipelineOp;
+
+
+static RBPlayerGstXFadePipelineOp *
+new_pipeline_op (RBPlayerGstXFade *player, GstElement *element)
+{
+	RBPlayerGstXFadePipelineOp *op;
+	op = g_new0 (RBPlayerGstXFadePipelineOp, 1);
+	op->player = g_object_ref (player);
+	op->element = g_object_ref (element);
+	return op;
+}
+
+static void
+free_pipeline_op (RBPlayerGstXFadePipelineOp *op)
+{
+	g_object_unref (op->player);
+	g_object_unref (op->element);
+	g_free (op);
+}
+
+static void
+pipeline_op_done (GstPad *pad, gboolean blocked, gpointer nothing)
+{
+	rb_debug ("pipeline unblocked after op.  yay.");
+}
+
+static void
+really_add_tee (GstPad *pad, gboolean blocked, RBPlayerGstXFadePipelineOp *op)
+{
+	GstElement *queue;
+	GstElement *audioconvert;
+	GstElement *bin;
+	GstPad *sinkpad;
+	GstPad *ghostpad;
+
+	rb_debug ("really adding tee %p", op->element);
+
+	/* set up containing bin */
+	bin = gst_bin_new (NULL);
+	queue = gst_element_factory_make ("queue", NULL);
+	audioconvert = gst_element_factory_make ("audioconvert", NULL);
+
+	g_object_set (queue, "max-size-buffers", 3, NULL);
+
+	gst_bin_add_many (GST_BIN (bin), queue, audioconvert, op->element, NULL);
+	gst_element_link_many (queue, audioconvert, op->element, NULL);
+
+	/* add ghost pad */
+	sinkpad = gst_element_get_pad (queue, "sink");
+	ghostpad = gst_ghost_pad_new ("sink", sinkpad);
+	gst_element_add_pad (bin, ghostpad);
+	gst_object_unref (sinkpad);
+
+	/* add it into the pipeline */
+	gst_bin_add (GST_BIN (op->player->priv->outputbin), bin);
+	gst_element_link (op->player->priv->tee, bin);
+
+	/* if we're supposed to be playing, unblock the sink */
+	if (blocked) {
+		rb_debug ("unblocking pad after adding tee");
+		gst_element_set_state (bin, GST_STATE_PLAYING);
+		gst_pad_set_blocked_async (pad, FALSE, (GstPadBlockCallback)pipeline_op_done, NULL);
+	} else {
+		gst_element_set_state (bin, GST_STATE_PAUSED);
+	}
+
+	free_pipeline_op (op);
+}
+
+static void
+really_remove_tee (GstPad *pad, gboolean blocked, RBPlayerGstXFadePipelineOp *op)
+{
+	GstElement *bin;
+
+	rb_debug ("really removing tee %p", op->element);
+
+	/* find bin, remove everything */
+	bin = GST_ELEMENT_PARENT (op->element);
+	g_object_ref (bin);
+	gst_bin_remove (GST_BIN (op->player->priv->outputbin), bin);
+
+	gst_element_set_state (bin, GST_STATE_NULL);
+	gst_bin_remove (GST_BIN (bin), op->element);
+	g_object_unref (bin);
+
+	/* if we're supposed to be playing, unblock the sink */
+	if (blocked) {
+		rb_debug ("unblocking pad after removing tee");
+		gst_pad_set_blocked_async (pad, FALSE, (GstPadBlockCallback)pipeline_op_done, NULL);
+	}
+
+	free_pipeline_op (op);
+}
+
+static gboolean
+tee_op (RBPlayerGstXFade *player,
+	GstElement *element,
+	GstPadBlockCallback callback)
+{
+	RBPlayerGstXFadePipelineOp *op;
+	GstPad *block_pad;
+
+	op = new_pipeline_op (player, element);
+
+	block_pad = gst_element_get_pad (player->priv->volume, "src");
+	if (player->priv->sink_playing) {
+		rb_debug ("blocking the volume src pad to perform a tee operation");
+		gst_pad_set_blocked_async (block_pad,
+					   TRUE,
+					   callback,
+					   op);
+	} else {
+		rb_debug ("sink not playing; calling tee op directly");
+		(*callback) (block_pad, FALSE, op);
+	}
+
+	gst_object_unref (block_pad);
+	return TRUE;
+}
+
+static gboolean
+rb_player_gst_xfade_add_tee (RBPlayerGstTee *iplayer, GstElement *element)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	if (player->priv->tee == NULL) {
+		player->priv->waiting_tees = g_list_prepend (player->priv->waiting_tees, element);
+		return TRUE;
+	}
+
+	return tee_op (player,
+		       element,
+		       (GstPadBlockCallback) really_add_tee);
+}
+
+static gboolean
+rb_player_gst_xfade_remove_tee (RBPlayerGstTee *iplayer, GstElement *element)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	if (player->priv->tee == NULL) {
+		gst_object_sink (element);
+		player->priv->waiting_tees = g_list_remove (player->priv->waiting_tees, element);
+		return TRUE;
+	}
+
+	return tee_op (RB_PLAYER_GST_XFADE (iplayer),
+		       element,
+		       (GstPadBlockCallback) really_remove_tee);
+}
+
+
+/* RBPlayerGstFilter implementation */
+
+
+static void
+really_add_filter (GstPad *pad,
+		   gboolean blocked,
+		   RBPlayerGstXFadePipelineOp *op)
+{
+	GstPad *binsinkpad;
+	GstPad *binsrcpad;
+	GstPad *realpad;
+	GstPad *ghostpad;
+	GstElement *bin;
+	GstElement *audioconvert;
+	GstIterator *sink_pads;
+	gboolean sink_pad_found;
+	gboolean stop_scan;
+	gpointer element_sink_pad;
+	GstPadLinkReturn link;
+
+	rb_debug ("adding filter %p", op->element);
+
+	/* find the element's first unlinked source pad */
+	sink_pad_found = FALSE;
+	stop_scan = FALSE;
+	sink_pads = gst_element_iterate_sink_pads (op->element);
+	while (!sink_pad_found && !stop_scan) {
+		gpointer *esp_pointer = &element_sink_pad; /* stop type-punning warnings */
+		switch (gst_iterator_next (sink_pads, esp_pointer)) {
+			case GST_ITERATOR_OK:
+				sink_pad_found = !gst_pad_is_linked (GST_PAD (element_sink_pad));
+				break;
+			case GST_ITERATOR_RESYNC:
+				gst_iterator_resync (sink_pads);
+				break;
+			case GST_ITERATOR_ERROR:
+			case GST_ITERATOR_DONE:
+				stop_scan = TRUE;
+				break;
+		}
+	}
+	gst_iterator_free (sink_pads);
+
+	if (!sink_pad_found) {
+		g_warning ("Could not find a free sink pad on filter");
+		return;
+	}
+
+	/* create containing bin */
+	bin = gst_bin_new (NULL);
+	audioconvert = gst_element_factory_make ("audioconvert", NULL);
+	gst_bin_add_many (GST_BIN (bin), op->element, audioconvert, NULL);
+	gst_element_link (op->element, audioconvert);
+
+	/* create ghost pads */
+	binsinkpad = gst_ghost_pad_new ("sink", GST_PAD (element_sink_pad));
+	gst_element_add_pad (bin, binsinkpad);
+
+	realpad = gst_element_get_pad (audioconvert, "src");
+	binsrcpad = gst_ghost_pad_new ("src", realpad);
+	gst_element_add_pad (bin, binsrcpad);
+	gst_object_unref (realpad);
+
+	/* chuck it into the filter bin */
+	gst_bin_add (GST_BIN (op->player->priv->filterbin), bin);
+
+	ghostpad = gst_element_get_pad (op->player->priv->filterbin, "src");
+	realpad = gst_ghost_pad_get_target (GST_GHOST_PAD (ghostpad));
+	gst_ghost_pad_set_target (GST_GHOST_PAD (ghostpad), binsrcpad);
+	gst_object_unref (ghostpad);
+
+	link = gst_pad_link (realpad, binsinkpad);
+	gst_object_unref (realpad);
+	if (link != GST_PAD_LINK_OK) {
+		g_warning ("could not link new filter into pipeline");
+		/* can't really do much else at this point.. */
+	}
+
+	/* if we're supposed to be playing, unblock the sink */
+	if (blocked) {
+		rb_debug ("unblocking pad after adding tee");
+		gst_element_set_state (bin, GST_STATE_PLAYING);
+		gst_pad_set_blocked_async (pad, FALSE, (GstPadBlockCallback)pipeline_op_done, NULL);
+	} else {
+		gst_element_set_state (bin, GST_STATE_PAUSED);
+	}
+
+	free_pipeline_op (op);
+}
+
+static void
+really_remove_filter (GstPad *pad,
+		      gboolean blocked,
+		      RBPlayerGstXFadePipelineOp *op)
+{
+	GstPad *mypad;
+	GstPad *prevpad, *nextpad;
+	GstPad *ghostpad;
+	GstPad *targetpad;
+	GstElement *bin;
+
+	/* get the containing bin and remove it */
+	bin = GST_ELEMENT (gst_element_get_parent (op->element));
+	if (bin == NULL) {
+		return;
+	}
+
+	rb_debug ("removing filter %p", op->element);
+
+	/* probably check return? */
+	gst_element_set_state (bin, GST_STATE_NULL);
+
+	mypad = gst_element_get_pad (bin, "sink");
+	prevpad = gst_pad_get_peer (mypad);
+	gst_pad_unlink (prevpad, mypad);
+	gst_object_unref (mypad);
+
+	ghostpad = gst_element_get_pad (bin, "src");
+	nextpad = gst_element_get_pad (op->player->priv->filterbin, "src");
+
+	targetpad = gst_ghost_pad_get_target (GST_GHOST_PAD (nextpad));
+	if (targetpad == ghostpad) {
+		/* we're at the end of the filter chain, so redirect
+		 * the ghostpad to the previous element.
+		 */
+		gst_ghost_pad_set_target (GST_GHOST_PAD (nextpad), prevpad);
+		gst_object_unref (nextpad);
+	} else {
+		/* we are in the middle, so link the previous and next elements */
+		gst_object_unref (nextpad);
+		nextpad = gst_pad_get_peer (ghostpad);
+		gst_pad_unlink (ghostpad, nextpad);
+
+		if (gst_pad_link (prevpad, nextpad) != GST_PAD_LINK_OK) {
+			/* crap */
+		}
+	}
+
+	gst_object_unref (prevpad);
+	gst_object_unref (ghostpad);
+	gst_object_unref (targetpad);
+	gst_object_unref (nextpad);
+
+	gst_bin_remove (GST_BIN (op->player->priv->filterbin), bin);
+	gst_object_unref (bin);
+
+	/* if we're supposed to be playing, unblock the sink */
+	if (blocked) {
+		rb_debug ("unblocking pad after adding tee");
+		gst_pad_set_blocked_async (pad, FALSE, (GstPadBlockCallback)pipeline_op_done, NULL);
+	}
+
+	free_pipeline_op (op);
+}
+
+static gboolean
+filter_op (RBPlayerGstXFade *player,
+	   GstElement *element,
+	   GstPadBlockCallback callback)
+{
+	RBPlayerGstXFadePipelineOp *op;
+	GstPad *block_pad;
+
+	op = new_pipeline_op (player, element);
+
+	block_pad = gst_element_get_pad (player->priv->filterbin, "sink");
+	if (player->priv->sink_playing) {
+		rb_debug ("blocking the filterbin sink pad to perform a filter operation");
+		gst_pad_set_blocked_async (block_pad,
+					   TRUE,
+					   callback,
+					   op);
+	} else {
+		rb_debug ("sink not playing; calling filter op directly");
+		(*callback) (block_pad, FALSE, op);
+	}
+
+	gst_object_unref (block_pad);
+	return TRUE;
+}
+
+static gboolean
+rb_player_gst_xfade_add_filter (RBPlayerGstFilter *iplayer, GstElement *element)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	if (player->priv->filterbin == NULL) {
+		player->priv->waiting_filters = g_list_prepend (player->priv->waiting_filters, element);
+		return TRUE;
+	}
+
+	return filter_op (player,
+			  element,
+			  (GstPadBlockCallback) really_add_filter);
+}
+
+
+static gboolean
+rb_player_gst_xfade_remove_filter (RBPlayerGstFilter *iplayer, GstElement *element)
+{
+	RBPlayerGstXFade *player = RB_PLAYER_GST_XFADE (iplayer);
+	if (player->priv->filterbin == NULL) {
+		gst_object_sink (element);
+		player->priv->waiting_filters = g_list_remove (player->priv->waiting_filters, element);
+		return TRUE;
+	}
+	return filter_op (player,
+			  element,
+			  (GstPadBlockCallback) really_remove_filter);
+}
+
+
+
+
+
diff -rN -u -p old-npb/backends/gstreamer/rb-player-gst-xfade.h new-npb/backends/gstreamer/rb-player-gst-xfade.h
--- old-npb/backends/gstreamer/rb-player-gst-xfade.h	1970-01-01 10:00:00.000000000 +1000
+++ new-npb/backends/gstreamer/rb-player-gst-xfade.h	2007-03-18 18:49:31.000000000 +1000
@@ -0,0 +1,63 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2006 James Livingston <jrl@ids.org.au>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __RB_PLAYER_GST_XFADE_H__
+#define __RB_PLAYER_GST_XFADE_H__
+
+#include <glib-object.h>
+
+#include <gst/gst.h>
+
+#include "rb-player.h"
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_PLAYER_GST_XFADE      (rb_player_gst_xfade_get_type ())
+#define RB_PLAYER_GST_XFADE(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), RB_TYPE_PLAYER_GST_XFADE, RBPlayerGstXFade))
+#define RB_IS_PLAYER_GST_XFADE(obj)   (G_TYPE_CHECK_INSTANCE_TYPE ((obj), RB_TYPE_PLAYER_GST_XFADE))
+
+typedef struct _RBPlayerGstXFadePrivate RBPlayerGstXFadePrivate;
+
+typedef struct
+{
+	GObject obj;
+	RBPlayerGstXFadePrivate *priv;
+} RBPlayerGstXFade;
+
+typedef struct
+{
+	GObjectClass obj_class;
+
+	gboolean (*can_reuse_stream) (RBPlayerGstXFade *player,
+				      const char *new_uri,
+				      const char *stream_uri,
+				      GstElement *stream_bin);
+	void (*reuse_stream) (RBPlayerGstXFade *player,
+			      const char *new_uri,
+			      const char *stream_uri,
+			      GstElement *stream_bin);
+} RBPlayerGstXFadeClass;
+
+RBPlayer*	rb_player_gst_xfade_new (GError **error);
+GType		rb_player_gst_xfade_get_type (void);
+
+G_END_DECLS
+
+#endif /* __RB_PLAYER_GST_XFADE_H__ */
diff -rN -u -p old-npb/backends/rb-player.c new-npb/backends/rb-player.c
--- old-npb/backends/rb-player.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/backends/rb-player.c	2007-03-18 18:49:31.000000000 +1000
@@ -22,6 +22,7 @@
 
 #include "rb-player.h"
 #include "rb-player-gst.h"
+#include "rb-player-gst-xfade.h"
 #include "rb-marshal.h"
 
 /* Signals */
@@ -32,6 +33,7 @@ enum {
 	ERROR,
 	TICK,
 	EVENT,
+	PLAYING_STREAM,
 	LAST_SIGNAL
 };
 
@@ -46,54 +48,64 @@ rb_player_interface_init (RBPlayerIface 
 			      G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE,
 			      G_STRUCT_OFFSET (RBPlayerIface, eos),
 			      NULL, NULL,
-			      g_cclosure_marshal_VOID__VOID,
+			      g_cclosure_marshal_VOID__POINTER,
 			      G_TYPE_NONE,
-			      0);
+			      1, G_TYPE_POINTER);
 	signals[INFO] =
 		g_signal_new ("info",
 			      G_TYPE_FROM_INTERFACE (iface),
 			      G_SIGNAL_RUN_LAST,
 			      G_STRUCT_OFFSET (RBPlayerIface, info),
 			      NULL, NULL,
-			      rb_marshal_VOID__INT_POINTER,
+			      rb_marshal_VOID__POINTER_INT_POINTER,
 			      G_TYPE_NONE,
-			      2, G_TYPE_INT, G_TYPE_VALUE);
+			      3, G_TYPE_POINTER, G_TYPE_INT, G_TYPE_VALUE);
 	signals[ERROR] =
 		g_signal_new ("error",
 			      G_TYPE_FROM_INTERFACE (iface),
 			      G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE,
 			      G_STRUCT_OFFSET (RBPlayerIface, error),
 			      NULL, NULL,
-			      g_cclosure_marshal_VOID__POINTER,
+			      rb_marshal_VOID__POINTER_POINTER,
 			      G_TYPE_NONE,
-			      1,
-			      G_TYPE_POINTER);
+			      2,
+			      G_TYPE_POINTER, G_TYPE_POINTER);
 	signals[TICK] =
 		g_signal_new ("tick",
 			      G_TYPE_FROM_INTERFACE (iface),
 			      G_SIGNAL_RUN_LAST,
 			      G_STRUCT_OFFSET (RBPlayerIface, tick),
 			      NULL, NULL,
-			      g_cclosure_marshal_VOID__LONG,
+			      rb_marshal_VOID__POINTER_LONG_LONG,
 			      G_TYPE_NONE,
-			      1,
-			      G_TYPE_LONG);
+			      3,
+			      G_TYPE_POINTER, G_TYPE_LONG, G_TYPE_LONG);
 	signals[BUFFERING] =
 		g_signal_new ("buffering",
 			      G_TYPE_FROM_INTERFACE (iface),
 			      G_SIGNAL_RUN_LAST,
 			      G_STRUCT_OFFSET (RBPlayerIface, buffering),
 			      NULL, NULL,
-			      g_cclosure_marshal_VOID__UINT,
+			      rb_marshal_VOID__POINTER_UINT,
 			      G_TYPE_NONE,
-			      1,
-			      G_TYPE_UINT);
+			      2,
+			      G_TYPE_POINTER, G_TYPE_UINT);
 	signals[EVENT] =
 		g_signal_new ("event",
 			      G_TYPE_FROM_INTERFACE (iface),
 			      G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
 			      G_STRUCT_OFFSET (RBPlayerIface, event),
 			      NULL, NULL,
+			      rb_marshal_VOID__POINTER_POINTER,
+			      G_TYPE_NONE,
+			      2,
+			      G_TYPE_POINTER, G_TYPE_POINTER);
+	signals[PLAYING_STREAM] =
+		g_signal_new ("playing-stream",
+			      G_TYPE_FROM_INTERFACE (iface),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBPlayerIface, playing_stream),
+			      NULL, NULL,
 			      g_cclosure_marshal_VOID__POINTER,
 			      G_TYPE_NONE,
 			      1,
@@ -124,14 +136,35 @@ rb_player_get_type (void)
 	return our_type;
 }
 
+/**
+ * rb_player_open:
+ * @player:	a #RBPlayer
+ * @uri:	URI to open
+ * @stream_data: arbitrary data to associate with the stream
+ * @stream_data_destroy: function to call to destroy the stream data
+ * @error:	returns error information
+ *
+ * Prepares a stream for playback.  Depending on the player
+ * implementation, this may stop any existing stream being
+ * played.  The stream preparation process may continue
+ * asynchronously, in which case errors may be reported from
+ * rb_player_play or using the 'error' signal.
+ *
+ * @returns TRUE if the stream preparation was not unsuccessful
+ */
 gboolean
-rb_player_open (RBPlayer *player, const char *uri, GError **error)
+rb_player_open (RBPlayer *player, const char *uri, gpointer stream_data, GDestroyNotify stream_data_destroy, GError **error)
 {
 	RBPlayerIface *iface = RB_PLAYER_GET_IFACE (player);
 
-	return iface->open (player, uri, error);
+	return iface->open (player, uri, stream_data, stream_data_destroy, error);
 }
 
+/**
+ * rb_player_opened:
+ * @player: 	a #RBPlayer
+ * @returns TRUE if a stream is prepared for playback
+ */
 gboolean
 rb_player_opened (RBPlayer *player)
 {
@@ -140,22 +173,71 @@ rb_player_opened (RBPlayer *player)
 	return iface->opened (player);
 }
 
+/**
+ * rb_player_close:
+ * @player:	a #RBPlayer
+ * @uri:	optionally, the URI of the stream to close
+ * @error:	returns error information
+ *
+ * If a URI is specified, this will close the stream corresponding
+ * to that URI and free any resources related resources.  If @uri
+ * is NULL, this will close all streams.
+ *
+ * If no streams remain open after this call, the audio device will
+ * be released.
+ *
+ * @returns TRUE if a stream was found and closed
+ */
 gboolean
-rb_player_close (RBPlayer *player, GError **error)
+rb_player_close (RBPlayer *player, const char *uri, GError **error)
 {
 	RBPlayerIface *iface = RB_PLAYER_GET_IFACE (player);
 
-	return iface->close (player, error);
+	return iface->close (player, uri, error);
 }
 
+/**
+ * rb_player_play:
+ * @player:	a #RBPlayer
+ * @crossfade:	requested crossfade duration
+ * @error:	returns error information
+ *
+ * Starts playback of the most recently opened stream.
+ * If @crossfade is greater than zero, the player may attempt
+ * to crossfade the new stream with any existing streams.
+ * If a stream is faded out as a result, an EOS signal will be
+ * emitted when the fade is complete, which may not be at the
+ * actual end of the stream. Or maybe no EOS signal will be
+ * emitted, even if it reaches EOS while fading out.
+ * I haven't decided yet.
+ *
+ * If @crossfade is zero, the player may attempt to start the
+ * stream immediately after the current playing stream reaches
+ * EOS.  This may or may not result in the phenomemon known
+ * as 'gapless playback'.
+ *
+ * If @crossfade is less than zero, the player will stop any
+ * existing stream before starting the new stream. It may do
+ * this anyway, regardless of the value of @crossfade.
+ *
+ * @returns: TRUE if playback started successfully
+ */
 gboolean
-rb_player_play (RBPlayer *player, GError **error)
+rb_player_play (RBPlayer *player, gint crossfade, GError **error)
 {
 	RBPlayerIface *iface = RB_PLAYER_GET_IFACE (player);
 
-	return iface->play (player, error);
+	return iface->play (player, crossfade, error);
 }
 
+/**
+ * rb_player_pause:
+ * @player:	a #RBPlayer
+ *
+ * Pauses playback of the most recently started stream.  Any
+ * streams being faded out may continue until the fade is
+ * complete.
+ */
 void
 rb_player_pause (RBPlayer *player)
 {
@@ -164,6 +246,12 @@ rb_player_pause (RBPlayer *player)
 	iface->pause (player);
 }
 
+/**
+ * rb_player_playing:
+ * @player:	a #RBPlayer
+ * @returns:	TRUE if a stream is currently being played (not paused
+ *  or being faded out).
+ */
 gboolean
 rb_player_playing (RBPlayer *player)
 {
@@ -172,6 +260,15 @@ rb_player_playing (RBPlayer *player)
 	return iface->playing (player);
 }
 
+/**
+ * rb_player_set_volume:
+ * @player:	a #RBPlayer
+ * @volume:	new output volume level
+ *
+ * Adjusts the output volume level.  This affects all streams.
+ * The player may use a hardware volume control to implement
+ * this volume adjustment.
+ */
 void
 rb_player_set_volume (RBPlayer *player, float volume)
 {
@@ -180,6 +277,11 @@ rb_player_set_volume (RBPlayer *player, 
 	iface->set_volume (player, volume);
 }
 
+/**
+ * rb_player_get_volume:
+ * @player:	a #RBPlayer
+ * @returns:	current output volume level
+ */
 float
 rb_player_get_volume (RBPlayer *player)
 {
@@ -188,16 +290,33 @@ rb_player_get_volume (RBPlayer *player)
 	return iface->get_volume (player);
 }
 
+/**
+ * rb_player_set_replaygain:
+ * @player:	a #RBPlayer
+ * @uri:	URI of stream to adjust
+ * @track_gain: ReplayGain track gain level
+ * @track_peak: ReplayGain track peak level
+ * @album_gain: ReplayGain album gain level
+ * @album_peak: ReplayGain album peak level
+ *
+ * Sets ReplayGain values for a stream
+ */
 void
 rb_player_set_replaygain (RBPlayer *player,
+			  const char *uri,
 			  double track_gain, double track_peak,
 			  double album_gain, double album_peak)
 {
 	RBPlayerIface *iface = RB_PLAYER_GET_IFACE (player);
 
-	iface->set_replaygain (player, track_gain, track_peak, album_gain, album_peak);
+	iface->set_replaygain (player, uri, track_gain, track_peak, album_gain, album_peak);
 }
 
+/**
+ * rb_player_seekable:
+ * @player:	a #RBPlayer
+ * @returns:	TRUE if the current stream is seekable
+ */
 gboolean
 rb_player_seekable (RBPlayer *player)
 {
@@ -206,6 +325,15 @@ rb_player_seekable (RBPlayer *player)
 	return iface->seekable (player);
 }
 
+/**
+ * rb_player_set_time:
+ * @player:	a #RBPlayer
+ * @time:	seek target position in seconds
+ *
+ * Attempts to seek in the current stream.  The player
+ * may ignore this if the stream is not seekable.
+ * The seek may take place asynchronously.
+ */
 void
 rb_player_set_time (RBPlayer *player, long time)
 {
@@ -214,6 +342,11 @@ rb_player_set_time (RBPlayer *player, lo
 	iface->set_time (player, time);
 }
 
+/**
+ * rb_player_get_time:
+ * @player:	a #RBPlayer
+ * @returns:	the current playback position in the current stream
+ */
 long
 rb_player_get_time (RBPlayer *player)
 {
@@ -222,48 +355,83 @@ rb_player_get_time (RBPlayer *player)
 	return iface->get_time (player);
 }
 
+/**
+ * rb_player_multiple_open:
+ * @player:	a #RBPlayer
+ * @returns:	TRUE if the player supports multiple open streams
+ * 		(not necessarily multiple playing streams, though)
+ */
+gboolean
+rb_player_multiple_open (RBPlayer *player)
+{
+	RBPlayerIface *iface = RB_PLAYER_GET_IFACE (player);
+
+	if (iface->multiple_open)
+		return iface->multiple_open (player);
+	else
+		return FALSE;
+}
+
+/**
+ * rb_player_new:
+ * @want_crossfade: if TRUE, try to use a backend that supports
+ * 		    crossfading and other track transitions.
+ * @error:	returns error information
+ *
+ * Creates a new player object.
+ */
 RBPlayer*
-rb_player_new (GError **error)
+rb_player_new (gboolean want_crossfade, GError **error)
 {
-	return rb_player_gst_new (error);
+	if (want_crossfade)
+		return rb_player_gst_xfade_new (error);
+	else
+		return rb_player_gst_new (error);
 }
 
 void
-_rb_player_emit_eos (RBPlayer *player)
+_rb_player_emit_eos (RBPlayer *player, gpointer stream_data)
 {
-	g_signal_emit (player, signals[EOS], 0);
+	g_signal_emit (player, signals[EOS], 0, stream_data);
 }
 
 void
 _rb_player_emit_info (RBPlayer *player,
+		      gpointer stream_data,
 		      RBMetaDataField field,
 		      GValue *value)
 {
-	g_signal_emit (player, signals[INFO], 0, field, value);
+	g_signal_emit (player, signals[INFO], 0, stream_data, field, value);
+}
+
+void
+_rb_player_emit_buffering (RBPlayer *player, gpointer stream_data, guint progress)
+{
+	g_signal_emit (player, signals[BUFFERING], 0, stream_data, progress);
 }
 
 void
-_rb_player_emit_buffering (RBPlayer *player, guint progress)
+_rb_player_emit_error (RBPlayer *player, gpointer stream_data, GError *error)
 {
-	g_signal_emit (player, signals[BUFFERING], 0, progress);
+	g_signal_emit (player, signals[ERROR], 0, stream_data, error);
 }
 
 void
-_rb_player_emit_error (RBPlayer *player, GError *error)
+_rb_player_emit_tick (RBPlayer *player, gpointer stream_data, long elapsed, long duration)
 {
-	g_signal_emit (player, signals[ERROR], 0, error);
+	g_signal_emit (player, signals[TICK], 0, stream_data, elapsed, duration);
 }
 
 void
-_rb_player_emit_tick (RBPlayer *player, long elapsed)
+_rb_player_emit_event (RBPlayer *player, gpointer stream_data, const char *name, gpointer data)
 {
-	g_signal_emit (player, signals[TICK], 0, elapsed);
+	g_signal_emit (player, signals[EVENT], g_quark_from_string (name), stream_data, data);
 }
 
 void
-_rb_player_emit_event (RBPlayer *player, const char *name, gpointer data)
+_rb_player_emit_playing_stream (RBPlayer *player, gpointer stream_data)
 {
-	g_signal_emit (player, signals[EVENT], g_quark_from_string (name), data);
+	g_signal_emit (player, signals[PLAYING_STREAM], 0, stream_data);
 }
 
 GQuark
diff -rN -u -p old-npb/backends/rb-player.h new-npb/backends/rb-player.h
--- old-npb/backends/rb-player.h	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/backends/rb-player.h	2007-03-18 18:49:31.000000000 +1000
@@ -48,6 +48,8 @@ GQuark rb_player_error_quark (void);
 
 typedef struct _RBPlayer RBPlayer;
 
+typedef gboolean (*RBPlayerFeatureFunc) (RBPlayer *player);
+
 typedef struct
 {
 	GTypeInterface	g_iface;
@@ -55,12 +57,16 @@ typedef struct
 	/* virtual functions */
 	gboolean        (*open)			(RBPlayer *player,
 						 const char *uri,
+						 gpointer stream_data,
+						 GDestroyNotify stream_data_destroy,
 						 GError **error);
 	gboolean	(*opened)		(RBPlayer *player);
 	gboolean        (*close)		(RBPlayer *player,
+						 const char *uri,
 						 GError **error);
 
 	gboolean	(*play)			(RBPlayer *player,
+						 gint crossfade,
 						 GError **error);
 	void		(*pause)		(RBPlayer *player);
 	gboolean	(*playing)		(RBPlayer *player);
@@ -69,6 +75,7 @@ typedef struct
 						 float volume);
 	float		(*get_volume)		(RBPlayer *player);
 	void		(*set_replaygain)	(RBPlayer *player,
+						 const char *uri,
 						 double track_gain,
 						 double track_peak,
 						 double album_gain,
@@ -78,37 +85,55 @@ typedef struct
 	void		(*set_time)		(RBPlayer *player,
 						 long time);
 	long		(*get_time)		(RBPlayer *player);
+	gboolean	(*multiple_open)	(RBPlayer *player);
+
 
 	/* signals */
-	void		(*eos)			(RBPlayer *player);
+	void		(*playing_stream)	(RBPlayer *player,
+						 gpointer stream_data);
+	void		(*eos)			(RBPlayer *player,
+						 gpointer stream_data);
 	void		(*info)			(RBPlayer *player,
+						 gpointer stream_data,
 						 RBMetaDataField field,
 						 GValue *value);
 	void		(*buffering)		(RBPlayer *player,
+						 gpointer stream_data,
 						 guint progress);
 	void		(*error)           	(RBPlayer *player,
+						 gpointer stream_data,
 						 GError *error);
 	void		(*tick)            	(RBPlayer *player,
-						 long elapsed);
-	void		(*event)		(RBPlayer *player, gpointer data);
+						 gpointer stream_data,
+						 long elapsed,
+						 long duration);
+	void		(*event)		(RBPlayer *player,
+						 gpointer stream_data,
+						 gpointer data);
 } RBPlayerIface;
 
 GType		rb_player_get_type   (void);
-RBPlayer *	rb_player_new        (GError **error);
+RBPlayer *	rb_player_new        (gboolean want_crossfade,
+				      GError **error);
 
 gboolean        rb_player_open       (RBPlayer *player,
 				      const char *uri,
+				      gpointer stream_data,
+				      GDestroyNotify stream_data_destroy,
 				      GError **error);
 gboolean	rb_player_opened     (RBPlayer *player);
-gboolean        rb_player_close      (RBPlayer *player, GError **error);
+gboolean        rb_player_close      (RBPlayer *player,
+				      const char *uri,
+				      GError **error);
 
-gboolean	rb_player_play       (RBPlayer *player, GError **error);
+gboolean	rb_player_play       (RBPlayer *player, gint crossfade, GError **error);
 void		rb_player_pause      (RBPlayer *player);
 gboolean	rb_player_playing    (RBPlayer *player);
 
 void		rb_player_set_volume (RBPlayer *player, float volume);
 float		rb_player_get_volume (RBPlayer *player);
 void		rb_player_set_replaygain (RBPlayer *player,
+					  const char *uri,
 					  double track_gain, double track_peak,
 					  double album_gain, double album_peak);
 
@@ -116,13 +141,16 @@ gboolean	rb_player_seekable   (RBPlayer 
 void		rb_player_set_time   (RBPlayer *player, long time);
 long		rb_player_get_time   (RBPlayer *player);
 
+gboolean	rb_player_multiple_open (RBPlayer *player);
+
 /* only to be used by subclasses */
-void	_rb_player_emit_eos (RBPlayer *player);
-void	_rb_player_emit_info (RBPlayer *player, RBMetaDataField field, GValue *value);
-void	_rb_player_emit_buffering (RBPlayer *player, guint progress);
-void	_rb_player_emit_error (RBPlayer *player, GError *error);
-void	_rb_player_emit_tick (RBPlayer *player, long elapsed);
-void	_rb_player_emit_event (RBPlayer *player, const char *name, gpointer data);
+void	_rb_player_emit_eos (RBPlayer *player, gpointer stream_data);
+void	_rb_player_emit_info (RBPlayer *player, gpointer stream_data, RBMetaDataField field, GValue *value);
+void	_rb_player_emit_buffering (RBPlayer *player, gpointer stream_data, guint progress);
+void	_rb_player_emit_error (RBPlayer *player, gpointer stream_data, GError *error);
+void	_rb_player_emit_tick (RBPlayer *player, gpointer stream_data, long elapsed, long duration);
+void	_rb_player_emit_event (RBPlayer *player, gpointer stream_data, const char *name, gpointer data);
+void	_rb_player_emit_playing_stream (RBPlayer *player, gpointer stream_data);
 
 G_END_DECLS
 
diff -rN -u -p old-npb/bindings/python/rb.defs new-npb/bindings/python/rb.defs
--- old-npb/bindings/python/rb.defs	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/bindings/python/rb.defs	2007-03-18 18:49:31.000000000 +1000
@@ -2139,6 +2139,7 @@
   (return-type "gboolean")
   (parameters
     '("const-char*" "uri")
+    '("gpointer" "stream_data")
     '("GError**" "error")
   )
 )
@@ -2154,6 +2155,7 @@
   (c-name "rb_player_close")
   (return-type "gboolean")
   (parameters
+    '("const-char*" "uri")
     '("GError**" "error")
   )
 )
@@ -2163,6 +2165,7 @@
   (c-name "rb_player_play")
   (return-type "gboolean")
   (parameters
+    '("gint" "crossfade")
     '("GError**" "error")
   )
 )
@@ -2199,6 +2202,7 @@
   (c-name "rb_player_set_replaygain")
   (return-type "none")
   (parameters
+    '("const-char*" "uri")
     '("double" "track_gain")
     '("double" "track_peak")
     '("double" "album_gain")
diff -rN -u -p old-npb/data/glade/Makefile.am new-npb/data/glade/Makefile.am
--- old-npb/data/glade/Makefile.am	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/data/glade/Makefile.am	2007-03-18 18:49:31.000000000 +1000
@@ -12,6 +12,7 @@ glade_DATA =	general-prefs.glade	  	\
 		uri.glade			\
 		plugins.glade			\
 		playlist-save.glade		\
+		playback-prefs.glade		\
 		uri-new.glade
 
 gladedir = $(pkgdatadir)/glade
diff -rN -u -p old-npb/data/glade/playback-prefs.glade new-npb/data/glade/playback-prefs.glade
--- old-npb/data/glade/playback-prefs.glade	1970-01-01 10:00:00.000000000 +1000
+++ new-npb/data/glade/playback-prefs.glade	2007-03-18 18:49:31.000000000 +1000
@@ -0,0 +1,425 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+<requires lib="gnome"/>
+
+<widget class="GtkWindow" id="window1">
+  <property name="visible">True</property>
+  <property name="title" translatable="yes">window1</property>
+  <property name="type">GTK_WINDOW_TOPLEVEL</property>
+  <property name="window_position">GTK_WIN_POS_NONE</property>
+  <property name="modal">False</property>
+  <property name="resizable">True</property>
+  <property name="destroy_with_parent">False</property>
+  <property name="decorated">True</property>
+  <property name="skip_taskbar_hint">False</property>
+  <property name="skip_pager_hint">False</property>
+  <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
+  <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
+  <property name="focus_on_map">True</property>
+  <property name="urgency_hint">False</property>
+
+  <child>
+    <widget class="GtkAlignment" id="alignment1">
+      <property name="visible">True</property>
+      <property name="xalign">0.5</property>
+      <property name="yalign">0.5</property>
+      <property name="xscale">1</property>
+      <property name="yscale">1</property>
+      <property name="top_padding">0</property>
+      <property name="bottom_padding">0</property>
+      <property name="left_padding">0</property>
+      <property name="right_padding">0</property>
+
+      <child>
+	<widget class="GtkVBox" id="playback_prefs_box">
+	  <property name="border_width">12</property>
+	  <property name="visible">True</property>
+	  <property name="homogeneous">False</property>
+	  <property name="spacing">18</property>
+
+	  <child>
+	    <widget class="GtkVBox" id="vbox3">
+	      <property name="visible">True</property>
+	      <property name="homogeneous">False</property>
+	      <property name="spacing">6</property>
+
+	      <child>
+		<widget class="GtkLabel" id="backend_label">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Player backend</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">0</property>
+		  <property name="ypad">0</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">False</property>
+		  <property name="fill">False</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkHBox" id="hbox1">
+		  <property name="visible">True</property>
+		  <property name="homogeneous">False</property>
+		  <property name="spacing">0</property>
+
+		  <child>
+		    <widget class="GtkLabel" id="label1">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">    </property>
+		      <property name="use_underline">False</property>
+		      <property name="use_markup">False</property>
+		      <property name="justify">GTK_JUSTIFY_LEFT</property>
+		      <property name="wrap">False</property>
+		      <property name="selectable">False</property>
+		      <property name="xalign">0.5</property>
+		      <property name="yalign">0.5</property>
+		      <property name="xpad">0</property>
+		      <property name="ypad">0</property>
+		      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		      <property name="width_chars">-1</property>
+		      <property name="single_line_mode">False</property>
+		      <property name="angle">0</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkCheckButton" id="use_xfade_backend">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="label" translatable="yes">Use crossfading backend (requires restart)</property>
+		      <property name="use_underline">True</property>
+		      <property name="relief">GTK_RELIEF_NORMAL</property>
+		      <property name="focus_on_click">True</property>
+		      <property name="active">False</property>
+		      <property name="inconsistent">False</property>
+		      <property name="draw_indicator">True</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">True</property>
+		  <property name="fill">True</property>
+		</packing>
+	      </child>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkVBox" id="vbox5">
+	      <property name="visible">True</property>
+	      <property name="homogeneous">False</property>
+	      <property name="spacing">6</property>
+
+	      <child>
+		<widget class="GtkLabel" id="duration_label">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Crossfade duration</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">0</property>
+		  <property name="ypad">0</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">False</property>
+		  <property name="fill">False</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkHBox" id="hbox4">
+		  <property name="visible">True</property>
+		  <property name="homogeneous">False</property>
+		  <property name="spacing">0</property>
+
+		  <child>
+		    <widget class="GtkLabel" id="label5">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">    </property>
+		      <property name="use_underline">False</property>
+		      <property name="use_markup">False</property>
+		      <property name="justify">GTK_JUSTIFY_LEFT</property>
+		      <property name="wrap">False</property>
+		      <property name="selectable">False</property>
+		      <property name="xalign">0.5</property>
+		      <property name="yalign">0.5</property>
+		      <property name="xpad">0</property>
+		      <property name="ypad">0</property>
+		      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		      <property name="width_chars">-1</property>
+		      <property name="single_line_mode">False</property>
+		      <property name="angle">0</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkHScale" id="duration">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="draw_value">True</property>
+		      <property name="value_pos">GTK_POS_TOP</property>
+		      <property name="digits">2</property>
+		      <property name="update_policy">GTK_UPDATE_CONTINUOUS</property>
+		      <property name="inverted">False</property>
+		      <property name="adjustment">0 0 60 0 0 0</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">True</property>
+		      <property name="fill">True</property>
+		    </packing>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">True</property>
+		  <property name="fill">True</property>
+		</packing>
+	      </child>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkVBox" id="vbox7">
+	      <property name="visible">True</property>
+	      <property name="homogeneous">False</property>
+	      <property name="spacing">0</property>
+
+	      <child>
+		<widget class="GtkLabel" id="crossfade_type_label">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Crossfade type</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">0</property>
+		  <property name="ypad">0</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">False</property>
+		  <property name="fill">False</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkHBox" id="hbox5">
+		  <property name="visible">True</property>
+		  <property name="homogeneous">False</property>
+		  <property name="spacing">0</property>
+
+		  <child>
+		    <widget class="GtkLabel" id="label7">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">    </property>
+		      <property name="use_underline">False</property>
+		      <property name="use_markup">False</property>
+		      <property name="justify">GTK_JUSTIFY_LEFT</property>
+		      <property name="wrap">False</property>
+		      <property name="selectable">False</property>
+		      <property name="xalign">0.5</property>
+		      <property name="yalign">0.5</property>
+		      <property name="xpad">0</property>
+		      <property name="ypad">0</property>
+		      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		      <property name="width_chars">-1</property>
+		      <property name="single_line_mode">False</property>
+		      <property name="angle">0</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkCheckButton" id="album_check">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="label" translatable="yes">Crossfade between songs on the same album</property>
+		      <property name="use_underline">True</property>
+		      <property name="relief">GTK_RELIEF_NORMAL</property>
+		      <property name="focus_on_click">True</property>
+		      <property name="active">False</property>
+		      <property name="inconsistent">False</property>
+		      <property name="draw_indicator">True</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">False</property>
+		  <property name="fill">False</property>
+		</packing>
+	      </child>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">False</property>
+	      <property name="fill">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkVBox" id="vbox6">
+	      <property name="visible">True</property>
+	      <property name="homogeneous">False</property>
+	      <property name="spacing">6</property>
+
+	      <child>
+		<widget class="GtkLabel" id="buffer_size_label">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes">Network buffer size (kB)</property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">0</property>
+		  <property name="ypad">0</property>
+		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		  <property name="width_chars">-1</property>
+		  <property name="single_line_mode">False</property>
+		  <property name="angle">0</property>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">False</property>
+		  <property name="fill">False</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkHBox" id="hbox3">
+		  <property name="visible">True</property>
+		  <property name="homogeneous">False</property>
+		  <property name="spacing">0</property>
+
+		  <child>
+		    <widget class="GtkLabel" id="label4">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">    </property>
+		      <property name="use_underline">False</property>
+		      <property name="use_markup">False</property>
+		      <property name="justify">GTK_JUSTIFY_LEFT</property>
+		      <property name="wrap">False</property>
+		      <property name="selectable">False</property>
+		      <property name="xalign">0.5</property>
+		      <property name="yalign">0.5</property>
+		      <property name="xpad">0</property>
+		      <property name="ypad">0</property>
+		      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+		      <property name="width_chars">-1</property>
+		      <property name="single_line_mode">False</property>
+		      <property name="angle">0</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">False</property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkHScale" id="network_buffer_size">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="draw_value">True</property>
+		      <property name="value_pos">GTK_POS_TOP</property>
+		      <property name="digits">0</property>
+		      <property name="update_policy">GTK_UPDATE_CONTINUOUS</property>
+		      <property name="inverted">False</property>
+		      <property name="adjustment">32 32 1024 32 0 0</property>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">True</property>
+		      <property name="fill">True</property>
+		    </packing>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">False</property>
+		  <property name="fill">False</property>
+		</packing>
+	      </child>
+	    </widget>
+	    <packing>
+	      <property name="padding">0</property>
+	      <property name="expand">True</property>
+	      <property name="fill">True</property>
+	    </packing>
+	  </child>
+	</widget>
+      </child>
+    </widget>
+  </child>
+</widget>
+
+</glade-interface>
diff -rN -u -p old-npb/data/rhythmbox.schemas new-npb/data/rhythmbox.schemas
--- old-npb/data/rhythmbox.schemas	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/data/rhythmbox.schemas	2007-03-18 18:49:31.000000000 +1000
@@ -991,6 +991,51 @@
 	</locale>
       </schema>
       <schema>
+        <key>/schemas/apps/rhythmbox/player/use_xfade_backend</key>
+        <applyto>/apps/rhythmbox/player/use_xfade_backend</applyto>
+        <owner>rhythmbox</owner>
+        <type>boolean</type>
+        <default>false</default>
+        <locale name="C">
+        <short>Whether to use the experimental crossfading player backend</short>
+        <long>Whether to use the experimental crossfading player backend.
+        Changes to this setting only take effect after a restart.</long>
+        </locale>
+      </schema>
+      <schema>
+        <key>/schemas/apps/rhythmbox/player/transition_album_check</key>
+        <applyto>/apps/rhythmbox/player/transition_album_check</applyto>
+        <owner>rhythmbox</owner>
+        <type>boolean</type>
+        <default>true</default>
+        <locale name="C">
+        <short>Perform transitions between songs on the same album</short>
+        <long>Perform transitions between songs on the same album</long>
+        </locale>
+      </schema>
+      <schema>
+        <key>/schemas/apps/rhythmbox/player/transition_time</key>
+        <applyto>/apps/rhythmbox/player/transition_time</applyto>
+        <owner>rhythmbox</owner>
+        <type>float</type>
+        <default>0.0</default>
+        <locale name="C">
+        <short>Duration of a track transition in seconds</short>
+        <long>Duration of a track transition in seconds</long>
+        </locale>
+      </schema>
+      <schema>
+        <key>/schemas/apps/rhythmbox/player/network_buffer_size</key>
+        <applyto>/apps/rhythmbox/player/network_buffer_size</applyto>
+        <owner>rhyhtmbox</owner>
+        <type>int</type>
+        <default>32</default>
+        <locale name="C">
+        <short>Size (kB) of buffer for playing remote files and streams</short>
+        <long>Size (kB) of buffer for playing remote files and streams</long>
+        </locale>
+      </schema>
+      <schema>
 	<key>/schemas/apps/rhythmbox/plugins/visualizer/active</key>
 	<applyto>/apps/rhythmbox/plugins/visualizer/active</applyto>
 	<owner>rhythmbox</owner>
diff -rN -u -p old-npb/lib/rb-marshal.list new-npb/lib/rb-marshal.list
--- old-npb/lib/rb-marshal.list	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/lib/rb-marshal.list	2007-03-18 18:49:31.000000000 +1000
@@ -1,5 +1,6 @@
 BOOLEAN:BOOLEAN,BOOLEAN,BOOLEAN
 BOOLEAN:POINTER
+BOOLEAN:STRING,STRING,OBJECT
 INT:VOID
 OBJECT:OBJECT
 STRING:STRING
@@ -13,18 +14,20 @@ VOID:BOXED,STRING,BOXED
 VOID:BOXED,ULONG
 VOID:DOUBLE,LONG
 VOID:INT,INT,DOUBLE
-VOID:INT,POINTER
 VOID:OBJECT,INT,INT
 VOID:OBJECT,INT,INT,BOXED,UINT,UINT
 VOID:OBJECT,INT,POINTER
 VOID:POINTER,INT
 VOID:POINTER,INT,POINTER
+VOID:POINTER,LONG,LONG
 VOID:POINTER,POINTER
+VOID:POINTER,UINT
 VOID:POINTER,ULONG
 VOID:STRING,DOUBLE
 VOID:STRING,INT
 VOID:STRING,POINTER
 VOID:STRING,STRING
+VOID:STRING,STRING,OBJECT
 VOID:STRING,STRING,STRING
 VOID:STRING,STRING,STRING,UINT,BOOLEAN
 VOID:ULONG,FLOAT
diff -rN -u -p old-npb/lib/rb-preferences.h new-npb/lib/rb-preferences.h
--- old-npb/lib/rb-preferences.h	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/lib/rb-preferences.h	2007-03-18 18:49:31.000000000 +1000
@@ -35,7 +35,7 @@ G_BEGIN_DECLS
 #define CONF_UI_TOOLBAR_STYLE	CONF_PREFIX "/ui/toolbar_style"
 #define CONF_UI_SONG_POSITION_SLIDER_HIDDEN  CONF_PREFIX "/ui/song_position_slider_hidden"
 #define CONF_UI_SIDEPANE_HIDDEN CONF_PREFIX "/ui/sourcelist_hidden"
-#define CONF_UI_QUEUE_AS_SIDEBAR  CONF_PREFIX "/ui/queue_as_sidebar" 
+#define CONF_UI_QUEUE_AS_SIDEBAR  CONF_PREFIX "/ui/queue_as_sidebar"
 #define CONF_UI_SMALL_DISPLAY     CONF_PREFIX "/ui/small_display"
 #define CONF_UI_FULLSCREEN     CONF_PREFIX "/ui/fullscreen"
 #define CONF_UI_OBSOLETE_COLUMNS_SETUP CONF_PREFIX "/ui/columns_setup"
@@ -70,6 +70,12 @@ G_BEGIN_DECLS
 #define CONF_PLUGIN_ACTIVE_KEY		CONF_PLUGINS_PREFIX "/%s/active"
 #define CONF_PLUGIN_HIDDEN_KEY		CONF_PLUGINS_PREFIX "/%s/hidden"
 
+#define CONF_PLAYER_DIR			CONF_PREFIX "/player"
+#define CONF_PLAYER_USE_XFADE_BACKEND 	CONF_PREFIX "/player/use_xfade_backend"
+#define CONF_PLAYER_TRANSITION_ALBUM_CHECK CONF_PREFIX "/player/transition_album_check"
+#define CONF_PLAYER_TRANSITION_TIME 	CONF_PREFIX "/player/transition_time"
+#define CONF_PLAYER_NETWORK_BUFFER_SIZE	CONF_PREFIX "/player/network_buffer_size"
+
 G_END_DECLS
 
 #endif /* __RB_PREFERENCES_H */
diff -rN -u -p old-npb/plugins/audiocd/Makefile.am new-npb/plugins/audiocd/Makefile.am
--- old-npb/plugins/audiocd/Makefile.am	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/plugins/audiocd/Makefile.am	2007-03-18 18:49:31.000000000 +1000
@@ -36,6 +36,7 @@ INCLUDES = 						\
 	-I$(top_builddir)/remote			\
 	-I$(top_srcdir)/plugins				\
 	-I$(top_srcdir)/shell				\
+	-I$(top_srcdir)/backends			\
 	-DPIXMAP_DIR=\""$(datadir)/pixmaps"\"		\
 	-DSHARE_DIR=\"$(pkgdatadir)\"                   \
 	-DDATADIR=\""$(datadir)"\"			\
diff -rN -u -p old-npb/plugins/audiocd/rb-audiocd-plugin.c new-npb/plugins/audiocd/rb-audiocd-plugin.c
--- old-npb/plugins/audiocd/rb-audiocd-plugin.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/plugins/audiocd/rb-audiocd-plugin.c	2007-03-18 18:49:31.000000000 +1000
@@ -43,6 +43,9 @@
 #define nautilus_burn_drive_unref nautilus_burn_drive_free
 #endif
 
+#ifdef HAVE_GSTREAMER_0_10
+#include <gst/gst.h>
+#endif
 
 #include "rb-plugin.h"
 #include "rb-debug.h"
@@ -51,6 +54,7 @@
 #include "rb-dialog.h"
 #include "rb-removable-media-manager.h"
 #include "rb-audiocd-source.h"
+#include "rb-player.h"
 
 
 #define RB_TYPE_AUDIOCD_PLUGIN		(rb_audiocd_plugin_get_type ())
@@ -351,6 +355,75 @@ rb_audiocd_plugin_playing_uri_changed_cb
 	plugin->playing_uri = uri ? g_strdup (uri) : NULL;
 }
 
+#ifdef HAVE_GSTREAMER_0_10
+
+static gboolean
+rb_audiocd_plugin_can_reuse_stream_cb (RBPlayer *player,
+				       const char *new_uri,
+				       const char *stream_uri,
+				       GstElement *stream_bin,
+				       RBAudioCdPlugin *plugin)
+{
+	const char *new_device;
+	const char *old_device;
+
+	/* can't do anything with non-cdda URIs */
+	if (g_str_has_prefix (new_uri, "cdda://") == FALSE ||
+	    g_str_has_prefix (stream_uri, "cdda://") == FALSE) {
+		return FALSE;
+	}
+
+	/* check the device matches */
+	new_device = g_utf8_strrchr (new_uri, -1, '#');
+	old_device = g_utf8_strrchr (stream_uri, -1, '#');
+	if (strcmp (old_device, new_device) != 0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static void
+rb_audiocd_plugin_reuse_stream_cb (RBPlayer *player,
+				   const char *new_uri,
+				   const char *stream_uri,
+				   GstElement *stream_bin,
+				   RBAudioCdPlugin *plugin)
+{
+	GstFormat track_format = gst_format_get_by_nick ("track");
+	char *track_str;
+	char *new_device;
+	guint track;
+	guint cdda_len;
+	GstPad *ghost_pad;
+	GstPad *pad;
+
+	/* get the new track number */
+	cdda_len = strlen ("cdda://");
+	new_device = g_utf8_strrchr (new_uri, -1, '#');
+	track_str = g_strndup (new_uri + cdda_len, new_device - (new_uri + cdda_len));
+	track = atoi (track_str);
+	g_free (track_str);
+
+	rb_debug ("seeking to track %d on CD device %s", track, new_device+1);
+
+	ghost_pad = gst_element_get_pad (stream_bin, "src");
+	if (GST_IS_GHOST_PAD (ghost_pad)) {
+		pad = gst_ghost_pad_get_target (GST_GHOST_PAD (ghost_pad));
+		gst_object_unref (ghost_pad);
+	} else {
+		pad = ghost_pad;
+	}
+
+	gst_element_seek (GST_PAD_PARENT (pad),
+			  1.0, track_format, GST_SEEK_FLAG_FLUSH,
+			  GST_SEEK_TYPE_SET, track-1,
+			  GST_SEEK_TYPE_NONE, -1);
+	gst_object_unref (pad);
+}
+
+#endif
+
 #if !NAUTILUS_BURN_CHECK_VERSION(2,15,3)
 static const char *
 nautilus_burn_drive_get_device (NautilusBurnDrive *drive)
@@ -403,6 +476,10 @@ impl_activate (RBPlugin *plugin,
 	GList                   *drives;
 	GList                   *it;
 	GnomeVFSVolumeMonitor   *monitor;
+	GObject                 *shell_player;
+#ifdef HAVE_GSTREAMER_0_10
+	RBPlayer                *player_backend;
+#endif
 	GtkUIManager            *uimanager;
 	char                    *filename;
 
@@ -444,8 +521,30 @@ impl_activate (RBPlugin *plugin,
 
 	g_object_unref (rmm);
 
+	/* if we're using the gapless/crossfade player backend, make it reuse
+	 * audio cd playback streams to avoid closing and reopening the device
+	 * (and prevent it from even thinking about crossfading songs on the same cd)
+	 */
+	shell_player = rb_shell_get_player (shell);
+#ifdef HAVE_GSTREAMER_0_10
+	g_object_get (shell_player, "player", &player_backend, NULL);
+	if (player_backend) {
+		GObjectClass *klass = G_OBJECT_GET_CLASS (player_backend);
+		if (g_signal_lookup ("reuse-stream", G_OBJECT_CLASS_TYPE (klass)) != 0) {
+			g_signal_connect_object (player_backend,
+						 "can-reuse-stream",
+						 G_CALLBACK (rb_audiocd_plugin_can_reuse_stream_cb),
+						 plugin, 0);
+			g_signal_connect_object (player_backend,
+						 "reuse-stream",
+						 G_CALLBACK (rb_audiocd_plugin_reuse_stream_cb),
+						 plugin, 0);
+		}
+	}
+#endif
+
 	/* monitor the playing song, to disable cd drive polling */
-	g_signal_connect_object (rb_shell_get_player (shell), "playing-uri-changed",
+	g_signal_connect_object (shell_player, "playing-uri-changed",
 				 G_CALLBACK (rb_audiocd_plugin_playing_uri_changed_cb),
 				 plugin, 0);
 
diff -rN -u -p old-npb/plugins/iradio/rb-iradio-source.c new-npb/plugins/iradio/rb-iradio-source.c
--- old-npb/plugins/iradio/rb-iradio-source.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/plugins/iradio/rb-iradio-source.c	2007-03-18 18:49:31.000000000 +1000
@@ -1024,6 +1024,7 @@ check_entry_type (RBIRadioSource *source
 
 static void
 info_available_cb (RBPlayer *backend,
+		   const char *uri,
 		   RBMetaDataField field,
 		   GValue *value,
 		   RBIRadioSource *source)
diff -rN -u -p old-npb/plugins/visualizer/rb-visualizer-plugin.c new-npb/plugins/visualizer/rb-visualizer-plugin.c
--- old-npb/plugins/visualizer/rb-visualizer-plugin.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/plugins/visualizer/rb-visualizer-plugin.c	2007-03-18 18:49:31.000000000 +1000
@@ -19,20 +19,6 @@
  */
 
 /*
- * visualizer plugin.
- * - assumes we're going to continue using playbin
- *     (it would be nice if any replacement pipeline had equivalent
- *      hooks for a visualization->video output branch..)
- * - does nasty tricks using a fake visualizer to convince
- *   playbin to set up the machinery for visualization so we can
- *   enable it at any time without even pausing the pipeline
- *     (it would be nice if any replacement pipeline had the
- *      ability to splice in a visualization->video output
- *      branch at any time)
- * - does further nasty tricks to make sure the video sink always
- *     has a valid window ID to scribble on, so it doesn't go
- *     creating its own window
- *
  * things to do:
  * - libvisual opengl actors may require some work here too
  * - maybe mangle caps filter when the output window size changes?
@@ -76,6 +62,7 @@
 #include "rb-shell-player.h"
 #include "rb-player.h"
 #include "rb-player-gst.h"
+#include "rb-player-gst-tee.h"
 #include "rb-dialog.h"
 #include "rb-file-helpers.h"
 #include "rb-preferences.h"
@@ -154,9 +141,16 @@ typedef struct
 	GtkWidget *vis_shell;	/* notebook page */
 	GtkWidget *vis_box;
 	GtkWidget *vis_widget;
+
 	GstElement *visualizer;
 	GstElement *video_sink;
+
 	GstElement *playbin;
+
+	GstElement *identity;
+	GstElement *capsfilter;
+	GstElement *vis_plugin;
+
 	GstXOverlay *xoverlay;
 	gint bus_sync_id;
 	GdkWindow *fake_window;
@@ -485,47 +479,54 @@ fixate_vis_caps (RBVisualizerPlugin *pi,
 		gst_caps_unref (caps);
 }
 
+static GstElement *
+create_visualizer_element (const char *vis_override)
+{
+	char *vis_element_name = NULL;
+	GstElement *element;
+
+	if (vis_override) {
+		vis_element_name = g_strdup (vis_override);
+	} else {
+		vis_element_name = eel_gconf_get_string (CONF_VIS_ELEMENT);
+	}
+
+	if (vis_element_name == NULL) {
+		vis_element_name = g_strdup (DEFAULT_VIS_ELEMENT);
+	}
+	rb_debug ("creating new visualizer: %s", vis_element_name);
+
+	element = gst_element_factory_make (vis_element_name, NULL);
+	g_free (vis_element_name);
+
+	return element;
+}
+
 static void
-update_visualizer (RBVisualizerPlugin *plugin, const char *vis_override, int quality)
+update_playbin_visualizer (RBVisualizerPlugin *plugin,
+			   const char *vis_override,
+			   int quality)
 {
-	GstElement *capsfilter;
 	GstPad *pad;
-	char *vis_element_name = NULL;
 	GstElement *vis_plugin;
 
-	if (plugin->playbin == NULL)
-		return;
-
 	if (plugin->visualizer)
 		g_object_unref (plugin->visualizer);
 
 	plugin->visualizer = gst_bin_new (NULL);
 
 	/* set up capsfilter */
-	capsfilter = gst_element_factory_make ("capsfilter", NULL);
-	gst_bin_add (GST_BIN (plugin->visualizer), capsfilter);
+	plugin->capsfilter = gst_element_factory_make ("capsfilter", NULL);
+	gst_bin_add (GST_BIN (plugin->visualizer), plugin->capsfilter);
 
-	pad = gst_element_get_pad (capsfilter, "src");
+	pad = gst_element_get_pad (plugin->capsfilter, "src");
 	gst_element_add_pad (plugin->visualizer, gst_ghost_pad_new ("src", pad));
 	gst_object_unref (pad);
 
 	/* set up visualizer */
 	if (plugin->active) {
-		if (vis_override) {
-			vis_element_name = g_strdup (vis_override);
-		} else {
-			vis_element_name = eel_gconf_get_string (CONF_VIS_ELEMENT);
-		}
-
-		if (vis_element_name == NULL) {
-			vis_element_name = g_strdup (DEFAULT_VIS_ELEMENT);
-		}
-		rb_debug ("creating new visualizer: %s", vis_element_name);
-
-		vis_plugin = gst_element_factory_make (vis_element_name, NULL);
+		vis_plugin = create_visualizer_element (vis_override);
 		gst_bin_add (GST_BIN (plugin->visualizer), vis_plugin);
-		g_free (vis_element_name);
-
 	} else {
 		vis_plugin = g_object_new (rb_fake_vis_get_type (), NULL);
 		gst_bin_add (GST_BIN (plugin->visualizer), vis_plugin);
@@ -535,8 +536,8 @@ update_visualizer (RBVisualizerPlugin *p
 	gst_element_add_pad (plugin->visualizer, gst_ghost_pad_new ("sink", pad));
 	gst_object_unref (pad);
 
-	gst_element_link (vis_plugin, capsfilter);
-	fixate_vis_caps (plugin, vis_plugin, capsfilter, quality);
+	gst_element_link (vis_plugin, plugin->capsfilter);
+	fixate_vis_caps (plugin, vis_plugin, plugin->capsfilter, quality);
 
 	g_object_ref (plugin->visualizer);
 
@@ -544,6 +545,102 @@ update_visualizer (RBVisualizerPlugin *p
 }
 
 static void
+update_tee_visualizer (RBVisualizerPlugin *plugin,
+		       const char *vis_override,
+		       int quality)
+{
+	GstElement *old_vis_plugin = NULL;
+	GstPad *blocked_pad = NULL;
+	gboolean add_tee;
+
+	/* if we're not active, just make sure the bin isn't in the pipeline. */
+	if (plugin->active == FALSE) {
+		if (GST_ELEMENT_PARENT (plugin->visualizer)) {
+			rb_debug ("removing visualizer bin from the pipeline");
+			rb_player_gst_tee_remove_tee (RB_PLAYER_GST_TEE (plugin->player),
+						      plugin->visualizer);
+		} else {
+			rb_debug ("visualizer bin isn't in the pipeline");
+		}
+		return;
+	}
+
+	if (GST_ELEMENT_PARENT (plugin->visualizer) != NULL) {
+		GstStateChangeReturn ret;
+		GstState state, pending;
+
+		add_tee = FALSE;
+		ret = gst_element_get_state (plugin->visualizer, &state, &pending, GST_CLOCK_TIME_NONE);
+		if (ret == GST_STATE_CHANGE_SUCCESS && state == GST_STATE_PLAYING) {
+			GstPad *pad;
+
+			/* probably should do this async.. */
+			rb_debug ("blocking visualizer bin sink pad");
+			pad = gst_element_get_pad (plugin->visualizer, "sink");
+			blocked_pad = gst_ghost_pad_get_target (GST_GHOST_PAD (pad));
+			gst_pad_set_blocked (blocked_pad, TRUE);
+			gst_object_unref (pad);
+			rb_debug ("blocked visualizer bin sink pad");
+		}
+	} else {
+		add_tee = TRUE;
+	}
+
+	/* otherwise, update the visualizer element */
+	if (plugin->vis_plugin != NULL) {
+		/* clean up the element once we've unblocked the pipeline;
+		 * otherwise it seems to deadlock.
+		 */
+		old_vis_plugin = g_object_ref (plugin->vis_plugin);
+		gst_bin_remove (GST_BIN (plugin->visualizer), plugin->vis_plugin);
+	}
+
+	plugin->vis_plugin = create_visualizer_element (vis_override);
+	gst_bin_add (GST_BIN (plugin->visualizer), plugin->vis_plugin);
+
+	if (gst_element_link_many (plugin->identity, plugin->vis_plugin, plugin->capsfilter, NULL) == FALSE) {
+		rb_debug ("failed to link in new visualizer element");
+		/* um, what now? */
+	}
+
+	/* update the capsfilter */
+	fixate_vis_caps (plugin, plugin->vis_plugin, plugin->capsfilter, quality);
+
+	/* make sure the visualizer is in the pipeline, and sync its state
+	 * with the rest of the pipeline.
+	 */
+	if (add_tee) {
+		rb_debug ("adding visualizer bin to the pipeline");
+		rb_player_gst_tee_add_tee (RB_PLAYER_GST_TEE (plugin->player),
+					   plugin->visualizer);
+	} else if (blocked_pad != NULL) {
+		gst_element_set_state (plugin->vis_plugin, GST_STATE_PLAYING);
+		gst_pad_set_blocked (blocked_pad, FALSE);
+		gst_object_unref (blocked_pad);
+	} else {
+		gst_element_set_state (plugin->vis_plugin, GST_STATE_PAUSED);
+	}
+
+	if (old_vis_plugin != NULL) {
+		rb_debug ("cleaning up old visualizer element");
+		gst_element_set_state (old_vis_plugin, GST_STATE_NULL);
+		g_object_unref (old_vis_plugin);
+	}
+}
+
+static void
+update_visualizer (RBVisualizerPlugin *plugin,
+		   const char *vis_override,
+		   int quality)
+{
+	if (plugin->playbin != NULL) {
+		update_playbin_visualizer (plugin, vis_override, quality);
+	} else {
+		update_tee_visualizer (plugin, vis_override, quality);
+	}
+}
+
+static void
 actually_hide_controls (RBVisualizerPlugin *plugin)
 {
 	rb_debug ("hiding controls");
@@ -1337,9 +1434,12 @@ impl_activate (RBPlugin *plugin,
 	}
 
 	g_object_get (pi->shell_player, "player", &pi->player, NULL);
-	if (pi->player && g_object_class_find_property (G_OBJECT_GET_CLASS (pi->player), "playbin")) {
+	if (pi->player == NULL) {
+		g_warning ("no player exists yet");
+	} else if (g_object_class_find_property (G_OBJECT_GET_CLASS (pi->player), "playbin")) {
 		GstElement *playbin;
 
+		rb_debug ("using playbin-based visualization");
 		pi->playbin_notify_id =
 			g_signal_connect_object (pi->player,
 						 "notify::playbin",
@@ -1353,8 +1453,40 @@ impl_activate (RBPlugin *plugin,
 		}
 
 		connected = TRUE;
+	} else if (RB_IS_PLAYER_GST_TEE (pi->player)) {
+		GstElement *videoscale;
+		GstElement *colorspace;
+		GstPad *pad;
+
+		rb_debug ("using tee-based visualization");
+
+		/* create the sink */
+		pi->video_sink = gst_element_factory_make ("gconfvideosink", "videosink");
+		gst_element_set_state (pi->video_sink, GST_STATE_READY);
+		find_xoverlay (pi);
+
+		/* create the rest of the bin */
+		pi->visualizer = gst_bin_new (NULL);
+		g_object_ref (pi->visualizer);
+
+		pi->identity = gst_element_factory_make ("identity", NULL);
+		pi->capsfilter = gst_element_factory_make ("capsfilter", NULL);
+		colorspace = gst_element_factory_make ("ffmpegcolorspace", NULL);
+		videoscale = gst_element_factory_make ("videoscale", NULL);
+
+		gst_bin_add_many (GST_BIN (pi->visualizer),
+				  pi->identity, pi->capsfilter, colorspace, videoscale, pi->video_sink,
+				  NULL);
+		gst_element_link_many (pi->capsfilter, colorspace, videoscale, pi->video_sink, NULL);
+		/* leave identity unlinked until we have a visualizer element */
+
+		pad = gst_element_get_pad (pi->identity, "sink");
+		gst_element_add_pad (pi->visualizer, gst_ghost_pad_new ("sink", pad));
+		gst_object_unref (pad);
+
+		connected = TRUE;
 	} else {
-		g_warning ("no player backend exists or wrong type?");
+		g_warning ("unknown player backend type");
 		g_object_unref (pi->player);
 		pi->player = NULL;
 	}
@@ -1362,8 +1494,6 @@ impl_activate (RBPlugin *plugin,
 	if (!connected)
 		return;
 
-	rb_debug ("connected to playbin mutation signal");
-
 	/* create action group */
 	pi->action_group = gtk_action_group_new ("VisualizerActions");
 	gtk_action_group_set_translation_domain (pi->action_group, GETTEXT_PACKAGE);
diff -rN -u -p old-npb/shell/Makefile.am new-npb/shell/Makefile.am
--- old-npb/shell/Makefile.am	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/shell/Makefile.am	2007-03-18 18:49:31.000000000 +1000
@@ -141,6 +141,10 @@ if WITH_OLD_DBUS
 rhythmbox_LDADD += $(DBUS_LIBS)
 endif
 
+if USE_GSTREAMER_0_10
+rhythmbox_LDADD += -lgstcontroller-0.10
+endif
+
 rhythmbox_LDADD += \
 	$(top_builddir)/backends/librbbackends.la	\
 	$(GDA_LIBS)					\
diff -rN -u -p old-npb/shell/rb-shell.c new-npb/shell/rb-shell.c
--- old-npb/shell/rb-shell.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/shell/rb-shell.c	2007-03-18 18:49:31.000000000 +1000
@@ -229,7 +229,7 @@ static gboolean rb_shell_show_popup_cb (
 					RBSource *target,
 					RBShell *shell);
 static gboolean tray_destroy_cb (GtkObject *object, RBShell *shell);
-static void rb_shell_construct_notify_titles (RBShell *shell, 
+static void rb_shell_construct_notify_titles (RBShell *shell,
 					      RhythmDBEntry *entry);
 
 static gboolean save_yourself_cb (GnomeClient *client,
@@ -903,10 +903,9 @@ rb_shell_finalize (GObject *object)
 
 	gtk_widget_destroy (GTK_WIDGET (shell->priv->tray_icon));
 
-	if (shell->priv->cached_notify_primary != NULL)
-		g_free (shell->priv->cached_notify_primary);
-	if (shell->priv->cached_notify_secondary != NULL)
-		g_free (shell->priv->cached_notify_secondary);
+	g_free (shell->priv->cached_notify_primary);
+	g_free (shell->priv->cached_notify_secondary);
+
 	if (shell->priv->cached_art_icon != NULL)
 		g_object_unref (shell->priv->cached_art_icon);
 
@@ -2957,16 +2956,13 @@ rb_shell_construct_notify_titles (RBShel
 	char *title = NULL;
 	GString *secondary;
 
-	if (shell->priv->cached_notify_primary != NULL)
-		g_free (shell->priv->cached_notify_primary);
-	if (shell->priv->cached_notify_secondary != NULL)
-		g_free (shell->priv->cached_notify_secondary);
+	g_free (shell->priv->cached_notify_primary);
+	g_free (shell->priv->cached_notify_secondary);
+	shell->priv->cached_notify_primary = NULL;
+	shell->priv->cached_notify_secondary = NULL;
 
-	if (entry == NULL) {
-		shell->priv->cached_notify_primary = NULL;
-		shell->priv->cached_notify_secondary = NULL;
+	if (entry == NULL)
 		return;
-	}
 
 	secondary = g_string_sized_new (100);
 
diff -rN -u -p old-npb/shell/rb-shell-player.c new-npb/shell/rb-shell-player.c
--- old-npb/shell/rb-shell-player.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/shell/rb-shell-player.c	2007-03-18 18:49:31.000000000 +1000
@@ -117,8 +117,9 @@ static void rb_shell_player_property_row
 static void rb_shell_player_sync_volume (RBShellPlayer *player, gboolean notify);
 static void rb_shell_player_sync_replaygain (RBShellPlayer *player,
                                              RhythmDBEntry *entry);
-static void tick_cb (RBPlayer *player, long elapsed, gpointer data);
-static void error_cb (RBPlayer *player, const GError *err, gpointer data);
+static void tick_cb (RBPlayer *player, RhythmDBEntry *entry, long elapsed, long duration, gpointer data);
+static void error_cb (RBPlayer *player, RhythmDBEntry *entry, const GError *err, gpointer data);
+static void playing_stream_cb (RBPlayer *player, RhythmDBEntry *entry, RBShellPlayer *shell_player);
 static void rb_shell_player_error (RBShellPlayer *player, gboolean async, const GError *err);
 
 static void rb_shell_player_set_play_order (RBShellPlayer *player,
@@ -135,8 +136,14 @@ static void rb_shell_player_sync_buttons
 
 static void gconf_play_order_changed (GConfClient *client,guint cnxn_id,
 				      GConfEntry *entry, RBShellPlayer *player);
-static void gconf_song_position_slider_visibility_changed (GConfClient *client,guint cnxn_id,
+static void gconf_song_position_slider_visibility_changed (GConfClient *client, guint cnxn_id,
 							   GConfEntry *entry, RBShellPlayer *player);
+static void gconf_track_transition_time_changed (GConfClient *client, guint cnxn_id,
+						 GConfEntry *entry, RBShellPlayer *player);
+static void gconf_track_transition_album_check_changed (GConfClient *client, guint cnxn_id,
+							GConfEntry *entry, RBShellPlayer *player);
+static void gconf_network_buffer_size_changed (GConfClient *client, guint cnxn_id,
+					       GConfEntry *entry, RBShellPlayer *player);
 static void rb_shell_player_playing_changed_cb (RBShellPlayer *player,
 						GParamSpec *arg1,
 						gpointer user_data);
@@ -146,11 +153,23 @@ static void rb_shell_player_extra_metada
 					       GValue *metadata,
 					       RBShellPlayer *player);
 
+static gboolean rb_shell_player_do_next_internal (RBShellPlayer *player,
+						  gboolean from_eos,
+						  GError **error);
 
 static gboolean rb_shell_player_jump_to_current_idle (RBShellPlayer *player);
 
 #define CONF_STATE		CONF_PREFIX "/state"
 
+/* number of seconds before the end of a track to start prerolling the next */
+#define PREROLL_TIME		2
+
+typedef enum {
+	STOP_CURRENT = 0,
+	CROSSFADE,
+	WAIT_EOS
+} PlaybackStartType;
+
 struct RBShellPlayerPrivate
 {
 	RhythmDB *db;
@@ -173,9 +192,11 @@ struct RBShellPlayerPrivate
 
 	RBPlayer *mmplayer;
 
-	char *song;
-	char *url;
 	guint elapsed;
+	gint track_transition_time;
+	gboolean track_transition_album_check;
+	RhythmDBEntry *playing_entry;
+	gboolean playing_entry_eos;
 
 	RBPlayOrder *play_order;
 	RBPlayOrder *queue_play_order;
@@ -187,6 +208,9 @@ struct RBShellPlayerPrivate
 
 	guint gconf_play_order_id;
 	guint gconf_song_position_slider_visibility_id;
+	guint gconf_track_transition_time_id;
+	guint gconf_track_transition_album_check_id;
+	guint gconf_network_buffer_size_id;
 
 	gboolean mute;
 	float volume;
@@ -491,7 +515,7 @@ rb_shell_player_constructor (GType type,
 
 	rb_shell_player_sync_song_position_slider_visibility (player);
 
-	g_signal_connect (G_OBJECT (player),
+	g_signal_connect (player,
 			  "notify::playing",
 			  G_CALLBACK (rb_shell_player_playing_changed_cb),
 			  NULL);
@@ -558,16 +582,42 @@ notify_playing_idle (RBShellPlayer *play
 	return FALSE;
 }
 
+static int
+rb_shell_player_get_crossfade (RBShellPlayer *player, PlaybackStartType play_type)
+{
+	switch (play_type) {
+	case STOP_CURRENT:
+		return -1;
+		break;
+	case CROSSFADE:
+		return player->priv->track_transition_time;
+		break;
+	case WAIT_EOS:
+		return 0;
+		break;
+	default:
+		g_assert_not_reached ();
+	}
+}
+
+
 static void
 rb_shell_player_open_playlist_url (RBShellPlayer *player,
-				   const char *location)
+				   const char *location,
+				   RhythmDBEntry *entry,
+				   PlaybackStartType play_type)
 {
 	GError *error = NULL;
+	gint crossfade = rb_shell_player_get_crossfade (player, play_type);
 
 	rb_debug ("playing stream url %s", location);
-	rb_player_open (player->priv->mmplayer, location, &error);
+	rb_player_open (player->priv->mmplayer,
+			location,
+			entry,
+			(GDestroyNotify) rhythmdb_entry_unref,
+			&error);
 	if (error == NULL)
-		rb_player_play (player->priv->mmplayer, &error);
+		rb_player_play (player->priv->mmplayer, crossfade, &error);
 
 	if (error) {
 		GDK_THREADS_ENTER ();
@@ -579,9 +629,9 @@ rb_shell_player_open_playlist_url (RBShe
 }
 
 static void
-rb_shell_player_handle_eos_unlocked (RBShellPlayer *player)
+rb_shell_player_handle_eos_unlocked (RBShellPlayer *player, RhythmDBEntry *entry)
 {
-	RhythmDBEntry *entry;
+	RhythmDBEntry *playing_entry;
 	RBSource *source;
 
 	source = player->priv->current_playing_source;
@@ -591,7 +641,19 @@ rb_shell_player_handle_eos_unlocked (RBS
 		return;
 	}
 
-	entry = rb_shell_player_get_playing_entry (player);
+	playing_entry = rb_shell_player_get_playing_entry (player);
+	if (entry != NULL ) {
+		if (playing_entry != entry) {
+			rb_debug ("EOS'd entry is not the current playing entry; ignoring");
+			return;
+		}
+	}
+
+	if (player->priv->playing_entry_eos) {
+		rb_debug ("playing entry has already EOS'd");
+		return;
+	}
+	player->priv->playing_entry_eos = TRUE;
 
 	switch (rb_source_handle_eos (source)) {
 	case RB_SOURCE_EOF_ERROR:
@@ -615,7 +677,10 @@ rb_shell_player_handle_eos_unlocked (RBS
 			char *location = g_queue_pop_head (player->priv->playlist_urls);
 			rb_debug ("trying next radio stream url: %s", location);
 
-			rb_shell_player_open_playlist_url (player, location);
+			/* we're handling an unexpected EOS here, so crossfading isn't
+			 * really possible anyway -> specify FALSE.
+			 */
+			rb_shell_player_open_playlist_url (player, location, entry, FALSE);
 			g_free (location);
 			break;
 		}
@@ -632,7 +697,7 @@ rb_shell_player_handle_eos_unlocked (RBS
 		{
 			GError *error = NULL;
 
-			if (!rb_shell_player_do_next (player, &error)) {
+			if (!rb_shell_player_do_next_internal (player, TRUE, &error)) {
 				if (error->domain != RB_SHELL_PLAYER_ERROR ||
 				    error->code != RB_SHELL_PLAYER_ERROR_END_OF_PLAYLIST)
 					g_warning ("Unhandled error: %s", error->message);
@@ -656,13 +721,23 @@ rb_shell_player_handle_eos_unlocked (RBS
 }
 
 static void
-rb_shell_player_handle_eos (RBShellPlayer *player)
+rb_shell_player_handle_eos (RBPlayer *player,
+			    RhythmDBEntry *entry,
+			    RBShellPlayer *shell_player)
 {
-	rb_debug ("handling eos!");
+	const char *location;
+	if (entry == NULL)
+		return;
 
 	GDK_THREADS_ENTER ();
 
-	rb_shell_player_handle_eos_unlocked (player);
+	location = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
+	if (entry != shell_player->priv->playing_entry) {
+		rb_debug ("got unexpected eos for %s", location);
+	} else {
+		rb_debug ("handling eos for %s", location);
+		rb_shell_player_handle_eos_unlocked (shell_player, entry);
+	}
 
 	GDK_THREADS_LEAVE ();
 }
@@ -674,7 +749,8 @@ rb_shell_player_init (RBShellPlayer *pla
 
 	player->priv = RB_SHELL_PLAYER_GET_PRIVATE (player);
 
-	player->priv->mmplayer = rb_player_new (&error);
+	player->priv->mmplayer = rb_player_new (eel_gconf_get_boolean (CONF_PLAYER_USE_XFADE_BACKEND),
+					        &error);
 	if (error != NULL) {
 		GtkWidget *dialog;
 		dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL,
@@ -689,21 +765,26 @@ rb_shell_player_init (RBShellPlayer *pla
 	gtk_box_set_spacing (GTK_BOX (player), 12);
 	gtk_container_set_border_width (GTK_CONTAINER (player), 3);
 
-	g_signal_connect_object (G_OBJECT (player->priv->mmplayer),
+	g_signal_connect_object (player->priv->mmplayer,
 				 "eos",
 				 G_CALLBACK (rb_shell_player_handle_eos),
-				 player, G_CONNECT_SWAPPED);
+				 player, 0);
 
-	g_signal_connect_object (G_OBJECT (player->priv->mmplayer),
+	g_signal_connect_object (player->priv->mmplayer,
 				 "tick",
 				 G_CALLBACK (tick_cb),
 				 player, 0);
 
-	g_signal_connect_object (G_OBJECT (player->priv->mmplayer),
+	g_signal_connect_object (player->priv->mmplayer,
 				 "error",
 				 G_CALLBACK (error_cb),
 				 player, 0);
 
+	g_signal_connect_object (player->priv->mmplayer,
+				 "playing-stream",
+				 G_CALLBACK (playing_stream_cb),
+				 player, 0);
+
 	g_signal_connect (G_OBJECT (gnome_vfs_get_volume_monitor ()),
 			  "volume-pre-unmount",
 			  G_CALLBACK (volume_pre_unmount_cb),
@@ -716,6 +797,22 @@ rb_shell_player_init (RBShellPlayer *pla
 
 	player->priv->volume = eel_gconf_get_float (CONF_STATE_VOLUME);
 
+	player->priv->track_transition_time = (int)(eel_gconf_get_float (CONF_PLAYER_TRANSITION_TIME));
+	player->priv->track_transition_album_check = eel_gconf_get_boolean (CONF_PLAYER_TRANSITION_ALBUM_CHECK);
+	player->priv->gconf_track_transition_time_id =
+		eel_gconf_notification_add (CONF_PLAYER_TRANSITION_TIME,
+					    (GConfClientNotifyFunc) gconf_track_transition_time_changed,
+					    player);
+	player->priv->gconf_track_transition_album_check_id =
+		eel_gconf_notification_add (CONF_PLAYER_TRANSITION_ALBUM_CHECK,
+					    (GConfClientNotifyFunc) gconf_track_transition_album_check_changed,
+					    player);
+	player->priv->gconf_network_buffer_size_id =
+		eel_gconf_notification_add (CONF_PLAYER_NETWORK_BUFFER_SIZE,
+					    (GConfClientNotifyFunc) gconf_network_buffer_size_changed,
+					    player);
+	gconf_network_buffer_size_changed (NULL, 0, NULL, player);
+
 	g_signal_connect (player, "notify::playing",
 			  G_CALLBACK (reemit_playing_signal), NULL);
 
@@ -1068,6 +1165,7 @@ rb_shell_player_new (RhythmDB *db,
 RhythmDBEntry *
 rb_shell_player_get_playing_entry (RBShellPlayer *player)
 {
+	/*
 	RBPlayOrder *porder;
 	RhythmDBEntry *entry;
 
@@ -1086,11 +1184,21 @@ rb_shell_player_get_playing_entry (RBShe
 	entry = rb_play_order_get_playing_entry (porder);
 
 	return entry;
+	*/
+
+	/* ? */
+	if (player->priv->playing_entry != NULL) {
+		return rhythmdb_entry_ref (player->priv->playing_entry);
+	} else {
+		return NULL;
+	}
 }
 
 typedef struct {
 	RBShellPlayer *player;
 	char *location;
+	RhythmDBEntry *entry;
+	PlaybackStartType play_type;
 } OpenLocationThreadData;
 
 static void
@@ -1133,13 +1241,13 @@ open_location_thread (OpenLocationThread
 
 			location = g_queue_pop_head (data->player->priv->playlist_urls);
 			rb_debug ("playing first stream url %s", data->location);
-			rb_shell_player_open_playlist_url (data->player, location);
+			rb_shell_player_open_playlist_url (data->player, location, data->entry, data->play_type);
 			g_free (location);
 		}
 	} else {
 		/* if we can't parse it as a playlist, just try playing it */
 		rb_debug ("playlist parser failed, playing %s directly", data->location);
-		rb_shell_player_open_playlist_url (data->player, data->location);
+		rb_shell_player_open_playlist_url (data->player, data->location, data->entry, data->play_type);
 	}
 
 	g_free (data);
@@ -1148,15 +1256,18 @@ open_location_thread (OpenLocationThread
 
 static gboolean
 rb_shell_player_open_location (RBShellPlayer *player,
-			       const char *location,
+			       RhythmDBEntry *entry,
+			       PlaybackStartType play_type,
 			       GError **error)
 {
-	char *unescaped;
+	char *location;
 	gboolean was_playing;
+	gboolean ret = TRUE;
 
-	unescaped = gnome_vfs_unescape_string_for_display (location);
-	rb_debug ("Opening %s...", unescaped);
-	g_free (unescaped);
+	location = rhythmdb_entry_get_playback_uri (entry);
+	if (location == NULL) {
+		return FALSE;
+	}
 
 	was_playing = rb_player_playing (player->priv->mmplayer);
 
@@ -1165,6 +1276,8 @@ rb_shell_player_open_location (RBShellPl
 
 		data = g_new0 (OpenLocationThreadData, 1);
 		data->player = player;
+		data->play_type = play_type;
+		data->entry = entry;
 
 		/* dispose of any existing playlist urls */
 		if (player->priv->playlist_urls) {
@@ -1183,36 +1296,22 @@ rb_shell_player_open_location (RBShellPl
 			data->location = g_strconcat ("http://", location, NULL);
 
 		g_thread_create ((GThreadFunc)open_location_thread, data, FALSE, NULL);
-		return TRUE;
 	} else {
-		if (!rb_player_open (player->priv->mmplayer, location, error))
-			return FALSE;
-
-		if (!rb_player_play (player->priv->mmplayer, error))
-			return FALSE;
+		gint crossfade;
+		gboolean ret = TRUE;
 
-		g_object_notify (G_OBJECT (player), "playing");
-	}
+		crossfade = rb_shell_player_get_crossfade (player, play_type);
+		ret = ret && rb_player_open (player->priv->mmplayer, location, entry, (GDestroyNotify) rhythmdb_entry_unref, error);
 
-	return TRUE;
-}
+		ret = ret && rb_player_play (player->priv->mmplayer, crossfade, error);
 
-static gboolean
-rb_shell_player_open_entry (RBShellPlayer *player,
-			    RhythmDBEntry *entry,
-			    GError **error)
-{
-	char *location;
-	gboolean result;
-
-	location = rhythmdb_entry_get_playback_uri (entry);
-	if (location == NULL)
-		return FALSE;
+		/*
+		if (ret)
+			g_object_notify (G_OBJECT (player), "playing"); */
+	}
 
-	result = rb_shell_player_open_location (player, location, error);
 	g_free (location);
-
-	return result;
+	return ret;
 }
 
 /**
@@ -1237,15 +1336,18 @@ rb_shell_player_play (RBShellPlayer *pla
 	if (rb_player_playing (player->priv->mmplayer))
 		return TRUE;
 
-	if (!rb_player_play (player->priv->mmplayer, error))
+	/* we're obviously not playing anything, so crossfading is irrelevant */
+	if (!rb_player_play (player->priv->mmplayer, FALSE, error))
 		return FALSE;
 
 	songs = rb_source_get_entry_view (player->priv->current_playing_source);
 	if (songs)
 		rb_entry_view_set_state (songs, RB_ENTRY_VIEW_PLAYING);
 
+	/*
 	rb_shell_player_sync_with_source (player);
 	rb_shell_player_sync_buttons (player);
+	*/
 
 	return TRUE;
 }
@@ -1273,7 +1375,7 @@ static gboolean
 do_next_idle (RBShellPlayer *player)
 {
 	/* use the EOS callback, so that EOF_SOURCE_ conditions are handled properly */
-	rb_shell_player_handle_eos (player);
+	rb_shell_player_handle_eos (NULL, NULL, player);
 	player->priv->do_next_idle_id = 0;
 
 	return FALSE;
@@ -1283,26 +1385,55 @@ static gboolean
 rb_shell_player_set_playing_entry (RBShellPlayer *player,
 				   RhythmDBEntry *entry,
 				   gboolean out_of_order,
+				   gboolean wait_for_eos,
 				   GError **error)
 {
 	GError *tmp_error = NULL;
-	const char *location;
 	GValue val = {0,};
+	PlaybackStartType play_type;
 
 	g_return_val_if_fail (player->priv->current_playing_source != NULL, TRUE);
 	g_return_val_if_fail (entry != NULL, TRUE);
 
+	play_type = wait_for_eos ? WAIT_EOS : STOP_CURRENT;
+
 	if (out_of_order) {
 		RBPlayOrder *porder;
-		if (player->priv->current_playing_source == RB_SOURCE (player->priv->queue_source))
+
+		if (player->priv->current_playing_source == RB_SOURCE (player->priv->queue_source)) {
 			porder = player->priv->queue_play_order;
-		else
+		} else {
 			porder = player->priv->play_order;
+		}
 		rb_play_order_set_playing_entry (porder, entry);
 	}
 
-	if (!rb_shell_player_open_entry (player, entry, &tmp_error))
+	if (player->priv->playing_entry != NULL &&
+	    player->priv->track_transition_time > 0) {
+		if (player->priv->track_transition_album_check) {
+			const char *previous_album;
+			const char *album;
+
+			previous_album = rhythmdb_entry_get_string (player->priv->playing_entry, RHYTHMDB_PROP_ALBUM);
+			album = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
+			/* only crossfade if we're not going from the end of one song on an
+			 * album to the start of another.  "Unknown" doesn't count as an album.
+			 */
+			if (wait_for_eos == FALSE ||
+			    strcmp (album, _("Unknown")) == 0 ||
+			    strcmp (album, previous_album) != 0) {
+				play_type = CROSSFADE;
+			}
+		} else {
+			/* always crossfade */
+			play_type = CROSSFADE;
+		}
+	}
+
+	if (rb_shell_player_open_location (player, entry, play_type, error) == FALSE) {
 		goto lose;
+	}
+
 	rb_shell_player_sync_replaygain (player, entry);
 
 	rb_debug ("Success!");
@@ -1312,21 +1443,11 @@ rb_shell_player_set_playing_entry (RBShe
 	rhythmdb_entry_set (player->priv->db, entry, RHYTHMDB_PROP_PLAYBACK_ERROR, &val);
 	g_value_unset (&val);
 
-	location = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
-	g_signal_emit (G_OBJECT (player),
-		       rb_shell_player_signals[PLAYING_SONG_CHANGED], 0,
-		       entry);
-	g_signal_emit (G_OBJECT (player),
-		       rb_shell_player_signals[PLAYING_URI_CHANGED], 0,
-		       location);
-
-	rb_shell_player_sync_with_source (player);
-	rb_shell_player_sync_buttons (player);
-
 	return TRUE;
  lose:
 	/* Ignore errors, shutdown the player */
-	rb_player_close (player->priv->mmplayer, NULL);
+	rb_player_close (player->priv->mmplayer, NULL /* XXX specify uri? */, NULL);
+
 	if (tmp_error == NULL)
 		tmp_error = g_error_new (RB_SHELL_PLAYER_ERROR,
 					 RB_SHELL_PLAYER_ERROR_NOT_PLAYING,
@@ -1601,12 +1722,12 @@ rb_shell_player_do_previous (RBShellPlay
 		if (new_source != player->priv->current_playing_source)
 			swap_playing_source (player, new_source);
 
-		if (!rb_shell_player_set_playing_entry (player, entry, FALSE, error)) {
+		if (!rb_shell_player_set_playing_entry (player, entry, FALSE, FALSE, error)) {
 			rhythmdb_entry_unref (entry);
 			return FALSE;
 		}
 
-		rb_shell_player_jump_to_current (player);
+		/*rb_shell_player_jump_to_current (player);*/
 		rhythmdb_entry_unref (entry);
 	} else {
 		rb_debug ("no previous song found, signaling error");
@@ -1621,9 +1742,8 @@ rb_shell_player_do_previous (RBShellPlay
 	return TRUE;
 }
 
-gboolean
-rb_shell_player_do_next (RBShellPlayer *player,
-			 GError **error)
+static gboolean
+rb_shell_player_do_next_internal (RBShellPlayer *player, gboolean from_eos, GError **error)
 {
 	RBSource *new_source = NULL;
 	RhythmDBEntry *entry = NULL;
@@ -1684,7 +1804,7 @@ rb_shell_player_do_next (RBShellPlayer *
 		if (new_source != player->priv->current_playing_source)
 			swap_playing_source (player, new_source);
 
-		if (!rb_shell_player_set_playing_entry (player, entry, FALSE, error))
+		if (!rb_shell_player_set_playing_entry (player, entry, FALSE, from_eos, error))
 			rv = FALSE;
 	} else {
 		g_set_error (error,
@@ -1698,7 +1818,7 @@ rb_shell_player_do_next (RBShellPlayer *
 		rv = FALSE;
 	}
 
-	g_idle_add ((GSourceFunc)rb_shell_player_jump_to_current_idle, player);
+	/*g_idle_add ((GSourceFunc)rb_shell_player_jump_to_current_idle, player);*/
 
 	if (entry != NULL) {
 		rhythmdb_entry_unref (entry);
@@ -1707,6 +1827,13 @@ rb_shell_player_do_next (RBShellPlayer *
 	return rv;
 }
 
+gboolean
+rb_shell_player_do_next (RBShellPlayer *player,
+			 GError **error)
+{
+	return rb_shell_player_do_next_internal (player, FALSE, error);
+}
+
 static gboolean
 rb_shell_player_do_previous_or_seek (RBShellPlayer *player,
 				     GError **error)
@@ -1777,7 +1904,7 @@ rb_shell_player_play_entry (RBShellPlaye
 		source = player->priv->selected_source;
 	rb_shell_player_set_playing_source (player, source);
 
-	if (!rb_shell_player_set_playing_entry (player, entry, TRUE, &error)) {
+	if (!rb_shell_player_set_playing_entry (player, entry, TRUE, FALSE, &error)) {
 		rb_shell_player_error (player, FALSE, error);
 		g_clear_error (&error);
 	}
@@ -1834,9 +1961,15 @@ rb_shell_player_playpause (RBShellPlayer
 			songs = rb_source_get_entry_view (player->priv->current_playing_source);
 			if (songs)
 				rb_entry_view_set_state (songs, RB_ENTRY_VIEW_PAUSED);
+
+			/* might need a signal for when the player has actually paused here? */
+			g_object_notify (G_OBJECT (player), "playing");
+			/* mostly for that */
 		} else {
 			rb_debug ("setting playing source to NULL");
 			rb_shell_player_set_playing_source (player, NULL);
+
+			g_object_notify (G_OBJECT (player), "playing");
 		}
 	} else {
 		RhythmDBEntry *entry;
@@ -1893,9 +2026,9 @@ rb_shell_player_playpause (RBShellPlayer
 				if (new_source != player->priv->current_playing_source)
 					swap_playing_source (player, new_source);
 
-				if (!rb_shell_player_set_playing_entry (player, entry, out_of_order, error))
+				if (!rb_shell_player_set_playing_entry (player, entry, out_of_order, FALSE, error))
 					ret = FALSE;
-				rb_shell_player_jump_to_current (player);
+				/*rb_shell_player_jump_to_current (player);*/
 			}
 		} else {
 			if (!rb_shell_player_play (player, error)) {
@@ -1911,7 +2044,7 @@ rb_shell_player_playpause (RBShellPlayer
 
 	rb_shell_player_sync_with_source (player);
 	rb_shell_player_sync_buttons (player);
-	g_object_notify (G_OBJECT (player), "playing");
+	/*g_object_notify (G_OBJECT (player), "playing");*/
 
 	return ret;
 }
@@ -1996,8 +2129,9 @@ rb_shell_player_sync_replaygain (RBShell
 	}
 
 	if (eel_gconf_get_boolean (CONF_USE_REPLAYGAIN)) {
-		rb_player_set_replaygain (player->priv->mmplayer, entry_track_gain,
-					  entry_track_peak, entry_album_gain, entry_album_peak);
+		rb_player_set_replaygain (player->priv->mmplayer, NULL,
+					  entry_track_gain, entry_track_peak,
+					  entry_album_gain, entry_album_peak);
 	}
 }
 
@@ -2161,7 +2295,7 @@ rb_shell_player_entry_activated_cb (RBEn
 			/* since we just moved the entry, we should give it focus.
 			 * just calling rb_shell_player_jump_to_current here
 			 * looks terribly ugly, though. */
-			g_idle_add ((GSourceFunc)rb_shell_player_jump_to_current_idle, playa);
+			/*g_idle_add ((GSourceFunc)rb_shell_player_jump_to_current_idle, playa);*/
 			was_from_queue = FALSE;
 			source_set = TRUE;
 		} else {
@@ -2186,7 +2320,7 @@ rb_shell_player_entry_activated_cb (RBEn
 		source_set = TRUE;
 	}
 
-	if (!rb_shell_player_set_playing_entry (playa, entry, TRUE, &error)) {
+	if (!rb_shell_player_set_playing_entry (playa, entry, TRUE, FALSE, &error)) {
 		rb_shell_player_error (playa, FALSE, error);
 		g_clear_error (&error);
 	}
@@ -2229,7 +2363,7 @@ rb_shell_player_property_row_activated_c
 
 	rb_play_order_go_next (player->priv->play_order);
 
-	if (!rb_shell_player_set_playing_entry (player, entry, TRUE, &error)) {
+	if (!rb_shell_player_set_playing_entry (player, entry, TRUE, FALSE, &error)) {
 		rb_shell_player_error (player, FALSE, error);
 		g_clear_error (&error);
 	}
@@ -2561,14 +2695,11 @@ rb_shell_player_set_playing_source_inter
 		actually_set_playing_source (player, source, sync_entry_view);
 	}
 
-	g_free (player->priv->url);
-	player->priv->url = NULL;
-
 	if (player->priv->current_playing_source == NULL)
 		rb_shell_player_stop (player);
 
 	rb_shell_player_sync_with_source (player);
-	g_object_notify (G_OBJECT (player), "playing");
+	/*g_object_notify (G_OBJECT (player), "playing");*/
 	if (player->priv->selected_source)
 		rb_shell_player_sync_buttons (player);
 
@@ -2600,7 +2731,7 @@ rb_shell_player_stop (RBShellPlayer *pla
 	g_return_if_fail (RB_IS_SHELL_PLAYER (player));
 
 	if (error == NULL)
-		rb_player_close (player->priv->mmplayer, &error);
+		rb_player_close (player->priv->mmplayer, NULL, &error);
 	if (error) {
 		rb_error_dialog (NULL,
 				 _("Couldn't stop playback"),
@@ -2795,8 +2926,82 @@ rb_shell_player_error (RBShellPlayer *pl
 	}
 }
 
+static gboolean
+new_playing_stream_idle_cb (RBShellPlayer *player)
+{
+	RhythmDBEntry *entry;
+	const char *location;
+
+	GDK_THREADS_ENTER ();
+
+	/* emit some interesting signals */
+	entry = rb_shell_player_get_playing_entry (player);
+	location = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
+	rb_debug ("new playing stream: %s", location);
+	g_signal_emit (G_OBJECT (player),
+		       rb_shell_player_signals[PLAYING_SONG_CHANGED], 0,
+		       entry);
+	g_signal_emit (G_OBJECT (player),
+		       rb_shell_player_signals[PLAYING_URI_CHANGED], 0,
+		       location);
+
+	/* resync UI */
+	rb_shell_player_sync_with_source (player);
+	rb_shell_player_sync_buttons (player);
+	g_object_notify (G_OBJECT (player), "playing");
+
+	/* we probably don't want to do this for all cases, actually.
+	 * do this in a timeout because if we do it immediately (or even in
+	 * another idle handler), it causes gaps in the audio output.
+	 * I have no idea why this is.  Even 250ms later, it's mostly OK and
+	 * the UI doesn't look too sluggish.  hmm.
+	 */
+	g_timeout_add (250, (GSourceFunc) rb_shell_player_jump_to_current_idle, player);
+
+	GDK_THREADS_LEAVE ();
+	return FALSE;
+}
+
+static gboolean
+current_playing_stream_idle_cb (RBShellPlayer *player)
+{
+	GDK_THREADS_ENTER ();
+
+	/* resync UI */
+	rb_shell_player_sync_with_source (player);
+	rb_shell_player_sync_buttons (player);
+	g_object_notify (G_OBJECT (player), "playing");
+
+	GDK_THREADS_LEAVE ();
+	return FALSE;
+}
+
+static void
+playing_stream_cb (RBPlayer *mmplayer,
+		   RhythmDBEntry *entry,
+		   RBShellPlayer *player)
+{
+	GDK_THREADS_ENTER ();
+
+	/* only emit playing-song-changed etc. when the entry changes */
+	if (player->priv->playing_entry != entry) {
+		g_idle_add ((GSourceFunc) new_playing_stream_idle_cb, player);
+	} else {
+		g_idle_add ((GSourceFunc) current_playing_stream_idle_cb, player);
+	}
+
+	/* update playing entry */
+	if (player->priv->playing_entry)
+		rhythmdb_entry_unref (player->priv->playing_entry);
+	player->priv->playing_entry = rhythmdb_entry_ref (entry);
+	player->priv->playing_entry_eos = FALSE;
+
+	GDK_THREADS_LEAVE ();
+}
+
 static void
 error_cb (RBPlayer *mmplayer,
+	  RhythmDBEntry *entry,
 	  const GError *err,
 	  gpointer data)
 {
@@ -2812,21 +3017,51 @@ error_cb (RBPlayer *mmplayer,
 
 	GDK_THREADS_ENTER ();
 
-	rb_shell_player_error (player, TRUE, err);
+	if (entry != player->priv->playing_entry) {
+		rb_debug ("got error for unexpected entry %p (expected %p)", entry, player->priv->playing_entry);
+	} else {
+		rb_shell_player_error (player, TRUE, err);
+		rb_debug ("exiting error hander");
+	}
 
-	rb_debug ("exiting error hander");
 	GDK_THREADS_LEAVE ();
 }
 
 static void
 tick_cb (RBPlayer *mmplayer,
+	 RhythmDBEntry *entry,
 	 long elapsed,
+	 long duration,
 	 gpointer data)
 {
  	RBShellPlayer *player = RB_SHELL_PLAYER (data);
+	gint remaining_check = 0;
+	gboolean duration_from_player = TRUE;
+	const char *uri;
 
 	GDK_THREADS_ENTER ();
 
+	if (player->priv->playing_entry != entry) {
+		rb_debug ("got tick for unexpected entry %p (expected %p)", entry, player->priv->playing_entry);
+		GDK_THREADS_LEAVE ();
+		return;
+	}
+
+	/* if we aren't getting a duration value from the player, use the
+	 * value from the entry, if any.
+	 */
+	if (duration < 1) {
+		duration = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DURATION);
+		duration_from_player = FALSE;
+	}
+
+	uri = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
+	rb_debug ("tick: [%s, %lu:%lu(%d)]",
+		  uri,
+		  elapsed,
+		  duration,
+		  duration_from_player);
+
 	if (rb_player_playing (mmplayer)) {
 		if (elapsed < 0)
 			elapsed = 0;
@@ -2836,6 +3071,33 @@ tick_cb (RBPlayer *mmplayer,
 			g_signal_emit (G_OBJECT (player), rb_shell_player_signals[ELAPSED_CHANGED],
 				       0, player->priv->elapsed);
 		}
+
+		if (duration_from_player) {
+			/* XXX update duration in various things? */
+		}
+	}
+
+	/* check if we should start a crossfade */
+	if (rb_player_multiple_open (mmplayer)) {
+		if (player->priv->track_transition_time == 0) {
+			remaining_check = PREROLL_TIME;
+		} else {
+			remaining_check = player->priv->track_transition_time;
+		}
+	}
+
+	/*
+	 * just pretending we got an EOS will do exactly what we want
+	 * here.  for the album crossfade case, it might mean we leave
+	 * the stream prerolled for a few extra seconds, in which time
+	 * something interesting (file being deleted?) might happen.
+	 */
+	if (remaining_check > 0 &&
+	    duration > 0 &&
+	    elapsed > 0 &&
+	    ((duration - elapsed) <= remaining_check)) {
+		rb_debug ("%ld seconds remaining in stream %s; need %d for transition", duration - elapsed, uri, remaining_check);
+		rb_shell_player_handle_eos_unlocked (player, entry);
 	}
 
 	GDK_THREADS_LEAVE ();
@@ -2934,3 +3196,46 @@ rb_shell_player_error_get_type (void)
 
 	return etype;
 }
+
+static void
+gconf_track_transition_time_changed (GConfClient *client,
+				     guint cnxn_id,
+				     GConfEntry *entry,
+				     RBShellPlayer *player)
+{
+	rb_debug ("track transition time changed");
+	player->priv->track_transition_time = (int)(eel_gconf_get_float (CONF_PLAYER_TRANSITION_TIME));
+}
+
+static void
+gconf_track_transition_album_check_changed (GConfClient *client,
+					    guint cnxn_id,
+					    GConfEntry *entry,
+					    RBShellPlayer *player)
+{
+	rb_debug ("track transition album check flag changed");
+	player->priv->track_transition_album_check = eel_gconf_get_boolean (CONF_PLAYER_TRANSITION_ALBUM_CHECK);
+}
+
+static void
+gconf_network_buffer_size_changed (GConfClient *client,
+				   guint cnxn_id,
+				   GConfEntry *entry,
+				   RBShellPlayer *player)
+{
+	guint buffer_size;
+
+	if (player->priv->mmplayer == NULL
+	    || (g_object_class_find_property (G_OBJECT_GET_CLASS (player->priv->mmplayer),
+			    		      "buffer-size") == NULL)) {
+		return;
+	}
+
+	rb_debug ("network buffer size changed");
+	buffer_size = eel_gconf_get_integer (CONF_PLAYER_NETWORK_BUFFER_SIZE);
+	if (buffer_size < 64)
+		buffer_size = 64;
+
+	g_object_set (player->priv->mmplayer, "buffer-size", buffer_size, NULL);
+}
+
diff -rN -u -p old-npb/shell/rb-shell-preferences.c new-npb/shell/rb-shell-preferences.c
--- old-npb/shell/rb-shell-preferences.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/shell/rb-shell-preferences.c	2007-03-18 18:49:31.000000000 +1000
@@ -60,6 +60,15 @@ void rb_shell_preferences_browser_views_
 						      RBShellPreferences *shell_preferences);
 static void rb_shell_preferences_toolbar_style_cb (GtkComboBox *box,
 						   RBShellPreferences *preferences);
+static void rb_shell_preferences_player_backend_cb (GtkToggleButton *button,
+						    RBShellPreferences *preferences);
+static void rb_shell_preferences_album_crossfade_cb (GtkToggleButton *button,
+						     RBShellPreferences *preferences);
+static void rb_shell_preferences_transition_duration_cb (GtkRange *range,
+							 RBShellPreferences *preferences);
+static void rb_shell_preferences_network_buffer_size_cb (GtkRange *range,
+							 RBShellPreferences *preferences);
+static void update_playback_prefs_sensitivity (RBShellPreferences *preferences);
 
 enum
 {
@@ -85,6 +94,11 @@ struct RBShellPreferencesPrivate
 	GtkWidget *quality_check;
 	GtkWidget *year_check;
 
+	GtkWidget *xfade_backend_check;
+	GtkWidget *album_crossfade_check;
+	GtkWidget *transition_duration;
+	GtkWidget *network_buffer_size;
+
 	GSList *browser_views_group;
 
 	GtkWidget *toolbar_style_menu;
@@ -131,11 +145,11 @@ rb_shell_preferences_init (RBShellPrefer
 
 	shell_preferences->priv = RB_SHELL_PREFERENCES_GET_PRIVATE (shell_preferences);
 
-	g_signal_connect_object (G_OBJECT (shell_preferences),
+	g_signal_connect_object (shell_preferences,
 				 "delete_event",
 				 G_CALLBACK (rb_shell_preferences_window_delete_cb),
 				 shell_preferences, 0);
-	g_signal_connect_object (G_OBJECT (shell_preferences),
+	g_signal_connect_object (shell_preferences,
 				 "response",
 				 G_CALLBACK (rb_shell_preferences_response_cb),
 				 shell_preferences, 0);
@@ -146,7 +160,7 @@ rb_shell_preferences_init (RBShellPrefer
 	tmp = gtk_dialog_add_button (GTK_DIALOG (shell_preferences),
 			              GTK_STOCK_HELP,
 			              GTK_RESPONSE_HELP);
-	g_signal_connect_object (G_OBJECT (tmp), "clicked",
+	g_signal_connect_object (tmp, "clicked",
 				 G_CALLBACK (help_cb), shell_preferences, 0);
 	gtk_dialog_set_default_response (GTK_DIALOG (shell_preferences),
 					 GTK_RESPONSE_CLOSE);
@@ -220,7 +234,56 @@ rb_shell_preferences_init (RBShellPrefer
 	eel_gconf_notification_add (CONF_UI_DIR,
 				    (GConfClientNotifyFunc) rb_shell_preferences_ui_pref_changed,
 				    shell_preferences);
-	g_object_unref (G_OBJECT (xml));
+	g_object_unref (xml);
+
+	/* playback preferences */
+	xml = rb_glade_xml_new ("playback-prefs.glade",
+				"playback_prefs_box",
+				shell_preferences);
+
+	rb_glade_boldify_label (xml, "backend_label");
+	rb_glade_boldify_label (xml, "crossfade_type_label");
+	rb_glade_boldify_label (xml, "duration_label");
+	rb_glade_boldify_label (xml, "buffer_size_label");
+
+	shell_preferences->priv->xfade_backend_check =
+		glade_xml_get_widget (xml, "use_xfade_backend");
+	shell_preferences->priv->album_crossfade_check =
+		glade_xml_get_widget (xml, "album_check");
+	shell_preferences->priv->transition_duration =
+		glade_xml_get_widget (xml, "duration");
+	shell_preferences->priv->network_buffer_size =
+		glade_xml_get_widget (xml, "network_buffer_size");
+
+	g_signal_connect_object (shell_preferences->priv->xfade_backend_check,
+				 "toggled",
+				 G_CALLBACK (rb_shell_preferences_player_backend_cb),
+				 shell_preferences, 0);
+
+	g_signal_connect_object (shell_preferences->priv->album_crossfade_check,
+				 "toggled",
+				 G_CALLBACK (rb_shell_preferences_album_crossfade_cb),
+				 shell_preferences, 0);
+
+	g_signal_connect_object (shell_preferences->priv->transition_duration,
+				 "value-changed",
+				 G_CALLBACK (rb_shell_preferences_transition_duration_cb),
+				 shell_preferences, 0);
+
+	g_signal_connect_object (shell_preferences->priv->network_buffer_size,
+				 "value-changed",
+				 G_CALLBACK (rb_shell_preferences_network_buffer_size_cb),
+				 shell_preferences, 0);
+
+	gtk_notebook_append_page (GTK_NOTEBOOK (shell_preferences->priv->notebook),
+				  glade_xml_get_widget (xml, "playback_prefs_box"),
+				  gtk_label_new (_("Playback")));
+	g_object_unref (xml);
+
+	eel_gconf_notification_add (CONF_PLAYER_DIR,
+				    (GConfClientNotifyFunc) rb_shell_preferences_ui_pref_changed,
+				    shell_preferences);
+
 	rb_shell_preferences_sync (shell_preferences);
 }
 
@@ -404,6 +467,9 @@ rb_shell_preferences_sync (RBShellPrefer
 	char *columns;
 	GSList  *l;
 	gint view, i;
+	gboolean b;
+	float duration;
+	int buffer_size;
 
 	shell_preferences->priv->loading = TRUE;
 
@@ -468,6 +534,21 @@ rb_shell_preferences_sync (RBShellPrefer
 					   G_CALLBACK (rb_shell_preferences_toolbar_style_cb),
 					   shell_preferences);
 
+	/* player preferences */
+	b = eel_gconf_get_boolean (CONF_PLAYER_USE_XFADE_BACKEND);
+	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (shell_preferences->priv->xfade_backend_check), b);
+
+	b = eel_gconf_get_boolean (CONF_PLAYER_TRANSITION_ALBUM_CHECK);
+	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (shell_preferences->priv->album_crossfade_check), !b);
+
+	duration = eel_gconf_get_float (CONF_PLAYER_TRANSITION_TIME);
+	gtk_range_set_value (GTK_RANGE (shell_preferences->priv->transition_duration), duration);
+
+	buffer_size = eel_gconf_get_integer (CONF_PLAYER_NETWORK_BUFFER_SIZE);
+	gtk_range_set_value (GTK_RANGE (shell_preferences->priv->network_buffer_size), buffer_size);
+
+	update_playback_prefs_sensitivity (shell_preferences);
+
 	shell_preferences->priv->loading = FALSE;
 }
 
@@ -498,3 +579,58 @@ rb_shell_preferences_browser_views_activ
 
 	eel_gconf_set_integer (CONF_UI_BROWSER_VIEWS, index);
 }
+
+
+static void
+update_playback_prefs_sensitivity (RBShellPreferences *preferences)
+{
+	gboolean backend;
+
+	backend = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (preferences->priv->xfade_backend_check));
+
+	gtk_widget_set_sensitive (preferences->priv->album_crossfade_check, backend);
+	gtk_widget_set_sensitive (preferences->priv->network_buffer_size, backend);
+	gtk_widget_set_sensitive (preferences->priv->transition_duration, backend);
+}
+
+static void
+rb_shell_preferences_player_backend_cb (GtkToggleButton *button,
+					RBShellPreferences *preferences)
+{
+	update_playback_prefs_sensitivity (preferences);
+
+	eel_gconf_set_boolean (CONF_PLAYER_USE_XFADE_BACKEND,
+			       gtk_toggle_button_get_active (button));
+}
+
+static void
+rb_shell_preferences_album_crossfade_cb (GtkToggleButton *button,
+					 RBShellPreferences *preferences)
+{
+	/* sense is inverted here because the wording sounded more natural
+	 * this way.  "[ ] Don't do x" sucks.
+	 */
+	eel_gconf_set_boolean (CONF_PLAYER_TRANSITION_ALBUM_CHECK,
+			       !gtk_toggle_button_get_active (button));
+}
+
+static void
+rb_shell_preferences_transition_duration_cb (GtkRange *range,
+					     RBShellPreferences *preferences)
+{
+	gdouble v;
+
+	v = gtk_range_get_value (range);
+	eel_gconf_set_float (CONF_PLAYER_TRANSITION_TIME, (float)v);
+}
+
+static void
+rb_shell_preferences_network_buffer_size_cb (GtkRange *range,
+					     RBShellPreferences *preferences)
+{
+	gdouble v;
+
+	v = gtk_range_get_value (range);
+	eel_gconf_set_integer (CONF_PLAYER_NETWORK_BUFFER_SIZE, (int)v);
+}
+
diff -rN -u -p old-npb/sources/rb-streaming-source.c new-npb/sources/rb-streaming-source.c
--- old-npb/sources/rb-streaming-source.c	2007-03-18 18:49:31.000000000 +1000
+++ new-npb/sources/rb-streaming-source.c	2007-03-18 18:49:31.000000000 +1000
@@ -214,7 +214,7 @@ rb_streaming_source_get_progress (RBStre
 }
 
 static void
-buffering_cb (GObject *backend, guint progress, RBStreamingSource *source)
+buffering_cb (GObject *backend, gpointer whatever, guint progress, RBStreamingSource *source)
 {
 	if (progress == 0)
 		return;

