/**
 * @file backend-fs2.c Farstream backend for media API
 * @ingroup core
 */

/* purple
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * 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 Street, Fifth Floor, Boston, MA  02111-1301  USA
 */

#include "internal.h"

#include "backend-fs2.h"

#ifdef USE_VV
#include "backend-iface.h"
#include "debug.h"
#include "network.h"
#include "media-gst.h"

#ifdef HAVE_FARSIGHT
#include <gst/farsight/fs-conference-iface.h>
#include <gst/farsight/fs-element-added-notifier.h>
#else
#include <farstream/fs-conference.h>
#include <farstream/fs-element-added-notifier.h>
#include <farstream/fs-utils.h>
#include <gst/gststructure.h>
#endif

#if !GST_CHECK_VERSION(1,0,0)
#define gst_registry_get() gst_registry_get_default()
#endif

/** @copydoc _PurpleMediaBackendFs2Class */
typedef struct _PurpleMediaBackendFs2Class PurpleMediaBackendFs2Class;
/** @copydoc _PurpleMediaBackendFs2Private */
typedef struct _PurpleMediaBackendFs2Private PurpleMediaBackendFs2Private;
/** @copydoc _PurpleMediaBackendFs2Session */
typedef struct _PurpleMediaBackendFs2Session PurpleMediaBackendFs2Session;
/** @copydoc _PurpleMediaBackendFs2Stream */
typedef struct _PurpleMediaBackendFs2Stream PurpleMediaBackendFs2Stream;

#define PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(obj) \
		(G_TYPE_INSTANCE_GET_PRIVATE((obj), \
		PURPLE_TYPE_MEDIA_BACKEND_FS2, PurpleMediaBackendFs2Private))

static void purple_media_backend_iface_init(PurpleMediaBackendIface *iface);

static gboolean
gst_bus_cb(GstBus *bus, GstMessage *msg, PurpleMediaBackendFs2 *self);
static void
state_changed_cb(PurpleMedia *media, PurpleMediaState state,
		gchar *sid, gchar *name, PurpleMediaBackendFs2 *self);
static void
stream_info_cb(PurpleMedia *media, PurpleMediaInfoType type,
		gchar *sid, gchar *name, gboolean local,
		PurpleMediaBackendFs2 *self);

static gboolean purple_media_backend_fs2_add_stream(PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *who,
		PurpleMediaSessionType type, gboolean initiator,
		const gchar *transmitter,
		guint num_params, GParameter *params);
static void purple_media_backend_fs2_add_remote_candidates(
		PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant,
		GList *remote_candidates);
static gboolean purple_media_backend_fs2_codecs_ready(PurpleMediaBackend *self,
		const gchar *sess_id);
static GList *purple_media_backend_fs2_get_codecs(PurpleMediaBackend *self,
		const gchar *sess_id);
static GList *purple_media_backend_fs2_get_local_candidates(
		PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant);
#if GST_CHECK_VERSION(1,0,0)
static gboolean purple_media_backend_fs2_set_encryption_parameters (
	PurpleMediaBackend *self, const gchar *sess_id, const gchar *cipher,
	const gchar *auth, const gchar *key, gsize key_len);
static gboolean purple_media_backend_fs2_set_decryption_parameters(
		PurpleMediaBackend *self, const gchar *sess_id,
		const gchar *participant, const gchar *cipher,
		const gchar *auth, const gchar *key, gsize key_len);
static gboolean purple_media_backend_fs2_set_require_encryption(
		PurpleMediaBackend *self, const gchar *sess_id,
		const gchar *participant, gboolean require_encryption);
#endif
static gboolean purple_media_backend_fs2_set_remote_codecs(
		PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant,
		GList *codecs);
static gboolean purple_media_backend_fs2_set_send_codec(
		PurpleMediaBackend *self, const gchar *sess_id,
		PurpleMediaCodec *codec);
static void purple_media_backend_fs2_set_params(PurpleMediaBackend *self,
		guint num_params, GParameter *params);
static const gchar **purple_media_backend_fs2_get_available_params(void);
static gboolean purple_media_backend_fs2_send_dtmf(
		PurpleMediaBackend *self, const gchar *sess_id,
		gchar dtmf, guint8 volume, guint16 duration);
static gboolean purple_media_backend_fs2_set_send_rtcp_mux(
		PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant,
		gboolean send_rtcp_mux);

static void remove_element(GstElement *element);

static void free_stream(PurpleMediaBackendFs2Stream *stream);
static void free_session(PurpleMediaBackendFs2Session *session);

struct _PurpleMediaBackendFs2Class
{
	GObjectClass parent_class;
};

struct _PurpleMediaBackendFs2
{
	GObject parent;
};

G_DEFINE_TYPE_WITH_CODE(PurpleMediaBackendFs2, purple_media_backend_fs2,
		G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE(
		PURPLE_TYPE_MEDIA_BACKEND, purple_media_backend_iface_init));

struct _PurpleMediaBackendFs2Stream
{
	PurpleMediaBackendFs2Session *session;
	gchar *participant;
	FsStream *stream;

#ifndef HAVE_FARSIGHT
	gboolean supports_add;
#endif

	GstElement *src;
	GstElement *tee;
	GstElement *volume;
	GstElement *level;
	GstElement *fakesink;
	GstElement *queue;

	GList *local_candidates;
	GList *remote_candidates;

	guint connected_cb_id;
};

struct _PurpleMediaBackendFs2Session
{
	PurpleMediaBackendFs2 *backend;
	gchar *id;
	FsSession *session;

	GstElement *src;
	GstElement *tee;
	GstElement *srcvalve;

	GstPad *srcpad;

	PurpleMediaSessionType type;
};

struct _PurpleMediaBackendFs2Private
{
	PurpleMedia *media;
	GstElement *confbin;
	FsConference *conference;
	gchar *conference_type;

#ifndef HAVE_FARSIGHT
	FsElementAddedNotifier *notifier;
#endif

	GHashTable *sessions;
	GHashTable *participants;

	GList *streams;

	gdouble silence_threshold;
};

enum {
	PROP_0,
	PROP_CONFERENCE_TYPE,
	PROP_MEDIA,
};

static void
purple_media_backend_fs2_init(PurpleMediaBackendFs2 *self)
{}

static FsCandidateType
purple_media_candidate_type_to_fs(PurpleMediaCandidateType type)
{
	switch (type) {
		case PURPLE_MEDIA_CANDIDATE_TYPE_HOST:
			return FS_CANDIDATE_TYPE_HOST;
		case PURPLE_MEDIA_CANDIDATE_TYPE_SRFLX:
			return FS_CANDIDATE_TYPE_SRFLX;
		case PURPLE_MEDIA_CANDIDATE_TYPE_PRFLX:
			return FS_CANDIDATE_TYPE_PRFLX;
		case PURPLE_MEDIA_CANDIDATE_TYPE_RELAY:
			return FS_CANDIDATE_TYPE_RELAY;
		case PURPLE_MEDIA_CANDIDATE_TYPE_MULTICAST:
			return FS_CANDIDATE_TYPE_MULTICAST;
	}
	g_return_val_if_reached(FS_CANDIDATE_TYPE_HOST);
}

static PurpleMediaCandidateType
purple_media_candidate_type_from_fs(FsCandidateType type)
{
	switch (type) {
		case FS_CANDIDATE_TYPE_HOST:
			return PURPLE_MEDIA_CANDIDATE_TYPE_HOST;
		case FS_CANDIDATE_TYPE_SRFLX:
			return PURPLE_MEDIA_CANDIDATE_TYPE_SRFLX;
		case FS_CANDIDATE_TYPE_PRFLX:
			return PURPLE_MEDIA_CANDIDATE_TYPE_PRFLX;
		case FS_CANDIDATE_TYPE_RELAY:
			return PURPLE_MEDIA_CANDIDATE_TYPE_RELAY;
		case FS_CANDIDATE_TYPE_MULTICAST:
			return PURPLE_MEDIA_CANDIDATE_TYPE_MULTICAST;
	}
	g_return_val_if_reached(PURPLE_MEDIA_CANDIDATE_TYPE_HOST);
}

static FsNetworkProtocol
purple_media_network_protocol_to_fs(PurpleMediaNetworkProtocol protocol)
{
	switch (protocol) {
		case PURPLE_MEDIA_NETWORK_PROTOCOL_UDP:
			return FS_NETWORK_PROTOCOL_UDP;
#if GST_CHECK_VERSION(1,0,0)
		case PURPLE_MEDIA_NETWORK_PROTOCOL_TCP_PASSIVE:
			return FS_NETWORK_PROTOCOL_TCP_PASSIVE;
		case PURPLE_MEDIA_NETWORK_PROTOCOL_TCP_ACTIVE:
			return FS_NETWORK_PROTOCOL_TCP_ACTIVE;
		case PURPLE_MEDIA_NETWORK_PROTOCOL_TCP_SO:
			return FS_NETWORK_PROTOCOL_TCP_SO;
#endif
		default:
			g_return_val_if_reached(FS_NETWORK_PROTOCOL_TCP);
	}
}

static PurpleMediaNetworkProtocol
purple_media_network_protocol_from_fs(FsNetworkProtocol protocol)
{
	switch (protocol) {
		case FS_NETWORK_PROTOCOL_UDP:
			return PURPLE_MEDIA_NETWORK_PROTOCOL_UDP;
#if GST_CHECK_VERSION(1,0,0)
		case FS_NETWORK_PROTOCOL_TCP_PASSIVE:
			return PURPLE_MEDIA_NETWORK_PROTOCOL_TCP_PASSIVE;
		case FS_NETWORK_PROTOCOL_TCP_ACTIVE:
			return PURPLE_MEDIA_NETWORK_PROTOCOL_TCP_ACTIVE;
		case FS_NETWORK_PROTOCOL_TCP_SO:
			return PURPLE_MEDIA_NETWORK_PROTOCOL_TCP_SO;
#endif
		default:
			g_return_val_if_reached(PURPLE_MEDIA_NETWORK_PROTOCOL_TCP_PASSIVE);
	}
}

#if GST_CHECK_VERSION(1,0,0)
static GstPadProbeReturn
event_probe_cb(GstPad *srcpad, GstPadProbeInfo *info, gpointer unused)
#else
static gboolean
event_probe_cb(GstPad *srcpad, GstEvent *event, gboolean release_pad)
#endif
{
#if GST_CHECK_VERSION(1,0,0)
	GstEvent *event = GST_PAD_PROBE_INFO_EVENT(info);
#endif
	if (GST_EVENT_TYPE(event) == GST_EVENT_CUSTOM_DOWNSTREAM
		&& gst_event_has_name(event, "purple-unlink-tee")) {

		const GstStructure *s = gst_event_get_structure(event);

		gst_pad_unlink(srcpad, gst_pad_get_peer(srcpad));

#if GST_CHECK_VERSION(1,0,0)
		gst_pad_remove_probe(srcpad,
			g_value_get_ulong(gst_structure_get_value(s, "handler-id")));
#else
		gst_pad_remove_event_probe(srcpad,
			g_value_get_uint(gst_structure_get_value(s, "handler-id")));
#endif

		if (g_value_get_boolean(gst_structure_get_value(s, "release-pad")))
			gst_element_release_request_pad(GST_ELEMENT_PARENT(srcpad), srcpad);

#if GST_CHECK_VERSION(1,0,0)
		return GST_PAD_PROBE_DROP;
#else
		return FALSE;
#endif
	}

#if GST_CHECK_VERSION(1,0,0)
	return GST_PAD_PROBE_OK;
#else
	return TRUE;
#endif
}

static void
unlink_teepad_dynamic(GstPad *srcpad, gboolean release_pad)
{
#if GST_CHECK_VERSION(1,0,0)
	gulong id = gst_pad_add_probe(srcpad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM,
	                              event_probe_cb, NULL, NULL);
#else
	guint id = gst_pad_add_event_probe(srcpad, G_CALLBACK(event_probe_cb), NULL);
#endif

	if (GST_IS_GHOST_PAD(srcpad))
		srcpad = gst_ghost_pad_get_target(GST_GHOST_PAD(srcpad));

	gst_element_send_event(gst_pad_get_parent_element(srcpad),
		gst_event_new_custom(GST_EVENT_CUSTOM_DOWNSTREAM,
			gst_structure_new("purple-unlink-tee",
				"release-pad", G_TYPE_BOOLEAN, release_pad,
#if GST_CHECK_VERSION(1,0,0)
				"handler-id", G_TYPE_ULONG, id,
#else
				"handler-id", G_TYPE_UINT, id,
#endif
				NULL)));
}

