/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 *  Copyright (C) 2003 Jorn Baayen <jorn@nl.linux.org>
 *  Copyright (C) 2003,2004 Colin Walters <walters@debian.org>
 *  Copyright (C) 2009 Jonathan Matthew  <jonathan@d14n.org>
 *
 *  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.
 *
 *  The Rhythmbox authors hereby grant permission for non-GPL compatible
 *  GStreamer plugins to be used and distributed together with GStreamer
 *  and Rhythmbox. This permission is above and beyond the permissions granted
 *  by the GPL license by which Rhythmbox is covered. If you modify this code
 *  you may extend this exception to your version of the code, but you are not
 *  obligated to do so. If you do not wish to do so, delete this exception
 *  statement from your 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.
 *
 */

#include <config.h>
#include <math.h>
#include <string.h>
#include <stdlib.h>

#include <glib/gi18n.h>
#include <gdk/gdk.h>
#include <gst/tag/tag.h>
#include <gst/pbutils/pbutils.h>

#include "rb-debug.h"
#include "rb-marshal.h"
#include "rb-util.h"

#include "rb-player.h"
#include "rb-player-gst.h"

static void rb_player_init (RBPlayerIface *iface);

G_DEFINE_TYPE_WITH_CODE(RBPlayerGst, rb_player_gst, G_TYPE_OBJECT,
			G_IMPLEMENT_INTERFACE(RB_TYPE_PLAYER, rb_player_init)
			)

#define RB_PLAYER_GST_TICK_HZ 5
#define STATE_CHANGE_MESSAGE_TIMEOUT 5

enum
{
	PROP_0,
	PROP_PLAYBIN
};

enum
{
	MISSING_PLUGINS,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

struct _RBPlayerGstPrivate
{
	char *uri;
	gpointer stream_data;
	GDestroyNotify stream_data_destroy;

	GstElement *playbin;
	GstElement *audio_sink;

	gboolean playing;
	gboolean buffering;

	gboolean emitted_error;

	float cur_volume;
	float replaygain_scale;

	guint tick_timeout_id;
};

static gboolean
tick_timeout (RBPlayerGst *mp)
{
	if (mp->priv->playing) {
		_rb_player_emit_tick (RB_PLAYER (mp),
				      mp->priv->stream_data,
				      rb_player_get_time (RB_PLAYER (mp)),
				      -1);
	}
	return TRUE;
}


static void
process_tag (const GstTagList *list, const gchar *tag, RBPlayerGst *player)
{
	int count;
	RBMetaDataField field;
	const GValue *val;
	GValue newval = {0,};

	count = gst_tag_list_get_tag_size (list, tag);
	if (count < 1)
		return;

	/* 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;

	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_player_emit_info (RB_PLAYER (player), player->priv->stream_data, field, &newval);
	g_value_unset (&newval);
}

static void
handle_missing_plugin_message (RBPlayerGst *player, GstMessage *message)
{
	char **details;
	char **descriptions;
	char *detail;
	char *description;
	int count;

	rb_debug ("got missing-plugin message from %s: %s",
		  GST_OBJECT_NAME (GST_MESSAGE_SRC (message)),
		  gst_missing_plugin_message_get_installer_detail (message));

	/* probably need to wait to collect any subsequent missing-plugin
	 * messages, but I think we'd need to wait for state changes to do
	 * that.  for now, we can only handle a single message.
	 */
	count = 1;

	details = g_new0 (char *, count + 1);
	descriptions = g_new0 (char *, count + 1);

	detail = gst_missing_plugin_message_get_installer_detail (message);
	description = gst_missing_plugin_message_get_description (message);
	details[0] = g_strdup (detail);
	descriptions[0] = g_strdup (description);

	g_signal_emit (player, signals[MISSING_PLUGINS], 0, player->priv->stream_data, details, descriptions);
	g_strfreev (details);
	g_strfreev (descriptions);
}

static gboolean
bus_cb (GstBus *bus, GstMessage *message, RBPlayerGst *mp)
{
	const GstStructure *structure;
	g_return_val_if_fail (mp != NULL, FALSE);

	switch (GST_MESSAGE_TYPE (message)) {
	case GST_MESSAGE_ERROR: {
		char *debug;
		GError *error, *sig_error;
		int code;
		gboolean emit = TRUE;

		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 (mp->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);
			mp->priv->emitted_error = TRUE;
			_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, NULL);

		g_error_free (error);
		g_free (debug);
		break;
	}

	case GST_MESSAGE_EOS:
		_rb_player_emit_eos (RB_PLAYER (mp), mp->priv->stream_data);
		break;

	case GST_MESSAGE_TAG: {
		GstTagList *tags;
		gst_message_parse_tag (message, &tags);

		gst_tag_list_foreach (tags, (GstTagForeachFunc) process_tag, mp);
		gst_tag_list_free (tags);
		break;
	}

	case GST_MESSAGE_BUFFERING: {
		gint progress;

		structure = gst_message_get_structure (message);
		if (!gst_structure_get_int (structure, "buffer-percent", &progress)) {
			g_warning ("Could not get value from BUFFERING message");
			break;
		}
		if (progress >= 100) {
			mp->priv->buffering = FALSE;
			if (mp->priv->playing) {
				rb_debug ("buffering done, setting pipeline back to PLAYING");
				gst_element_set_state (mp->priv->playbin, GST_STATE_PLAYING);
			} else {
				rb_debug ("buffering done, leaving pipeline PAUSED");
			}
		} else if (mp->priv->buffering == FALSE && mp->priv->playing) {
			GstState cur_state;

			gst_element_get_state (mp->priv->playbin, &cur_state, NULL, 0);
			if (cur_state == GST_STATE_PLAYING) {
				rb_debug ("buffering - temporarily pausing playback");
				gst_element_set_state (mp->priv->playbin, GST_STATE_PAUSED);
			} else {
				rb_debug ("buffering - during preroll; doing nothing");
			}
			mp->priv->buffering = TRUE;
		}

		_rb_player_emit_buffering (RB_PLAYER (mp), mp->priv->stream_data, progress);
		break;
	}

	case GST_MESSAGE_APPLICATION:
		structure = gst_message_get_structure (message);
		_rb_player_emit_event (RB_PLAYER (mp), mp->priv->stream_data, gst_structure_get_name (structure), NULL);
		break;

	case GST_MESSAGE_ELEMENT:
		if (gst_is_missing_plugin_message (message)) {
			handle_missing_plugin_message (mp, message);
		}
		break;

	default:
		break;
	}

