diff options
Diffstat (limited to 'rakia/media-channel.c')
-rw-r--r-- | rakia/media-channel.c | 2096 |
1 files changed, 2096 insertions, 0 deletions
diff --git a/rakia/media-channel.c b/rakia/media-channel.c new file mode 100644 index 0000000..089b2b9 --- /dev/null +++ b/rakia/media-channel.c @@ -0,0 +1,2096 @@ +/* + * sip-media-channel.c - Source for RakiaMediaChannel + * Copyright (C) 2005-2008 Collabora Ltd. + * Copyright (C) 2005-2010 Nokia Corporation + * @author Kai Vehmanen <first.surname@nokia.com> + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * Based on telepathy-gabble implementation (gabble-media-channel). + * @author Ole Andre Vadla Ravnaas <ole.andre.ravnaas@collabora.co.uk> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "rakia/media-channel.h" + +#include <stdlib.h> +#include <string.h> + +#include <telepathy-glib/channel-iface.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/errors.h> +#include <telepathy-glib/exportable-channel.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-channel.h> + +#include <rakia/event-target.h> + +#define DEBUG_FLAG TPSIP_DEBUG_MEDIA +#include "rakia/debug.h" + +#include <rakia/media-session.h> +#include <rakia/base-connection.h> + +#define TPSIP_CHANNEL_CALL_STATE_PROCEEDING_MASK \ + (TP_CHANNEL_CALL_STATE_RINGING | \ + TP_CHANNEL_CALL_STATE_QUEUED | \ + TP_CHANNEL_CALL_STATE_IN_PROGRESS) + +static void event_target_init (gpointer, gpointer); +static void channel_iface_init (gpointer, gpointer); +static void media_signalling_iface_init (gpointer, gpointer); +static void streamed_media_iface_init (gpointer, gpointer); +static void dtmf_iface_init (gpointer, gpointer); +static void call_state_iface_init (gpointer, gpointer); +static void hold_iface_init (gpointer, gpointer); + +G_DEFINE_TYPE_WITH_CODE (RakiaMediaChannel, rakia_media_channel, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (TPSIP_TYPE_EVENT_TARGET, event_target_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, + tp_dbus_properties_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_PROPERTIES_INTERFACE, + tp_properties_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL, channel_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_GROUP, + tp_group_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_MEDIA_SIGNALLING, + media_signalling_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_DTMF, + dtmf_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_CALL_STATE, + call_state_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_HOLD, + hold_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_TYPE_STREAMED_MEDIA, + streamed_media_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_EXPORTABLE_CHANNEL, NULL); + G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_IFACE, NULL)); + +static const gchar *rakia_media_channel_interfaces[] = { + TP_IFACE_CHANNEL_INTERFACE_GROUP, + TP_IFACE_CHANNEL_INTERFACE_MEDIA_SIGNALLING, + TP_IFACE_CHANNEL_INTERFACE_DTMF, + TP_IFACE_CHANNEL_INTERFACE_CALL_STATE, + TP_IFACE_CHANNEL_INTERFACE_HOLD, + TP_IFACE_PROPERTIES_INTERFACE, + NULL +}; + +/* properties */ +enum +{ + PROP_CONNECTION = 1, + PROP_OBJECT_PATH, + PROP_CHANNEL_TYPE, + PROP_HANDLE_TYPE, + PROP_HANDLE, + PROP_TARGET_ID, + PROP_INITIATOR, + PROP_INITIATOR_ID, + PROP_REQUESTED, + PROP_INTERFACES, + PROP_CHANNEL_DESTROYED, + PROP_CHANNEL_PROPERTIES, + PROP_INITIAL_AUDIO, + PROP_INITIAL_VIDEO, + PROP_IMMUTABLE_STREAMS, + /* Telepathy properties (see below too) */ + PROP_NAT_TRAVERSAL, + PROP_STUN_SERVER, + PROP_STUN_PORT, + LAST_PROPERTY +}; + +/* TP channel properties */ +enum +{ + TP_PROP_NAT_TRAVERSAL = 0, + TP_PROP_STUN_SERVER, + TP_PROP_STUN_PORT, + NUM_TP_PROPS +}; + +static const TpPropertySignature media_channel_property_signatures[NUM_TP_PROPS] = +{ + { "nat-traversal", G_TYPE_STRING }, + { "stun-server", G_TYPE_STRING }, + { "stun-port", G_TYPE_UINT }, +}; + +/* signals */ +enum +{ + SIG_INCOMING_CALL, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0 }; + + +/* private structure */ +typedef struct _RakiaMediaChannelPrivate RakiaMediaChannelPrivate; + +struct _RakiaMediaChannelPrivate +{ + RakiaBaseConnection *conn; + RakiaMediaSession *session; + gchar *object_path; + TpHandle handle; + TpHandle initiator; + GHashTable *call_states; + gchar *stun_server; + guint stun_port; + + gboolean initial_audio; + gboolean initial_video; + gboolean immutable_streams; + gboolean closed; + gboolean dispose_has_run; +}; + +#define TPSIP_MEDIA_CHANNEL_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), TPSIP_TYPE_MEDIA_CHANNEL, RakiaMediaChannelPrivate)) + +/*********************************************************************** + * Set: Gobject interface + ***********************************************************************/ + +static void +rakia_media_channel_init (RakiaMediaChannel *self) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + /* allocate any data required by the object here */ + priv->call_states = g_hash_table_new (NULL, NULL); + + /* initialise the properties mixin *before* GObject + * sets the construct-time properties */ + tp_properties_mixin_init (G_OBJECT (self), + G_STRUCT_OFFSET (RakiaMediaChannel, properties)); +} + +static void +rakia_media_channel_constructed (GObject *obj) +{ + RakiaMediaChannel *chan = TPSIP_MEDIA_CHANNEL (obj); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (chan); + TpBaseConnection *conn = (TpBaseConnection *)(priv->conn); + GObjectClass *parent_object_class = + G_OBJECT_CLASS (rakia_media_channel_parent_class); + TpDBusDaemon *bus; + TpHandleRepoIface *contact_repo; + TpIntSet *add; + + if (parent_object_class->constructed != NULL) + parent_object_class->constructed (obj); + + contact_repo = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + + if (priv->handle != 0) + tp_handle_ref (contact_repo, priv->handle); + + /* register object on the bus */ + bus = tp_base_connection_get_dbus_daemon (conn); + + DEBUG("registering object to dbus path=%s", priv->object_path); + tp_dbus_daemon_register_object (bus, priv->object_path, obj); + + /* initialize group mixin */ + tp_group_mixin_init (obj, + G_STRUCT_OFFSET (RakiaMediaChannel, group), + contact_repo, + conn->self_handle); + + /* automatically add initiator to channel, but also ref them again (because + * priv->initiator is the InitiatorHandle) */ + g_assert (priv->initiator != 0); + tp_handle_ref (contact_repo, priv->initiator); + + add = tp_intset_new_containing (priv->initiator); + tp_group_mixin_change_members (obj, "", add, NULL, NULL, NULL, 0, 0); + tp_intset_destroy (add); + + /* We start off with lots of flags, and then delete them as we work out what + * kind of channel we are, rather than trying to track what we need to + * add/remove over time. We should always have the right flags before we are + * advertised on the bus. */ + tp_group_mixin_change_flags (obj, + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | + TP_CHANNEL_GROUP_FLAG_CAN_RESCIND | TP_CHANNEL_GROUP_FLAG_PROPERTIES, 0); +} + +static void rakia_media_channel_dispose (GObject *object); +static void rakia_media_channel_finalize (GObject *object); +static void rakia_media_channel_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); +static void rakia_media_channel_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); + +static void priv_create_session (RakiaMediaChannel *channel, + nua_handle_t *nh, + TpHandle peer); +static void priv_destroy_session(RakiaMediaChannel *channel); + +static void priv_outbound_call (RakiaMediaChannel *channel, + TpHandle peer); + +static gboolean rakia_media_channel_remove_with_reason ( + GObject *iface, + TpHandle handle, + const gchar *message, + guint reason, + GError **error); + +static void +rakia_media_channel_class_init (RakiaMediaChannelClass *klass) +{ + static TpDBusPropertiesMixinPropImpl channel_props[] = { + { "ChannelType", "channel-type", NULL }, + { "Interfaces", "interfaces", NULL }, + { "TargetHandleType", "handle-type", NULL }, + { "TargetHandle", "handle", NULL }, + { "TargetID", "target-id", NULL }, + { "InitiatorHandle", "initiator", NULL }, + { "InitiatorID", "initiator-id", NULL }, + { "Requested", "requested", NULL }, + { NULL } + }; + static TpDBusPropertiesMixinPropImpl streamed_media_props[] = { + { "InitialAudio", "initial-audio", NULL }, + { "InitialVideo", "initial-video", NULL }, + { "ImmutableStreams", "immutable-streams", NULL }, + { NULL } + }; + + static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = { + { TP_IFACE_CHANNEL, + tp_dbus_properties_mixin_getter_gobject_properties, + NULL, + channel_props, + }, + { TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, + tp_dbus_properties_mixin_getter_gobject_properties, + NULL, + streamed_media_props, + }, + { NULL } + }; + + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GParamSpec *param_spec; + + DEBUG("enter"); + + g_type_class_add_private (klass, sizeof (RakiaMediaChannelPrivate)); + + object_class->constructed = rakia_media_channel_constructed; + object_class->dispose = rakia_media_channel_dispose; + object_class->finalize = rakia_media_channel_finalize; + + object_class->get_property = rakia_media_channel_get_property; + object_class->set_property = rakia_media_channel_set_property; + + g_object_class_override_property (object_class, PROP_HANDLE_TYPE, + "handle-type"); + g_object_class_override_property (object_class, PROP_HANDLE, "handle"); + g_object_class_override_property (object_class, PROP_OBJECT_PATH, + "object-path"); + g_object_class_override_property (object_class, PROP_CHANNEL_TYPE, + "channel-type"); + + g_object_class_override_property (object_class, PROP_CHANNEL_DESTROYED, + "channel-destroyed"); + g_object_class_override_property (object_class, PROP_CHANNEL_PROPERTIES, + "channel-properties"); + + param_spec = g_param_spec_object ("connection", "RakiaConnection object", + "SIP connection object that owns this SIP media channel object.", + TPSIP_TYPE_BASE_CONNECTION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_CONNECTION, param_spec); + + param_spec = g_param_spec_string ("nat-traversal", "NAT traversal mechanism", + "A string representing the type of NAT traversal that should be " + "performed for streams on this channel.", + "none", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_NAT_TRAVERSAL, param_spec); + + param_spec = g_param_spec_string ("stun-server", "STUN server", + "IP or address of STUN server.", NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STUN_SERVER, param_spec); + + param_spec = g_param_spec_uint ("stun-port", "STUN port", + "UDP port of STUN server.", 0, G_MAXUINT16, 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STUN_PORT, param_spec); + + param_spec = g_param_spec_boxed ("interfaces", "Extra D-Bus interfaces", + "Addition Channel.Interface.* interfaces", G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INTERFACES, param_spec); + + param_spec = g_param_spec_string ("target-id", "Target SIP URI", + "Currently empty, because this channel always has handle 0.", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_TARGET_ID, param_spec); + + param_spec = g_param_spec_uint ("initiator", "Channel initiator", + "The TpHandle representing the contact who created the channel.", + 0, G_MAXUINT32, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIATOR, param_spec); + + param_spec = g_param_spec_string ("initiator-id", "Creator URI", + "The URI obtained by inspecting the initiator handle.", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIATOR_ID, param_spec); + + param_spec = g_param_spec_boolean ("requested", "Requested?", + "True if this channel was requested by the local user", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REQUESTED, param_spec); + + param_spec = g_param_spec_boolean ("initial-audio", "InitialAudio", + "Whether the channel initially contained an audio stream", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIAL_AUDIO, + param_spec); + + param_spec = g_param_spec_boolean ("initial-video", "InitialVideo", + "Whether the channel initially contained a video stream", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIAL_VIDEO, + param_spec); + + param_spec = g_param_spec_boolean ("immutable-streams", "ImmutableStreams", + "Whether the set of streams on this channel are fixed once requested", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_IMMUTABLE_STREAMS, + param_spec); + + signals[SIG_INCOMING_CALL] = + g_signal_new ("incoming-call", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + tp_properties_mixin_class_init (object_class, + G_STRUCT_OFFSET (RakiaMediaChannelClass, properties_class), + media_channel_property_signatures, NUM_TP_PROPS, NULL); + + klass->dbus_props_class.interfaces = + prop_interfaces; + tp_dbus_properties_mixin_class_init (object_class, + G_STRUCT_OFFSET (RakiaMediaChannelClass, dbus_props_class)); + + tp_group_mixin_class_init (object_class, + G_STRUCT_OFFSET (RakiaMediaChannelClass, group_class), + _rakia_media_channel_add_member, + NULL); + tp_group_mixin_class_allow_self_removal (object_class); + tp_group_mixin_class_set_remove_with_reason_func(object_class, + rakia_media_channel_remove_with_reason); + tp_group_mixin_init_dbus_properties (object_class); + +} + +static void +rakia_media_channel_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + RakiaMediaChannel *chan = TPSIP_MEDIA_CHANNEL (object); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (chan); + TpBaseConnection *base_conn = TP_BASE_CONNECTION (priv->conn); + + switch (property_id) { + case PROP_CONNECTION: + g_value_set_object (value, priv->conn); + break; + case PROP_OBJECT_PATH: + g_value_set_string (value, priv->object_path); + break; + case PROP_CHANNEL_TYPE: + g_value_set_static_string (value, TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA); + break; + case PROP_HANDLE: + g_value_set_uint (value, priv->handle); + break; + case PROP_HANDLE_TYPE: + g_value_set_uint (value, priv->handle? + TP_HANDLE_TYPE_CONTACT : TP_HANDLE_TYPE_NONE); + break; + case PROP_TARGET_ID: + if (priv->handle != 0) + { + TpHandleRepoIface *repo = tp_base_connection_get_handles ( + base_conn, TP_HANDLE_TYPE_CONTACT); + + g_value_set_string (value, tp_handle_inspect (repo, priv->handle)); + } + else + g_value_set_static_string (value, ""); + break; + case PROP_INITIATOR: + g_value_set_uint (value, priv->initiator); + break; + case PROP_INITIATOR_ID: + { + TpHandleRepoIface *repo = tp_base_connection_get_handles ( + base_conn, TP_HANDLE_TYPE_CONTACT); + + g_value_set_string (value, tp_handle_inspect (repo, priv->initiator)); + } + break; + case PROP_REQUESTED: + g_value_set_boolean (value, (priv->initiator == base_conn->self_handle)); + break; + case PROP_INTERFACES: + g_value_set_static_boxed (value, rakia_media_channel_interfaces); + break; + case PROP_CHANNEL_DESTROYED: + g_value_set_boolean (value, priv->closed); + break; + case PROP_CHANNEL_PROPERTIES: + g_value_take_boxed (value, + tp_dbus_properties_mixin_make_properties_hash (object, + TP_IFACE_CHANNEL, "ChannelType", + TP_IFACE_CHANNEL, "TargetHandleType", + TP_IFACE_CHANNEL, "TargetHandle", + TP_IFACE_CHANNEL, "TargetID", + TP_IFACE_CHANNEL, "InitiatorHandle", + TP_IFACE_CHANNEL, "InitiatorID", + TP_IFACE_CHANNEL, "Requested", + TP_IFACE_CHANNEL, "Interfaces", + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, "InitialAudio", + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, "InitialVideo", + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, "ImmutableStreams", + NULL)); + break; + case PROP_INITIAL_AUDIO: + g_value_set_boolean (value, priv->initial_audio); + break; + case PROP_INITIAL_VIDEO: + g_value_set_boolean (value, priv->initial_video); + break; + case PROP_IMMUTABLE_STREAMS: + g_value_set_boolean (value, priv->immutable_streams); + break; + case PROP_STUN_SERVER: + g_value_set_string (value, priv->stun_server); + break; + case PROP_STUN_PORT: + g_value_set_uint (value, priv->stun_port); + break; + default: + /* Some properties live in the mixin */ + { + const gchar *param_name; + guint tp_property_id; + GValue *tp_property_value; + + param_name = g_param_spec_get_name (pspec); + if (G_LIKELY (tp_properties_mixin_has_property (object, param_name, + &tp_property_id))) + { + tp_property_value = + chan->properties.properties[tp_property_id].value; + + if (G_LIKELY (tp_property_value != NULL)) + { + g_value_copy (tp_property_value, value); + return; + } + } + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gboolean +rakia_media_channel_set_tp_property (RakiaMediaChannel *chan, + const GValue *value, + GParamSpec *pspec) +{ + GObject *obj = (GObject *) chan; + const gchar *param_name = g_param_spec_get_name (pspec); + guint tp_property_id; + + if (G_LIKELY (tp_properties_mixin_has_property (obj, param_name, + &tp_property_id))) + { + tp_properties_mixin_change_value (obj, tp_property_id, + value, NULL); + tp_properties_mixin_change_flags (obj, tp_property_id, + TP_PROPERTY_FLAG_READ, 0, NULL); + return TRUE; + } + else + { + WARNING("Telepathy property '%s' is not defined for media channels", + param_name); + return FALSE; + } +} + +static void +rakia_media_channel_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + RakiaMediaChannel *chan = TPSIP_MEDIA_CHANNEL (object); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (chan); + + switch (property_id) { + case PROP_HANDLE_TYPE: + case PROP_CHANNEL_TYPE: + /* this property is writable in the interface, but not actually + * meaningfully changable on this channel, so we do nothing */ + break; + case PROP_CONNECTION: + priv->conn = g_value_dup_object (value); + break; + case PROP_OBJECT_PATH: + g_free (priv->object_path); + priv->object_path = g_value_dup_string (value); + break; + case PROP_HANDLE: + /* XXX: this property is defined as writable, + * but don't set it after construction, mmkay? */ + /* we don't ref it here because we don't necessarily have access to the + * contact repo yet - instead we ref it in constructed. */ + priv->handle = g_value_get_uint (value); + break; + case PROP_INITIATOR: + /* similarly we can't ref this yet */ + priv->initiator = g_value_get_uint (value); + break; + case PROP_INITIAL_AUDIO: + priv->initial_audio = g_value_get_boolean (value); + break; + case PROP_INITIAL_VIDEO: + priv->initial_video = g_value_get_boolean (value); + break; + case PROP_IMMUTABLE_STREAMS: + priv->immutable_streams = g_value_get_boolean (value); + break; + case PROP_STUN_SERVER: + priv->stun_server = g_value_dup_string (value); + /* Also expose as a legacy Telepathy property */ + rakia_media_channel_set_tp_property (chan, value, pspec); + break; + case PROP_STUN_PORT: + priv->stun_port = g_value_get_uint (value); + /* Also expose as a legacy Telepathy property */ + rakia_media_channel_set_tp_property (chan, value, pspec); + break; + default: + /* some properties live in the mixin */ + if (rakia_media_channel_set_tp_property (chan, value, pspec)) + return; + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +rakia_media_channel_dispose (GObject *object) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (object); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpHandleRepoIface *contact_handles; + + if (priv->dispose_has_run) + return; + + DEBUG("enter"); + + priv->dispose_has_run = TRUE; + + if (!priv->closed) + rakia_media_channel_close (self); + + contact_handles = tp_base_connection_get_handles ( + TP_BASE_CONNECTION (priv->conn), TP_HANDLE_TYPE_CONTACT); + + tp_handle_unref (contact_handles, priv->initiator); + priv->initiator = 0; + + g_object_unref (priv->conn); + + if (G_OBJECT_CLASS (rakia_media_channel_parent_class)->dispose) + G_OBJECT_CLASS (rakia_media_channel_parent_class)->dispose (object); + + DEBUG("exit"); +} + +static void +rakia_media_channel_finalize (GObject *object) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (object); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + g_hash_table_destroy (priv->call_states); + + g_free (priv->stun_server); + + g_free (priv->object_path); + + tp_group_mixin_finalize (object); + + tp_properties_mixin_finalize (object); + + G_OBJECT_CLASS (rakia_media_channel_parent_class)->finalize (object); + + DEBUG("exit"); +} + +/*********************************************************************** + * Set: Channel interface implementation (same for 0.12/0.13) + ***********************************************************************/ + +/** + * rakia_media_channel_close_async + * + * Implements DBus method Close + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_media_channel_dbus_close (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + + rakia_media_channel_close (self); + tp_svc_channel_return_from_close (context); +} + +void +rakia_media_channel_close (RakiaMediaChannel *obj) +{ + RakiaMediaChannelPrivate *priv; + + DEBUG("enter"); + + g_assert (TPSIP_IS_MEDIA_CHANNEL (obj)); + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (obj); + + if (priv->closed) + return; + + priv->closed = TRUE; + + if (priv->session) { + rakia_media_session_terminate (priv->session); + g_assert (priv->session == NULL); + } + + tp_svc_channel_emit_closed ((TpSvcChannel *)obj); + + return; +} + +/** + * rakia_media_channel_get_channel_type + * + * Implements DBus method GetChannelType + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_media_channel_get_channel_type (TpSvcChannel *obj, + DBusGMethodInvocation *context) +{ + tp_svc_channel_return_from_get_channel_type (context, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA); +} + + +/** + * rakia_media_channel_get_handle + * + * Implements DBus method GetHandle + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_media_channel_get_handle (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->handle != 0) + tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_CONTACT, + priv->handle); + else + tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_NONE, 0); +} + +/** + * rakia_media_channel_get_interfaces + * + * Implements DBus method GetInterfaces + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_media_channel_get_interfaces (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + tp_svc_channel_return_from_get_interfaces (context, + rakia_media_channel_interfaces); +} + +/*********************************************************************** + * Set: Channel.Interface.MediaSignalling Telepathy-0.13 interface + ***********************************************************************/ + +/** + * rakia_media_channel_get_session_handlers + * + * Implements DBus method GetSessionHandlers + * on interface org.freedesktop.Telepathy.Channel.Interface.MediaSignalling + * + * @error: Used to return a pointer to a GError detailing any error + * that occured, DBus will throw the error only if this + * function returns false. + * + * Returns: TRUE if successful, FALSE if an error was thrown. + */ +static void +rakia_media_channel_get_session_handlers (TpSvcChannelInterfaceMediaSignalling *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GPtrArray *ret; + GValue handler = { 0 }; + + DEBUG("enter"); + + g_assert (TPSIP_IS_MEDIA_CHANNEL (self)); + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + ret = g_ptr_array_new (); + + if (priv->session) + { + GType handler_type; + gchar *path; + + g_object_get (priv->session, + "object-path", &path, + NULL); + + handler_type = dbus_g_type_get_struct ("GValueArray", + DBUS_TYPE_G_OBJECT_PATH, + G_TYPE_STRING, + G_TYPE_INVALID); + + g_value_init (&handler, handler_type); + g_value_take_boxed (&handler, + dbus_g_type_specialized_construct (handler_type)); + + dbus_g_type_struct_set (&handler, + 0, path, + 1, "rtp", + G_MAXUINT); + + g_free (path); + + g_ptr_array_add (ret, g_value_get_boxed (&handler)); + } + + tp_svc_channel_interface_media_signalling_return_from_get_session_handlers ( + context, ret); + + if (G_IS_VALUE(&handler)) + g_value_unset (&handler); + + g_ptr_array_free (ret, TRUE); +} + + +/*********************************************************************** + * Set: Channel.Type.StreamedMedia Telepathy-0.13 interface + ***********************************************************************/ + +/** + * rakia_media_channel_list_streams + * + * Implements D-Bus method ListStreams + * on interface org.freedesktop.Telepathy.Channel.Type.StreamedMedia + */ +static void +rakia_media_channel_list_streams (TpSvcChannelTypeStreamedMedia *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GPtrArray *ret = NULL; + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + ret = g_ptr_array_new (); + + if (priv->session != NULL) + rakia_media_session_list_streams (priv->session, ret); + + tp_svc_channel_type_streamed_media_return_from_list_streams (context, ret); + + g_boxed_free (TP_ARRAY_TYPE_MEDIA_STREAM_INFO_LIST, ret); +} + +/** + * rakia_media_channel_remove_streams + * + * Implements D-Bus method RemoveStreams + * on interface org.freedesktop.Telepathy.Channel.Type.StreamedMedia + */ +static void +rakia_media_channel_remove_streams (TpSvcChannelTypeStreamedMedia *iface, + const GArray *streams, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GError *error = NULL; + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->immutable_streams) + { + error = g_error_new (TP_ERRORS, TP_ERROR_NOT_IMPLEMENTED, + "Cannot remove streams from the existing channel"); + } + else if (priv->session != NULL) + { + rakia_media_session_remove_streams(priv->session, + streams, + &error); + } + else + { + error = g_error_new (TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "No session is available"); + } + + if (error != NULL) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + tp_svc_channel_type_streamed_media_return_from_remove_streams (context); +} + +/** + * rakia_media_channel_request_stream_direction + * + * Implements D-Bus method RequestStreamDirection + * on interface org.freedesktop.Telepathy.Channel.Type.StreamedMedia + */ +static void +rakia_media_channel_request_stream_direction (TpSvcChannelTypeStreamedMedia *iface, + guint stream_id, + guint stream_direction, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GError *error = NULL; + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->immutable_streams) + { + GError e = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Cannot change directions on an immutable channel" }; + dbus_g_method_return_error (context, &e); + return; + } + + if (priv->session != NULL) + { + rakia_media_session_request_stream_direction (priv->session, + stream_id, + stream_direction, + &error); + } + else + { + error = g_error_new (TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "The media session is not available"); + } + + if (error == NULL) + { + tp_svc_channel_type_streamed_media_return_from_request_stream_direction (context); + } + else + { + dbus_g_method_return_error (context, error); + g_error_free (error); + } +} + + +/** + * rakia_media_channel_request_streams + * + * Implements D-Bus method RequestStreams + * on interface org.freedesktop.Telepathy.Channel.Type.StreamedMedia + */ +static void +rakia_media_channel_request_streams (TpSvcChannelTypeStreamedMedia *iface, + guint contact_handle, + const GArray *types, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + GError *error = NULL; + GPtrArray *ret = NULL; + RakiaMediaChannelPrivate *priv; + TpHandleRepoIface *contact_repo; + + DEBUG("enter"); + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->immutable_streams) + { + GError e = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Cannot add streams to the immutable channel" }; + dbus_g_method_return_error (context, &e); + return; + } + + contact_repo = tp_base_connection_get_handles ( + (TpBaseConnection *)(priv->conn), TP_HANDLE_TYPE_CONTACT); + + if (!tp_handle_is_valid (contact_repo, contact_handle, &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + priv_outbound_call (self, contact_handle); + + ret = g_ptr_array_sized_new (types->len); + + if (rakia_media_session_request_streams (priv->session, types, ret, &error)) + { + g_assert (types->len == ret->len); + tp_svc_channel_type_streamed_media_return_from_request_streams (context, + ret); + } + else + { + dbus_g_method_return_error (context, error); + g_error_free (error); + } + + g_boxed_free (TP_ARRAY_TYPE_MEDIA_STREAM_INFO_LIST, ret); + + DEBUG ("exit"); +} + +/*********************************************************************** + * Set: sip-media-channel API towards sip-connection + ***********************************************************************/ + +void +rakia_media_channel_create_initial_streams (RakiaMediaChannel *self) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + g_assert (priv->initiator != priv->handle); + + /* RequestChannel(None, 0) => channel is anonymous: + * caller uses RequestStreams to set the peer and start the call. */ + if (priv->handle == 0) + return; + + priv_outbound_call (self, priv->handle); + + g_assert (priv->session != NULL); + + if (priv->initial_audio) + rakia_media_session_add_stream (priv->session, + TP_MEDIA_STREAM_TYPE_AUDIO, + TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, + TRUE); + + if (priv->initial_video) + rakia_media_session_add_stream (priv->session, + TP_MEDIA_STREAM_TYPE_VIDEO, + TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, + TRUE); +} + +/* + * Handles an incoming call, called shortly after the channel + * has been created with initiator handle of the sender, when remote SDP + * session data are reported by the NUA stack. + */ +static void +rakia_media_channel_handle_incoming_call (RakiaMediaChannel *self, + nua_handle_t *nh, + const sdp_session_t *sdp) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpBaseConnection *conn = TP_BASE_CONNECTION (priv->conn); + + g_assert (priv->initiator != conn->self_handle); + g_assert (priv->session == NULL); + + if (sdp != NULL) + { + /* Get the initial media properties from the session offer */ + const sdp_media_t *media; + + for (media = sdp->sdp_media; media != NULL; media = media->m_next) + { + if (media->m_rejected || media->m_port == 0) + continue; + + switch (media->m_type) + { + case sdp_media_audio: + priv->initial_audio = TRUE; + DEBUG("has initial audio"); + break; + case sdp_media_video: + priv->initial_video = TRUE; + DEBUG("has initial video"); + break; + default: + break; + } + } + } + + /* Tell the factory to emit NewChannel(s) */ + g_signal_emit (self, signals[SIG_INCOMING_CALL], 0); + + /* Offer the session handler to the client */ + priv_create_session (self, nh, priv->initiator); + + g_assert (priv->session != NULL); + rakia_media_session_receive_invite (priv->session); +} + +static gboolean +priv_nua_i_invite_cb (RakiaMediaChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + /* nua_i_invite delivered for a bound handle means a re-INVITE */ + + g_return_val_if_fail (priv->session != NULL, FALSE); + + rakia_media_session_receive_reinvite (priv->session); + + return TRUE; +} + +static guint +rakia_media_channel_get_call_state (RakiaMediaChannel *self, + TpHandle peer) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + return GPOINTER_TO_UINT (g_hash_table_lookup (priv->call_states, + GUINT_TO_POINTER (peer))); +} + +static void +rakia_media_channel_peer_error (RakiaMediaChannel *self, + TpHandle peer, + guint status, + const char* message) +{ + TpGroupMixin *mixin = TP_GROUP_MIXIN (self); + TpIntSet *remove; + guint reason = TP_CHANNEL_GROUP_CHANGE_REASON_ERROR; + + switch (status) + { + case 410: + case 604: + reason = TP_CHANNEL_GROUP_CHANGE_REASON_INVALID_CONTACT; + break; + case 486: + case 600: + reason = TP_CHANNEL_GROUP_CHANGE_REASON_BUSY; + break; + case 408: + reason = TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER; + break; + case 404: + case 480: + reason = (rakia_media_channel_get_call_state (self, peer) + & TPSIP_CHANNEL_CALL_STATE_PROCEEDING_MASK) + ? TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER + : TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE; + break; + case 603: + /* No reason means roughly "rejected" */ + reason = TP_CHANNEL_GROUP_CHANGE_REASON_NONE; + break; + case 403: + case 401: + case 407: + reason = TP_CHANNEL_GROUP_CHANGE_REASON_PERMISSION_DENIED; + break; + } + + if (message == NULL || !g_utf8_validate (message, -1, NULL)) + message = ""; + + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); + tp_group_mixin_change_members ((GObject *)self, message, + NULL, remove, NULL, NULL, peer, reason); + tp_intset_destroy (remove); +} + +guint +rakia_media_channel_change_call_state (RakiaMediaChannel *self, + TpHandle peer, + guint flags_add, + guint flags_remove) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + gpointer key = GUINT_TO_POINTER (peer); + guint old_state; + guint new_state; + + /* XXX: check if the peer is a member? */ + + old_state = GPOINTER_TO_UINT (g_hash_table_lookup (priv->call_states, key)); + new_state = (old_state | flags_add) & ~flags_remove; + + if (new_state != old_state) + { + DEBUG ("setting call state %u for peer %u", new_state, peer); + if (new_state == 0) + g_hash_table_remove (priv->call_states, key); + else + g_hash_table_replace (priv->call_states, key, + GUINT_TO_POINTER (new_state)); + + tp_svc_channel_interface_call_state_emit_call_state_changed (self, + peer, + new_state); + } + + return new_state; +} + +static gboolean +priv_nua_i_bye_cb (RakiaMediaChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpGroupMixin *mixin = TP_GROUP_MIXIN (self); + TpIntSet *remove; + TpHandle peer; + + g_return_val_if_fail (priv->session != NULL, FALSE); + + peer = rakia_media_session_get_peer (priv->session); + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); + + tp_group_mixin_change_members ((GObject *) self, "", + NULL, remove, NULL, NULL, + peer, TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + tp_intset_destroy (remove); + + return TRUE; +} + +static gboolean +priv_nua_i_cancel_cb (RakiaMediaChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpGroupMixin *mixin = TP_GROUP_MIXIN (self); + TpIntSet *remove; + TpHandle actor = 0; + TpHandle peer; + const sip_reason_t *reason; + guint cause = 0; + const gchar *message = NULL; + + g_return_val_if_fail (priv->session != NULL, FALSE); + + /* FIXME: implement cancellation of an incoming re-INVITE, if ever + * found in real usage and not caused by a request timeout */ + + if (ev->sip != NULL) + for (reason = ev->sip->sip_reason; + reason != NULL; + reason = reason->re_next) + { + const char *protocol = reason->re_protocol; + if (protocol == NULL || strcmp (protocol, "SIP") != 0) + continue; + if (reason->re_cause != NULL) + { + cause = (guint) g_ascii_strtoull (reason->re_cause, NULL, 10); + message = reason->re_text; + break; + } + } + + peer = rakia_media_session_get_peer (priv->session); + + switch (cause) + { + case 200: + case 603: + /* The user must have acted on another branch of the forked call */ + actor = mixin->self_handle; + break; + default: + actor = peer; + } + + if (message == NULL || !g_utf8_validate (message, -1, NULL)) + message = ""; + + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); + + tp_group_mixin_change_members ((GObject *) self, message, + NULL, remove, NULL, NULL, + actor, TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + tp_intset_destroy (remove); + + return TRUE; +} + +static gboolean +priv_nua_i_state_cb (RakiaMediaChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + const sdp_session_t *r_sdp = NULL; + int offer_recv = 0; + int answer_recv = 0; + int ss_state = nua_callstate_init; + gint status = ev->status; + TpHandle peer; + + tl_gets(tags, + NUTAG_CALLSTATE_REF(ss_state), + NUTAG_OFFER_RECV_REF(offer_recv), + NUTAG_ANSWER_RECV_REF(answer_recv), + SOATAG_REMOTE_SDP_REF(r_sdp), + TAG_END()); + + DEBUG("call with handle %p is %s", ev->nua_handle, nua_callstate_name (ss_state)); + + if (ss_state == nua_callstate_received && priv->session == NULL) + { + /* We get the session data for initial media properties with this event; + * initialize the session before we can create any streams below. + */ + rakia_media_channel_handle_incoming_call (self, ev->nua_handle, r_sdp); + } + + g_return_val_if_fail (priv->session != NULL, FALSE); + + if (r_sdp) + { + g_return_val_if_fail (answer_recv || offer_recv, FALSE); + if (!rakia_media_session_set_remote_media (priv->session, r_sdp)) + { + rakia_media_channel_close (self); + return TRUE; + } + } + + peer = rakia_media_session_get_peer (priv->session); + + switch ((enum nua_callstate)ss_state) + { + case nua_callstate_proceeding: + switch (status) + { + case 180: + rakia_media_channel_change_call_state (self, peer, + TP_CHANNEL_CALL_STATE_RINGING, 0); + break; + case 182: + rakia_media_channel_change_call_state (self, peer, + TP_CHANNEL_CALL_STATE_QUEUED, 0); + break; + case 183: + rakia_media_channel_change_call_state (self, peer, + TP_CHANNEL_CALL_STATE_IN_PROGRESS, 0); + break; + } + break; + + case nua_callstate_completing: + /* In auto-ack mode, we don't need to call nua_ack(), see NUTAG_AUTOACK() */ + break; + + case nua_callstate_ready: + + /* Clear any pre-establishment call states */ + rakia_media_channel_change_call_state (self, peer, 0, + TPSIP_CHANNEL_CALL_STATE_PROCEEDING_MASK); + + if (status < 300) + { + TpIntSet *add = tp_intset_new_containing (peer); + + tp_group_mixin_change_members ((GObject *) self, + "", + add, /* add */ + NULL, /* remove */ + NULL, + NULL, + peer, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + tp_intset_destroy (add); + + rakia_media_session_accept (priv->session); + } + else if (status == 491) + rakia_media_session_resolve_glare (priv->session); + else + { + /* Was something wrong with our re-INVITE? We can't cope anyway. */ + MESSAGE ("can't handle non-fatal response %d %s", status, ev->text); + rakia_media_session_terminate (priv->session); + } + break; + + case nua_callstate_terminated: + /* In cases of self-inflicted termination, + * we should have already gone through the moves */ + if (rakia_media_session_get_state (priv->session) + == TPSIP_MEDIA_SESSION_STATE_ENDED) + break; + + if (status >= 300) + { + rakia_media_channel_peer_error ( + self, peer, status, ev->text); + } + + rakia_media_session_change_state (priv->session, + TPSIP_MEDIA_SESSION_STATE_ENDED); + break; + + default: + break; + } + + return TRUE; +} + +static void priv_session_state_changed_cb (RakiaMediaSession *session, + guint old_state, + guint state, + RakiaMediaChannel *channel) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (channel); + TpGroupMixin *mixin = TP_GROUP_MIXIN (channel); + TpHandle self_handle; + TpHandle peer; + TpIntSet *set = NULL; + + DEBUG("enter"); + + self_handle = mixin->self_handle; + peer = rakia_media_session_get_peer (session); + + switch (state) + { + case TPSIP_MEDIA_SESSION_STATE_INVITE_SENT: + g_assert (priv->initiator == self_handle); + + /* add the peer to remote pending */ + set = tp_intset_new_containing (peer); + tp_group_mixin_change_members ((GObject *)channel, + "", + NULL, /* add */ + NULL, /* remove */ + NULL, /* local pending */ + set, /* remote pending */ + self_handle, /* actor */ + TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); + + /* update flags: no more adding */ + tp_group_mixin_change_flags ((GObject *)channel, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD); + + break; + + case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED: + /* add ourself to local pending */ + set = tp_intset_new_containing (self_handle); + tp_group_mixin_change_members ((GObject *) channel, "", + NULL, /* add */ + NULL, /* remove */ + set, /* local pending */ + NULL, /* remote pending */ + priv->initiator, /* actor */ + TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); + + /* No adding more members to the incoming call. Therefore also not + * possible to add anyone to remote-pending, so rescinding would make + * utterly no sense. We also disallow removing the remote peer if + * we are not the initiator, so disallow that too. + * Removing yourself to end the call is not represented by group flags. + */ + tp_group_mixin_change_flags ((GObject *) channel, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | + TP_CHANNEL_GROUP_FLAG_CAN_RESCIND); + + break; + + case TPSIP_MEDIA_SESSION_STATE_ACTIVE: + if (priv->initiator == self_handle) + { + if (!tp_handle_set_is_member (mixin->remote_pending, peer)) + break; /* no-op */ + + /* the peer has promoted itself to members */ + set = tp_intset_new_containing (peer); + tp_group_mixin_change_members ((GObject *)channel, "", + set, /* add */ + NULL, /* remove */ + NULL, + NULL, + peer, 0); + } + else + { + if (!tp_handle_set_is_member (mixin->local_pending, self_handle)) + break; /* no-op */ + + /* promote ourselves to members */ + set = tp_intset_new_containing (self_handle); + tp_group_mixin_change_members ((GObject *)channel, "", + set, /* add */ + NULL, /* remove */ + NULL, + NULL, + self_handle, 0); + } + + /* update flags: deny adding and rescinding. Removing the remote peer is + * still allowed. + * Removing yourself to end the call is not represented by group flags. + */ + tp_group_mixin_change_flags ((GObject *)channel, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND); + + break; + + case TPSIP_MEDIA_SESSION_STATE_ENDED: + set = tp_intset_new (); + + /* remove us and the peer from the member list */ + tp_intset_add (set, self_handle); + tp_intset_add (set, peer); + tp_group_mixin_change_members ((GObject *)channel, "", + NULL, /* add */ + set, /* remove */ + NULL, + NULL, + 0, 0); + + /* Close the channel; destroy the session first to avoid + * the rakia_media_session_terminate() path in this case */ + priv_destroy_session (channel); + rakia_media_channel_close (channel); + break; + } + + if (set != NULL) + tp_intset_destroy (set); +} + +void +rakia_media_channel_attach_to_nua_handle (RakiaMediaChannel *self, + nua_handle_t *nh) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + rakia_event_target_attach (nh, (GObject *) self); + + /* have the connection handle authentication, before all other + * response callbacks */ + rakia_base_connection_add_auth_handler (priv->conn, TPSIP_EVENT_TARGET (self)); + + g_signal_connect (self, + "nua-event::nua_i_invite", + G_CALLBACK (priv_nua_i_invite_cb), + NULL); + g_signal_connect (self, + "nua-event::nua_i_bye", + G_CALLBACK (priv_nua_i_bye_cb), + NULL); + g_signal_connect (self, + "nua-event::nua_i_cancel", + G_CALLBACK (priv_nua_i_cancel_cb), + NULL); + g_signal_connect (self, + "nua-event::nua_i_state", + G_CALLBACK (priv_nua_i_state_cb), + NULL); + +} + +/** + * priv_create_session: + * + * Creates a RakiaMediaSession object for given peer. + **/ +static void +priv_create_session (RakiaMediaChannel *channel, + nua_handle_t *nh, + TpHandle peer) +{ + RakiaMediaChannelPrivate *priv; + RakiaMediaSession *session; + TpBaseConnection *conn; + TpHandleRepoIface *contact_repo; + gchar *object_path; + gchar *local_ip_address = NULL; + + DEBUG("enter"); + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (channel); + conn = (TpBaseConnection *)(priv->conn); + contact_repo = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + + g_assert (priv->session == NULL); + + object_path = g_strdup_printf ("%s/MediaSession%u", priv->object_path, peer); + + DEBUG("allocating session, peer=%u", peer); + + /* The channel manages references to the peer handle for the session */ + tp_handle_ref (contact_repo, peer); + + g_object_get (priv->conn, + "local-ip-address", &local_ip_address, + NULL); + + session = g_object_new (TPSIP_TYPE_MEDIA_SESSION, + "dbus-daemon", + tp_base_connection_get_dbus_daemon (conn), + "media-channel", channel, + "object-path", object_path, + "nua-handle", nh, + "peer", peer, + "local-ip-address", local_ip_address, + NULL); + + g_free (local_ip_address); + + g_signal_connect_object (session, + "state-changed", + G_CALLBACK(priv_session_state_changed_cb), + channel, + 0); + + priv->session = session; + + tp_svc_channel_interface_media_signalling_emit_new_session_handler ( + (TpSvcChannelInterfaceMediaSignalling *)channel, object_path, "rtp"); + + g_free (object_path); + + DEBUG ("exit"); +} + +static void +priv_destroy_session(RakiaMediaChannel *channel) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (channel); + RakiaMediaSession *session; + TpBaseConnection *conn; + TpHandleRepoIface *contact_repo; + + session = priv->session; + if (session == NULL) + return; + + DEBUG("enter"); + + /* Release the peer handle */ + conn = (TpBaseConnection *)(priv->conn); + contact_repo = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + tp_handle_unref (contact_repo, rakia_media_session_get_peer (session)); + + priv->session = NULL; + g_object_unref (session); + + DEBUG("exit"); +} + +/* + * Creates an outbound call session if a session does not exist + */ +static void +priv_outbound_call (RakiaMediaChannel *channel, + TpHandle peer) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (channel); + nua_handle_t *nh; + + if (priv->session == NULL) + { + DEBUG("making outbound call - setting peer handle to %u", peer); + + nh = rakia_base_connection_create_handle (priv->conn, peer); + priv_create_session (channel, nh, peer); + + /* Bind the channel object to the handle to handle NUA events */ + rakia_media_channel_attach_to_nua_handle (channel, nh); + + nua_handle_unref (nh); + } + else + DEBUG("session already exists"); + + g_assert (priv->session != NULL); +} + +gboolean +_rakia_media_channel_add_member (GObject *iface, + TpHandle handle, + const gchar *message, + GError **error) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpGroupMixin *mixin = TP_GROUP_MIXIN (iface); + + DEBUG("mixin->self_handle=%d, handle=%d", mixin->self_handle, handle); + + if (priv->initiator == mixin->self_handle) + { + TpIntSet *remote_pending; + + /* case a: an old-school outbound call + * (we are the initiator, a new handle added with AddMembers) */ + + priv_outbound_call (self, handle); + + /* Backwards compatible behavior: + * add the peer to remote pending without waiting for the actual request + * to be sent */ + remote_pending = tp_intset_new_containing (handle); + tp_group_mixin_change_members (iface, + "", + NULL, /* add */ + NULL, /* remove */ + NULL, /* local pending */ + remote_pending, /* remote pending */ + mixin->self_handle, /* actor */ + TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); + tp_intset_destroy (remote_pending); + + /* update flags: no more adding. + * Removal and rescinding are still allowed. */ + tp_group_mixin_change_flags (iface, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD); + + return TRUE; + } + if (priv->session && + handle == mixin->self_handle && + tp_handle_set_is_member (mixin->local_pending, handle)) + { + /* case b: an incoming invite */ + DEBUG("accepting an incoming invite"); + g_return_val_if_fail (priv->session != NULL, FALSE); + + rakia_media_session_accept (priv->session); + + return TRUE; + } + + MESSAGE ("unsupported member change requested for a media channel"); + + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "handle %u cannot be added in the current state", handle); + return FALSE; +} + +static gint +rakia_status_from_tp_reason (TpChannelGroupChangeReason reason) +{ + switch (reason) + { + case TP_CHANNEL_GROUP_CHANGE_REASON_NONE: + return 603; /* Decline */ + case TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER: + case TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE: + return 480; /* Temporarily Unavailable */ + case TP_CHANNEL_GROUP_CHANGE_REASON_BUSY: + return 486; /* Busy Here */ + case TP_CHANNEL_GROUP_CHANGE_REASON_PERMISSION_DENIED: + case TP_CHANNEL_GROUP_CHANGE_REASON_BANNED: + return 403; /* Forbidden */ + case TP_CHANNEL_GROUP_CHANGE_REASON_INVALID_CONTACT: + return 404; /* Not Found */ + default: + return 500; /* Server Internal Error */ + } +} + +static gboolean +rakia_media_channel_remove_with_reason (GObject *obj, + TpHandle handle, + const gchar *message, + guint reason, + GError **error) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (obj); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpGroupMixin *mixin = TP_GROUP_MIXIN (obj); + TpIntSet *set = NULL; + TpHandle self_handle; + gboolean rejected; + + self_handle = mixin->self_handle; + + if (priv->initiator != self_handle && handle != self_handle) + { + g_set_error (error, TP_ERRORS, TP_ERROR_PERMISSION_DENIED, + "handle %u cannot be removed because you are not the initiator of the" + " channel", handle); + + return FALSE; + } + + if (priv->session == NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "handle %u cannot be removed in the current state", handle); + + return FALSE; + } + + rejected = (handle == self_handle + && tp_handle_set_is_member (mixin->local_pending, handle)); + + /* We have excluded all the problem cases. + * Now we always want to remove both members on behalf of the local user */ + set = tp_intset_new (); + tp_intset_add (set, self_handle); + tp_intset_add (set, rakia_media_session_get_peer (priv->session)); + tp_group_mixin_change_members (obj, "", + NULL, /* add */ + set, /* remove */ + NULL, + NULL, + self_handle, 0); + tp_intset_destroy (set); + + if (rejected) + { + /* The user has rejected the call */ + + gint status; + + status = rakia_status_from_tp_reason (reason); + + /* XXX: raise NotAvailable if it's the wrong state? */ + rakia_media_session_respond (priv->session, status, message); + + /* This session is effectively ended, prevent the nua_i_state handler + * from useless work */ + rakia_media_session_change_state (priv->session, + TPSIP_MEDIA_SESSION_STATE_ENDED); + } + else + { + /* Want to terminate the call in whatever other situation; + * rescinding is handled by sending CANCEL */ + rakia_media_session_terminate (priv->session); + } + + return TRUE; +} + +static void +rakia_media_channel_get_call_states (TpSvcChannelInterfaceCallState *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + tp_svc_channel_interface_call_state_return_from_get_call_states ( + context, + priv->call_states); +} + +static void +rakia_media_channel_get_hold_state (TpSvcChannelInterfaceHold *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpLocalHoldState hold_state = TP_LOCAL_HOLD_STATE_UNHELD; + TpLocalHoldStateReason hold_reason = TP_LOCAL_HOLD_STATE_REASON_NONE; + + if (priv->session == NULL) + { + GError e = {TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "The media session is not available"}; + dbus_g_method_return_error (context, &e); + } + + g_object_get (priv->session, + "hold-state", &hold_state, + "hold-state-reason", &hold_reason, + NULL); + + tp_svc_channel_interface_hold_return_from_get_hold_state (context, + hold_state, + hold_reason); +} + +static void +rakia_media_channel_request_hold (TpSvcChannelInterfaceHold *iface, + gboolean hold, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->immutable_streams) + { + GError e = {TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Session modification disabled"}; + dbus_g_method_return_error (context, &e); + return; + } + else if (priv->session != NULL) + { + rakia_media_session_request_hold (priv->session, hold); + } + else + { + GError e = {TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "The media session is not available"}; + dbus_g_method_return_error (context, &e); + return; + } + + tp_svc_channel_interface_hold_return_from_request_hold (context); +} + +static void +rakia_media_channel_start_tone (TpSvcChannelInterfaceDTMF *iface, + guint stream_id, + guchar event, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GError *error = NULL; + + DEBUG("enter"); + + g_assert (TPSIP_IS_MEDIA_CHANNEL (self)); + + if (event >= NUM_TP_DTMF_EVENTS) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "event %u is not a known DTMF event", event); + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (!rakia_media_session_start_telephony_event (priv->session, + stream_id, + event, + &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + tp_svc_channel_interface_dtmf_return_from_start_tone (context); +} + +static void +rakia_media_channel_stop_tone (TpSvcChannelInterfaceDTMF *iface, + guint stream_id, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GError *error = NULL; + + DEBUG("enter"); + + g_assert (TPSIP_IS_MEDIA_CHANNEL (self)); + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (!rakia_media_session_stop_telephony_event (priv->session, + stream_id, + &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + tp_svc_channel_interface_dtmf_return_from_stop_tone (context); +} + +static void +event_target_init(gpointer g_iface, gpointer iface_data) +{ +} + +static void +channel_iface_init(gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelClass *klass = (TpSvcChannelClass *)g_iface; + + tp_svc_channel_implement_close ( + klass, rakia_media_channel_dbus_close); +#define IMPLEMENT(x) tp_svc_channel_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT(get_channel_type); + IMPLEMENT(get_handle); + IMPLEMENT(get_interfaces); +#undef IMPLEMENT +} + +static void +streamed_media_iface_init(gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelTypeStreamedMediaClass *klass = (TpSvcChannelTypeStreamedMediaClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_channel_type_streamed_media_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT(list_streams); + IMPLEMENT(remove_streams); + IMPLEMENT(request_stream_direction); + IMPLEMENT(request_streams); +#undef IMPLEMENT +} + +static void +media_signalling_iface_init(gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelInterfaceMediaSignallingClass *klass = (TpSvcChannelInterfaceMediaSignallingClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_channel_interface_media_signalling_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT(get_session_handlers); +#undef IMPLEMENT +} + +static void +dtmf_iface_init (gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelInterfaceDTMFClass *klass = (TpSvcChannelInterfaceDTMFClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_channel_interface_dtmf_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT(start_tone); + IMPLEMENT(stop_tone); +#undef IMPLEMENT +} + +static void +call_state_iface_init (gpointer g_iface, + gpointer iface_data) +{ + TpSvcChannelInterfaceCallStateClass *klass = g_iface; +#define IMPLEMENT(x) tp_svc_channel_interface_call_state_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT (get_call_states); +#undef IMPLEMENT +} + +static void +hold_iface_init (gpointer g_iface, + gpointer iface_data) +{ + TpSvcChannelInterfaceHoldClass *klass = g_iface; + +#define IMPLEMENT(x) tp_svc_channel_interface_hold_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT (get_hold_state); + IMPLEMENT (request_hold); +#undef IMPLEMENT +} |