static void
purple_media_backend_fs2_dispose(GObject *obj)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(obj);
	GList *iter = NULL;

	purple_debug_info("backend-fs2", "purple_media_backend_fs2_dispose\n");

#ifndef HAVE_FARSIGHT
	if (priv->notifier) {
		g_object_unref(priv->notifier);
		priv->notifier = NULL;
	}
#endif

	if (priv->confbin) {
		GstElement *pipeline;

		pipeline = purple_media_manager_get_pipeline(
				purple_media_get_manager(priv->media));

		/* All connections to media sources should be blocked before confbin is
		 * removed, to prevent freezing of any other simultaneously running
		 * media calls. */
		if (priv->sessions) {
			GList *sessions = g_hash_table_get_values(priv->sessions);
			for (; sessions; sessions =
					g_list_delete_link(sessions, sessions)) {
				PurpleMediaBackendFs2Session *session = sessions->data;
				if (session->srcpad) {
					unlink_teepad_dynamic(session->srcpad, FALSE);
					gst_object_unref(session->srcpad);
					session->srcpad = NULL;
				}
			}
		}

		gst_element_set_locked_state(priv->confbin, TRUE);
		gst_element_set_state(GST_ELEMENT(priv->confbin),
				GST_STATE_NULL);

		if (pipeline) {
			GstBus *bus;
			gst_bin_remove(GST_BIN(pipeline), priv->confbin);
			bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
			g_signal_handlers_disconnect_matched(G_OBJECT(bus),
					G_SIGNAL_MATCH_FUNC |
					G_SIGNAL_MATCH_DATA,
					0, 0, 0, gst_bus_cb, obj);
			gst_object_unref(bus);
		} else {
			purple_debug_warning("backend-fs2", "Unable to "
					"properly dispose the conference. "
					"Couldn't get the pipeline.\n");
		}

		priv->confbin = NULL;
		priv->conference = NULL;

	}

	if (priv->sessions) {
		GList *sessions = g_hash_table_get_values(priv->sessions);

		for (; sessions; sessions =
				g_list_delete_link(sessions, sessions)) {
			PurpleMediaBackendFs2Session *session =
					sessions->data;

			if (session->session) {
				g_object_unref(session->session);
				session->session = NULL;
			}
		}
	}

	if (priv->participants) {
		g_hash_table_destroy(priv->participants);
		priv->participants = NULL;
	}

	for (iter = priv->streams; iter; iter = g_list_next(iter)) {
		PurpleMediaBackendFs2Stream *stream = iter->data;
		if (stream->stream) {
			g_object_unref(stream->stream);
			stream->stream = NULL;
		}
	}

	if (priv->media) {
		g_object_remove_weak_pointer(G_OBJECT(priv->media),
				(gpointer*)&priv->media);
		priv->media = NULL;
	}

	G_OBJECT_CLASS(purple_media_backend_fs2_parent_class)->dispose(obj);
}

static void
purple_media_backend_fs2_finalize(GObject *obj)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(obj);

	purple_debug_info("backend-fs2", "purple_media_backend_fs2_finalize\n");

	g_free(priv->conference_type);

	for (; priv->streams; priv->streams =
			g_list_delete_link(priv->streams, priv->streams)) {
		PurpleMediaBackendFs2Stream *stream = priv->streams->data;
		free_stream(stream);
	}

	if (priv->sessions) {
		GList *sessions = g_hash_table_get_values(priv->sessions);

		for (; sessions; sessions =
				g_list_delete_link(sessions, sessions)) {
			PurpleMediaBackendFs2Session *session =
					sessions->data;
			free_session(session);
		}

		g_hash_table_destroy(priv->sessions);
	}

	G_OBJECT_CLASS(purple_media_backend_fs2_parent_class)->finalize(obj);
}

static void
purple_media_backend_fs2_set_property(GObject *object, guint prop_id,
		const GValue *value, GParamSpec *pspec)
{
	PurpleMediaBackendFs2Private *priv;
	g_return_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(object));

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(object);

	switch (prop_id) {
		case PROP_CONFERENCE_TYPE:
			priv->conference_type = g_value_dup_string(value);
			break;
		case PROP_MEDIA:
			priv->media = g_value_get_object(value);

			if (priv->media == NULL)
				break;

			g_object_add_weak_pointer(G_OBJECT(priv->media),
					(gpointer*)&priv->media);

			g_signal_connect(G_OBJECT(priv->media),
					"state-changed",
					G_CALLBACK(state_changed_cb),
					PURPLE_MEDIA_BACKEND_FS2(object));
			g_signal_connect(G_OBJECT(priv->media), "stream-info",
					G_CALLBACK(stream_info_cb),
					PURPLE_MEDIA_BACKEND_FS2(object));
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(
					object, prop_id, pspec);
			break;
	}
}

static void
purple_media_backend_fs2_get_property(GObject *object, guint prop_id,
		GValue *value, GParamSpec *pspec)
{
	PurpleMediaBackendFs2Private *priv;
	g_return_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(object));

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(object);

	switch (prop_id) {
		case PROP_CONFERENCE_TYPE:
			g_value_set_string(value, priv->conference_type);
			break;
		case PROP_MEDIA:
			g_value_set_object(value, priv->media);
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(
					object, prop_id, pspec);
			break;
	}
}

static void
purple_media_backend_fs2_class_init(PurpleMediaBackendFs2Class *klass)
{
	GObjectClass *gobject_class = (GObjectClass*)klass;
	GList *features;
	GList *it;

	gobject_class->dispose = purple_media_backend_fs2_dispose;
	gobject_class->finalize = purple_media_backend_fs2_finalize;
	gobject_class->set_property = purple_media_backend_fs2_set_property;
	gobject_class->get_property = purple_media_backend_fs2_get_property;

	g_object_class_override_property(gobject_class, PROP_CONFERENCE_TYPE,
			"conference-type");
	g_object_class_override_property(gobject_class, PROP_MEDIA, "media");

	g_type_class_add_private(klass, sizeof(PurpleMediaBackendFs2Private));

	/* VA-API elements aren't well supported in Farstream. Ignore them. */
	features = gst_registry_get_feature_list_by_plugin(gst_registry_get(),
			"vaapi");
	for (it = features; it; it = it->next) {
		gst_plugin_feature_set_rank((GstPluginFeature *)it->data,
				GST_RANK_NONE);
	}
	gst_plugin_feature_list_free(features);
}

static void
purple_media_backend_iface_init(PurpleMediaBackendIface *iface)
{
	iface->add_stream = purple_media_backend_fs2_add_stream;
	iface->add_remote_candidates =
			purple_media_backend_fs2_add_remote_candidates;
	iface->codecs_ready = purple_media_backend_fs2_codecs_ready;
	iface->get_codecs = purple_media_backend_fs2_get_codecs;
	iface->get_local_candidates =
			purple_media_backend_fs2_get_local_candidates;
	iface->set_remote_codecs = purple_media_backend_fs2_set_remote_codecs;
	iface->set_send_codec = purple_media_backend_fs2_set_send_codec;
#if GST_CHECK_VERSION(1,0,0)
	iface->set_encryption_parameters =
			purple_media_backend_fs2_set_encryption_parameters;
	iface->set_decryption_parameters =
			purple_media_backend_fs2_set_decryption_parameters;
	iface->set_require_encryption =
			purple_media_backend_fs2_set_require_encryption;
#endif
	iface->set_params = purple_media_backend_fs2_set_params;
	iface->get_available_params = purple_media_backend_fs2_get_available_params;
	iface->send_dtmf = purple_media_backend_fs2_send_dtmf;
	iface->set_send_rtcp_mux = purple_media_backend_fs2_set_send_rtcp_mux;
}

static FsMediaType
session_type_to_fs_media_type(PurpleMediaSessionType type)
{
	if (type & PURPLE_MEDIA_AUDIO)
		return FS_MEDIA_TYPE_AUDIO;
	else if (type & PURPLE_MEDIA_VIDEO)
		return FS_MEDIA_TYPE_VIDEO;
#ifdef HAVE_MEDIA_APPLICATION
	else if (type & PURPLE_MEDIA_APPLICATION)
		return FS_MEDIA_TYPE_APPLICATION;
#endif
	else
		return 0;
}

static FsStreamDirection
session_type_to_fs_stream_direction(PurpleMediaSessionType type)
{
	if ((type & PURPLE_MEDIA_AUDIO) == PURPLE_MEDIA_AUDIO ||
		(type & PURPLE_MEDIA_VIDEO) == PURPLE_MEDIA_VIDEO)
		return FS_DIRECTION_BOTH;
	else if ((type & PURPLE_MEDIA_SEND_AUDIO) ||
			(type & PURPLE_MEDIA_SEND_VIDEO))
		return FS_DIRECTION_SEND;
	else if ((type & PURPLE_MEDIA_RECV_AUDIO) ||
			(type & PURPLE_MEDIA_RECV_VIDEO))
		return FS_DIRECTION_RECV;
#ifdef HAVE_MEDIA_APPLICATION
	else if ((type & PURPLE_MEDIA_APPLICATION) == PURPLE_MEDIA_APPLICATION)
		return FS_DIRECTION_BOTH;
	else if (type & PURPLE_MEDIA_SEND_APPLICATION)
		return FS_DIRECTION_SEND;
	else if (type & PURPLE_MEDIA_RECV_APPLICATION)
		return FS_DIRECTION_RECV;
#endif
	else
		return FS_DIRECTION_NONE;
}

static PurpleMediaSessionType
session_type_from_fs(FsMediaType type, FsStreamDirection direction)
{
	PurpleMediaSessionType result = PURPLE_MEDIA_NONE;
	if (type == FS_MEDIA_TYPE_AUDIO) {
		if (direction & FS_DIRECTION_SEND)
			result |= PURPLE_MEDIA_SEND_AUDIO;
		if (direction & FS_DIRECTION_RECV)
			result |= PURPLE_MEDIA_RECV_AUDIO;
	} else if (type == FS_MEDIA_TYPE_VIDEO) {
		if (direction & FS_DIRECTION_SEND)
			result |= PURPLE_MEDIA_SEND_VIDEO;
		if (direction & FS_DIRECTION_RECV)
			result |= PURPLE_MEDIA_RECV_VIDEO;
#ifdef HAVE_MEDIA_APPLICATION
	} else if (type == FS_MEDIA_TYPE_APPLICATION) {
		if (direction & FS_DIRECTION_SEND)
			result |= PURPLE_MEDIA_SEND_APPLICATION;
		if (direction & FS_DIRECTION_RECV)
			result |= PURPLE_MEDIA_RECV_APPLICATION;
#endif
	}
	return result;
}

static FsCandidate *
candidate_to_fs(PurpleMediaCandidate *candidate)
{
	FsCandidate *fscandidate;
	gchar *foundation;
	guint component_id;
	gchar *ip;
	guint port;
	gchar *base_ip;
	guint base_port;
	PurpleMediaNetworkProtocol proto;
	guint32 priority;
	PurpleMediaCandidateType type;
	gchar *username;
	gchar *password;
	guint ttl;

	if (candidate == NULL)
		return NULL;

	g_object_get(G_OBJECT(candidate),
			"foundation", &foundation,
			"component-id", &component_id,
			"ip", &ip,
			"port", &port,
			"base-ip", &base_ip,
			"base-port", &base_port,
			"protocol", &proto,
			"priority", &priority,
			"type", &type,
			"username", &username,
			"password", &password,
			"ttl", &ttl,
			NULL);

	fscandidate = fs_candidate_new(foundation,
			component_id, purple_media_candidate_type_to_fs(type),
			purple_media_network_protocol_to_fs(proto), ip, port);

	fscandidate->base_ip = base_ip;
	fscandidate->base_port = base_port;
	fscandidate->priority = priority;
	fscandidate->username = username;
	fscandidate->password = password;
	fscandidate->ttl = ttl;

	g_free(foundation);
	g_free(ip);
	return fscandidate;
}

static GList *
candidate_list_to_fs(GList *candidates)
{
	GList *new_list = NULL;

	for (; candidates; candidates = g_list_next(candidates)) {
		new_list = g_list_prepend(new_list,
				candidate_to_fs(candidates->data));
	}

	new_list = g_list_reverse(new_list);
	return new_list;
}