	return TRUE;
}

static gboolean
construct_pipeline (RBPlayerGst *mp, GError **error)
{
	GstElement *sink;

	mp->priv->playbin = gst_element_factory_make ("playbin2", NULL);
	if (mp->priv->playbin == NULL) {
		g_set_error (error,
			     RB_PLAYER_ERROR,
			     RB_PLAYER_ERROR_GENERAL,
			     _("Failed to create playbin2 element; check your GStreamer installation"));
		return FALSE;
	}

	gst_bus_add_watch (gst_element_get_bus (mp->priv->playbin),
			   (GstBusFunc) bus_cb,
			   mp);

	/* let plugins add bits to playbin */
	g_object_notify (G_OBJECT (mp), "playbin");

	/* Use gconfaudiosink for audio if there's no audio sink yet */
	g_object_get (mp->priv->playbin, "audio-sink", &mp->priv->audio_sink, NULL);
	if (mp->priv->audio_sink == NULL) {
		mp->priv->audio_sink = gst_element_factory_make ("gconfaudiosink", NULL);
		if (mp->priv->audio_sink == NULL) {
			/* fall back to autoaudiosink */
			rb_debug ("falling back to autoaudiosink");
			mp->priv->audio_sink = gst_element_factory_make ("autoaudiosink", NULL);
		} else {
			rb_debug ("using gconfaudiosink");
		}

		if (mp->priv->audio_sink != NULL) {
			/* set the profile property on the gconfaudiosink to "music and movies" */
			if (g_object_class_find_property (G_OBJECT_GET_CLASS (mp->priv->audio_sink), "profile")) {
				rb_debug ("setting profile property on audio sink");
				g_object_set (mp->priv->audio_sink, "profile", 1, NULL);
			}

			g_object_set (mp->priv->playbin, "audio-sink", mp->priv->audio_sink, NULL);
		}
	} else {
		rb_debug ("existing audio sink found");
		g_object_unref (mp->priv->audio_sink);
	}

	/* Use fakesink for video if there's no video sink yet */
	g_object_get (mp->priv->playbin, "video-sink", &sink, NULL);
	if (sink == NULL) {
		sink = gst_element_factory_make ("fakesink", NULL);
		g_object_set (mp->priv->playbin, "video-sink", sink, NULL);
	} else {
		g_object_unref (sink);
	}

	if (mp->priv->cur_volume > 1.0)
		mp->priv->cur_volume = 1.0;
	if (mp->priv->cur_volume < 0.0)
		mp->priv->cur_volume = 0;
	mp->priv->replaygain_scale = 1.0f;

	rb_player_set_volume (RB_PLAYER (mp), mp->priv->cur_volume);

	rb_debug ("pipeline construction complete");
	return TRUE;
}

