[Telepathy-commits] [telepathy-gabble/master] rename gabble-media-session.c to media-session.c

Dafydd Harries dafydd.harries at collabora.co.uk
Tue Aug 19 10:53:18 PDT 2008


20080714115835-c9803-d8d7f9467a1da82d7b731f9201be900ae78bbfa5.gz
---
 src/Makefile.am            |    2 +-
 src/gabble-media-session.c | 2938 --------------------------------------------
 src/media-session.c        | 2938 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 2939 insertions(+), 2939 deletions(-)
 delete mode 100644 src/gabble-media-session.c
 create mode 100644 src/media-session.c

diff --git a/src/Makefile.am b/src/Makefile.am
index e98ebd0..3a0ff48 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -60,7 +60,7 @@ libgabble_convenience_la_our_sources = \
     media-channel.h \
     media-channel.c \
     media-session.h \
-    gabble-media-session.c \
+    media-session.c \
     media-stream.h \
     gabble-media-stream.c \
     gabble-register.c \
diff --git a/src/gabble-media-session.c b/src/gabble-media-session.c
deleted file mode 100644
index 6001797..0000000
--- a/src/gabble-media-session.c
+++ /dev/null
@@ -1,2938 +0,0 @@
-/*
- * gabble-media-session.c - Source for GabbleMediaSession
- * Copyright (C) 2006 Collabora Ltd.
- * Copyright (C) 2006 Nokia Corporation
- *   @author Ole Andre Vadla Ravnaas <ole.andre.ravnaas at collabora.co.uk>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
- */
-
-#include "media-session.h"
-
-#include <dbus/dbus-glib.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-
-#define DEBUG_FLAG GABBLE_DEBUG_MEDIA
-
-#include <telepathy-glib/debug-ansi.h>
-#include "debug.h"
-#include "namespaces.h"
-#include "util.h"
-
-#include <telepathy-glib/dbus.h>
-#include <telepathy-glib/errors.h>
-#include <telepathy-glib/svc-media-interfaces.h>
-
-#include "connection.h"
-#include "media-channel.h"
-#include "media-stream.h"
-#include "presence-cache.h"
-#include "presence.h"
-
-#include "gabble-signals-marshal.h"
-
-static void session_handler_iface_init (gpointer, gpointer);
-
-G_DEFINE_TYPE_WITH_CODE(GabbleMediaSession,
-    gabble_media_session,
-    G_TYPE_OBJECT,
-    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_MEDIA_SESSION_HANDLER,
-      session_handler_iface_init)
-    )
-
-#define DEFAULT_SESSION_TIMEOUT 60000
-
-#define GTALK_STREAM_NAME "gtalk"
-
-/* 99 streams gives us a max name length of 8 (videoXX\0 or audioXX\0) */
-#define MAX_STREAMS 99
-#define MAX_STREAM_NAME_LEN 8
-
-/* signal enum */
-enum
-{
-    STREAM_ADDED,
-    TERMINATED,
-    LAST_SIGNAL
-};
-
-static guint signals[LAST_SIGNAL] = {0};
-
-/* properties */
-enum
-{
-  PROP_CONNECTION = 1,
-  PROP_MEDIA_CHANNEL,
-  PROP_OBJECT_PATH,
-  PROP_SESSION_ID,
-  PROP_INITIATOR,
-  PROP_PEER,
-  PROP_PEER_RESOURCE,
-  PROP_STATE,
-  LAST_PROPERTY
-};
-
-/* private structure */
-typedef struct _GabbleMediaSessionPrivate GabbleMediaSessionPrivate;
-
-struct _GabbleMediaSessionPrivate
-{
-  GabbleConnection *conn;
-  GabbleMediaChannel *channel;
-  GabbleMediaSessionMode mode;
-  gchar *object_path;
-
-  GPtrArray *streams;
-  GPtrArray *remove_requests;
-
-  gchar *id;
-  TpHandle peer;
-  gchar *peer_resource;
-
-  JingleSessionState state;
-  gboolean ready;
-  gboolean locally_accepted;
-  gboolean terminated;
-
-  guint timer_id;
-
-  gboolean dispose_has_run;
-};
-
-#define GABBLE_MEDIA_SESSION_GET_PRIVATE(obj) \
-    ((GabbleMediaSessionPrivate *)obj->priv)
-
-typedef struct {
-    gchar *name;
-    gchar *attributes;
-} SessionStateDescription;
-
-static const SessionStateDescription session_states[] =
-{
-    { "JS_STATE_PENDING_CREATED",
-      TP_ANSI_BOLD_ON TP_ANSI_FG_BLACK TP_ANSI_BG_WHITE   },
-    { "JS_STATE_PENDING_INITIATE_SENT",
-      TP_ANSI_BOLD_ON                  TP_ANSI_BG_CYAN    },
-    { "JS_STATE_PENDING_INITIATED",
-      TP_ANSI_BOLD_ON                  TP_ANSI_BG_MAGENTA },
-    { "JS_STATE_PENDING_ACCEPT_SENT",
-      TP_ANSI_BOLD_ON                  TP_ANSI_BG_CYAN    },
-    { "JS_STATE_ACTIVE",
-      TP_ANSI_BOLD_ON                  TP_ANSI_BG_BLUE    },
-    { "JS_STATE_ENDED",
-                                       TP_ANSI_BG_RED     }
-};
-
-static void
-gabble_media_session_init (GabbleMediaSession *self)
-{
-  GabbleMediaSessionPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
-      GABBLE_TYPE_MEDIA_SESSION, GabbleMediaSessionPrivate);
-
-  self->priv = priv;
-
-  priv->mode = MODE_JINGLE;
-  priv->state = JS_STATE_PENDING_CREATED;
-  priv->streams = g_ptr_array_new ();
-  priv->remove_requests = g_ptr_array_new ();
-}
-
-static void stream_connection_state_changed_cb (GabbleMediaStream *stream,
-                                                GParamSpec *param,
-                                                GabbleMediaSession *session);
-static void stream_got_local_codecs_changed_cb (GabbleMediaStream *stream,
-                                                GParamSpec *param,
-                                                GabbleMediaSession *session);
-
-static void
-_emit_new_stream (GabbleMediaSession *session,
-                  GabbleMediaStream *stream)
-{
-  gchar *object_path;
-  guint id, media_type;
-
-  g_object_get (stream,
-                "object-path", &object_path,
-                "id", &id,
-                "media-type", &media_type,
-                NULL);
-
-  /* all of the streams are bidirectional from farsight's point of view, it's
-   * just in the signalling they change */
-  tp_svc_media_session_handler_emit_new_stream_handler (session,
-      object_path, id, media_type, TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL);
-
-  g_free (object_path);
-}
-
-
-static GabbleMediaStream *
-_lookup_stream_by_name_and_initiator (GabbleMediaSession *session,
-                                      const gchar *stream_name,
-                                      JingleInitiator stream_initiator)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i;
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (tp_strdiff (stream->name, stream_name))
-        continue;
-
-      if (stream_initiator != INITIATOR_INVALID &&
-          stream_initiator != stream->initiator)
-        continue;
-
-      return stream;
-    }
-
-  return NULL;
-}
-
-
-static GabbleMediaStream *
-create_media_stream (GabbleMediaSession *session,
-                     const gchar *name,
-                     JingleInitiator initiator,
-                     guint media_type)
-{
-  GabbleMediaSessionPrivate *priv;
-  gchar *object_path;
-  GabbleMediaStream *stream;
-  guint id;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (session));
-  g_assert (name != NULL);
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  /* assert that if we're in google mode:
-   *  - we only try to make one stream
-   *  - it's an audio stream
-   *  - it's called GTALK_STREAM_NAME */
-  if (priv->mode == MODE_GOOGLE)
-    {
-      g_assert (priv->streams->len == 0);
-      g_assert (media_type == TP_MEDIA_STREAM_TYPE_AUDIO);
-      g_assert (!tp_strdiff (name, GTALK_STREAM_NAME));
-    }
-
-  g_assert (priv->streams->len < MAX_STREAMS);
-  g_assert (_lookup_stream_by_name_and_initiator (session, name, initiator) ==
-      NULL);
-
-  id = _gabble_media_channel_get_stream_id (priv->channel);
-
-  GMS_DEBUG_INFO (session,
-      "creating new %s %s stream called \"%s\" with id %u",
-      priv->mode == MODE_GOOGLE ? "google" : "jingle",
-      media_type == TP_MEDIA_STREAM_TYPE_AUDIO ? "audio" : "video",
-      name, id);
-
-  object_path = g_strdup_printf ("%s/MediaStream%u", priv->object_path, id);
-
-  stream = g_object_new (GABBLE_TYPE_MEDIA_STREAM,
-                         "connection", priv->conn,
-                         "media-session", session,
-                         "object-path", object_path,
-                         "mode", priv->mode,
-                         "name", name,
-                         "id", id,
-                         "initiator", initiator,
-                         "media-type", media_type,
-                         NULL);
-
-  g_signal_connect (stream, "notify::connection-state",
-                    (GCallback) stream_connection_state_changed_cb,
-                    session);
-  g_signal_connect (stream, "notify::got-local-codecs",
-                    (GCallback) stream_got_local_codecs_changed_cb,
-                    session);
-
-  g_ptr_array_add (priv->streams, stream);
-
-  g_free (object_path);
-
-  if (priv->ready)
-    _emit_new_stream (session, stream);
-
-  g_signal_emit (session, signals[STREAM_ADDED], 0, stream);
-
-  return stream;
-}
-
-static void
-destroy_media_stream (GabbleMediaSession *session,
-                      GabbleMediaStream *stream)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  _gabble_media_stream_close (stream);
-  g_ptr_array_remove_fast (priv->streams, stream);
-  g_object_unref (stream);
-}
-
-static GObject *
-gabble_media_session_constructor (GType type, guint n_props,
-                                  GObjectConstructParam *props)
-{
-  GObject *obj;
-  GabbleMediaSessionPrivate *priv;
-  DBusGConnection *bus;
-  TpHandleRepoIface *contact_handles;
-
-  obj = G_OBJECT_CLASS (gabble_media_session_parent_class)->
-           constructor (type, n_props, props);
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (GABBLE_MEDIA_SESSION (obj));
-
-  bus = tp_get_bus ();
-  dbus_g_connection_register_g_object (bus, priv->object_path, obj);
-
-  contact_handles = tp_base_connection_get_handles (
-      (TpBaseConnection *) priv->conn, TP_HANDLE_TYPE_CONTACT);
-
-  tp_handle_ref (contact_handles, priv->peer);
-
-  return obj;
-}
-
-static void
-gabble_media_session_get_property (GObject    *object,
-                                   guint       property_id,
-                                   GValue     *value,
-                                   GParamSpec *pspec)
-{
-  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  switch (property_id) {
-    case PROP_CONNECTION:
-      g_value_set_object (value, priv->conn);
-      break;
-    case PROP_MEDIA_CHANNEL:
-      g_value_set_object (value, priv->channel);
-      break;
-    case PROP_OBJECT_PATH:
-      g_value_set_string (value, priv->object_path);
-      break;
-    case PROP_SESSION_ID:
-      g_value_set_string (value, priv->id);
-      break;
-    case PROP_INITIATOR:
-      g_value_set_uint (value, session->initiator);
-      break;
-    case PROP_PEER:
-      g_value_set_uint (value, priv->peer);
-      break;
-    case PROP_PEER_RESOURCE:
-      g_value_set_string (value, priv->peer_resource);
-      break;
-    case PROP_STATE:
-      g_value_set_uint (value, priv->state);
-      break;
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
-      break;
-  }
-}
-
-static void session_state_changed (GabbleMediaSession *session,
-                                   JingleSessionState prev_state,
-                                   JingleSessionState new_state);
-
-static void
-gabble_media_session_set_property (GObject      *object,
-                                   guint         property_id,
-                                   const GValue *value,
-                                   GParamSpec   *pspec)
-{
-  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  JingleSessionState prev_state;
-
-  switch (property_id) {
-    case PROP_CONNECTION:
-      priv->conn = g_value_get_object (value);
-      break;
-    case PROP_MEDIA_CHANNEL:
-      priv->channel = g_value_get_object (value);
-      break;
-    case PROP_OBJECT_PATH:
-      g_free (priv->object_path);
-      priv->object_path = g_value_dup_string (value);
-      break;
-    case PROP_SESSION_ID:
-      g_free (priv->id);
-      priv->id = g_value_dup_string (value);
-      break;
-    case PROP_INITIATOR:
-      session->initiator = g_value_get_uint (value);
-      break;
-    case PROP_PEER:
-      priv->peer = g_value_get_uint (value);
-      break;
-    case PROP_PEER_RESOURCE:
-      g_free (priv->peer_resource);
-      priv->peer_resource = g_value_dup_string (value);
-      break;
-    case PROP_STATE:
-      prev_state = priv->state;
-      priv->state = g_value_get_uint (value);
-
-      if (priv->state == JS_STATE_ENDED)
-        g_assert (priv->terminated);
-
-      if (priv->state != prev_state)
-        session_state_changed (session, prev_state, priv->state);
-
-      break;
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
-      break;
-  }
-}
-
-static void gabble_media_session_dispose (GObject *object);
-static void gabble_media_session_finalize (GObject *object);
-
-static void
-gabble_media_session_class_init (GabbleMediaSessionClass *gabble_media_session_class)
-{
-  GObjectClass *object_class = G_OBJECT_CLASS (gabble_media_session_class);
-  GParamSpec *param_spec;
-
-  g_type_class_add_private (gabble_media_session_class,
-      sizeof (GabbleMediaSessionPrivate));
-
-  object_class->constructor = gabble_media_session_constructor;
-
-  object_class->get_property = gabble_media_session_get_property;
-  object_class->set_property = gabble_media_session_set_property;
-
-  object_class->dispose = gabble_media_session_dispose;
-  object_class->finalize = gabble_media_session_finalize;
-
-  param_spec = g_param_spec_object ("connection", "GabbleConnection object",
-                                    "Gabble connection object that owns this "
-                                    "media session's channel.",
-                                    GABBLE_TYPE_CONNECTION,
-                                    G_PARAM_CONSTRUCT_ONLY |
-                                    G_PARAM_READWRITE |
-                                    G_PARAM_STATIC_NICK |
-                                    G_PARAM_STATIC_BLURB);
-  g_object_class_install_property (object_class, PROP_CONNECTION, param_spec);
-
-  param_spec = g_param_spec_object ("media-channel",
-      "GabbleMediaChannel object",
-      "Gabble media channel object that owns this media session object.",
-      GABBLE_TYPE_MEDIA_CHANNEL,
-      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NICK |
-      G_PARAM_STATIC_BLURB);
-  g_object_class_install_property (object_class, PROP_MEDIA_CHANNEL,
-      param_spec);
-
-  param_spec = g_param_spec_string ("object-path", "D-Bus object path",
-                                    "The D-Bus object path used for this "
-                                    "object on the bus.",
-                                    NULL,
-                                    G_PARAM_CONSTRUCT_ONLY |
-                                    G_PARAM_READWRITE |
-                                    G_PARAM_STATIC_NAME |
-                                    G_PARAM_STATIC_BLURB);
-  g_object_class_install_property (object_class, PROP_OBJECT_PATH, param_spec);
-
-  param_spec = g_param_spec_string ("session-id", "Session ID",
-                                    "A unique session identifier used "
-                                    "throughout all communication.",
-                                    NULL,
-                                    G_PARAM_CONSTRUCT_ONLY |
-                                    G_PARAM_READWRITE |
-                                    G_PARAM_STATIC_NAME |
-                                    G_PARAM_STATIC_BLURB);
-  g_object_class_install_property (object_class, PROP_SESSION_ID, param_spec);
-
-  param_spec = g_param_spec_uint ("initiator", "Session initiator",
-                                  "An enum signifying which end initiated "
-                                  "the session.",
-                                  INITIATOR_LOCAL,
-                                  INITIATOR_REMOTE,
-                                  INITIATOR_LOCAL,
-                                  G_PARAM_CONSTRUCT_ONLY |
-                                  G_PARAM_READWRITE |
-                                  G_PARAM_STATIC_NAME |
-                                  G_PARAM_STATIC_BLURB);
-  g_object_class_install_property (object_class, PROP_INITIATOR, param_spec);
-
-  param_spec = g_param_spec_uint ("peer", "Session peer",
-                                  "The TpHandle representing the contact "
-                                  "with whom this session communicates.",
-                                  0, G_MAXUINT32, 0,
-                                  G_PARAM_CONSTRUCT_ONLY |
-                                  G_PARAM_READWRITE |
-                                  G_PARAM_STATIC_NAME |
-                                  G_PARAM_STATIC_BLURB);
-  g_object_class_install_property (object_class, PROP_PEER, param_spec);
-
-  param_spec = g_param_spec_string ("peer-resource",
-                                    "Session peer's resource",
-                                    "The resource of the contact "
-                                    "with whom this session communicates, "
-                                    "if applicable",
-                                    NULL,
-                                    G_PARAM_CONSTRUCT_ONLY |
-                                    G_PARAM_WRITABLE |
-                                    G_PARAM_STATIC_NAME |
-                                    G_PARAM_STATIC_BLURB);
-  g_object_class_install_property (object_class, PROP_PEER_RESOURCE,
-                                   param_spec);
-
-  param_spec = g_param_spec_uint ("state", "Session state",
-                                  "The current state that the session is in.",
-                                  0, G_MAXUINT32, 0,
-                                  G_PARAM_READWRITE |
-                                  G_PARAM_STATIC_NAME |
-                                  G_PARAM_STATIC_BLURB);
-  g_object_class_install_property (object_class, PROP_STATE, param_spec);
-
-  signals[STREAM_ADDED] =
-    g_signal_new ("stream-added",
-                  G_OBJECT_CLASS_TYPE (gabble_media_session_class),
-                  G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
-                  0,
-                  NULL, NULL,
-                  g_cclosure_marshal_VOID__OBJECT,
-                  G_TYPE_NONE, 1, G_TYPE_OBJECT);
-
-  signals[TERMINATED] =
-    g_signal_new ("terminated",
-                  G_OBJECT_CLASS_TYPE (gabble_media_session_class),
-                  G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
-                  0,
-                  NULL, NULL,
-                  gabble_marshal_VOID__UINT_UINT,
-                  G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT);
-}
-
-static void
-gabble_media_session_dispose (GObject *object)
-{
-  GabbleMediaSession *self = GABBLE_MEDIA_SESSION (object);
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self);
-  guint i;
-  TpHandleRepoIface *contact_handles;
-
-  DEBUG ("called");
-
-  if (priv->dispose_has_run)
-    return;
-
-  priv->dispose_has_run = TRUE;
-
-  _gabble_media_session_terminate (self, INITIATOR_LOCAL,
-      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
-
-  if (priv->timer_id != 0)
-    g_source_remove (priv->timer_id);
-
-  if (priv->streams != NULL)
-    {
-      for (i = 0; i < priv->streams->len; i++)
-        g_object_unref (g_ptr_array_index (priv->streams, i));
-      g_ptr_array_free (priv->streams, TRUE);
-      priv->streams = NULL;
-    }
-
-  for (i = 0; i < priv->remove_requests->len; i++)
-    g_ptr_array_free (g_ptr_array_index (priv->remove_requests, i), TRUE);
-  g_ptr_array_free (priv->remove_requests, TRUE);
-  priv->remove_requests = NULL;
-
-  contact_handles = tp_base_connection_get_handles (
-      (TpBaseConnection *) priv->conn, TP_HANDLE_TYPE_CONTACT);
-
-  tp_handle_unref (contact_handles, priv->peer);
-
-  if (G_OBJECT_CLASS (gabble_media_session_parent_class)->dispose)
-    G_OBJECT_CLASS (gabble_media_session_parent_class)->dispose (object);
-}
-
-static void
-gabble_media_session_finalize (GObject *object)
-{
-  GabbleMediaSession *self = GABBLE_MEDIA_SESSION (object);
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self);
-
-  g_free (priv->id);
-  g_free (priv->object_path);
-  g_free (priv->peer_resource);
-  G_OBJECT_CLASS (gabble_media_session_parent_class)->finalize (object);
-}
-
-
-/**
- * gabble_media_session_error
- *
- * Implements D-Bus method Error
- * on interface org.freedesktop.Telepathy.Media.SessionHandler
- */
-static void
-gabble_media_session_error (TpSvcMediaSessionHandler *iface,
-                            guint errno,
-                            const gchar *message,
-                            DBusGMethodInvocation *context)
-{
-  GabbleMediaSession *self = GABBLE_MEDIA_SESSION (iface);
-  GabbleMediaSessionPrivate *priv;
-  GPtrArray *tmp;
-  guint i;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (self));
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self);
-
-  GMS_DEBUG_INFO (self, "Media.SessionHandler::Error called, error %u (%s) -- "
-      "emitting error on each stream", errno, message);
-
-  if (priv->state == JS_STATE_ENDED)
-    {
-      tp_svc_media_session_handler_return_from_error (context);
-      return;
-    }
-  else if (priv->state == JS_STATE_PENDING_CREATED)
-    {
-      /* shortcut to prevent sending remove actions if we haven't sent an
-       * initiate yet */
-      g_object_set (self, "state", JS_STATE_ENDED, NULL);
-      tp_svc_media_session_handler_return_from_error (context);
-      return;
-    }
-
-  g_assert (priv->streams != NULL);
-
-  tmp = priv->streams;
-  priv->streams = NULL;
-
-  for (i = 0; i < tmp->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (tmp, i);
-
-      gabble_media_stream_error (stream, errno, message, NULL);
-    }
-
-  g_ptr_array_free (tmp, TRUE);
-
-  tp_svc_media_session_handler_return_from_error (context);
-}
-
-
-/**
- * gabble_media_session_ready
- *
- * Implements D-Bus method Ready
- * on interface org.freedesktop.Telepathy.Media.SessionHandler
- */
-static void
-gabble_media_session_ready (TpSvcMediaSessionHandler *iface,
-                            DBusGMethodInvocation *context)
-{
-  GabbleMediaSession *self = GABBLE_MEDIA_SESSION (iface);
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self);
-
-  if (!priv->ready)
-    {
-      guint i;
-
-      priv->ready = TRUE;
-
-      for (i = 0; i < priv->streams->len; i++)
-        _emit_new_stream (self, g_ptr_array_index (priv->streams, i));
-    }
-
-  tp_svc_media_session_handler_return_from_ready (context);
-}
-
-
-static gboolean
-_handle_create (GabbleMediaSession *session,
-                LmMessage *message,
-                LmMessageNode *content_node,
-                const gchar *stream_name,
-                GabbleMediaStream *stream,
-                LmMessageNode *desc_node,
-                LmMessageNode *trans_node,
-                GError **error)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  GabbleMediaSessionMode session_mode;
-  TpMediaStreamType stream_type;
-  gboolean override_existing = FALSE;
-
-  if ((priv->state == JS_STATE_PENDING_CREATED) &&
-      (session->initiator == INITIATOR_LOCAL))
-    {
-      DEBUG ("we're trying to call ourselves, rejecting with busy");
-      _gabble_media_session_terminate (session, INITIATOR_REMOTE,
-          TP_CHANNEL_GROUP_CHANGE_REASON_BUSY);
-          return FALSE;
-    }
-
-
-  if (stream != NULL)
-    {
-      /* streams added by the session initiator may replace similarly-named
-       * streams which we are trying to add (but havn't had acknowledged) */
-      if (stream->signalling_state < STREAM_SIG_STATE_ACKNOWLEDGED)
-        {
-          if (session->initiator == INITIATOR_REMOTE)
-            {
-              override_existing = TRUE;
-            }
-          else
-            {
-              g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_CONFLICT,
-                  "session initiator is creating a stream named \"%s\" "
-                  "already", stream_name);
-              return FALSE;
-            }
-        }
-      else
-        {
-          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_CONFLICT,
-              "can't create new stream called \"%s\", it already exists, "
-              "rejecting", stream_name);
-          return FALSE;
-        }
-    }
-
-  if (desc_node == NULL)
-    {
-      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
-          "unable to create stream without a content description");
-      return FALSE;
-    }
-
-  if (lm_message_node_has_namespace (desc_node,
-        NS_GOOGLE_SESSION_PHONE, NULL))
-    {
-      session_mode = MODE_GOOGLE;
-      stream_type = TP_MEDIA_STREAM_TYPE_AUDIO;
-    }
-  else if (lm_message_node_has_namespace (desc_node,
-        NS_JINGLE_DESCRIPTION_AUDIO, NULL))
-    {
-      session_mode = MODE_JINGLE;
-      stream_type = TP_MEDIA_STREAM_TYPE_AUDIO;
-    }
-  else if (lm_message_node_has_namespace (desc_node,
-        NS_JINGLE_DESCRIPTION_VIDEO, NULL))
-    {
-      session_mode = MODE_JINGLE;
-      stream_type = TP_MEDIA_STREAM_TYPE_VIDEO;
-    }
-  else
-    {
-      g_set_error (error, GABBLE_XMPP_ERROR,
-          XMPP_ERROR_JINGLE_UNSUPPORTED_CONTENT,
-          "refusing to create stream for unsupported content description");
-      return FALSE;
-    }
-
-  /* MODE_GOOGLE is allowed to have a null transport node */
-  if (session_mode == MODE_JINGLE && trans_node == NULL)
-    {
-      g_set_error (error, GABBLE_XMPP_ERROR,
-          XMPP_ERROR_JINGLE_UNSUPPORTED_TRANSPORT,
-          "refusing to create stream for unsupported transport");
-      return FALSE;
-    }
-
-  if (session_mode != priv->mode)
-    {
-      if (priv->streams->len > 0)
-        {
-          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_UNEXPECTED_REQUEST,
-              "refusing to change mode because streams already exist");
-          return FALSE;
-        }
-      else
-        {
-          GMS_DEBUG_INFO (session, "setting session mode to %s",
-              session_mode == MODE_GOOGLE ? "google" : "jingle");
-          priv->mode = session_mode;
-        }
-    }
-
-  if (override_existing)
-    {
-      GMS_DEBUG_INFO (session, "removing our unacknowledged stream \"%s\" "
-          "in favour of the session initiator's", stream_name);
-
-      /* disappear this stream */
-      destroy_media_stream (session, stream);
-
-      stream = NULL;
-    }
-
-  if (priv->streams->len == MAX_STREAMS)
-    {
-      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_RESOURCE_CONSTRAINT,
-          "refusing to create more than " G_STRINGIFY (MAX_STREAMS)
-          " streams");
-      return FALSE;
-    }
-
-  stream = create_media_stream (session, stream_name, INITIATOR_REMOTE,
-      stream_type);
-
-  /* set the signalling state to ACKNOWLEDGED */
-  g_object_set (stream,
-      "signalling-state", STREAM_SIG_STATE_ACKNOWLEDGED,
-      NULL);
-
-  /* for jingle streams, set the direction to none, so that the
-   * direction handler adds the right flags */
-  if (priv->mode == MODE_JINGLE)
-    g_object_set (stream,
-        "combined-direction", TP_MEDIA_STREAM_DIRECTION_NONE,
-        NULL);
-
-  return TRUE;
-}
-
-
-static TpMediaStreamDirection
-_senders_to_direction (GabbleMediaSession *session,
-                       const gchar *senders)
-{
-  TpMediaStreamDirection ret = TP_MEDIA_STREAM_DIRECTION_NONE;
-
-  if (!tp_strdiff (senders, "initiator"))
-    {
-      if (session->initiator == INITIATOR_LOCAL)
-        ret = TP_MEDIA_STREAM_DIRECTION_SEND;
-      else
-        ret = TP_MEDIA_STREAM_DIRECTION_RECEIVE;
-    }
-  else if (!tp_strdiff (senders, "responder"))
-    {
-      if (session->initiator == INITIATOR_REMOTE)
-        ret = TP_MEDIA_STREAM_DIRECTION_SEND;
-      else
-        ret = TP_MEDIA_STREAM_DIRECTION_RECEIVE;
-    }
-  else if (!tp_strdiff (senders, "both"))
-    {
-      ret = TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL;
-    }
-
-  return ret;
-}
-
-static gboolean
-_handle_direction (GabbleMediaSession *session,
-                   LmMessage *message,
-                   LmMessageNode *content_node,
-                   const gchar *stream_name,
-                   GabbleMediaStream *stream,
-                   LmMessageNode *desc_node,
-                   LmMessageNode *trans_node,
-                   GError **error)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  const gchar *senders;
-  CombinedStreamDirection new_combined_dir;
-  TpMediaStreamDirection requested_dir, current_dir;
-  TpMediaStreamPendingSend pending_send;
-
-  if (priv->mode == MODE_GOOGLE)
-    return TRUE;
-
-  requested_dir = TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL;
-
-  senders = lm_message_node_get_attribute (content_node, "senders");
-  if (senders != NULL)
-    requested_dir = _senders_to_direction (session, senders);
-
-  if (requested_dir == TP_MEDIA_STREAM_DIRECTION_NONE)
-    {
-      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
-          "received invalid content senders value \"%s\" on stream \"%s\"; "
-          "rejecting", senders, stream_name);
-      return FALSE;
-    }
-
-  current_dir = COMBINED_DIRECTION_GET_DIRECTION (stream->combined_direction);
-  pending_send = COMBINED_DIRECTION_GET_PENDING_SEND
-    (stream->combined_direction);
-
-  GMS_DEBUG_INFO (session, "received request for senders \"%s\" on stream "
-      "\"%s\"", senders, stream_name);
-
-  /* if local sending has been added, remove it,
-   * and set the pending local send flag */
-  if (((current_dir & TP_MEDIA_STREAM_DIRECTION_SEND) == 0) &&
-    ((requested_dir & TP_MEDIA_STREAM_DIRECTION_SEND) != 0))
-    {
-      GMS_DEBUG_INFO (session, "setting pending local send flag");
-      requested_dir &= ~TP_MEDIA_STREAM_DIRECTION_SEND;
-      pending_send |= TP_MEDIA_STREAM_PENDING_LOCAL_SEND;
-    }
-
-#if 0
-  /* clear any pending remote send */
-  if ((pending_send & TP_MEDIA_STREAM_PENDING_REMOTE_SEND) != 0)
-    {
-      GMS_DEBUG_INFO (session, "setting pending local send flag");
-      pending_send &= ~TP_MEDIA_STREAM_PENDING_REMOTE_SEND;
-    }
-#endif
-
-  /* make any necessary changes */
-  new_combined_dir = MAKE_COMBINED_DIRECTION (requested_dir, pending_send);
-  if (new_combined_dir != stream->combined_direction)
-    {
-      g_object_set (stream, "combined-direction", new_combined_dir, NULL);
-      _gabble_media_stream_update_sending (stream, FALSE);
-    }
-
-  return TRUE;
-}
-
-
-static gboolean
-_handle_accept (GabbleMediaSession *session,
-                LmMessage *message,
-                LmMessageNode *content_node,
-                const gchar *stream_name,
-                GabbleMediaStream *stream,
-                LmMessageNode *desc_node,
-                LmMessageNode *trans_node,
-                GError **error)
-{
-  g_object_set (stream, "playing", TRUE, NULL);
-
-  _gabble_media_stream_update_sending (stream, TRUE);
-
-  return TRUE;
-}
-
-
-static gboolean
-_handle_codecs (GabbleMediaSession *session,
-                LmMessage *message,
-                LmMessageNode *content_node,
-                const gchar *stream_name,
-                GabbleMediaStream *stream,
-                LmMessageNode *desc_node,
-                LmMessageNode *trans_node,
-                GError **error)
-{
-  if (desc_node == NULL)
-    {
-      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
-          "unable to handle codecs without a content description node");
-      return FALSE;
-    }
-
-  if (!_gabble_media_stream_post_remote_codecs (stream, message, desc_node,
-        error))
-    return FALSE;
-
-  return TRUE;
-}
-
-
-static gboolean
-_handle_candidates (GabbleMediaSession *session,
-                    LmMessage *message,
-                    LmMessageNode *content_node,
-                    const gchar *stream_name,
-                    GabbleMediaStream *stream,
-                    LmMessageNode *desc_node,
-                    LmMessageNode *trans_node,
-                    GError **error)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  if (trans_node == NULL)
-    {
-      if (priv->mode == MODE_GOOGLE)
-        {
-          trans_node = content_node;
-        }
-      else
-        {
-          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
-              "unable to handle candidates without a transport node");
-          return FALSE;
-        }
-    }
-
-  if (!_gabble_media_stream_post_remote_candidates (stream, message,
-        trans_node, error))
-    return FALSE;
-
-  return TRUE;
-}
-
-static guint
-_count_non_removing_streams (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i, ret = 0;
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (stream->signalling_state < STREAM_SIG_STATE_REMOVING)
-        ret++;
-    }
-
-  return ret;
-}
-
-static gboolean
-_handle_remove (GabbleMediaSession *session,
-                LmMessage *message,
-                LmMessageNode *content_node,
-                const gchar *stream_name,
-                GabbleMediaStream *stream,
-                LmMessageNode *desc_node,
-                LmMessageNode *trans_node,
-                GError **error)
-{
-  /* reducing a session to contain 0 streams is invalid; instead the peer
-   * should terminate the session. I guess we'll do it for them... */
-  if (_count_non_removing_streams (session) == 1)
-    {
-      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
-          "unable to remove the last stream in a Jingle call");
-      return FALSE;
-    }
-
-  /* close the stream */
-  destroy_media_stream (session, stream);
-
-  return TRUE;
-}
-
-
-static gboolean
-_handle_terminate (GabbleMediaSession *session,
-                   LmMessage *message,
-                   LmMessageNode *content_node,
-                   const gchar *stream_name,
-                   GabbleMediaStream *stream,
-                   LmMessageNode *desc_node,
-                   LmMessageNode *trans_node,
-                   GError **error)
-{
-  DEBUG ("called for %s", stream_name);
-
-  _gabble_media_session_terminate (session, INITIATOR_REMOTE,
-      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
-
-  return TRUE;
-}
-
-
-typedef gboolean (*StreamHandlerFunc)(GabbleMediaSession *session,
-                                      LmMessage *message,
-                                      LmMessageNode *content_node,
-                                      const gchar *stream_name,
-                                      GabbleMediaStream *stream,
-                                      LmMessageNode *desc_node,
-                                      LmMessageNode *trans_node,
-                                      GError **error);
-
-typedef struct _Handler Handler;
-
-struct _Handler {
-  const gchar *actions[3];
-  JingleSessionState min_allowed_state;
-  JingleSessionState max_allowed_state;
-  StreamHandlerFunc stream_handlers[4];
-  JingleSessionState new_state;
-};
-
-static Handler handlers[] = {
-  {
-    { "initiate", "session-initiate", NULL },
-    JS_STATE_PENDING_CREATED,
-    JS_STATE_PENDING_CREATED,
-    { _handle_create, _handle_direction, _handle_codecs, NULL },
-    JS_STATE_PENDING_INITIATED
-  },
-  {
-    { "accept", "session-accept", NULL },
-    JS_STATE_PENDING_INITIATED,
-    JS_STATE_PENDING_INITIATED,
-    { _handle_direction, _handle_codecs, _handle_accept, NULL },
-    JS_STATE_ACTIVE
-  },
-  {
-    { "reject", NULL },
-    JS_STATE_PENDING_INITIATE_SENT,
-    JS_STATE_PENDING_INITIATED,
-    { _handle_terminate, NULL },
-    JS_STATE_INVALID
-  },
-  {
-    { "terminate", "session-terminate", NULL },
-    JS_STATE_PENDING_INITIATED,
-    JS_STATE_ENDED,
-    { _handle_terminate, NULL },
-    JS_STATE_INVALID
-  },
-  {
-    { "candidates", "transport-info", NULL },
-    JS_STATE_PENDING_INITIATED,
-    JS_STATE_ACTIVE,
-    { _handle_candidates, NULL },
-    JS_STATE_INVALID
-  },
-  {
-    { "content-add", NULL },
-    JS_STATE_ACTIVE,
-    JS_STATE_ACTIVE,
-    { _handle_create, _handle_direction, _handle_codecs, NULL },
-    JS_STATE_INVALID,
-  },
-  {
-    { "content-modify", NULL },
-    JS_STATE_PENDING_INITIATED,
-    JS_STATE_ACTIVE,
-    { _handle_direction, NULL },
-    JS_STATE_INVALID
-  },
-  {
-    { "content-accept", NULL },
-    JS_STATE_PENDING_INITIATED,
-    JS_STATE_ACTIVE,
-    { _handle_direction, _handle_codecs, _handle_accept, NULL },
-    JS_STATE_INVALID
-  },
-  {
-    { "content-remove", "content-decline", NULL },
-    JS_STATE_PENDING_INITIATED,
-    JS_STATE_ACTIVE,
-    { _handle_remove, NULL },
-    JS_STATE_INVALID
-  },
-  {
-    { NULL },
-    JS_STATE_INVALID,
-    JS_STATE_INVALID,
-    { NULL },
-    JS_STATE_INVALID
-  }
-};
-
-
-static gboolean
-_call_handlers_on_stream (GabbleMediaSession *session,
-                          LmMessage *message,
-                          LmMessageNode *content_node,
-                          const gchar *stream_name,
-                          JingleInitiator stream_creator,
-                          StreamHandlerFunc *func,
-                          GError **error)
-{
-  GabbleMediaStream *stream = NULL;
-  LmMessageNode *desc_node = NULL, *trans_node = NULL;
-  StreamHandlerFunc *tmp;
-  gboolean stream_created = FALSE;
-
-  if (content_node != NULL)
-    {
-      desc_node = lm_message_node_get_child (content_node, "description");
-
-      trans_node = lm_message_node_get_child_with_namespace (content_node,
-          "transport", NS_GOOGLE_TRANSPORT_P2P);
-    }
-
-  for (tmp = func; *tmp != NULL; tmp++)
-    {
-      /* handlers may create the stream */
-      if (stream == NULL && stream_name != NULL)
-        stream = _lookup_stream_by_name_and_initiator (session, stream_name,
-            stream_creator);
-
-      /* the create handler is able to check whether or not the stream
-       * exists, and act accordingly (sometimes it will replace an existing
-       * stream, sometimes it will reject). the termination handler
-       * also requires no stream to do it's job. */
-      if (*tmp != _handle_create && *tmp != _handle_terminate)
-        {
-          /* all other handlers require the stream to exist */
-          if (stream == NULL)
-            {
-              const gchar *created = "";
-
-              if (stream_creator == INITIATOR_LOCAL)
-                created = "locally-created ";
-              else if (stream_creator == INITIATOR_REMOTE)
-                created = "remotely-created ";
-
-              g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_ITEM_NOT_FOUND,
-                  "unable to handle action for unknown %sstream \"%s\" ",
-                  created, stream_name);
-
-              return FALSE;
-            }
-          else
-            {
-              /* don't do anything with actions on streams which have not been
-               * acknowledged, or that we're trying to remove, to deal with
-               * adding/removing race conditions (actions sent by the other end
-               * before they're aware that we've added or removed a stream) */
-              if (stream->signalling_state != STREAM_SIG_STATE_ACKNOWLEDGED)
-                {
-                  GMS_DEBUG_WARNING (session, "ignoring action because stream "
-                      "%s is in state %d, not ACKNOWLEDGED", stream->name,
-                      stream->signalling_state);
-                  return TRUE;
-                }
-            }
-        }
-
-      if (!(*tmp) (session, message, content_node, stream_name, stream,
-            desc_node, trans_node, error))
-        {
-          /* if we successfully created the stream but failed to do something
-           * with it later, remove it */
-          if (stream_created)
-            destroy_media_stream (session, stream);
-
-          return FALSE;
-        }
-
-      if (*tmp == _handle_create)
-        {
-          stream_created = TRUE;
-          /* force a stream lookup after the create handler, even if we
-           * already had one (it has replacement semantics in certain
-           * situations) */
-          stream = NULL;
-        }
-    }
-
-  return TRUE;
-}
-
-
-static JingleInitiator
-_creator_to_initiator (GabbleMediaSession *session, const gchar *creator)
-{
-  if (!tp_strdiff (creator, "initiator"))
-    {
-      if (session->initiator == INITIATOR_LOCAL)
-        return INITIATOR_LOCAL;
-      else
-        return INITIATOR_REMOTE;
-    }
-  else if (!tp_strdiff (creator, "responder"))
-    {
-      if (session->initiator == INITIATOR_LOCAL)
-        return INITIATOR_REMOTE;
-      else
-        return INITIATOR_LOCAL;
-    }
-  else
-    return INITIATOR_INVALID;
-}
-
-
-static gboolean
-_call_handlers_on_streams (GabbleMediaSession *session,
-                           LmMessage *message,
-                           LmMessageNode *session_node,
-                           StreamHandlerFunc *func,
-                           GError **error)
-{
-  LmMessageNode *content_node;
-
-  if (lm_message_node_has_namespace (session_node, NS_GOOGLE_SESSION, NULL))
-    return _call_handlers_on_stream (session, message, session_node,
-        GTALK_STREAM_NAME, INITIATOR_INVALID, func, error);
-
-  if (session_node->children == NULL)
-    return _call_handlers_on_stream (session, message, NULL, NULL,
-        INITIATOR_INVALID, func, error);
-
-  for (content_node = session_node->children;
-       NULL != content_node;
-       content_node = content_node->next)
-    {
-      const gchar *stream_name, *stream_creator;
-      JingleInitiator stream_initiator;
-
-      if (tp_strdiff (content_node->name, "content"))
-        continue;
-
-      stream_name = lm_message_node_get_attribute (content_node, "name");
-
-      if (stream_name == NULL)
-        {
-          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
-              "rejecting content node with no name");
-          return FALSE;
-        }
-
-      stream_creator = lm_message_node_get_attribute (content_node, "creator");
-      stream_initiator = _creator_to_initiator (session, stream_creator);
-
-      /* we allow NULL creator to mean INITIATOR_INVALID for backwards
-       * compatibility with clients that don't put a creator attribute in */
-      if (stream_creator != NULL && stream_initiator == INITIATOR_INVALID)
-        {
-          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
-              "rejecting content node with invalid creators value");
-          return FALSE;
-        }
-
-      if (!_call_handlers_on_stream (session, message, content_node,
-            stream_name, stream_initiator, func, error))
-        return FALSE;
-    }
-
-  return TRUE;
-}
-
-
-gboolean
-_gabble_media_session_handle_action (GabbleMediaSession *session,
-                                     LmMessage *message,
-                                     LmMessageNode *session_node,
-                                     const gchar *action,
-                                     GError **error)
-{
-  GabbleMediaSessionPrivate *priv;
-  StreamHandlerFunc *funcs = NULL;
-  JingleSessionState new_state = JS_STATE_INVALID;
-  Handler *i;
-  const gchar **tmp;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (session));
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  GMS_DEBUG_INFO (session, "got jingle session action \"%s\" from peer",
-      action);
-
-  /* do the state machine dance */
-
-  /* search the table of handlers for the action */
-  for (i = handlers; NULL != i->actions[0]; i++)
-    {
-      for (tmp = i->actions; NULL != *tmp; tmp++)
-        if (0 == strcmp (*tmp, action))
-          break;
-
-      if (NULL == *tmp)
-        continue;
-
-      /* if we're outside the allowable states for this action, return an error
-       * immediately */
-      if (priv->state < i->min_allowed_state ||
-          priv->state > i->max_allowed_state)
-        {
-          g_set_error (error, GABBLE_XMPP_ERROR,
-              XMPP_ERROR_JINGLE_OUT_OF_ORDER,
-              "action \"%s\" not allowed in current state", action);
-          goto ERROR;
-        }
-
-      funcs = i->stream_handlers;
-      new_state = i->new_state;
-
-      break;
-    }
-
-  /* pointer is not NULL if we found a matching action */
-  if (NULL == funcs)
-    {
-      g_set_error (error, GABBLE_XMPP_ERROR,
-          XMPP_ERROR_FEATURE_NOT_IMPLEMENTED, "action \"%s\" not implemented",
-          action);
-      goto ERROR;
-    }
-
-  /* call handlers if there are any (NULL-terminated array) */
-  if (NULL != *funcs)
-    {
-      if (!_call_handlers_on_streams (session, message, session_node, funcs,
-            error))
-        {
-          if (*error == NULL)
-            g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
-                "unknown error encountered with action \"%s\"",
-                action);
-
-          goto ERROR;
-        }
-    }
-
-  /* acknowledge the IQ before changing the state because the new state
-   * could perform some actions which the other end will only accept
-   * if this action has been acknowledged */
-  _gabble_connection_acknowledge_set_iq (priv->conn, message);
-
-  /* if the action specified a new state to go to, set it */
-  if (JS_STATE_INVALID != new_state)
-    g_object_set (session, "state", new_state, NULL);
-
-  return TRUE;
-
-ERROR:
-  g_assert (error != NULL);
-  GMS_DEBUG_ERROR (session, (*error)->message);
-  return FALSE;
-}
-
-static gboolean
-timeout_session (gpointer data)
-{
-  GabbleMediaSession *session = data;
-
-  DEBUG ("session timed out");
-
-  if (session->initiator == INITIATOR_LOCAL)
-      _gabble_media_session_terminate (session, INITIATOR_LOCAL,
-          TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER);
-  else
-      _gabble_media_session_terminate (session, INITIATOR_LOCAL,
-          TP_CHANNEL_GROUP_CHANGE_REASON_ERROR);
-
-  return FALSE;
-}
-
-static void do_content_add (GabbleMediaSession *, GabbleMediaStream *);
-
-static void
-_add_ready_new_streams (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i;
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      GMS_DEBUG_DUMP (session, "pondering accept-time add for stream: %s, got "
-          "local codecs: %s, initiator: %s, signalling state: %d",
-          stream->name, stream->got_local_codecs ? "true" : "false",
-          stream->initiator == INITIATOR_LOCAL ? "local" : "remote",
-          stream->signalling_state);
-
-      if (stream->got_local_codecs == FALSE)
-        continue;
-
-      if (stream->initiator == INITIATOR_REMOTE)
-        continue;
-
-      if (stream->signalling_state > STREAM_SIG_STATE_NEW)
-        continue;
-
-      do_content_add (session, stream);
-    }
-}
-
-static void
-session_state_changed (GabbleMediaSession *session,
-                       JingleSessionState prev_state,
-                       JingleSessionState new_state)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  GMS_DEBUG_EVENT (session, "state changed from %s to %s",
-                   session_states[prev_state].name,
-                   session_states[new_state].name);
-
-  /*
-   * If the state goes from CREATED to INITIATED (which means the remote
-   * end initiated), set the timer. If, OTOH, we're the end which just sent an
-   * initiate, set the timer.
-   */
-  if ((prev_state == JS_STATE_PENDING_CREATED &&
-       new_state == JS_STATE_PENDING_INITIATED) ||
-      (new_state == JS_STATE_PENDING_INITIATE_SENT))
-    {
-      priv->timer_id =
-        g_timeout_add (DEFAULT_SESSION_TIMEOUT, timeout_session, session);
-    }
-  else if (new_state == JS_STATE_ACTIVE)
-    {
-      g_source_remove (priv->timer_id);
-      priv->timer_id = 0;
-
-      /* signal any streams to the remote end which were added locally &
-       * became ready before the session was accepted, so haven't been
-       * mentioned yet */
-      _add_ready_new_streams (session);
-    }
-}
-
-static void
-_mark_local_streams_sent (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i;
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (stream->initiator == INITIATOR_REMOTE)
-        continue;
-
-      GMS_DEBUG_INFO (session, "marking local stream %s as signalled",
-          stream->name);
-
-      g_object_set (stream, "signalling-state", STREAM_SIG_STATE_SENT, NULL);
-    }
-}
-
-static void
-_mark_local_streams_acked (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i;
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (stream->initiator == INITIATOR_REMOTE)
-        continue;
-
-      if (stream->signalling_state != STREAM_SIG_STATE_SENT)
-        continue;
-
-      GMS_DEBUG_INFO (session, "marking local stream %s as acknowledged",
-          stream->name);
-
-      g_object_set (stream,
-          "signalling-state", STREAM_SIG_STATE_ACKNOWLEDGED,
-          NULL);
-    }
-}
-
-static void
-_set_remote_streams_playing (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i;
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (stream->initiator == INITIATOR_LOCAL)
-        continue;
-
-      GMS_DEBUG_INFO (session, "setting remote stream %s as playing",
-          stream->name);
-
-      g_object_set (stream, "playing", TRUE, NULL);
-    }
-}
-
-static const gchar *_direction_to_senders (GabbleMediaSession *,
-    TpMediaStreamDirection);
-
-static void
-_add_content_descriptions_one (GabbleMediaSession *session,
-                               GabbleMediaStream *stream,
-                               LmMessageNode *session_node)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  LmMessageNode *content_node;
-
-  if (priv->mode == MODE_GOOGLE)
-    {
-      content_node = session_node;
-    }
-  else
-    {
-      TpMediaStreamDirection direction;
-      TpMediaStreamPendingSend pending_send;
-
-      content_node = _gabble_media_stream_add_content_node (stream,
-          session_node);
-
-      direction = COMBINED_DIRECTION_GET_DIRECTION (stream->combined_direction);
-      pending_send = COMBINED_DIRECTION_GET_PENDING_SEND
-        (stream->combined_direction);
-
-      /* if we have a pending local send flag set, the signalled (ie understood
-       * by both ends) direction of the stream is assuming that we are actually
-       * sending, so we should OR that into the direction before deciding what
-       * to signal the stream with. we don't need to consider pending remote
-       * send because it doesn't happen in Jingle */
-
-      if ((pending_send & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0)
-        direction |= TP_MEDIA_STREAM_DIRECTION_SEND;
-
-      if (direction != TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL)
-        {
-          const gchar *senders;
-          senders = _direction_to_senders (session, direction);
-          lm_message_node_set_attribute (content_node, "senders", senders);
-        }
-    }
-
-  _gabble_media_stream_content_node_add_description (stream, content_node);
-
-  _gabble_media_stream_content_node_add_transport (stream, content_node);
-}
-
-static void
-_add_content_descriptions (GabbleMediaSession *session,
-                           LmMessageNode *session_node,
-                           JingleInitiator stream_initiator)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i;
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (stream->initiator != stream_initiator)
-        {
-          GMS_DEBUG_INFO (session,
-              "not adding content description for %s stream %s",
-              stream->initiator == INITIATOR_LOCAL ? "local" : "remote",
-              stream->name);
-          continue;
-        }
-
-      _add_content_descriptions_one (session, stream, session_node);
-    }
-}
-
-static LmHandlerResult
-accept_msg_reply_cb (GabbleConnection *conn,
-                     LmMessage *sent_msg,
-                     LmMessage *reply_msg,
-                     GObject *object,
-                     gpointer user_data)
-{
-  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i;
-
-  MSG_REPLY_CB_END_SESSION_IF_NOT_SUCCESSFUL (session, "accept failed");
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (stream->initiator == INITIATOR_LOCAL)
-        continue;
-
-      _gabble_media_stream_update_sending (stream, TRUE);
-    }
-
-  g_object_set (session, "state", JS_STATE_ACTIVE, NULL);
-
-  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
-}
-
-static gboolean
-_stream_not_ready_for_accept (GabbleMediaSession *session,
-                              GabbleMediaStream *stream)
-{
-  /* locally initiated streams shouldn't delay acceptance */
-  if (stream->initiator == INITIATOR_LOCAL)
-    return FALSE;
-
-  if (!stream->got_local_codecs)
-    {
-      GMS_DEBUG_INFO (session, "stream %s does not yet have local codecs",
-          stream->name);
-
-      return TRUE;
-    }
-
-  if (stream->connection_state != TP_MEDIA_STREAM_STATE_CONNECTED)
-    {
-      GMS_DEBUG_INFO (session, "stream %s is not yet connected", stream->name);
-
-      return TRUE;
-    }
-
-  return FALSE;
-}
-
-static void
-try_session_accept (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  LmMessage *msg;
-  LmMessageNode *session_node;
-  const gchar *action;
-  guint i;
-
-  if (priv->state < JS_STATE_ACTIVE && !priv->locally_accepted)
-    {
-      GMS_DEBUG_INFO (session, "not sending accept yet, waiting for local "
-          "user to accept call");
-      return;
-    }
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (_stream_not_ready_for_accept (session, stream))
-        {
-          GMS_DEBUG_INFO (session, "not sending accept yet, found a stream "
-              "which was not yet connected or was missing local codecs");
-          return;
-        }
-    }
-
-  if (priv->mode == MODE_GOOGLE)
-    action = "accept";
-  else
-    action = "session-accept";
-
-  /* construct a session acceptance message */
-  msg = _gabble_media_session_message_new (session, action, &session_node);
-
-  /* only accept REMOTE streams; any LOCAL streams were added by the local
-   * user before accepting and should be signalled after the accept */
-  _add_content_descriptions (session, session_node, INITIATOR_REMOTE);
-
-  GMS_DEBUG_INFO (session, "sending jingle session action \"%s\" to peer",
-      action);
-
-  /* send the final acceptance message */
-  _gabble_connection_send_with_reply (priv->conn, msg, accept_msg_reply_cb,
-                                      G_OBJECT (session), NULL, NULL);
-
-  lm_message_unref (msg);
-
-  /* set remote streams playing */
-  _set_remote_streams_playing (session);
-
-  g_object_set (session, "state", JS_STATE_PENDING_ACCEPT_SENT, NULL);
-}
-
-static LmHandlerResult
-content_accept_msg_reply_cb (GabbleConnection *conn,
-                             LmMessage *sent_msg,
-                             LmMessage *reply_msg,
-                             GObject *object,
-                             gpointer user_data)
-{
-  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (user_data);
-  GabbleMediaStream *stream = GABBLE_MEDIA_STREAM (object);
-
-  if (lm_message_get_sub_type (reply_msg) != LM_MESSAGE_SUB_TYPE_RESULT)
-    {
-      GMS_DEBUG_ERROR (session, "content-accept failed; removing stream");
-      NODE_DEBUG (sent_msg->node, "message sent");
-      NODE_DEBUG (reply_msg->node, "message reply");
-
-      _gabble_media_session_remove_streams (session, &stream, 1);
-
-      return LM_HANDLER_RESULT_REMOVE_MESSAGE;
-    }
-
-  _gabble_media_stream_update_sending (stream, TRUE);
-
-  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
-}
-
-static void
-try_content_accept (GabbleMediaSession *session,
-                    GabbleMediaStream *stream)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  LmMessage *msg;
-  LmMessageNode *session_node;
-
-  g_assert (priv->state == JS_STATE_ACTIVE);
-  g_assert (priv->mode == MODE_JINGLE);
-
-  if (_stream_not_ready_for_accept (session, stream))
-    {
-      GMS_DEBUG_INFO (session, "not sending content-accept yet, stream %s "
-          "is disconnected or missing local codecs", stream->name);
-      return;
-    }
-
-  /* send a content acceptance message */
-  msg = _gabble_media_session_message_new (session, "content-accept",
-      &session_node);
-
-  _add_content_descriptions_one (session, stream, session_node);
-
-  GMS_DEBUG_INFO (session, "sending jingle session action \"content-accept\" "
-      "to peer for stream %s", stream->name);
-
-  _gabble_connection_send_with_reply (priv->conn, msg,
-      content_accept_msg_reply_cb, G_OBJECT (stream), session, NULL);
-
-  lm_message_unref (msg);
-
-  /* set stream playing */
-  g_object_set (stream, "playing", TRUE, NULL);
-}
-
-static LmHandlerResult
-initiate_msg_reply_cb (GabbleConnection *conn,
-                       LmMessage *sent_msg,
-                       LmMessage *reply_msg,
-                       GObject *object,
-                       gpointer user_data)
-{
-  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
-
-  MSG_REPLY_CB_END_SESSION_IF_NOT_SUCCESSFUL (session, "initiate failed");
-
-  g_object_set (session, "state", JS_STATE_PENDING_INITIATED, NULL);
-
-  /* mark all of the streams that we sent in the initiate as acknowledged */
-  _mark_local_streams_acked (session);
-
-  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
-}
-
-static gboolean
-_stream_not_ready_for_initiate (GabbleMediaSession *session,
-                                GabbleMediaStream *stream)
-{
-  if (!stream->got_local_codecs)
-    {
-      GMS_DEBUG_INFO (session, "stream %s does not yet have local codecs",
-          stream->name);
-
-      return TRUE;
-    }
-
-  return FALSE;
-}
-
-static void
-try_session_initiate (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  LmMessage *msg;
-  LmMessageNode *session_node;
-  const gchar *action;
-  guint i;
-
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-
-      if (_stream_not_ready_for_initiate (session, stream))
-        {
-          GMS_DEBUG_INFO (session, "not sending initiate yet, found a stream "
-            "which was missing local codecs");
-          return;
-        }
-    }
-
-  if (priv->mode == MODE_GOOGLE)
-      action = "initiate";
-  else
-      action = "session-initiate";
-
-  msg = _gabble_media_session_message_new (session, action, &session_node);
-
-  _add_content_descriptions (session, session_node, INITIATOR_LOCAL);
-
-  GMS_DEBUG_INFO (session, "sending jingle action \"%s\" to peer", action);
-
-  _gabble_connection_send_with_reply (priv->conn, msg, initiate_msg_reply_cb,
-                                      G_OBJECT (session), NULL, NULL);
-
-  lm_message_unref (msg);
-
-  /* mark local streams as sent (so that eg candidates will be sent) */
-  _mark_local_streams_sent (session);
-
-  g_object_set (session, "state", JS_STATE_PENDING_INITIATE_SENT, NULL);
-}
-
-static LmHandlerResult
-content_add_msg_reply_cb (GabbleConnection *conn,
-                          LmMessage *sent_msg,
-                          LmMessage *reply_msg,
-                          GObject *object,
-                          gpointer user_data)
-{
-  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (user_data);
-  GabbleMediaStream *stream = GABBLE_MEDIA_STREAM (object);
-
-  if (lm_message_get_sub_type (reply_msg) != LM_MESSAGE_SUB_TYPE_RESULT)
-    {
-      if (session->initiator == INITIATOR_REMOTE &&
-          stream->signalling_state == STREAM_SIG_STATE_ACKNOWLEDGED)
-        {
-          GMS_DEBUG_INFO (session, "ignoring content-add failure, stream has "
-              "been successfully created by the session initiator");
-        }
-      else
-        {
-          GMS_DEBUG_ERROR (session, "content-add failed; removing stream");
-          NODE_DEBUG (sent_msg->node, "message sent");
-          NODE_DEBUG (reply_msg->node, "message reply");
-
-          _gabble_media_stream_close (stream);
-        }
-    }
-  else
-    {
-      if (stream->signalling_state == STREAM_SIG_STATE_SENT)
-        {
-          GMS_DEBUG_INFO (session, "content-add succeeded, marking stream as "
-              "ACKNOWLEDGED");
-
-          g_object_set (stream,
-              "signalling-state", STREAM_SIG_STATE_ACKNOWLEDGED,
-              NULL);
-        }
-      else
-        {
-          GMS_DEBUG_INFO (session, "content-add succeeded, but not marking"
-              "stream as ACKNOWLEDGED, it's in state %d",
-              stream->signalling_state);
-        }
-    }
-
-  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
-}
-
-static void
-do_content_add (GabbleMediaSession *session,
-                GabbleMediaStream *stream)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  LmMessage *msg;
-  LmMessageNode *session_node;
-
-  g_assert (priv->state == JS_STATE_ACTIVE);
-  g_assert (priv->mode == MODE_JINGLE);
-
-  if (_stream_not_ready_for_initiate (session, stream))
-    {
-      GMS_DEBUG_ERROR (session, "trying to send content-add for stream %s "
-          "but we have no local codecs. what?!", stream->name);
-      g_assert_not_reached ();
-      return;
-    }
-
-  msg = _gabble_media_session_message_new (session, "content-add",
-      &session_node);
-
-  _add_content_descriptions_one (session, stream, session_node);
-
-  GMS_DEBUG_INFO (session, "sending jingle action \"content-add\" to peer for "
-      "stream %s", stream->name);
-
-  _gabble_connection_send_with_reply (priv->conn, msg,
-      content_add_msg_reply_cb, G_OBJECT (stream), session, NULL);
-
-  lm_message_unref (msg);
-
-  /* mark stream as sent */
-  g_object_set (stream, "signalling-state", STREAM_SIG_STATE_SENT, NULL);
-}
-
-static void
-stream_connection_state_changed_cb (GabbleMediaStream *stream,
-                                    GParamSpec *param,
-                                    GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (session));
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  if (stream->connection_state != TP_MEDIA_STREAM_STATE_CONNECTED)
-    return;
-
-  GMS_DEBUG_INFO (session, "stream %s has gone connected", stream->name);
-
-  if (stream->playing)
-    {
-      GMS_DEBUG_INFO (session, "doing nothing, stream is already playing");
-      return;
-    }
-
-  /* after session is active, we do things per-stream with content-* actions */
-  if (priv->state < JS_STATE_ACTIVE)
-    {
-      /* send a session accept if the session was initiated by the peer */
-      if (session->initiator == INITIATOR_REMOTE)
-        {
-          try_session_accept (session);
-        }
-      else
-        {
-          GMS_DEBUG_INFO (session, "session initiated by us, so we're not "
-              "going to consider sending an accept");
-        }
-    }
-  else
-    {
-      /* send a content accept if the stream was added by the peer */
-      if (stream->initiator == INITIATOR_REMOTE)
-        {
-          try_content_accept (session, stream);
-        }
-      else
-        {
-          GMS_DEBUG_INFO (session, "stream added by us, so we're not going "
-              "to send an accept");
-        }
-    }
-}
-
-static void
-stream_got_local_codecs_changed_cb (GabbleMediaStream *stream,
-                                    GParamSpec *param,
-                                    GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (session));
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  if (!stream->got_local_codecs)
-    return;
-
-  GMS_DEBUG_INFO (session, "stream %s has got local codecs", stream->name);
-
-  if (stream->playing)
-    {
-      GMS_DEBUG_ERROR (session, "stream was already playing and we got local "
-          "codecs. what?!");
-      g_assert_not_reached ();
-      return;
-    }
-
-  /* after session is active, we do things per-stream with content-* actions */
-  if (priv->state < JS_STATE_ACTIVE)
-    {
-      if (session->initiator == INITIATOR_REMOTE)
-        {
-          if (priv->state < JS_STATE_PENDING_ACCEPT_SENT)
-            {
-              try_session_accept (session);
-            }
-          else
-            {
-              GMS_DEBUG_INFO (session, "stream added after sending accept; "
-                  "not doing content-add until remote end acknowledges");
-            }
-        }
-      else
-        {
-          if (priv->state < JS_STATE_PENDING_INITIATE_SENT)
-            {
-              try_session_initiate (session);
-            }
-          else
-            {
-              GMS_DEBUG_INFO (session, "stream added after sending initiate; "
-                  "not doing content-add until remote end accepts");
-            }
-        }
-    }
-  else
-    {
-      if (stream->initiator == INITIATOR_REMOTE)
-        {
-          try_content_accept (session, stream);
-        }
-      else
-        {
-          do_content_add (session, stream);
-        }
-    }
-}
-
-static gchar *
-get_jid_for_contact (GabbleMediaSession *session,
-                     TpHandle handle)
-{
-  GabbleMediaSessionPrivate *priv;
-  TpBaseConnection *conn;
-  const gchar *base_jid;
-  TpHandle self;
-  TpHandleRepoIface *contact_handles;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (session));
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  conn = (TpBaseConnection *)priv->conn;
-  contact_handles = tp_base_connection_get_handles (conn,
-      TP_HANDLE_TYPE_CONTACT);
-  self = conn->self_handle;
-
-  base_jid = tp_handle_inspect (contact_handles, handle);
-  g_assert (base_jid != NULL);
-
-  if (handle == self)
-    {
-      gchar *resource, *ret;
-      g_object_get (priv->conn, "resource", &resource, NULL);
-      g_assert (resource != NULL);
-      ret = g_strdup_printf ("%s/%s", base_jid, resource);
-      g_free (resource);
-      return ret;
-    }
-  else
-    {
-      g_assert (priv->peer_resource != NULL);
-      return g_strdup_printf ("%s/%s", base_jid, priv->peer_resource);
-    }
-}
-
-LmMessage *
-_gabble_media_session_message_new (GabbleMediaSession *session,
-                                   const gchar *action,
-                                   LmMessageNode **session_node)
-{
-  GabbleMediaSessionPrivate *priv;
-  TpBaseConnection *conn;
-  LmMessage *msg;
-  LmMessageNode *iq_node, *node;
-  gchar *peer_jid, *initiator_jid;
-  TpHandle initiator_handle;
-  const gchar *element, *xmlns;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (session));
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  conn = (TpBaseConnection *)priv->conn;
-
-  peer_jid = get_jid_for_contact (session, priv->peer);
-
-  msg = lm_message_new_with_sub_type (
-      peer_jid,
-      LM_MESSAGE_TYPE_IQ,
-      LM_MESSAGE_SUB_TYPE_SET);
-
-  g_free (peer_jid);
-
-  iq_node = lm_message_get_node (msg);
-
-  if (priv->mode == MODE_GOOGLE)
-    element = "session";
-  else
-    element = "jingle";
-
-  if (session->initiator == INITIATOR_LOCAL)
-    initiator_handle = conn->self_handle;
-  else
-    initiator_handle = priv->peer;
-
-  node = lm_message_node_add_child (iq_node, element, NULL);
-  initiator_jid = get_jid_for_contact (session, initiator_handle);
-
-  lm_message_node_set_attributes (node,
-      (priv->mode == MODE_GOOGLE) ? "id" : "sid", priv->id,
-      (priv->mode == MODE_GOOGLE) ? "type" : "action", action,
-      "initiator", initiator_jid,
-      NULL);
-
-  if (priv->mode == MODE_GOOGLE)
-    xmlns = NS_GOOGLE_SESSION;
-  else
-    xmlns = NS_JINGLE;
-
-  lm_message_node_set_attribute (node, "xmlns", xmlns);
-  g_free (initiator_jid);
-
-  if (session_node)
-    *session_node = node;
-
-  return msg;
-}
-
-void
-_gabble_media_session_accept (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  guint i;
-
-  priv->locally_accepted = TRUE;
-
-  /* accept any local pending sends */
-  for (i = 0; i < priv->streams->len; i++)
-    {
-      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
-      CombinedStreamDirection combined_dir = stream->combined_direction;
-      TpMediaStreamDirection current_dir;
-      TpMediaStreamPendingSend pending_send;
-
-      current_dir = COMBINED_DIRECTION_GET_DIRECTION (combined_dir);
-      pending_send = COMBINED_DIRECTION_GET_PENDING_SEND (combined_dir);
-
-      if ((pending_send & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0)
-        {
-          GMS_DEBUG_INFO (session, "accepting pending local send on stream %s",
-              stream->name);
-
-          current_dir |= TP_MEDIA_STREAM_DIRECTION_SEND;
-          pending_send &= ~TP_MEDIA_STREAM_PENDING_LOCAL_SEND;
-          combined_dir = MAKE_COMBINED_DIRECTION (current_dir, pending_send);
-          g_object_set (stream, "combined-direction", combined_dir, NULL);
-          _gabble_media_stream_update_sending (stream, FALSE);
-        }
-    }
-
-  try_session_accept (session);
-}
-
-static LmHandlerResult
-content_remove_msg_reply_cb (GabbleConnection *conn,
-                             LmMessage *sent_msg,
-                             LmMessage *reply_msg,
-                             GObject *object,
-                             gpointer user_data)
-{
-  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  GPtrArray *removing = (GPtrArray *) user_data;
-  guint i;
-
-  MSG_REPLY_CB_END_SESSION_IF_NOT_SUCCESSFUL (session,
-      "stream removal failed");
-
-  for (i = 0; i < removing->len; i++)
-    destroy_media_stream (session,
-        GABBLE_MEDIA_STREAM (g_ptr_array_index (removing, i)));
-
-  g_ptr_array_remove_fast (priv->remove_requests, removing);
-  g_ptr_array_free (removing, TRUE);
-
-  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
-}
-
-void
-_gabble_media_session_remove_streams (GabbleMediaSession *session,
-                                      GabbleMediaStream **streams,
-                                      guint len)
-{
-  GabbleMediaSessionPrivate *priv;
-  LmMessage *msg = NULL;
-  LmMessageNode *session_node;
-  GPtrArray *removing = NULL;
-  guint i;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (session));
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  /* end the session if there'd be no streams left after reducing it */
-  if (_count_non_removing_streams (session) == len)
-    {
-      _gabble_media_session_terminate (session, INITIATOR_LOCAL,
-          TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
-      return;
-    }
-
-  /* construct a remove message if we're in a state greater than CREATED (ie
-   * something has been sent/received about this session) */
-  if (priv->state > JS_STATE_PENDING_CREATED)
-    {
-      msg = _gabble_media_session_message_new (session, "content-remove",
-          &session_node);
-      removing = g_ptr_array_sized_new (len);
-    }
-
-  /* right, remove them */
-  for (i = 0; i < len; i++)
-    {
-      GabbleMediaStream *stream = streams[i];
-
-      switch (stream->signalling_state)
-        {
-        case STREAM_SIG_STATE_NEW:
-          destroy_media_stream (session, stream);
-          break;
-        case STREAM_SIG_STATE_SENT:
-        case STREAM_SIG_STATE_ACKNOWLEDGED:
-          {
-            g_assert (priv->state > JS_STATE_PENDING_CREATED);
-            g_assert (msg != NULL);
-            g_assert (removing != NULL);
-            g_assert (session_node != NULL);
-
-            _gabble_media_stream_add_content_node (stream, session_node);
-
-            g_object_set (stream,
-                "playing", FALSE,
-                "signalling-state", STREAM_SIG_STATE_REMOVING,
-                NULL);
-
-            /* close the stream now, but don't forget about it until the
-             * removal message is acknowledged, since we need to be able to
-             * detect content-remove cross-talk */
-            _gabble_media_stream_close (stream);
-            g_ptr_array_add (removing, stream);
-          }
-          break;
-        case STREAM_SIG_STATE_REMOVING:
-          break;
-        }
-    }
-
-  /* send the remove message if necessary */
-  if (msg != NULL)
-    {
-      if (removing->len > 0)
-        {
-          GMS_DEBUG_INFO (session, "sending jingle session action "
-              "\"content-remove\" to peer");
-
-          _gabble_connection_send_with_reply (priv->conn, msg,
-              content_remove_msg_reply_cb, G_OBJECT (session), removing, NULL);
-
-          g_ptr_array_add (priv->remove_requests, removing);
-        }
-      else
-        {
-          g_ptr_array_free (removing, TRUE);
-        }
-
-      lm_message_unref (msg);
-    }
-  else
-    {
-      GMS_DEBUG_INFO (session, "not sending jingle session action "
-          "\"content-remove\" to peer, no initiates or adds sent for "
-          "these streams");
-      if (priv->state < JS_STATE_PENDING_INITIATE_SENT)
-        {
-           try_session_initiate (session);
-        }
-    }
-}
-
-static void
-send_reject_message (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  LmMessage *msg;
-  LmMessageNode *session_node;
-
-  /* this should only happen in google mode, and we should only arrive in that
-   * mode when we've ended up talking to a resource that doesn't support
-   * jingle */
-  g_assert (priv->mode == MODE_GOOGLE);
-  g_assert (priv->peer_resource != NULL);
-
-  /* construct a session terminate message */
-  msg = _gabble_media_session_message_new (session, "reject", &session_node);
-
-  GMS_DEBUG_INFO (session, "sending jingle session action \"reject\" to peer");
-
-  /* send it */
-  _gabble_connection_send_with_reply (priv->conn, msg, NULL,
-                                      G_OBJECT (session), NULL, NULL);
-
-  lm_message_unref (msg);
-}
-
-static void
-send_terminate_message (GabbleMediaSession *session)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  const gchar *action;
-  LmMessage *msg;
-  LmMessageNode *session_node;
-
-  /* construct a session terminate message */
-  if (priv->mode == MODE_GOOGLE)
-    action = "terminate";
-  else
-    action = "session-terminate";
-
-  msg = _gabble_media_session_message_new (session, action, &session_node);
-
-  GMS_DEBUG_INFO (session, "sending jingle session action \"%s\" to peer",
-      action);
-
-  /* send it */
-  _gabble_connection_send_with_reply (priv->conn, msg, NULL,
-                                      G_OBJECT (session), NULL, NULL);
-
-  lm_message_unref (msg);
-}
-
-void
-_gabble_media_session_terminate (GabbleMediaSession *session,
-                                 JingleInitiator who,
-                                 TpChannelGroupChangeReason why)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  TpBaseConnection *conn = (TpBaseConnection *)priv->conn;
-  TpHandle actor;
-
-  if (priv->state == JS_STATE_ENDED)
-    return;
-
-  if (who == INITIATOR_REMOTE)
-    {
-      actor = priv->peer;
-    }
-  else
-    {
-      actor = conn->self_handle;
-
-      /* Need to tell them that it's all over. */
-
-      /* Jingle doesn't have a "reject" action; a termination before an
-       * acceptance indicates that the call has been declined */
-
-      if (session->initiator == INITIATOR_REMOTE &&
-          priv->state == JS_STATE_PENDING_INITIATED &&
-          priv->mode == MODE_GOOGLE)
-        {
-          send_reject_message (session);
-        }
-
-      /* if we're still in CREATED, then we've not sent or received any
-       * messages about this session yet, so no terminate is necessary */
-      else if (priv->state > JS_STATE_PENDING_CREATED)
-        {
-          send_terminate_message (session);
-        }
-
-      while (priv->streams->len > 0)
-        destroy_media_stream (session, g_ptr_array_index (priv->streams, 0));
-    }
-
-  priv->terminated = TRUE;
-  g_object_set (session, "state", JS_STATE_ENDED, NULL);
-  g_signal_emit (session, signals[TERMINATED], 0, actor, why);
-}
-
-#if defined (ENABLE_DEBUG) && _GMS_DEBUG_LEVEL
-void
-_gabble_media_session_debug (GabbleMediaSession *session,
-                             DebugMessageType type,
-                             const gchar *format, ...)
-{
-  if (DEBUGGING)
-    {
-      va_list list;
-      gchar buf[512];
-      GabbleMediaSessionPrivate *priv;
-      time_t curtime;
-      struct tm *loctime;
-      gchar stamp[10];
-      const gchar *type_str;
-
-      g_assert (GABBLE_IS_MEDIA_SESSION (session));
-
-      priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-      curtime = time (NULL);
-      loctime = localtime (&curtime);
-
-      strftime (stamp, sizeof (stamp), "%T", loctime);
-
-      va_start (list, format);
-
-      vsnprintf (buf, sizeof (buf), format, list);
-
-      va_end (list);
-
-      switch (type) {
-        case DEBUG_MSG_INFO:
-          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_WHITE;
-          break;
-        case DEBUG_MSG_DUMP:
-          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_GREEN;
-          break;
-        case DEBUG_MSG_WARNING:
-          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_YELLOW;
-          break;
-        case DEBUG_MSG_ERROR:
-          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_WHITE TP_ANSI_BG_RED;
-          break;
-        case DEBUG_MSG_EVENT:
-          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_CYAN;
-          break;
-        default:
-          g_assert_not_reached ();
-          return;
-      }
-
-      printf ("[%s%s%s] %s%-26s%s %s%s%s\n",
-          TP_ANSI_BOLD_ON TP_ANSI_FG_WHITE,
-          stamp,
-          TP_ANSI_RESET,
-          session_states[priv->state].attributes,
-          session_states[priv->state].name,
-          TP_ANSI_RESET,
-          type_str,
-          buf,
-          TP_ANSI_RESET);
-
-      fflush (stdout);
-    }
-}
-
-#endif /* _GMS_DEBUG_LEVEL */
-
-static const gchar *
-_name_stream (GabbleMediaSession *session,
-              TpMediaStreamType media_type)
-{
-  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  static gchar ret[MAX_STREAM_NAME_LEN] = GTALK_STREAM_NAME;
-
-  if (priv->mode != MODE_GOOGLE)
-    {
-      guint i = 1;
-
-      do {
-          g_snprintf (ret, MAX_STREAM_NAME_LEN, "%s%u",
-              media_type == TP_MEDIA_STREAM_TYPE_AUDIO ? "audio" : "video",
-              i++);
-
-          /* even though we now have seperate namespaces for local and remote,
-           * actually check in both so that we can still support clients which
-           * have 1 namespace (such as our older selves :D) */
-          if (_lookup_stream_by_name_and_initiator (session, ret,
-                INITIATOR_INVALID) != NULL)
-            {
-              ret[0] = '\0';
-            }
-      } while (ret[0] == '\0');
-    }
-
-  return ret;
-}
-
-
-gboolean
-_gabble_media_session_request_streams (GabbleMediaSession *session,
-                                       const GArray *media_types,
-                                       GPtrArray **ret,
-                                       GError **error)
-{
-  /* if you change the caps in this function, you almost certainly also need
-   * to change _gabble_media_channel_typeflags_to_caps and
-   * _gabble_media_channel_caps_to_typeflags in media-channel.c */
-  static GabblePresenceCapabilities google_audio_caps =
-    PRESENCE_CAP_GOOGLE_VOICE;
-  static GabblePresenceCapabilities jingle_audio_caps =
-    PRESENCE_CAP_JINGLE | PRESENCE_CAP_JINGLE_DESCRIPTION_AUDIO |
-    PRESENCE_CAP_GOOGLE_TRANSPORT_P2P;
-  static GabblePresenceCapabilities jingle_video_caps =
-    PRESENCE_CAP_JINGLE | PRESENCE_CAP_JINGLE_DESCRIPTION_VIDEO |
-    PRESENCE_CAP_GOOGLE_TRANSPORT_P2P;
-
-  GabbleMediaSessionPrivate *priv;
-  GabblePresence *presence;
-  gboolean want_audio, want_video;
-  GabblePresenceCapabilities jingle_desired_caps;
-  guint idx;
-  gchar *dump;
-
-  g_assert (GABBLE_IS_MEDIA_SESSION (session));
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  presence = gabble_presence_cache_get (priv->conn->presence_cache,
-      priv->peer);
-
-  if (presence == NULL)
-    {
-      g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
-          "member has no audio/video capabilities");
-
-      return FALSE;
-    }
-
-  dump = gabble_presence_dump (presence);
-  GMS_DEBUG_DUMP (session, "presence for peer %d:\n%s", priv->peer, dump);
-  g_free (dump);
-
-  want_audio = want_video = FALSE;
-
-  for (idx = 0; idx < media_types->len; idx++)
-    {
-      guint media_type = g_array_index (media_types, guint, idx);
-
-      if (media_type == TP_MEDIA_STREAM_TYPE_AUDIO)
-        {
-          want_audio = TRUE;
-        }
-      else if (media_type == TP_MEDIA_STREAM_TYPE_VIDEO)
-        {
-          want_video = TRUE;
-        }
-      else
-        {
-          g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
-              "given media type %u is invalid", media_type);
-          return FALSE;
-        }
-    }
-
-  /* work out what we'd need to do these streams with jingle */
-  jingle_desired_caps = 0;
-
-  if (want_audio)
-    jingle_desired_caps |= jingle_audio_caps;
-
-  if (want_video)
-    jingle_desired_caps |= jingle_video_caps;
-
-  GMS_DEBUG_INFO (session, "want audio: %s; want video: %s",
-    want_audio ? "yes" : "no", want_video ? "yes" : "no");
-
-  /* existing call; the recipient and the mode has already been decided */
-  if (priv->peer_resource)
-    {
-      /* is a google call... we have no other option */
-      if (priv->mode == MODE_GOOGLE)
-        {
-          GMS_DEBUG_INFO (session, "already in Google mode; can't add new "
-              "stream");
-
-          g_assert (priv->streams->len == 1);
-
-          g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
-              "Google Talk calls may only contain one stream");
-
-          return FALSE;
-        }
-
-      if (!gabble_presence_resource_has_caps (presence, priv->peer_resource,
-            jingle_desired_caps))
-        {
-          GMS_DEBUG_INFO (session,
-            "in Jingle mode but have insufficient caps for requested streams");
-
-          g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
-              "existing call member doesn't support all requested media"
-              " types");
-
-          return FALSE;
-        }
-
-      GMS_DEBUG_INFO (session,
-        "in Jingle mode, and have necessary caps");
-    }
-
-  /* no existing call; we should choose a recipient and a mode */
-  else
-    {
-      const gchar *resource;
-
-      g_assert (priv->streams->len == 0);
-
-      /* see if we have a fully-capable jingle resource; regardless of the
-       * desired media type it's best if we can add/remove the others later */
-      resource = gabble_presence_pick_resource_by_caps (presence,
-          jingle_audio_caps | jingle_video_caps);
-
-      if (resource == NULL)
-        {
-          GMS_DEBUG_INFO (session, "contact is not fully jingle-capable");
-
-          /* ok, no problem. see if we can do just what's wanted with jingle */
-          resource = gabble_presence_pick_resource_by_caps (presence,
-              jingle_desired_caps);
-
-          if (resource == NULL && want_audio && !want_video)
-            {
-              GMS_DEBUG_INFO (session,
-                "contact doesn't have desired Jingle capabilities");
-
-              /* last ditch... if we want only audio and not video, we can make
-               * do with google talk */
-              resource = gabble_presence_pick_resource_by_caps (presence,
-                  google_audio_caps);
-
-              if (resource != NULL)
-                {
-                  /* only one stream possible with google */
-                  if (media_types->len == 1)
-                    {
-                      GMS_DEBUG_INFO (session,
-                        "contact has no Jingle capabilities; "
-                        "falling back to Google audio call");
-                      priv->mode = MODE_GOOGLE;
-                    }
-                  else
-                    {
-                      g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
-                          "Google Talk calls may only contain one stream");
-
-                      return FALSE;
-                    }
-                }
-              else
-                {
-                  GMS_DEBUG_INFO (session,
-                    "contact doesn't have desired Google capabilities");
-                }
-            }
-        }
-
-      if (resource == NULL)
-        {
-          GMS_DEBUG_INFO (session,
-            "contact doesn't have a resource with suitable capabilities");
-
-          g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
-              "member does not have the desired audio/video capabilities");
-
-          return FALSE;
-        }
-
-      priv->peer_resource = g_strdup (resource);
-    }
-
-  /* check it's not a ridiculous number of streams */
-  if ((priv->streams->len + media_types->len) > MAX_STREAMS)
-    {
-      g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
-          "I think that's quite enough streams already");
-      return FALSE;
-    }
-
-  /* if we've got here, we're good to make the streams */
-
-  *ret = g_ptr_array_sized_new (media_types->len);
-
-  for (idx = 0; idx < media_types->len; idx++)
-    {
-      guint media_type = g_array_index (media_types, guint, idx);
-      GabbleMediaStream *stream;
-      const gchar *stream_name;
-
-      if (priv->mode == MODE_GOOGLE)
-        stream_name = GTALK_STREAM_NAME;
-      else
-        stream_name = _name_stream (session, media_type);
-
-      stream = create_media_stream (session, stream_name, INITIATOR_LOCAL,
-                                    media_type);
-
-      g_ptr_array_add (*ret, stream);
-    }
-
-  return TRUE;
-}
-
-static const gchar *
-_direction_to_senders (GabbleMediaSession *session,
-                       TpMediaStreamDirection dir)
-{
-  const gchar *ret = NULL;
-
-  switch (dir)
-    {
-      case TP_MEDIA_STREAM_DIRECTION_NONE:
-        g_assert_not_reached ();
-        break;
-      case TP_MEDIA_STREAM_DIRECTION_SEND:
-        if (session->initiator == INITIATOR_LOCAL)
-          ret = "initiator";
-        else
-          ret = "responder";
-        break;
-      case TP_MEDIA_STREAM_DIRECTION_RECEIVE:
-        if (session->initiator == INITIATOR_REMOTE)
-          ret = "initiator";
-        else
-          ret = "responder";
-        break;
-      case TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL:
-        ret = "both";
-        break;
-    }
-
-  g_assert (ret != NULL);
-
-  return ret;
-}
-
-static LmHandlerResult
-direction_msg_reply_cb (GabbleConnection *conn,
-                        LmMessage *sent_msg,
-                        LmMessage *reply_msg,
-                        GObject *object,
-                        gpointer user_data)
-{
-  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (user_data);
-  GabbleMediaStream *stream = GABBLE_MEDIA_STREAM (object);
-
-  MSG_REPLY_CB_END_SESSION_IF_NOT_SUCCESSFUL (session,
-      "direction change failed");
-
-  if (stream->playing)
-    {
-      _gabble_media_stream_update_sending (stream, TRUE);
-    }
-
-  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
-}
-
-static gboolean
-send_direction_change (GabbleMediaSession *session,
-                       GabbleMediaStream *stream,
-                       TpMediaStreamDirection dir,
-                       GError **error)
-{
-  GabbleMediaSessionPrivate *priv;
-  const gchar *senders;
-  LmMessage *msg;
-  LmMessageNode *session_node, *content_node;
-  gboolean ret;
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-  senders = _direction_to_senders (session, dir);
-
-  if (stream->signalling_state == STREAM_SIG_STATE_NEW ||
-      stream->signalling_state == STREAM_SIG_STATE_REMOVING)
-    {
-      GMS_DEBUG_INFO (session, "not sending content-modify for %s stream %s",
-          stream->signalling_state == STREAM_SIG_STATE_NEW ? "new" : "removing",
-          stream->name);
-      return TRUE;
-    }
-
-  GMS_DEBUG_INFO (session, "sending jingle session action \"content-modify\" "
-      "to peer for stream %s (senders=%s)", stream->name, senders);
-
-  msg = _gabble_media_session_message_new (session, "content-modify",
-      &session_node);
-  content_node = _gabble_media_stream_add_content_node (stream, session_node);
-
-  lm_message_node_set_attribute (content_node, "senders", senders);
-
-  ret = _gabble_connection_send_with_reply (priv->conn, msg,
-      direction_msg_reply_cb, G_OBJECT (stream), session, error);
-
-  lm_message_unref (msg);
-
-  return ret;
-}
-
-gboolean
-_gabble_media_session_request_stream_direction (GabbleMediaSession *session,
-                                                GabbleMediaStream *stream,
-                                                TpMediaStreamDirection requested_dir,
-                                                GError **error)
-{
-  GabbleMediaSessionPrivate *priv;
-  CombinedStreamDirection new_combined_dir;
-  TpMediaStreamDirection current_dir; //, new_dir;
-  TpMediaStreamPendingSend pending_send;
-
-  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
-
-  current_dir = COMBINED_DIRECTION_GET_DIRECTION (stream->combined_direction);
-  pending_send = COMBINED_DIRECTION_GET_PENDING_SEND
-    (stream->combined_direction);
-
-  if (priv->mode == MODE_GOOGLE)
-    {
-      g_assert (current_dir == TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL);
-
-      if (requested_dir == TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL)
-        return TRUE;
-
-      g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
-          "Google Talk calls can only be bi-directional");
-      return FALSE;
-    }
-
-  if (requested_dir == TP_MEDIA_STREAM_DIRECTION_NONE)
-    {
-      GMS_DEBUG_INFO (session, "request for NONE direction; removing stream");
-
-      _gabble_media_session_remove_streams (session, &stream, 1);
-
-      return TRUE;
-    }
-
-  /* if we're awaiting a local decision on sending... */
-  if ((pending_send & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0)
-    {
-      /* clear the flag */
-      pending_send &= ~TP_MEDIA_STREAM_PENDING_LOCAL_SEND;
-
-      /* make our current_dir match what other end thinks (he thinks we're
-       * bidirectional) so that we send the correct transitions */
-      current_dir ^= TP_MEDIA_STREAM_DIRECTION_SEND;
-    }
-
-#if 0
-  /* if we're asking the remote end to start sending, set the pending flag and
-   * don't change our directionality just yet */
-  new_dir = requested_dir;
-  if (((current_dir & TP_MEDIA_STREAM_DIRECTION_RECEIVE) == 0) &&
-      ((new_dir & TP_MEDIA_STREAM_DIRECTION_RECEIVE) != 0))
-    {
-      pending_send ^= TP_MEDIA_STREAM_PENDING_REMOTE_SEND;
-      new_dir &= ~TP_MEDIA_STREAM_DIRECTION_RECEIVE;
-    }
-#endif
-
-  /* make any necessary changes */
-  new_combined_dir = MAKE_COMBINED_DIRECTION (requested_dir, pending_send);
-  if (new_combined_dir != stream->combined_direction)
-    {
-      g_object_set (stream, "combined-direction", new_combined_dir, NULL);
-      _gabble_media_stream_update_sending (stream, FALSE);
-    }
-
-  /* short-circuit sending a request if we're not asking for anything new */
-  if (current_dir == requested_dir)
-    return TRUE;
-
-  /* send request */
-  return send_direction_change (session, stream, requested_dir, error);
-}
-
-static void
-session_handler_iface_init (gpointer g_iface, gpointer iface_data)
-{
-  TpSvcMediaSessionHandlerClass *klass =
-    (TpSvcMediaSessionHandlerClass *)g_iface;
-
-#define IMPLEMENT(x) tp_svc_media_session_handler_implement_##x (\
-    klass, gabble_media_session_##x)
-  IMPLEMENT(error);
-  IMPLEMENT(ready);
-#undef IMPLEMENT
-}
diff --git a/src/media-session.c b/src/media-session.c
new file mode 100644
index 0000000..6001797
--- /dev/null
+++ b/src/media-session.c
@@ -0,0 +1,2938 @@
+/*
+ * gabble-media-session.c - Source for GabbleMediaSession
+ * Copyright (C) 2006 Collabora Ltd.
+ * Copyright (C) 2006 Nokia Corporation
+ *   @author Ole Andre Vadla Ravnaas <ole.andre.ravnaas at collabora.co.uk>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "media-session.h"
+
+#include <dbus/dbus-glib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#define DEBUG_FLAG GABBLE_DEBUG_MEDIA
+
+#include <telepathy-glib/debug-ansi.h>
+#include "debug.h"
+#include "namespaces.h"
+#include "util.h"
+
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/errors.h>
+#include <telepathy-glib/svc-media-interfaces.h>
+
+#include "connection.h"
+#include "media-channel.h"
+#include "media-stream.h"
+#include "presence-cache.h"
+#include "presence.h"
+
+#include "gabble-signals-marshal.h"
+
+static void session_handler_iface_init (gpointer, gpointer);
+
+G_DEFINE_TYPE_WITH_CODE(GabbleMediaSession,
+    gabble_media_session,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_MEDIA_SESSION_HANDLER,
+      session_handler_iface_init)
+    )
+
+#define DEFAULT_SESSION_TIMEOUT 60000
+
+#define GTALK_STREAM_NAME "gtalk"
+
+/* 99 streams gives us a max name length of 8 (videoXX\0 or audioXX\0) */
+#define MAX_STREAMS 99
+#define MAX_STREAM_NAME_LEN 8
+
+/* signal enum */
+enum
+{
+    STREAM_ADDED,
+    TERMINATED,
+    LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+/* properties */
+enum
+{
+  PROP_CONNECTION = 1,
+  PROP_MEDIA_CHANNEL,
+  PROP_OBJECT_PATH,
+  PROP_SESSION_ID,
+  PROP_INITIATOR,
+  PROP_PEER,
+  PROP_PEER_RESOURCE,
+  PROP_STATE,
+  LAST_PROPERTY
+};
+
+/* private structure */
+typedef struct _GabbleMediaSessionPrivate GabbleMediaSessionPrivate;
+
+struct _GabbleMediaSessionPrivate
+{
+  GabbleConnection *conn;
+  GabbleMediaChannel *channel;
+  GabbleMediaSessionMode mode;
+  gchar *object_path;
+
+  GPtrArray *streams;
+  GPtrArray *remove_requests;
+
+  gchar *id;
+  TpHandle peer;
+  gchar *peer_resource;
+
+  JingleSessionState state;
+  gboolean ready;
+  gboolean locally_accepted;
+  gboolean terminated;
+
+  guint timer_id;
+
+  gboolean dispose_has_run;
+};
+
+#define GABBLE_MEDIA_SESSION_GET_PRIVATE(obj) \
+    ((GabbleMediaSessionPrivate *)obj->priv)
+
+typedef struct {
+    gchar *name;
+    gchar *attributes;
+} SessionStateDescription;
+
+static const SessionStateDescription session_states[] =
+{
+    { "JS_STATE_PENDING_CREATED",
+      TP_ANSI_BOLD_ON TP_ANSI_FG_BLACK TP_ANSI_BG_WHITE   },
+    { "JS_STATE_PENDING_INITIATE_SENT",
+      TP_ANSI_BOLD_ON                  TP_ANSI_BG_CYAN    },
+    { "JS_STATE_PENDING_INITIATED",
+      TP_ANSI_BOLD_ON                  TP_ANSI_BG_MAGENTA },
+    { "JS_STATE_PENDING_ACCEPT_SENT",
+      TP_ANSI_BOLD_ON                  TP_ANSI_BG_CYAN    },
+    { "JS_STATE_ACTIVE",
+      TP_ANSI_BOLD_ON                  TP_ANSI_BG_BLUE    },
+    { "JS_STATE_ENDED",
+                                       TP_ANSI_BG_RED     }
+};
+
+static void
+gabble_media_session_init (GabbleMediaSession *self)
+{
+  GabbleMediaSessionPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      GABBLE_TYPE_MEDIA_SESSION, GabbleMediaSessionPrivate);
+
+  self->priv = priv;
+
+  priv->mode = MODE_JINGLE;
+  priv->state = JS_STATE_PENDING_CREATED;
+  priv->streams = g_ptr_array_new ();
+  priv->remove_requests = g_ptr_array_new ();
+}
+
+static void stream_connection_state_changed_cb (GabbleMediaStream *stream,
+                                                GParamSpec *param,
+                                                GabbleMediaSession *session);
+static void stream_got_local_codecs_changed_cb (GabbleMediaStream *stream,
+                                                GParamSpec *param,
+                                                GabbleMediaSession *session);
+
+static void
+_emit_new_stream (GabbleMediaSession *session,
+                  GabbleMediaStream *stream)
+{
+  gchar *object_path;
+  guint id, media_type;
+
+  g_object_get (stream,
+                "object-path", &object_path,
+                "id", &id,
+                "media-type", &media_type,
+                NULL);
+
+  /* all of the streams are bidirectional from farsight's point of view, it's
+   * just in the signalling they change */
+  tp_svc_media_session_handler_emit_new_stream_handler (session,
+      object_path, id, media_type, TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL);
+
+  g_free (object_path);
+}
+
+
+static GabbleMediaStream *
+_lookup_stream_by_name_and_initiator (GabbleMediaSession *session,
+                                      const gchar *stream_name,
+                                      JingleInitiator stream_initiator)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i;
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (tp_strdiff (stream->name, stream_name))
+        continue;
+
+      if (stream_initiator != INITIATOR_INVALID &&
+          stream_initiator != stream->initiator)
+        continue;
+
+      return stream;
+    }
+
+  return NULL;
+}
+
+
+static GabbleMediaStream *
+create_media_stream (GabbleMediaSession *session,
+                     const gchar *name,
+                     JingleInitiator initiator,
+                     guint media_type)
+{
+  GabbleMediaSessionPrivate *priv;
+  gchar *object_path;
+  GabbleMediaStream *stream;
+  guint id;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (session));
+  g_assert (name != NULL);
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  /* assert that if we're in google mode:
+   *  - we only try to make one stream
+   *  - it's an audio stream
+   *  - it's called GTALK_STREAM_NAME */
+  if (priv->mode == MODE_GOOGLE)
+    {
+      g_assert (priv->streams->len == 0);
+      g_assert (media_type == TP_MEDIA_STREAM_TYPE_AUDIO);
+      g_assert (!tp_strdiff (name, GTALK_STREAM_NAME));
+    }
+
+  g_assert (priv->streams->len < MAX_STREAMS);
+  g_assert (_lookup_stream_by_name_and_initiator (session, name, initiator) ==
+      NULL);
+
+  id = _gabble_media_channel_get_stream_id (priv->channel);
+
+  GMS_DEBUG_INFO (session,
+      "creating new %s %s stream called \"%s\" with id %u",
+      priv->mode == MODE_GOOGLE ? "google" : "jingle",
+      media_type == TP_MEDIA_STREAM_TYPE_AUDIO ? "audio" : "video",
+      name, id);
+
+  object_path = g_strdup_printf ("%s/MediaStream%u", priv->object_path, id);
+
+  stream = g_object_new (GABBLE_TYPE_MEDIA_STREAM,
+                         "connection", priv->conn,
+                         "media-session", session,
+                         "object-path", object_path,
+                         "mode", priv->mode,
+                         "name", name,
+                         "id", id,
+                         "initiator", initiator,
+                         "media-type", media_type,
+                         NULL);
+
+  g_signal_connect (stream, "notify::connection-state",
+                    (GCallback) stream_connection_state_changed_cb,
+                    session);
+  g_signal_connect (stream, "notify::got-local-codecs",
+                    (GCallback) stream_got_local_codecs_changed_cb,
+                    session);
+
+  g_ptr_array_add (priv->streams, stream);
+
+  g_free (object_path);
+
+  if (priv->ready)
+    _emit_new_stream (session, stream);
+
+  g_signal_emit (session, signals[STREAM_ADDED], 0, stream);
+
+  return stream;
+}
+
+static void
+destroy_media_stream (GabbleMediaSession *session,
+                      GabbleMediaStream *stream)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  _gabble_media_stream_close (stream);
+  g_ptr_array_remove_fast (priv->streams, stream);
+  g_object_unref (stream);
+}
+
+static GObject *
+gabble_media_session_constructor (GType type, guint n_props,
+                                  GObjectConstructParam *props)
+{
+  GObject *obj;
+  GabbleMediaSessionPrivate *priv;
+  DBusGConnection *bus;
+  TpHandleRepoIface *contact_handles;
+
+  obj = G_OBJECT_CLASS (gabble_media_session_parent_class)->
+           constructor (type, n_props, props);
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (GABBLE_MEDIA_SESSION (obj));
+
+  bus = tp_get_bus ();
+  dbus_g_connection_register_g_object (bus, priv->object_path, obj);
+
+  contact_handles = tp_base_connection_get_handles (
+      (TpBaseConnection *) priv->conn, TP_HANDLE_TYPE_CONTACT);
+
+  tp_handle_ref (contact_handles, priv->peer);
+
+  return obj;
+}
+
+static void
+gabble_media_session_get_property (GObject    *object,
+                                   guint       property_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  switch (property_id) {
+    case PROP_CONNECTION:
+      g_value_set_object (value, priv->conn);
+      break;
+    case PROP_MEDIA_CHANNEL:
+      g_value_set_object (value, priv->channel);
+      break;
+    case PROP_OBJECT_PATH:
+      g_value_set_string (value, priv->object_path);
+      break;
+    case PROP_SESSION_ID:
+      g_value_set_string (value, priv->id);
+      break;
+    case PROP_INITIATOR:
+      g_value_set_uint (value, session->initiator);
+      break;
+    case PROP_PEER:
+      g_value_set_uint (value, priv->peer);
+      break;
+    case PROP_PEER_RESOURCE:
+      g_value_set_string (value, priv->peer_resource);
+      break;
+    case PROP_STATE:
+      g_value_set_uint (value, priv->state);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+  }
+}
+
+static void session_state_changed (GabbleMediaSession *session,
+                                   JingleSessionState prev_state,
+                                   JingleSessionState new_state);
+
+static void
+gabble_media_session_set_property (GObject      *object,
+                                   guint         property_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  JingleSessionState prev_state;
+
+  switch (property_id) {
+    case PROP_CONNECTION:
+      priv->conn = g_value_get_object (value);
+      break;
+    case PROP_MEDIA_CHANNEL:
+      priv->channel = g_value_get_object (value);
+      break;
+    case PROP_OBJECT_PATH:
+      g_free (priv->object_path);
+      priv->object_path = g_value_dup_string (value);
+      break;
+    case PROP_SESSION_ID:
+      g_free (priv->id);
+      priv->id = g_value_dup_string (value);
+      break;
+    case PROP_INITIATOR:
+      session->initiator = g_value_get_uint (value);
+      break;
+    case PROP_PEER:
+      priv->peer = g_value_get_uint (value);
+      break;
+    case PROP_PEER_RESOURCE:
+      g_free (priv->peer_resource);
+      priv->peer_resource = g_value_dup_string (value);
+      break;
+    case PROP_STATE:
+      prev_state = priv->state;
+      priv->state = g_value_get_uint (value);
+
+      if (priv->state == JS_STATE_ENDED)
+        g_assert (priv->terminated);
+
+      if (priv->state != prev_state)
+        session_state_changed (session, prev_state, priv->state);
+
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+  }
+}
+
+static void gabble_media_session_dispose (GObject *object);
+static void gabble_media_session_finalize (GObject *object);
+
+static void
+gabble_media_session_class_init (GabbleMediaSessionClass *gabble_media_session_class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (gabble_media_session_class);
+  GParamSpec *param_spec;
+
+  g_type_class_add_private (gabble_media_session_class,
+      sizeof (GabbleMediaSessionPrivate));
+
+  object_class->constructor = gabble_media_session_constructor;
+
+  object_class->get_property = gabble_media_session_get_property;
+  object_class->set_property = gabble_media_session_set_property;
+
+  object_class->dispose = gabble_media_session_dispose;
+  object_class->finalize = gabble_media_session_finalize;
+
+  param_spec = g_param_spec_object ("connection", "GabbleConnection object",
+                                    "Gabble connection object that owns this "
+                                    "media session's channel.",
+                                    GABBLE_TYPE_CONNECTION,
+                                    G_PARAM_CONSTRUCT_ONLY |
+                                    G_PARAM_READWRITE |
+                                    G_PARAM_STATIC_NICK |
+                                    G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_CONNECTION, param_spec);
+
+  param_spec = g_param_spec_object ("media-channel",
+      "GabbleMediaChannel object",
+      "Gabble media channel object that owns this media session object.",
+      GABBLE_TYPE_MEDIA_CHANNEL,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NICK |
+      G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_MEDIA_CHANNEL,
+      param_spec);
+
+  param_spec = g_param_spec_string ("object-path", "D-Bus object path",
+                                    "The D-Bus object path used for this "
+                                    "object on the bus.",
+                                    NULL,
+                                    G_PARAM_CONSTRUCT_ONLY |
+                                    G_PARAM_READWRITE |
+                                    G_PARAM_STATIC_NAME |
+                                    G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_OBJECT_PATH, param_spec);
+
+  param_spec = g_param_spec_string ("session-id", "Session ID",
+                                    "A unique session identifier used "
+                                    "throughout all communication.",
+                                    NULL,
+                                    G_PARAM_CONSTRUCT_ONLY |
+                                    G_PARAM_READWRITE |
+                                    G_PARAM_STATIC_NAME |
+                                    G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_SESSION_ID, param_spec);
+
+  param_spec = g_param_spec_uint ("initiator", "Session initiator",
+                                  "An enum signifying which end initiated "
+                                  "the session.",
+                                  INITIATOR_LOCAL,
+                                  INITIATOR_REMOTE,
+                                  INITIATOR_LOCAL,
+                                  G_PARAM_CONSTRUCT_ONLY |
+                                  G_PARAM_READWRITE |
+                                  G_PARAM_STATIC_NAME |
+                                  G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_INITIATOR, param_spec);
+
+  param_spec = g_param_spec_uint ("peer", "Session peer",
+                                  "The TpHandle representing the contact "
+                                  "with whom this session communicates.",
+                                  0, G_MAXUINT32, 0,
+                                  G_PARAM_CONSTRUCT_ONLY |
+                                  G_PARAM_READWRITE |
+                                  G_PARAM_STATIC_NAME |
+                                  G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_PEER, param_spec);
+
+  param_spec = g_param_spec_string ("peer-resource",
+                                    "Session peer's resource",
+                                    "The resource of the contact "
+                                    "with whom this session communicates, "
+                                    "if applicable",
+                                    NULL,
+                                    G_PARAM_CONSTRUCT_ONLY |
+                                    G_PARAM_WRITABLE |
+                                    G_PARAM_STATIC_NAME |
+                                    G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_PEER_RESOURCE,
+                                   param_spec);
+
+  param_spec = g_param_spec_uint ("state", "Session state",
+                                  "The current state that the session is in.",
+                                  0, G_MAXUINT32, 0,
+                                  G_PARAM_READWRITE |
+                                  G_PARAM_STATIC_NAME |
+                                  G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (object_class, PROP_STATE, param_spec);
+
+  signals[STREAM_ADDED] =
+    g_signal_new ("stream-added",
+                  G_OBJECT_CLASS_TYPE (gabble_media_session_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+                  0,
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, G_TYPE_OBJECT);
+
+  signals[TERMINATED] =
+    g_signal_new ("terminated",
+                  G_OBJECT_CLASS_TYPE (gabble_media_session_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+                  0,
+                  NULL, NULL,
+                  gabble_marshal_VOID__UINT_UINT,
+                  G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT);
+}
+
+static void
+gabble_media_session_dispose (GObject *object)
+{
+  GabbleMediaSession *self = GABBLE_MEDIA_SESSION (object);
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self);
+  guint i;
+  TpHandleRepoIface *contact_handles;
+
+  DEBUG ("called");
+
+  if (priv->dispose_has_run)
+    return;
+
+  priv->dispose_has_run = TRUE;
+
+  _gabble_media_session_terminate (self, INITIATOR_LOCAL,
+      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+  if (priv->timer_id != 0)
+    g_source_remove (priv->timer_id);
+
+  if (priv->streams != NULL)
+    {
+      for (i = 0; i < priv->streams->len; i++)
+        g_object_unref (g_ptr_array_index (priv->streams, i));
+      g_ptr_array_free (priv->streams, TRUE);
+      priv->streams = NULL;
+    }
+
+  for (i = 0; i < priv->remove_requests->len; i++)
+    g_ptr_array_free (g_ptr_array_index (priv->remove_requests, i), TRUE);
+  g_ptr_array_free (priv->remove_requests, TRUE);
+  priv->remove_requests = NULL;
+
+  contact_handles = tp_base_connection_get_handles (
+      (TpBaseConnection *) priv->conn, TP_HANDLE_TYPE_CONTACT);
+
+  tp_handle_unref (contact_handles, priv->peer);
+
+  if (G_OBJECT_CLASS (gabble_media_session_parent_class)->dispose)
+    G_OBJECT_CLASS (gabble_media_session_parent_class)->dispose (object);
+}
+
+static void
+gabble_media_session_finalize (GObject *object)
+{
+  GabbleMediaSession *self = GABBLE_MEDIA_SESSION (object);
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self);
+
+  g_free (priv->id);
+  g_free (priv->object_path);
+  g_free (priv->peer_resource);
+  G_OBJECT_CLASS (gabble_media_session_parent_class)->finalize (object);
+}
+
+
+/**
+ * gabble_media_session_error
+ *
+ * Implements D-Bus method Error
+ * on interface org.freedesktop.Telepathy.Media.SessionHandler
+ */
+static void
+gabble_media_session_error (TpSvcMediaSessionHandler *iface,
+                            guint errno,
+                            const gchar *message,
+                            DBusGMethodInvocation *context)
+{
+  GabbleMediaSession *self = GABBLE_MEDIA_SESSION (iface);
+  GabbleMediaSessionPrivate *priv;
+  GPtrArray *tmp;
+  guint i;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (self));
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self);
+
+  GMS_DEBUG_INFO (self, "Media.SessionHandler::Error called, error %u (%s) -- "
+      "emitting error on each stream", errno, message);
+
+  if (priv->state == JS_STATE_ENDED)
+    {
+      tp_svc_media_session_handler_return_from_error (context);
+      return;
+    }
+  else if (priv->state == JS_STATE_PENDING_CREATED)
+    {
+      /* shortcut to prevent sending remove actions if we haven't sent an
+       * initiate yet */
+      g_object_set (self, "state", JS_STATE_ENDED, NULL);
+      tp_svc_media_session_handler_return_from_error (context);
+      return;
+    }
+
+  g_assert (priv->streams != NULL);
+
+  tmp = priv->streams;
+  priv->streams = NULL;
+
+  for (i = 0; i < tmp->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (tmp, i);
+
+      gabble_media_stream_error (stream, errno, message, NULL);
+    }
+
+  g_ptr_array_free (tmp, TRUE);
+
+  tp_svc_media_session_handler_return_from_error (context);
+}
+
+
+/**
+ * gabble_media_session_ready
+ *
+ * Implements D-Bus method Ready
+ * on interface org.freedesktop.Telepathy.Media.SessionHandler
+ */
+static void
+gabble_media_session_ready (TpSvcMediaSessionHandler *iface,
+                            DBusGMethodInvocation *context)
+{
+  GabbleMediaSession *self = GABBLE_MEDIA_SESSION (iface);
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self);
+
+  if (!priv->ready)
+    {
+      guint i;
+
+      priv->ready = TRUE;
+
+      for (i = 0; i < priv->streams->len; i++)
+        _emit_new_stream (self, g_ptr_array_index (priv->streams, i));
+    }
+
+  tp_svc_media_session_handler_return_from_ready (context);
+}
+
+
+static gboolean
+_handle_create (GabbleMediaSession *session,
+                LmMessage *message,
+                LmMessageNode *content_node,
+                const gchar *stream_name,
+                GabbleMediaStream *stream,
+                LmMessageNode *desc_node,
+                LmMessageNode *trans_node,
+                GError **error)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  GabbleMediaSessionMode session_mode;
+  TpMediaStreamType stream_type;
+  gboolean override_existing = FALSE;
+
+  if ((priv->state == JS_STATE_PENDING_CREATED) &&
+      (session->initiator == INITIATOR_LOCAL))
+    {
+      DEBUG ("we're trying to call ourselves, rejecting with busy");
+      _gabble_media_session_terminate (session, INITIATOR_REMOTE,
+          TP_CHANNEL_GROUP_CHANGE_REASON_BUSY);
+          return FALSE;
+    }
+
+
+  if (stream != NULL)
+    {
+      /* streams added by the session initiator may replace similarly-named
+       * streams which we are trying to add (but havn't had acknowledged) */
+      if (stream->signalling_state < STREAM_SIG_STATE_ACKNOWLEDGED)
+        {
+          if (session->initiator == INITIATOR_REMOTE)
+            {
+              override_existing = TRUE;
+            }
+          else
+            {
+              g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_CONFLICT,
+                  "session initiator is creating a stream named \"%s\" "
+                  "already", stream_name);
+              return FALSE;
+            }
+        }
+      else
+        {
+          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_CONFLICT,
+              "can't create new stream called \"%s\", it already exists, "
+              "rejecting", stream_name);
+          return FALSE;
+        }
+    }
+
+  if (desc_node == NULL)
+    {
+      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
+          "unable to create stream without a content description");
+      return FALSE;
+    }
+
+  if (lm_message_node_has_namespace (desc_node,
+        NS_GOOGLE_SESSION_PHONE, NULL))
+    {
+      session_mode = MODE_GOOGLE;
+      stream_type = TP_MEDIA_STREAM_TYPE_AUDIO;
+    }
+  else if (lm_message_node_has_namespace (desc_node,
+        NS_JINGLE_DESCRIPTION_AUDIO, NULL))
+    {
+      session_mode = MODE_JINGLE;
+      stream_type = TP_MEDIA_STREAM_TYPE_AUDIO;
+    }
+  else if (lm_message_node_has_namespace (desc_node,
+        NS_JINGLE_DESCRIPTION_VIDEO, NULL))
+    {
+      session_mode = MODE_JINGLE;
+      stream_type = TP_MEDIA_STREAM_TYPE_VIDEO;
+    }
+  else
+    {
+      g_set_error (error, GABBLE_XMPP_ERROR,
+          XMPP_ERROR_JINGLE_UNSUPPORTED_CONTENT,
+          "refusing to create stream for unsupported content description");
+      return FALSE;
+    }
+
+  /* MODE_GOOGLE is allowed to have a null transport node */
+  if (session_mode == MODE_JINGLE && trans_node == NULL)
+    {
+      g_set_error (error, GABBLE_XMPP_ERROR,
+          XMPP_ERROR_JINGLE_UNSUPPORTED_TRANSPORT,
+          "refusing to create stream for unsupported transport");
+      return FALSE;
+    }
+
+  if (session_mode != priv->mode)
+    {
+      if (priv->streams->len > 0)
+        {
+          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_UNEXPECTED_REQUEST,
+              "refusing to change mode because streams already exist");
+          return FALSE;
+        }
+      else
+        {
+          GMS_DEBUG_INFO (session, "setting session mode to %s",
+              session_mode == MODE_GOOGLE ? "google" : "jingle");
+          priv->mode = session_mode;
+        }
+    }
+
+  if (override_existing)
+    {
+      GMS_DEBUG_INFO (session, "removing our unacknowledged stream \"%s\" "
+          "in favour of the session initiator's", stream_name);
+
+      /* disappear this stream */
+      destroy_media_stream (session, stream);
+
+      stream = NULL;
+    }
+
+  if (priv->streams->len == MAX_STREAMS)
+    {
+      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_RESOURCE_CONSTRAINT,
+          "refusing to create more than " G_STRINGIFY (MAX_STREAMS)
+          " streams");
+      return FALSE;
+    }
+
+  stream = create_media_stream (session, stream_name, INITIATOR_REMOTE,
+      stream_type);
+
+  /* set the signalling state to ACKNOWLEDGED */
+  g_object_set (stream,
+      "signalling-state", STREAM_SIG_STATE_ACKNOWLEDGED,
+      NULL);
+
+  /* for jingle streams, set the direction to none, so that the
+   * direction handler adds the right flags */
+  if (priv->mode == MODE_JINGLE)
+    g_object_set (stream,
+        "combined-direction", TP_MEDIA_STREAM_DIRECTION_NONE,
+        NULL);
+
+  return TRUE;
+}
+
+
+static TpMediaStreamDirection
+_senders_to_direction (GabbleMediaSession *session,
+                       const gchar *senders)
+{
+  TpMediaStreamDirection ret = TP_MEDIA_STREAM_DIRECTION_NONE;
+
+  if (!tp_strdiff (senders, "initiator"))
+    {
+      if (session->initiator == INITIATOR_LOCAL)
+        ret = TP_MEDIA_STREAM_DIRECTION_SEND;
+      else
+        ret = TP_MEDIA_STREAM_DIRECTION_RECEIVE;
+    }
+  else if (!tp_strdiff (senders, "responder"))
+    {
+      if (session->initiator == INITIATOR_REMOTE)
+        ret = TP_MEDIA_STREAM_DIRECTION_SEND;
+      else
+        ret = TP_MEDIA_STREAM_DIRECTION_RECEIVE;
+    }
+  else if (!tp_strdiff (senders, "both"))
+    {
+      ret = TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL;
+    }
+
+  return ret;
+}
+
+static gboolean
+_handle_direction (GabbleMediaSession *session,
+                   LmMessage *message,
+                   LmMessageNode *content_node,
+                   const gchar *stream_name,
+                   GabbleMediaStream *stream,
+                   LmMessageNode *desc_node,
+                   LmMessageNode *trans_node,
+                   GError **error)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  const gchar *senders;
+  CombinedStreamDirection new_combined_dir;
+  TpMediaStreamDirection requested_dir, current_dir;
+  TpMediaStreamPendingSend pending_send;
+
+  if (priv->mode == MODE_GOOGLE)
+    return TRUE;
+
+  requested_dir = TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL;
+
+  senders = lm_message_node_get_attribute (content_node, "senders");
+  if (senders != NULL)
+    requested_dir = _senders_to_direction (session, senders);
+
+  if (requested_dir == TP_MEDIA_STREAM_DIRECTION_NONE)
+    {
+      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
+          "received invalid content senders value \"%s\" on stream \"%s\"; "
+          "rejecting", senders, stream_name);
+      return FALSE;
+    }
+
+  current_dir = COMBINED_DIRECTION_GET_DIRECTION (stream->combined_direction);
+  pending_send = COMBINED_DIRECTION_GET_PENDING_SEND
+    (stream->combined_direction);
+
+  GMS_DEBUG_INFO (session, "received request for senders \"%s\" on stream "
+      "\"%s\"", senders, stream_name);
+
+  /* if local sending has been added, remove it,
+   * and set the pending local send flag */
+  if (((current_dir & TP_MEDIA_STREAM_DIRECTION_SEND) == 0) &&
+    ((requested_dir & TP_MEDIA_STREAM_DIRECTION_SEND) != 0))
+    {
+      GMS_DEBUG_INFO (session, "setting pending local send flag");
+      requested_dir &= ~TP_MEDIA_STREAM_DIRECTION_SEND;
+      pending_send |= TP_MEDIA_STREAM_PENDING_LOCAL_SEND;
+    }
+
+#if 0
+  /* clear any pending remote send */
+  if ((pending_send & TP_MEDIA_STREAM_PENDING_REMOTE_SEND) != 0)
+    {
+      GMS_DEBUG_INFO (session, "setting pending local send flag");
+      pending_send &= ~TP_MEDIA_STREAM_PENDING_REMOTE_SEND;
+    }
+#endif
+
+  /* make any necessary changes */
+  new_combined_dir = MAKE_COMBINED_DIRECTION (requested_dir, pending_send);
+  if (new_combined_dir != stream->combined_direction)
+    {
+      g_object_set (stream, "combined-direction", new_combined_dir, NULL);
+      _gabble_media_stream_update_sending (stream, FALSE);
+    }
+
+  return TRUE;
+}
+
+
+static gboolean
+_handle_accept (GabbleMediaSession *session,
+                LmMessage *message,
+                LmMessageNode *content_node,
+                const gchar *stream_name,
+                GabbleMediaStream *stream,
+                LmMessageNode *desc_node,
+                LmMessageNode *trans_node,
+                GError **error)
+{
+  g_object_set (stream, "playing", TRUE, NULL);
+
+  _gabble_media_stream_update_sending (stream, TRUE);
+
+  return TRUE;
+}
+
+
+static gboolean
+_handle_codecs (GabbleMediaSession *session,
+                LmMessage *message,
+                LmMessageNode *content_node,
+                const gchar *stream_name,
+                GabbleMediaStream *stream,
+                LmMessageNode *desc_node,
+                LmMessageNode *trans_node,
+                GError **error)
+{
+  if (desc_node == NULL)
+    {
+      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
+          "unable to handle codecs without a content description node");
+      return FALSE;
+    }
+
+  if (!_gabble_media_stream_post_remote_codecs (stream, message, desc_node,
+        error))
+    return FALSE;
+
+  return TRUE;
+}
+
+
+static gboolean
+_handle_candidates (GabbleMediaSession *session,
+                    LmMessage *message,
+                    LmMessageNode *content_node,
+                    const gchar *stream_name,
+                    GabbleMediaStream *stream,
+                    LmMessageNode *desc_node,
+                    LmMessageNode *trans_node,
+                    GError **error)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  if (trans_node == NULL)
+    {
+      if (priv->mode == MODE_GOOGLE)
+        {
+          trans_node = content_node;
+        }
+      else
+        {
+          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
+              "unable to handle candidates without a transport node");
+          return FALSE;
+        }
+    }
+
+  if (!_gabble_media_stream_post_remote_candidates (stream, message,
+        trans_node, error))
+    return FALSE;
+
+  return TRUE;
+}
+
+static guint
+_count_non_removing_streams (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i, ret = 0;
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (stream->signalling_state < STREAM_SIG_STATE_REMOVING)
+        ret++;
+    }
+
+  return ret;
+}
+
+static gboolean
+_handle_remove (GabbleMediaSession *session,
+                LmMessage *message,
+                LmMessageNode *content_node,
+                const gchar *stream_name,
+                GabbleMediaStream *stream,
+                LmMessageNode *desc_node,
+                LmMessageNode *trans_node,
+                GError **error)
+{
+  /* reducing a session to contain 0 streams is invalid; instead the peer
+   * should terminate the session. I guess we'll do it for them... */
+  if (_count_non_removing_streams (session) == 1)
+    {
+      g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
+          "unable to remove the last stream in a Jingle call");
+      return FALSE;
+    }
+
+  /* close the stream */
+  destroy_media_stream (session, stream);
+
+  return TRUE;
+}
+
+
+static gboolean
+_handle_terminate (GabbleMediaSession *session,
+                   LmMessage *message,
+                   LmMessageNode *content_node,
+                   const gchar *stream_name,
+                   GabbleMediaStream *stream,
+                   LmMessageNode *desc_node,
+                   LmMessageNode *trans_node,
+                   GError **error)
+{
+  DEBUG ("called for %s", stream_name);
+
+  _gabble_media_session_terminate (session, INITIATOR_REMOTE,
+      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+  return TRUE;
+}
+
+
+typedef gboolean (*StreamHandlerFunc)(GabbleMediaSession *session,
+                                      LmMessage *message,
+                                      LmMessageNode *content_node,
+                                      const gchar *stream_name,
+                                      GabbleMediaStream *stream,
+                                      LmMessageNode *desc_node,
+                                      LmMessageNode *trans_node,
+                                      GError **error);
+
+typedef struct _Handler Handler;
+
+struct _Handler {
+  const gchar *actions[3];
+  JingleSessionState min_allowed_state;
+  JingleSessionState max_allowed_state;
+  StreamHandlerFunc stream_handlers[4];
+  JingleSessionState new_state;
+};
+
+static Handler handlers[] = {
+  {
+    { "initiate", "session-initiate", NULL },
+    JS_STATE_PENDING_CREATED,
+    JS_STATE_PENDING_CREATED,
+    { _handle_create, _handle_direction, _handle_codecs, NULL },
+    JS_STATE_PENDING_INITIATED
+  },
+  {
+    { "accept", "session-accept", NULL },
+    JS_STATE_PENDING_INITIATED,
+    JS_STATE_PENDING_INITIATED,
+    { _handle_direction, _handle_codecs, _handle_accept, NULL },
+    JS_STATE_ACTIVE
+  },
+  {
+    { "reject", NULL },
+    JS_STATE_PENDING_INITIATE_SENT,
+    JS_STATE_PENDING_INITIATED,
+    { _handle_terminate, NULL },
+    JS_STATE_INVALID
+  },
+  {
+    { "terminate", "session-terminate", NULL },
+    JS_STATE_PENDING_INITIATED,
+    JS_STATE_ENDED,
+    { _handle_terminate, NULL },
+    JS_STATE_INVALID
+  },
+  {
+    { "candidates", "transport-info", NULL },
+    JS_STATE_PENDING_INITIATED,
+    JS_STATE_ACTIVE,
+    { _handle_candidates, NULL },
+    JS_STATE_INVALID
+  },
+  {
+    { "content-add", NULL },
+    JS_STATE_ACTIVE,
+    JS_STATE_ACTIVE,
+    { _handle_create, _handle_direction, _handle_codecs, NULL },
+    JS_STATE_INVALID,
+  },
+  {
+    { "content-modify", NULL },
+    JS_STATE_PENDING_INITIATED,
+    JS_STATE_ACTIVE,
+    { _handle_direction, NULL },
+    JS_STATE_INVALID
+  },
+  {
+    { "content-accept", NULL },
+    JS_STATE_PENDING_INITIATED,
+    JS_STATE_ACTIVE,
+    { _handle_direction, _handle_codecs, _handle_accept, NULL },
+    JS_STATE_INVALID
+  },
+  {
+    { "content-remove", "content-decline", NULL },
+    JS_STATE_PENDING_INITIATED,
+    JS_STATE_ACTIVE,
+    { _handle_remove, NULL },
+    JS_STATE_INVALID
+  },
+  {
+    { NULL },
+    JS_STATE_INVALID,
+    JS_STATE_INVALID,
+    { NULL },
+    JS_STATE_INVALID
+  }
+};
+
+
+static gboolean
+_call_handlers_on_stream (GabbleMediaSession *session,
+                          LmMessage *message,
+                          LmMessageNode *content_node,
+                          const gchar *stream_name,
+                          JingleInitiator stream_creator,
+                          StreamHandlerFunc *func,
+                          GError **error)
+{
+  GabbleMediaStream *stream = NULL;
+  LmMessageNode *desc_node = NULL, *trans_node = NULL;
+  StreamHandlerFunc *tmp;
+  gboolean stream_created = FALSE;
+
+  if (content_node != NULL)
+    {
+      desc_node = lm_message_node_get_child (content_node, "description");
+
+      trans_node = lm_message_node_get_child_with_namespace (content_node,
+          "transport", NS_GOOGLE_TRANSPORT_P2P);
+    }
+
+  for (tmp = func; *tmp != NULL; tmp++)
+    {
+      /* handlers may create the stream */
+      if (stream == NULL && stream_name != NULL)
+        stream = _lookup_stream_by_name_and_initiator (session, stream_name,
+            stream_creator);
+
+      /* the create handler is able to check whether or not the stream
+       * exists, and act accordingly (sometimes it will replace an existing
+       * stream, sometimes it will reject). the termination handler
+       * also requires no stream to do it's job. */
+      if (*tmp != _handle_create && *tmp != _handle_terminate)
+        {
+          /* all other handlers require the stream to exist */
+          if (stream == NULL)
+            {
+              const gchar *created = "";
+
+              if (stream_creator == INITIATOR_LOCAL)
+                created = "locally-created ";
+              else if (stream_creator == INITIATOR_REMOTE)
+                created = "remotely-created ";
+
+              g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_ITEM_NOT_FOUND,
+                  "unable to handle action for unknown %sstream \"%s\" ",
+                  created, stream_name);
+
+              return FALSE;
+            }
+          else
+            {
+              /* don't do anything with actions on streams which have not been
+               * acknowledged, or that we're trying to remove, to deal with
+               * adding/removing race conditions (actions sent by the other end
+               * before they're aware that we've added or removed a stream) */
+              if (stream->signalling_state != STREAM_SIG_STATE_ACKNOWLEDGED)
+                {
+                  GMS_DEBUG_WARNING (session, "ignoring action because stream "
+                      "%s is in state %d, not ACKNOWLEDGED", stream->name,
+                      stream->signalling_state);
+                  return TRUE;
+                }
+            }
+        }
+
+      if (!(*tmp) (session, message, content_node, stream_name, stream,
+            desc_node, trans_node, error))
+        {
+          /* if we successfully created the stream but failed to do something
+           * with it later, remove it */
+          if (stream_created)
+            destroy_media_stream (session, stream);
+
+          return FALSE;
+        }
+
+      if (*tmp == _handle_create)
+        {
+          stream_created = TRUE;
+          /* force a stream lookup after the create handler, even if we
+           * already had one (it has replacement semantics in certain
+           * situations) */
+          stream = NULL;
+        }
+    }
+
+  return TRUE;
+}
+
+
+static JingleInitiator
+_creator_to_initiator (GabbleMediaSession *session, const gchar *creator)
+{
+  if (!tp_strdiff (creator, "initiator"))
+    {
+      if (session->initiator == INITIATOR_LOCAL)
+        return INITIATOR_LOCAL;
+      else
+        return INITIATOR_REMOTE;
+    }
+  else if (!tp_strdiff (creator, "responder"))
+    {
+      if (session->initiator == INITIATOR_LOCAL)
+        return INITIATOR_REMOTE;
+      else
+        return INITIATOR_LOCAL;
+    }
+  else
+    return INITIATOR_INVALID;
+}
+
+
+static gboolean
+_call_handlers_on_streams (GabbleMediaSession *session,
+                           LmMessage *message,
+                           LmMessageNode *session_node,
+                           StreamHandlerFunc *func,
+                           GError **error)
+{
+  LmMessageNode *content_node;
+
+  if (lm_message_node_has_namespace (session_node, NS_GOOGLE_SESSION, NULL))
+    return _call_handlers_on_stream (session, message, session_node,
+        GTALK_STREAM_NAME, INITIATOR_INVALID, func, error);
+
+  if (session_node->children == NULL)
+    return _call_handlers_on_stream (session, message, NULL, NULL,
+        INITIATOR_INVALID, func, error);
+
+  for (content_node = session_node->children;
+       NULL != content_node;
+       content_node = content_node->next)
+    {
+      const gchar *stream_name, *stream_creator;
+      JingleInitiator stream_initiator;
+
+      if (tp_strdiff (content_node->name, "content"))
+        continue;
+
+      stream_name = lm_message_node_get_attribute (content_node, "name");
+
+      if (stream_name == NULL)
+        {
+          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
+              "rejecting content node with no name");
+          return FALSE;
+        }
+
+      stream_creator = lm_message_node_get_attribute (content_node, "creator");
+      stream_initiator = _creator_to_initiator (session, stream_creator);
+
+      /* we allow NULL creator to mean INITIATOR_INVALID for backwards
+       * compatibility with clients that don't put a creator attribute in */
+      if (stream_creator != NULL && stream_initiator == INITIATOR_INVALID)
+        {
+          g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
+              "rejecting content node with invalid creators value");
+          return FALSE;
+        }
+
+      if (!_call_handlers_on_stream (session, message, content_node,
+            stream_name, stream_initiator, func, error))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+
+gboolean
+_gabble_media_session_handle_action (GabbleMediaSession *session,
+                                     LmMessage *message,
+                                     LmMessageNode *session_node,
+                                     const gchar *action,
+                                     GError **error)
+{
+  GabbleMediaSessionPrivate *priv;
+  StreamHandlerFunc *funcs = NULL;
+  JingleSessionState new_state = JS_STATE_INVALID;
+  Handler *i;
+  const gchar **tmp;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (session));
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  GMS_DEBUG_INFO (session, "got jingle session action \"%s\" from peer",
+      action);
+
+  /* do the state machine dance */
+
+  /* search the table of handlers for the action */
+  for (i = handlers; NULL != i->actions[0]; i++)
+    {
+      for (tmp = i->actions; NULL != *tmp; tmp++)
+        if (0 == strcmp (*tmp, action))
+          break;
+
+      if (NULL == *tmp)
+        continue;
+
+      /* if we're outside the allowable states for this action, return an error
+       * immediately */
+      if (priv->state < i->min_allowed_state ||
+          priv->state > i->max_allowed_state)
+        {
+          g_set_error (error, GABBLE_XMPP_ERROR,
+              XMPP_ERROR_JINGLE_OUT_OF_ORDER,
+              "action \"%s\" not allowed in current state", action);
+          goto ERROR;
+        }
+
+      funcs = i->stream_handlers;
+      new_state = i->new_state;
+
+      break;
+    }
+
+  /* pointer is not NULL if we found a matching action */
+  if (NULL == funcs)
+    {
+      g_set_error (error, GABBLE_XMPP_ERROR,
+          XMPP_ERROR_FEATURE_NOT_IMPLEMENTED, "action \"%s\" not implemented",
+          action);
+      goto ERROR;
+    }
+
+  /* call handlers if there are any (NULL-terminated array) */
+  if (NULL != *funcs)
+    {
+      if (!_call_handlers_on_streams (session, message, session_node, funcs,
+            error))
+        {
+          if (*error == NULL)
+            g_set_error (error, GABBLE_XMPP_ERROR, XMPP_ERROR_BAD_REQUEST,
+                "unknown error encountered with action \"%s\"",
+                action);
+
+          goto ERROR;
+        }
+    }
+
+  /* acknowledge the IQ before changing the state because the new state
+   * could perform some actions which the other end will only accept
+   * if this action has been acknowledged */
+  _gabble_connection_acknowledge_set_iq (priv->conn, message);
+
+  /* if the action specified a new state to go to, set it */
+  if (JS_STATE_INVALID != new_state)
+    g_object_set (session, "state", new_state, NULL);
+
+  return TRUE;
+
+ERROR:
+  g_assert (error != NULL);
+  GMS_DEBUG_ERROR (session, (*error)->message);
+  return FALSE;
+}
+
+static gboolean
+timeout_session (gpointer data)
+{
+  GabbleMediaSession *session = data;
+
+  DEBUG ("session timed out");
+
+  if (session->initiator == INITIATOR_LOCAL)
+      _gabble_media_session_terminate (session, INITIATOR_LOCAL,
+          TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER);
+  else
+      _gabble_media_session_terminate (session, INITIATOR_LOCAL,
+          TP_CHANNEL_GROUP_CHANGE_REASON_ERROR);
+
+  return FALSE;
+}
+
+static void do_content_add (GabbleMediaSession *, GabbleMediaStream *);
+
+static void
+_add_ready_new_streams (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i;
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      GMS_DEBUG_DUMP (session, "pondering accept-time add for stream: %s, got "
+          "local codecs: %s, initiator: %s, signalling state: %d",
+          stream->name, stream->got_local_codecs ? "true" : "false",
+          stream->initiator == INITIATOR_LOCAL ? "local" : "remote",
+          stream->signalling_state);
+
+      if (stream->got_local_codecs == FALSE)
+        continue;
+
+      if (stream->initiator == INITIATOR_REMOTE)
+        continue;
+
+      if (stream->signalling_state > STREAM_SIG_STATE_NEW)
+        continue;
+
+      do_content_add (session, stream);
+    }
+}
+
+static void
+session_state_changed (GabbleMediaSession *session,
+                       JingleSessionState prev_state,
+                       JingleSessionState new_state)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  GMS_DEBUG_EVENT (session, "state changed from %s to %s",
+                   session_states[prev_state].name,
+                   session_states[new_state].name);
+
+  /*
+   * If the state goes from CREATED to INITIATED (which means the remote
+   * end initiated), set the timer. If, OTOH, we're the end which just sent an
+   * initiate, set the timer.
+   */
+  if ((prev_state == JS_STATE_PENDING_CREATED &&
+       new_state == JS_STATE_PENDING_INITIATED) ||
+      (new_state == JS_STATE_PENDING_INITIATE_SENT))
+    {
+      priv->timer_id =
+        g_timeout_add (DEFAULT_SESSION_TIMEOUT, timeout_session, session);
+    }
+  else if (new_state == JS_STATE_ACTIVE)
+    {
+      g_source_remove (priv->timer_id);
+      priv->timer_id = 0;
+
+      /* signal any streams to the remote end which were added locally &
+       * became ready before the session was accepted, so haven't been
+       * mentioned yet */
+      _add_ready_new_streams (session);
+    }
+}
+
+static void
+_mark_local_streams_sent (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i;
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (stream->initiator == INITIATOR_REMOTE)
+        continue;
+
+      GMS_DEBUG_INFO (session, "marking local stream %s as signalled",
+          stream->name);
+
+      g_object_set (stream, "signalling-state", STREAM_SIG_STATE_SENT, NULL);
+    }
+}
+
+static void
+_mark_local_streams_acked (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i;
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (stream->initiator == INITIATOR_REMOTE)
+        continue;
+
+      if (stream->signalling_state != STREAM_SIG_STATE_SENT)
+        continue;
+
+      GMS_DEBUG_INFO (session, "marking local stream %s as acknowledged",
+          stream->name);
+
+      g_object_set (stream,
+          "signalling-state", STREAM_SIG_STATE_ACKNOWLEDGED,
+          NULL);
+    }
+}
+
+static void
+_set_remote_streams_playing (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i;
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (stream->initiator == INITIATOR_LOCAL)
+        continue;
+
+      GMS_DEBUG_INFO (session, "setting remote stream %s as playing",
+          stream->name);
+
+      g_object_set (stream, "playing", TRUE, NULL);
+    }
+}
+
+static const gchar *_direction_to_senders (GabbleMediaSession *,
+    TpMediaStreamDirection);
+
+static void
+_add_content_descriptions_one (GabbleMediaSession *session,
+                               GabbleMediaStream *stream,
+                               LmMessageNode *session_node)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  LmMessageNode *content_node;
+
+  if (priv->mode == MODE_GOOGLE)
+    {
+      content_node = session_node;
+    }
+  else
+    {
+      TpMediaStreamDirection direction;
+      TpMediaStreamPendingSend pending_send;
+
+      content_node = _gabble_media_stream_add_content_node (stream,
+          session_node);
+
+      direction = COMBINED_DIRECTION_GET_DIRECTION (stream->combined_direction);
+      pending_send = COMBINED_DIRECTION_GET_PENDING_SEND
+        (stream->combined_direction);
+
+      /* if we have a pending local send flag set, the signalled (ie understood
+       * by both ends) direction of the stream is assuming that we are actually
+       * sending, so we should OR that into the direction before deciding what
+       * to signal the stream with. we don't need to consider pending remote
+       * send because it doesn't happen in Jingle */
+
+      if ((pending_send & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0)
+        direction |= TP_MEDIA_STREAM_DIRECTION_SEND;
+
+      if (direction != TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL)
+        {
+          const gchar *senders;
+          senders = _direction_to_senders (session, direction);
+          lm_message_node_set_attribute (content_node, "senders", senders);
+        }
+    }
+
+  _gabble_media_stream_content_node_add_description (stream, content_node);
+
+  _gabble_media_stream_content_node_add_transport (stream, content_node);
+}
+
+static void
+_add_content_descriptions (GabbleMediaSession *session,
+                           LmMessageNode *session_node,
+                           JingleInitiator stream_initiator)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i;
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (stream->initiator != stream_initiator)
+        {
+          GMS_DEBUG_INFO (session,
+              "not adding content description for %s stream %s",
+              stream->initiator == INITIATOR_LOCAL ? "local" : "remote",
+              stream->name);
+          continue;
+        }
+
+      _add_content_descriptions_one (session, stream, session_node);
+    }
+}
+
+static LmHandlerResult
+accept_msg_reply_cb (GabbleConnection *conn,
+                     LmMessage *sent_msg,
+                     LmMessage *reply_msg,
+                     GObject *object,
+                     gpointer user_data)
+{
+  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i;
+
+  MSG_REPLY_CB_END_SESSION_IF_NOT_SUCCESSFUL (session, "accept failed");
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (stream->initiator == INITIATOR_LOCAL)
+        continue;
+
+      _gabble_media_stream_update_sending (stream, TRUE);
+    }
+
+  g_object_set (session, "state", JS_STATE_ACTIVE, NULL);
+
+  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
+}
+
+static gboolean
+_stream_not_ready_for_accept (GabbleMediaSession *session,
+                              GabbleMediaStream *stream)
+{
+  /* locally initiated streams shouldn't delay acceptance */
+  if (stream->initiator == INITIATOR_LOCAL)
+    return FALSE;
+
+  if (!stream->got_local_codecs)
+    {
+      GMS_DEBUG_INFO (session, "stream %s does not yet have local codecs",
+          stream->name);
+
+      return TRUE;
+    }
+
+  if (stream->connection_state != TP_MEDIA_STREAM_STATE_CONNECTED)
+    {
+      GMS_DEBUG_INFO (session, "stream %s is not yet connected", stream->name);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+try_session_accept (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  LmMessage *msg;
+  LmMessageNode *session_node;
+  const gchar *action;
+  guint i;
+
+  if (priv->state < JS_STATE_ACTIVE && !priv->locally_accepted)
+    {
+      GMS_DEBUG_INFO (session, "not sending accept yet, waiting for local "
+          "user to accept call");
+      return;
+    }
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (_stream_not_ready_for_accept (session, stream))
+        {
+          GMS_DEBUG_INFO (session, "not sending accept yet, found a stream "
+              "which was not yet connected or was missing local codecs");
+          return;
+        }
+    }
+
+  if (priv->mode == MODE_GOOGLE)
+    action = "accept";
+  else
+    action = "session-accept";
+
+  /* construct a session acceptance message */
+  msg = _gabble_media_session_message_new (session, action, &session_node);
+
+  /* only accept REMOTE streams; any LOCAL streams were added by the local
+   * user before accepting and should be signalled after the accept */
+  _add_content_descriptions (session, session_node, INITIATOR_REMOTE);
+
+  GMS_DEBUG_INFO (session, "sending jingle session action \"%s\" to peer",
+      action);
+
+  /* send the final acceptance message */
+  _gabble_connection_send_with_reply (priv->conn, msg, accept_msg_reply_cb,
+                                      G_OBJECT (session), NULL, NULL);
+
+  lm_message_unref (msg);
+
+  /* set remote streams playing */
+  _set_remote_streams_playing (session);
+
+  g_object_set (session, "state", JS_STATE_PENDING_ACCEPT_SENT, NULL);
+}
+
+static LmHandlerResult
+content_accept_msg_reply_cb (GabbleConnection *conn,
+                             LmMessage *sent_msg,
+                             LmMessage *reply_msg,
+                             GObject *object,
+                             gpointer user_data)
+{
+  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (user_data);
+  GabbleMediaStream *stream = GABBLE_MEDIA_STREAM (object);
+
+  if (lm_message_get_sub_type (reply_msg) != LM_MESSAGE_SUB_TYPE_RESULT)
+    {
+      GMS_DEBUG_ERROR (session, "content-accept failed; removing stream");
+      NODE_DEBUG (sent_msg->node, "message sent");
+      NODE_DEBUG (reply_msg->node, "message reply");
+
+      _gabble_media_session_remove_streams (session, &stream, 1);
+
+      return LM_HANDLER_RESULT_REMOVE_MESSAGE;
+    }
+
+  _gabble_media_stream_update_sending (stream, TRUE);
+
+  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
+}
+
+static void
+try_content_accept (GabbleMediaSession *session,
+                    GabbleMediaStream *stream)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  LmMessage *msg;
+  LmMessageNode *session_node;
+
+  g_assert (priv->state == JS_STATE_ACTIVE);
+  g_assert (priv->mode == MODE_JINGLE);
+
+  if (_stream_not_ready_for_accept (session, stream))
+    {
+      GMS_DEBUG_INFO (session, "not sending content-accept yet, stream %s "
+          "is disconnected or missing local codecs", stream->name);
+      return;
+    }
+
+  /* send a content acceptance message */
+  msg = _gabble_media_session_message_new (session, "content-accept",
+      &session_node);
+
+  _add_content_descriptions_one (session, stream, session_node);
+
+  GMS_DEBUG_INFO (session, "sending jingle session action \"content-accept\" "
+      "to peer for stream %s", stream->name);
+
+  _gabble_connection_send_with_reply (priv->conn, msg,
+      content_accept_msg_reply_cb, G_OBJECT (stream), session, NULL);
+
+  lm_message_unref (msg);
+
+  /* set stream playing */
+  g_object_set (stream, "playing", TRUE, NULL);
+}
+
+static LmHandlerResult
+initiate_msg_reply_cb (GabbleConnection *conn,
+                       LmMessage *sent_msg,
+                       LmMessage *reply_msg,
+                       GObject *object,
+                       gpointer user_data)
+{
+  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
+
+  MSG_REPLY_CB_END_SESSION_IF_NOT_SUCCESSFUL (session, "initiate failed");
+
+  g_object_set (session, "state", JS_STATE_PENDING_INITIATED, NULL);
+
+  /* mark all of the streams that we sent in the initiate as acknowledged */
+  _mark_local_streams_acked (session);
+
+  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
+}
+
+static gboolean
+_stream_not_ready_for_initiate (GabbleMediaSession *session,
+                                GabbleMediaStream *stream)
+{
+  if (!stream->got_local_codecs)
+    {
+      GMS_DEBUG_INFO (session, "stream %s does not yet have local codecs",
+          stream->name);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+try_session_initiate (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  LmMessage *msg;
+  LmMessageNode *session_node;
+  const gchar *action;
+  guint i;
+
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+
+      if (_stream_not_ready_for_initiate (session, stream))
+        {
+          GMS_DEBUG_INFO (session, "not sending initiate yet, found a stream "
+            "which was missing local codecs");
+          return;
+        }
+    }
+
+  if (priv->mode == MODE_GOOGLE)
+      action = "initiate";
+  else
+      action = "session-initiate";
+
+  msg = _gabble_media_session_message_new (session, action, &session_node);
+
+  _add_content_descriptions (session, session_node, INITIATOR_LOCAL);
+
+  GMS_DEBUG_INFO (session, "sending jingle action \"%s\" to peer", action);
+
+  _gabble_connection_send_with_reply (priv->conn, msg, initiate_msg_reply_cb,
+                                      G_OBJECT (session), NULL, NULL);
+
+  lm_message_unref (msg);
+
+  /* mark local streams as sent (so that eg candidates will be sent) */
+  _mark_local_streams_sent (session);
+
+  g_object_set (session, "state", JS_STATE_PENDING_INITIATE_SENT, NULL);
+}
+
+static LmHandlerResult
+content_add_msg_reply_cb (GabbleConnection *conn,
+                          LmMessage *sent_msg,
+                          LmMessage *reply_msg,
+                          GObject *object,
+                          gpointer user_data)
+{
+  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (user_data);
+  GabbleMediaStream *stream = GABBLE_MEDIA_STREAM (object);
+
+  if (lm_message_get_sub_type (reply_msg) != LM_MESSAGE_SUB_TYPE_RESULT)
+    {
+      if (session->initiator == INITIATOR_REMOTE &&
+          stream->signalling_state == STREAM_SIG_STATE_ACKNOWLEDGED)
+        {
+          GMS_DEBUG_INFO (session, "ignoring content-add failure, stream has "
+              "been successfully created by the session initiator");
+        }
+      else
+        {
+          GMS_DEBUG_ERROR (session, "content-add failed; removing stream");
+          NODE_DEBUG (sent_msg->node, "message sent");
+          NODE_DEBUG (reply_msg->node, "message reply");
+
+          _gabble_media_stream_close (stream);
+        }
+    }
+  else
+    {
+      if (stream->signalling_state == STREAM_SIG_STATE_SENT)
+        {
+          GMS_DEBUG_INFO (session, "content-add succeeded, marking stream as "
+              "ACKNOWLEDGED");
+
+          g_object_set (stream,
+              "signalling-state", STREAM_SIG_STATE_ACKNOWLEDGED,
+              NULL);
+        }
+      else
+        {
+          GMS_DEBUG_INFO (session, "content-add succeeded, but not marking"
+              "stream as ACKNOWLEDGED, it's in state %d",
+              stream->signalling_state);
+        }
+    }
+
+  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
+}
+
+static void
+do_content_add (GabbleMediaSession *session,
+                GabbleMediaStream *stream)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  LmMessage *msg;
+  LmMessageNode *session_node;
+
+  g_assert (priv->state == JS_STATE_ACTIVE);
+  g_assert (priv->mode == MODE_JINGLE);
+
+  if (_stream_not_ready_for_initiate (session, stream))
+    {
+      GMS_DEBUG_ERROR (session, "trying to send content-add for stream %s "
+          "but we have no local codecs. what?!", stream->name);
+      g_assert_not_reached ();
+      return;
+    }
+
+  msg = _gabble_media_session_message_new (session, "content-add",
+      &session_node);
+
+  _add_content_descriptions_one (session, stream, session_node);
+
+  GMS_DEBUG_INFO (session, "sending jingle action \"content-add\" to peer for "
+      "stream %s", stream->name);
+
+  _gabble_connection_send_with_reply (priv->conn, msg,
+      content_add_msg_reply_cb, G_OBJECT (stream), session, NULL);
+
+  lm_message_unref (msg);
+
+  /* mark stream as sent */
+  g_object_set (stream, "signalling-state", STREAM_SIG_STATE_SENT, NULL);
+}
+
+static void
+stream_connection_state_changed_cb (GabbleMediaStream *stream,
+                                    GParamSpec *param,
+                                    GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (session));
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  if (stream->connection_state != TP_MEDIA_STREAM_STATE_CONNECTED)
+    return;
+
+  GMS_DEBUG_INFO (session, "stream %s has gone connected", stream->name);
+
+  if (stream->playing)
+    {
+      GMS_DEBUG_INFO (session, "doing nothing, stream is already playing");
+      return;
+    }
+
+  /* after session is active, we do things per-stream with content-* actions */
+  if (priv->state < JS_STATE_ACTIVE)
+    {
+      /* send a session accept if the session was initiated by the peer */
+      if (session->initiator == INITIATOR_REMOTE)
+        {
+          try_session_accept (session);
+        }
+      else
+        {
+          GMS_DEBUG_INFO (session, "session initiated by us, so we're not "
+              "going to consider sending an accept");
+        }
+    }
+  else
+    {
+      /* send a content accept if the stream was added by the peer */
+      if (stream->initiator == INITIATOR_REMOTE)
+        {
+          try_content_accept (session, stream);
+        }
+      else
+        {
+          GMS_DEBUG_INFO (session, "stream added by us, so we're not going "
+              "to send an accept");
+        }
+    }
+}
+
+static void
+stream_got_local_codecs_changed_cb (GabbleMediaStream *stream,
+                                    GParamSpec *param,
+                                    GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (session));
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  if (!stream->got_local_codecs)
+    return;
+
+  GMS_DEBUG_INFO (session, "stream %s has got local codecs", stream->name);
+
+  if (stream->playing)
+    {
+      GMS_DEBUG_ERROR (session, "stream was already playing and we got local "
+          "codecs. what?!");
+      g_assert_not_reached ();
+      return;
+    }
+
+  /* after session is active, we do things per-stream with content-* actions */
+  if (priv->state < JS_STATE_ACTIVE)
+    {
+      if (session->initiator == INITIATOR_REMOTE)
+        {
+          if (priv->state < JS_STATE_PENDING_ACCEPT_SENT)
+            {
+              try_session_accept (session);
+            }
+          else
+            {
+              GMS_DEBUG_INFO (session, "stream added after sending accept; "
+                  "not doing content-add until remote end acknowledges");
+            }
+        }
+      else
+        {
+          if (priv->state < JS_STATE_PENDING_INITIATE_SENT)
+            {
+              try_session_initiate (session);
+            }
+          else
+            {
+              GMS_DEBUG_INFO (session, "stream added after sending initiate; "
+                  "not doing content-add until remote end accepts");
+            }
+        }
+    }
+  else
+    {
+      if (stream->initiator == INITIATOR_REMOTE)
+        {
+          try_content_accept (session, stream);
+        }
+      else
+        {
+          do_content_add (session, stream);
+        }
+    }
+}
+
+static gchar *
+get_jid_for_contact (GabbleMediaSession *session,
+                     TpHandle handle)
+{
+  GabbleMediaSessionPrivate *priv;
+  TpBaseConnection *conn;
+  const gchar *base_jid;
+  TpHandle self;
+  TpHandleRepoIface *contact_handles;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (session));
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  conn = (TpBaseConnection *)priv->conn;
+  contact_handles = tp_base_connection_get_handles (conn,
+      TP_HANDLE_TYPE_CONTACT);
+  self = conn->self_handle;
+
+  base_jid = tp_handle_inspect (contact_handles, handle);
+  g_assert (base_jid != NULL);
+
+  if (handle == self)
+    {
+      gchar *resource, *ret;
+      g_object_get (priv->conn, "resource", &resource, NULL);
+      g_assert (resource != NULL);
+      ret = g_strdup_printf ("%s/%s", base_jid, resource);
+      g_free (resource);
+      return ret;
+    }
+  else
+    {
+      g_assert (priv->peer_resource != NULL);
+      return g_strdup_printf ("%s/%s", base_jid, priv->peer_resource);
+    }
+}
+
+LmMessage *
+_gabble_media_session_message_new (GabbleMediaSession *session,
+                                   const gchar *action,
+                                   LmMessageNode **session_node)
+{
+  GabbleMediaSessionPrivate *priv;
+  TpBaseConnection *conn;
+  LmMessage *msg;
+  LmMessageNode *iq_node, *node;
+  gchar *peer_jid, *initiator_jid;
+  TpHandle initiator_handle;
+  const gchar *element, *xmlns;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (session));
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  conn = (TpBaseConnection *)priv->conn;
+
+  peer_jid = get_jid_for_contact (session, priv->peer);
+
+  msg = lm_message_new_with_sub_type (
+      peer_jid,
+      LM_MESSAGE_TYPE_IQ,
+      LM_MESSAGE_SUB_TYPE_SET);
+
+  g_free (peer_jid);
+
+  iq_node = lm_message_get_node (msg);
+
+  if (priv->mode == MODE_GOOGLE)
+    element = "session";
+  else
+    element = "jingle";
+
+  if (session->initiator == INITIATOR_LOCAL)
+    initiator_handle = conn->self_handle;
+  else
+    initiator_handle = priv->peer;
+
+  node = lm_message_node_add_child (iq_node, element, NULL);
+  initiator_jid = get_jid_for_contact (session, initiator_handle);
+
+  lm_message_node_set_attributes (node,
+      (priv->mode == MODE_GOOGLE) ? "id" : "sid", priv->id,
+      (priv->mode == MODE_GOOGLE) ? "type" : "action", action,
+      "initiator", initiator_jid,
+      NULL);
+
+  if (priv->mode == MODE_GOOGLE)
+    xmlns = NS_GOOGLE_SESSION;
+  else
+    xmlns = NS_JINGLE;
+
+  lm_message_node_set_attribute (node, "xmlns", xmlns);
+  g_free (initiator_jid);
+
+  if (session_node)
+    *session_node = node;
+
+  return msg;
+}
+
+void
+_gabble_media_session_accept (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  guint i;
+
+  priv->locally_accepted = TRUE;
+
+  /* accept any local pending sends */
+  for (i = 0; i < priv->streams->len; i++)
+    {
+      GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+      CombinedStreamDirection combined_dir = stream->combined_direction;
+      TpMediaStreamDirection current_dir;
+      TpMediaStreamPendingSend pending_send;
+
+      current_dir = COMBINED_DIRECTION_GET_DIRECTION (combined_dir);
+      pending_send = COMBINED_DIRECTION_GET_PENDING_SEND (combined_dir);
+
+      if ((pending_send & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0)
+        {
+          GMS_DEBUG_INFO (session, "accepting pending local send on stream %s",
+              stream->name);
+
+          current_dir |= TP_MEDIA_STREAM_DIRECTION_SEND;
+          pending_send &= ~TP_MEDIA_STREAM_PENDING_LOCAL_SEND;
+          combined_dir = MAKE_COMBINED_DIRECTION (current_dir, pending_send);
+          g_object_set (stream, "combined-direction", combined_dir, NULL);
+          _gabble_media_stream_update_sending (stream, FALSE);
+        }
+    }
+
+  try_session_accept (session);
+}
+
+static LmHandlerResult
+content_remove_msg_reply_cb (GabbleConnection *conn,
+                             LmMessage *sent_msg,
+                             LmMessage *reply_msg,
+                             GObject *object,
+                             gpointer user_data)
+{
+  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (object);
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  GPtrArray *removing = (GPtrArray *) user_data;
+  guint i;
+
+  MSG_REPLY_CB_END_SESSION_IF_NOT_SUCCESSFUL (session,
+      "stream removal failed");
+
+  for (i = 0; i < removing->len; i++)
+    destroy_media_stream (session,
+        GABBLE_MEDIA_STREAM (g_ptr_array_index (removing, i)));
+
+  g_ptr_array_remove_fast (priv->remove_requests, removing);
+  g_ptr_array_free (removing, TRUE);
+
+  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
+}
+
+void
+_gabble_media_session_remove_streams (GabbleMediaSession *session,
+                                      GabbleMediaStream **streams,
+                                      guint len)
+{
+  GabbleMediaSessionPrivate *priv;
+  LmMessage *msg = NULL;
+  LmMessageNode *session_node;
+  GPtrArray *removing = NULL;
+  guint i;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (session));
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  /* end the session if there'd be no streams left after reducing it */
+  if (_count_non_removing_streams (session) == len)
+    {
+      _gabble_media_session_terminate (session, INITIATOR_LOCAL,
+          TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+      return;
+    }
+
+  /* construct a remove message if we're in a state greater than CREATED (ie
+   * something has been sent/received about this session) */
+  if (priv->state > JS_STATE_PENDING_CREATED)
+    {
+      msg = _gabble_media_session_message_new (session, "content-remove",
+          &session_node);
+      removing = g_ptr_array_sized_new (len);
+    }
+
+  /* right, remove them */
+  for (i = 0; i < len; i++)
+    {
+      GabbleMediaStream *stream = streams[i];
+
+      switch (stream->signalling_state)
+        {
+        case STREAM_SIG_STATE_NEW:
+          destroy_media_stream (session, stream);
+          break;
+        case STREAM_SIG_STATE_SENT:
+        case STREAM_SIG_STATE_ACKNOWLEDGED:
+          {
+            g_assert (priv->state > JS_STATE_PENDING_CREATED);
+            g_assert (msg != NULL);
+            g_assert (removing != NULL);
+            g_assert (session_node != NULL);
+
+            _gabble_media_stream_add_content_node (stream, session_node);
+
+            g_object_set (stream,
+                "playing", FALSE,
+                "signalling-state", STREAM_SIG_STATE_REMOVING,
+                NULL);
+
+            /* close the stream now, but don't forget about it until the
+             * removal message is acknowledged, since we need to be able to
+             * detect content-remove cross-talk */
+            _gabble_media_stream_close (stream);
+            g_ptr_array_add (removing, stream);
+          }
+          break;
+        case STREAM_SIG_STATE_REMOVING:
+          break;
+        }
+    }
+
+  /* send the remove message if necessary */
+  if (msg != NULL)
+    {
+      if (removing->len > 0)
+        {
+          GMS_DEBUG_INFO (session, "sending jingle session action "
+              "\"content-remove\" to peer");
+
+          _gabble_connection_send_with_reply (priv->conn, msg,
+              content_remove_msg_reply_cb, G_OBJECT (session), removing, NULL);
+
+          g_ptr_array_add (priv->remove_requests, removing);
+        }
+      else
+        {
+          g_ptr_array_free (removing, TRUE);
+        }
+
+      lm_message_unref (msg);
+    }
+  else
+    {
+      GMS_DEBUG_INFO (session, "not sending jingle session action "
+          "\"content-remove\" to peer, no initiates or adds sent for "
+          "these streams");
+      if (priv->state < JS_STATE_PENDING_INITIATE_SENT)
+        {
+           try_session_initiate (session);
+        }
+    }
+}
+
+static void
+send_reject_message (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  LmMessage *msg;
+  LmMessageNode *session_node;
+
+  /* this should only happen in google mode, and we should only arrive in that
+   * mode when we've ended up talking to a resource that doesn't support
+   * jingle */
+  g_assert (priv->mode == MODE_GOOGLE);
+  g_assert (priv->peer_resource != NULL);
+
+  /* construct a session terminate message */
+  msg = _gabble_media_session_message_new (session, "reject", &session_node);
+
+  GMS_DEBUG_INFO (session, "sending jingle session action \"reject\" to peer");
+
+  /* send it */
+  _gabble_connection_send_with_reply (priv->conn, msg, NULL,
+                                      G_OBJECT (session), NULL, NULL);
+
+  lm_message_unref (msg);
+}
+
+static void
+send_terminate_message (GabbleMediaSession *session)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  const gchar *action;
+  LmMessage *msg;
+  LmMessageNode *session_node;
+
+  /* construct a session terminate message */
+  if (priv->mode == MODE_GOOGLE)
+    action = "terminate";
+  else
+    action = "session-terminate";
+
+  msg = _gabble_media_session_message_new (session, action, &session_node);
+
+  GMS_DEBUG_INFO (session, "sending jingle session action \"%s\" to peer",
+      action);
+
+  /* send it */
+  _gabble_connection_send_with_reply (priv->conn, msg, NULL,
+                                      G_OBJECT (session), NULL, NULL);
+
+  lm_message_unref (msg);
+}
+
+void
+_gabble_media_session_terminate (GabbleMediaSession *session,
+                                 JingleInitiator who,
+                                 TpChannelGroupChangeReason why)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  TpBaseConnection *conn = (TpBaseConnection *)priv->conn;
+  TpHandle actor;
+
+  if (priv->state == JS_STATE_ENDED)
+    return;
+
+  if (who == INITIATOR_REMOTE)
+    {
+      actor = priv->peer;
+    }
+  else
+    {
+      actor = conn->self_handle;
+
+      /* Need to tell them that it's all over. */
+
+      /* Jingle doesn't have a "reject" action; a termination before an
+       * acceptance indicates that the call has been declined */
+
+      if (session->initiator == INITIATOR_REMOTE &&
+          priv->state == JS_STATE_PENDING_INITIATED &&
+          priv->mode == MODE_GOOGLE)
+        {
+          send_reject_message (session);
+        }
+
+      /* if we're still in CREATED, then we've not sent or received any
+       * messages about this session yet, so no terminate is necessary */
+      else if (priv->state > JS_STATE_PENDING_CREATED)
+        {
+          send_terminate_message (session);
+        }
+
+      while (priv->streams->len > 0)
+        destroy_media_stream (session, g_ptr_array_index (priv->streams, 0));
+    }
+
+  priv->terminated = TRUE;
+  g_object_set (session, "state", JS_STATE_ENDED, NULL);
+  g_signal_emit (session, signals[TERMINATED], 0, actor, why);
+}
+
+#if defined (ENABLE_DEBUG) && _GMS_DEBUG_LEVEL
+void
+_gabble_media_session_debug (GabbleMediaSession *session,
+                             DebugMessageType type,
+                             const gchar *format, ...)
+{
+  if (DEBUGGING)
+    {
+      va_list list;
+      gchar buf[512];
+      GabbleMediaSessionPrivate *priv;
+      time_t curtime;
+      struct tm *loctime;
+      gchar stamp[10];
+      const gchar *type_str;
+
+      g_assert (GABBLE_IS_MEDIA_SESSION (session));
+
+      priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+      curtime = time (NULL);
+      loctime = localtime (&curtime);
+
+      strftime (stamp, sizeof (stamp), "%T", loctime);
+
+      va_start (list, format);
+
+      vsnprintf (buf, sizeof (buf), format, list);
+
+      va_end (list);
+
+      switch (type) {
+        case DEBUG_MSG_INFO:
+          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_WHITE;
+          break;
+        case DEBUG_MSG_DUMP:
+          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_GREEN;
+          break;
+        case DEBUG_MSG_WARNING:
+          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_YELLOW;
+          break;
+        case DEBUG_MSG_ERROR:
+          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_WHITE TP_ANSI_BG_RED;
+          break;
+        case DEBUG_MSG_EVENT:
+          type_str = TP_ANSI_BOLD_ON TP_ANSI_FG_CYAN;
+          break;
+        default:
+          g_assert_not_reached ();
+          return;
+      }
+
+      printf ("[%s%s%s] %s%-26s%s %s%s%s\n",
+          TP_ANSI_BOLD_ON TP_ANSI_FG_WHITE,
+          stamp,
+          TP_ANSI_RESET,
+          session_states[priv->state].attributes,
+          session_states[priv->state].name,
+          TP_ANSI_RESET,
+          type_str,
+          buf,
+          TP_ANSI_RESET);
+
+      fflush (stdout);
+    }
+}
+
+#endif /* _GMS_DEBUG_LEVEL */
+
+static const gchar *
+_name_stream (GabbleMediaSession *session,
+              TpMediaStreamType media_type)
+{
+  GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  static gchar ret[MAX_STREAM_NAME_LEN] = GTALK_STREAM_NAME;
+
+  if (priv->mode != MODE_GOOGLE)
+    {
+      guint i = 1;
+
+      do {
+          g_snprintf (ret, MAX_STREAM_NAME_LEN, "%s%u",
+              media_type == TP_MEDIA_STREAM_TYPE_AUDIO ? "audio" : "video",
+              i++);
+
+          /* even though we now have seperate namespaces for local and remote,
+           * actually check in both so that we can still support clients which
+           * have 1 namespace (such as our older selves :D) */
+          if (_lookup_stream_by_name_and_initiator (session, ret,
+                INITIATOR_INVALID) != NULL)
+            {
+              ret[0] = '\0';
+            }
+      } while (ret[0] == '\0');
+    }
+
+  return ret;
+}
+
+
+gboolean
+_gabble_media_session_request_streams (GabbleMediaSession *session,
+                                       const GArray *media_types,
+                                       GPtrArray **ret,
+                                       GError **error)
+{
+  /* if you change the caps in this function, you almost certainly also need
+   * to change _gabble_media_channel_typeflags_to_caps and
+   * _gabble_media_channel_caps_to_typeflags in media-channel.c */
+  static GabblePresenceCapabilities google_audio_caps =
+    PRESENCE_CAP_GOOGLE_VOICE;
+  static GabblePresenceCapabilities jingle_audio_caps =
+    PRESENCE_CAP_JINGLE | PRESENCE_CAP_JINGLE_DESCRIPTION_AUDIO |
+    PRESENCE_CAP_GOOGLE_TRANSPORT_P2P;
+  static GabblePresenceCapabilities jingle_video_caps =
+    PRESENCE_CAP_JINGLE | PRESENCE_CAP_JINGLE_DESCRIPTION_VIDEO |
+    PRESENCE_CAP_GOOGLE_TRANSPORT_P2P;
+
+  GabbleMediaSessionPrivate *priv;
+  GabblePresence *presence;
+  gboolean want_audio, want_video;
+  GabblePresenceCapabilities jingle_desired_caps;
+  guint idx;
+  gchar *dump;
+
+  g_assert (GABBLE_IS_MEDIA_SESSION (session));
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  presence = gabble_presence_cache_get (priv->conn->presence_cache,
+      priv->peer);
+
+  if (presence == NULL)
+    {
+      g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+          "member has no audio/video capabilities");
+
+      return FALSE;
+    }
+
+  dump = gabble_presence_dump (presence);
+  GMS_DEBUG_DUMP (session, "presence for peer %d:\n%s", priv->peer, dump);
+  g_free (dump);
+
+  want_audio = want_video = FALSE;
+
+  for (idx = 0; idx < media_types->len; idx++)
+    {
+      guint media_type = g_array_index (media_types, guint, idx);
+
+      if (media_type == TP_MEDIA_STREAM_TYPE_AUDIO)
+        {
+          want_audio = TRUE;
+        }
+      else if (media_type == TP_MEDIA_STREAM_TYPE_VIDEO)
+        {
+          want_video = TRUE;
+        }
+      else
+        {
+          g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
+              "given media type %u is invalid", media_type);
+          return FALSE;
+        }
+    }
+
+  /* work out what we'd need to do these streams with jingle */
+  jingle_desired_caps = 0;
+
+  if (want_audio)
+    jingle_desired_caps |= jingle_audio_caps;
+
+  if (want_video)
+    jingle_desired_caps |= jingle_video_caps;
+
+  GMS_DEBUG_INFO (session, "want audio: %s; want video: %s",
+    want_audio ? "yes" : "no", want_video ? "yes" : "no");
+
+  /* existing call; the recipient and the mode has already been decided */
+  if (priv->peer_resource)
+    {
+      /* is a google call... we have no other option */
+      if (priv->mode == MODE_GOOGLE)
+        {
+          GMS_DEBUG_INFO (session, "already in Google mode; can't add new "
+              "stream");
+
+          g_assert (priv->streams->len == 1);
+
+          g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+              "Google Talk calls may only contain one stream");
+
+          return FALSE;
+        }
+
+      if (!gabble_presence_resource_has_caps (presence, priv->peer_resource,
+            jingle_desired_caps))
+        {
+          GMS_DEBUG_INFO (session,
+            "in Jingle mode but have insufficient caps for requested streams");
+
+          g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+              "existing call member doesn't support all requested media"
+              " types");
+
+          return FALSE;
+        }
+
+      GMS_DEBUG_INFO (session,
+        "in Jingle mode, and have necessary caps");
+    }
+
+  /* no existing call; we should choose a recipient and a mode */
+  else
+    {
+      const gchar *resource;
+
+      g_assert (priv->streams->len == 0);
+
+      /* see if we have a fully-capable jingle resource; regardless of the
+       * desired media type it's best if we can add/remove the others later */
+      resource = gabble_presence_pick_resource_by_caps (presence,
+          jingle_audio_caps | jingle_video_caps);
+
+      if (resource == NULL)
+        {
+          GMS_DEBUG_INFO (session, "contact is not fully jingle-capable");
+
+          /* ok, no problem. see if we can do just what's wanted with jingle */
+          resource = gabble_presence_pick_resource_by_caps (presence,
+              jingle_desired_caps);
+
+          if (resource == NULL && want_audio && !want_video)
+            {
+              GMS_DEBUG_INFO (session,
+                "contact doesn't have desired Jingle capabilities");
+
+              /* last ditch... if we want only audio and not video, we can make
+               * do with google talk */
+              resource = gabble_presence_pick_resource_by_caps (presence,
+                  google_audio_caps);
+
+              if (resource != NULL)
+                {
+                  /* only one stream possible with google */
+                  if (media_types->len == 1)
+                    {
+                      GMS_DEBUG_INFO (session,
+                        "contact has no Jingle capabilities; "
+                        "falling back to Google audio call");
+                      priv->mode = MODE_GOOGLE;
+                    }
+                  else
+                    {
+                      g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+                          "Google Talk calls may only contain one stream");
+
+                      return FALSE;
+                    }
+                }
+              else
+                {
+                  GMS_DEBUG_INFO (session,
+                    "contact doesn't have desired Google capabilities");
+                }
+            }
+        }
+
+      if (resource == NULL)
+        {
+          GMS_DEBUG_INFO (session,
+            "contact doesn't have a resource with suitable capabilities");
+
+          g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+              "member does not have the desired audio/video capabilities");
+
+          return FALSE;
+        }
+
+      priv->peer_resource = g_strdup (resource);
+    }
+
+  /* check it's not a ridiculous number of streams */
+  if ((priv->streams->len + media_types->len) > MAX_STREAMS)
+    {
+      g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+          "I think that's quite enough streams already");
+      return FALSE;
+    }
+
+  /* if we've got here, we're good to make the streams */
+
+  *ret = g_ptr_array_sized_new (media_types->len);
+
+  for (idx = 0; idx < media_types->len; idx++)
+    {
+      guint media_type = g_array_index (media_types, guint, idx);
+      GabbleMediaStream *stream;
+      const gchar *stream_name;
+
+      if (priv->mode == MODE_GOOGLE)
+        stream_name = GTALK_STREAM_NAME;
+      else
+        stream_name = _name_stream (session, media_type);
+
+      stream = create_media_stream (session, stream_name, INITIATOR_LOCAL,
+                                    media_type);
+
+      g_ptr_array_add (*ret, stream);
+    }
+
+  return TRUE;
+}
+
+static const gchar *
+_direction_to_senders (GabbleMediaSession *session,
+                       TpMediaStreamDirection dir)
+{
+  const gchar *ret = NULL;
+
+  switch (dir)
+    {
+      case TP_MEDIA_STREAM_DIRECTION_NONE:
+        g_assert_not_reached ();
+        break;
+      case TP_MEDIA_STREAM_DIRECTION_SEND:
+        if (session->initiator == INITIATOR_LOCAL)
+          ret = "initiator";
+        else
+          ret = "responder";
+        break;
+      case TP_MEDIA_STREAM_DIRECTION_RECEIVE:
+        if (session->initiator == INITIATOR_REMOTE)
+          ret = "initiator";
+        else
+          ret = "responder";
+        break;
+      case TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL:
+        ret = "both";
+        break;
+    }
+
+  g_assert (ret != NULL);
+
+  return ret;
+}
+
+static LmHandlerResult
+direction_msg_reply_cb (GabbleConnection *conn,
+                        LmMessage *sent_msg,
+                        LmMessage *reply_msg,
+                        GObject *object,
+                        gpointer user_data)
+{
+  GabbleMediaSession *session = GABBLE_MEDIA_SESSION (user_data);
+  GabbleMediaStream *stream = GABBLE_MEDIA_STREAM (object);
+
+  MSG_REPLY_CB_END_SESSION_IF_NOT_SUCCESSFUL (session,
+      "direction change failed");
+
+  if (stream->playing)
+    {
+      _gabble_media_stream_update_sending (stream, TRUE);
+    }
+
+  return LM_HANDLER_RESULT_REMOVE_MESSAGE;
+}
+
+static gboolean
+send_direction_change (GabbleMediaSession *session,
+                       GabbleMediaStream *stream,
+                       TpMediaStreamDirection dir,
+                       GError **error)
+{
+  GabbleMediaSessionPrivate *priv;
+  const gchar *senders;
+  LmMessage *msg;
+  LmMessageNode *session_node, *content_node;
+  gboolean ret;
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+  senders = _direction_to_senders (session, dir);
+
+  if (stream->signalling_state == STREAM_SIG_STATE_NEW ||
+      stream->signalling_state == STREAM_SIG_STATE_REMOVING)
+    {
+      GMS_DEBUG_INFO (session, "not sending content-modify for %s stream %s",
+          stream->signalling_state == STREAM_SIG_STATE_NEW ? "new" : "removing",
+          stream->name);
+      return TRUE;
+    }
+
+  GMS_DEBUG_INFO (session, "sending jingle session action \"content-modify\" "
+      "to peer for stream %s (senders=%s)", stream->name, senders);
+
+  msg = _gabble_media_session_message_new (session, "content-modify",
+      &session_node);
+  content_node = _gabble_media_stream_add_content_node (stream, session_node);
+
+  lm_message_node_set_attribute (content_node, "senders", senders);
+
+  ret = _gabble_connection_send_with_reply (priv->conn, msg,
+      direction_msg_reply_cb, G_OBJECT (stream), session, error);
+
+  lm_message_unref (msg);
+
+  return ret;
+}
+
+gboolean
+_gabble_media_session_request_stream_direction (GabbleMediaSession *session,
+                                                GabbleMediaStream *stream,
+                                                TpMediaStreamDirection requested_dir,
+                                                GError **error)
+{
+  GabbleMediaSessionPrivate *priv;
+  CombinedStreamDirection new_combined_dir;
+  TpMediaStreamDirection current_dir; //, new_dir;
+  TpMediaStreamPendingSend pending_send;
+
+  priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session);
+
+  current_dir = COMBINED_DIRECTION_GET_DIRECTION (stream->combined_direction);
+  pending_send = COMBINED_DIRECTION_GET_PENDING_SEND
+    (stream->combined_direction);
+
+  if (priv->mode == MODE_GOOGLE)
+    {
+      g_assert (current_dir == TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL);
+
+      if (requested_dir == TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL)
+        return TRUE;
+
+      g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+          "Google Talk calls can only be bi-directional");
+      return FALSE;
+    }
+
+  if (requested_dir == TP_MEDIA_STREAM_DIRECTION_NONE)
+    {
+      GMS_DEBUG_INFO (session, "request for NONE direction; removing stream");
+
+      _gabble_media_session_remove_streams (session, &stream, 1);
+
+      return TRUE;
+    }
+
+  /* if we're awaiting a local decision on sending... */
+  if ((pending_send & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0)
+    {
+      /* clear the flag */
+      pending_send &= ~TP_MEDIA_STREAM_PENDING_LOCAL_SEND;
+
+      /* make our current_dir match what other end thinks (he thinks we're
+       * bidirectional) so that we send the correct transitions */
+      current_dir ^= TP_MEDIA_STREAM_DIRECTION_SEND;
+    }
+
+#if 0
+  /* if we're asking the remote end to start sending, set the pending flag and
+   * don't change our directionality just yet */
+  new_dir = requested_dir;
+  if (((current_dir & TP_MEDIA_STREAM_DIRECTION_RECEIVE) == 0) &&
+      ((new_dir & TP_MEDIA_STREAM_DIRECTION_RECEIVE) != 0))
+    {
+      pending_send ^= TP_MEDIA_STREAM_PENDING_REMOTE_SEND;
+      new_dir &= ~TP_MEDIA_STREAM_DIRECTION_RECEIVE;
+    }
+#endif
+
+  /* make any necessary changes */
+  new_combined_dir = MAKE_COMBINED_DIRECTION (requested_dir, pending_send);
+  if (new_combined_dir != stream->combined_direction)
+    {
+      g_object_set (stream, "combined-direction", new_combined_dir, NULL);
+      _gabble_media_stream_update_sending (stream, FALSE);
+    }
+
+  /* short-circuit sending a request if we're not asking for anything new */
+  if (current_dir == requested_dir)
+    return TRUE;
+
+  /* send request */
+  return send_direction_change (session, stream, requested_dir, error);
+}
+
+static void
+session_handler_iface_init (gpointer g_iface, gpointer iface_data)
+{
+  TpSvcMediaSessionHandlerClass *klass =
+    (TpSvcMediaSessionHandlerClass *)g_iface;
+
+#define IMPLEMENT(x) tp_svc_media_session_handler_implement_##x (\
+    klass, gabble_media_session_##x)
+  IMPLEMENT(error);
+  IMPLEMENT(ready);
+#undef IMPLEMENT
+}
-- 
1.5.6.3




More information about the Telepathy-commits mailing list