static PurpleMediaCandidate *
candidate_from_fs(FsCandidate *fscandidate)
{
	PurpleMediaCandidate *candidate;

	if (fscandidate == NULL)
		return NULL;

	candidate = purple_media_candidate_new(fscandidate->foundation,
		fscandidate->component_id,
		purple_media_candidate_type_from_fs(fscandidate->type),
		purple_media_network_protocol_from_fs(fscandidate->proto),
		fscandidate->ip, fscandidate->port);
	g_object_set(candidate,
			"base-ip", fscandidate->base_ip,
			"base-port", fscandidate->base_port,
			"priority", fscandidate->priority,
			"username", fscandidate->username,
			"password", fscandidate->password,
			"ttl", fscandidate->ttl, NULL);
	return candidate;
}

static GList *
candidate_list_from_fs(GList *candidates)
{
	GList *new_list = NULL;

	for (; candidates; candidates = g_list_next(candidates)) {
		new_list = g_list_prepend(new_list,
			candidate_from_fs(candidates->data));
	}

	new_list = g_list_reverse(new_list);
	return new_list;
}

static FsCodec *
codec_to_fs(const PurpleMediaCodec *codec)
{
	FsCodec *new_codec;
	gint id;
	char *encoding_name;
	PurpleMediaSessionType media_type;
	guint clock_rate;
	guint channels;
	GList *iter;

	if (codec == NULL)
		return NULL;

	g_object_get(G_OBJECT(codec),
			"id", &id,
			"encoding-name", &encoding_name,
			"media-type", &media_type,
			"clock-rate", &clock_rate,
			"channels", &channels,
			"optional-params", &iter,
			NULL);

	new_codec = fs_codec_new(id, encoding_name,
			session_type_to_fs_media_type(media_type),
			clock_rate);
	new_codec->channels = channels;

	for (; iter; iter = g_list_next(iter)) {
		PurpleKeyValuePair *param = (PurpleKeyValuePair*)iter->data;
		fs_codec_add_optional_parameter(new_codec,
				param->key, param->value);
	}

	g_free(encoding_name);
	return new_codec;
}

static PurpleMediaCodec *
codec_from_fs(const FsCodec *codec)
{
	PurpleMediaCodec *new_codec;
	GList *iter;

	if (codec == NULL)
		return NULL;

	new_codec = purple_media_codec_new(codec->id, codec->encoding_name,
			session_type_from_fs(codec->media_type,
			FS_DIRECTION_BOTH), codec->clock_rate);
	g_object_set(new_codec, "channels", codec->channels, NULL);

	for (iter = codec->optional_params; iter; iter = g_list_next(iter)) {
		FsCodecParameter *param = (FsCodecParameter*)iter->data;
		purple_media_codec_add_optional_parameter(new_codec,
				param->name, param->value);
	}

	return new_codec;
}

static GList *
codec_list_from_fs(GList *codecs)
{
	GList *new_list = NULL;

	for (; codecs; codecs = g_list_next(codecs)) {
		new_list = g_list_prepend(new_list,
				codec_from_fs(codecs->data));
	}

	new_list = g_list_reverse(new_list);
	return new_list;
}

static GList *
codec_list_to_fs(GList *codecs)
{
	GList *new_list = NULL;

	for (; codecs; codecs = g_list_next(codecs)) {
		new_list = g_list_prepend(new_list,
				codec_to_fs(codecs->data));
	}

	new_list = g_list_reverse(new_list);
	return new_list;
}

static PurpleMediaBackendFs2Session *
get_session(PurpleMediaBackendFs2 *self, const gchar *sess_id)
{
	PurpleMediaBackendFs2Private *priv;
	PurpleMediaBackendFs2Session *session = NULL;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), NULL);

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);

	if (priv->sessions != NULL)
		session = g_hash_table_lookup(priv->sessions, sess_id);

	return session;
}

static FsParticipant *
get_participant(PurpleMediaBackendFs2 *self, const gchar *name)
{
	PurpleMediaBackendFs2Private *priv;
	FsParticipant *participant = NULL;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), NULL);

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);

	if (priv->participants != NULL)
		participant = g_hash_table_lookup(priv->participants, name);

	return participant;
}

static PurpleMediaBackendFs2Stream *
get_stream(PurpleMediaBackendFs2 *self,
		const gchar *sess_id, const gchar *name)
{
	PurpleMediaBackendFs2Private *priv;
	GList *streams;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), NULL);

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	streams = priv->streams;

	for (; streams; streams = g_list_next(streams)) {
		PurpleMediaBackendFs2Stream *stream = streams->data;
		if (purple_strequal(stream->session->id, sess_id) &&
				purple_strequal(stream->participant, name))
			return stream;
	}

	return NULL;
}

static GList *
get_streams(PurpleMediaBackendFs2 *self,
		const gchar *sess_id, const gchar *name)
{
	PurpleMediaBackendFs2Private *priv;
	GList *streams, *ret = NULL;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), NULL);

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	streams = priv->streams;

	for (; streams; streams = g_list_next(streams)) {
		PurpleMediaBackendFs2Stream *stream = streams->data;

		if (sess_id != NULL && !purple_strequal(stream->session->id, sess_id))
			continue;
		else if (name != NULL && !purple_strequal(stream->participant, name))
			continue;
		else
			ret = g_list_prepend(ret, stream);
	}

	ret = g_list_reverse(ret);
	return ret;
}

static PurpleMediaBackendFs2Session *
get_session_from_fs_stream(PurpleMediaBackendFs2 *self, FsStream *stream)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	FsSession *fssession;
	GList *values;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), NULL);
	g_return_val_if_fail(FS_IS_STREAM(stream), NULL);

	g_object_get(stream, "session", &fssession, NULL);

	values = g_hash_table_get_values(priv->sessions);

	for (; values; values = g_list_delete_link(values, values)) {
		PurpleMediaBackendFs2Session *session = values->data;

		if (session->session == fssession) {
			g_list_free(values);
			g_object_unref(fssession);
			return session;
		}
	}

	g_object_unref(fssession);
	return NULL;
}

static gdouble
gst_msg_db_to_percent(GstMessage *msg, gchar *value_name)
{
	const GValue *list;
	const GValue *value;
	gdouble value_db;
	gdouble percent;

	list = gst_structure_get_value(gst_message_get_structure(msg), value_name);
#if GST_CHECK_VERSION(1,0,0)
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
	value = g_value_array_get_nth(g_value_get_boxed(list), 0);
G_GNUC_END_IGNORE_DEPRECATIONS
#else
	value = gst_value_list_get_value(list, 0);
#endif
	value_db = g_value_get_double(value);
	percent = pow(10, value_db / 20);
	return (percent > 1.0) ? 1.0 : percent;
}

static void
purple_media_error_fs(PurpleMedia *media, const gchar *error,
		const GstStructure *fs_error)
{
	const gchar *error_msg = gst_structure_get_string(fs_error, "error-msg");

	purple_media_error(media, "%s%s%s", error,
	                   error_msg ? _("\n\nMessage from Farsight: ") : "",
	                   error_msg ? error_msg : "");
}

static void
gst_handle_message_element(GstBus *bus, GstMessage *msg,
		PurpleMediaBackendFs2 *self)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	GstElement *src = GST_ELEMENT(GST_MESSAGE_SRC(msg));
	static guint level_id = 0;
	const GstStructure *structure = gst_message_get_structure(msg);

	if (level_id == 0)
		level_id = g_signal_lookup("level", PURPLE_TYPE_MEDIA);

	if (gst_structure_has_name(structure, "level")) {
		GstElement *src = GST_ELEMENT(GST_MESSAGE_SRC(msg));
		gchar *name;
		gchar *participant = NULL;
		PurpleMediaBackendFs2Session *session = NULL;
		gdouble percent;

		if (!PURPLE_IS_MEDIA(priv->media) ||
				GST_ELEMENT_PARENT(src) != priv->confbin)
			return;

		name = gst_element_get_name(src);

		if (!strncmp(name, "sendlevel_", 10)) {
			session = get_session(self, name+10);
			if (priv->silence_threshold > 0) {
				percent = gst_msg_db_to_percent(msg, "decay");
				g_object_set(session->srcvalve,
						"drop", (percent < priv->silence_threshold), NULL);
			}
		}

		g_free(name);

		if (!g_signal_has_handler_pending(priv->media, level_id, 0, FALSE))
			return;

		if (!session) {
			GList *iter = priv->streams;
			PurpleMediaBackendFs2Stream *stream;
			for (; iter; iter = g_list_next(iter)) {
				stream = iter->data;
				if (stream->level == src) {
					session = stream->session;
					participant = stream->participant;
					break;
				}
			}
		}

		if (!session)
			return;

		percent = gst_msg_db_to_percent(msg, "rms");

		g_signal_emit(priv->media, level_id, 0,
				session->id, participant, percent);
		return;
	}

	if (!FS_IS_CONFERENCE(src) || !PURPLE_IS_MEDIA_BACKEND(self) ||
			priv->conference != FS_CONFERENCE(src))
		return;