static gboolean
message_from_sink (GstElement *sink, GstMessage *message)
{
	GstElement *src;
	GstElement *match;
	char *name;

	src = GST_ELEMENT (GST_MESSAGE_SRC (message));

	if (GST_IS_BIN (sink) == FALSE) {
		return (src == sink);
	}

	name = gst_element_get_name (src);
	match = gst_bin_get_by_name (GST_BIN (sink), name);
	g_free (name);

	if (match != NULL) {
		g_object_unref (match);
		return (match == src);
	}

	return FALSE;
}

static gboolean
set_state_and_wait (RBPlayerGst *player, GstState target, GError **error)
{
	GstBus *bus;
	gboolean waiting;
	gboolean result;

	g_assert (player->priv->playbin != NULL);

	rb_debug ("setting playbin state to %s", gst_element_state_get_name (target));

	switch (gst_element_set_state (player->priv->playbin, target)) {
	case GST_STATE_CHANGE_SUCCESS:
		rb_debug ("state change was successful");
		return TRUE;

	case GST_STATE_CHANGE_NO_PREROLL:
		rb_debug ("state change was successful (no preroll)");
		return TRUE;

	case GST_STATE_CHANGE_ASYNC:
		rb_debug ("state is changing asynchronously");
		result = TRUE;
		break;

	case GST_STATE_CHANGE_FAILURE:
		rb_debug ("state change failed");
		result = FALSE;
		break;

	default:
		rb_debug ("unknown state change return..");
		result = TRUE;
		break;
	}

	bus = gst_element_get_bus (player->priv->playbin);
	waiting = TRUE;
	while (waiting) {
		GstMessage *message;

		message = gst_bus_timed_pop (bus, GST_SECOND * STATE_CHANGE_MESSAGE_TIMEOUT);
		if (message == NULL) {
			rb_debug ("state change is taking too long..");
			break;
		}

		switch (GST_MESSAGE_TYPE (message)) {
		case GST_MESSAGE_ERROR:
			{
				char *debug;
				GError *gst_error = NULL;

				gst_message_parse_error (message, &gst_error, &debug);

				if (message_from_sink (player->priv->audio_sink, message)) {
					rb_debug ("got error from sink: %s (%s)", gst_error->message, debug);
					g_set_error (error,
						     RB_PLAYER_ERROR,
						     RB_PLAYER_ERROR_INTERNAL,
						     _("Failed to open output device: %s"),
						     gst_error->message);
				} else {
					rb_debug ("got error from stream: %s (%s)", gst_error->message, debug);
					g_set_error (error,
						     RB_PLAYER_ERROR,
						     RB_PLAYER_ERROR_GENERAL,
						     "%s",
						     gst_error->message);
				}

				g_error_free (gst_error);
				g_free (debug);

				waiting = FALSE;
				result = FALSE;
				break;
			}

		case GST_MESSAGE_STATE_CHANGED:
			{
				GstState oldstate;
				GstState newstate;
				GstState pending;
				gst_message_parse_state_changed (message, &oldstate, &newstate, &pending);
				if (GST_MESSAGE_SRC (message) == GST_OBJECT (player->priv->playbin)) {
					rb_debug ("playbin reached state %s", gst_element_state_get_name (newstate));
					if (pending == GST_STATE_VOID_PENDING && newstate == target) {
						waiting = FALSE;
					}
				}
				break;
			}

		default:
			/* pass back to regular message handler */
			bus_cb (bus, message, player);
			break;
		}
	}

	if (result == FALSE && *error == NULL) {
		g_set_error (error,
			     RB_PLAYER_ERROR,
			     RB_PLAYER_ERROR_GENERAL,
			     _("Unable to start playback pipeline"));
	}

	return result;
}

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
impl_close (RBPlayer *player, const char *uri, GError **error)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);

	mp->priv->playing = FALSE;
	mp->priv->buffering = FALSE;

	if ((uri != NULL) && (mp->priv->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;

	mp->priv->replaygain_scale = 1.0f;

	if (mp->priv->tick_timeout_id != 0) {
		g_source_remove (mp->priv->tick_timeout_id);
		mp->priv->tick_timeout_id = 0;
	}

	if (mp->priv->playbin == NULL)
		return TRUE;

	return set_state_and_wait (mp, GST_STATE_READY, error);
}

static gboolean
impl_open (RBPlayer *player,
	   const char *uri,
	   gpointer stream_data,
	   GDestroyNotify stream_data_destroy,
	   GError **error)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);

	if (mp->priv->playbin == NULL) {
		if (!construct_pipeline (mp, error))
			return FALSE;
	} else {
		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;
		mp->priv->buffering = FALSE;
		return TRUE;
	}

	_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;

	g_object_set (mp->priv->playbin, "uri", uri, NULL);

	if (set_state_and_wait (mp, GST_STATE_PLAYING, error) == FALSE) {
		impl_close (player, uri, NULL);
		return FALSE;
	}

	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);

	return TRUE;
}

