diff -r 000000000000 -r d0f3a028347a telepathygabble/src/gabble-media-session.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/telepathygabble/src/gabble-media-session.c Tue Feb 02 01:10:06 2010 +0200 @@ -0,0 +1,3026 @@ +/* + * gabble-media-session.c - Source for GabbleMediaSession + * Copyright (C) 2006 Collabora Ltd. + * + * @author Ole Andre Vadla Ravnaas + * + * 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 +#include +#include +#include +#include + +#include "debug.h" +#include "ansi.h" +#include "handles.h" +#include "namespaces.h" +#include "util.h" + +#include "telepathy-errors.h" +#include "telepathy-helpers.h" + +#include "gabble-connection.h" +#include "gabble-media-channel.h" +#include "gabble-media-stream.h" +#include "gabble-presence-cache.h" +#include "gabble-presence.h" + +#include "gabble-media-session.h" +#include "gabble-media-session-signals-marshal.h" +#include "gabble-media-session-glue.h" + +#include "gabble_enums.h" + +#ifndef EMULATOR +G_DEFINE_TYPE(GabbleMediaSession, gabble_media_session, G_TYPE_OBJECT) +#endif + + +#define DEBUG_FLAG GABBLE_DEBUG_MEDIA +//vinod +#ifdef DEBUG_FLAG +//#define DEBUG(format, ...) +#define DEBUGGING 0 +//#define NODE_DEBUG(n, s); +#endif /* DEBUG_FLAG */ + +#define DEFAULT_SESSION_TIMEOUT 50000 + +#define GTALK_STREAM_NAME "gtalk" + +/* 99 streams gives us a max name length of 8 (videoXX\0 or audioXX\0) */ +#define MAX_STREAMS 99 + +#ifndef EMULATOR +#define MAX_STREAM_NAME_LEN 8 +#endif + +//#define DEBUGGING 0 + +/* signal enum */ +enum +{ + NEW_STREAM_HANDLER, + STREAM_ADDED, + TERMINATED, + LAST_SIGNAL +}; + +#ifdef EMULATOR +#include "libgabble_wsd_solution.h" + + GET_STATIC_ARRAY_FROM_TLS(signals,gabble_med_sess,guint) + #define signals (GET_WSD_VAR_NAME(signals,gabble_med_sess, s)()) + + + GET_STATIC_VAR_FROM_TLS(google_audio_caps,gabble_med_sess,GabblePresenceCapabilities) + #define google_audio_caps (*GET_WSD_VAR_NAME(google_audio_caps,gabble_med_sess, s)()) + + GET_STATIC_VAR_FROM_TLS(jingle_audio_caps,gabble_med_sess,GabblePresenceCapabilities) + #define jingle_audio_caps (*GET_WSD_VAR_NAME(jingle_audio_caps,gabble_med_sess, s)()) + + GET_STATIC_VAR_FROM_TLS(jingle_video_caps,gabble_med_sess,GabblePresenceCapabilities) + #define jingle_video_caps (*GET_WSD_VAR_NAME(jingle_video_caps,gabble_med_sess, s)()) + + GET_STATIC_ARRAY_FROM_TLS(ret_sess,gabble_med_sess,gchar) + #define ret_sess (GET_WSD_VAR_NAME(ret_sess,gabble_med_sess, s)()) + + GET_STATIC_VAR_FROM_TLS(gabble_media_session_parent_class,gabble_med_sess,gpointer) + #define gabble_media_session_parent_class (*GET_WSD_VAR_NAME(gabble_media_session_parent_class,gabble_med_sess,s)()) + + GET_STATIC_VAR_FROM_TLS(g_define_type_id,gabble_med_sess,GType) + #define g_define_type_id (*GET_WSD_VAR_NAME(g_define_type_id,gabble_med_sess,s)()) + + + static void gabble_media_session_init (GabbleMediaSession *self); + static void gabble_media_session_class_init (GabbleMediaSessionClass *klass); + static void gabble_media_session_class_intern_init (gpointer klass) + { + gabble_media_session_parent_class = g_type_class_peek_parent (klass); + gabble_media_session_class_init ((GabbleMediaSessionClass*) klass); } + + EXPORT_C GType gabble_media_session_get_type (void) + { + if ((g_define_type_id == 0)) { static const GTypeInfo g_define_type_info = { sizeof (GabbleMediaSessionClass), (GBaseInitFunc) ((void *)0), (GBaseFinalizeFunc) ((void *)0), (GClassInitFunc) gabble_media_session_class_intern_init, (GClassFinalizeFunc) ((void *)0), ((void *)0), sizeof (GabbleMediaSession), 0, (GInstanceInitFunc) gabble_media_session_init, ((void *)0) }; g_define_type_id = g_type_register_static ( ((GType) ((20) << (2))), g_intern_static_string ("GabbleMediaSession"), &g_define_type_info, (GTypeFlags) 0); { {} ; } } return g_define_type_id; + } + + + +#else + + static guint signals[LAST_SIGNAL] = {0}; + +#endif + +/* 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; + GabbleHandle 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", ANSI_BOLD_ON ANSI_FG_BLACK ANSI_BG_WHITE }, + { "JS_STATE_PENDING_INITIATE_SENT", ANSI_BOLD_ON ANSI_BG_CYAN }, + { "JS_STATE_PENDING_INITIATED", ANSI_BOLD_ON ANSI_BG_MAGENTA }, + { "JS_STATE_PENDING_ACCEPT_SENT", ANSI_BOLD_ON ANSI_BG_CYAN }, + { "JS_STATE_ACTIVE", ANSI_BOLD_ON ANSI_BG_BLUE }, + { "JS_STATE_ENDED", 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 */ + g_signal_emit (session, signals[NEW_STREAM_HANDLER], 0, 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 (g_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 (!g_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); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, + "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; + + 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); + + 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 GabbleHandle 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[NEW_STREAM_HANDLER] = + g_signal_new ("new-stream-handler", + G_OBJECT_CLASS_TYPE (gabble_media_session_class), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + gabble_media_session_marshal_VOID__STRING_UINT_UINT_UINT, + G_TYPE_NONE, 4, DBUS_TYPE_G_OBJECT_PATH, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT); + + 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_media_session_marshal_VOID__UINT_UINT, + G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT); + + dbus_g_object_type_install_info (G_TYPE_FROM_CLASS (gabble_media_session_class), &dbus_glib_gabble_media_session_object_info); +} + +static void +gabble_media_session_dispose (GObject *object) +{ + GabbleMediaSession *self = GABBLE_MEDIA_SESSION (object); + GabbleMediaSessionPrivate *priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self); + guint i; + + gabble_debug (DEBUG_FLAG, "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; + + 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 + * + * @error: Used to return a pointer to a GError detailing any error + * that occurred, D-Bus will throw the error only if this + * function returns FALSE. + * + * Returns: TRUE if successful, FALSE if an error was thrown. + */ +gboolean +gabble_media_session_error (GabbleMediaSession *self, + guint errno, + const gchar *message, + GError **error) +{ + GabbleMediaSessionPrivate *priv; + GPtrArray *tmp; + guint i; + + g_assert (GABBLE_IS_MEDIA_SESSION (self)); + + priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self); + + _gabble_media_session_debug (self, DEBUG_MSG_INFO, "Media.SessionHandler::Error called, error %u (%s) -- " + "emitting error on each stream", errno, message); + + if (priv->state == JS_STATE_ENDED) + { + return TRUE; + } + 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); + return TRUE; + } + + g_assert (priv->streams != NULL); + + tmp = priv->streams; + priv->streams = NULL; + + for (i = 0; i < tmp->len; i++) + { + GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i); + //FIXME: Temporary fix to comment "errno" to resolve the mecevo integration build break, fix me when this method is being used. + gabble_media_stream_error (stream, /*errno*/0, message, NULL); + } + + g_ptr_array_free (tmp, TRUE); + + return TRUE; +} + + +/** + * gabble_media_session_ready + * + * Implements D-Bus method Ready + * on interface org.freedesktop.Telepathy.Media.SessionHandler + * + * @error: Used to return a pointer to a GError detailing any error + * that occurred, D-Bus will throw the error only if this + * function returns FALSE. + * + * Returns: TRUE if successful, FALSE if an error was thrown. + */ +gboolean +gabble_media_session_ready (GabbleMediaSession *self, + GError **error) +{ + GabbleMediaSessionPrivate *priv; + guint i; + + g_assert (GABBLE_IS_MEDIA_SESSION (self)); + + priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (self); + + priv->ready = TRUE; + + for (i = 0; i < priv->streams->len; i++) + _emit_new_stream (self, g_ptr_array_index (priv->streams, i)); + + return TRUE; +} + + +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)) + { + gabble_debug (DEBUG_FLAG, "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 + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "setting session mode to %s", + session_mode == MODE_GOOGLE ? "google" : "jingle"); + priv->mode = session_mode; + } + } + + if (override_existing) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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 (!g_strdiff (senders, "initiator")) + { + if (session->initiator == INITIATOR_LOCAL) + ret = TP_MEDIA_STREAM_DIRECTION_SEND; + else + ret = TP_MEDIA_STREAM_DIRECTION_RECEIVE; + } + else if (!g_strdiff (senders, "responder")) + { + if (session->initiator == INITIATOR_REMOTE) + ret = TP_MEDIA_STREAM_DIRECTION_SEND; + else + ret = TP_MEDIA_STREAM_DIRECTION_RECEIVE; + } + else if (!g_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); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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)) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) +{ + gabble_debug (DEBUG_FLAG, "called for %s", stream_name); + + _gabble_media_session_terminate (session, INITIATOR_REMOTE, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + return TRUE; +} + + +#ifndef EMULATOR + +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 + } +}; +#else + +Handler *_s_gabble_med_sess_handlers() + { + Handler* handler = libgabble_ImpurePtr()->_s_gabble_med_sess_handlers; + handler[0].stream_handlers[0] = _handle_create; + handler[0].stream_handlers[1] = _handle_direction; + handler[0].stream_handlers[2] = _handle_codecs; + + handler[1].stream_handlers[0] = _handle_direction; + handler[1].stream_handlers[1] = _handle_codecs; + handler[1].stream_handlers[2] = _handle_accept; + + handler[2].stream_handlers[0] = _handle_terminate; + + handler[3].stream_handlers[0] = _handle_terminate; + + handler[4].stream_handlers[0] = _handle_candidates; + + handler[5].stream_handlers[0] = _handle_create; + handler[5].stream_handlers[0] = _handle_direction; + handler[5].stream_handlers[0] = _handle_codecs; + + handler[6].stream_handlers[0] = _handle_direction; + + handler[7].stream_handlers[0] = _handle_direction; + handler[7].stream_handlers[2] = _handle_codecs; + handler[7].stream_handlers[3] = _handle_accept; + + handler[8].stream_handlers[0] = _handle_remove; + + + return handler; + + + + + } + #define handlers (GET_WSD_VAR_NAME(handlers,gabble_med_sess, s)()) + + + +#endif + + +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) + { + _gabble_media_session_debug (session, DEBUG_MSG_WARNING, "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 (!g_strdiff (creator, "initiator")) + { + if (session->initiator == INITIATOR_LOCAL) + return INITIATOR_LOCAL; + else + return INITIATOR_REMOTE; + } + else if (!g_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 (g_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; + +#ifdef EMULATOR + gchar **tmp; +#else + const gchar **tmp; +#endif + + g_assert (GABBLE_IS_MEDIA_SESSION (session)); + + priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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 = (char**)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); + _gabble_media_session_debug (session, DEBUG_MSG_ERROR, (*error)->message); + return FALSE; +} + +static gboolean +timeout_session (gpointer data) +{ + GabbleMediaSession *session = data; + + gabble_debug (DEBUG_FLAG, "session timed out"); + + _gabble_media_session_terminate (session, INITIATOR_LOCAL, + TP_CHANNEL_GROUP_CHANGE_REASON_ERROR); + + return FALSE; +} + +static void do_content_add (GabbleMediaSession *, GabbleMediaStream *); + +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); + + _gabble_media_session_debug (session, DEBUG_MSG_DUMP, "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); + + _gabble_media_session_debug (session, DEBUG_MSG_EVENT, "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; + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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; + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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; + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, + "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "stream %s does not yet have local codecs", + stream->name); + + return TRUE; + } + + if (stream->connection_state != TP_MEDIA_STREAM_STATE_CONNECTED) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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)) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_ERROR, "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)) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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)) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "ignoring content-add failure, stream has " + "been successfully created by the session initiator"); + } + else + { + _gabble_media_session_debug (session, DEBUG_MSG_ERROR, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "content-add succeeded, marking stream as " + "ACKNOWLEDGED"); + + g_object_set (stream, + "signalling-state", STREAM_SIG_STATE_ACKNOWLEDGED, + NULL); + } + else + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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)) + { + _gabble_media_session_debug (session, DEBUG_MSG_ERROR, "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); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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; + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "stream %s has gone connected", stream->name); + + if (stream->playing) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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 + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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 + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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; + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "stream %s has got local codecs", stream->name); + + if (stream->playing) + { + _gabble_media_session_debug (session, DEBUG_MSG_ERROR, "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 + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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 + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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, + GabbleHandle handle) +{ + GabbleMediaSessionPrivate *priv; + const gchar *base_jid; + GabbleHandle self; + + g_assert (GABBLE_IS_MEDIA_SESSION (session)); + + priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session); + self = priv->conn->self_handle; + + base_jid = gabble_handle_inspect (priv->conn->handles, + TP_HANDLE_TYPE_CONTACT, 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; + LmMessage *msg; + LmMessageNode *iq_node, *node; + gchar *peer_jid, *initiator_jid; + GabbleHandle initiator_handle; + const gchar *element, *xmlns; + + g_assert (GABBLE_IS_MEDIA_SESSION (session)); + + priv = GABBLE_MEDIA_SESSION_GET_PRIVATE (session); + + 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 = priv->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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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: + { + LmMessageNode *content_node; + + g_assert (msg != NULL); + g_assert (removing != NULL); + + content_node = _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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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 + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "not sending jingle session action " + "\"content-remove\" to peer, no initiates or adds sent for " + "these streams"); + } +} + +/* for when you want the reply to be removed from + * the handler chain, but don't care what it is */ +static LmHandlerResult +ignore_reply_cb (GabbleConnection *conn, + LmMessage *sent_msg, + LmMessage *reply_msg, + GObject *object, + gpointer user_data) +{ + return LM_HANDLER_RESULT_REMOVE_MESSAGE; +} + +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); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "sending jingle session action \"reject\" to peer"); + + /* send it */ + _gabble_connection_send_with_reply (priv->conn, msg, ignore_reply_cb, + 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); + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "sending jingle session action \"%s\" to peer", + action); + + /* send it */ + _gabble_connection_send_with_reply (priv->conn, msg, ignore_reply_cb, + 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); + GabbleHandle actor; + + if (priv->state == JS_STATE_ENDED) + return; + + if (who == INITIATOR_REMOTE) + { + actor = priv->peer; + } + else + { + actor = priv->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 _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 = ANSI_BOLD_ON ANSI_FG_WHITE; + break; + case DEBUG_MSG_DUMP: + type_str = ANSI_BOLD_ON ANSI_FG_GREEN; + break; + case DEBUG_MSG_WARNING: + type_str = ANSI_BOLD_ON ANSI_FG_YELLOW; + break; + case DEBUG_MSG_ERROR: + type_str = ANSI_BOLD_ON ANSI_FG_WHITE ANSI_BG_RED; + break; + case DEBUG_MSG_EVENT: + type_str = ANSI_BOLD_ON ANSI_FG_CYAN; + break; + default: + g_assert_not_reached (); + return; + } + + g_message ("[%s%s%s] %s%-26s%s %s%s%s\n", + ANSI_BOLD_ON ANSI_FG_WHITE, + stamp, + ANSI_RESET, + session_states[priv->state].attributes, + session_states[priv->state].name, + ANSI_RESET, + type_str, + buf, + 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); + +#ifndef EMULATOR + static gchar ret_sess[MAX_STREAM_NAME_LEN] = GTALK_STREAM_NAME; +#endif + + if (priv->mode != MODE_GOOGLE) + { + guint i = 1; + + do { + g_snprintf (ret_sess, 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_sess, + INITIATOR_INVALID) != NULL) + { + ret_sess[0] = '\0'; + } + } while (ret_sess[0] == '\0'); + } + + return ret_sess; +} + + +gboolean +_gabble_media_session_request_streams (GabbleMediaSession *session, + const GArray *media_types, + GPtrArray **ret, + GError **error) +{ + +#ifndef EMULATOR + 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; +#endif + + 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, TELEPATHY_ERRORS, NotAvailable, + "member has no audio/video capabilities"); + + return FALSE; + } + + dump = gabble_presence_dump (presence); + _gabble_media_session_debug (session, DEBUG_MSG_DUMP, "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, TELEPATHY_ERRORS, InvalidArgument, + "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; + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "already in Google mode; can't add new " + "stream"); + + g_assert (priv->streams->len == 1); + + g_set_error (error, TELEPATHY_ERRORS, NotAvailable, + "Google Talk calls may only contain one stream"); + + return FALSE; + } + + if (!gabble_presence_resource_has_caps (presence, priv->peer_resource, + jingle_desired_caps)) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, + "in Jingle mode but have insufficient caps for requested streams"); + + g_set_error (error, TELEPATHY_ERRORS, NotAvailable, + "existing call member doesn't support all requested media" + " types"); + + return FALSE; + } + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, + "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, + "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, + "contact has no Jingle capabilities; " + "falling back to Google audio call"); + priv->mode = MODE_GOOGLE; + } + else + { + g_set_error (error, TELEPATHY_ERRORS, NotAvailable, + "Google Talk calls may only contain one stream"); + + return FALSE; + } + } + else + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, + "contact doesn't have desired Google capabilities"); + } + } + } + + if (resource == NULL) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, + "contact doesn't have a resource with suitable capabilities"); + + g_set_error (error, TELEPATHY_ERRORS, NotAvailable, + "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, TELEPATHY_ERRORS, NotAvailable, + "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) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "not sending content-modify for %s stream %s", + stream->signalling_state == STREAM_SIG_STATE_NEW ? "new" : "removing", + stream->name); + return TRUE; + } + + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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, TELEPATHY_ERRORS, NotAvailable, + "Google Talk calls can only be bi-directional"); + return FALSE; + } + + if (requested_dir == TP_MEDIA_STREAM_DIRECTION_NONE) + { + _gabble_media_session_debug (session, DEBUG_MSG_INFO, "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); +} +