#ifdef HAVE_FARSIGHT
	if (gst_structure_has_name(structure, "farsight-error")) {
#else
	if (gst_structure_has_name(structure, "farstream-error")) {
#endif
		FsError error_no;
		gboolean error_emitted = FALSE;
		gst_structure_get_enum(structure, "error-no",
				FS_TYPE_ERROR, (gint*)&error_no);
		switch (error_no) {
			case FS_ERROR_CONSTRUCTION:
				purple_media_error_fs(priv->media,
				                      _("Error initializing the call. "
				                        "This probably denotes problem in "
#ifdef HAVE_FARSIGHT
				                        "installation of GStreamer or Farsight."),
#else
				                        "installation of GStreamer or Farstream."),
#endif
				                      structure);
				error_emitted = TRUE;
				break;
			case FS_ERROR_NETWORK:
				purple_media_error_fs(priv->media, _("Network error."),
				                      structure);
				error_emitted = TRUE;
				purple_media_end(priv->media, NULL, NULL);
				break;
			case FS_ERROR_NEGOTIATION_FAILED:
				purple_media_error_fs(priv->media,
				                      _("Codec negotiation failed. "
				                        "This problem might be resolved by installing "
				                        "more GStreamer codecs."),
				                      structure);
				error_emitted = TRUE;
				purple_media_end(priv->media, NULL, NULL);
				break;
			case FS_ERROR_NO_CODECS:
				purple_media_error(priv->media,
				                   _("No codecs found. "
				                     "Install some GStreamer codecs found "
				                     "in GStreamer plugins packages."));
				error_emitted = TRUE;
				purple_media_end(priv->media, NULL, NULL);
				break;
#ifdef HAVE_FARSIGHT
			case FS_ERROR_NO_CODECS_LEFT:
				purple_media_error(priv->media,
				                   _("No codecs left. Your codec preferences "
				                     "in fs-codecs.conf are too strict."));
				error_emitted = TRUE;
				purple_media_end(priv->media, NULL, NULL);
				break;
			case FS_ERROR_CONNECTION_FAILED:
				purple_media_error(priv->media,
				                   _("Could not connect to the remote party"));
				error_emitted = TRUE;
				break;
			case FS_ERROR_UNKNOWN_CNAME:
				/*
				 * Unknown CName is only a problem for the
				 * multicast transmitter which isn't used.
				 * It is also deprecated.
				 */
				break;
#endif
			default:
				purple_debug_error("backend-fs2",
#ifdef HAVE_FARSIGHT
						"farsight-error: %i: %s\n",
#else
						"farstream-error: %i: %s\n",
#endif
						error_no,
						gst_structure_get_string(structure, "error-msg"));
				break;
		}

		if (FS_ERROR_IS_FATAL(error_no)) {
			if (!error_emitted)
#ifdef HAVE_FARSIGHT
			purple_media_error(priv->media,
			                   _("A non-recoverable Farsight2 error has occurred."));
#else
			purple_media_error(priv->media,
			                   _("A non-recoverable Farstream error has occurred."));
#endif
			purple_media_end(priv->media, NULL, NULL);
		}
	} else if (gst_structure_has_name(structure,
#ifdef HAVE_FARSIGHT
			"farsight-new-local-candidate")) {
#else
			"farstream-new-local-candidate")) {
#endif
		const GValue *value;
		FsStream *stream;
		FsCandidate *local_candidate;
		PurpleMediaCandidate *candidate;
		FsParticipant *participant;
		PurpleMediaBackendFs2Session *session;
		PurpleMediaBackendFs2Stream *media_stream;
		const gchar *name;

		value = gst_structure_get_value(structure, "stream");
		stream = g_value_get_object(value);
		value = gst_structure_get_value(structure, "candidate");
		local_candidate = g_value_get_boxed(value);

		session = get_session_from_fs_stream(self, stream);

		purple_debug_info("backend-fs2",
				"got new local candidate: %s\n",
				local_candidate->foundation);

		g_object_get(stream, "participant", &participant, NULL);
		name = g_object_get_data(G_OBJECT(participant), "purple-name");

		media_stream = get_stream(self, session->id, name);
		media_stream->local_candidates = g_list_append(
				media_stream->local_candidates,
				fs_candidate_copy(local_candidate));

		candidate = candidate_from_fs(local_candidate);
		g_signal_emit_by_name(self, "new-candidate",
				session->id, name, candidate);
		g_object_unref(candidate);
		g_object_unref(participant);
	} else if (gst_structure_has_name(structure,
#ifdef HAVE_FARSIGHT
			"farsight-local-candidates-prepared")) {
#else
			"farstream-local-candidates-prepared")) {
#endif
		const GValue *value;
		FsStream *stream;
		FsParticipant *participant;
		PurpleMediaBackendFs2Session *session;

		value = gst_structure_get_value(structure, "stream");
		stream = g_value_get_object(value);
		session = get_session_from_fs_stream(self, stream);

		g_object_get(stream, "participant", &participant, NULL);

		g_signal_emit_by_name(self, "candidates-prepared",
				session->id,
				g_object_get_data(G_OBJECT(participant), "purple-name"));

		g_object_unref(participant);
	} else if (gst_structure_has_name(structure,
#ifdef HAVE_FARSIGHT
			"farsight-new-active-candidate-pair")) {
#else
			"farstream-new-active-candidate-pair")) {
#endif
		const GValue *value;
		FsStream *stream;
		FsCandidate *local_candidate;
		FsCandidate *remote_candidate;
		FsParticipant *participant;
		PurpleMediaBackendFs2Session *session;
		PurpleMediaCandidate *lcandidate, *rcandidate;

		value = gst_structure_get_value(structure, "stream");
		stream = g_value_get_object(value);
		value = gst_structure_get_value(structure, "local-candidate");
		local_candidate = g_value_get_boxed(value);
		value = gst_structure_get_value(structure, "remote-candidate");
		remote_candidate = g_value_get_boxed(value);

		g_object_get(stream, "participant", &participant, NULL);

		session = get_session_from_fs_stream(self, stream);

		lcandidate = candidate_from_fs(local_candidate);
		rcandidate = candidate_from_fs(remote_candidate);

		g_signal_emit_by_name(self, "active-candidate-pair",
				session->id,
				g_object_get_data(G_OBJECT(participant), "purple-name"),
				lcandidate, rcandidate);

		g_object_unref(participant);
		g_object_unref(lcandidate);
		g_object_unref(rcandidate);
	} else if (gst_structure_has_name(structure,
#ifdef HAVE_FARSIGHT
			"farsight-recv-codecs-changed")) {
#else
			"farstream-recv-codecs-changed")) {
#endif
		const GValue *value;
		GList *codecs;
		FsCodec *codec;

		value = gst_structure_get_value(structure, "codecs");
		codecs = g_value_get_boxed(value);
		codec = codecs->data;

		purple_debug_info("backend-fs2",
#ifdef HAVE_FARSIGHT
				"farsight-recv-codecs-changed: %s\n",
#else
				"farstream-recv-codecs-changed: %s\n",
#endif
				codec->encoding_name);
	} else if (gst_structure_has_name(structure,
#ifdef HAVE_FARSIGHT
			"farsight-component-state-changed")) {
#else
			"farstream-component-state-changed")) {
#endif
		const GValue *value;
		FsStreamState fsstate;
		guint component;
		const gchar *state;

		value = gst_structure_get_value(structure, "state");
		fsstate = g_value_get_enum(value);
		value = gst_structure_get_value(structure, "component");
		component = g_value_get_uint(value);

		switch (fsstate) {
			case FS_STREAM_STATE_FAILED:
				state = "FAILED";
				break;
			case FS_STREAM_STATE_DISCONNECTED:
				state = "DISCONNECTED";
				break;
			case FS_STREAM_STATE_GATHERING:
				state = "GATHERING";
				break;
			case FS_STREAM_STATE_CONNECTING:
				state = "CONNECTING";
				break;
			case FS_STREAM_STATE_CONNECTED:
				state = "CONNECTED";
				break;
			case FS_STREAM_STATE_READY:
				state = "READY";
				break;
			default:
				state = "UNKNOWN";
				break;
		}

		purple_debug_info("backend-fs2",
#ifdef HAVE_FARSIGHT
				"farsight-component-state-changed: "
#else
				"farstream-component-state-changed: "
#endif
				"component: %u state: %s\n",
				component, state);
	} else if (gst_structure_has_name(structure,
#ifdef HAVE_FARSIGHT
			"farsight-send-codec-changed")) {
#else
			"farstream-send-codec-changed")) {
#endif
		const GValue *value;
		FsCodec *codec;
		gchar *codec_str;

		value = gst_structure_get_value(structure, "codec");
		codec = g_value_get_boxed(value);
		codec_str = fs_codec_to_string(codec);

		purple_debug_info("backend-fs2",
#ifdef HAVE_FARSIGHT
				"farsight-send-codec-changed: codec: %s\n",
#else
				"farstream-send-codec-changed: codec: %s\n",
#endif
				codec_str);

		g_free(codec_str);
	} else if (gst_structure_has_name(structure,
#ifdef HAVE_FARSIGHT
			"farsight-codecs-changed")) {
#else
			"farstream-codecs-changed")) {
#endif
		const GValue *value;
		FsSession *fssession;
		GList *sessions;

		value = gst_structure_get_value(structure, "session");
		fssession = g_value_get_object(value);
		sessions = g_hash_table_get_values(priv->sessions);

		for (; sessions; sessions =
				g_list_delete_link(sessions, sessions)) {
			PurpleMediaBackendFs2Session *session = sessions->data;
			gchar *session_id;

			if (session->session != fssession)
				continue;

			session_id = g_strdup(session->id);
			g_signal_emit_by_name(self, "codecs-changed",
					session_id);
			g_free(session_id);
			g_list_free(sessions);
			break;
		}
	}
}

static gboolean
downgrade_video_source(PurpleMediaBackendFs2 *self, GstBin *srcbin)
{
	PurpleMediaBackendFs2Private *priv;
	GstElement *src;
	GstPad *srcpad = NULL;
	GstPad *p;

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);

	src = gst_bin_get_by_name(srcbin, "purplevideodowngrade");
	if (src) {
		/* The failing source has already been downgraded. Stop here in
		 * order not to cause an infinite loop. */
		gst_object_unref(src);
		return FALSE;
	}

	src = gst_bin_get_by_name(srcbin, "tee");
	if (!src) {
		return FALSE;
	}

	while((p = gst_element_get_static_pad(src, "sink"))) {
		if (srcpad) {
			gst_object_unref(srcpad);
		}
		srcpad = gst_pad_get_peer(p);
		gst_object_unref(src);
		src = gst_pad_get_parent_element(srcpad);
		gst_object_unref(p);
	};

	if (srcpad) {
		PurpleMediaManager *manager;
		GstElement *pipeline;
		GstElement *dest;
		GstElement *newsrc;

		manager = purple_media_get_manager(priv->media);
		pipeline = purple_media_manager_get_pipeline(manager);

		dest = gst_pad_get_parent_element(GST_PAD_PEER(srcpad));

		remove_element(src);

		newsrc = gst_element_factory_make("videotestsrc",
				"purplevideodowngrade");
		g_object_set(newsrc, "is-live", TRUE, NULL);
		gst_bin_add_many(srcbin, newsrc, NULL);
		gst_element_link(newsrc, dest);
		gst_element_set_state(newsrc, GST_STATE_PLAYING);

		/* Pipeline with an error might not be any longer PLAYING. */
		gst_element_set_state(pipeline, GST_STATE_PLAYING);

		gst_object_unref(srcpad);

		/* Flush the video pipeline. */
		srcpad = gst_element_get_static_pad(newsrc, "src");
		gst_pad_push_event(srcpad, gst_event_new_flush_start());
#if GST_CHECK_VERSION(1,0,0)
		gst_pad_push_event(srcpad, gst_event_new_flush_stop(FALSE));
#else
		gst_pad_push_event(srcpad, gst_event_new_flush_stop());
#endif
		gst_object_unref(srcpad);

		gst_object_unref(dest);
	}

	gst_object_unref(src);

	return TRUE;
}

static void
gst_handle_message_error(GstBus *bus, GstMessage *msg,
		PurpleMediaBackendFs2 *self)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	GstElement *element = GST_ELEMENT(GST_MESSAGE_SRC(msg));
	GstElement *lastElement = NULL;
	GList *sessions;
	gboolean fatal = TRUE;

	GError *error = NULL;
	gchar *debug_msg = NULL;

	gst_message_parse_error(msg, &error, &debug_msg);
	purple_debug_error("backend-fs2", "gst error %s\ndebugging: %s\n",
			error->message, debug_msg);

	g_error_free(error);
	g_free(debug_msg);

	while (element && !GST_IS_PIPELINE(element)) {
		if (element == priv->confbin)
			break;

		lastElement = element;
		element = GST_ELEMENT_PARENT(element);
	}

	if (!element || !GST_IS_PIPELINE(element))
		return;

	sessions = purple_media_get_session_ids(priv->media);

	for (; sessions; sessions = g_list_delete_link(sessions, sessions)) {
		PurpleMediaSessionType session_type;

		if (purple_media_get_src(priv->media, sessions->data)
				!= lastElement)
			continue;

		session_type = purple_media_get_session_type(priv->media,
				sessions->data);

		if (session_type & PURPLE_MEDIA_AUDIO) {
			purple_media_error(priv->media,
					_("Error with your microphone"));
		} else if (session_type & PURPLE_MEDIA_VIDEO) {
			fatal = !downgrade_video_source(self,
					GST_BIN(lastElement));

			if (fatal) {
				purple_media_error(priv->media,
						_("Error with your webcam"));
			}
		}

		break;
	}

	g_list_free(sessions);

	if (fatal) {
		purple_media_error(priv->media, _("Conference error"));
		purple_media_end(priv->media, NULL, NULL);
	}
}

static gboolean
gst_bus_cb(GstBus *bus, GstMessage *msg, PurpleMediaBackendFs2 *self)
{
	switch(GST_MESSAGE_TYPE(msg)) {
		case GST_MESSAGE_ELEMENT:
			gst_handle_message_element(bus, msg, self);
			break;
		case GST_MESSAGE_ERROR:
			gst_handle_message_error(bus, msg, self);
			break;
		default:
			break;
	}

	return TRUE;
}

static void
remove_element(GstElement *element)
{
	if (element) {
		gst_element_set_locked_state(element, TRUE);
		gst_element_set_state(element, GST_STATE_NULL);
		gst_bin_remove(GST_BIN(GST_ELEMENT_PARENT(element)), element);
	}
}

static void
state_changed_cb(PurpleMedia *media, PurpleMediaState state,
		gchar *sid, gchar *name, PurpleMediaBackendFs2 *self)
{
	if (state == PURPLE_MEDIA_STATE_END) {
		PurpleMediaBackendFs2Private *priv =
				PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);

		if (sid && name) {
			PurpleMediaBackendFs2Stream *stream = get_stream(self, sid, name);
			gst_object_unref(stream->stream);

			priv->streams = g_list_remove(priv->streams, stream);

			remove_element(stream->src);
			remove_element(stream->tee);
			remove_element(stream->volume);
			remove_element(stream->level);
			remove_element(stream->fakesink);
			remove_element(stream->queue);

			free_stream(stream);
		} else if (sid && !name) {
			PurpleMediaBackendFs2Session *session = get_session(self, sid);
			GstPad *pad;

			g_object_get(session->session, "sink-pad", &pad, NULL);
			gst_pad_unlink(GST_PAD_PEER(pad), pad);
			gst_object_unref(pad);

			gst_object_unref(session->session);
			g_hash_table_remove(priv->sessions, session->id);

			if (session->srcpad) {
				pad = gst_pad_get_peer(session->srcpad);
				if (pad) {
					gst_element_remove_pad(
							GST_PAD_PARENT(pad),
							pad);
					gst_object_unref(pad);
				}
				gst_object_unref(session->srcpad);
			}

			remove_element(session->srcvalve);
			remove_element(session->tee);

			free_session(session);
		}

		purple_media_manager_remove_output_windows(
				purple_media_get_manager(media), media, sid, name);
	}
}