static gboolean
impl_opened (RBPlayer *player)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);

	return mp->priv->uri != NULL;
}

static gboolean
impl_play (RBPlayer *player, gint crossfade, GError **error)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);
	gboolean result;

	mp->priv->playing = TRUE;
	mp->priv->buffering = FALSE;

	g_return_val_if_fail (mp->priv->playbin != NULL, FALSE);

	result = set_state_and_wait (mp, GST_STATE_PLAYING, error);

	_rb_player_emit_playing_stream (RB_PLAYER (mp), mp->priv->stream_data);

	if (result == TRUE && mp->priv->tick_timeout_id == 0)
		mp->priv->tick_timeout_id = g_timeout_add (1000 / RB_PLAYER_GST_TICK_HZ, (GSourceFunc) tick_timeout, mp);

	return result;
}

static void
impl_pause (RBPlayer *player)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);
	GError *error = NULL;

	if (!mp->priv->playing)
		return;

	mp->priv->playing = FALSE;

	g_return_if_fail (mp->priv->playbin != NULL);

	if (set_state_and_wait (mp, GST_STATE_PAUSED, &error) == FALSE) {
		g_warning ("unable to pause playback: %s\n", error->message);
		g_error_free (error);
	}

	if (mp->priv->tick_timeout_id != 0) {
		g_source_remove (mp->priv->tick_timeout_id);
		mp->priv->tick_timeout_id = 0;
	}
}

static gboolean
impl_playing (RBPlayer *player)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);

	return mp->priv->playing;
}

static void
impl_set_replaygain (RBPlayer *player,
		     const char *uri,
		     double track_gain,
		     double track_peak,
		     double album_gain,
		     double album_peak)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);
	double scale;
	double gain = 0;
	double peak = 0;

	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;

	rb_debug ("Scale : %f New volume : %f", scale, mp->priv->cur_volume * scale);
	mp->priv->replaygain_scale = scale;

	if (mp->priv->playbin != NULL) {
		g_object_set (mp->priv->playbin, "volume", mp->priv->cur_volume * scale, NULL);
	}
}

static void
impl_set_volume (RBPlayer *player,
		 float volume)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);
	g_return_if_fail (volume >= 0.0 && volume <= 1.0);

	if (mp->priv->playbin != NULL) {
		g_object_set (mp->priv->playbin, "volume", volume * mp->priv->replaygain_scale, NULL);
	}

	mp->priv->cur_volume = volume;
}

static float
impl_get_volume (RBPlayer *player)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);

	return mp->priv->cur_volume;
}

static gboolean
impl_seekable (RBPlayer *player)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);
	gboolean can_seek = TRUE;
	GstQuery *query;

	if (mp->priv->playbin == NULL)
		return FALSE;

	query = gst_query_new_seeking (GST_FORMAT_TIME);
	if (gst_element_query (mp->priv->playbin, 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 (mp->priv->playbin, query);
	}
	gst_query_unref (query);

	return can_seek;
}