static void
stream_info_cb(PurpleMedia *media, PurpleMediaInfoType type,
		gchar *sid, gchar *name, gboolean local,
		PurpleMediaBackendFs2 *self)
{
	if (type == PURPLE_MEDIA_INFO_ACCEPT && sid != NULL && name != NULL) {
		PurpleMediaBackendFs2Stream *stream =
				get_stream(self, sid, name);
		GError *err = NULL;

		g_object_set(G_OBJECT(stream->stream), "direction",
				session_type_to_fs_stream_direction(
				stream->session->type), NULL);

		if (stream->remote_candidates == NULL ||
				purple_media_is_initiator(media, sid, name))
			return;

#ifdef HAVE_FARSIGHT
		fs_stream_set_remote_candidates(stream->stream,
				stream->remote_candidates, &err);
#else
		if (stream->supports_add)
			fs_stream_add_remote_candidates(stream->stream,
					stream->remote_candidates, &err);
		else
			fs_stream_force_remote_candidates(stream->stream,
					stream->remote_candidates, &err);
#endif

		if (err == NULL)
			return;

		purple_debug_error("backend-fs2", "Error adding "
				"remote candidates: %s\n",
				err->message);
		g_error_free(err);
	} else if (local == TRUE && (type == PURPLE_MEDIA_INFO_MUTE ||
			type == PURPLE_MEDIA_INFO_UNMUTE)) {
		PurpleMediaBackendFs2Private *priv =
				PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
		gboolean active = (type == PURPLE_MEDIA_INFO_MUTE);
		GList *sessions;

		if (sid == NULL)
			sessions = g_hash_table_get_values(priv->sessions);
		else
			sessions = g_list_prepend(NULL,
					get_session(self, sid));

		purple_debug_info("media", "Turning mute %s\n",
				active ? "on" : "off");

		for (; sessions; sessions = g_list_delete_link(
				sessions, sessions)) {
			PurpleMediaBackendFs2Session *session =
					sessions->data;

			if (session->type & PURPLE_MEDIA_SEND_AUDIO) {
				gchar *name = g_strdup_printf("volume_%s",
						session->id);
				GstElement *volume = gst_bin_get_by_name(
						GST_BIN(priv->confbin), name);
				g_free(name);
				g_object_set(volume, "mute", active, NULL);
			}
		}
	} else if (local == TRUE && (type == PURPLE_MEDIA_INFO_HOLD ||
			type == PURPLE_MEDIA_INFO_UNHOLD)) {
		gboolean active = (type == PURPLE_MEDIA_INFO_HOLD);
		GList *streams = get_streams(self, sid, name);
		for (; streams; streams =
				g_list_delete_link(streams, streams)) {
			PurpleMediaBackendFs2Stream *stream = streams->data;
			if (stream->session->type & PURPLE_MEDIA_SEND_AUDIO) {
				g_object_set(stream->stream, "direction",
						session_type_to_fs_stream_direction(
						stream->session->type & ((active) ?
						~PURPLE_MEDIA_SEND_AUDIO :
						PURPLE_MEDIA_AUDIO)), NULL);
			}
		}
	} else if (local == TRUE && (type == PURPLE_MEDIA_INFO_PAUSE ||
			type == PURPLE_MEDIA_INFO_UNPAUSE)) {
		gboolean active = (type == PURPLE_MEDIA_INFO_PAUSE);
		GList *streams = get_streams(self, sid, name);
		for (; streams; streams =
				g_list_delete_link(streams, streams)) {
			PurpleMediaBackendFs2Stream *stream = streams->data;
			if (stream->session->type & PURPLE_MEDIA_SEND_VIDEO) {
				g_object_set(stream->stream, "direction",
						session_type_to_fs_stream_direction(
						stream->session->type & ((active) ?
						~PURPLE_MEDIA_SEND_VIDEO :
						PURPLE_MEDIA_VIDEO)), NULL);
			}
		}
	}
}

static gboolean
init_conference(PurpleMediaBackendFs2 *self)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	GstElement *pipeline;
	GstBus *bus;
	gchar *name;
#ifndef HAVE_FARSIGHT
	GKeyFile *default_props;
#endif

	priv->conference = FS_CONFERENCE(
			gst_element_factory_make(priv->conference_type, NULL));

	if (priv->conference == NULL) {
		purple_debug_error("backend-fs2", "Conference == NULL\n");
		return FALSE;
	}

	if (purple_account_get_silence_suppression(
				purple_media_get_account(priv->media)))
		priv->silence_threshold = purple_prefs_get_int(
				"/purple/media/audio/silence_threshold") / 100.0;
	else
		priv->silence_threshold = 0;

	pipeline = purple_media_manager_get_pipeline(
			purple_media_get_manager(priv->media));

	if (pipeline == NULL) {
		purple_debug_error("backend-fs2",
				"Couldn't retrieve pipeline.\n");
		return FALSE;
	}

	name = g_strdup_printf("conf_%p", priv->conference);
	priv->confbin = gst_bin_new(name);
	if (priv->confbin == NULL) {
		purple_debug_error("backend-fs2",
				"Couldn't create confbin.\n");
		return FALSE;
	}

	g_free(name);

	bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
	if (bus == NULL) {
		purple_debug_error("backend-fs2",
				"Couldn't get the pipeline's bus.\n");
		return FALSE;
	}

#ifndef HAVE_FARSIGHT
	default_props = fs_utils_get_default_element_properties(GST_ELEMENT(priv->conference));
	if (default_props != NULL) {
		priv->notifier = fs_element_added_notifier_new();
		fs_element_added_notifier_add(priv->notifier,
				GST_BIN(priv->confbin));
		fs_element_added_notifier_set_properties_from_keyfile(priv->notifier, default_props);
	}
#endif

	g_signal_connect(G_OBJECT(bus), "message",
			G_CALLBACK(gst_bus_cb), self);
	gst_object_unref(bus);

	if (!gst_bin_add(GST_BIN(pipeline),
			GST_ELEMENT(priv->confbin))) {
		purple_debug_error("backend-fs2", "Couldn't add confbin "
				"element to the pipeline\n");
		return FALSE;
	}

	if (!gst_bin_add(GST_BIN(priv->confbin),
			GST_ELEMENT(priv->conference))) {
		purple_debug_error("backend-fs2", "Couldn't add conference "
				"element to the confbin\n");
		return FALSE;
	}

	if (gst_element_set_state(GST_ELEMENT(priv->confbin),
			GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
		purple_debug_error("backend-fs2",
				"Failed to start conference.\n");
		return FALSE;
	}

	return TRUE;
}

static void
gst_element_added_cb(FsElementAddedNotifier *self,
		GstBin *bin, GstElement *element, gpointer user_data)
{
	/*
	 * Hack to make H264 work with Gmail video.
	 */
	if (!strncmp(GST_ELEMENT_NAME(element), "x264", 4)) {
		g_object_set(GST_OBJECT(element), "cabac", FALSE, NULL);
	}
}

static gboolean
create_src(PurpleMediaBackendFs2 *self, const gchar *sess_id,
		PurpleMediaSessionType type)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	PurpleMediaBackendFs2Session *session;
	PurpleMediaSessionType session_type;
	FsMediaType media_type = session_type_to_fs_media_type(type);
	FsStreamDirection type_direction =
			session_type_to_fs_stream_direction(type);
	GstElement *src;
	GstPad *sinkpad, *srcpad;
	GstPad *ghost = NULL;

	if ((type_direction & FS_DIRECTION_SEND) == 0)
		return TRUE;

	session_type = session_type_from_fs(
			media_type, FS_DIRECTION_SEND);
	src = purple_media_manager_get_element(
			purple_media_get_manager(priv->media),
			session_type, priv->media, sess_id, NULL);

	if (!GST_IS_ELEMENT(src)) {
		purple_debug_error("backend-fs2",
				"Error creating src for session %s\n",
				sess_id);
		return FALSE;
	}

	session = get_session(self, sess_id);

	if (session == NULL) {
		purple_debug_warning("backend-fs2",
				"purple_media_set_src: trying to set"
				" src on non-existent session\n");
		return FALSE;
	}

	if (session->src)
		gst_object_unref(session->src);

	session->src = src;
	gst_element_set_locked_state(session->src, TRUE);

	session->tee = gst_element_factory_make("tee", NULL);
	gst_bin_add(GST_BIN(priv->confbin), session->tee);

	/* This supposedly isn't necessary, but it silences some warnings */
	if (GST_ELEMENT_PARENT(priv->confbin)
			== GST_ELEMENT_PARENT(session->src)) {
		GstPad *pad = gst_element_get_static_pad(session->tee, "sink");
		ghost = gst_ghost_pad_new(NULL, pad);
		gst_object_unref(pad);
		gst_pad_set_active(ghost, TRUE);
		gst_element_add_pad(priv->confbin, ghost);
	}

	gst_element_set_state(session->tee, GST_STATE_PLAYING);
	gst_element_link(session->src, priv->confbin);
	if (ghost)
		session->srcpad = gst_pad_get_peer(ghost);

	g_object_get(session->session, "sink-pad", &sinkpad, NULL);
	if (session->type & PURPLE_MEDIA_SEND_AUDIO) {
		gchar *name = g_strdup_printf("volume_%s", session->id);
		GstElement *level;
		GstElement *volume = gst_element_factory_make("volume", name);
		double input_volume = purple_prefs_get_int(
				"/purple/media/audio/volume/input")/10.0;
		g_free(name);
		name = g_strdup_printf("sendlevel_%s", session->id);
		level = gst_element_factory_make("level", name);
		g_free(name);
		session->srcvalve = gst_element_factory_make("valve", NULL);
		gst_bin_add(GST_BIN(priv->confbin), volume);
		gst_bin_add(GST_BIN(priv->confbin), level);
		gst_bin_add(GST_BIN(priv->confbin), session->srcvalve);
		gst_element_set_state(level, GST_STATE_PLAYING);
		gst_element_set_state(volume, GST_STATE_PLAYING);
		gst_element_set_state(session->srcvalve, GST_STATE_PLAYING);
		gst_element_link(level, session->srcvalve);
		gst_element_link(volume, level);
		gst_element_link(session->tee, volume);
		srcpad = gst_element_get_static_pad(session->srcvalve, "src");
		g_object_set(volume, "volume", input_volume, NULL);
	} else {
#if GST_CHECK_VERSION(1,0,0)
		srcpad = gst_element_get_request_pad(session->tee, "src_%u");
#else
		srcpad = gst_element_get_request_pad(session->tee, "src%d");
#endif
	}

	purple_debug_info("backend-fs2", "connecting pad: %s\n",
			  gst_pad_link(srcpad, sinkpad) == GST_PAD_LINK_OK
			  ? "success" : "failure");
	gst_element_set_locked_state(session->src, FALSE);
	gst_object_unref(session->src);
	gst_object_unref(sinkpad);
	gst_object_unref(srcpad);

	purple_media_manager_create_output_window(purple_media_get_manager(
			priv->media), priv->media, sess_id, NULL);

	purple_debug_info("backend-fs2", "create_src: setting source "
		"state to GST_STATE_PLAYING - it may hang here on win32\n");
	gst_element_set_state(session->src, GST_STATE_PLAYING);
	purple_debug_info("backend-fs2", "create_src: state set\n");

	return TRUE;
}

static gboolean
create_session(PurpleMediaBackendFs2 *self, const gchar *sess_id,
		PurpleMediaSessionType type, gboolean initiator,
		const gchar *transmitter)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	PurpleMediaBackendFs2Session *session;
	GError *err = NULL;
	GList *codec_conf = NULL, *iter = NULL;
	gchar *filename = NULL;
	gboolean is_nice = purple_strequal(transmitter, "nice");

	session = g_new0(PurpleMediaBackendFs2Session, 1);

	session->session = fs_conference_new_session(priv->conference,
			session_type_to_fs_media_type(type), &err);

#ifdef HAVE_MEDIA_APPLICATION
	if (type == PURPLE_MEDIA_APPLICATION) {
		GstCaps *caps;
		GObject *rtpsession = NULL;

		caps = gst_caps_new_empty_simple ("application/octet-stream");
		fs_session_set_allowed_caps (session->session, caps, caps, NULL);
		gst_caps_unref (caps);
		g_object_get (session->session, "internal-session", &rtpsession, NULL);
		if (rtpsession) {
			g_object_set (rtpsession, "probation", 0, NULL);
			g_object_unref (rtpsession);
		}
	}
#endif
	if (err != NULL) {
		purple_media_error(priv->media,
				_("Error creating session: %s"),
				err->message);
		g_error_free(err);
		g_free(session);
		return FALSE;
	}

	filename = g_build_filename(purple_user_dir(), "fs-codec.conf", NULL);
	codec_conf = fs_codec_list_from_keyfile(filename, &err);
	g_free(filename);

	if (err != NULL) {
		if (err->code == 4)
			purple_debug_info("backend-fs2", "Couldn't read "
					"fs-codec.conf: %s\n",
					err->message);
		else
			purple_debug_error("backend-fs2", "Error reading "
					"fs-codec.conf: %s\n",
					err->message);
		g_error_free(err);
	}

	/*
	 * Add SPEEX if the configuration file doesn't exist or
	 * there isn't a speex entry.
	 */
	for (iter = codec_conf; iter; iter = g_list_next(iter)) {
		FsCodec *codec = iter->data;
		if (!g_ascii_strcasecmp(codec->encoding_name, "speex"))
			break;
	}

	if (iter == NULL) {
		codec_conf = g_list_prepend(codec_conf,
				fs_codec_new(FS_CODEC_ID_ANY,
				"SPEEX", FS_MEDIA_TYPE_AUDIO, 8000));
		codec_conf = g_list_prepend(codec_conf,
				fs_codec_new(FS_CODEC_ID_ANY,
				"SPEEX", FS_MEDIA_TYPE_AUDIO, 16000));
	}

	fs_session_set_codec_preferences(session->session, codec_conf, NULL);
	fs_codec_list_destroy(codec_conf);

	/*
	 * Removes a 5-7 second delay before
	 * receiving the src-pad-added signal.
	 * Only works for non-multicast FsRtpSessions.
	 */
	if (!purple_strequal(transmitter, "multicast"))
		g_object_set(G_OBJECT(session->session),
				"no-rtcp-timeout", 0, NULL);

	/*
	 * Hack to make x264 work with Gmail video.
	 */
	if (is_nice && purple_strequal(sess_id, "google-video")) {
		FsElementAddedNotifier *notifier =
				fs_element_added_notifier_new();
		g_signal_connect(G_OBJECT(notifier), "element-added",
				G_CALLBACK(gst_element_added_cb), NULL);
		fs_element_added_notifier_add(notifier,
				GST_BIN(priv->conference));
	}

	session->id = g_strdup(sess_id);
	session->backend = self;
	session->type = type;

	if (!priv->sessions) {
		purple_debug_info("backend-fs2",
				"Creating hash table for sessions\n");
		priv->sessions = g_hash_table_new_full(g_str_hash, g_str_equal,
		                                       g_free, NULL);
	}

	g_hash_table_insert(priv->sessions, g_strdup(session->id), session);

	if (!create_src(self, sess_id, type)) {
		purple_debug_info("backend-fs2", "Error creating the src\n");
		return FALSE;
	}

	return TRUE;
}

static void
free_session(PurpleMediaBackendFs2Session *session)
{
	g_free(session->id);
	g_free(session);
}

static gboolean
create_participant(PurpleMediaBackendFs2 *self, const gchar *name)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	FsParticipant *participant;
	GError *err = NULL;

	participant = fs_conference_new_participant(
#ifdef HAVE_FARSIGHT
			priv->conference, name, &err);
#else
			priv->conference, &err);
#endif

	if (err) {
		purple_debug_error("backend-fs2",
				"Error creating participant: %s\n",
				err->message);
		g_error_free(err);
		return FALSE;
	}

	g_object_set_data_full(G_OBJECT(participant), "purple-name",
			g_strdup(name), g_free);

#ifndef HAVE_FARSIGHT
	if (g_object_class_find_property(G_OBJECT_GET_CLASS(participant),
			"cname")) {
		g_object_set(participant, "cname", name, NULL);
	}
#endif

	if (!priv->participants) {
		purple_debug_info("backend-fs2",
				"Creating hash table for participants\n");
		priv->participants = g_hash_table_new_full(g_str_hash,
				g_str_equal, g_free, g_object_unref);
	}

	g_hash_table_insert(priv->participants, g_strdup(name), participant);

	return TRUE;
}

static gboolean
src_pad_added_cb_cb(PurpleMediaBackendFs2Stream *stream)
{
	PurpleMediaBackendFs2Private *priv;

	g_return_val_if_fail(stream != NULL, FALSE);

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(stream->session->backend);
	stream->connected_cb_id = 0;

	if (stream->src == NULL) {
		GstElement *pipeline = purple_media_manager_get_pipeline(
			purple_media_get_manager(priv->media));
		GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipeline),
					  GST_DEBUG_GRAPH_SHOW_ALL, "media-fail");

		purple_media_error(priv->media,
				   _("Could not create media pipeline"));
		purple_media_end(priv->media, NULL, NULL);
		return FALSE;
	}

	purple_media_manager_create_output_window(
			purple_media_get_manager(priv->media), priv->media,
			stream->session->id, stream->participant);

	g_signal_emit_by_name(priv->media, "state-changed",
			PURPLE_MEDIA_STATE_CONNECTED,
			stream->session->id, stream->participant);
	return FALSE;
}

static void
src_pad_added_cb(FsStream *fsstream, GstPad *srcpad,
		FsCodec *codec, PurpleMediaBackendFs2Stream *stream)
{
	PurpleMediaBackendFs2Private *priv;
	GstPad *sinkpad;

	g_return_if_fail(FS_IS_STREAM(fsstream));
	g_return_if_fail(stream != NULL);

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(stream->session->backend);

	if (stream->src == NULL) {
		GstElement *sink = NULL;

		if (codec->media_type == FS_MEDIA_TYPE_AUDIO) {
			double output_volume = purple_prefs_get_int(
					"/purple/media/audio/volume/output")/10.0;
			stream->queue = gst_element_factory_make("queue", NULL);
			stream->volume = gst_element_factory_make("volume", NULL);
			g_object_set(stream->volume, "volume", output_volume, NULL);
			stream->level = gst_element_factory_make("level", NULL);
			stream->src = gst_element_factory_make("audiomixer", NULL);
			g_object_set(stream->src, "start-time-selection", 1, NULL);
			sink = purple_media_manager_get_element(
					purple_media_get_manager(priv->media),
					PURPLE_MEDIA_RECV_AUDIO, priv->media,
					stream->session->id,
					stream->participant);
			gst_bin_add(GST_BIN(priv->confbin), stream->queue);
			gst_bin_add(GST_BIN(priv->confbin), stream->volume);
			gst_bin_add(GST_BIN(priv->confbin), stream->level);
			gst_bin_add(GST_BIN(priv->confbin), sink);
			gst_element_set_state(sink, GST_STATE_PLAYING);
			gst_element_set_state(stream->level, GST_STATE_PLAYING);
			gst_element_set_state(stream->volume, GST_STATE_PLAYING);
			gst_element_set_state(stream->queue, GST_STATE_PLAYING);
			gst_element_link(stream->level, sink);
			gst_element_link(stream->volume, stream->level);
			gst_element_link(stream->queue, stream->volume);
			sink = stream->queue;
		} else if (codec->media_type == FS_MEDIA_TYPE_VIDEO) {
#if GST_CHECK_VERSION(1,0,0)
			stream->src = gst_element_factory_make("funnel", NULL);
#else
			stream->src = gst_element_factory_make("fsfunnel", NULL);
#endif
			sink = gst_element_factory_make("fakesink", NULL);
			g_object_set(G_OBJECT(sink), "async", FALSE, NULL);
			gst_bin_add(GST_BIN(priv->confbin), sink);
			gst_element_set_state(sink, GST_STATE_PLAYING);
			stream->fakesink = sink;
#ifdef HAVE_MEDIA_APPLICATION
		} else if (codec->media_type == FS_MEDIA_TYPE_APPLICATION) {
#if GST_CHECK_VERSION(1,0,0)
			stream->src = gst_element_factory_make("funnel", NULL);
#else
			stream->src = gst_element_factory_make("fsfunnel", NULL);
#endif
			sink = purple_media_manager_get_element(
					purple_media_get_manager(priv->media),
					PURPLE_MEDIA_RECV_APPLICATION, priv->media,
					stream->session->id,
					stream->participant);
			gst_bin_add(GST_BIN(priv->confbin), sink);
			gst_element_set_state(sink, GST_STATE_PLAYING);
#endif
		}
		stream->tee = gst_element_factory_make("tee", NULL);
		gst_bin_add_many(GST_BIN(priv->confbin),
				stream->src, stream->tee, NULL);
		gst_element_set_state(stream->tee, GST_STATE_PLAYING);
		gst_element_set_state(stream->src, GST_STATE_PLAYING);
		gst_element_link_many(stream->src, stream->tee, sink, NULL);
	} else {
		if (codec->media_type == FS_MEDIA_TYPE_AUDIO) {
			GstElement *convert, *resample, *capsfilter;
			GstPad *mixer_srcpad;
			GstCaps *caps;

			/* The audiomixer element requires that all input
			 * streams have the same rate, so resample if
			 * needed
			 */
			mixer_srcpad = gst_element_get_static_pad(stream->src, "src");
			caps = gst_pad_get_current_caps(mixer_srcpad);

			if (caps) {
				convert = gst_element_factory_make("audioconvert", NULL);
				resample = gst_element_factory_make("audioresample", NULL);
				capsfilter = gst_element_factory_make("capsfilter", NULL);

				gst_bin_add_many(GST_BIN(priv->confbin), convert,
						resample, capsfilter, NULL);
				gst_element_link_many(gst_pad_get_parent_element(srcpad),
						convert, resample, capsfilter, NULL);

				g_object_set(capsfilter, "caps", caps, NULL);
				gst_element_set_state(convert, GST_STATE_PLAYING);
				gst_element_set_state(resample, GST_STATE_PLAYING);
				gst_element_set_state(capsfilter, GST_STATE_PLAYING);

				srcpad = gst_element_get_static_pad(capsfilter, "src");
				gst_object_unref(caps);
			}
			gst_object_unref(mixer_srcpad);
		}
	}

#if GST_CHECK_VERSION(1,0,0)
	sinkpad = gst_element_get_request_pad(stream->src, "sink_%u");
#else
	sinkpad = gst_element_get_request_pad(stream->src, "sink%d");
#endif
	gst_pad_link(srcpad, sinkpad);
	gst_object_unref(sinkpad);

	stream->connected_cb_id = purple_timeout_add(0,
			(GSourceFunc)src_pad_added_cb_cb, stream);
}

#ifdef HAVE_FARSIGHT
static GValueArray *
append_relay_info(GValueArray *relay_info, const gchar *ip, gint port,
	const gchar *username, const gchar *password, const gchar *type)
{
	GValue value;
	GstStructure *turn_setup = gst_structure_new("relay-info",
				"ip", G_TYPE_STRING, ip,
				"port", G_TYPE_UINT, port,
				"username", G_TYPE_STRING, username,
				"password", G_TYPE_STRING, password,
				"relay-type", G_TYPE_STRING, type,
				NULL);

	if (turn_setup) {
		memset(&value, 0, sizeof(GValue));
		g_value_init(&value, GST_TYPE_STRUCTURE);
		gst_value_set_structure(&value, turn_setup);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
		relay_info = g_value_array_append(relay_info, &value);
G_GNUC_END_IGNORE_DEPRECATIONS
		gst_structure_free(turn_setup);
	}

	return relay_info;
}
#endif