static void
impl_set_time (RBPlayer *player, long time)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);
	GError *error = NULL;

	g_return_if_fail (time >= 0);

	g_return_if_fail (mp->priv->playbin != NULL);

	if (set_state_and_wait (mp, GST_STATE_PAUSED, &error) == FALSE) {
		g_warning ("got error while pausing the pipelink for seeking: %s\n", error->message);
		g_clear_error (&error);

		/* keep going anyway? */
	}

	gst_element_seek (mp->priv->playbin, 1.0,
			  GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
			  GST_SEEK_TYPE_SET, time * GST_SECOND,
			  GST_SEEK_TYPE_NONE, -1);

	if (mp->priv->playing) {
		if (set_state_and_wait (mp, GST_STATE_PLAYING, &error) == FALSE) {
			g_warning ("unable to resume playback after seeking: %s\n", error->message);
			g_clear_error (&error);
		}
	}
}

static long
impl_get_time (RBPlayer *player)
{
	RBPlayerGst *mp = RB_PLAYER_GST (player);

	if (mp->priv->playbin != NULL) {
		gint64 position = -1;
		GstFormat fmt = GST_FORMAT_TIME;

		gst_element_query_position (mp->priv->playbin, &fmt, &position);
		if (position != -1)
			position /= GST_SECOND;

		return (long) position;
	} else {
		return -1;
	}
}



RBPlayer *
rb_player_gst_new (GError **error)
{
	return RB_PLAYER (g_object_new (RB_TYPE_PLAYER_GST, NULL, NULL));
}


static void
rb_player_gst_init (RBPlayerGst *mp)
{
	mp->priv = (G_TYPE_INSTANCE_GET_PRIVATE ((mp),
		    RB_TYPE_PLAYER_GST,
		    RBPlayerGstPrivate));
}

static void
impl_get_property (GObject *object,
		   guint prop_id,
		   GValue *value,
		   GParamSpec *pspec)
{
	RBPlayerGst *mp = RB_PLAYER_GST (object);

	switch (prop_id) {
	case PROP_PLAYBIN:
		g_value_set_object (value, mp->priv->playbin);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
impl_dispose (GObject *object)
{
	RBPlayerGst *mp;

	mp = RB_PLAYER_GST (object);

	if (mp->priv->tick_timeout_id != 0) {
		g_source_remove (mp->priv->tick_timeout_id);
		mp->priv->tick_timeout_id = 0;
	}

	if (mp->priv->playbin != NULL) {
		gst_element_set_state (mp->priv->playbin, GST_STATE_NULL);
		g_object_unref (mp->priv->playbin);
		mp->priv->playbin = NULL;
		mp->priv->audio_sink = NULL;
	}

	G_OBJECT_CLASS (rb_player_gst_parent_class)->dispose (object);
}

static void
rb_player_init (RBPlayerIface *iface)
{
	iface->open = impl_open;
	iface->opened = impl_opened;
	iface->close = impl_close;
	iface->play = impl_play;
	iface->pause = impl_pause;
	iface->playing = impl_playing;
	iface->set_volume = impl_set_volume;
	iface->get_volume = impl_get_volume;
	iface->set_replaygain = impl_set_replaygain;
	iface->seekable = impl_seekable;
	iface->set_time = impl_set_time;
	iface->get_time = impl_get_time;
	iface->multiple_open = (RBPlayerFeatureFunc) rb_false_function;
}

static void
rb_player_gst_class_init (RBPlayerGstClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->dispose = impl_dispose;
	object_class->get_property = impl_get_property;

	g_object_class_install_property (object_class,
					 PROP_PLAYBIN,
					 g_param_spec_object ("playbin",
						 	      "playbin",
							      "playbin element",
							      GST_TYPE_ELEMENT,
							      G_PARAM_READABLE));
	
	signals[MISSING_PLUGINS] =
		g_signal_new ("missing-plugins",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_LAST,
			      0,	/* no point handling this internally */
			      NULL, NULL,
			      rb_marshal_VOID__POINTER_POINTER_POINTER,
			      G_TYPE_NONE,
			      3,
			      G_TYPE_POINTER, G_TYPE_STRV, G_TYPE_STRV);

	g_type_class_add_private (klass, sizeof (RBPlayerGstPrivate));
}