static gboolean
create_stream(PurpleMediaBackendFs2 *self,
		const gchar *sess_id, const gchar *who,
		PurpleMediaSessionType type, gboolean initiator,
		const gchar *transmitter,
		guint num_params, GParameter *params)
{
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	GError *err = NULL;
	FsStream *fsstream = NULL;
	const gchar *stun_ip = purple_network_get_stun_ip();
	const gchar *turn_ip = purple_network_get_turn_ip();
	guint _num_params = num_params;
	GParameter *_params;
	FsStreamDirection type_direction =
			session_type_to_fs_stream_direction(type);
	PurpleMediaBackendFs2Session *session;
	PurpleMediaBackendFs2Stream *stream;
	FsParticipant *participant;
	/* check if the prpl has already specified a relay-info
	  we need to do this to allow them to override when using non-standard
	  TURN modes, like Google f.ex. */
	gboolean got_turn_from_prpl = FALSE;
	guint i;
#ifndef HAVE_FARSIGHT
	GPtrArray *relay_info = g_ptr_array_new_full (1, (GDestroyNotify) gst_structure_free);
	gboolean ret;
#endif

	session = get_session(self, sess_id);

	if (session == NULL) {
		purple_debug_error("backend-fs2",
				"Couldn't find session to create stream.\n");
		return FALSE;
	}

	participant = get_participant(self, who);

	if (participant == NULL) {
		purple_debug_error("backend-fs2", "Couldn't find "
				"participant to create stream.\n");
		return FALSE;
	}

#ifndef HAVE_FARSIGHT
	fsstream = fs_session_new_stream(session->session, participant,
			initiator == TRUE ? type_direction :
			(type_direction & FS_DIRECTION_RECV), &err);

	if (fsstream == NULL) {
		if (err) {
			purple_debug_error("backend-fs2",
					"Error creating stream: %s\n",
					err && err->message ?
					err->message : "NULL");
			g_error_free(err);
		} else
			purple_debug_error("backend-fs2",
					"Error creating stream\n");
		return FALSE;
	}
#endif

	for (i = 0 ; i < num_params ; i++) {
		if (purple_strequal(params[i].name, "relay-info")) {
			got_turn_from_prpl = TRUE;
			break;
		}
	}

	_params = g_new0(GParameter, num_params + 3);
	memcpy(_params, params, sizeof(GParameter) * num_params);

	/* set the controlling mode parameter */
	_params[_num_params].name = "controlling-mode";
	g_value_init(&_params[_num_params].value, G_TYPE_BOOLEAN);
	g_value_set_boolean(&_params[_num_params].value, initiator);
	++_num_params;

	if (stun_ip) {
		purple_debug_info("backend-fs2",
			"Setting stun-ip on new stream: %s\n", stun_ip);

		_params[_num_params].name = "stun-ip";
		g_value_init(&_params[_num_params].value, G_TYPE_STRING);
		g_value_set_string(&_params[_num_params].value, stun_ip);
		++_num_params;
	}

	if (turn_ip && purple_strequal("nice", transmitter) && !got_turn_from_prpl) {
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
#ifdef HAVE_FARSIGHT
		GValueArray *relay_info = g_value_array_new(0);
#endif
G_GNUC_END_IGNORE_DEPRECATIONS
		gint port;
		const gchar *username =	purple_prefs_get_string(
				"/purple/network/turn_username");
		const gchar *password = purple_prefs_get_string(
				"/purple/network/turn_password");

		/* UDP */
		port = purple_prefs_get_int("/purple/network/turn_port");
		if (port > 0) {
#ifdef HAVE_FARSIGHT
			relay_info = append_relay_info(relay_info, turn_ip, port, username,
				password, "udp");
#else
			g_ptr_array_add (relay_info,
				gst_structure_new ("relay-info",
				"ip", G_TYPE_STRING, turn_ip,
				"port", G_TYPE_UINT, port,
				"username", G_TYPE_STRING, username,
				"password", G_TYPE_STRING, password,
				"relay-type", G_TYPE_STRING, "udp",
				NULL));
#endif
		}

		/* TCP */
		port = purple_prefs_get_int("/purple/network/turn_port_tcp");
		if (port > 0) {
#ifdef HAVE_FARSIGHT
			relay_info = append_relay_info(relay_info, turn_ip, port, username,
				password, "tcp");
#else
			g_ptr_array_add (relay_info,
				gst_structure_new ("relay-info",
				"ip", G_TYPE_STRING, turn_ip,
				"port", G_TYPE_UINT, port,
				"username", G_TYPE_STRING, username,
				"password", G_TYPE_STRING, password,
				"relay-type", G_TYPE_STRING, "tcp",
				NULL));
#endif
		}

		/* TURN over SSL is only supported by libnice for Google's "psuedo" SSL mode
			at this time */

		purple_debug_info("backend-fs2",
			"Setting relay-info on new stream\n");
		_params[_num_params].name = "relay-info";
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
#ifdef HAVE_FARSIGHT
		g_value_init(&_params[_num_params].value, G_TYPE_VALUE_ARRAY);
		g_value_set_boxed(&_params[_num_params].value, relay_info);
		g_value_array_free(relay_info);
#else
		g_value_init(&_params[_num_params].value, G_TYPE_PTR_ARRAY);
		g_value_set_boxed(&_params[_num_params].value, relay_info);
#endif
G_GNUC_END_IGNORE_DEPRECATIONS
		_num_params++;
	}

#ifdef HAVE_FARSIGHT
	fsstream = fs_session_new_stream(session->session, participant,
			initiator == TRUE ? type_direction :
			(type_direction & FS_DIRECTION_RECV), transmitter,
			_num_params, _params, &err);
	g_free(_params);

	if (fsstream == NULL) {
		if (err) {
			purple_debug_error("backend-fs2",
					"Error creating stream: %s\n",
					err && err->message ?
					err->message : "NULL");
			g_error_free(err);
		} else
			purple_debug_error("backend-fs2",
					"Error creating stream\n");
		return FALSE;
	}
#else
	ret = fs_stream_set_transmitter(fsstream, transmitter,
			_params, _num_params, &err);
	for (i = 0 ; i < _num_params ; i++)
		g_value_unset (&_params[i].value);
	g_free(_params);
        if (relay_info)
		g_ptr_array_unref (relay_info);
	if (ret == FALSE) {
		purple_debug_error("backend-fs2",
			"Could not set transmitter %s: %s.\n",
			transmitter, err ? err->message : NULL);
		g_clear_error(&err);
		return FALSE;
	}
#endif

	stream = g_new0(PurpleMediaBackendFs2Stream, 1);
	stream->participant = g_strdup(who);
	stream->session = session;
	stream->stream = fsstream;
#ifndef HAVE_FARSIGHT
	stream->supports_add = purple_strequal(transmitter, "nice");
#endif

	priv->streams =	g_list_append(priv->streams, stream);

	g_signal_connect(G_OBJECT(fsstream), "src-pad-added",
			G_CALLBACK(src_pad_added_cb), stream);

	return TRUE;
}

static void
free_stream(PurpleMediaBackendFs2Stream *stream)
{
	/* Remove the connected_cb timeout */
	if (stream->connected_cb_id != 0)
		purple_timeout_remove(stream->connected_cb_id);

	g_free(stream->participant);

	if (stream->local_candidates)
		fs_candidate_list_destroy(stream->local_candidates);

	if (stream->remote_candidates)
		fs_candidate_list_destroy(stream->remote_candidates);

	g_free(stream);
}

static gboolean
purple_media_backend_fs2_add_stream(PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *who,
		PurpleMediaSessionType type, gboolean initiator,
		const gchar *transmitter,
		guint num_params, GParameter *params)
{
	PurpleMediaBackendFs2 *backend = PURPLE_MEDIA_BACKEND_FS2(self);
	PurpleMediaBackendFs2Private *priv =
			PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(backend);
	PurpleMediaBackendFs2Stream *stream;

	if (priv->conference == NULL && !init_conference(backend)) {
		purple_debug_error("backend-fs2",
				"Error initializing the conference.\n");
		return FALSE;
	}

	if (get_session(backend, sess_id) == NULL &&
			!create_session(backend, sess_id, type,
			initiator, transmitter)) {
		purple_debug_error("backend-fs2",
				"Error creating the session.\n");
		return FALSE;
	}

	if (get_participant(backend, who) == NULL &&
			!create_participant(backend, who)) {
		purple_debug_error("backend-fs2",
				"Error creating the participant.\n");
		return FALSE;
	}

	stream = get_stream(backend, sess_id, who);

	if (stream != NULL) {
		FsStreamDirection type_direction =
				session_type_to_fs_stream_direction(type);

		if (session_type_to_fs_stream_direction(
				stream->session->type) != type_direction) {
			/* change direction */
			g_object_set(stream->stream, "direction",
					type_direction, NULL);
		}
	} else if (!create_stream(backend, sess_id, who, type,
			initiator, transmitter, num_params, params)) {
		purple_debug_error("backend-fs2",
				"Error creating the stream.\n");
		return FALSE;
	}

	return TRUE;
}

static void
purple_media_backend_fs2_add_remote_candidates(PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant,
		GList *remote_candidates)
{
	PurpleMediaBackendFs2Private *priv;
	PurpleMediaBackendFs2Stream *stream;
	GError *err = NULL;

	g_return_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self));

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);
	stream = get_stream(PURPLE_MEDIA_BACKEND_FS2(self),
			sess_id, participant);

	if (stream == NULL) {
		purple_debug_error("backend-fs2",
				"purple_media_add_remote_candidates: "
				"couldn't find stream %s %s.\n",
				sess_id ? sess_id : "(null)",
				participant ? participant : "(null)");
		return;
	}

	stream->remote_candidates = g_list_concat(stream->remote_candidates,
			candidate_list_to_fs(remote_candidates));

	if (purple_media_is_initiator(priv->media, sess_id, participant) ||
			purple_media_accepted(
			priv->media, sess_id, participant)) {
#ifdef HAVE_FARSIGHT
		fs_stream_set_remote_candidates(stream->stream,
				stream->remote_candidates, &err);
#else
		if (stream->supports_add)
			fs_stream_add_remote_candidates(stream->stream,
					stream->remote_candidates, &err);
		else
			fs_stream_force_remote_candidates(stream->stream,
					stream->remote_candidates, &err);
#endif

		if (err) {
			purple_debug_error("backend-fs2", "Error adding remote"
					" candidates: %s\n", err->message);
			g_error_free(err);
		}
	}
}

static gboolean
purple_media_backend_fs2_codecs_ready(PurpleMediaBackend *self,
		const gchar *sess_id)
{
	PurpleMediaBackendFs2Private *priv;
	gboolean ret = FALSE;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), FALSE);

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);

	if (sess_id != NULL) {
		PurpleMediaBackendFs2Session *session = get_session(
				PURPLE_MEDIA_BACKEND_FS2(self), sess_id);

		if (session == NULL)
			return FALSE;

		if (session->type & (PURPLE_MEDIA_SEND_AUDIO |
#ifdef HAVE_MEDIA_APPLICATION
				PURPLE_MEDIA_SEND_APPLICATION |
#endif
				PURPLE_MEDIA_SEND_VIDEO)) {
#ifdef HAVE_FARSIGHT
			g_object_get(session->session,
					"codecs-ready", &ret, NULL);
#else
			GList *codecs = NULL;

			g_object_get(session->session,
					"codecs", &codecs, NULL);
			if (codecs) {
				fs_codec_list_destroy (codecs);
				ret = TRUE;
			}
#endif
		} else
			ret = TRUE;
	} else {
		GList *values = g_hash_table_get_values(priv->sessions);

		for (; values; values = g_list_delete_link(values, values)) {
			PurpleMediaBackendFs2Session *session = values->data;

			if (session->type & (PURPLE_MEDIA_SEND_AUDIO |
#ifdef HAVE_MEDIA_APPLICATION
					PURPLE_MEDIA_SEND_APPLICATION |
#endif
					PURPLE_MEDIA_SEND_VIDEO)) {
#ifdef HAVE_FARSIGHT
				g_object_get(session->session,
						"codecs-ready", &ret, NULL);
				if (ret == FALSE)
					break;
#else
				GList *codecs = NULL;

				g_object_get(session->session,
						"codecs", &codecs, NULL);
				if (codecs) {
					fs_codec_list_destroy (codecs);
					ret = TRUE;
				} else {
					ret = FALSE;
					break;
				}
#endif
			} else
				ret = TRUE;
		}

		if (values != NULL)
			g_list_free(values);
	}

	return ret;
}

static GList *
purple_media_backend_fs2_get_codecs(PurpleMediaBackend *self,
		const gchar *sess_id)
{
	PurpleMediaBackendFs2Session *session;
	GList *fscodecs;
	GList *codecs;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), NULL);

	session = get_session(PURPLE_MEDIA_BACKEND_FS2(self), sess_id);

	if (session == NULL)
		return NULL;

	g_object_get(G_OBJECT(session->session),
		     "codecs", &fscodecs, NULL);
	codecs = codec_list_from_fs(fscodecs);
	fs_codec_list_destroy(fscodecs);

	return codecs;
}

static GList *
purple_media_backend_fs2_get_local_candidates(PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant)
{
	PurpleMediaBackendFs2Stream *stream;
	GList *candidates = NULL;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), NULL);

	stream = get_stream(PURPLE_MEDIA_BACKEND_FS2(self),
			sess_id, participant);

	if (stream != NULL)
		candidates = candidate_list_from_fs(
				stream->local_candidates);
	return candidates;
}

static gboolean
purple_media_backend_fs2_set_remote_codecs(PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant,
		GList *codecs)
{
	PurpleMediaBackendFs2Stream *stream;
	GList *fscodecs;
	GError *err = NULL;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), FALSE);
	stream = get_stream(PURPLE_MEDIA_BACKEND_FS2(self),
			sess_id, participant);

	if (stream == NULL)
		return FALSE;

	fscodecs = codec_list_to_fs(codecs);
	fs_stream_set_remote_codecs(stream->stream, fscodecs, &err);
	fs_codec_list_destroy(fscodecs);

	if (err) {
		purple_debug_error("backend-fs2",
				"Error setting remote codecs: %s\n",
				err->message);
		g_error_free(err);
		return FALSE;
	}

	return TRUE;
}

#if GST_CHECK_VERSION(1,0,0)
static GstStructure *
create_fs2_srtp_structure(const gchar *cipher, const gchar *auth,
	const gchar *key, gsize key_len)
{
	GstStructure *result;
	GstBuffer *buffer;
	GstMapInfo info;

	buffer = gst_buffer_new_allocate(NULL, key_len, NULL);
	gst_buffer_map(buffer, &info, GST_MAP_WRITE);
	memcpy(info.data, key, key_len);
	gst_buffer_unmap(buffer, &info);

	result = gst_structure_new("FarstreamSRTP",
			"cipher", G_TYPE_STRING, cipher,
			"auth", G_TYPE_STRING, auth,
			"key", GST_TYPE_BUFFER, buffer,
			NULL);
	gst_buffer_unref(buffer);

	return result;
}

static gboolean
purple_media_backend_fs2_set_encryption_parameters (PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *cipher, const gchar *auth,
		const gchar *key, gsize key_len)
{
	PurpleMediaBackendFs2Session *session;
	GstStructure *srtp;
	GError *err = NULL;
	gboolean result;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), FALSE);

	session = get_session(PURPLE_MEDIA_BACKEND_FS2(self), sess_id);
	if (!session)
		return FALSE;

	srtp = create_fs2_srtp_structure(cipher, auth, key, key_len);
	if (!srtp)
		return FALSE;

	result = fs_session_set_encryption_parameters(session->session, srtp,
						&err);
	if (!result) {
		purple_debug_error("backend-fs2",
				"Error setting encryption parameters: %s\n", err->message);
		g_error_free(err);
	}

	gst_structure_free(srtp);
	return result;
}

static gboolean
purple_media_backend_fs2_set_decryption_parameters (PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant,
		const gchar *cipher, const gchar *auth,
		const gchar *key, gsize key_len)
{
	PurpleMediaBackendFs2Stream *stream;
	GstStructure *srtp;
	GError *err = NULL;
	gboolean result;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), FALSE);

	stream = get_stream(PURPLE_MEDIA_BACKEND_FS2(self), sess_id,
			participant);
	if (!stream)
		return FALSE;

	srtp = create_fs2_srtp_structure(cipher, auth, key, key_len);
	if (!srtp)
		return FALSE;

	result = fs_stream_set_decryption_parameters(stream->stream, srtp,
						&err);
	if (!result) {
		purple_debug_error("backend-fs2",
				"Error setting decryption parameters: %s\n", err->message);
		g_error_free(err);
	}

	gst_structure_free(srtp);
	return result;
}

static gboolean
purple_media_backend_fs2_set_require_encryption(PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant,
		gboolean require_encryption)
{
	PurpleMediaBackendFs2Stream *stream;
	gboolean result;

	stream = get_stream(PURPLE_MEDIA_BACKEND_FS2(self), sess_id,
			participant);
	if (!stream) {
		return FALSE;
	}

	g_object_set(stream->stream, "require-encryption",
			require_encryption, NULL);
	return TRUE;
}

#endif /* GST 1.0+ */

static gboolean
purple_media_backend_fs2_set_send_codec(PurpleMediaBackend *self,
		const gchar *sess_id, PurpleMediaCodec *codec)
{
	PurpleMediaBackendFs2Session *session;
	FsCodec *fscodec;
	GError *err = NULL;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), FALSE);

	session = get_session(PURPLE_MEDIA_BACKEND_FS2(self), sess_id);

	if (session == NULL)
		return FALSE;

	fscodec = codec_to_fs(codec);
	fs_session_set_send_codec(session->session, fscodec, &err);
	fs_codec_destroy(fscodec);

	if (err) {
		purple_debug_error("media", "Error setting send codec\n");
		g_error_free(err);
		return FALSE;
	}

	return TRUE;
}

static const gchar **
purple_media_backend_fs2_get_available_params(void)
{
	static const gchar *supported_params[] = {
		"sdes-cname", "sdes-email", "sdes-location", "sdes-name", "sdes-note",
		"sdes-phone", "sdes-tool", NULL
	};

	return supported_params;
}

static const gchar*
param_to_sdes_type(const gchar *param)
{
	const gchar **supported = purple_media_backend_fs2_get_available_params();
	static const gchar *sdes_types[] = {
		"cname", "email", "location", "name", "note", "phone", "tool", NULL
	};
	guint i;

	for (i = 0; supported[i] != NULL; ++i) {
		if (purple_strequal(param, supported[i])) {
			return sdes_types[i];
		}
	}

	return NULL;
}

static void
purple_media_backend_fs2_set_params(PurpleMediaBackend *self,
		guint num_params, GParameter *params)
{
	PurpleMediaBackendFs2Private *priv;
	guint i;
#ifndef HAVE_FARSIGHT
	GstStructure *sdes;
#endif

	g_return_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self));

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);

	if (priv->conference == NULL &&
		!init_conference(PURPLE_MEDIA_BACKEND_FS2(self))) {
		purple_debug_error("backend-fs2",
				"Error initializing the conference.\n");
		return;
	}

#ifdef HAVE_FARSIGHT
	for (i = 0; i != num_params; ++i) {
		if (param_to_sdes_type(params[i].name)) {
			g_object_set(priv->conference,
			             params[i].name, g_value_get_string(&params[i].value),
			             NULL);
		}
	}
#else
	g_object_get(G_OBJECT(priv->conference), "sdes", &sdes, NULL);

	for (i = 0; i != num_params; ++i) {
		const gchar *sdes_type = param_to_sdes_type(params[i].name);
		if (!sdes_type)
			continue;

		gst_structure_set(sdes, sdes_type,
		                  G_TYPE_STRING, g_value_get_string(&params[i].value),
		                  NULL);
	}

	g_object_set(G_OBJECT(priv->conference), "sdes", sdes, NULL);
	gst_structure_free(sdes);
#endif /* HAVE_FARSIGHT */
}
static gboolean
send_dtmf_callback(gpointer userdata)
{
	FsSession *session = userdata;

	fs_session_stop_telephony_event(session);

	return FALSE;
}
static gboolean
purple_media_backend_fs2_send_dtmf(PurpleMediaBackend *self,
		const gchar *sess_id, gchar dtmf, guint8 volume,
		guint16 duration)
{
	PurpleMediaBackendFs2Session *session;
	FsDTMFEvent event;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), FALSE);

	session = get_session(PURPLE_MEDIA_BACKEND_FS2(self), sess_id);
	if (session == NULL)
		return FALSE;

	/* Convert DTMF char into FsDTMFEvent enum */
	switch(dtmf) {
		case '0': event = FS_DTMF_EVENT_0; break;
		case '1': event = FS_DTMF_EVENT_1; break;
		case '2': event = FS_DTMF_EVENT_2; break;
		case '3': event = FS_DTMF_EVENT_3; break;
		case '4': event = FS_DTMF_EVENT_4; break;
		case '5': event = FS_DTMF_EVENT_5; break;
		case '6': event = FS_DTMF_EVENT_6; break;
		case '7': event = FS_DTMF_EVENT_7; break;
		case '8': event = FS_DTMF_EVENT_8; break;
		case '9': event = FS_DTMF_EVENT_9; break;
		case '*': event = FS_DTMF_EVENT_STAR; break;
		case '#': event = FS_DTMF_EVENT_POUND; break;
		case 'A': event = FS_DTMF_EVENT_A; break;
		case 'B': event = FS_DTMF_EVENT_B; break;
		case 'C': event = FS_DTMF_EVENT_C; break;
		case 'D': event = FS_DTMF_EVENT_D; break;
		default:
			return FALSE;
	}

	if (!fs_session_start_telephony_event(session->session,
			event, volume)) {
		return FALSE;
	}

	if (duration <= 50) {
		fs_session_stop_telephony_event(session->session);
	} else {
		purple_timeout_add(duration, send_dtmf_callback,
				session->session);
	}

	return TRUE;
}
#else
GType
purple_media_backend_fs2_get_type(void)
{
	return G_TYPE_NONE;
}
#endif /* USE_VV */

#ifdef USE_GSTREAMER
GstElement *
purple_media_backend_fs2_get_src(PurpleMediaBackendFs2 *self,
		const gchar *sess_id)
{
#ifdef USE_VV
	PurpleMediaBackendFs2Session *session = get_session(self, sess_id);
	return session != NULL ? session->src : NULL;
#else
	return NULL;
#endif
}

GstElement *
purple_media_backend_fs2_get_tee(PurpleMediaBackendFs2 *self,
		const gchar *sess_id, const gchar *who)
{
#ifdef USE_VV
	if (sess_id != NULL && who == NULL) {
		PurpleMediaBackendFs2Session *session =
				get_session(self, sess_id);
		return (session != NULL) ? session->tee : NULL;
	} else if (sess_id != NULL && who != NULL) {
		PurpleMediaBackendFs2Stream *stream =
				get_stream(self, sess_id, who);
		return (stream != NULL) ? stream->tee : NULL;
	}

#endif /* USE_VV */
	g_return_val_if_reached(NULL);
}

void
purple_media_backend_fs2_set_input_volume(PurpleMediaBackendFs2 *self,
		const gchar *sess_id, double level)
{
#ifdef USE_VV
	PurpleMediaBackendFs2Private *priv;
	GList *sessions;

	g_return_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self));

	priv = PURPLE_MEDIA_BACKEND_FS2_GET_PRIVATE(self);

	purple_prefs_set_int("/purple/media/audio/volume/input", level);

	if (sess_id == NULL)
		sessions = g_hash_table_get_values(priv->sessions);
	else
		sessions = g_list_append(NULL, get_session(self, sess_id));

	for (; sessions; sessions = g_list_delete_link(sessions, sessions)) {
		PurpleMediaBackendFs2Session *session = sessions->data;

		if (session->type & PURPLE_MEDIA_SEND_AUDIO) {
			gchar *name = g_strdup_printf("volume_%s",
					session->id);
			GstElement *volume = gst_bin_get_by_name(
					GST_BIN(priv->confbin), name);
			g_free(name);
			g_object_set(volume, "volume", level/10.0, NULL);
		}
	}
#endif /* USE_VV */
}

void
purple_media_backend_fs2_set_output_volume(PurpleMediaBackendFs2 *self,
		const gchar *sess_id, const gchar *who, double level)
{
#ifdef USE_VV
	GList *streams;

	g_return_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self));

	purple_prefs_set_int("/purple/media/audio/volume/output", level);

	streams = get_streams(self, sess_id, who);

	for (; streams; streams = g_list_delete_link(streams, streams)) {
		PurpleMediaBackendFs2Stream *stream = streams->data;

		if (stream->session->type & PURPLE_MEDIA_RECV_AUDIO
				&& GST_IS_ELEMENT(stream->volume)) {
			g_object_set(stream->volume, "volume",
					level/10.0, NULL);
		}
	}
#endif /* USE_VV */
}
#endif /* USE_GSTREAMER */

#ifdef USE_VV
static gboolean
purple_media_backend_fs2_set_send_rtcp_mux(PurpleMediaBackend *self,
		const gchar *sess_id, const gchar *participant,
		gboolean send_rtcp_mux)
{
	PurpleMediaBackendFs2Stream *stream;

	g_return_val_if_fail(PURPLE_IS_MEDIA_BACKEND_FS2(self), FALSE);
	stream = get_stream(PURPLE_MEDIA_BACKEND_FS2(self),
			sess_id, participant);

	if (stream != NULL &&
		g_object_class_find_property (G_OBJECT_GET_CLASS (stream->stream),
			"send-rtcp-mux") != NULL) {
		g_object_set (stream->stream, "send-rtcp-mux", send_rtcp_mux, NULL);
		return TRUE;
	}

	return FALSE;
}
#endif /* USE_VV */
