diff options
26 files changed, 7707 insertions, 8 deletions
diff --git a/configure.ac b/configure.ac index 9e2014180..7bb9a4a6b 100644 --- a/configure.ac +++ b/configure.ac @@ -43,7 +43,6 @@ KEYRING_REQUIRED=2.26.0 GCR_REQUIRED=2.91.4 LIBCANBERRA_GTK_REQUIRED=0.25 LIBNOTIFY_REQUIRED=0.7.0 -TELEPATHY_FARSIGHT_REQUIRED=0.0.18 TELEPATHY_FARSTREAM_REQUIRED=0.2.1 TELEPATHY_GLIB_REQUIRED=0.17.5 TELEPATHY_LOGGER=0.2.10 @@ -211,6 +210,24 @@ AC_DEFINE(EMPATHY_GOA_PROVIDER, "org.gnome.OnlineAccounts", [Name of provider for accounts imported from GOA]) # ----------------------------------------------------------- +# Build empathy-av? +# ----------------------------------------------------------- +AC_ARG_ENABLE(empathy-av, + AS_HELP_STRING([--enable-empathy-av=@<:@no/yes@:>@], + [build empathy-av (legacy call UI)]),, + [enable_empathy_av=no]) +if test "x$enable_empathy_av" != "xno" ; then + PKG_CHECK_MODULES(EMPATHY_AV, + [ + farstream-0.1 + telepathy-farstream >= $TELEPATHY_FARSTREAM_REQUIRED + ]) + AC_DEFINE(HAVE_EMPATHY_AV, 1, [Define if you have StreamedMedia channel support]) + have_empathy_av=yes +fi +AM_CONDITIONAL(HAVE_EMPATHY_AV, test "x$have_empathy_av" = "xyes") + +# ----------------------------------------------------------- # Call support in tp-logger # ----------------------------------------------------------- AC_ARG_ENABLE(call-logs, @@ -669,4 +686,5 @@ Configure summary: GOA MC plugin...............: ${have_goa} Salut E-D-S support.........: ${with_eds} Exp. Call log support.......: ${have_call_logs} + empathy-av (legacy).........: ${enable_empathy_av} " diff --git a/data/Empathy.AudioVideo.client b/data/Empathy.AudioVideo.client new file mode 100644 index 000000000..ab18a316a --- /dev/null +++ b/data/Empathy.AudioVideo.client @@ -0,0 +1,21 @@ +[org.freedesktop.Telepathy.Client] +Interfaces=org.freedesktop.Telepathy.Client.Handler + +[org.freedesktop.Telepathy.Client.Handler.HandlerChannelFilter 0] +org.freedesktop.Telepathy.Channel.ChannelType s=org.freedesktop.Telepathy.Channel.Type.StreamedMedia +org.freedesktop.Telepathy.Channel.TargetHandleType u=1 + +[org.freedesktop.Telepathy.Client.Handler.HandlerChannelFilter 1] +org.freedesktop.Telepathy.Channel.ChannelType s=org.freedesktop.Telepathy.Channel.Type.StreamedMedia +org.freedesktop.Telepathy.Channel.TargetHandleType u=1 +org.freedesktop.Telepathy.Channel.Type.StreamedMedia.InitialAudio b=true + +[org.freedesktop.Telepathy.Client.Handler.HandlerChannelFilter 2] +org.freedesktop.Telepathy.Channel.ChannelType s=org.freedesktop.Telepathy.Channel.Type.StreamedMedia +org.freedesktop.Telepathy.Channel.TargetHandleType u=1 +org.freedesktop.Telepathy.Channel.Type.StreamedMedia.InitialVideo b=true + +[org.freedesktop.Telepathy.Client.Handler.Capabilities] +org.freedesktop.Telepathy.Channel.Interface.MediaSignalling/ice-udp=true +org.freedesktop.Telepathy.Channel.Interface.MediaSignalling/gtalk-p2p=true +org.freedesktop.Telepathy.Channel.Interface.MediaSignalling/video/h264=true diff --git a/data/Makefile.am b/data/Makefile.am index 5819434a2..f3da574ae 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -26,11 +26,19 @@ servicefile_in_files = \ org.freedesktop.Telepathy.Client.Empathy.Call.service.in \ org.freedesktop.Telepathy.Client.Empathy.FileTransfer.service.in +if HAVE_EMPATHY_AV +servicefile_in_files += \ + org.freedesktop.Telepathy.Client.Empathy.AudioVideo.service.in +endif + servicefile_DATA = $(servicefile_in_files:.service.in=.service) org.freedesktop.Telepathy.Client.Empathy.Chat.service: org.freedesktop.Telepathy.Client.Empathy.Chat.service.in $(AM_V_GEN)sed -e "s|[@]libexecdir[@]|$(libexecdir)|" $< > $@ +org.freedesktop.Telepathy.Client.Empathy.AudioVideo.service: org.freedesktop.Telepathy.Client.Empathy.AudioVideo.service.in + $(AM_V_GEN)sed -e "s|[@]libexecdir[@]|$(libexecdir)|" $< > $@ + org.freedesktop.Telepathy.Client.Empathy.Auth.service: org.freedesktop.Telepathy.Client.Empathy.Auth.service.in $(AM_V_GEN)sed -e "s|[@]libexecdir[@]|$(libexecdir)|" $< > $@ @@ -47,6 +55,11 @@ clientfile_DATA = \ Empathy.Call.client \ Empathy.FileTransfer.client +if HAVE_EMPATHY_AV +clientfile_DATA += \ + Empathy.AudioVideo.client +endif + htmldir = $(datadir)/empathy html_DATA = \ Template.html \ diff --git a/data/org.freedesktop.Telepathy.Client.Empathy.AudioVideo.service.in b/data/org.freedesktop.Telepathy.Client.Empathy.AudioVideo.service.in new file mode 100644 index 000000000..177f395de --- /dev/null +++ b/data/org.freedesktop.Telepathy.Client.Empathy.AudioVideo.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Telepathy.Client.Empathy.AudioVideo +Exec=@libexecdir@/empathy-av diff --git a/libempathy-gtk/empathy-call-utils.c b/libempathy-gtk/empathy-call-utils.c index 77fdb565a..545ec7ce8 100644 --- a/libempathy-gtk/empathy-call-utils.c +++ b/libempathy-gtk/empathy-call-utils.c @@ -95,19 +95,71 @@ empathy_call_create_call_request (const gchar *contact, NULL); } +GHashTable * +empathy_call_create_streamed_media_request (const gchar *contact, + gboolean initial_audio, + gboolean initial_video) +{ + return tp_asv_new ( + TP_PROP_CHANNEL_CHANNEL_TYPE, G_TYPE_STRING, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, + TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, + TP_HANDLE_TYPE_CONTACT, + TP_PROP_CHANNEL_TARGET_ID, G_TYPE_STRING, + contact, + TP_PROP_CHANNEL_TYPE_STREAMED_MEDIA_INITIAL_AUDIO, G_TYPE_BOOLEAN, + initial_audio, + TP_PROP_CHANNEL_TYPE_STREAMED_MEDIA_INITIAL_VIDEO, G_TYPE_BOOLEAN, + initial_video, + NULL); +} + +static void +create_streamed_media_channel_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GError *error = NULL; + + if (!tp_account_channel_request_create_channel_finish ( + TP_ACCOUNT_CHANNEL_REQUEST (source), + result, + &error)) + { + DEBUG ("Failed to create StreamedMedia channel: %s", error->message); + show_call_error (error); + g_error_free (error); + } +} + static void create_call_channel_cb (GObject *source, GAsyncResult *result, gpointer user_data) { + TpAccountChannelRequest *streamed_media_req = user_data; GError *error = NULL; if (tp_account_channel_request_create_channel_finish ( TP_ACCOUNT_CHANNEL_REQUEST (source), result, &error)) - return; + { + g_clear_object (&streamed_media_req); + return; + } DEBUG ("Failed to create Call channel: %s", error->message); + if (streamed_media_req != NULL) + { + DEBUG ("Let's try with an StreamedMedia channel"); + g_error_free (error); + tp_account_channel_request_create_channel_async (streamed_media_req, + EMPATHY_AV_BUS_NAME, NULL, + create_streamed_media_channel_cb, + NULL); + return; + } + show_call_error (error); } @@ -120,7 +172,10 @@ call_new_with_streams (const gchar *contact, gint64 timestamp) { GHashTable *call_request; - TpAccountChannelRequest *call_req; + TpAccountChannelRequest *call_req, *streamed_media_req = NULL; +#ifdef HAVE_EMPATHY_AV + GHashTable *streamed_media_request; +#endif /* Call */ call_request = empathy_call_create_call_request (contact, @@ -131,8 +186,20 @@ call_new_with_streams (const gchar *contact, g_hash_table_unref (call_request); +#ifdef HAVE_EMPATHY_AV + /* StreamedMedia */ + streamed_media_request = empathy_call_create_streamed_media_request ( + contact, initial_audio, initial_video); + + streamed_media_req = tp_account_channel_request_new (account, + streamed_media_request, + timestamp); + + g_hash_table_unref (streamed_media_request); +#endif + tp_account_channel_request_create_channel_async (call_req, - EMPATHY_CALL_BUS_NAME, NULL, create_call_channel_cb, NULL); + EMPATHY_CALL_BUS_NAME, NULL, create_call_channel_cb, streamed_media_req); g_object_unref (call_req); } diff --git a/libempathy/Makefile.am b/libempathy/Makefile.am index a13158700..cc288bfa8 100644 --- a/libempathy/Makefile.am +++ b/libempathy/Makefile.am @@ -63,6 +63,7 @@ libempathy_headers = \ empathy-tp-contact-factory.h \ empathy-tp-contact-list.h \ empathy-tp-roomlist.h \ + empathy-tp-streamed-media.h \ empathy-types.h \ empathy-utils.h @@ -103,6 +104,7 @@ libempathy_handwritten_source = \ empathy-tp-contact-factory.c \ empathy-tp-contact-list.c \ empathy-tp-roomlist.c \ + empathy-tp-streamed-media.c \ empathy-utils.c # these are sources that depend on GOA diff --git a/libempathy/empathy-request-util.h b/libempathy/empathy-request-util.h index cdecc631d..6781b72d4 100644 --- a/libempathy/empathy-request-util.h +++ b/libempathy/empathy-request-util.h @@ -33,6 +33,9 @@ G_BEGIN_DECLS #define EMPATHY_CHAT_BUS_NAME_SUFFIX "Empathy.Chat" #define EMPATHY_CHAT_BUS_NAME TP_CLIENT_BUS_NAME_BASE EMPATHY_CHAT_BUS_NAME_SUFFIX +#define EMPATHY_AV_BUS_NAME_SUFFIX "Empathy.AudioVideo" +#define EMPATHY_AV_BUS_NAME TP_CLIENT_BUS_NAME_BASE EMPATHY_AV_BUS_NAME_SUFFIX + #define EMPATHY_CALL_BUS_NAME_SUFFIX "Empathy.Call" #define EMPATHY_CALL_BUS_NAME TP_CLIENT_BUS_NAME_BASE EMPATHY_CALL_BUS_NAME_SUFFIX diff --git a/libempathy/empathy-tp-streamed-media.c b/libempathy/empathy-tp-streamed-media.c new file mode 100644 index 000000000..f245dcfd5 --- /dev/null +++ b/libempathy/empathy-tp-streamed-media.c @@ -0,0 +1,856 @@ +/* + * Copyright (C) 2007 Elliot Fairweather + * Copyright (C) 2007-2008 Collabora Ltd. + * + * 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 + * + * Authors: Elliot Fairweather <elliot.fairweather@collabora.co.uk> + * Xavier Claessens <xclaesse@gmail.com> + */ + +#include "config.h" + +#include <string.h> + +#include <telepathy-glib/proxy-subclass.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/util.h> + +#include "empathy-tp-streamed-media.h" +#include "empathy-tp-contact-factory.h" +#include "empathy-utils.h" + +#define DEBUG_FLAG EMPATHY_DEBUG_VOIP +#include "empathy-debug.h" + +#define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyTpStreamedMedia) +typedef struct +{ + gboolean dispose_has_run; + TpAccount *account; + TpChannel *channel; + EmpathyContact *contact; + gboolean is_incoming; + guint status; + + EmpathyTpStreamedMediaStream *audio; + EmpathyTpStreamedMediaStream *video; +} EmpathyTpStreamedMediaPriv; + +/* signal enum */ +enum { + AUDIO_STREAM_ERROR, + VIDEO_STREAM_ERROR, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; + +enum +{ + PROP_0, + PROP_ACCOUNT, + PROP_CHANNEL, + PROP_CONTACT, + PROP_STATUS, + PROP_AUDIO_STREAM, + PROP_VIDEO_STREAM +}; + +G_DEFINE_TYPE (EmpathyTpStreamedMedia, empathy_tp_streamed_media, G_TYPE_OBJECT) + +static void +tp_streamed_media_add_stream (EmpathyTpStreamedMedia *call, + guint stream_id, + guint contact_handle, + guint stream_type, + guint stream_state, + guint stream_direction) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + switch (stream_type) + { + case TP_MEDIA_STREAM_TYPE_AUDIO: + DEBUG ("Audio stream - id: %d, state: %d, direction: %d", + stream_id, stream_state, stream_direction); + priv->audio->exists = TRUE; + priv->audio->id = stream_id; + priv->audio->state = stream_state; + priv->audio->direction = stream_direction; + g_object_notify (G_OBJECT (call), "audio-stream"); + break; + case TP_MEDIA_STREAM_TYPE_VIDEO: + DEBUG ("Video stream - id: %d, state: %d, direction: %d", + stream_id, stream_state, stream_direction); + priv->video->exists = TRUE; + priv->video->id = stream_id; + priv->video->state = stream_state; + priv->video->direction = stream_direction; + g_object_notify (G_OBJECT (call), "video-stream"); + break; + default: + DEBUG ("Unknown stream type: %d", stream_type); + } +} + +static void +tp_streamed_media_stream_added_cb (TpChannel *channel, + guint stream_id, + guint contact_handle, + guint stream_type, + gpointer user_data, + GObject *call) +{ + DEBUG ("Stream added - stream id: %d, contact handle: %d, stream type: %d", + stream_id, contact_handle, stream_type); + + tp_streamed_media_add_stream (EMPATHY_TP_STREAMED_MEDIA (call), stream_id, contact_handle, + stream_type, TP_MEDIA_STREAM_STATE_DISCONNECTED, + TP_MEDIA_STREAM_DIRECTION_NONE); +} + +static void +tp_streamed_media_stream_removed_cb (TpChannel *channel, + guint stream_id, + gpointer user_data, + GObject *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + DEBUG ("Stream removed - stream id: %d", stream_id); + + if (stream_id == priv->audio->id) + { + priv->audio->exists = FALSE; + g_object_notify (call, "audio-stream"); + } + else if (stream_id == priv->video->id) + { + priv->video->exists = FALSE; + g_object_notify (call, "video-stream"); + } +} + +static void +tp_streamed_media_stream_state_changed_cb (TpChannel *proxy, + guint stream_id, + guint stream_state, + gpointer user_data, + GObject *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + DEBUG ("Stream state changed - stream id: %d, state state: %d", + stream_id, stream_state); + + if (stream_id == priv->audio->id) + { + priv->audio->state = stream_state; + g_object_notify (call, "audio-stream"); + } + else if (stream_id == priv->video->id) + { + priv->video->state = stream_state; + g_object_notify (call, "video-stream"); + } +} + +static void +tp_streamed_media_stream_direction_changed_cb (TpChannel *channel, + guint stream_id, + guint stream_direction, + guint pending_flags, + gpointer user_data, + GObject *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + DEBUG ("Stream direction changed - stream: %d, direction: %d", + stream_id, stream_direction); + + if (stream_id == priv->audio->id) + { + priv->audio->direction = stream_direction; + g_object_notify (call, "audio-stream"); + } + else if (stream_id == priv->video->id) + { + priv->video->direction = stream_direction; + g_object_notify (call, "video-stream"); + } +} + +static void +tp_streamed_media_request_streams_cb (TpChannel *channel, + const GPtrArray *streams, + const GError *error, + gpointer user_data, + GObject *call) +{ + guint i; + + if (error) + { + DEBUG ("Error requesting streams: %s", error->message); + return; + } + + for (i = 0; i < streams->len; i++) + { + GValueArray *values; + guint stream_id; + guint contact_handle; + guint stream_type; + guint stream_state; + guint stream_direction; + + values = g_ptr_array_index (streams, i); + stream_id = g_value_get_uint (g_value_array_get_nth (values, 0)); + contact_handle = g_value_get_uint (g_value_array_get_nth (values, 1)); + stream_type = g_value_get_uint (g_value_array_get_nth (values, 2)); + stream_state = g_value_get_uint (g_value_array_get_nth (values, 3)); + stream_direction = g_value_get_uint (g_value_array_get_nth (values, 4)); + + tp_streamed_media_add_stream (EMPATHY_TP_STREAMED_MEDIA (call), stream_id, contact_handle, + stream_type, stream_state, stream_direction); + } +} + +static void +tp_streamed_media_request_streams_for_capabilities (EmpathyTpStreamedMedia *call, + EmpathyCapabilities capabilities) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + GArray *stream_types; + guint handle; + guint stream_type; + + if (capabilities == EMPATHY_CAPABILITIES_UNKNOWN) + capabilities = EMPATHY_CAPABILITIES_AUDIO | EMPATHY_CAPABILITIES_VIDEO; + + DEBUG ("Requesting new stream for capabilities %d", + capabilities); + + stream_types = g_array_new (FALSE, FALSE, sizeof (guint)); + handle = empathy_contact_get_handle (priv->contact); + + if (capabilities & EMPATHY_CAPABILITIES_AUDIO) + { + stream_type = TP_MEDIA_STREAM_TYPE_AUDIO; + g_array_append_val (stream_types, stream_type); + } + if (capabilities & EMPATHY_CAPABILITIES_VIDEO) + { + stream_type = TP_MEDIA_STREAM_TYPE_VIDEO; + g_array_append_val (stream_types, stream_type); + } + + tp_cli_channel_type_streamed_media_call_request_streams (priv->channel, -1, + handle, stream_types, tp_streamed_media_request_streams_cb, NULL, NULL, + G_OBJECT (call)); + + g_array_unref (stream_types); +} + +static void +tp_streamed_media_got_contact_cb (TpConnection *connection, + EmpathyContact *contact, + const GError *error, + gpointer user_data, + GObject *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + if (error) + { + DEBUG ("Error: %s", error->message); + return; + } + + priv->contact = g_object_ref (contact); + + if (priv->status < EMPATHY_TP_STREAMED_MEDIA_STATUS_PENDING) + { + priv->status = EMPATHY_TP_STREAMED_MEDIA_STATUS_PENDING; + g_object_notify (G_OBJECT (call), "status"); + } + + g_object_notify (G_OBJECT (call), "contact"); +} + +static void +tp_streamed_media_update_status (EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + TpHandle self_handle; + const TpIntSet *set; + TpIntSetIter iter; + + g_object_ref (call); + + self_handle = tp_channel_group_get_self_handle (priv->channel); + set = tp_channel_group_get_members (priv->channel); + tp_intset_iter_init (&iter, set); + while (tp_intset_iter_next (&iter)) + { + if (priv->status == EMPATHY_TP_STREAMED_MEDIA_STATUS_PENDING && + ((priv->is_incoming && iter.element == self_handle) || + (!priv->is_incoming && iter.element != self_handle))) + { + priv->status = EMPATHY_TP_STREAMED_MEDIA_STATUS_ACCEPTED; + g_object_notify (G_OBJECT (call), "status"); + } + } + + g_object_unref (call); +} + +static void +tp_streamed_media_channel_invalidated_cb (TpChannel *channel, + GQuark domain, + gint code, + gchar *message, + EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + DEBUG ("Channel invalidated: %s", message); + priv->status = EMPATHY_TP_STREAMED_MEDIA_STATUS_CLOSED; + g_object_notify (G_OBJECT (call), "status"); +} + +static void +tp_streamed_media_async_cb (TpProxy *proxy, + const GError *error, + gpointer user_data, + GObject *call) +{ + if (error) + DEBUG ("Error %s: %s", (gchar *) user_data, error->message); +} + +static void +tp_streamed_media_stream_error_cb (TpChannel *channel, + guint stream_id, + guint error_code, + const gchar *msg, + gpointer user_data, + GObject *call) +{ + EmpathyTpStreamedMedia *self = EMPATHY_TP_STREAMED_MEDIA (call); + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (self); + + DEBUG ("Stream error on stream %u: %s (code: %u)", stream_id, msg, + error_code); + + if (priv->audio->id == stream_id) + { + g_signal_emit (call, signals[AUDIO_STREAM_ERROR], 0, error_code, msg); + } + else if (priv->video->id == stream_id) + { + g_signal_emit (call, signals[VIDEO_STREAM_ERROR], 0, error_code, msg); + } + else + { + DEBUG ("Unknown stream id: %u", stream_id); + } +} + +static GObject * +tp_streamed_media_constructor (GType type, + guint n_construct_params, + GObjectConstructParam *construct_params) +{ + GObject *object; + EmpathyTpStreamedMedia *call; + EmpathyTpStreamedMediaPriv *priv; + + object = G_OBJECT_CLASS (empathy_tp_streamed_media_parent_class)->constructor (type, + n_construct_params, construct_params); + + call = EMPATHY_TP_STREAMED_MEDIA (object); + priv = GET_PRIV (call); + + /* Setup streamed media channel */ + g_signal_connect (priv->channel, "invalidated", + G_CALLBACK (tp_streamed_media_channel_invalidated_cb), call); + tp_cli_channel_type_streamed_media_connect_to_stream_added (priv->channel, + tp_streamed_media_stream_added_cb, NULL, NULL, G_OBJECT (call), NULL); + tp_cli_channel_type_streamed_media_connect_to_stream_removed (priv->channel, + tp_streamed_media_stream_removed_cb, NULL, NULL, G_OBJECT (call), NULL); + tp_cli_channel_type_streamed_media_connect_to_stream_state_changed (priv->channel, + tp_streamed_media_stream_state_changed_cb, NULL, NULL, G_OBJECT (call), NULL); + tp_cli_channel_type_streamed_media_connect_to_stream_direction_changed (priv->channel, + tp_streamed_media_stream_direction_changed_cb, NULL, NULL, G_OBJECT (call), NULL); + tp_cli_channel_type_streamed_media_connect_to_stream_error (priv->channel, + tp_streamed_media_stream_error_cb, NULL, NULL, G_OBJECT (call), NULL); + tp_cli_channel_type_streamed_media_call_list_streams (priv->channel, -1, + tp_streamed_media_request_streams_cb, NULL, NULL, G_OBJECT (call)); + + /* Is the call incoming? */ + priv->is_incoming = !tp_channel_get_requested (priv->channel); + + /* Get the remote contact */ + empathy_tp_contact_factory_get_from_handle ( + tp_channel_borrow_connection (priv->channel), + tp_channel_get_handle (priv->channel, NULL), tp_streamed_media_got_contact_cb, + NULL, NULL, object); + + /* Update status when members changes */ + tp_streamed_media_update_status (call); + tp_g_signal_connect_object (priv->channel, "group-members-changed", + G_CALLBACK (tp_streamed_media_update_status), call, G_CONNECT_SWAPPED); + + return object; +} +static void +tp_streamed_media_dispose (GObject *object) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (object); + + DEBUG ("Disposing: %p, %d", object, priv->dispose_has_run); + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + if (priv->channel != NULL) + { + g_signal_handlers_disconnect_by_func (priv->channel, + tp_streamed_media_channel_invalidated_cb, object); + + g_object_unref (priv->channel); + priv->channel = NULL; + } + + if (priv->contact != NULL) + g_object_unref (priv->contact); + + tp_clear_object (&priv->account); + + if (G_OBJECT_CLASS (empathy_tp_streamed_media_parent_class)->dispose) + G_OBJECT_CLASS (empathy_tp_streamed_media_parent_class)->dispose (object); +} + +static void +tp_streamed_media_finalize (GObject *object) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (object); + + DEBUG ("Finalizing: %p", object); + + g_slice_free (EmpathyTpStreamedMediaStream, priv->audio); + g_slice_free (EmpathyTpStreamedMediaStream, priv->video); + + (G_OBJECT_CLASS (empathy_tp_streamed_media_parent_class)->finalize) (object); +} + +static void +tp_streamed_media_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (object); + + switch (prop_id) + { + case PROP_ACCOUNT: + priv->account = g_value_dup_object (value); + break; + case PROP_CHANNEL: + priv->channel = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +tp_streamed_media_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (object); + + switch (prop_id) + { + case PROP_ACCOUNT: + g_value_set_object (value, priv->channel); + break; + case PROP_CHANNEL: + g_value_set_object (value, priv->channel); + break; + case PROP_CONTACT: + g_value_set_object (value, priv->contact); + break; + case PROP_STATUS: + g_value_set_uint (value, priv->status); + break; + case PROP_AUDIO_STREAM: + g_value_set_pointer (value, priv->audio); + break; + case PROP_VIDEO_STREAM: + g_value_set_pointer (value, priv->video); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +empathy_tp_streamed_media_class_init (EmpathyTpStreamedMediaClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = tp_streamed_media_constructor; + object_class->dispose = tp_streamed_media_dispose; + object_class->finalize = tp_streamed_media_finalize; + object_class->set_property = tp_streamed_media_set_property; + object_class->get_property = tp_streamed_media_get_property; + + g_type_class_add_private (klass, sizeof (EmpathyTpStreamedMediaPriv)); + + g_object_class_install_property (object_class, PROP_ACCOUNT, + g_param_spec_object ("account", "TpAccount", "TpAccount", + TP_TYPE_ACCOUNT, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_CHANNEL, + g_param_spec_object ("channel", "channel", "channel", + TP_TYPE_CHANNEL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); + + g_object_class_install_property (object_class, PROP_CONTACT, + g_param_spec_object ("contact", "Call contact", "Call contact", + EMPATHY_TYPE_CONTACT, + G_PARAM_READABLE | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); + + g_object_class_install_property (object_class, PROP_STATUS, + g_param_spec_uint ("status", "Call status", + "Call status", 0, 255, 0, G_PARAM_READABLE | G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + g_object_class_install_property (object_class, PROP_AUDIO_STREAM, + g_param_spec_pointer ("audio-stream", "Audio stream data", + "Audio stream data", + G_PARAM_READABLE | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); + + g_object_class_install_property (object_class, PROP_VIDEO_STREAM, + g_param_spec_pointer ("video-stream", "Video stream data", + "Video stream data", + G_PARAM_READABLE | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); + + signals[AUDIO_STREAM_ERROR] = + g_signal_new ("audio-stream-error", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, + 2, G_TYPE_UINT, G_TYPE_STRING); + + signals[VIDEO_STREAM_ERROR] = + g_signal_new ("video-stream-error", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, + 2, G_TYPE_UINT, G_TYPE_STRING); +} + +static void +empathy_tp_streamed_media_init (EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (call, + EMPATHY_TYPE_TP_STREAMED_MEDIA, EmpathyTpStreamedMediaPriv); + + call->priv = priv; + priv->status = EMPATHY_TP_STREAMED_MEDIA_STATUS_READYING; + priv->contact = NULL; + priv->audio = g_slice_new0 (EmpathyTpStreamedMediaStream); + priv->video = g_slice_new0 (EmpathyTpStreamedMediaStream); + priv->audio->exists = FALSE; + priv->video->exists = FALSE; +} + +EmpathyTpStreamedMedia * +empathy_tp_streamed_media_new (TpAccount *account, + TpChannel *channel) +{ + g_return_val_if_fail (TP_IS_ACCOUNT (account), NULL); + g_return_val_if_fail (TP_IS_CHANNEL (channel), NULL); + + return g_object_new (EMPATHY_TYPE_TP_STREAMED_MEDIA, + "account", account, + "channel", channel, + NULL); +} + +void +empathy_tp_streamed_media_accept_incoming_call (EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + TpHandle self_handle; + GArray handles = {(gchar *) &self_handle, 1}; + + g_return_if_fail (EMPATHY_IS_TP_STREAMED_MEDIA (call)); + g_return_if_fail (priv->status == EMPATHY_TP_STREAMED_MEDIA_STATUS_PENDING); + + if (!priv->is_incoming) + return; + + DEBUG ("Accepting incoming call"); + + self_handle = tp_channel_group_get_self_handle (priv->channel); + tp_cli_channel_interface_group_call_add_members (priv->channel, -1, + &handles, NULL, NULL, NULL, NULL, NULL); +} + +void +empathy_tp_streamed_media_close (EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + g_return_if_fail (EMPATHY_IS_TP_STREAMED_MEDIA (call)); + + if (priv->status == EMPATHY_TP_STREAMED_MEDIA_STATUS_CLOSED) + return; + + DEBUG ("Closing channel"); + + tp_cli_channel_call_close (priv->channel, -1, + NULL, NULL, NULL, NULL); + + priv->status = EMPATHY_TP_STREAMED_MEDIA_STATUS_CLOSED; + g_object_notify (G_OBJECT (call), "status"); +} + +void +empathy_tp_streamed_media_request_video_stream_direction (EmpathyTpStreamedMedia *call, + gboolean is_sending) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + guint new_direction; + + g_return_if_fail (EMPATHY_IS_TP_STREAMED_MEDIA (call)); + g_return_if_fail (priv->status == EMPATHY_TP_STREAMED_MEDIA_STATUS_ACCEPTED); + + DEBUG ("Requesting video stream direction - is_sending: %d", is_sending); + + if (!priv->video->exists) + { + if (is_sending) + tp_streamed_media_request_streams_for_capabilities (call, + EMPATHY_CAPABILITIES_VIDEO); + return; + } + + if (is_sending) + new_direction = priv->video->direction | TP_MEDIA_STREAM_DIRECTION_SEND; + else + new_direction = priv->video->direction & ~TP_MEDIA_STREAM_DIRECTION_SEND; + + tp_cli_channel_type_streamed_media_call_request_stream_direction (priv->channel, + -1, priv->video->id, new_direction, + (tp_cli_channel_type_streamed_media_callback_for_request_stream_direction) + tp_streamed_media_async_cb, NULL, NULL, G_OBJECT (call)); +} + +void +empathy_tp_streamed_media_start_tone (EmpathyTpStreamedMedia *call, TpDTMFEvent event) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + g_return_if_fail (EMPATHY_IS_TP_STREAMED_MEDIA (call)); + g_return_if_fail (priv->status == EMPATHY_TP_STREAMED_MEDIA_STATUS_ACCEPTED); + + if (!priv->audio->exists) + return; + + tp_cli_channel_interface_dtmf_call_start_tone (priv->channel, -1, + priv->audio->id, event, + (tp_cli_channel_interface_dtmf_callback_for_start_tone) tp_streamed_media_async_cb, + "starting tone", NULL, G_OBJECT (call)); +} + +void +empathy_tp_streamed_media_stop_tone (EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + g_return_if_fail (EMPATHY_IS_TP_STREAMED_MEDIA (call)); + g_return_if_fail (priv->status == EMPATHY_TP_STREAMED_MEDIA_STATUS_ACCEPTED); + + if (!priv->audio->exists) + return; + + tp_cli_channel_interface_dtmf_call_stop_tone (priv->channel, -1, + priv->audio->id, + (tp_cli_channel_interface_dtmf_callback_for_stop_tone) tp_streamed_media_async_cb, + "stoping tone", NULL, G_OBJECT (call)); +} + +gboolean +empathy_tp_streamed_media_has_dtmf (EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + g_return_val_if_fail (EMPATHY_IS_TP_STREAMED_MEDIA (call), FALSE); + + return tp_proxy_has_interface_by_id (priv->channel, + TP_IFACE_QUARK_CHANNEL_INTERFACE_DTMF); +} + +/** + * empathy_tp_streamed_media_is_receiving_video: + * @call: the call + * + * Indicates if the call is receiving video or not. + * + * Returns: %TRUE if the call is currently receiving video, %FALSE otherwise. + */ +gboolean +empathy_tp_streamed_media_is_receiving_video (EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + g_return_val_if_fail (EMPATHY_IS_TP_STREAMED_MEDIA (call), FALSE); + + if (!priv->video->exists) + return FALSE; + + return priv->video->direction & TP_MEDIA_STREAM_DIRECTION_RECEIVE ? + TRUE : FALSE; +} + +/** + * empathy_tp_streamed_media_is_sending_video: + * @call: the call + * + * Indicates if the call is sending video or not. + * + * Returns: %TRUE if the call is currently sending video, %FALSE otherwise. + */ +gboolean +empathy_tp_streamed_media_is_sending_video (EmpathyTpStreamedMedia *call) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (call); + + g_return_val_if_fail (EMPATHY_IS_TP_STREAMED_MEDIA (call), FALSE); + + if (!priv->video->exists) + return FALSE; + + return priv->video->direction & TP_MEDIA_STREAM_DIRECTION_SEND ? + TRUE : FALSE; +} + +const gchar * +empathy_tp_streamed_media_get_connection_manager (EmpathyTpStreamedMedia *self) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (self); + + return tp_account_get_connection_manager (priv->account); +} + +gboolean +empathy_tp_streamed_media_has_initial_video (EmpathyTpStreamedMedia *self) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (self); + GHashTable *props; + gboolean initial_video; + gboolean valid; + + if (priv->channel == NULL) + return FALSE; + + g_object_get (priv->channel, "channel-properties", &props, NULL); + + initial_video = tp_asv_get_boolean (props, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA ".InitialVideo", &valid); + if (!valid) + initial_video = FALSE; + + g_hash_table_unref (props); + return initial_video; +} + +static void +leave_remove_members_cb (TpChannel *proxy, + const GError *error, + gpointer user_data, + GObject *weak_object) +{ + EmpathyTpStreamedMedia *self = user_data; + + if (error == NULL) + return; + + DEBUG ("RemoveMembers failed (%s); closing the channel", error->message); + empathy_tp_streamed_media_close (self); +} + +void +empathy_tp_streamed_media_leave (EmpathyTpStreamedMedia *self) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (self); + TpHandle self_handle; + GArray array = { (gchar *) &self_handle, 1 }; + + if (!tp_proxy_has_interface_by_id (priv->channel, + TP_IFACE_QUARK_CHANNEL_INTERFACE_GROUP)) + { + empathy_tp_streamed_media_close (self); + return; + } + + self_handle = tp_channel_group_get_self_handle (priv->channel); + if (self_handle == 0) + { + /* we are not member of the channel */ + empathy_tp_streamed_media_close (self); + return; + } + + tp_cli_channel_interface_group_call_remove_members (priv->channel, -1, &array, + "", leave_remove_members_cb, self, NULL, G_OBJECT (self)); +} + +EmpathyTpStreamedMediaStatus +empathy_tp_streamed_media_get_status (EmpathyTpStreamedMedia *self) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (self); + + return priv->status; +} + +TpAccount * +empathy_tp_streamed_media_get_account (EmpathyTpStreamedMedia *self) +{ + EmpathyTpStreamedMediaPriv *priv = GET_PRIV (self); + + return priv->account; +} diff --git a/libempathy/empathy-tp-streamed-media.h b/libempathy/empathy-tp-streamed-media.h new file mode 100644 index 000000000..35612b292 --- /dev/null +++ b/libempathy/empathy-tp-streamed-media.h @@ -0,0 +1,113 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Elliot Fairweather + * Copyright (C) 2007-2008 Collabora Ltd. + * + * 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 + * + * Authors: Elliot Fairweather <elliot.fairweather@collabora.co.uk> + * Xavier Claessens <xclaesse@gmail.com> + */ + +#ifndef __EMPATHY_TP_STREAMED_MEDIA_H__ +#define __EMPATHY_TP_STREAMED_MEDIA_H__ + +#include <glib.h> +#include <telepathy-glib/channel.h> + +#include "empathy-contact.h" + +G_BEGIN_DECLS + +#define EMPATHY_TYPE_TP_STREAMED_MEDIA (empathy_tp_streamed_media_get_type ()) +#define EMPATHY_TP_STREAMED_MEDIA(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), \ + EMPATHY_TYPE_TP_STREAMED_MEDIA, EmpathyTpStreamedMedia)) +#define EMPATHY_TP_STREAMED_MEDIA_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), \ + EMPATHY_TYPE_TP_STREAMED_MEDIA, EmpathyTpStreamedMediaClass)) +#define EMPATHY_IS_TP_STREAMED_MEDIA(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), \ + EMPATHY_TYPE_TP_STREAMED_MEDIA)) +#define EMPATHY_IS_TP_STREAMED_MEDIA_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + EMPATHY_TYPE_TP_STREAMED_MEDIA)) +#define EMPATHY_TP_STREAMED_MEDIA_GET_CLASS(object) \ + (G_TYPE_INSTANCE_GET_CLASS ((object), \ + EMPATHY_TYPE_TP_STREAMED_MEDIA, EmpathyTpStreamedMediaClass)) + +typedef struct _EmpathyTpStreamedMedia EmpathyTpStreamedMedia; +typedef struct _EmpathyTpStreamedMediaClass EmpathyTpStreamedMediaClass; + +struct _EmpathyTpStreamedMedia { + GObject parent; + gpointer priv; +}; + +struct _EmpathyTpStreamedMediaClass { + GObjectClass parent_class; +}; + +typedef enum +{ + EMPATHY_TP_STREAMED_MEDIA_STATUS_READYING, + EMPATHY_TP_STREAMED_MEDIA_STATUS_PENDING, + EMPATHY_TP_STREAMED_MEDIA_STATUS_ACCEPTED, + EMPATHY_TP_STREAMED_MEDIA_STATUS_CLOSED +} EmpathyTpStreamedMediaStatus; + +typedef struct +{ + gboolean exists; + guint id; + guint state; + guint direction; +} EmpathyTpStreamedMediaStream; + +GType empathy_tp_streamed_media_get_type (void) G_GNUC_CONST; +EmpathyTpStreamedMedia *empathy_tp_streamed_media_new (TpAccount *account, + TpChannel *channel); +void empathy_tp_streamed_media_close (EmpathyTpStreamedMedia *streamed_media); + +void empathy_tp_streamed_media_accept_incoming_call ( + EmpathyTpStreamedMedia *streamed_media); +void empathy_tp_streamed_media_request_video_stream_direction ( + EmpathyTpStreamedMedia *streamed_media, + gboolean is_sending); +void empathy_tp_streamed_media_start_tone ( + EmpathyTpStreamedMedia *streamed_media, + TpDTMFEvent event); +void empathy_tp_streamed_media_stop_tone ( + EmpathyTpStreamedMedia *streamed_media); +gboolean empathy_tp_streamed_media_has_dtmf ( + EmpathyTpStreamedMedia *streamed_media); +gboolean empathy_tp_streamed_media_is_receiving_video ( + EmpathyTpStreamedMedia *streamed_media); +gboolean empathy_tp_streamed_media_is_sending_video ( + EmpathyTpStreamedMedia *streamed_media); + +const gchar * empathy_tp_streamed_media_get_connection_manager ( + EmpathyTpStreamedMedia *self); + +gboolean empathy_tp_streamed_media_has_initial_video ( + EmpathyTpStreamedMedia *self); + +void empathy_tp_streamed_media_leave (EmpathyTpStreamedMedia *self); + +EmpathyTpStreamedMediaStatus empathy_tp_streamed_media_get_status ( + EmpathyTpStreamedMedia *self); + +TpAccount * empathy_tp_streamed_media_get_account ( + EmpathyTpStreamedMedia *self); + +G_END_DECLS + +#endif /* __EMPATHY_TP_STREAMED_MEDIA_H__ */ diff --git a/po/POTFILES.in b/po/POTFILES.in index 2d7f06952..ca41d9fe1 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -77,6 +77,8 @@ src/empathy-about-dialog.c src/empathy-accounts-dialog.c [type: gettext/glade]src/empathy-accounts-dialog.ui src/empathy-auth-client.c +src/empathy-av.c +src/empathy-streamed-media-window.c [type: gettext/glade]src/empathy-call-window.ui src/empathy-chat-window.c [type: gettext/glade]src/empathy-chat-window.ui @@ -98,12 +100,14 @@ src/empathy-preferences.c [type: gettext/glade]src/empathy-preferences.ui src/empathy-status-icon.c [type: gettext/glade]src/empathy-status-icon.ui +[type: gettext/glade]src/empathy-streamed-media-window.ui [type: gettext/glade]src/empathy-call-window-fullscreen.ui src/empathy-map-view.c [type: gettext/glade]src/empathy-map-view.ui src/empathy-debug-window.c src/empathy-invite-participant-dialog.c src/empathy-accounts.c +src/empathy-av.c src/empathy-debugger.c src/empathy-chat.c src/empathy-notifications-approver.c diff --git a/src/Makefile.am b/src/Makefile.am index ee99bd6ed..8ffdedc6a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -52,6 +52,7 @@ bin_PROGRAMS = \ libexec_PROGRAMS = \ empathy-auth-client \ + empathy-call \ empathy-chat empathy_accounts_SOURCES = \ @@ -68,6 +69,38 @@ empathy_debugger_SOURCES = \ empathy-debugger.c \ $(NULL) +if HAVE_EMPATHY_AV +empathy_av_SOURCES = \ + empathy-av.c \ + empathy-audio-sink.c \ + empathy-audio-sink.h \ + empathy-audio-src.c \ + empathy-audio-src.h \ + empathy-streamed-media-factory.c \ + empathy-streamed-media-factory.h \ + empathy-streamed-media-handler.c \ + empathy-streamed-media-handler.h \ + empathy-streamed-media-window-fullscreen.c \ + empathy-streamed-media-window-fullscreen.h \ + empathy-streamed-media-window.c \ + empathy-streamed-media-window.h \ + empathy-video-src.c \ + empathy-video-src.h \ + empathy-video-widget.c \ + empathy-video-widget.h \ + ev-sidebar.c \ + ev-sidebar.h \ + empathy-mic-monitor.c \ + empathy-mic-monitor.h + $(NULL) + +empathy_av_CFLAGS = $(EMPATHY_AV_CFLAGS) +empathy_av_LDFLAGS = $(EMPATHY_AV_LIBS) $(LIBM) + +libexec_PROGRAMS += empathy-av + +endif + empathy_auth_client_SOURCES = \ empathy-auth-client.c \ $(NULL) @@ -81,8 +114,6 @@ empathy_chat_SOURCES = \ gedit-close-button.c gedit-close-button.h \ $(NULL) -libexec_PROGRAMS += empathy-call - empathy_call_SOURCES = \ empathy-call.c \ empathy-call-factory.c \ @@ -169,6 +200,7 @@ ui_DATA = \ empathy-accounts-dialog.ui \ empathy-call-window-fullscreen.ui \ empathy-call-window.ui \ + empathy-streamed-media-window.ui \ empathy-chat-window.ui \ empathy-chatrooms-window.ui \ empathy-ft-manager.ui \ diff --git a/src/empathy-av.c b/src/empathy-av.c new file mode 100644 index 000000000..70acfc4f3 --- /dev/null +++ b/src/empathy-av.c @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2007-2010 Collabora Ltd. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * Authors: Xavier Claessens <xclaesse@gmail.com> + * Guillaume Desmottes <guillaume.desmottes@collabora.co.uk> + */ + +#include <config.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include <telepathy-glib/debug-sender.h> + +#include <libempathy-gtk/empathy-ui-utils.h> + +#include "empathy-streamed-media-window.h" +#include "empathy-streamed-media-factory.h" + +#define DEBUG_FLAG EMPATHY_DEBUG_VOIP +#include <libempathy/empathy-debug.h> + +#include <gst/gst.h> + +/* Exit after $TIMEOUT seconds if not displaying any call window */ +#define TIMEOUT 60 + +#define EMPATHY_AV_DBUS_NAME "org.gnome.Empathy.AudioVideo" + +static GtkApplication *app = NULL; +static gboolean activated = FALSE; +static gboolean use_timer = TRUE; + +static EmpathyStreamedMediaFactory *call_factory = NULL; + +static void +new_call_handler_cb (EmpathyStreamedMediaFactory *factory, + EmpathyStreamedMediaHandler *handler, + gboolean outgoing, + gpointer user_data) +{ + EmpathyStreamedMediaWindow *window; + + DEBUG ("Create a new StreamedMedia window"); + + window = empathy_streamed_media_window_new (handler); + + g_application_hold (G_APPLICATION (app)); + + g_signal_connect_swapped (window, "destroy", + G_CALLBACK (g_application_release), app); + + gtk_widget_show (GTK_WIDGET (window)); +} + +static void +activate_cb (GApplication *application) +{ + GError *error = NULL; + + if (activated) + return; + + activated = TRUE; + + if (!use_timer) + { + /* keep a 'ref' to the application */ + g_application_hold (G_APPLICATION (app)); + } + + g_assert (call_factory == NULL); + call_factory = empathy_streamed_media_factory_initialise (); + + g_signal_connect (G_OBJECT (call_factory), "new-streamed-media-handler", + G_CALLBACK (new_call_handler_cb), NULL); + + if (!empathy_streamed_media_factory_register (call_factory, &error)) + { + g_critical ("Failed to register Handler: %s", error->message); + g_error_free (error); + } +} + +int +main (int argc, + char *argv[]) +{ + GOptionContext *optcontext; + GOptionEntry options[] = { + { NULL } + }; +#ifdef ENABLE_DEBUG + TpDebugSender *debug_sender; +#endif + GError *error = NULL; + gint retval; + + /* Init */ + g_thread_init (NULL); + + optcontext = g_option_context_new (N_("- Empathy Audio/Video Client")); + g_option_context_add_group (optcontext, gst_init_get_option_group ()); + g_option_context_add_group (optcontext, gtk_get_option_group (TRUE)); + g_option_context_add_main_entries (optcontext, options, GETTEXT_PACKAGE); + + if (!g_option_context_parse (optcontext, &argc, &argv, &error)) { + g_print ("%s\nRun '%s --help' to see a full list of available command " + "line options.\n", + error->message, argv[0]); + g_warning ("Error in empathy-av init: %s", error->message); + return EXIT_FAILURE; + } + + g_option_context_free (optcontext); + + empathy_gtk_init (); + g_set_application_name (_("Empathy Audio/Video Client")); + + /* Make empathy and empathy-av appear as the same app in gnome-shell */ + gdk_set_program_class ("Empathy"); + gtk_window_set_default_icon_name ("empathy"); + textdomain (GETTEXT_PACKAGE); + + app = gtk_application_new (EMPATHY_AV_DBUS_NAME, G_APPLICATION_FLAGS_NONE); + g_signal_connect (app, "activate", G_CALLBACK (activate_cb), NULL); + +#ifdef ENABLE_DEBUG + /* Set up debug sender */ + debug_sender = tp_debug_sender_dup (); + g_log_set_default_handler (tp_debug_sender_log_handler, G_LOG_DOMAIN); +#endif + + if (g_getenv ("EMPATHY_PERSIST") != NULL) + { + DEBUG ("Disable timer"); + + use_timer = FALSE; + } + + /* the inactivity timeout can only be set while the application is held */ + g_application_hold (G_APPLICATION (app)); + g_application_set_inactivity_timeout (G_APPLICATION (app), TIMEOUT * 1000); + g_application_release (G_APPLICATION (app)); + + retval = g_application_run (G_APPLICATION (app), argc, argv); + + g_object_unref (app); + tp_clear_object (&call_factory); + +#ifdef ENABLE_DEBUG + g_object_unref (debug_sender); +#endif + + return retval; +} diff --git a/src/empathy-event-manager.c b/src/empathy-event-manager.c index 75f2cb8ab..c6b3e29ed 100644 --- a/src/empathy-event-manager.c +++ b/src/empathy-event-manager.c @@ -33,6 +33,7 @@ #include <libempathy/empathy-tp-contact-factory.h> #include <libempathy/empathy-connection-aggregator.h> #include <libempathy/empathy-tp-chat.h> +#include <libempathy/empathy-tp-streamed-media.h> #include <libempathy/empathy-utils.h> #include <libempathy/empathy-gsettings.h> @@ -407,7 +408,11 @@ reject_channel_claim_cb (GObject *source, goto out; } - if (TP_IS_CALL_CHANNEL (user_data)) + if (EMPATHY_IS_TP_STREAMED_MEDIA (user_data)) + { + empathy_tp_streamed_media_close (user_data); + } + else if (TP_IS_CALL_CHANNEL (user_data)) { tp_call_channel_hangup_async (user_data, TP_CALL_STATE_CHANGE_REASON_USER_REQUESTED, @@ -494,7 +499,13 @@ event_channel_process_voip_func (EventPriv *event) return; } - if (etype == EMPATHY_EVENT_TYPE_CALL) + if (etype == EMPATHY_EVENT_TYPE_VOIP) + { + EmpathyTpStreamedMedia *call; + call = EMPATHY_TP_STREAMED_MEDIA (event->approval->handler_instance); + video = empathy_tp_streamed_media_has_initial_video (call); + } + else if (etype == EMPATHY_EVENT_TYPE_CALL) { TpCallChannel *call; call = TP_CALL_CHANNEL (event->approval->handler_instance); @@ -737,6 +748,54 @@ event_manager_call_channel_got_contact_cb (TpConnection *connection, } static void +event_manager_media_channel_got_contact (EventManagerApproval *approval) +{ + EmpathyEventManagerPriv *priv = GET_PRIV (approval->manager); + GtkWidget *window = empathy_roster_window_dup (); + gchar *header; + EmpathyTpStreamedMedia *call; + gboolean video; + + call = EMPATHY_TP_STREAMED_MEDIA (approval->handler_instance); + + video = empathy_tp_streamed_media_has_initial_video (call); + + header = g_strdup_printf ( + video ? _("Incoming video call from %s") :_("Incoming call from %s"), + empathy_contact_get_alias (approval->contact)); + + event_manager_add (approval->manager, NULL, + approval->contact, EMPATHY_EVENT_TYPE_VOIP, + video ? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP, + header, NULL, approval, + event_channel_process_voip_func, NULL); + + g_free (header); + + priv->ringing++; + if (priv->ringing == 1) + empathy_sound_manager_start_playing (priv->sound_mgr, window, + EMPATHY_SOUND_PHONE_INCOMING, MS_BETWEEN_RING); + + g_object_unref (window); +} + +static void +event_manager_media_channel_contact_changed_cb (EmpathyTpStreamedMedia *call, + GParamSpec *param, EventManagerApproval *approval) +{ + EmpathyContact *contact; + + g_object_get (G_OBJECT (call), "contact", &contact, NULL); + + if (contact == NULL) + return; + + approval->contact = contact; + event_manager_media_channel_got_contact (approval); +} + +static void invite_dialog_response_cb (GtkDialog *dialog, gint response, EventManagerApproval *approval) @@ -1021,6 +1080,29 @@ approve_channels (TpSimpleApprover *approver, event_manager_chat_message_received_cb (tp_chat, msg, approval); } } + else if (channel_type == TP_IFACE_QUARK_CHANNEL_TYPE_STREAMED_MEDIA) + { + EmpathyContact *contact; + EmpathyTpStreamedMedia *call = empathy_tp_streamed_media_new (account, + channel); + + approval->handler_instance = G_OBJECT (call); + + g_object_get (G_OBJECT (call), "contact", &contact, NULL); + + if (contact == NULL) + { + g_signal_connect (call, "notify::contact", + G_CALLBACK (event_manager_media_channel_contact_changed_cb), + approval); + } + else + { + approval->contact = contact; + event_manager_media_channel_got_contact (approval); + } + + } else if (channel_type == TP_IFACE_QUARK_CHANNEL_TYPE_CALL) { TpCallChannel *call = TP_CALL_CHANNEL (channel); diff --git a/src/empathy-event-manager.h b/src/empathy-event-manager.h index c780e5e73..91c6e1f99 100644 --- a/src/empathy-event-manager.h +++ b/src/empathy-event-manager.h @@ -50,6 +50,7 @@ struct _EmpathyEventManagerClass { typedef enum { EMPATHY_EVENT_TYPE_CHAT, + EMPATHY_EVENT_TYPE_VOIP, EMPATHY_EVENT_TYPE_CALL, EMPATHY_EVENT_TYPE_TRANSFER, EMPATHY_EVENT_TYPE_SUBSCRIPTION, diff --git a/src/empathy-notifications-approver.c b/src/empathy-notifications-approver.c index bb7f6b458..7a4f03731 100644 --- a/src/empathy-notifications-approver.c +++ b/src/empathy-notifications-approver.c @@ -25,6 +25,8 @@ #include <libnotify/notify.h> #include <telepathy-glib/telepathy-glib.h> +#include <libempathy/empathy-tp-streamed-media.h> + #include <libempathy-gtk/empathy-notify-manager.h> #include <libempathy-gtk/empathy-call-utils.h> @@ -188,7 +190,12 @@ add_notification_actions (EmpathyNotificationsApprover *self, self, NULL); break; + case EMPATHY_EVENT_TYPE_VOIP: case EMPATHY_EVENT_TYPE_CALL: + if (self->priv->event->type == EMPATHY_EVENT_TYPE_VOIP) + video = empathy_tp_streamed_media_has_initial_video ( + EMPATHY_TP_STREAMED_MEDIA (self->priv->event->handler_instance)); + else video = tp_call_channel_has_initial_video ( TP_CALL_CHANNEL (self->priv->event->handler_instance), NULL); @@ -253,6 +260,7 @@ notification_is_urgent (EmpathyNotificationsApprover *self, * interact ASAP */ switch (self->priv->event->type) { case EMPATHY_EVENT_TYPE_CHAT: + case EMPATHY_EVENT_TYPE_VOIP: case EMPATHY_EVENT_TYPE_CALL: case EMPATHY_EVENT_TYPE_TRANSFER: case EMPATHY_EVENT_TYPE_INVITATION: @@ -280,6 +288,7 @@ get_category_for_event_type (EmpathyEventType type) return "presence.online"; case EMPATHY_EVENT_TYPE_PRESENCE_OFFLINE: return "presence.offline"; + case EMPATHY_EVENT_TYPE_VOIP: case EMPATHY_EVENT_TYPE_CALL: return "x-empathy.call.incoming"; case EMPATHY_EVENT_TYPE_TRANSFER: diff --git a/src/empathy-streamed-media-factory.c b/src/empathy-streamed-media-factory.c new file mode 100644 index 000000000..1c25ae253 --- /dev/null +++ b/src/empathy-streamed-media-factory.c @@ -0,0 +1,269 @@ +/* + * empathy-streamed-media-factory.c - Source for EmpathyStreamedMediaFactory + * Copyright (C) 2008-2011 Collabora Ltd. + * @author Sjoerd Simons <sjoerd.simons@collabora.co.uk> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#include <stdio.h> +#include <stdlib.h> + +#include <telepathy-glib/account-channel-request.h> +#include <telepathy-glib/simple-handler.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/util.h> + +#include <libempathy/empathy-request-util.h> +#include <libempathy/empathy-utils.h> + +#include "empathy-streamed-media-factory.h" +#include "empathy-streamed-media-handler.h" + +#define DEBUG_FLAG EMPATHY_DEBUG_VOIP +#include <libempathy/empathy-debug.h> + +G_DEFINE_TYPE(EmpathyStreamedMediaFactory, empathy_streamed_media_factory, G_TYPE_OBJECT) + +static void handle_channels_cb (TpSimpleHandler *handler, + TpAccount *account, + TpConnection *connection, + GList *channels, + GList *requests_satisfied, + gint64 user_action_time, + TpHandleChannelsContext *context, + gpointer user_data); + +/* signal enum */ +enum +{ + NEW_STREAMED_MEDIA_HANDLER, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; + +/* private structure */ +typedef struct { + TpBaseClient *handler; + gboolean dispose_has_run; +} EmpathyStreamedMediaFactoryPriv; + +#define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyStreamedMediaFactory) + +static GObject *call_factory = NULL; + +static void +empathy_streamed_media_factory_init (EmpathyStreamedMediaFactory *obj) +{ + EmpathyStreamedMediaFactoryPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (obj, + EMPATHY_TYPE_STREAMED_MEDIA_FACTORY, EmpathyStreamedMediaFactoryPriv); + TpAccountManager *am; + + obj->priv = priv; + + am = tp_account_manager_dup (); + + priv->handler = tp_simple_handler_new_with_am (am, FALSE, FALSE, + EMPATHY_AV_BUS_NAME_SUFFIX, FALSE, handle_channels_cb, obj, NULL); + + tp_base_client_take_handler_filter (priv->handler, tp_asv_new ( + TP_PROP_CHANNEL_CHANNEL_TYPE, G_TYPE_STRING, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, + TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, TP_HANDLE_TYPE_CONTACT, + NULL)); + + tp_base_client_take_handler_filter (priv->handler, tp_asv_new ( + TP_PROP_CHANNEL_CHANNEL_TYPE, G_TYPE_STRING, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, + TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, TP_HANDLE_TYPE_CONTACT, + TP_PROP_CHANNEL_TYPE_STREAMED_MEDIA_INITIAL_AUDIO, G_TYPE_BOOLEAN, TRUE, + NULL)); + + tp_base_client_take_handler_filter (priv->handler, tp_asv_new ( + TP_PROP_CHANNEL_CHANNEL_TYPE, G_TYPE_STRING, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, + TP_PROP_CHANNEL_TARGET_HANDLE_TYPE, G_TYPE_UINT, TP_HANDLE_TYPE_CONTACT, + TP_PROP_CHANNEL_TYPE_STREAMED_MEDIA_INITIAL_VIDEO, G_TYPE_BOOLEAN, TRUE, + NULL)); + + tp_base_client_add_handler_capabilities_varargs (priv->handler, + "org.freedesktop.Telepathy.Channel.Interface.MediaSignalling/ice-udp", + "org.freedesktop.Telepathy.Channel.Interface.MediaSignalling/gtalk-p2p", + "org.freedesktop.Telepathy.Channel.Interface.MediaSignalling/video/h264", + NULL); + + g_object_unref (am); +} + +static GObject * +empathy_streamed_media_factory_constructor (GType type, guint n_construct_params, + GObjectConstructParam *construct_params) +{ + g_return_val_if_fail (call_factory == NULL, NULL); + + call_factory = G_OBJECT_CLASS (empathy_streamed_media_factory_parent_class)->constructor + (type, n_construct_params, construct_params); + g_object_add_weak_pointer (call_factory, (gpointer)&call_factory); + + return call_factory; +} + +static void +empathy_streamed_media_factory_finalize (GObject *object) +{ + /* free any data held directly by the object here */ + + if (G_OBJECT_CLASS (empathy_streamed_media_factory_parent_class)->finalize) + G_OBJECT_CLASS (empathy_streamed_media_factory_parent_class)->finalize (object); +} + +static void +empathy_streamed_media_factory_dispose (GObject *object) +{ + EmpathyStreamedMediaFactoryPriv *priv = GET_PRIV (object); + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + tp_clear_object (&priv->handler); + + if (G_OBJECT_CLASS (empathy_streamed_media_factory_parent_class)->dispose) + G_OBJECT_CLASS (empathy_streamed_media_factory_parent_class)->dispose (object); +} + +static void +empathy_streamed_media_factory_class_init ( + EmpathyStreamedMediaFactoryClass *empathy_streamed_media_factory_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (empathy_streamed_media_factory_class); + + g_type_class_add_private (empathy_streamed_media_factory_class, + sizeof (EmpathyStreamedMediaFactoryPriv)); + + object_class->constructor = empathy_streamed_media_factory_constructor; + object_class->dispose = empathy_streamed_media_factory_dispose; + object_class->finalize = empathy_streamed_media_factory_finalize; + + signals[NEW_STREAMED_MEDIA_HANDLER] = + g_signal_new ("new-streamed-media-handler", + G_TYPE_FROM_CLASS (empathy_streamed_media_factory_class), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, + 2, EMPATHY_TYPE_STREAMED_MEDIA_HANDLER, G_TYPE_BOOLEAN); +} + +EmpathyStreamedMediaFactory * +empathy_streamed_media_factory_initialise (void) +{ + g_return_val_if_fail (call_factory == NULL, NULL); + + return EMPATHY_STREAMED_MEDIA_FACTORY (g_object_new (EMPATHY_TYPE_STREAMED_MEDIA_FACTORY, NULL)); +} + +EmpathyStreamedMediaFactory * +empathy_streamed_media_factory_get (void) +{ + g_return_val_if_fail (call_factory != NULL, NULL); + + return EMPATHY_STREAMED_MEDIA_FACTORY (call_factory); +} + +static void +create_streamed_media_handler (EmpathyStreamedMediaFactory *factory, + EmpathyTpStreamedMedia *call) +{ + EmpathyStreamedMediaHandler *handler; + + g_return_if_fail (factory != NULL); + + handler = empathy_streamed_media_handler_new_for_channel (call); + + g_signal_emit (factory, signals[NEW_STREAMED_MEDIA_HANDLER], 0, + handler, FALSE); + + g_object_unref (handler); +} + +static void +call_status_changed_cb (EmpathyTpStreamedMedia *call, + GParamSpec *spec, + EmpathyStreamedMediaFactory *self) +{ + if (empathy_tp_streamed_media_get_status (call) <= EMPATHY_TP_STREAMED_MEDIA_STATUS_READYING) + return; + + create_streamed_media_handler (self, call); + + g_signal_handlers_disconnect_by_func (call, call_status_changed_cb, self); + g_object_unref (call); +} + +static void +handle_channels_cb (TpSimpleHandler *handler, + TpAccount *account, + TpConnection *connection, + GList *channels, + GList *requests_satisfied, + gint64 user_action_time, + TpHandleChannelsContext *context, + gpointer user_data) +{ + EmpathyStreamedMediaFactory *self = user_data; + GList *l; + + for (l = channels; l != NULL; l = g_list_next (l)) + { + TpChannel *channel = l->data; + EmpathyTpStreamedMedia *call; + + if (tp_proxy_get_invalidated (channel) != NULL) + continue; + + if (tp_channel_get_channel_type_id (channel) != + TP_IFACE_QUARK_CHANNEL_TYPE_STREAMED_MEDIA) + continue; + + call = empathy_tp_streamed_media_new (account, channel); + + if (empathy_tp_streamed_media_get_status (call) <= EMPATHY_TP_STREAMED_MEDIA_STATUS_READYING) + { + /* We have to wait that the TpStreamedMedia is ready as the + * call-handler rely on it. */ + tp_g_signal_connect_object (call, "notify::status", + G_CALLBACK (call_status_changed_cb), self, 0); + continue; + } + + create_streamed_media_handler (self, call); + g_object_unref (call); + } + + tp_handle_channels_context_accept (context); +} + +gboolean +empathy_streamed_media_factory_register (EmpathyStreamedMediaFactory *self, + GError **error) +{ + EmpathyStreamedMediaFactoryPriv *priv = GET_PRIV (self); + + return tp_base_client_register (priv->handler, error); +} diff --git a/src/empathy-streamed-media-factory.h b/src/empathy-streamed-media-factory.h new file mode 100644 index 000000000..c69073ec4 --- /dev/null +++ b/src/empathy-streamed-media-factory.h @@ -0,0 +1,69 @@ +/* + * empathy-streamed-media-factory.h - Header for EmpathyStreamedMediaFactory + * Copyright (C) 2008-2011 Collabora Ltd. + * @author Sjoerd Simons <sjoerd.simons@collabora.co.uk> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __EMPATHY_STREAMED_MEDIA_FACTORY_H__ +#define __EMPATHY_STREAMED_MEDIA_FACTORY_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct _EmpathyStreamedMediaFactory EmpathyStreamedMediaFactory; +typedef struct _EmpathyStreamedMediaFactoryClass EmpathyStreamedMediaFactoryClass; + +struct _EmpathyStreamedMediaFactoryClass { + GObjectClass parent_class; +}; + +struct _EmpathyStreamedMediaFactory { + GObject parent; + gpointer priv; +}; + +GType empathy_streamed_media_factory_get_type (void); + +/* TYPE MACROS */ +#define EMPATHY_TYPE_STREAMED_MEDIA_FACTORY \ + (empathy_streamed_media_factory_get_type ()) +#define EMPATHY_STREAMED_MEDIA_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), EMPATHY_TYPE_STREAMED_MEDIA_FACTORY, \ + EmpathyStreamedMediaFactory)) +#define EMPATHY_STREAMED_MEDIA_FACTORY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), EMPATHY_TYPE_STREAMED_MEDIA_FACTORY, \ + EmpathyStreamedMediaFactoryClass)) +#define EMPATHY_IS_STREAMED_MEDIA_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), EMPATHY_TYPE_STREAMED_MEDIA_FACTORY)) +#define EMPATHY_IS_STREAMED_MEDIA_FACTORY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), EMPATHY_TYPE_STREAMED_MEDIA_FACTORY)) +#define EMPATHY_STREAMED_MEDIA_FACTORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), EMPATHY_TYPE_STREAMED_MEDIA_FACTORY, \ + EmpathyStreamedMediaFactoryClass)) + + +EmpathyStreamedMediaFactory *empathy_streamed_media_factory_initialise (void); + +EmpathyStreamedMediaFactory *empathy_streamed_media_factory_get (void); + +gboolean empathy_streamed_media_factory_register (EmpathyStreamedMediaFactory *self, + GError **error); + +G_END_DECLS + +#endif /* #ifndef __EMPATHY_STREAMED_MEDIA_FACTORY_H__*/ diff --git a/src/empathy-streamed-media-handler.c b/src/empathy-streamed-media-handler.c new file mode 100644 index 000000000..0caa3988f --- /dev/null +++ b/src/empathy-streamed-media-handler.c @@ -0,0 +1,920 @@ +/* + * empathy-streamed-media-handler.c - Source for EmpathyStreamedMediaHandler + * Copyright (C) 2008-2011 Collabora Ltd. + * @author Sjoerd Simons <sjoerd.simons@collabora.co.uk> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#include <stdio.h> +#include <stdlib.h> + +#include <telepathy-glib/account-channel-request.h> +#include <telepathy-glib/util.h> +#include <telepathy-glib/interfaces.h> + +#include <telepathy-farstream/telepathy-farstream.h> + +#include <libempathy/empathy-utils.h> + +#include <libempathy-gtk/empathy-call-utils.h> + +#include "empathy-streamed-media-handler.h" + +#define DEBUG_FLAG EMPATHY_DEBUG_VOIP +#include <libempathy/empathy-debug.h> + +G_DEFINE_TYPE(EmpathyStreamedMediaHandler, empathy_streamed_media_handler, G_TYPE_OBJECT) + +/* signal enum */ +enum { + CONFERENCE_ADDED, + SRC_PAD_ADDED, + SINK_PAD_ADDED, + START_RECEIVING, + CLOSED, + STREAM_CLOSED, + CANDIDATES_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; + +enum { + PROP_TP_STREAMED_MEDIA = 1, + PROP_GST_BUS, + PROP_CONTACT, + PROP_INITIAL_AUDIO, + PROP_INITIAL_VIDEO, + PROP_SEND_AUDIO_CODEC, + PROP_SEND_VIDEO_CODEC, + PROP_RECV_AUDIO_CODECS, + PROP_RECV_VIDEO_CODECS, + PROP_AUDIO_REMOTE_CANDIDATE, + PROP_VIDEO_REMOTE_CANDIDATE, + PROP_AUDIO_LOCAL_CANDIDATE, + PROP_VIDEO_LOCAL_CANDIDATE, +}; + +/* private structure */ + +typedef struct { + gboolean dispose_has_run; + EmpathyTpStreamedMedia *call; + EmpathyContact *contact; + TfChannel *tfchannel; + gboolean initial_audio; + gboolean initial_video; + + FsCodec *send_audio_codec; + FsCodec *send_video_codec; + GList *recv_audio_codecs; + GList *recv_video_codecs; + FsCandidate *audio_remote_candidate; + FsCandidate *video_remote_candidate; + FsCandidate *audio_local_candidate; + FsCandidate *video_local_candidate; +} EmpathyStreamedMediaHandlerPriv; + +#define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyStreamedMediaHandler) + +static void +empathy_streamed_media_handler_dispose (GObject *object) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (object); + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + if (priv->contact != NULL) + g_object_unref (priv->contact); + + priv->contact = NULL; + + if (priv->tfchannel != NULL) + g_object_unref (priv->tfchannel); + + priv->tfchannel = NULL; + + if (priv->call != NULL) + { + empathy_tp_streamed_media_close (priv->call); + g_object_unref (priv->call); + } + + priv->call = NULL; + + /* release any references held by the object here */ + if (G_OBJECT_CLASS (empathy_streamed_media_handler_parent_class)->dispose) + G_OBJECT_CLASS (empathy_streamed_media_handler_parent_class)->dispose (object); +} + +static void +empathy_streamed_media_handler_finalize (GObject *object) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (object); + + fs_codec_destroy (priv->send_audio_codec); + fs_codec_destroy (priv->send_video_codec); + fs_codec_list_destroy (priv->recv_audio_codecs); + fs_codec_list_destroy (priv->recv_video_codecs); + fs_candidate_destroy (priv->audio_remote_candidate); + fs_candidate_destroy (priv->video_remote_candidate); + fs_candidate_destroy (priv->audio_local_candidate); + fs_candidate_destroy (priv->video_local_candidate); + + if (G_OBJECT_CLASS (empathy_streamed_media_handler_parent_class)->finalize) + G_OBJECT_CLASS (empathy_streamed_media_handler_parent_class)->finalize (object); +} + +static void +empathy_streamed_media_handler_init (EmpathyStreamedMediaHandler *obj) +{ + EmpathyStreamedMediaHandlerPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (obj, + EMPATHY_TYPE_STREAMED_MEDIA_HANDLER, EmpathyStreamedMediaHandlerPriv); + + obj->priv = priv; +} + +static void +empathy_streamed_media_handler_constructed (GObject *object) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (object); + + if (priv->contact == NULL) + { + g_object_get (priv->call, "contact", &(priv->contact), NULL); + } +} + +static void +empathy_streamed_media_handler_set_property (GObject *object, + guint property_id, const GValue *value, GParamSpec *pspec) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (object); + + switch (property_id) + { + case PROP_CONTACT: + priv->contact = g_value_dup_object (value); + break; + case PROP_TP_STREAMED_MEDIA: + priv->call = g_value_dup_object (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; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +empathy_streamed_media_handler_get_property (GObject *object, + guint property_id, GValue *value, GParamSpec *pspec) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (object); + + switch (property_id) + { + case PROP_CONTACT: + g_value_set_object (value, priv->contact); + break; + case PROP_TP_STREAMED_MEDIA: + g_value_set_object (value, priv->call); + 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_SEND_AUDIO_CODEC: + g_value_set_boxed (value, priv->send_audio_codec); + break; + case PROP_SEND_VIDEO_CODEC: + g_value_set_boxed (value, priv->send_video_codec); + break; + case PROP_RECV_AUDIO_CODECS: + g_value_set_boxed (value, priv->recv_audio_codecs); + break; + case PROP_RECV_VIDEO_CODECS: + g_value_set_boxed (value, priv->recv_video_codecs); + break; + case PROP_AUDIO_REMOTE_CANDIDATE: + g_value_set_boxed (value, priv->audio_remote_candidate); + break; + case PROP_VIDEO_REMOTE_CANDIDATE: + g_value_set_boxed (value, priv->video_remote_candidate); + break; + case PROP_AUDIO_LOCAL_CANDIDATE: + g_value_set_boxed (value, priv->audio_local_candidate); + break; + case PROP_VIDEO_LOCAL_CANDIDATE: + g_value_set_boxed (value, priv->video_local_candidate); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +empathy_streamed_media_handler_class_init (EmpathyStreamedMediaHandlerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GParamSpec *param_spec; + + g_type_class_add_private (klass, sizeof (EmpathyStreamedMediaHandlerPriv)); + + object_class->constructed = empathy_streamed_media_handler_constructed; + object_class->set_property = empathy_streamed_media_handler_set_property; + object_class->get_property = empathy_streamed_media_handler_get_property; + object_class->dispose = empathy_streamed_media_handler_dispose; + object_class->finalize = empathy_streamed_media_handler_finalize; + + param_spec = g_param_spec_object ("contact", + "contact", "The remote contact", + EMPATHY_TYPE_CONTACT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_CONTACT, param_spec); + + param_spec = g_param_spec_object ("tp-call", + "tp-call", "The calls channel wrapper", + EMPATHY_TYPE_TP_STREAMED_MEDIA, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_TP_STREAMED_MEDIA, param_spec); + + param_spec = g_param_spec_boolean ("initial-audio", + "initial-audio", "Whether the call should start with audio", + TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIAL_AUDIO, + param_spec); + + param_spec = g_param_spec_boolean ("initial-video", + "initial-video", "Whether the call should start with video", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIAL_VIDEO, + param_spec); + + param_spec = g_param_spec_boxed ("send-audio-codec", + "send audio codec", "Codec used to encode the outgoing video stream", + FS_TYPE_CODEC, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_SEND_AUDIO_CODEC, + param_spec); + + param_spec = g_param_spec_boxed ("send-video-codec", + "send video codec", "Codec used to encode the outgoing video stream", + FS_TYPE_CODEC, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_SEND_VIDEO_CODEC, + param_spec); + + param_spec = g_param_spec_boxed ("recv-audio-codecs", + "recvs audio codec", "Codecs used to decode the incoming audio stream", + FS_TYPE_CODEC_LIST, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_RECV_AUDIO_CODECS, + param_spec); + + param_spec = g_param_spec_boxed ("recv-video-codecs", + "recvs video codec", "Codecs used to decode the incoming video stream", + FS_TYPE_CODEC_LIST, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_RECV_VIDEO_CODECS, + param_spec); + + param_spec = g_param_spec_boxed ("audio-remote-candidate", + "audio remote candidate", + "Remote candidate used for the audio stream", + FS_TYPE_CANDIDATE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, + PROP_AUDIO_REMOTE_CANDIDATE, param_spec); + + param_spec = g_param_spec_boxed ("video-remote-candidate", + "video remote candidate", + "Remote candidate used for the video stream", + FS_TYPE_CANDIDATE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, + PROP_VIDEO_REMOTE_CANDIDATE, param_spec); + + param_spec = g_param_spec_boxed ("audio-local-candidate", + "audio local candidate", + "Local candidate used for the audio stream", + FS_TYPE_CANDIDATE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, + PROP_AUDIO_REMOTE_CANDIDATE, param_spec); + + param_spec = g_param_spec_boxed ("video-local-candidate", + "video local candidate", + "Local candidate used for the video stream", + FS_TYPE_CANDIDATE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, + PROP_VIDEO_REMOTE_CANDIDATE, param_spec); + + signals[CONFERENCE_ADDED] = + g_signal_new ("conference-added", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, + 1, FS_TYPE_CONFERENCE); + + signals[SRC_PAD_ADDED] = + g_signal_new ("src-pad-added", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_BOOLEAN, + 2, GST_TYPE_PAD, G_TYPE_UINT); + + signals[SINK_PAD_ADDED] = + g_signal_new ("sink-pad-added", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_BOOLEAN, + 2, GST_TYPE_PAD, G_TYPE_UINT); + + signals[START_RECEIVING] = + g_signal_new ("start-receiving", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + g_signal_accumulator_true_handled, NULL, + g_cclosure_marshal_generic, + G_TYPE_BOOLEAN, 2, G_TYPE_UINT, G_TYPE_UINT); + + signals[CLOSED] = + g_signal_new ("closed", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, + 0); + + signals[STREAM_CLOSED] = + g_signal_new ("stream-closed", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, TF_TYPE_CONTENT); + + signals[CANDIDATES_CHANGED] = + g_signal_new ("candidates-changed", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, G_TYPE_UINT); +} + +/** + * empathy_streamed_media_handler_new_for_contact: + * @contact: an #EmpathyContact + * + * Creates a new #EmpathyStreamedMediaHandler with contact @contact. + * + * Return value: a new #EmpathyStreamedMediaHandler + */ +EmpathyStreamedMediaHandler * +empathy_streamed_media_handler_new_for_contact (EmpathyContact *contact) +{ + return EMPATHY_STREAMED_MEDIA_HANDLER (g_object_new (EMPATHY_TYPE_STREAMED_MEDIA_HANDLER, + "contact", contact, NULL)); +} + +EmpathyStreamedMediaHandler * +empathy_streamed_media_handler_new_for_channel (EmpathyTpStreamedMedia *call) +{ + return EMPATHY_STREAMED_MEDIA_HANDLER (g_object_new (EMPATHY_TYPE_STREAMED_MEDIA_HANDLER, + "tp-call", call, + "initial-video", empathy_tp_streamed_media_is_receiving_video (call), + NULL)); +} + +static void +update_sending_codec (EmpathyStreamedMediaHandler *self, + FsCodec *codec, + FsSession *session) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + FsMediaType type; + + if (codec == NULL || session == NULL) + return; + + g_object_get (session, "media-type", &type, NULL); + + if (type == FS_MEDIA_TYPE_AUDIO) + { + priv->send_audio_codec = fs_codec_copy (codec); + g_object_notify (G_OBJECT (self), "send-audio-codec"); + } + else if (type == FS_MEDIA_TYPE_VIDEO) + { + priv->send_video_codec = fs_codec_copy (codec); + g_object_notify (G_OBJECT (self), "send-video-codec"); + } +} + +static void +update_receiving_codec (EmpathyStreamedMediaHandler *self, + GList *codecs, + FsStream *stream) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + FsSession *session; + FsMediaType type; + + if (codecs == NULL || stream == NULL) + return; + + g_object_get (stream, "session", &session, NULL); + if (session == NULL) + return; + + g_object_get (session, "media-type", &type, NULL); + + if (type == FS_MEDIA_TYPE_AUDIO) + { + priv->recv_audio_codecs = fs_codec_list_copy (codecs); + g_object_notify (G_OBJECT (self), "recv-audio-codecs"); + } + else if (type == FS_MEDIA_TYPE_VIDEO) + { + priv->recv_video_codecs = fs_codec_list_copy (codecs); + g_object_notify (G_OBJECT (self), "recv-video-codecs"); + } + + g_object_unref (session); +} + +static void +update_candidates (EmpathyStreamedMediaHandler *self, + FsCandidate *remote_candidate, + FsCandidate *local_candidate, + FsStream *stream) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + FsSession *session; + FsMediaType type; + + if (stream == NULL) + return; + + g_object_get (stream, "session", &session, NULL); + if (session == NULL) + return; + + g_object_get (session, "media-type", &type, NULL); + + if (type == FS_MEDIA_TYPE_AUDIO) + { + if (remote_candidate != NULL) + { + fs_candidate_destroy (priv->audio_remote_candidate); + priv->audio_remote_candidate = fs_candidate_copy (remote_candidate); + g_object_notify (G_OBJECT (self), "audio-remote-candidate"); + } + + if (local_candidate != NULL) + { + fs_candidate_destroy (priv->audio_local_candidate); + priv->audio_local_candidate = fs_candidate_copy (local_candidate); + g_object_notify (G_OBJECT (self), "audio-local-candidate"); + } + + g_signal_emit (G_OBJECT (self), signals[CANDIDATES_CHANGED], 0, + FS_MEDIA_TYPE_AUDIO); + } + else if (type == FS_MEDIA_TYPE_VIDEO) + { + if (remote_candidate != NULL) + { + fs_candidate_destroy (priv->video_remote_candidate); + priv->video_remote_candidate = fs_candidate_copy (remote_candidate); + g_object_notify (G_OBJECT (self), "video-remote-candidate"); + } + + if (local_candidate != NULL) + { + fs_candidate_destroy (priv->video_local_candidate); + priv->video_local_candidate = fs_candidate_copy (local_candidate); + g_object_notify (G_OBJECT (self), "video-local-candidate"); + } + + g_signal_emit (G_OBJECT (self), signals[CANDIDATES_CHANGED], 0, + FS_MEDIA_TYPE_VIDEO); + } + + g_object_unref (session); +} + +void +empathy_streamed_media_handler_bus_message (EmpathyStreamedMediaHandler *handler, + GstBus *bus, GstMessage *message) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (handler); + const GstStructure *s = gst_message_get_structure (message); + + if (priv->tfchannel == NULL) + return; + + if (s != NULL && + gst_structure_has_name (s, "farstream-send-codec-changed")) + { + const GValue *val; + FsCodec *codec; + FsSession *session; + + val = gst_structure_get_value (s, "codec"); + codec = g_value_get_boxed (val); + + val = gst_structure_get_value (s, "session"); + session = g_value_get_object (val); + + update_sending_codec (handler, codec, session); + } + else if (s != NULL && + gst_structure_has_name (s, "farstream-recv-codecs-changed")) + { + const GValue *val; + GList *codecs; + FsStream *stream; + + val = gst_structure_get_value (s, "codecs"); + codecs = g_value_get_boxed (val); + + val = gst_structure_get_value (s, "stream"); + stream = g_value_get_object (val); + + update_receiving_codec (handler, codecs, stream); + } + else if (s != NULL && + gst_structure_has_name (s, "farstream-new-active-candidate-pair")) + { + const GValue *val; + FsCandidate *remote_candidate, *local_candidate; + FsStream *stream; + + val = gst_structure_get_value (s, "remote-candidate"); + remote_candidate = g_value_get_boxed (val); + + val = gst_structure_get_value (s, "local-candidate"); + local_candidate = g_value_get_boxed (val); + + val = gst_structure_get_value (s, "stream"); + stream = g_value_get_object (val); + + update_candidates (handler, remote_candidate, local_candidate, stream); + } + + tf_channel_bus_message (priv->tfchannel, message); +} + +static void +empathy_streamed_media_handler_tf_channel_conference_added_cb (TfChannel *tfchannel, + FsConference *conference, + EmpathyStreamedMediaHandler *self) +{ + g_signal_emit (G_OBJECT (self), signals[CONFERENCE_ADDED], 0, + GST_ELEMENT (conference)); +} + +static gboolean +src_pad_added_error_idle (gpointer data) +{ + TfContent *content = data; + + tf_content_error_literal (content, "Could not link sink"); + g_object_unref (content); + + return FALSE; +} + +static void +empathy_streamed_media_handler_tf_content_src_pad_added_cb (TfContent *content, + guint handle, + FsStream *stream, + GstPad *pad, + FsCodec *codec, + EmpathyStreamedMediaHandler *handler) +{ + guint media_type; + gboolean retval; + + g_object_get (content, "media-type", &media_type, NULL); + + g_signal_emit (G_OBJECT (handler), signals[SRC_PAD_ADDED], 0, + pad, media_type, &retval); + + if (!retval) + g_idle_add (src_pad_added_error_idle, g_object_ref (content)); +} + + +static gboolean +empathy_streamed_media_handler_tf_content_start_receiving_cb (TfContent *content, + guint *handles, guint handle_count, EmpathyTpStreamedMedia *call) +{ + gboolean ret = FALSE; + guint media_type; + + g_object_get (G_OBJECT (content), "media-type", &media_type, NULL); + + g_signal_emit (G_OBJECT (call), + signals[START_RECEIVING], 0, media_type, &ret); + + return ret; +} + +static void +empathy_streamed_media_handler_tf_content_removed_cb (TfChannel *channel, + TfContent *content, EmpathyStreamedMediaHandler *handler) +{ + g_signal_emit (handler, signals[STREAM_CLOSED], 0, content); +} + +static void +empathy_streamed_media_handler_tf_channel_content_added_cb (TfChannel *tfchannel, + TfContent *content, EmpathyStreamedMediaHandler *handler) +{ + guint media_type; + GstPad *spad; + gboolean retval; + FsSession *session; + FsCodec *codec; + + g_signal_connect (content, "src-pad-added", + G_CALLBACK (empathy_streamed_media_handler_tf_content_src_pad_added_cb), handler); + + g_signal_connect (content, "start-receiving", + G_CALLBACK (empathy_streamed_media_handler_tf_content_start_receiving_cb), + handler); + + g_object_get (content, "media-type", &media_type, + "sink-pad", &spad, NULL); + + g_signal_emit (G_OBJECT (handler), signals[SINK_PAD_ADDED], 0, + spad, media_type, &retval); + + if (!retval) + tf_content_error_literal (content, "Could not link source"); + + /* Get sending codec */ + g_object_get (content, "fs-session", &session, NULL); + g_object_get (session, "current-send-codec", &codec, NULL); + + update_sending_codec (handler, codec, session); + + tp_clear_object (&session); + tp_clear_object (&codec); + + gst_object_unref (spad); +} + +static void +empathy_streamed_media_handler_tf_channel_closed_cb (TfChannel *tfchannel, + EmpathyStreamedMediaHandler *handler) +{ + g_signal_emit (G_OBJECT (handler), signals[CLOSED], 0); +} + +static void +empathy_streamed_media_handler_start_tpfs (GAsyncInitable *initable, + GAsyncResult *res, EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + GError *error = NULL; + GPtrArray *conferences; + + if (g_async_initable_init_finish (initable, res, &error)) + { + priv->tfchannel = TF_CHANNEL (initable); + + /* Set up the telepathy farsight channel */ + g_signal_connect (priv->tfchannel, "fs-conference-added", + G_CALLBACK (empathy_streamed_media_handler_tf_channel_conference_added_cb), self); + g_signal_connect (priv->tfchannel, "content-added", + G_CALLBACK (empathy_streamed_media_handler_tf_channel_content_added_cb), self); + g_signal_connect (priv->tfchannel, "content-removed", + G_CALLBACK (empathy_streamed_media_handler_tf_content_removed_cb), + self); + g_signal_connect (priv->tfchannel, "closed", + G_CALLBACK (empathy_streamed_media_handler_tf_channel_closed_cb), self); + + g_object_get (priv->tfchannel, "fs-conferences", &conferences, NULL); + if (conferences) + { + if (conferences->len > 0) + empathy_streamed_media_handler_tf_channel_conference_added_cb ( + priv->tfchannel, g_ptr_array_index (conferences, 0), self); + g_ptr_array_unref (conferences); + } + + empathy_tp_streamed_media_accept_incoming_call (priv->call); + } + else + { + g_warning ("Error creating tf channel"); + g_clear_error (&error); + } + g_object_unref (self); +} + +static void +empathy_streamed_media_handler_request_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + EmpathyStreamedMediaHandler *self = EMPATHY_STREAMED_MEDIA_HANDLER (user_data); + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + TpChannel *channel; + GError *error = NULL; + TpAccountChannelRequest *req = TP_ACCOUNT_CHANNEL_REQUEST (source); + TpAccount *account; + + channel = tp_account_channel_request_create_and_handle_channel_finish (req, + result, NULL, &error); + if (channel == NULL) + { + DEBUG ("Failed to create the channel: %s", error->message); + g_error_free (error); + return; + } + + account = tp_account_channel_request_get_account (req); + + priv->call = empathy_tp_streamed_media_new (account, channel); + + g_object_notify (G_OBJECT (self), "tp-call"); + + + tf_channel_new_async (channel, + (GAsyncReadyCallback) empathy_streamed_media_handler_start_tpfs, + g_object_ref (self)); + + g_object_unref (channel); +} + +void +empathy_streamed_media_handler_start_call (EmpathyStreamedMediaHandler *handler, + gint64 timestamp) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (handler); + TpAccountChannelRequest *req; + TpAccount *account; + GHashTable *request; + + if (priv->call != NULL) + { + TpChannel *channel; + + g_object_get (priv->call, "channel", &channel, NULL); + + tf_channel_new_async (channel, + (GAsyncReadyCallback) empathy_streamed_media_handler_start_tpfs, + g_object_ref (handler)); + g_object_unref (channel); + return; + } + + /* No TpStreamedMedia object (we are redialing). Request a new media channel that + * will be used to create a new EmpathyTpStreamedMedia. */ + g_assert (priv->contact != NULL); + + account = empathy_contact_get_account (priv->contact); + request = empathy_call_create_streamed_media_request ( + empathy_contact_get_id (priv->contact), + priv->initial_audio, priv->initial_video); + + req = tp_account_channel_request_new (account, request, timestamp); + + tp_account_channel_request_create_and_handle_channel_async (req, NULL, + empathy_streamed_media_handler_request_cb, handler); + + g_object_unref (req); + g_hash_table_unref (request); +} + +/** + * empathy_streamed_media_handler_stop_call: + * @handler: an #EmpathyStreamedMediaHandler + * + * Closes the #EmpathyStreamedMediaHandler's call and frees its resources. + */ +void +empathy_streamed_media_handler_stop_call (EmpathyStreamedMediaHandler *handler) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (handler); + + if (priv->call != NULL) + { + empathy_tp_streamed_media_leave (priv->call); + g_object_unref (priv->call); + } + + priv->call = NULL; +} + +/** + * empathy_streamed_media_handler_has_initial_video: + * @handler: an #EmpathyStreamedMediaHandler + * + * Return %TRUE if the call managed by this #EmpathyStreamedMediaHandler was + * created with video enabled + * + * Return value: %TRUE if the call was created as a video conversation. + */ +gboolean +empathy_streamed_media_handler_has_initial_video (EmpathyStreamedMediaHandler *handler) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (handler); + + return priv->initial_video; +} + +FsCodec * +empathy_streamed_media_handler_get_send_audio_codec (EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + + return priv->send_audio_codec; +} + +FsCodec * +empathy_streamed_media_handler_get_send_video_codec (EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + + return priv->send_video_codec; +} + +GList * +empathy_streamed_media_handler_get_recv_audio_codecs (EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + + return priv->recv_audio_codecs; +} + +GList * +empathy_streamed_media_handler_get_recv_video_codecs (EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + + return priv->recv_video_codecs; +} + +FsCandidate * +empathy_streamed_media_handler_get_audio_remote_candidate ( + EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + + return priv->audio_remote_candidate; +} + +FsCandidate * +empathy_streamed_media_handler_get_audio_local_candidate ( + EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + + return priv->audio_local_candidate; +} + +FsCandidate * +empathy_streamed_media_handler_get_video_remote_candidate ( + EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + + return priv->video_remote_candidate; +} + +FsCandidate * +empathy_streamed_media_handler_get_video_local_candidate ( + EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + + return priv->video_local_candidate; +} diff --git a/src/empathy-streamed-media-handler.h b/src/empathy-streamed-media-handler.h new file mode 100644 index 000000000..429c95447 --- /dev/null +++ b/src/empathy-streamed-media-handler.h @@ -0,0 +1,106 @@ +/* + * empathy-streamed-media-handler.h - Header for EmpathyStreamedMediaHandler + * Copyright (C) 2008-2009 Collabora Ltd. + * @author Sjoerd Simons <sjoerd.simons@collabora.co.uk> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __EMPATHY_STREAMED_MEDIA_HANDLER_H__ +#define __EMPATHY_STREAMED_MEDIA_HANDLER_H__ + +#include <glib-object.h> + +#include <gst/gst.h> +#include <farstream/fs-conference.h> + +#include <libempathy/empathy-tp-streamed-media.h> +#include <libempathy/empathy-contact.h> + +G_BEGIN_DECLS + +typedef struct _EmpathyStreamedMediaHandler EmpathyStreamedMediaHandler; +typedef struct _EmpathyStreamedMediaHandlerClass EmpathyStreamedMediaHandlerClass; + +struct _EmpathyStreamedMediaHandlerClass { + GObjectClass parent_class; +}; + +struct _EmpathyStreamedMediaHandler { + GObject parent; + gpointer priv; +}; + +GType empathy_streamed_media_handler_get_type (void); + +/* TYPE MACROS */ +#define EMPATHY_TYPE_STREAMED_MEDIA_HANDLER \ + (empathy_streamed_media_handler_get_type ()) +#define EMPATHY_STREAMED_MEDIA_HANDLER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), EMPATHY_TYPE_STREAMED_MEDIA_HANDLER, \ + EmpathyStreamedMediaHandler)) +#define EMPATHY_STREAMED_MEDIA_HANDLER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), EMPATHY_TYPE_STREAMED_MEDIA_HANDLER, \ + EmpathyStreamedMediaHandlerClass)) +#define EMPATHY_IS_STREAMED_MEDIA_HANDLER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), EMPATHY_TYPE_STREAMED_MEDIA_HANDLER)) +#define EMPATHY_IS_STREAMED_MEDIA_HANDLER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), EMPATHY_TYPE_STREAMED_MEDIA_HANDLER)) +#define EMPATHY_STREAMED_MEDIA_HANDLER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), EMPATHY_TYPE_STREAMED_MEDIA_HANDLER, \ + EmpathyStreamedMediaHandlerClass)) + +EmpathyStreamedMediaHandler * empathy_streamed_media_handler_new_for_contact ( + EmpathyContact *contact); + +EmpathyStreamedMediaHandler * empathy_streamed_media_handler_new_for_channel ( + EmpathyTpStreamedMedia *call); + +void empathy_streamed_media_handler_start_call (EmpathyStreamedMediaHandler *handler, + gint64 timestamp); +void empathy_streamed_media_handler_stop_call (EmpathyStreamedMediaHandler *handler); + +gboolean empathy_streamed_media_handler_has_initial_video (EmpathyStreamedMediaHandler *handler); + +void empathy_streamed_media_handler_bus_message (EmpathyStreamedMediaHandler *handler, + GstBus *bus, GstMessage *message); + +FsCodec * empathy_streamed_media_handler_get_send_audio_codec ( + EmpathyStreamedMediaHandler *self); + +FsCodec * empathy_streamed_media_handler_get_send_video_codec ( + EmpathyStreamedMediaHandler *self); + +GList * empathy_streamed_media_handler_get_recv_audio_codecs ( + EmpathyStreamedMediaHandler *self); + +GList * empathy_streamed_media_handler_get_recv_video_codecs ( + EmpathyStreamedMediaHandler *self); + +FsCandidate * empathy_streamed_media_handler_get_audio_remote_candidate ( + EmpathyStreamedMediaHandler *self); + +FsCandidate * empathy_streamed_media_handler_get_audio_local_candidate ( + EmpathyStreamedMediaHandler *self); + +FsCandidate * empathy_streamed_media_handler_get_video_remote_candidate ( + EmpathyStreamedMediaHandler *self); + +FsCandidate * empathy_streamed_media_handler_get_video_local_candidate ( + EmpathyStreamedMediaHandler *self); + +G_END_DECLS + +#endif /* #ifndef __EMPATHY_STREAMED_MEDIA_HANDLER_H__*/ diff --git a/src/empathy-streamed-media-window-fullscreen.c b/src/empathy-streamed-media-window-fullscreen.c new file mode 100644 index 000000000..caa34ffd1 --- /dev/null +++ b/src/empathy-streamed-media-window-fullscreen.c @@ -0,0 +1,309 @@ +/* + * empathy-streamed-media-window-fullscreen.c - Source for EmpathyStreamedMediaWindowFullscreen + * Copyright (C) 2009 Collabora Ltd. + * + * Some code is based on the Totem Movie Player, especially + * totem-fullscreen.c which has the following copyright: + * Copyright (C) 2001-2007 Bastien Nocera <hadess@hadess.net> + * Copyright (C) 2007 Sunil Mohan Adapa <sunilmohan@gnu.org.in> + * + * 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 "empathy-streamed-media-window-fullscreen.h" + +#include <gtk/gtk.h> + +#include <libempathy/empathy-utils.h> +#include <libempathy-gtk/empathy-ui-utils.h> + +/* The number of seconds for which the "leave fullscreen" popup should + be shown */ +#define FULLSCREEN_POPUP_TIMEOUT 5 + +G_DEFINE_TYPE (EmpathyStreamedMediaWindowFullscreen, empathy_streamed_media_window_fullscreen, + G_TYPE_OBJECT) + +/* private structure */ +typedef struct _EmpathyStreamedMediaWindowFullscreenPriv + EmpathyStreamedMediaWindowFullscreenPriv; + +struct _EmpathyStreamedMediaWindowFullscreenPriv +{ + EmpathyStreamedMediaWindow *parent_window; + + GtkWidget *leave_fullscreen_popup; + GtkWidget *video_widget; + + guint popup_timeout; + gboolean popup_creation_in_progress; + gboolean dispose_has_run; +}; + +#define GET_PRIV(o) \ + (G_TYPE_INSTANCE_GET_PRIVATE ((o), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW_FULLSCREEN, \ + EmpathyStreamedMediaWindowFullscreenPriv)) + +static void empathy_streamed_media_window_fullscreen_dispose (GObject *object); +static void empathy_streamed_media_window_fullscreen_finalize (GObject *object); + +static gboolean empathy_streamed_media_window_fullscreen_hide_popup ( + EmpathyStreamedMediaWindowFullscreen *fs); + +static void +empathy_streamed_media_window_fullscreen_set_cursor_visible ( + EmpathyStreamedMediaWindowFullscreen *fs, + gboolean show_cursor) +{ + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (fs); + GdkWindow *window; + + if (priv->video_widget == NULL) + return; + + window = gtk_widget_get_window (priv->video_widget); + + if (!show_cursor) + gdk_window_set_cursor (window, gdk_cursor_new (GDK_BLANK_CURSOR)); + else + gdk_window_set_cursor (window, NULL); +} + +static void +empathy_streamed_media_window_fullscreen_add_popup_timeout ( + EmpathyStreamedMediaWindowFullscreen *self) +{ + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (self); + + if (priv->popup_timeout == 0) + { + priv->popup_timeout = g_timeout_add_seconds (FULLSCREEN_POPUP_TIMEOUT, + (GSourceFunc) empathy_streamed_media_window_fullscreen_hide_popup, self); + } +} + +static void +empathy_streamed_media_window_fullscreen_remove_popup_timeout ( + EmpathyStreamedMediaWindowFullscreen *self) +{ + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (self); + + if (priv->popup_timeout != 0) + { + g_source_remove (priv->popup_timeout); + priv->popup_timeout = 0; + } +} + +void +empathy_streamed_media_window_fullscreen_show_popup (EmpathyStreamedMediaWindowFullscreen *self) +{ + gint leave_fullscreen_width, leave_fullscreen_height; + GdkScreen *screen; + GdkRectangle fullscreen_rect; + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (self); + + g_assert (self->is_fullscreen); + + g_return_if_fail (priv->parent_window != NULL); + + if (priv->popup_creation_in_progress) + return; + + if (!gtk_window_is_active (GTK_WINDOW (priv->parent_window))) + return; + + priv->popup_creation_in_progress = TRUE; + + empathy_streamed_media_window_fullscreen_set_cursor_visible (self, TRUE); + + /* Obtaining the screen rectangle */ + screen = gtk_window_get_screen (GTK_WINDOW (priv->parent_window)); + gdk_screen_get_monitor_geometry (screen, + gdk_screen_get_monitor_at_window (screen, + gtk_widget_get_window (GTK_WIDGET (priv->parent_window))), + &fullscreen_rect); + + /* Getting the popup window sizes */ + gtk_window_get_size (GTK_WINDOW (priv->leave_fullscreen_popup), + &leave_fullscreen_width, &leave_fullscreen_height); + + /* Moving the popup to the top-right corner (if the direction is LTR) or the + top-left corner (if the direction is RTL).*/ + if (gtk_widget_get_direction (priv->leave_fullscreen_popup) + == GTK_TEXT_DIR_LTR) + { + gtk_window_move (GTK_WINDOW (priv->leave_fullscreen_popup), + fullscreen_rect.width + fullscreen_rect.x - leave_fullscreen_width, + fullscreen_rect.y); + + } + else + { + gtk_window_move (GTK_WINDOW (priv->leave_fullscreen_popup), + fullscreen_rect.x, fullscreen_rect.y); + } + + gtk_widget_show_all (priv->leave_fullscreen_popup); + empathy_streamed_media_window_fullscreen_add_popup_timeout (self); + + priv->popup_creation_in_progress = FALSE; +} + +static gboolean +empathy_streamed_media_window_fullscreen_hide_popup (EmpathyStreamedMediaWindowFullscreen *fs) +{ + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (fs); + + if (priv->video_widget == NULL || !fs->is_fullscreen) + return TRUE; + + gtk_widget_hide (priv->leave_fullscreen_popup); + empathy_streamed_media_window_fullscreen_remove_popup_timeout (fs); + + empathy_streamed_media_window_fullscreen_set_cursor_visible (fs, FALSE); + + return FALSE; +} + +static void +empathy_streamed_media_window_fullscreen_init (EmpathyStreamedMediaWindowFullscreen *self) +{ + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (self); + GtkBuilder *gui; + gchar *filename; + + filename = empathy_file_lookup ("empathy-call-window-fullscreen.ui", "src"); + gui = empathy_builder_get_file (filename, + "leave_fullscreen_window", &priv->leave_fullscreen_popup, + "leave_fullscreen_button", &self->leave_fullscreen_button, + NULL); + + gtk_widget_add_events (priv->leave_fullscreen_popup, GDK_POINTER_MOTION_MASK); + + g_object_unref (gui); + g_free (filename); +} + +static void +empathy_streamed_media_window_fullscreen_class_init ( + EmpathyStreamedMediaWindowFullscreenClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (klass, sizeof (EmpathyStreamedMediaWindowFullscreenPriv)); + + object_class->dispose = empathy_streamed_media_window_fullscreen_dispose; + object_class->finalize = empathy_streamed_media_window_fullscreen_finalize; +} + +void +empathy_streamed_media_window_fullscreen_dispose (GObject *object) +{ + EmpathyStreamedMediaWindowFullscreen *self = EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN (object); + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (self); + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + if (priv->leave_fullscreen_popup != NULL) + gtk_widget_destroy (priv->leave_fullscreen_popup); + priv->leave_fullscreen_popup = NULL; + + if (G_OBJECT_CLASS (empathy_streamed_media_window_fullscreen_parent_class)->dispose) + { + G_OBJECT_CLASS ( + empathy_streamed_media_window_fullscreen_parent_class)->dispose (object); + } +} + +void +empathy_streamed_media_window_fullscreen_finalize (GObject *object) +{ + EmpathyStreamedMediaWindowFullscreen *self = EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN (object); + + empathy_streamed_media_window_fullscreen_remove_popup_timeout (self); + + G_OBJECT_CLASS ( + empathy_streamed_media_window_fullscreen_parent_class)->finalize (object); +} + +static void +empathy_streamed_media_window_fullscreen_parent_window_notify (GtkWidget *parent_window, + GParamSpec *property, EmpathyStreamedMediaWindowFullscreen *fs) +{ + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (fs); + + if (!fs->is_fullscreen) + return; + + if (parent_window == GTK_WIDGET (priv->parent_window) && + !gtk_window_is_active (GTK_WINDOW (parent_window))) + { + empathy_streamed_media_window_fullscreen_hide_popup (fs); + empathy_streamed_media_window_fullscreen_set_cursor_visible (fs, TRUE); + } +} + +EmpathyStreamedMediaWindowFullscreen * +empathy_streamed_media_window_fullscreen_new (EmpathyStreamedMediaWindow *parent_window) +{ + EmpathyStreamedMediaWindowFullscreen *self = EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN ( + g_object_new (EMPATHY_TYPE_STREAMED_MEDIA_WINDOW_FULLSCREEN, NULL)); + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (self); + + priv->parent_window = parent_window; + g_signal_connect (G_OBJECT (priv->parent_window), "notify::is-active", + G_CALLBACK (empathy_streamed_media_window_fullscreen_parent_window_notify), self); + + return self; +} + +void +empathy_streamed_media_window_fullscreen_set_fullscreen (EmpathyStreamedMediaWindowFullscreen *fs, + gboolean set_fullscreen) +{ + + if (set_fullscreen) + empathy_streamed_media_window_fullscreen_remove_popup_timeout (fs); + else + empathy_streamed_media_window_fullscreen_hide_popup (fs); + + empathy_streamed_media_window_fullscreen_set_cursor_visible (fs, !set_fullscreen); + fs->is_fullscreen = set_fullscreen; +} + +static void +video_widget_destroy_cb (GtkWidget *widget, + EmpathyStreamedMediaWindowFullscreen *self) +{ + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (self); + + priv->video_widget = NULL; +} + +void +empathy_streamed_media_window_fullscreen_set_video_widget ( + EmpathyStreamedMediaWindowFullscreen *fs, + GtkWidget *video_widget) +{ + EmpathyStreamedMediaWindowFullscreenPriv *priv = GET_PRIV (fs); + priv->video_widget = video_widget; + + tp_g_signal_connect_object (video_widget, "destroy", + G_CALLBACK (video_widget_destroy_cb), fs, 0); +} diff --git a/src/empathy-streamed-media-window-fullscreen.h b/src/empathy-streamed-media-window-fullscreen.h new file mode 100644 index 000000000..bdb95fb0b --- /dev/null +++ b/src/empathy-streamed-media-window-fullscreen.h @@ -0,0 +1,77 @@ +/* + * empathy-streamed-media-window-fullscreen.h - Header for EmpathyStreamedMediaWindowFullscreen + * Copyright (C) 2009 Collabora Ltd. + * + * 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 + */ + +#ifndef __EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN_H__ +#define __EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN_H__ + +#include <glib-object.h> +#include <gtk/gtk.h> + +#include "empathy-streamed-media-window.h" + +G_BEGIN_DECLS + +typedef struct _EmpathyStreamedMediaWindowFullscreen EmpathyStreamedMediaWindowFullscreen; +typedef struct _EmpathyStreamedMediaWindowFullscreenClass + EmpathyStreamedMediaWindowFullscreenClass; + +struct _EmpathyStreamedMediaWindowFullscreenClass { + GObjectClass parent_class; +}; + +struct _EmpathyStreamedMediaWindowFullscreen { + GObject parent; + gboolean is_fullscreen; + GtkWidget *leave_fullscreen_button; +}; + +GType empathy_streamed_media_window_fullscreen_get_type (void); + +/* TYPE MACROS */ +#define EMPATHY_TYPE_STREAMED_MEDIA_WINDOW_FULLSCREEN \ + (empathy_streamed_media_window_fullscreen_get_type ()) +#define EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW_FULLSCREEN, \ + EmpathyStreamedMediaWindowFullscreen)) +#define EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW_FULLSCREEN, \ + EmpathyStreamedMediaWindowClassFullscreen)) +#define EMPATHY_IS_STREAMED_MEDIA_WINDOW_FULLSCREEN(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW_FULLSCREEN)) +#define EMPATHY_IS_STREAMED_MEDIA_WINDOW_FULLSCREEN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW_FULLSCREEN)) +#define EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW_FULLSCREEN, \ + EmpathyStreamedMediaWindowFullscreenClass)) + +EmpathyStreamedMediaWindowFullscreen *empathy_streamed_media_window_fullscreen_new ( + EmpathyStreamedMediaWindow *parent); + +void empathy_streamed_media_window_fullscreen_set_fullscreen ( + EmpathyStreamedMediaWindowFullscreen *fs, + gboolean set_fullscreen); +void empathy_streamed_media_window_fullscreen_set_video_widget ( + EmpathyStreamedMediaWindowFullscreen *fs, + GtkWidget *video_widget); +void empathy_streamed_media_window_fullscreen_show_popup ( + EmpathyStreamedMediaWindowFullscreen *fs); + +G_END_DECLS + +#endif /* #ifndef __EMPATHY_STREAMED_MEDIA_WINDOW_FULLSCREEN_H__*/ diff --git a/src/empathy-streamed-media-window.c b/src/empathy-streamed-media-window.c new file mode 100644 index 000000000..38cd980cc --- /dev/null +++ b/src/empathy-streamed-media-window.c @@ -0,0 +1,3199 @@ +/* + * empathy-streamed-media-window.c - Source for EmpathyStreamedMediaWindow + * Copyright (C) 2008-2009 Collabora Ltd. + * @author Sjoerd Simons <sjoerd.simons@collabora.co.uk> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include <stdio.h> +#include <stdlib.h> + +#include <math.h> + +#include <gdk/gdkkeysyms.h> +#include <gst/gst.h> +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include <telepathy-glib/util.h> + +#include <telepathy-farstream/telepathy-farstream.h> + +#include <farstream/fs-element-added-notifier.h> +#include <farstream/fs-utils.h> + +#include <libempathy/empathy-tp-contact-factory.h> +#include <libempathy/empathy-utils.h> +#include <libempathy-gtk/empathy-avatar-image.h> +#include <libempathy-gtk/empathy-dialpad-widget.h> +#include <libempathy-gtk/empathy-ui-utils.h> +#include <libempathy-gtk/empathy-sound-manager.h> +#include <libempathy-gtk/empathy-geometry.h> +#include <libempathy-gtk/empathy-images.h> + +#define DEBUG_FLAG EMPATHY_DEBUG_VOIP +#include <libempathy/empathy-debug.h> + +#include "empathy-streamed-media-window.h" +#include "empathy-streamed-media-window-fullscreen.h" +#include "empathy-video-widget.h" +#include "empathy-audio-src.h" +#include "empathy-audio-sink.h" +#include "empathy-video-src.h" +#include "ev-sidebar.h" + +#define CONTENT_HBOX_BORDER_WIDTH 6 +#define CONTENT_HBOX_SPACING 3 +#define CONTENT_HBOX_CHILDREN_PACKING_PADDING 3 + +#define SELF_VIDEO_SECTION_WIDTH 160 +#define SELF_VIDEO_SECTION_HEIGTH 120 + +/* The avatar's default width and height are set to the same value because we + want a square icon. */ +#define REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH EMPATHY_VIDEO_WIDGET_DEFAULT_HEIGHT +#define REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT \ + EMPATHY_VIDEO_WIDGET_DEFAULT_HEIGHT + +/* If an video input error occurs, the error message will start with "v4l" */ +#define VIDEO_INPUT_ERROR_PREFIX "v4l" + +/* The time interval in milliseconds between 2 outgoing rings */ +#define MS_BETWEEN_RING 500 + +G_DEFINE_TYPE(EmpathyStreamedMediaWindow, empathy_streamed_media_window, GTK_TYPE_WINDOW) + +/* signal enum */ +#if 0 +enum +{ + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; +#endif + +enum { + PROP_STREAMED_MEDIA_HANDLER = 1, +}; + +typedef enum { + CONNECTING, + CONNECTED, + DISCONNECTED, + REDIALING +} CallState; + +typedef enum { + CAMERA_STATE_OFF = 0, + CAMERA_STATE_PREVIEW, + CAMERA_STATE_ON, +} CameraState; + +/* private structure */ +typedef struct _EmpathyStreamedMediaWindowPriv EmpathyStreamedMediaWindowPriv; + +struct _EmpathyStreamedMediaWindowPriv +{ + gboolean dispose_has_run; + EmpathyStreamedMediaHandler *handler; + EmpathyContact *contact; + + guint call_state; + gboolean outgoing; + + GtkUIManager *ui_manager; + GtkWidget *errors_vbox; + /* widget displays the video received from the remote user. This widget is + * alive only during call. */ + GtkWidget *video_output; + GtkWidget *video_preview; + GtkWidget *remote_user_avatar_widget; + GtkWidget *self_user_avatar_widget; + GtkWidget *sidebar; + GtkWidget *sidebar_button; + GtkWidget *statusbar; + GtkWidget *volume_button; + GtkWidget *redial_button; + GtkWidget *mic_button; + GtkWidget *toolbar; + GtkWidget *pane; + GtkAction *redial; + GtkAction *menu_fullscreen; + GtkAction *action_camera_on; + GtkWidget *tool_button_camera_off; + GtkWidget *tool_button_camera_preview; + GtkWidget *tool_button_camera_on; + + /* The frames and boxes that contain self and remote avatar and video + input/output. When we redial, we destroy and re-create the boxes */ + GtkWidget *remote_user_output_frame; + GtkWidget *self_user_output_frame; + GtkWidget *remote_user_output_hbox; + GtkWidget *self_user_output_hbox; + + /* We keep a reference on the hbox which contains the main content so we can + easilly repack everything when toggling fullscreen */ + GtkWidget *content_hbox; + + /* This vbox is contained in the content_hbox and it contains the + self_user_output_frame and the sidebar button. When toggling fullscreen, + it needs to be repacked. We keep a reference on it for easier access. */ + GtkWidget *vbox; + + gulong video_output_motion_handler_id; + guint bus_message_source_id; + + gdouble volume; + GtkWidget *volume_scale; + GtkWidget *volume_progress_bar; + GtkAdjustment *audio_input_adj; + + GtkWidget *dtmf_panel; + + /* Details vbox */ + GtkWidget *details_vbox; + GtkWidget *vcodec_encoding_label; + GtkWidget *acodec_encoding_label; + GtkWidget *vcodec_decoding_label; + GtkWidget *acodec_decoding_label; + + GtkWidget *audio_remote_candidate_label; + GtkWidget *audio_local_candidate_label; + GtkWidget *video_remote_candidate_label; + GtkWidget *video_local_candidate_label; + GtkWidget *video_remote_candidate_info_img; + GtkWidget *video_local_candidate_info_img; + GtkWidget *audio_remote_candidate_info_img; + GtkWidget *audio_local_candidate_info_img; + + GstElement *video_input; + GstElement *audio_input; + GstElement *audio_output; + GstElement *pipeline; + GstElement *video_tee; + + GstElement *funnel; + + FsElementAddedNotifier *fsnotifier; + + guint context_id; + + GTimer *timer; + guint timer_id; + + GtkWidget *video_contrast; + GtkWidget *video_brightness; + GtkWidget *video_gamma; + + GMutex *lock; + gboolean call_started; + gboolean sending_video; + CameraState camera_state; + + EmpathyStreamedMediaWindowFullscreen *fullscreen; + gboolean is_fullscreen; + + /* Those fields represent the state of the window before it actually was in + fullscreen mode. */ + gboolean sidebar_was_visible_before_fs; + gint original_width_before_fs; + gint original_height_before_fs; + + /* TRUE if the call should be started when the pipeline is playing */ + gboolean start_call_when_playing; + /* TRUE if we requested to set the pipeline in the playing state */ + gboolean pipeline_playing; + + EmpathySoundManager *sound_mgr; +}; + +#define GET_PRIV(o) \ + (G_TYPE_INSTANCE_GET_PRIVATE ((o), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW, \ + EmpathyStreamedMediaWindowPriv)) + +static void empathy_streamed_media_window_realized_cb (GtkWidget *widget, + EmpathyStreamedMediaWindow *window); + +static gboolean empathy_streamed_media_window_delete_cb (GtkWidget *widget, + GdkEvent *event, EmpathyStreamedMediaWindow *window); + +static gboolean empathy_streamed_media_window_state_event_cb (GtkWidget *widget, + GdkEventWindowState *event, EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_sidebar_toggled_cb (GtkToggleButton *toggle, + EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_set_send_video (EmpathyStreamedMediaWindow *window, + CameraState state); + +static void empathy_streamed_media_window_mic_toggled_cb ( + GtkToggleToolButton *toggle, EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_sidebar_hidden_cb (EvSidebar *sidebar, + EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_sidebar_shown_cb (EvSidebar *sidebar, + EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_hangup_cb (gpointer object, + EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_fullscreen_cb (gpointer object, + EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_fullscreen_toggle (EmpathyStreamedMediaWindow *window); + +static gboolean empathy_streamed_media_window_video_button_press_cb ( + GtkWidget *video_output, GdkEventButton *event, EmpathyStreamedMediaWindow *window); + +static gboolean empathy_streamed_media_window_key_press_cb (GtkWidget *video_output, + GdkEventKey *event, EmpathyStreamedMediaWindow *window); + +static gboolean empathy_streamed_media_window_video_output_motion_notify ( + GtkWidget *widget, GdkEventMotion *event, EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_video_menu_popup (EmpathyStreamedMediaWindow *window, + guint button); + +static void empathy_streamed_media_window_redial_cb (gpointer object, + EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_restart_call (EmpathyStreamedMediaWindow *window); + +static void empathy_streamed_media_window_status_message (EmpathyStreamedMediaWindow *window, + gchar *message); + +static void empathy_streamed_media_window_update_avatars_visibility (EmpathyTpStreamedMedia *call, + EmpathyStreamedMediaWindow *window); + +static gboolean empathy_streamed_media_window_bus_message (GstBus *bus, + GstMessage *message, gpointer user_data); + +static void +empathy_streamed_media_window_volume_changed_cb (GtkScaleButton *button, + gdouble value, EmpathyStreamedMediaWindow *window); + +static void block_camera_control_signals (EmpathyStreamedMediaWindow *self); +static void unblock_camera_control_signals (EmpathyStreamedMediaWindow *self); + +static void +empathy_streamed_media_window_setup_toolbar (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GtkToolItem *tool_item; + GtkWidget *camera_off_icon; + GdkPixbuf *pixbuf, *modded_pixbuf; + + /* set the icon of the 'camera off' button by greying off the webcam icon */ + pixbuf = empathy_pixbuf_from_icon_name ("camera-web", + GTK_ICON_SIZE_SMALL_TOOLBAR); + + modded_pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, + gdk_pixbuf_get_width (pixbuf), + gdk_pixbuf_get_height (pixbuf)); + + gdk_pixbuf_saturate_and_pixelate (pixbuf, modded_pixbuf, 1.0, TRUE); + g_object_unref (pixbuf); + + camera_off_icon = gtk_image_new_from_pixbuf (modded_pixbuf); + g_object_unref (modded_pixbuf); + gtk_tool_button_set_icon_widget (GTK_TOOL_BUTTON ( + priv->tool_button_camera_off), camera_off_icon); + + /* Add an empty expanded GtkToolItem so the volume button is at the end of + * the toolbar. */ + tool_item = gtk_tool_item_new (); + gtk_tool_item_set_expand (tool_item, TRUE); + gtk_widget_show (GTK_WIDGET (tool_item)); + gtk_toolbar_insert (GTK_TOOLBAR (priv->toolbar), tool_item, -1); + + priv->volume_button = gtk_volume_button_new (); + /* FIXME listen to the audiosinks signals and update the button according to + * that, for now starting out at 1.0 and assuming only the app changes the + * volume will do */ + gtk_scale_button_set_value (GTK_SCALE_BUTTON (priv->volume_button), 1.0); + g_signal_connect (G_OBJECT (priv->volume_button), "value-changed", + G_CALLBACK (empathy_streamed_media_window_volume_changed_cb), self); + + tool_item = gtk_tool_item_new (); + gtk_container_add (GTK_CONTAINER (tool_item), priv->volume_button); + gtk_widget_show_all (GTK_WIDGET (tool_item)); + gtk_toolbar_insert (GTK_TOOLBAR (priv->toolbar), tool_item, -1); +} + +static void +dtmf_start_tone_cb (EmpathyDialpadWidget *dialpad, + TpDTMFEvent event, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + EmpathyTpStreamedMedia *call; + + g_object_get (priv->handler, "tp-call", &call, NULL); + + empathy_tp_streamed_media_start_tone (call, event); + + g_object_unref (call); +} + +static void +dtmf_stop_tone_cb (EmpathyDialpadWidget *self, + TpDTMFEvent event, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + EmpathyTpStreamedMedia *call; + + g_object_get (priv->handler, "tp-call", &call, NULL); + + empathy_tp_streamed_media_stop_tone (call); + + g_object_unref (call); +} + +static GtkWidget * +empathy_streamed_media_window_create_video_input_add_slider (EmpathyStreamedMediaWindow *self, + gchar *label_text, GtkWidget *bin) +{ + GtkWidget *vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2); + GtkWidget *scale = gtk_vscale_new_with_range (0, 100, 10); + GtkWidget *label = gtk_label_new (label_text); + + gtk_widget_set_sensitive (scale, FALSE); + + gtk_container_add (GTK_CONTAINER (bin), vbox); + + gtk_range_set_inverted (GTK_RANGE (scale), TRUE); + gtk_box_pack_start (GTK_BOX (vbox), scale, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0); + + return scale; +} + +static void +empathy_streamed_media_window_video_contrast_changed_cb (GtkAdjustment *adj, + EmpathyStreamedMediaWindow *self) + +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + empathy_video_src_set_channel (priv->video_input, + EMPATHY_GST_VIDEO_SRC_CHANNEL_CONTRAST, gtk_adjustment_get_value (adj)); +} + +static void +empathy_streamed_media_window_video_brightness_changed_cb (GtkAdjustment *adj, + EmpathyStreamedMediaWindow *self) + +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + empathy_video_src_set_channel (priv->video_input, + EMPATHY_GST_VIDEO_SRC_CHANNEL_BRIGHTNESS, gtk_adjustment_get_value (adj)); +} + +static void +empathy_streamed_media_window_video_gamma_changed_cb (GtkAdjustment *adj, + EmpathyStreamedMediaWindow *self) + +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + empathy_video_src_set_channel (priv->video_input, + EMPATHY_GST_VIDEO_SRC_CHANNEL_GAMMA, gtk_adjustment_get_value (adj)); +} + + +static GtkWidget * +empathy_streamed_media_window_create_video_input (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GtkWidget *hbox; + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 3); + gtk_box_set_homogeneous (GTK_BOX (hbox), TRUE); + + priv->video_contrast = empathy_streamed_media_window_create_video_input_add_slider ( + self, _("Contrast"), hbox); + + priv->video_brightness = empathy_streamed_media_window_create_video_input_add_slider ( + self, _("Brightness"), hbox); + + priv->video_gamma = empathy_streamed_media_window_create_video_input_add_slider ( + self, _("Gamma"), hbox); + + return hbox; +} + +static void +empathy_streamed_media_window_setup_video_input (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + guint supported; + GtkAdjustment *adj; + + supported = empathy_video_src_get_supported_channels (priv->video_input); + + if (supported & EMPATHY_GST_VIDEO_SRC_SUPPORTS_CONTRAST) + { + adj = gtk_range_get_adjustment (GTK_RANGE (priv->video_contrast)); + + gtk_adjustment_set_value (adj, + empathy_video_src_get_channel (priv->video_input, + EMPATHY_GST_VIDEO_SRC_CHANNEL_CONTRAST)); + + g_signal_connect (G_OBJECT (adj), "value-changed", + G_CALLBACK (empathy_streamed_media_window_video_contrast_changed_cb), self); + + gtk_widget_set_sensitive (priv->video_contrast, TRUE); + } + + if (supported & EMPATHY_GST_VIDEO_SRC_SUPPORTS_BRIGHTNESS) + { + adj = gtk_range_get_adjustment (GTK_RANGE (priv->video_brightness)); + + gtk_adjustment_set_value (adj, + empathy_video_src_get_channel (priv->video_input, + EMPATHY_GST_VIDEO_SRC_CHANNEL_BRIGHTNESS)); + + g_signal_connect (G_OBJECT (adj), "value-changed", + G_CALLBACK (empathy_streamed_media_window_video_brightness_changed_cb), self); + gtk_widget_set_sensitive (priv->video_brightness, TRUE); + } + + if (supported & EMPATHY_GST_VIDEO_SRC_SUPPORTS_GAMMA) + { + adj = gtk_range_get_adjustment (GTK_RANGE (priv->video_gamma)); + + gtk_adjustment_set_value (adj, + empathy_video_src_get_channel (priv->video_input, + EMPATHY_GST_VIDEO_SRC_CHANNEL_GAMMA)); + + g_signal_connect (G_OBJECT (adj), "value-changed", + G_CALLBACK (empathy_streamed_media_window_video_gamma_changed_cb), self); + gtk_widget_set_sensitive (priv->video_gamma, TRUE); + } +} + +static void +empathy_streamed_media_window_mic_volume_changed_cb (GtkAdjustment *adj, + EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + gdouble volume; + + volume = gtk_adjustment_get_value (adj)/100.0; + + /* Don't store the volume because of muting */ + if (volume > 0 || gtk_toggle_tool_button_get_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->mic_button))) + priv->volume = volume; + + /* Ensure that the toggle button is active if the volume is > 0 and inactive + * if it's smaller than 0 */ + if ((volume > 0) != gtk_toggle_tool_button_get_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->mic_button))) + gtk_toggle_tool_button_set_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->mic_button), volume > 0); + + empathy_audio_src_set_volume (EMPATHY_GST_AUDIO_SRC (priv->audio_input), + volume); +} + +static void +empathy_streamed_media_window_audio_input_level_changed_cb (EmpathyGstAudioSrc *src, + gdouble level, EmpathyStreamedMediaWindow *window) +{ + gdouble value; + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + value = CLAMP (pow (10, level / 20), 0.0, 1.0); + gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->volume_progress_bar), + value); +} + +static GtkWidget * +empathy_streamed_media_window_create_audio_input (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GtkWidget *hbox, *vbox, *label; + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 3); + gtk_box_set_homogeneous (GTK_BOX (hbox), TRUE); + + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 3); + gtk_box_pack_start (GTK_BOX (hbox), vbox, FALSE, FALSE, 3); + + priv->volume_scale = gtk_vscale_new_with_range (0, 150, 100); + gtk_range_set_inverted (GTK_RANGE (priv->volume_scale), TRUE); + label = gtk_label_new (_("Volume")); + + priv->audio_input_adj = gtk_range_get_adjustment ( + GTK_RANGE (priv->volume_scale)); + priv->volume = empathy_audio_src_get_volume (EMPATHY_GST_AUDIO_SRC + (priv->audio_input)); + gtk_adjustment_set_value (priv->audio_input_adj, priv->volume * 100); + + g_signal_connect (G_OBJECT (priv->audio_input_adj), "value-changed", + G_CALLBACK (empathy_streamed_media_window_mic_volume_changed_cb), self); + + gtk_box_pack_start (GTK_BOX (vbox), priv->volume_scale, TRUE, TRUE, 3); + gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 3); + + priv->volume_progress_bar = gtk_progress_bar_new (); + + gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->volume_progress_bar), + GTK_ORIENTATION_VERTICAL); + + gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->volume_progress_bar), + 0); + + gtk_box_pack_start (GTK_BOX (hbox), priv->volume_progress_bar, FALSE, FALSE, + 3); + + return hbox; +} + +static void +create_video_output_widget (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstBus *bus; + + g_assert (priv->video_output == NULL); + g_assert (priv->pipeline != NULL); + + bus = gst_pipeline_get_bus (GST_PIPELINE (priv->pipeline)); + priv->video_output = empathy_video_widget_new (bus); + + gtk_box_pack_start (GTK_BOX (priv->remote_user_output_hbox), + priv->video_output, TRUE, TRUE, 0); + + gtk_widget_add_events (priv->video_output, + GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK); + g_signal_connect (G_OBJECT (priv->video_output), "button-press-event", + G_CALLBACK (empathy_streamed_media_window_video_button_press_cb), self); + + g_object_unref (bus); +} + +static void +create_video_input (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + g_assert (priv->video_input == NULL); + priv->video_input = empathy_video_src_new (); + gst_object_ref (priv->video_input); + gst_object_sink (priv->video_input); +} + +static void +create_audio_input (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + g_assert (priv->audio_input == NULL); + priv->audio_input = empathy_audio_src_new (); + gst_object_ref (priv->audio_input); + gst_object_sink (priv->audio_input); + + tp_g_signal_connect_object (priv->audio_input, "peak-level-changed", + G_CALLBACK (empathy_streamed_media_window_audio_input_level_changed_cb), + self, 0); +} + +static void +add_video_preview_to_pipeline (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstElement *preview; + + g_assert (priv->video_preview != NULL); + g_assert (priv->pipeline != NULL); + g_assert (priv->video_input != NULL); + g_assert (priv->video_tee != NULL); + + preview = empathy_video_widget_get_element ( + EMPATHY_VIDEO_WIDGET (priv->video_preview)); + + if (!gst_bin_add (GST_BIN (priv->pipeline), priv->video_input)) + { + g_warning ("Could not add video input to pipeline"); + return; + } + + if (!gst_bin_add (GST_BIN (priv->pipeline), priv->video_tee)) + { + g_warning ("Could not add video tee to pipeline"); + return; + } + + if (!gst_bin_add (GST_BIN (priv->pipeline), preview)) + { + g_warning ("Could not add video preview to pipeline"); + return; + } + + if (!gst_element_link (priv->video_input, priv->video_tee)) + { + g_warning ("Could not link video input to video tee"); + return; + } + + if (!gst_element_link (priv->video_tee, preview)) + { + g_warning ("Could not link video tee to video preview"); + return; + } +} + +static void +create_video_preview (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstBus *bus; + + g_assert (priv->video_preview == NULL); + g_assert (priv->video_tee == NULL); + + bus = gst_pipeline_get_bus (GST_PIPELINE (priv->pipeline)); + + priv->video_preview = empathy_video_widget_new_with_size (bus, + SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGTH); + g_object_set (priv->video_preview, + "sync", FALSE, + "async", TRUE, + "flip-video", TRUE, + NULL); + + gtk_box_pack_start (GTK_BOX (priv->self_user_output_hbox), + priv->video_preview, TRUE, TRUE, 0); + + priv->video_tee = gst_element_factory_make ("tee", NULL); + gst_object_ref (priv->video_tee); + gst_object_sink (priv->video_tee); + + g_object_unref (bus); +} + +static void +play_camera (EmpathyStreamedMediaWindow *window, + gboolean play) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + GstElement *preview; + GstState state; + + if (priv->video_preview == NULL) + { + create_video_preview (window); + add_video_preview_to_pipeline (window); + } + + if (play) + state = GST_STATE_PLAYING; + else + state = GST_STATE_NULL; + + preview = empathy_video_widget_get_element ( + EMPATHY_VIDEO_WIDGET (priv->video_preview)); + + gst_element_set_state (preview, state); + gst_element_set_state (priv->video_input, state); + gst_element_set_state (priv->video_tee, state); +} + +static void +display_video_preview (EmpathyStreamedMediaWindow *self, + gboolean display) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (display) + { + /* Display the preview and hide the self avatar */ + DEBUG ("Show video preview"); + + play_camera (self, TRUE); + gtk_widget_show (priv->video_preview); + gtk_widget_hide (priv->self_user_avatar_widget); + } + else + { + /* Display the self avatar and hide the preview */ + DEBUG ("Show self avatar"); + + if (priv->video_preview != NULL) + { + gtk_widget_hide (priv->video_preview); + play_camera (self, FALSE); + } + gtk_widget_show (priv->self_user_avatar_widget); + } +} + +static void +empathy_streamed_media_window_set_state_connecting (EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + empathy_streamed_media_window_status_message (window, _("Connecting…")); + priv->call_state = CONNECTING; + + if (priv->outgoing) + empathy_sound_manager_start_playing (priv->sound_mgr, GTK_WIDGET (window), + EMPATHY_SOUND_PHONE_OUTGOING, MS_BETWEEN_RING); +} + +static void +disable_camera (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (priv->camera_state == CAMERA_STATE_OFF) + return; + + DEBUG ("Disable camera"); + + display_video_preview (self, FALSE); + + if (priv->camera_state == CAMERA_STATE_ON) + empathy_streamed_media_window_set_send_video (self, CAMERA_STATE_OFF); + + block_camera_control_signals (self); + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_on), FALSE); + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_preview), FALSE); + + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_off), TRUE); + gtk_radio_action_set_current_value (GTK_RADIO_ACTION (priv->action_camera_on), + CAMERA_STATE_OFF); + unblock_camera_control_signals (self); + + priv->camera_state = CAMERA_STATE_OFF; +} + +static void +tool_button_camera_off_toggled_cb (GtkToggleToolButton *toggle, + EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (!gtk_toggle_tool_button_get_active (toggle)) + { + if (priv->camera_state == CAMERA_STATE_OFF) + { + /* We can't change the state by disabling the button */ + block_camera_control_signals (self); + gtk_toggle_tool_button_set_active (toggle, TRUE); + unblock_camera_control_signals (self); + } + + return; + } + + disable_camera (self); +} + +static void +enable_preview (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (priv->camera_state == CAMERA_STATE_PREVIEW) + return; + + DEBUG ("Enable preview"); + + if (priv->camera_state == CAMERA_STATE_ON) + { + /* preview is already displayed so we just have to stop sending */ + empathy_streamed_media_window_set_send_video (self, CAMERA_STATE_PREVIEW); + } + else + { + display_video_preview (self, TRUE); + } + + block_camera_control_signals (self); + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_off), FALSE); + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_on), FALSE); + + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_preview), TRUE); + gtk_radio_action_set_current_value (GTK_RADIO_ACTION (priv->action_camera_on), + CAMERA_STATE_PREVIEW); + unblock_camera_control_signals (self); + + priv->camera_state = CAMERA_STATE_PREVIEW; +} + +static void +tool_button_camera_preview_toggled_cb (GtkToggleToolButton *toggle, + EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (!gtk_toggle_tool_button_get_active (toggle)) + { + if (priv->camera_state == CAMERA_STATE_PREVIEW) + { + /* We can't change the state by disabling the button */ + block_camera_control_signals (self); + gtk_toggle_tool_button_set_active (toggle, TRUE); + unblock_camera_control_signals (self); + } + + return; + } + + enable_preview (self); +} + +static void +enable_camera (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (priv->camera_state == CAMERA_STATE_ON) + return; + + if (priv->video_input == NULL) + { + DEBUG ("Can't enable camera, no input"); + return; + } + + + DEBUG ("Enable camera"); + + empathy_streamed_media_window_set_send_video (self, CAMERA_STATE_ON); + + block_camera_control_signals (self); + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_off), FALSE); + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_preview), FALSE); + + gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON ( + priv->tool_button_camera_on), TRUE); + gtk_radio_action_set_current_value (GTK_RADIO_ACTION (priv->action_camera_on), + CAMERA_STATE_ON); + unblock_camera_control_signals (self); + + priv->camera_state = CAMERA_STATE_ON; +} + +static void +tool_button_camera_on_toggled_cb (GtkToggleToolButton *toggle, + EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (!gtk_toggle_tool_button_get_active (toggle)) + { + if (priv->camera_state == CAMERA_STATE_ON) + { + /* We can't change the state by disabling the button */ + block_camera_control_signals (self); + gtk_toggle_tool_button_set_active (toggle, TRUE); + unblock_camera_control_signals (self); + } + + return; + } + + enable_camera (self); +} + +static void +action_camera_change_cb (GtkRadioAction *action, + GtkRadioAction *current, + EmpathyStreamedMediaWindow *self) +{ + CameraState state; + + state = gtk_radio_action_get_current_value (current); + + switch (state) + { + case CAMERA_STATE_OFF: + disable_camera (self); + break; + + case CAMERA_STATE_PREVIEW: + enable_preview (self); + break; + + case CAMERA_STATE_ON: + enable_camera (self); + break; + + default: + g_assert_not_reached (); + } +} + +static void +create_pipeline (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstBus *bus; + + g_assert (priv->pipeline == NULL); + + priv->pipeline = gst_pipeline_new (NULL); + priv->pipeline_playing = FALSE; + + bus = gst_pipeline_get_bus (GST_PIPELINE (priv->pipeline)); + priv->bus_message_source_id = gst_bus_add_watch (bus, + empathy_streamed_media_window_bus_message, self); + + g_object_unref (bus); +} + + +static void +empathy_streamed_media_window_init (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GtkBuilder *gui; + GtkWidget *top_vbox; + GtkWidget *h; + GtkWidget *arrow; + GtkWidget *page; + gchar *filename; + GtkWidget *scroll; + + filename = empathy_file_lookup ("empathy-streamed-media-window.ui", "src"); + gui = empathy_builder_get_file (filename, + "call_window_vbox", &top_vbox, + "errors_vbox", &priv->errors_vbox, + "pane", &priv->pane, + "statusbar", &priv->statusbar, + "redial", &priv->redial_button, + "microphone", &priv->mic_button, + "toolbar", &priv->toolbar, + "menuredial", &priv->redial, + "ui_manager", &priv->ui_manager, + "menufullscreen", &priv->menu_fullscreen, + "camera_off", &priv->tool_button_camera_off, + "camera_preview", &priv->tool_button_camera_preview, + "camera_on", &priv->tool_button_camera_on, + "action_camera_on", &priv->action_camera_on, + "details_vbox", &priv->details_vbox, + "vcodec_encoding_label", &priv->vcodec_encoding_label, + "acodec_encoding_label", &priv->acodec_encoding_label, + "acodec_decoding_label", &priv->acodec_decoding_label, + "vcodec_decoding_label", &priv->vcodec_decoding_label, + "audio_remote_candidate_label", &priv->audio_remote_candidate_label, + "audio_local_candidate_label", &priv->audio_local_candidate_label, + "video_remote_candidate_label", &priv->video_remote_candidate_label, + "video_local_candidate_label", &priv->video_local_candidate_label, + "video_remote_candidate_info_img", &priv->video_remote_candidate_info_img, + "video_local_candidate_info_img", &priv->video_local_candidate_info_img, + "audio_remote_candidate_info_img", &priv->audio_remote_candidate_info_img, + "audio_local_candidate_info_img", &priv->audio_local_candidate_info_img, + NULL); + g_free (filename); + + empathy_builder_connect (gui, self, + "menuhangup", "activate", empathy_streamed_media_window_hangup_cb, + "hangup", "clicked", empathy_streamed_media_window_hangup_cb, + "menuredial", "activate", empathy_streamed_media_window_redial_cb, + "redial", "clicked", empathy_streamed_media_window_redial_cb, + "microphone", "toggled", empathy_streamed_media_window_mic_toggled_cb, + "menufullscreen", "activate", empathy_streamed_media_window_fullscreen_cb, + "camera_off", "toggled", tool_button_camera_off_toggled_cb, + "camera_preview", "toggled", tool_button_camera_preview_toggled_cb, + "camera_on", "toggled", tool_button_camera_on_toggled_cb, + "action_camera_on", "changed", action_camera_change_cb, + NULL); + + gtk_action_set_sensitive (priv->menu_fullscreen, FALSE); + + priv->lock = g_mutex_new (); + + gtk_container_add (GTK_CONTAINER (self), top_vbox); + + priv->content_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, + CONTENT_HBOX_SPACING); + gtk_container_set_border_width (GTK_CONTAINER (priv->content_hbox), + CONTENT_HBOX_BORDER_WIDTH); + gtk_paned_pack1 (GTK_PANED (priv->pane), priv->content_hbox, TRUE, FALSE); + + /* remote user output frame */ + priv->remote_user_output_frame = gtk_frame_new (NULL); + gtk_widget_set_size_request (priv->remote_user_output_frame, + EMPATHY_VIDEO_WIDGET_DEFAULT_WIDTH, EMPATHY_VIDEO_WIDGET_DEFAULT_HEIGHT); + gtk_box_pack_start (GTK_BOX (priv->content_hbox), + priv->remote_user_output_frame, TRUE, TRUE, + CONTENT_HBOX_CHILDREN_PACKING_PADDING); + + priv->remote_user_output_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + + priv->remote_user_avatar_widget = gtk_image_new (); + + gtk_box_pack_start (GTK_BOX (priv->remote_user_output_hbox), + priv->remote_user_avatar_widget, TRUE, TRUE, 0); + + gtk_container_add (GTK_CONTAINER (priv->remote_user_output_frame), + priv->remote_user_output_hbox); + + /* self user output frame */ + priv->self_user_output_frame = gtk_frame_new (NULL); + gtk_widget_set_size_request (priv->self_user_output_frame, + SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGTH); + + priv->self_user_output_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + + priv->self_user_avatar_widget = gtk_image_new (); + gtk_box_pack_start (GTK_BOX (priv->self_user_output_hbox), + priv->self_user_avatar_widget, TRUE, TRUE, 0); + + gtk_container_add (GTK_CONTAINER (priv->self_user_output_frame), + priv->self_user_output_hbox); + + create_pipeline (self); + create_video_output_widget (self); + create_audio_input (self); + create_video_input (self); + + priv->fsnotifier = fs_element_added_notifier_new (); + fs_element_added_notifier_add (priv->fsnotifier, GST_BIN (priv->pipeline)); + + /* The call will be started as soon the pipeline is playing */ + priv->start_call_when_playing = TRUE; + + priv->vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 3); + gtk_box_pack_start (GTK_BOX (priv->content_hbox), priv->vbox, + FALSE, FALSE, CONTENT_HBOX_CHILDREN_PACKING_PADDING); + gtk_box_pack_start (GTK_BOX (priv->vbox), priv->self_user_output_frame, + FALSE, FALSE, 0); + + empathy_streamed_media_window_setup_toolbar (self); + + priv->sidebar_button = gtk_toggle_button_new_with_mnemonic (_("_Sidebar")); + arrow = gtk_arrow_new (GTK_ARROW_RIGHT, GTK_SHADOW_NONE); + g_signal_connect (G_OBJECT (priv->sidebar_button), "toggled", + G_CALLBACK (empathy_streamed_media_window_sidebar_toggled_cb), self); + + gtk_button_set_image (GTK_BUTTON (priv->sidebar_button), arrow); + + h = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 3); + gtk_box_pack_end (GTK_BOX (priv->vbox), h, FALSE, FALSE, 3); + gtk_box_pack_end (GTK_BOX (h), priv->sidebar_button, FALSE, FALSE, 3); + + priv->sidebar = ev_sidebar_new (); + g_signal_connect (G_OBJECT (priv->sidebar), + "hide", G_CALLBACK (empathy_streamed_media_window_sidebar_hidden_cb), self); + g_signal_connect (G_OBJECT (priv->sidebar), + "show", G_CALLBACK (empathy_streamed_media_window_sidebar_shown_cb), self); + gtk_paned_pack2 (GTK_PANED (priv->pane), priv->sidebar, FALSE, FALSE); + + page = empathy_streamed_media_window_create_audio_input (self); + ev_sidebar_add_page (EV_SIDEBAR (priv->sidebar), "audio-input", + _("Audio input"), page); + + page = empathy_streamed_media_window_create_video_input (self); + ev_sidebar_add_page (EV_SIDEBAR (priv->sidebar), "video-input", + _("Video input"), page); + + priv->dtmf_panel = empathy_dialpad_widget_new (); + g_signal_connect (priv->dtmf_panel, "start-tone", + G_CALLBACK (dtmf_start_tone_cb), self); + g_signal_connect (priv->dtmf_panel, "stop-tone", + G_CALLBACK (dtmf_stop_tone_cb), self); + ev_sidebar_add_page (EV_SIDEBAR (priv->sidebar), "dialpad", + _("Dialpad"), priv->dtmf_panel); + + gtk_widget_set_sensitive (priv->dtmf_panel, FALSE); + + /* Put the details vbox in a scroll window as it can require a lot of + * horizontal space. */ + scroll = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scroll), + priv->details_vbox); + + ev_sidebar_add_page (EV_SIDEBAR (priv->sidebar), "details", + _("Details"), scroll); + + gtk_widget_show_all (top_vbox); + + gtk_widget_hide (priv->sidebar); + + priv->fullscreen = empathy_streamed_media_window_fullscreen_new (self); + empathy_streamed_media_window_fullscreen_set_video_widget (priv->fullscreen, + priv->video_output); + g_signal_connect (G_OBJECT (priv->fullscreen->leave_fullscreen_button), + "clicked", G_CALLBACK (empathy_streamed_media_window_fullscreen_cb), self); + + g_signal_connect (G_OBJECT (self), "realize", + G_CALLBACK (empathy_streamed_media_window_realized_cb), self); + + g_signal_connect (G_OBJECT (self), "delete-event", + G_CALLBACK (empathy_streamed_media_window_delete_cb), self); + + g_signal_connect (G_OBJECT (self), "window-state-event", + G_CALLBACK (empathy_streamed_media_window_state_event_cb), self); + + g_signal_connect (G_OBJECT (self), "key-press-event", + G_CALLBACK (empathy_streamed_media_window_key_press_cb), self); + + priv->timer = g_timer_new (); + + g_object_ref (priv->ui_manager); + g_object_unref (gui); + + priv->sound_mgr = empathy_sound_manager_dup_singleton (); + + empathy_geometry_bind (GTK_WINDOW (self), "av-window"); +} + +/* Instead of specifying a width and a height, we specify only one size. That's + because we want a square avatar icon. */ +static void +init_contact_avatar_with_size (EmpathyContact *contact, + GtkWidget *image_widget, + gint size) +{ + GdkPixbuf *pixbuf_avatar = NULL; + + if (contact != NULL) + { + pixbuf_avatar = empathy_pixbuf_avatar_from_contact_scaled (contact, + size, size); + } + + if (pixbuf_avatar == NULL) + { + pixbuf_avatar = empathy_pixbuf_from_icon_name_sized ( + EMPATHY_IMAGE_AVATAR_DEFAULT, size); + } + + gtk_image_set_from_pixbuf (GTK_IMAGE (image_widget), pixbuf_avatar); + + if (pixbuf_avatar != NULL) + g_object_unref (pixbuf_avatar); +} + +static void +set_window_title (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + gchar *tmp; + + /* translators: Call is a noun and %s is the contact name. This string + * is used in the window title */ + tmp = g_strdup_printf (_("Call with %s"), + empathy_contact_get_alias (priv->contact)); + gtk_window_set_title (GTK_WINDOW (self), tmp); + g_free (tmp); +} + +static void +contact_name_changed_cb (EmpathyContact *contact, + GParamSpec *pspec, EmpathyStreamedMediaWindow *self) +{ + set_window_title (self); +} + +static void +contact_avatar_changed_cb (EmpathyContact *contact, + GParamSpec *pspec, GtkWidget *avatar_widget) +{ + int size; + GtkAllocation allocation; + + gtk_widget_get_allocation (avatar_widget, &allocation); + size = allocation.height; + + if (size == 0) + { + /* the widget is not allocated yet, set a default size */ + size = MIN (REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT, + REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH); + } + + init_contact_avatar_with_size (contact, avatar_widget, size); +} + +static void +empathy_streamed_media_window_got_self_contact_cb (TpConnection *connection, + EmpathyContact *contact, const GError *error, gpointer user_data, + GObject *weak_object) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + init_contact_avatar_with_size (contact, priv->self_user_avatar_widget, + MIN (SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGTH)); + + g_signal_connect (contact, "notify::avatar", + G_CALLBACK (contact_avatar_changed_cb), priv->self_user_avatar_widget); +} + +static void +empathy_streamed_media_window_setup_avatars (EmpathyStreamedMediaWindow *self, + EmpathyStreamedMediaHandler *handler) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + g_object_get (handler, "contact", &(priv->contact), NULL); + + if (priv->contact != NULL) + { + TpConnection *connection; + + set_window_title (self); + + g_signal_connect (priv->contact, "notify::name", + G_CALLBACK (contact_name_changed_cb), self); + g_signal_connect (priv->contact, "notify::avatar", + G_CALLBACK (contact_avatar_changed_cb), + priv->remote_user_avatar_widget); + + /* Retreiving the self avatar */ + connection = empathy_contact_get_connection (priv->contact); + empathy_tp_contact_factory_get_from_handle (connection, + tp_connection_get_self_handle (connection), + empathy_streamed_media_window_got_self_contact_cb, self, NULL, G_OBJECT (self)); + } + else + { + g_warning ("call handler doesn't have a contact"); + /* translators: Call is a noun. This string is used in the window + * title */ + gtk_window_set_title (GTK_WINDOW (self), _("Call")); + + /* Since we can't access the remote contact, we can't get a connection + to it and can't get the self contact (and its avatar). This means + that we have to manually set the self avatar. */ + init_contact_avatar_with_size (NULL, priv->self_user_avatar_widget, + MIN (SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGTH)); + } + + init_contact_avatar_with_size (priv->contact, + priv->remote_user_avatar_widget, + MIN (REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH, + REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT)); + + /* The remote avatar is shown by default and will be hidden when we receive + video from the remote side. */ + gtk_widget_hide (priv->video_output); + gtk_widget_show (priv->remote_user_avatar_widget); +} + +static void +update_send_codec (EmpathyStreamedMediaWindow *self, + gboolean audio) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + FsCodec *codec; + GtkWidget *widget; + gchar *tmp; + + if (audio) + { + codec = empathy_streamed_media_handler_get_send_audio_codec (priv->handler); + widget = priv->acodec_encoding_label; + } + else + { + codec = empathy_streamed_media_handler_get_send_video_codec (priv->handler); + widget = priv->vcodec_encoding_label; + } + + if (codec == NULL) + return; + + tmp = g_strdup_printf ("%s/%u", codec->encoding_name, codec->clock_rate); + gtk_label_set_text (GTK_LABEL (widget), tmp); + g_free (tmp); +} + +static void +send_audio_codec_notify_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = user_data; + + update_send_codec (self, TRUE); +} + +static void +send_video_codec_notify_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = user_data; + + update_send_codec (self, FALSE); +} + +static void +update_recv_codec (EmpathyStreamedMediaWindow *self, + gboolean audio) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GList *codecs, *l; + GtkWidget *widget; + GString *str = NULL; + + if (audio) + { + codecs = empathy_streamed_media_handler_get_recv_audio_codecs (priv->handler); + widget = priv->acodec_decoding_label; + } + else + { + codecs = empathy_streamed_media_handler_get_recv_video_codecs (priv->handler); + widget = priv->vcodec_decoding_label; + } + + if (codecs == NULL) + return; + + for (l = codecs; l != NULL; l = g_list_next (l)) + { + FsCodec *codec = l->data; + + if (str == NULL) + str = g_string_new (NULL); + else + g_string_append (str, ", "); + + g_string_append_printf (str, "%s/%u", codec->encoding_name, + codec->clock_rate); + } + + gtk_label_set_text (GTK_LABEL (widget), str->str); + g_string_free (str, TRUE); +} + +static void +recv_audio_codecs_notify_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = user_data; + + update_recv_codec (self, TRUE); +} + +static void +recv_video_codecs_notify_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = user_data; + + update_recv_codec (self, FALSE); +} + +static const gchar * +candidate_type_to_str (FsCandidate *candidate) +{ + switch (candidate->type) + { + case FS_CANDIDATE_TYPE_HOST: + return "host"; + case FS_CANDIDATE_TYPE_SRFLX: + return "server reflexive"; + case FS_CANDIDATE_TYPE_PRFLX: + return "peer reflexive"; + case FS_CANDIDATE_TYPE_RELAY: + return "relay"; + case FS_CANDIDATE_TYPE_MULTICAST: + return "multicast"; + } + + return NULL; +} + +static const gchar * +candidate_type_to_desc (FsCandidate *candidate) +{ + switch (candidate->type) + { + case FS_CANDIDATE_TYPE_HOST: + return _("The IP address as seen by the machine"); + case FS_CANDIDATE_TYPE_SRFLX: + return _("The IP address as seen by a server on the Internet"); + case FS_CANDIDATE_TYPE_PRFLX: + return _("The IP address of the peer as seen by the other side"); + case FS_CANDIDATE_TYPE_RELAY: + return _("The IP address of a relay server"); + case FS_CANDIDATE_TYPE_MULTICAST: + return _("The IP address of the multicast group"); + } + + return NULL; +} + +static void +update_candidat_widget (EmpathyStreamedMediaWindow *self, + GtkWidget *label, + GtkWidget *img, + FsCandidate *candidate) +{ + gchar *str; + + g_assert (candidate != NULL); + str = g_strdup_printf ("%s %u (%s)", candidate->ip, + candidate->port, candidate_type_to_str (candidate)); + + gtk_label_set_text (GTK_LABEL (label), str); + gtk_widget_set_tooltip_text (img, candidate_type_to_desc (candidate)); + + g_free (str); +} + +static void +candidates_changed_cb (GObject *object, + FsMediaType type, + EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + FsCandidate *candidate = NULL; + + if (type == FS_MEDIA_TYPE_VIDEO) + { + /* Update remote candidate */ + candidate = empathy_streamed_media_handler_get_video_remote_candidate ( + priv->handler); + + update_candidat_widget (self, priv->video_remote_candidate_label, + priv->video_remote_candidate_info_img, candidate); + + /* Update local candidate */ + candidate = empathy_streamed_media_handler_get_video_local_candidate ( + priv->handler); + + update_candidat_widget (self, priv->video_local_candidate_label, + priv->video_local_candidate_info_img, candidate); + } + else + { + /* Update remote candidate */ + candidate = empathy_streamed_media_handler_get_audio_remote_candidate ( + priv->handler); + + update_candidat_widget (self, priv->audio_remote_candidate_label, + priv->audio_remote_candidate_info_img, candidate); + + /* Update local candidate */ + candidate = empathy_streamed_media_handler_get_audio_local_candidate ( + priv->handler); + + update_candidat_widget (self, priv->audio_local_candidate_label, + priv->audio_local_candidate_info_img, candidate); + } +} + +static void +empathy_streamed_media_window_constructed (GObject *object) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (object); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + EmpathyTpStreamedMedia *call; + + g_assert (priv->handler != NULL); + + g_object_get (priv->handler, "tp-call", &call, NULL); + priv->outgoing = (call == NULL); + if (call != NULL) + g_object_unref (call); + + empathy_streamed_media_window_setup_avatars (self, priv->handler); + empathy_streamed_media_window_set_state_connecting (self); + + if (!empathy_streamed_media_handler_has_initial_video (priv->handler)) + { + gtk_toggle_tool_button_set_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->tool_button_camera_off), TRUE); + } + /* If call has InitialVideo, the preview will be started once the call has + * been started (start_call()). */ + + update_send_codec (self, TRUE); + update_send_codec (self, FALSE); + update_recv_codec (self, TRUE); + update_recv_codec (self, FALSE); + + tp_g_signal_connect_object (priv->handler, "notify::send-audio-codec", + G_CALLBACK (send_audio_codec_notify_cb), self, 0); + tp_g_signal_connect_object (priv->handler, "notify::send-video-codec", + G_CALLBACK (send_video_codec_notify_cb), self, 0); + tp_g_signal_connect_object (priv->handler, "notify::recv-audio-codecs", + G_CALLBACK (recv_audio_codecs_notify_cb), self, 0); + tp_g_signal_connect_object (priv->handler, "notify::recv-video-codecs", + G_CALLBACK (recv_video_codecs_notify_cb), self, 0); + + tp_g_signal_connect_object (priv->handler, "candidates-changed", + G_CALLBACK (candidates_changed_cb), self, 0); +} + +static void empathy_streamed_media_window_dispose (GObject *object); +static void empathy_streamed_media_window_finalize (GObject *object); + +static void +empathy_streamed_media_window_set_property (GObject *object, + guint property_id, const GValue *value, GParamSpec *pspec) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (object); + + switch (property_id) + { + case PROP_STREAMED_MEDIA_HANDLER: + priv->handler = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +empathy_streamed_media_window_get_property (GObject *object, + guint property_id, GValue *value, GParamSpec *pspec) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (object); + + switch (property_id) + { + case PROP_STREAMED_MEDIA_HANDLER: + g_value_set_object (value, priv->handler); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +empathy_streamed_media_window_class_init ( + EmpathyStreamedMediaWindowClass *empathy_streamed_media_window_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (empathy_streamed_media_window_class); + GParamSpec *param_spec; + + g_type_class_add_private (empathy_streamed_media_window_class, + sizeof (EmpathyStreamedMediaWindowPriv)); + + object_class->constructed = empathy_streamed_media_window_constructed; + object_class->set_property = empathy_streamed_media_window_set_property; + object_class->get_property = empathy_streamed_media_window_get_property; + + object_class->dispose = empathy_streamed_media_window_dispose; + object_class->finalize = empathy_streamed_media_window_finalize; + + param_spec = g_param_spec_object ("handler", + "handler", "The call handler", + EMPATHY_TYPE_STREAMED_MEDIA_HANDLER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, + PROP_STREAMED_MEDIA_HANDLER, param_spec); +} + +static void +empathy_streamed_media_window_video_stream_changed_cb (EmpathyTpStreamedMedia *call, + GParamSpec *property, EmpathyStreamedMediaWindow *self) +{ + DEBUG ("video stream changed"); + empathy_streamed_media_window_update_avatars_visibility (call, self); +} + +void +empathy_streamed_media_window_dispose (GObject *object) +{ + EmpathyTpStreamedMedia *call; + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (object); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + g_object_get (priv->handler, "tp-call", &call, NULL); + + if (call != NULL) + { + g_object_unref (call); + } + + if (priv->handler != NULL) + { + empathy_streamed_media_handler_stop_call (priv->handler); + g_object_unref (priv->handler); + } + priv->handler = NULL; + + if (priv->bus_message_source_id != 0) + { + g_source_remove (priv->bus_message_source_id); + priv->bus_message_source_id = 0; + } + + if (priv->pipeline != NULL) + g_object_unref (priv->pipeline); + priv->pipeline = NULL; + + if (priv->video_input != NULL) + g_object_unref (priv->video_input); + priv->video_input = NULL; + + if (priv->audio_input != NULL) + g_object_unref (priv->audio_input); + priv->audio_input = NULL; + + if (priv->video_tee != NULL) + g_object_unref (priv->video_tee); + priv->video_tee = NULL; + + if (priv->fsnotifier != NULL) + g_object_unref (priv->fsnotifier); + priv->fsnotifier = NULL; + + if (priv->timer_id != 0) + g_source_remove (priv->timer_id); + priv->timer_id = 0; + + if (priv->ui_manager != NULL) + g_object_unref (priv->ui_manager); + priv->ui_manager = NULL; + + if (priv->fullscreen != NULL) + g_object_unref (priv->fullscreen); + priv->fullscreen = NULL; + + if (priv->contact != NULL) + { + g_signal_handlers_disconnect_by_func (priv->contact, + contact_name_changed_cb, self); + g_object_unref (priv->contact); + priv->contact = NULL; + } + + tp_clear_object (&priv->sound_mgr); + + /* release any references held by the object here */ + if (G_OBJECT_CLASS (empathy_streamed_media_window_parent_class)->dispose) + G_OBJECT_CLASS (empathy_streamed_media_window_parent_class)->dispose (object); +} + +static void +disconnect_video_output_motion_handler (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (priv->video_output_motion_handler_id != 0) + { + g_signal_handler_disconnect (G_OBJECT (priv->video_output), + priv->video_output_motion_handler_id); + priv->video_output_motion_handler_id = 0; + } +} + +void +empathy_streamed_media_window_finalize (GObject *object) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (object); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + disconnect_video_output_motion_handler (self); + + /* free any data held directly by the object here */ + g_mutex_free (priv->lock); + + g_timer_destroy (priv->timer); + + G_OBJECT_CLASS (empathy_streamed_media_window_parent_class)->finalize (object); +} + + +EmpathyStreamedMediaWindow * +empathy_streamed_media_window_new (EmpathyStreamedMediaHandler *handler) +{ + return EMPATHY_STREAMED_MEDIA_WINDOW ( + g_object_new (EMPATHY_TYPE_STREAMED_MEDIA_WINDOW, "handler", handler, NULL)); +} + +static void +empathy_streamed_media_window_conference_added_cb (EmpathyStreamedMediaHandler *handler, + GstElement *conference, gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GKeyFile *keyfile; + + keyfile = fs_utils_get_default_element_properties (conference); + if (keyfile != NULL) + fs_element_added_notifier_set_properties_from_keyfile (priv->fsnotifier, + keyfile); + + gst_bin_add (GST_BIN (priv->pipeline), conference); + + gst_element_set_state (conference, GST_STATE_PLAYING); +} + +static gboolean +empathy_streamed_media_window_start_receiving_cb (EmpathyStreamedMediaHandler *handler, + FsMediaType type,gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (type != FS_MEDIA_TYPE_VIDEO) + return TRUE; + + /* video and direction is send */ + return priv->video_input != NULL; +} + +static gboolean +empathy_streamed_media_window_reset_pipeline (EmpathyStreamedMediaWindow *self) +{ + GstStateChangeReturn state_change_return; + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (priv->pipeline == NULL) + return TRUE; + + if (priv->bus_message_source_id != 0) + { + g_source_remove (priv->bus_message_source_id); + priv->bus_message_source_id = 0; + } + + state_change_return = gst_element_set_state (priv->pipeline, GST_STATE_NULL); + + if (state_change_return == GST_STATE_CHANGE_SUCCESS || + state_change_return == GST_STATE_CHANGE_NO_PREROLL) + { + if (priv->pipeline != NULL) + g_object_unref (priv->pipeline); + priv->pipeline = NULL; + + g_signal_handlers_disconnect_by_func (priv->audio_input_adj, + empathy_streamed_media_window_mic_volume_changed_cb, self); + + if (priv->video_tee != NULL) + g_object_unref (priv->video_tee); + priv->video_tee = NULL; + + if (priv->video_preview != NULL) + gtk_widget_destroy (priv->video_preview); + priv->video_preview = NULL; + + priv->funnel = NULL; + + create_pipeline (self); + /* Call will be started when user will hit the 'redial' button */ + priv->start_call_when_playing = FALSE; + gst_element_set_state (priv->pipeline, GST_STATE_PAUSED); + + return TRUE; + } + else + { + g_message ("Error: could not destroy pipeline. Closing call window"); + gtk_widget_destroy (GTK_WIDGET (self)); + + return FALSE; + } +} + +static void +reset_details_pane (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + /* translators: encoding video codec is unknown */ + gtk_label_set_text (GTK_LABEL (priv->vcodec_encoding_label), + C_("codec", "Unknown")); + /* translators: encoding audio codec is unknown */ + gtk_label_set_text (GTK_LABEL (priv->acodec_encoding_label), + C_("codec", "Unknown")); + /* translators: decoding video codec is unknown */ + gtk_label_set_text (GTK_LABEL (priv->vcodec_decoding_label), + C_("codec", "Unknown")); + /* translators: decoding audio codec is unknown */ + gtk_label_set_text (GTK_LABEL (priv->acodec_decoding_label), + C_("codec", "Unknown")); +} + +static gboolean +empathy_streamed_media_window_disconnected (EmpathyStreamedMediaWindow *self, + gboolean restart) +{ + gboolean could_disconnect = FALSE; + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + gboolean could_reset_pipeline; + + /* Leave full screen mode if needed */ + gtk_window_unfullscreen (GTK_WINDOW (self)); + + gtk_action_set_sensitive (priv->menu_fullscreen, FALSE); + + could_reset_pipeline = empathy_streamed_media_window_reset_pipeline (self); + + if (priv->call_state == CONNECTING) + empathy_sound_manager_stop (priv->sound_mgr, + EMPATHY_SOUND_PHONE_OUTGOING); + + if (priv->call_state != REDIALING) + priv->call_state = DISCONNECTED; + + if (could_reset_pipeline) + { + g_mutex_lock (priv->lock); + + g_timer_stop (priv->timer); + + if (priv->timer_id != 0) + g_source_remove (priv->timer_id); + priv->timer_id = 0; + + g_mutex_unlock (priv->lock); + + if (!restart) + /* We are about to destroy the window, no need to update it or create + * a video preview */ + return TRUE; + + empathy_streamed_media_window_status_message (self, _("Disconnected")); + + gtk_action_set_sensitive (priv->redial, TRUE); + gtk_widget_set_sensitive (priv->redial_button, TRUE); + + /* Unsensitive the camera and mic button */ + gtk_widget_set_sensitive (priv->tool_button_camera_on, FALSE); + gtk_action_set_sensitive (priv->action_camera_on, FALSE); + gtk_widget_set_sensitive (priv->mic_button, FALSE); + + /* Be sure that the mic button is enabled */ + gtk_toggle_tool_button_set_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->mic_button), TRUE); + + if (priv->camera_state == CAMERA_STATE_ON) + { + /* Enable the 'preview' button as we are not sending atm. */ + gtk_toggle_tool_button_set_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->tool_button_camera_preview), TRUE); + } + else if (priv->camera_state == CAMERA_STATE_PREVIEW) + { + /* Restart the preview with the new pipeline. */ + display_video_preview (self, TRUE); + } + + gtk_progress_bar_set_fraction ( + GTK_PROGRESS_BAR (priv->volume_progress_bar), 0); + + /* destroy the video output; it will be recreated when we'll redial */ + disconnect_video_output_motion_handler (self); + gtk_widget_destroy (priv->video_output); + priv->video_output = NULL; + + gtk_widget_show (priv->remote_user_avatar_widget); + + reset_details_pane (self); + + priv->sending_video = FALSE; + priv->call_started = FALSE; + + could_disconnect = TRUE; + + /* TODO: display the self avatar of the preview (depends if the "Always + * Show Video Preview" is enabled or not) */ + } + + return could_disconnect; +} + + +static void +empathy_streamed_media_window_channel_closed_cb (EmpathyStreamedMediaHandler *handler, + gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (empathy_streamed_media_window_disconnected (self, TRUE) && + priv->call_state == REDIALING) + empathy_streamed_media_window_restart_call (self); +} + + +static void +empathy_streamed_media_window_channel_stream_closed_cb (EmpathyStreamedMediaHandler *handler, + TfContent *content, gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + guint media_type; + + g_object_get (content, "media-type", &media_type, NULL); + + /* + * This assumes that there is only one video stream per channel... + */ + + if (media_type == TP_MEDIA_STREAM_TYPE_VIDEO) + { + if (priv->funnel != NULL) + { + GstElement *output; + + output = empathy_video_widget_get_element (EMPATHY_VIDEO_WIDGET + (priv->video_output)); + + gst_element_set_state (output, GST_STATE_NULL); + gst_element_set_state (priv->funnel, GST_STATE_NULL); + + gst_bin_remove (GST_BIN (priv->pipeline), output); + gst_bin_remove (GST_BIN (priv->pipeline), priv->funnel); + priv->funnel = NULL; + } + } + else if (media_type == TP_MEDIA_STREAM_TYPE_AUDIO) + { + if (priv->audio_output != NULL) + { + gst_element_set_state (priv->audio_output, GST_STATE_NULL); + + gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_output); + priv->audio_output = NULL; + } + } +} + +/* Called with global lock held */ +static GstPad * +empathy_streamed_media_window_get_video_sink_pad (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstPad *pad; + GstElement *output; + + if (priv->funnel == NULL) + { + output = empathy_video_widget_get_element (EMPATHY_VIDEO_WIDGET + (priv->video_output)); + + priv->funnel = gst_element_factory_make ("fsfunnel", NULL); + + if (!priv->funnel) + { + g_warning ("Could not create fsfunnel"); + return NULL; + } + + if (!gst_bin_add (GST_BIN (priv->pipeline), priv->funnel)) + { + gst_object_unref (priv->funnel); + priv->funnel = NULL; + g_warning ("Could not add funnel to pipeline"); + return NULL; + } + + if (!gst_bin_add (GST_BIN (priv->pipeline), output)) + { + g_warning ("Could not add the video output widget to the pipeline"); + goto error; + } + + if (!gst_element_link (priv->funnel, output)) + { + g_warning ("Could not link output sink to funnel"); + goto error_output_added; + } + + if (gst_element_set_state (output, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) + { + g_warning ("Could not start video sink"); + goto error_output_added; + } + + if (gst_element_set_state (priv->funnel, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) + { + g_warning ("Could not start funnel"); + goto error_output_added; + } + } + + pad = gst_element_get_request_pad (priv->funnel, "sink%d"); + + if (!pad) + g_warning ("Could not get request pad from funnel"); + + return pad; + + + error_output_added: + + gst_element_set_locked_state (priv->funnel, TRUE); + gst_element_set_locked_state (output, TRUE); + + gst_element_set_state (priv->funnel, GST_STATE_NULL); + gst_element_set_state (output, GST_STATE_NULL); + + gst_bin_remove (GST_BIN (priv->pipeline), output); + gst_element_set_locked_state (output, FALSE); + + error: + + gst_bin_remove (GST_BIN (priv->pipeline), priv->funnel); + priv->funnel = NULL; + + return NULL; +} + +/* Called with global lock held */ +static GstPad * +empathy_streamed_media_window_get_audio_sink_pad (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstPad *pad; + GstPadTemplate *template; + + if (priv->audio_output == NULL) + { + priv->audio_output = empathy_audio_sink_new (); + + if (!gst_bin_add (GST_BIN (priv->pipeline), priv->audio_output)) + { + g_warning ("Could not add audio sink to pipeline"); + g_object_unref (priv->audio_output); + goto error_add_output; + } + + if (gst_element_set_state (priv->audio_output, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) + { + g_warning ("Could not start audio sink"); + goto error; + } + } + + template = gst_element_class_get_pad_template ( + GST_ELEMENT_GET_CLASS (priv->audio_output), "sink%d"); + + pad = gst_element_request_pad (priv->audio_output, + template, NULL, NULL); + + if (pad == NULL) + { + g_warning ("Could not get sink pad from sink"); + return NULL; + } + + return pad; + +error: + gst_element_set_locked_state (priv->audio_output, TRUE); + gst_element_set_state (priv->audio_output, GST_STATE_NULL); + gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_output); + priv->audio_output = NULL; + +error_add_output: + + return NULL; +} + +static gboolean +empathy_streamed_media_window_update_timer (gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + gchar *str; + gdouble time_; + + time_ = g_timer_elapsed (priv->timer, NULL); + + /* Translators: number of minutes:seconds the caller has been connected */ + str = g_strdup_printf (_("Connected — %d:%02dm"), (int) time_ / 60, + (int) time_ % 60); + empathy_streamed_media_window_status_message (self, str); + g_free (str); + + return TRUE; +} + +static void +display_error (EmpathyStreamedMediaWindow *self, + EmpathyTpStreamedMedia *call, + const gchar *img, + const gchar *title, + const gchar *desc, + const gchar *details) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GtkWidget *info_bar; + GtkWidget *content_area; + GtkWidget *hbox; + GtkWidget *vbox; + GtkWidget *image; + GtkWidget *label; + gchar *txt; + + /* Create info bar */ + info_bar = gtk_info_bar_new_with_buttons (GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, + NULL); + + gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_WARNING); + + content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar)); + + /* hbox containing the image and the messages vbox */ + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 3); + gtk_container_add (GTK_CONTAINER (content_area), hbox); + + /* Add image */ + image = gtk_image_new_from_icon_name (img, GTK_ICON_SIZE_DIALOG); + gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0); + + /* vbox containing the main message and the details expander */ + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 3); + gtk_box_pack_start (GTK_BOX (hbox), vbox, TRUE, TRUE, 0); + + /* Add text */ + txt = g_strdup_printf ("<b>%s</b>\n%s", title, desc); + + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), txt); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_misc_set_alignment (GTK_MISC (label), 0, 0); + g_free (txt); + + gtk_box_pack_start (GTK_BOX (vbox), label, TRUE, TRUE, 0); + + /* Add details */ + if (details != NULL) + { + GtkWidget *expander; + + expander = gtk_expander_new (_("Technical Details")); + + txt = g_strdup_printf ("<i>%s</i>", details); + + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), txt); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_misc_set_alignment (GTK_MISC (label), 0, 0); + g_free (txt); + + gtk_container_add (GTK_CONTAINER (expander), label); + gtk_box_pack_start (GTK_BOX (vbox), expander, TRUE, TRUE, 0); + } + + g_signal_connect (info_bar, "response", + G_CALLBACK (gtk_widget_destroy), NULL); + + gtk_box_pack_start (GTK_BOX (priv->errors_vbox), info_bar, + FALSE, FALSE, CONTENT_HBOX_CHILDREN_PACKING_PADDING); + gtk_widget_show_all (info_bar); +} + +static gchar * +media_stream_error_to_txt (EmpathyStreamedMediaWindow *self, + EmpathyTpStreamedMedia *call, + gboolean audio, + TpMediaStreamError error) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + const gchar *cm; + gchar *url; + gchar *result; + + switch (error) + { + case TP_MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED: + if (audio) + return g_strdup_printf ( + _("%s's software does not understand any of the audio formats " + "supported by your computer"), + empathy_contact_get_alias (priv->contact)); + else + return g_strdup_printf ( + _("%s's software does not understand any of the video formats " + "supported by your computer"), + empathy_contact_get_alias (priv->contact)); + + case TP_MEDIA_STREAM_ERROR_CONNECTION_FAILED: + return g_strdup_printf ( + _("Can't establish a connection to %s. " + "One of you might be on a network that does not allow " + "direct connections."), + empathy_contact_get_alias (priv->contact)); + + case TP_MEDIA_STREAM_ERROR_NETWORK_ERROR: + return g_strdup (_("There was a failure on the network")); + + case TP_MEDIA_STREAM_ERROR_NO_CODECS: + if (audio) + return g_strdup (_("The audio formats necessary for this call " + "are not installed on your computer")); + else + return g_strdup (_("The video formats necessary for this call " + "are not installed on your computer")); + + case TP_MEDIA_STREAM_ERROR_INVALID_CM_BEHAVIOR: + cm = empathy_tp_streamed_media_get_connection_manager (call); + + url = g_strdup_printf ("http://bugs.freedesktop.org/enter_bug.cgi?" + "product=Telepathy&component=%s", cm); + + result = g_strdup_printf ( + _("Something unexpected happened in a Telepathy component. " + "Please <a href=\"%s\">report this bug</a> and attach " + "logs gathered from the 'Debug' window in the Help menu."), url); + + g_free (url); + return result; + + case TP_MEDIA_STREAM_ERROR_MEDIA_ERROR: + return g_strdup (_("There was a failure in the call engine")); + + case TP_MEDIA_STREAM_ERROR_EOS: + return g_strdup (_("The end of the stream was reached")); + + case TP_MEDIA_STREAM_ERROR_UNKNOWN: + default: + return NULL; + } +} + +static void +empathy_streamed_media_window_stream_error (EmpathyStreamedMediaWindow *self, + EmpathyTpStreamedMedia *call, + gboolean audio, + guint code, + const gchar *msg, + const gchar *icon, + const gchar *title) +{ + gchar *desc; + + desc = media_stream_error_to_txt (self, call, audio, code); + if (desc == NULL) + { + /* No description, use the error message. That's not great as it's not + * localized but it's better than nothing. */ + display_error (self, call, icon, title, msg, NULL); + } + else + { + display_error (self, call, icon, title, desc, msg); + g_free (desc); + } +} + +static void +empathy_streamed_media_window_audio_stream_error (EmpathyTpStreamedMedia *call, + guint code, + const gchar *msg, + EmpathyStreamedMediaWindow *self) +{ + empathy_streamed_media_window_stream_error (self, call, TRUE, code, msg, + "gnome-stock-mic", _("Can't establish audio stream")); +} + +static void +empathy_streamed_media_window_video_stream_error (EmpathyTpStreamedMedia *call, + guint code, + const gchar *msg, + EmpathyStreamedMediaWindow *self) +{ + empathy_streamed_media_window_stream_error (self, call, FALSE, code, msg, + "camera-web", _("Can't establish video stream")); +} + +static gboolean +empathy_streamed_media_window_connected (gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + EmpathyTpStreamedMedia *call; + gboolean can_send_video; + + empathy_sound_manager_stop (priv->sound_mgr, EMPATHY_SOUND_PHONE_OUTGOING); + + can_send_video = priv->video_input != NULL && priv->contact != NULL && + empathy_contact_can_voip_video (priv->contact); + + g_object_get (priv->handler, "tp-call", &call, NULL); + + tp_g_signal_connect_object (call, "notify::video-stream", + G_CALLBACK (empathy_streamed_media_window_video_stream_changed_cb), + self, 0); + + if (empathy_tp_streamed_media_has_dtmf (call)) + gtk_widget_set_sensitive (priv->dtmf_panel, TRUE); + + if (priv->video_input == NULL) + empathy_streamed_media_window_set_send_video (self, CAMERA_STATE_OFF); + + priv->sending_video = can_send_video ? + empathy_tp_streamed_media_is_sending_video (call) : FALSE; + + gtk_toggle_tool_button_set_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->tool_button_camera_on), + priv->sending_video && priv->video_input != NULL); + gtk_widget_set_sensitive (priv->tool_button_camera_on, can_send_video); + gtk_action_set_sensitive (priv->action_camera_on, can_send_video); + + gtk_action_set_sensitive (priv->redial, FALSE); + gtk_widget_set_sensitive (priv->redial_button, FALSE); + + gtk_widget_set_sensitive (priv->mic_button, TRUE); + + empathy_streamed_media_window_update_avatars_visibility (call, self); + + g_object_unref (call); + + g_mutex_lock (priv->lock); + + priv->timer_id = g_timeout_add_seconds (1, + empathy_streamed_media_window_update_timer, self); + + g_mutex_unlock (priv->lock); + + empathy_streamed_media_window_update_timer (self); + + gtk_action_set_sensitive (priv->menu_fullscreen, TRUE); + + return FALSE; +} + + +/* Called from the streaming thread */ +static gboolean +empathy_streamed_media_window_src_added_cb (EmpathyStreamedMediaHandler *handler, + GstPad *src, guint media_type, gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + gboolean retval = FALSE; + + GstPad *pad; + + g_mutex_lock (priv->lock); + + if (priv->call_state != CONNECTED) + { + g_timer_start (priv->timer); + priv->timer_id = g_idle_add (empathy_streamed_media_window_connected, self); + priv->call_state = CONNECTED; + } + + switch (media_type) + { + case TP_MEDIA_STREAM_TYPE_AUDIO: + pad = empathy_streamed_media_window_get_audio_sink_pad (self); + break; + case TP_MEDIA_STREAM_TYPE_VIDEO: + gtk_widget_hide (priv->remote_user_avatar_widget); + gtk_widget_show (priv->video_output); + pad = empathy_streamed_media_window_get_video_sink_pad (self); + break; + default: + g_assert_not_reached (); + } + + if (pad == NULL) + goto out; + + if (GST_PAD_LINK_FAILED (gst_pad_link (src, pad))) + g_warning ("Could not link %s sink pad", + media_type == TP_MEDIA_STREAM_TYPE_AUDIO ? "audio" : "video"); + else + retval = TRUE; + + gst_object_unref (pad); + + out: + + /* If no sink could be linked, try to add fakesink to prevent the whole call + * aborting */ + + if (!retval) + { + GstElement *fakesink = gst_element_factory_make ("fakesink", NULL); + + if (gst_bin_add (GST_BIN (priv->pipeline), fakesink)) + { + GstPad *sinkpad = gst_element_get_static_pad (fakesink, "sink"); + if (gst_element_set_state (fakesink, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE || + GST_PAD_LINK_FAILED (gst_pad_link (src, sinkpad))) + { + gst_element_set_locked_state (fakesink, TRUE); + gst_element_set_state (fakesink, GST_STATE_NULL); + gst_bin_remove (GST_BIN (priv->pipeline), fakesink); + } + else + { + DEBUG ("Could not link real sink, linked fakesink instead"); + } + gst_object_unref (sinkpad); + } + else + { + gst_object_unref (fakesink); + } + } + + + g_mutex_unlock (priv->lock); + + return TRUE; +} + +static gboolean +empathy_streamed_media_window_sink_added_cb (EmpathyStreamedMediaHandler *handler, + GstPad *sink, guint media_type, gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstPad *pad; + gboolean retval = FALSE; + + switch (media_type) + { + case TP_MEDIA_STREAM_TYPE_AUDIO: + if (!gst_bin_add (GST_BIN (priv->pipeline), priv->audio_input)) + { + g_warning ("Could not add audio source to pipeline"); + break; + } + + pad = gst_element_get_static_pad (priv->audio_input, "src"); + if (!pad) + { + gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input); + g_warning ("Could not get source pad from audio source"); + break; + } + + if (GST_PAD_LINK_FAILED (gst_pad_link (pad, sink))) + { + gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input); + g_warning ("Could not link audio source to farsight"); + break; + } + + if (gst_element_set_state (priv->audio_input, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) + { + g_warning ("Could not start audio source"); + gst_element_set_state (priv->audio_input, GST_STATE_NULL); + gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input); + break; + } + + retval = TRUE; + break; + case TP_MEDIA_STREAM_TYPE_VIDEO: + if (priv->video_input != NULL) + { + if (priv->video_tee != NULL) + { + pad = gst_element_get_request_pad (priv->video_tee, "src%d"); + if (GST_PAD_LINK_FAILED (gst_pad_link (pad, sink))) + { + g_warning ("Could not link videp soure input pipeline"); + break; + } + gst_object_unref (pad); + } + + retval = TRUE; + } + break; + default: + g_assert_not_reached (); + } + + return retval; +} + +static void +empathy_streamed_media_window_remove_video_input (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstElement *preview; + + disable_camera (self); + + DEBUG ("remove video input"); + preview = empathy_video_widget_get_element ( + EMPATHY_VIDEO_WIDGET (priv->video_preview)); + + gst_element_set_state (priv->video_input, GST_STATE_NULL); + gst_element_set_state (priv->video_tee, GST_STATE_NULL); + gst_element_set_state (preview, GST_STATE_NULL); + + gst_bin_remove_many (GST_BIN (priv->pipeline), priv->video_input, + priv->video_tee, preview, NULL); + + g_object_unref (priv->video_input); + priv->video_input = NULL; + g_object_unref (priv->video_tee); + priv->video_tee = NULL; + gtk_widget_destroy (priv->video_preview); + priv->video_preview = NULL; + + gtk_widget_set_sensitive (priv->tool_button_camera_on, FALSE); + gtk_action_set_sensitive (priv->action_camera_on, FALSE); + gtk_widget_set_sensitive (priv->tool_button_camera_preview, FALSE); +} + +static void +start_call (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + priv->call_started = TRUE; + empathy_streamed_media_handler_start_call (priv->handler, + empathy_get_current_action_time ()); + + if (empathy_streamed_media_handler_has_initial_video (priv->handler)) + { + /* Enable 'send video' buttons and display the preview */ + gtk_toggle_tool_button_set_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->tool_button_camera_on), TRUE); + } +} + +static gboolean +empathy_streamed_media_window_bus_message (GstBus *bus, GstMessage *message, + gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + GstState newstate; + + empathy_streamed_media_handler_bus_message (priv->handler, bus, message); + + switch (GST_MESSAGE_TYPE (message)) + { + case GST_MESSAGE_STATE_CHANGED: + if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->video_input)) + { + gst_message_parse_state_changed (message, NULL, &newstate, NULL); + if (newstate == GST_STATE_PAUSED) + empathy_streamed_media_window_setup_video_input (self); + } + if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->pipeline) && + !priv->call_started) + { + gst_message_parse_state_changed (message, NULL, &newstate, NULL); + if (newstate == GST_STATE_PAUSED) + { + gst_element_set_state (priv->pipeline, GST_STATE_PLAYING); + priv->pipeline_playing = TRUE; + + if (priv->start_call_when_playing) + start_call (self); + } + } + break; + case GST_MESSAGE_ERROR: + { + GError *error = NULL; + GstElement *gst_error; + gchar *debug; + + gst_message_parse_error (message, &error, &debug); + gst_error = GST_ELEMENT (GST_MESSAGE_SRC (message)); + + g_message ("Element error: %s -- %s\n", error->message, debug); + + if (g_str_has_prefix (gst_element_get_name (gst_error), + VIDEO_INPUT_ERROR_PREFIX)) + { + /* Remove the video input and continue */ + if (priv->video_input != NULL) + empathy_streamed_media_window_remove_video_input (self); + gst_element_set_state (priv->pipeline, GST_STATE_PLAYING); + } + else + { + empathy_streamed_media_window_disconnected (self, TRUE); + } + g_error_free (error); + g_free (debug); + } + case GST_MESSAGE_UNKNOWN: + case GST_MESSAGE_EOS: + case GST_MESSAGE_WARNING: + case GST_MESSAGE_INFO: + case GST_MESSAGE_TAG: + case GST_MESSAGE_BUFFERING: + case GST_MESSAGE_STATE_DIRTY: + case GST_MESSAGE_STEP_DONE: + case GST_MESSAGE_CLOCK_PROVIDE: + case GST_MESSAGE_CLOCK_LOST: + case GST_MESSAGE_NEW_CLOCK: + case GST_MESSAGE_STRUCTURE_CHANGE: + case GST_MESSAGE_STREAM_STATUS: + case GST_MESSAGE_APPLICATION: + case GST_MESSAGE_ELEMENT: + case GST_MESSAGE_SEGMENT_START: + case GST_MESSAGE_SEGMENT_DONE: + case GST_MESSAGE_DURATION: + case GST_MESSAGE_ANY: + default: + break; + } + + return TRUE; +} + +static void +empathy_streamed_media_window_update_avatars_visibility (EmpathyTpStreamedMedia *call, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + if (empathy_tp_streamed_media_is_receiving_video (call)) + { + gtk_widget_hide (priv->remote_user_avatar_widget); + gtk_widget_show (priv->video_output); + } + else + { + gtk_widget_hide (priv->video_output); + gtk_widget_show (priv->remote_user_avatar_widget); + } +} + +static void +call_handler_notify_tp_streamed_media_cb (EmpathyStreamedMediaHandler *handler, + GParamSpec *spec, + EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + EmpathyTpStreamedMedia *call; + + g_object_get (priv->handler, "tp-call", &call, NULL); + if (call == NULL) + return; + + tp_g_signal_connect_object (call, "audio-stream-error", + G_CALLBACK (empathy_streamed_media_window_audio_stream_error), self, 0); + tp_g_signal_connect_object (call, "video-stream-error", + G_CALLBACK (empathy_streamed_media_window_video_stream_error), self, 0); + + g_object_unref (call); +} + +static void +empathy_streamed_media_window_realized_cb (GtkWidget *widget, EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + EmpathyTpStreamedMedia *call; + + g_signal_connect (priv->handler, "conference-added", + G_CALLBACK (empathy_streamed_media_window_conference_added_cb), window); + g_signal_connect (priv->handler, "start-receiving", + G_CALLBACK (empathy_streamed_media_window_start_receiving_cb), window); + g_signal_connect (priv->handler, "closed", + G_CALLBACK (empathy_streamed_media_window_channel_closed_cb), window); + g_signal_connect (priv->handler, "src-pad-added", + G_CALLBACK (empathy_streamed_media_window_src_added_cb), window); + g_signal_connect (priv->handler, "sink-pad-added", + G_CALLBACK (empathy_streamed_media_window_sink_added_cb), window); + g_signal_connect (priv->handler, "stream-closed", + G_CALLBACK (empathy_streamed_media_window_channel_stream_closed_cb), window); + + g_object_get (priv->handler, "tp-call", &call, NULL); + if (call != NULL) + { + tp_g_signal_connect_object (call, "audio-stream-error", + G_CALLBACK (empathy_streamed_media_window_audio_stream_error), window, + 0); + tp_g_signal_connect_object (call, "video-stream-error", + G_CALLBACK (empathy_streamed_media_window_video_stream_error), window, + 0); + + g_object_unref (call); + } + else + { + /* tp-call doesn't exist yet, we'll connect signals once it has been + * set */ + g_signal_connect (priv->handler, "notify::tp-call", + G_CALLBACK (call_handler_notify_tp_streamed_media_cb), window); + } + + gst_element_set_state (priv->pipeline, GST_STATE_PAUSED); +} + +static gboolean +empathy_streamed_media_window_delete_cb (GtkWidget *widget, GdkEvent*event, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + if (priv->pipeline != NULL) + { + if (priv->bus_message_source_id != 0) + { + g_source_remove (priv->bus_message_source_id); + priv->bus_message_source_id = 0; + } + + gst_element_set_state (priv->pipeline, GST_STATE_NULL); + } + + if (priv->call_state == CONNECTING) + empathy_sound_manager_stop (priv->sound_mgr, EMPATHY_SOUND_PHONE_OUTGOING); + + return FALSE; +} + +static void +show_controls (EmpathyStreamedMediaWindow *window, gboolean set_fullscreen) +{ + GtkWidget *menu; + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + menu = gtk_ui_manager_get_widget (priv->ui_manager, + "/menubar1"); + + if (set_fullscreen) + { + gtk_widget_hide (priv->sidebar); + gtk_widget_hide (menu); + gtk_widget_hide (priv->vbox); + gtk_widget_hide (priv->statusbar); + gtk_widget_hide (priv->toolbar); + } + else + { + if (priv->sidebar_was_visible_before_fs) + gtk_widget_show (priv->sidebar); + + gtk_widget_show (menu); + gtk_widget_show (priv->vbox); + gtk_widget_show (priv->statusbar); + gtk_widget_show (priv->toolbar); + + gtk_window_resize (GTK_WINDOW (window), priv->original_width_before_fs, + priv->original_height_before_fs); + } +} + +static void +show_borders (EmpathyStreamedMediaWindow *window, gboolean set_fullscreen) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + gtk_container_set_border_width (GTK_CONTAINER (priv->content_hbox), + set_fullscreen ? 0 : CONTENT_HBOX_BORDER_WIDTH); + gtk_box_set_spacing (GTK_BOX (priv->content_hbox), + set_fullscreen ? 0 : CONTENT_HBOX_SPACING); + + if (priv->video_output != NULL) + { + gtk_box_set_child_packing (GTK_BOX (priv->content_hbox), + priv->video_output, TRUE, TRUE, + set_fullscreen ? 0 : CONTENT_HBOX_CHILDREN_PACKING_PADDING, + GTK_PACK_START); + } + + gtk_box_set_child_packing (GTK_BOX (priv->content_hbox), + priv->vbox, TRUE, TRUE, + set_fullscreen ? 0 : CONTENT_HBOX_CHILDREN_PACKING_PADDING, + GTK_PACK_START); +} + +static gboolean +empathy_streamed_media_window_state_event_cb (GtkWidget *widget, + GdkEventWindowState *event, EmpathyStreamedMediaWindow *window) +{ + if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) + { + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + gboolean set_fullscreen = event->new_window_state & + GDK_WINDOW_STATE_FULLSCREEN; + + if (set_fullscreen) + { + gboolean sidebar_was_visible; + GtkAllocation allocation; + gint original_width, original_height; + + gtk_widget_get_allocation (GTK_WIDGET (window), &allocation); + original_width = allocation.width; + original_height = allocation.height; + + g_object_get (priv->sidebar, "visible", &sidebar_was_visible, NULL); + + priv->sidebar_was_visible_before_fs = sidebar_was_visible; + priv->original_width_before_fs = original_width; + priv->original_height_before_fs = original_height; + + if (priv->video_output_motion_handler_id == 0 && + priv->video_output != NULL) + { + priv->video_output_motion_handler_id = g_signal_connect ( + G_OBJECT (priv->video_output), "motion-notify-event", + G_CALLBACK (empathy_streamed_media_window_video_output_motion_notify), + window); + } + } + else + { + disconnect_video_output_motion_handler (window); + } + + empathy_streamed_media_window_fullscreen_set_fullscreen (priv->fullscreen, + set_fullscreen); + show_controls (window, set_fullscreen); + show_borders (window, set_fullscreen); + gtk_action_set_stock_id (priv->menu_fullscreen, + (set_fullscreen ? "gtk-leave-fullscreen" : "gtk-fullscreen")); + priv->is_fullscreen = set_fullscreen; + } + + return FALSE; +} + +static void +empathy_streamed_media_window_sidebar_toggled_cb (GtkToggleButton *toggle, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + GtkWidget *arrow; + int w, h, handle_size; + GtkAllocation allocation, sidebar_allocation; + + gtk_widget_get_allocation (GTK_WIDGET (window), &allocation); + w = allocation.width; + h = allocation.height; + + gtk_widget_style_get (priv->pane, "handle_size", &handle_size, NULL); + + gtk_widget_get_allocation (priv->sidebar, &sidebar_allocation); + if (gtk_toggle_button_get_active (toggle)) + { + arrow = gtk_arrow_new (GTK_ARROW_LEFT, GTK_SHADOW_NONE); + gtk_widget_show (priv->sidebar); + w += sidebar_allocation.width + handle_size; + } + else + { + arrow = gtk_arrow_new (GTK_ARROW_RIGHT, GTK_SHADOW_NONE); + w -= sidebar_allocation.width + handle_size; + gtk_widget_hide (priv->sidebar); + } + + gtk_button_set_image (GTK_BUTTON (priv->sidebar_button), arrow); + + if (w > 0 && h > 0) + gtk_window_resize (GTK_WINDOW (window), w, h); +} + +static void +empathy_streamed_media_window_set_send_video (EmpathyStreamedMediaWindow *window, + CameraState state) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + EmpathyTpStreamedMedia *call; + + priv->sending_video = (state == CAMERA_STATE_ON); + + if (state == CAMERA_STATE_PREVIEW || + state == CAMERA_STATE_ON) + { + /* When we start sending video, we want to show the video preview by + default. */ + display_video_preview (window, TRUE); + } + else + { + display_video_preview (window, FALSE); + } + + if (priv->call_state != CONNECTED) + return; + + g_object_get (priv->handler, "tp-call", &call, NULL); + DEBUG ("%s sending video", priv->sending_video ? "start": "stop"); + empathy_tp_streamed_media_request_video_stream_direction (call, priv->sending_video); + g_object_unref (call); +} + +static void +empathy_streamed_media_window_mic_toggled_cb (GtkToggleToolButton *toggle, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + gboolean active; + + active = (gtk_toggle_tool_button_get_active (toggle)); + + if (active) + { + empathy_audio_src_set_volume (EMPATHY_GST_AUDIO_SRC (priv->audio_input), + priv->volume); + gtk_adjustment_set_value (priv->audio_input_adj, priv->volume * 100); + } + else + { + /* TODO, Instead of setting the input volume to 0 we should probably + * stop sending but this would cause the audio call to drop if both + * sides mute at the same time on certain CMs AFAIK. Need to revisit this + * in the future. GNOME #574574 + */ + empathy_audio_src_set_volume (EMPATHY_GST_AUDIO_SRC (priv->audio_input), + 0); + gtk_adjustment_set_value (priv->audio_input_adj, 0); + } +} + +static void +empathy_streamed_media_window_sidebar_hidden_cb (EvSidebar *sidebar, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->sidebar_button), + FALSE); +} + +static void +empathy_streamed_media_window_sidebar_shown_cb (EvSidebar *sidebar, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->sidebar_button), + TRUE); +} + +static void +empathy_streamed_media_window_hangup_cb (gpointer object, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + empathy_streamed_media_handler_stop_call (priv->handler); + + if (empathy_streamed_media_window_disconnected (window, FALSE)) + gtk_widget_destroy (GTK_WIDGET (window)); +} + +static void +empathy_streamed_media_window_restart_call (EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + /* Remove error info bars */ + gtk_container_forall (GTK_CONTAINER (priv->errors_vbox), + (GtkCallback) gtk_widget_destroy, NULL); + + create_video_output_widget (window); + + g_signal_connect (G_OBJECT (priv->audio_input_adj), "value-changed", + G_CALLBACK (empathy_streamed_media_window_mic_volume_changed_cb), window); + + /* While the call was disconnected, the input volume might have changed. + * However, since the audio_input source was destroyed, its volume has not + * been updated during that time. That's why we manually update it here */ + empathy_streamed_media_window_mic_volume_changed_cb (priv->audio_input_adj, window); + + priv->outgoing = TRUE; + empathy_streamed_media_window_set_state_connecting (window); + + if (priv->pipeline_playing) + start_call (window); + else + /* call will be started when the pipeline is ready */ + priv->start_call_when_playing = TRUE; + + + empathy_streamed_media_window_setup_avatars (window, priv->handler); + + gtk_action_set_sensitive (priv->redial, FALSE); + gtk_widget_set_sensitive (priv->redial_button, FALSE); +} + +static void +empathy_streamed_media_window_redial_cb (gpointer object, + EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + if (priv->call_state == CONNECTED) + priv->call_state = REDIALING; + + empathy_streamed_media_handler_stop_call (priv->handler); + + if (priv->call_state != CONNECTED) + empathy_streamed_media_window_restart_call (window); +} + +static void +empathy_streamed_media_window_fullscreen_cb (gpointer object, + EmpathyStreamedMediaWindow *window) +{ + empathy_streamed_media_window_fullscreen_toggle (window); +} + +static void +empathy_streamed_media_window_fullscreen_toggle (EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + if (priv->is_fullscreen) + gtk_window_unfullscreen (GTK_WINDOW (window)); + else + gtk_window_fullscreen (GTK_WINDOW (window)); +} + +static gboolean +empathy_streamed_media_window_video_button_press_cb (GtkWidget *video_output, + GdkEventButton *event, EmpathyStreamedMediaWindow *window) +{ + if (event->button == 3 && event->type == GDK_BUTTON_PRESS) + { + empathy_streamed_media_window_video_menu_popup (window, event->button); + return TRUE; + } + + return FALSE; +} + +static gboolean +empathy_streamed_media_window_key_press_cb (GtkWidget *video_output, + GdkEventKey *event, EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + if (priv->is_fullscreen && event->keyval == GDK_KEY_Escape) + { + /* Since we are in fullscreen mode, toggling will bring us back to + normal mode. */ + empathy_streamed_media_window_fullscreen_toggle (window); + return TRUE; + } + + return FALSE; +} + +static gboolean +empathy_streamed_media_window_video_output_motion_notify (GtkWidget *widget, + GdkEventMotion *event, EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + if (priv->is_fullscreen) + { + empathy_streamed_media_window_fullscreen_show_popup (priv->fullscreen); + return TRUE; + } + return FALSE; +} + +static void +empathy_streamed_media_window_video_menu_popup (EmpathyStreamedMediaWindow *window, + guint button) +{ + GtkWidget *menu; + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + menu = gtk_ui_manager_get_widget (priv->ui_manager, + "/video-popup"); + gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, + button, gtk_get_current_event_time ()); + gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE); +} + +static void +empathy_streamed_media_window_status_message (EmpathyStreamedMediaWindow *window, + gchar *message) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + if (priv->context_id == 0) + { + priv->context_id = gtk_statusbar_get_context_id ( + GTK_STATUSBAR (priv->statusbar), "voip call status messages"); + } + else + { + gtk_statusbar_pop (GTK_STATUSBAR (priv->statusbar), priv->context_id); + } + + gtk_statusbar_push (GTK_STATUSBAR (priv->statusbar), priv->context_id, + message); +} + +static void +empathy_streamed_media_window_volume_changed_cb (GtkScaleButton *button, + gdouble value, EmpathyStreamedMediaWindow *window) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (window); + + if (priv->audio_output == NULL) + return; + + empathy_audio_sink_set_volume (EMPATHY_GST_AUDIO_SINK (priv->audio_output), + value); +} + +/* block all the signals related to camera control widgets. This is useful + * when we are manually updating the UI and so don't want to fire the + * callbacks */ +static void +block_camera_control_signals (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + g_signal_handlers_block_by_func (priv->tool_button_camera_off, + tool_button_camera_off_toggled_cb, self); + g_signal_handlers_block_by_func (priv->tool_button_camera_preview, + tool_button_camera_preview_toggled_cb, self); + g_signal_handlers_block_by_func (priv->tool_button_camera_on, + tool_button_camera_on_toggled_cb, self); + g_signal_handlers_block_by_func (priv->action_camera_on, + action_camera_change_cb, self); +} + +static void +unblock_camera_control_signals (EmpathyStreamedMediaWindow *self) +{ + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + g_signal_handlers_unblock_by_func (priv->tool_button_camera_off, + tool_button_camera_off_toggled_cb, self); + g_signal_handlers_unblock_by_func (priv->tool_button_camera_preview, + tool_button_camera_preview_toggled_cb, self); + g_signal_handlers_unblock_by_func (priv->tool_button_camera_on, + tool_button_camera_on_toggled_cb, self); + g_signal_handlers_unblock_by_func (priv->action_camera_on, + action_camera_change_cb, self); +} diff --git a/src/empathy-streamed-media-window.h b/src/empathy-streamed-media-window.h new file mode 100644 index 000000000..aae56d882 --- /dev/null +++ b/src/empathy-streamed-media-window.h @@ -0,0 +1,65 @@ +/* + * empathy-streamed-media-window.h - Header for EmpathyStreamedMediaWindow + * Copyright (C) 2008 Collabora Ltd. + * @author Sjoerd Simons <sjoerd.simons@collabora.co.uk> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __EMPATHY_STREAMED_MEDIA_WINDOW_H__ +#define __EMPATHY_STREAMED_MEDIA_WINDOW_H__ + +#include <glib-object.h> +#include <gtk/gtk.h> + +#include "empathy-streamed-media-handler.h" + +G_BEGIN_DECLS + +typedef struct _EmpathyStreamedMediaWindow EmpathyStreamedMediaWindow; +typedef struct _EmpathyStreamedMediaWindowClass EmpathyStreamedMediaWindowClass; + +struct _EmpathyStreamedMediaWindowClass { + GtkWindowClass parent_class; +}; + +struct _EmpathyStreamedMediaWindow { + GtkWindow parent; +}; + +GType empathy_streamed_media_window_get_type (void); + +/* TYPE MACROS */ +#define EMPATHY_TYPE_STREAMED_MEDIA_WINDOW \ + (empathy_streamed_media_window_get_type ()) +#define EMPATHY_STREAMED_MEDIA_WINDOW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW, \ + EmpathyStreamedMediaWindow)) +#define EMPATHY_STREAMED_MEDIA_WINDOW_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW, \ + EmpathyStreamedMediaWindowClass)) +#define EMPATHY_IS_STREAMED_MEDIA_WINDOW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW)) +#define EMPATHY_IS_STREAMED_MEDIA_WINDOW_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW)) +#define EMPATHY_STREAMED_MEDIA_WINDOW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), EMPATHY_TYPE_STREAMED_MEDIA_WINDOW, \ + EmpathyStreamedMediaWindowClass)) + +EmpathyStreamedMediaWindow *empathy_streamed_media_window_new (EmpathyStreamedMediaHandler *handler); + +G_END_DECLS + +#endif /* #ifndef __EMPATHY_STREAMED_MEDIA_WINDOW_H__*/ diff --git a/src/empathy-streamed-media-window.ui b/src/empathy-streamed-media-window.ui new file mode 100644 index 000000000..5f1a16e8c --- /dev/null +++ b/src/empathy-streamed-media-window.ui @@ -0,0 +1,861 @@ +<?xml version="1.0"?> +<!--Generated with glade3 3.4.5 on Mon Feb 16 12:31:56 2009 --> +<interface> + <object class="GtkUIManager" id="ui_manager"> + <child> + <object class="GtkActionGroup" id="actiongroup1"> + <child> + <object class="GtkAction" id="call"> + <property name="name">call</property> + <property name="label" translatable="yes">_Call</property> + </object> + </child> + <child> + <object class="GtkAction" id="menuhangup"> + <property name="icon_name">call-stop</property> + <property name="name">menuhangup</property> + <property name="label" translatable="yes">Hang up</property> + </object> + </child> + <child> + <object class="GtkAction" id="menuredial"> + <property name="stock_id">gtk-refresh</property> + <property name="name">menuredial</property> + <property name="label" translatable="yes">Redial</property> + <property name="sensitive">False</property> + </object> + </child> + <child> + <object class="GtkAction" id="camera"> + <property name="name">camera</property> + <property name="label" translatable="yes">V_ideo</property> + </object> + </child> + <child> + <object class="GtkRadioAction" id="action_camera_off"> + <property name="name">action_camera_off</property> + <property name="visible">True</property> + <property name="label" translatable="yes">Video Off</property> + <property name="draw_as_radio">True</property> + <property name="value">0</property> + <property name="current-value">0</property> + </object> + </child> + <child> + <object class="GtkRadioAction" id="action_camera_preview"> + <property name="name">action_camera_preview</property> + <property name="visible">True</property> + <property name="label" translatable="yes">Video Preview</property> + <property name="draw_as_radio">True</property> + <property name="group">action_camera_off</property> + <property name="value">1</property> + </object> + </child> + <child> + <object class="GtkRadioAction" id="action_camera_on"> + <property name="name">action_camera_on</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="label" translatable="yes">Video On</property> + <property name="draw_as_radio">True</property> + <property name="group">action_camera_off</property> + <property name="value">2</property> + </object> + </child> + <child> + <object class="GtkAction" id="view"> + <property name="name">view</property> + <property name="label" translatable="yes">_View</property> + </object> + </child> + <child> + <object class="GtkAction" id="menufullscreen"> + <property name="stock_id">gtk-fullscreen</property> + <property name="name">menufullscreen</property> + </object> + <accelerator key="F11"/> + </child> + </object> + </child> + <ui> + <menubar name="menubar1"> + <menu action="call"> + <menuitem action="menuhangup"/> + <menuitem action="menuredial"/> + </menu> + <menu action="camera"> + <menuitem action="action_camera_off"/> + <menuitem action="action_camera_preview"/> + <menuitem action="action_camera_on"/> + </menu> + <menu action="view"> + <menuitem action="menufullscreen"/> + </menu> + </menubar> + <popup name="video-popup"> + <menuitem name="menufullscreen" action="menufullscreen"/> + </popup> + </ui> + </object> + <object class="GtkVBox" id="call_window_vbox"> + <property name="visible">True</property> + <child> + <object class="GtkMenuBar" constructor="ui_manager" id="menubar1"> + <property name="visible">True</property> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolbar" id="toolbar"> + <property name="visible">True</property> + <style> + <class name="primary-toolbar"/> + </style> + <child> + <object class="GtkToolButton" id="hangup"> + <property name="visible">True</property> + <property name="is_important">True</property> + <property name="label" translatable="yes">Hang up</property> + <property name="icon_name">call-stop</property> + <property name="tooltip_text" translatable="yes">Hang up current call</property> + </object> + <packing> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="redial"> + <property name="visible">True</property> + <property name="is_important">True</property> + <property name="label" translatable="yes">Redial</property> + <property name="stock_id">gtk-refresh</property> + <property name="sensitive">False</property> + <property name="tooltip_text" translatable="yes">Call the contact again</property> + </object> + <packing> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="toolbutton1"> + <property name="visible">True</property> + </object> + <packing> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="microphone"> + <property name="visible">True</property> + <property name="active">True</property> + <property name="label" translatable="yes">Send Audio</property> + <property name="icon_name">gnome-stock-mic</property> + <property name="tooltip_text" translatable="yes">Toggle audio transmission</property> + </object> + <packing> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="camera_separator"> + <property name="visible">True</property> + </object> + <packing> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="camera_off"> + <property name="visible">True</property> + <property name="label" translatable="yes">Camera Off</property> + <property name="sensitive">True</property> + <property name="tooltip_text" translatable="yes">Disable camera and stop sending video</property> + </object> + <packing> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="camera_preview"> + <property name="visible">True</property> + <property name="label" translatable="yes">Preview</property> + <property name="icon_name">avatar-default</property> + <property name="sensitive">True</property> + <property name="tooltip_text" translatable="yes">Enable camera but don't send video</property> + </object> + <packing> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="camera_on"> + <property name="visible">True</property> + <property name="label" translatable="yes">Camera On</property> + <property name="icon_name">camera-web</property> + <property name="sensitive">False</property> + <property name="tooltip_text" translatable="yes">Enable camera and send video</property> + </object> + <packing> + <property name="homogeneous">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="errors_vbox"> + <property name="visible">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHPaned" id="pane"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkStatusbar" id="statusbar"> + <property name="visible">True</property> + <property name="spacing">2</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + + <object class="GtkVBox" id="details_vbox"> + <property name="border_width">6</property> + <property name="visible">False</property> + <property name="homogeneous">False</property> + <property name="spacing">18</property> + <property name="orientation">vertical</property> + + <child> + <object class="GtkVBox" id="video_vbox"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">6</property> + <property name="orientation">vertical</property> + + <child> + <object class="GtkLabel" id="video_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Video</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xscale">1</property> + <property name="yscale">1</property> + <property name="top_padding">0</property> + <property name="bottom_padding">0</property> + <property name="left_padding">12</property> + <property name="right_padding">0</property> + + <child> + <object class="GtkGrid" id="video"> + <property name="visible">True</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> + + <child> + <object class="GtkLabel" id="vcodec1_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Encoding Codec:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="style" value="PANGO_STYLE_ITALIC"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + + <child> + + <object class="GtkLabel" id="vcodec_encoding_label"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Unknown</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="vcodec2_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Decoding Codec:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="style" value="PANGO_STYLE_ITALIC"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + + <child> + + <object class="GtkLabel" id="vcodec_decoding_label"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Unknown</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="vrc_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Remote Candidate:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="style" value="PANGO_STYLE_ITALIC"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="video_remote_candidate_label"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Unknown</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + + <child> + <object class="GtkImage" id="video_remote_candidate_info_img"> + <property name="visible">True</property> + <property name="stock">gtk-info</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">3</property> + </packing> + </child> + + + <child> + <object class="GtkLabel" id="vlc_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Local Candidate:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="style" value="PANGO_STYLE_ITALIC"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="video_local_candidate_label"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Unknown</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + + <child> + <object class="GtkImage" id="video_local_candidate_info_img"> + <property name="visible">True</property> + <property name="stock">gtk-info</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">4</property> + </packing> + </child> + + </object> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + + <child> + <object class="GtkVBox" id="audio_vbox"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">6</property> + <property name="orientation">vertical</property> + + <child> + <object class="GtkLabel" id="bvwp_audio_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Audio</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xscale">1</property> + <property name="yscale">1</property> + <property name="top_padding">0</property> + <property name="bottom_padding">0</property> + <property name="left_padding">12</property> + <property name="right_padding">0</property> + + <child> + <object class="GtkGrid" id="audio"> + <property name="visible">True</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> + + <child> + <object class="GtkLabel" id="acodec1_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Encoding Codec:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="style" value="PANGO_STYLE_ITALIC"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="acodec_encoding_label"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Unknown</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="acodec2_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Decoding Codec:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="style" value="PANGO_STYLE_ITALIC"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="acodec_decoding_label"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Unknown</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="arc_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Remote Candidate:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="style" value="PANGO_STYLE_ITALIC"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="audio_remote_candidate_label"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Unknown</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + + <child> + <object class="GtkImage" id="audio_remote_candidate_info_img"> + <property name="visible">True</property> + <property name="stock">gtk-info</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="alc_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Local Candidate:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + <attributes> + <attribute name="style" value="PANGO_STYLE_ITALIC"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + + <child> + <object class="GtkLabel" id="audio_local_candidate_label"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Unknown</property> + <property name="use_underline">False</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">True</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + + <child> + <object class="GtkImage" id="audio_local_candidate_info_img"> + <property name="visible">True</property> + <property name="stock">gtk-info</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">3</property> + </packing> + </child> + + </object> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </object> + +</interface> diff --git a/src/ev-sidebar.c b/src/ev-sidebar.c new file mode 100644 index 000000000..a50736982 --- /dev/null +++ b/src/ev-sidebar.c @@ -0,0 +1,352 @@ +/* this file is part of evince, a gnome document viewer + * + * Copyright (C) 2004 Red Hat, Inc. + * (C) 2007 Jan Arne Petersen + * + * Authors: + * Jonathan Blandford <jrb@alum.mit.edu> + * Jan Arne Petersen <jpetersen@jpetersen.org> + * + * Evince is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Evince is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02110-1301 USA. + * + * Thursday 03 May 2007: Bastien Nocera: Add exception clause. + * See license_change file for details. + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <string.h> +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> + +#include "ev-sidebar.h" + +enum +{ + PAGE_COLUMN_ID, + PAGE_COLUMN_TITLE, + PAGE_COLUMN_NUM_COLS +}; + +struct _EvSidebarPrivate { + GtkWidget *combobox; + GtkWidget *notebook; +}; + +enum { + CLOSED, + CHANGED, + LAST_SIGNAL +}; + +static int ev_sidebar_table_signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (EvSidebar, ev_sidebar, GTK_TYPE_VBOX) + +#define EV_SIDEBAR_GET_PRIVATE(object) \ + (G_TYPE_INSTANCE_GET_PRIVATE ((object), EV_TYPE_SIDEBAR, EvSidebarPrivate)) + +static void +ev_sidebar_class_init (EvSidebarClass *ev_sidebar_class) +{ + GObjectClass *g_object_class; + + g_object_class = G_OBJECT_CLASS (ev_sidebar_class); + + g_type_class_add_private (g_object_class, sizeof (EvSidebarPrivate)); + + ev_sidebar_table_signals[CLOSED] = + g_signal_new ("closed", + G_TYPE_FROM_CLASS (g_object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EvSidebarClass, closed), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 0); + + ev_sidebar_table_signals[CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (g_object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EvSidebarClass, closed), + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, G_TYPE_STRING); + +} + +static void +ev_sidebar_close_clicked_cb (GtkWidget *widget, + gpointer user_data) +{ + EvSidebar *ev_sidebar = EV_SIDEBAR (user_data); + + g_signal_emit (G_OBJECT (ev_sidebar), + ev_sidebar_table_signals[CLOSED], 0, NULL); + gtk_widget_hide (GTK_WIDGET (ev_sidebar)); +} + +static void +ev_sidebar_combobox_changed_cb (GtkComboBox *combo_box, + gpointer user_data) +{ + EvSidebar *ev_sidebar = EV_SIDEBAR (user_data); + GtkTreeModel *model; + GtkTreeIter iter; + + model = gtk_combo_box_get_model (combo_box); + + if (gtk_combo_box_get_active_iter (combo_box, &iter)) { + GtkTreePath *path; + gint *indices; + + path = gtk_tree_model_get_path (model, &iter); + indices = gtk_tree_path_get_indices (path); + + if (indices != NULL) { + gchar *page; + + gtk_notebook_set_current_page (GTK_NOTEBOOK (ev_sidebar->priv->notebook), indices[0]); + gtk_tree_model_get (model, &iter, PAGE_COLUMN_ID, &page, -1); + g_signal_emit (G_OBJECT (ev_sidebar), ev_sidebar_table_signals[CHANGED], 0, page); + g_free (page); + } + + gtk_tree_path_free (path); + } + +} + +static void +ev_sidebar_init (EvSidebar *ev_sidebar) +{ + GtkTreeModel *page_model; + GtkWidget *vbox, *hbox; + GtkWidget *close_button; + GtkCellRenderer *renderer; + GtkWidget *image; + + ev_sidebar->priv = EV_SIDEBAR_GET_PRIVATE (ev_sidebar); + + /* data model */ + page_model = (GtkTreeModel *) + gtk_list_store_new (PAGE_COLUMN_NUM_COLS, + G_TYPE_STRING, + G_TYPE_STRING, + GTK_TYPE_WIDGET, + G_TYPE_INT); + + /* create a 6 6 6 0 border with GtkBoxes */ + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (ev_sidebar), hbox, TRUE, TRUE, 6); + + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_box_pack_start (GTK_BOX (hbox), vbox, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (hbox), gtk_box_new (GTK_ORIENTATION_VERTICAL, 0), FALSE, FALSE, 0); + + /* top option menu */ + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + gtk_widget_show (hbox); + + ev_sidebar->priv->combobox = gtk_combo_box_new_with_model (page_model); + g_signal_connect (ev_sidebar->priv->combobox, "changed", + G_CALLBACK (ev_sidebar_combobox_changed_cb), + ev_sidebar); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ev_sidebar->priv->combobox), renderer, TRUE); + gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (ev_sidebar->priv->combobox), renderer, "text", PAGE_COLUMN_TITLE); + + gtk_box_pack_start (GTK_BOX (hbox), ev_sidebar->priv->combobox, TRUE, TRUE, 0); + gtk_widget_show (ev_sidebar->priv->combobox); + + g_object_unref (G_OBJECT (page_model)); + + close_button = gtk_button_new (); + gtk_button_set_relief (GTK_BUTTON (close_button), GTK_RELIEF_NONE); + g_signal_connect (close_button, "clicked", + G_CALLBACK (ev_sidebar_close_clicked_cb), + ev_sidebar); + + image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, + GTK_ICON_SIZE_MENU); + gtk_container_add (GTK_CONTAINER (close_button), image); + gtk_widget_show (image); + + gtk_box_pack_end (GTK_BOX (hbox), close_button, FALSE, FALSE, 0); + gtk_widget_show (close_button); + + ev_sidebar->priv->notebook = gtk_notebook_new (); + gtk_notebook_set_show_border (GTK_NOTEBOOK (ev_sidebar->priv->notebook), FALSE); + gtk_notebook_set_show_tabs (GTK_NOTEBOOK (ev_sidebar->priv->notebook), FALSE); + gtk_box_pack_start (GTK_BOX (vbox), ev_sidebar->priv->notebook, + TRUE, TRUE, 0); + gtk_widget_show (ev_sidebar->priv->notebook); +} + +/* Public functions */ + +GtkWidget * +ev_sidebar_new (void) +{ + GtkWidget *ev_sidebar; + + ev_sidebar = g_object_new (EV_TYPE_SIDEBAR, NULL); + + return ev_sidebar; +} + +/* NOTE: Return values from this have to be g_free()d */ +char * +ev_sidebar_get_current_page (EvSidebar *ev_sidebar) +{ + GtkTreeModel *model; + GtkTreeIter iter; + char *id; + + g_return_val_if_fail (EV_IS_SIDEBAR (ev_sidebar), NULL); + g_return_val_if_fail (ev_sidebar->priv != NULL, NULL); + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (ev_sidebar->priv->combobox)); + + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ev_sidebar->priv->combobox), &iter)) { + gtk_tree_model_get (model, &iter, PAGE_COLUMN_ID, &id, -1); + + return id; + } + + return NULL; +} + +static gboolean +ev_sidebar_get_iter_for_page_id (EvSidebar *ev_sidebar, + const char *new_page_id, + GtkTreeIter *iter) +{ + GtkTreeModel *model; + gboolean valid; + gchar *page_id; + + g_return_val_if_fail (EV_IS_SIDEBAR (ev_sidebar), FALSE); + g_return_val_if_fail (ev_sidebar->priv != NULL, FALSE); + g_return_val_if_fail (iter != NULL, FALSE); + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (ev_sidebar->priv->combobox)); + + valid = gtk_tree_model_get_iter_first (model, iter); + + while (valid) { + gtk_tree_model_get (model, iter, PAGE_COLUMN_ID, &page_id, -1); + + if (page_id != NULL && strcmp (new_page_id, page_id) == 0) { + g_free (page_id); + return TRUE; + } + g_free (page_id); + + valid = gtk_tree_model_iter_next (model, iter); + } + + return FALSE; +} + +void +ev_sidebar_set_current_page (EvSidebar *ev_sidebar, const char *new_page_id) +{ + GtkTreeIter iter; + + g_return_if_fail (EV_IS_SIDEBAR (ev_sidebar)); + g_return_if_fail (new_page_id != NULL); + + + if (ev_sidebar_get_iter_for_page_id (ev_sidebar, new_page_id, &iter)) { + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ev_sidebar->priv->combobox), &iter); + } +} + +void +ev_sidebar_add_page (EvSidebar *ev_sidebar, + const gchar *page_id, + const gchar *title, + GtkWidget *main_widget) +{ + GtkTreeIter iter, iter2; + GtkTreeModel *model; + + g_return_if_fail (EV_IS_SIDEBAR (ev_sidebar)); + g_return_if_fail (page_id != NULL); + g_return_if_fail (title != NULL); + g_return_if_fail (GTK_IS_WIDGET (main_widget)); + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (ev_sidebar->priv->combobox)); + + gtk_widget_set_sensitive (GTK_WIDGET (ev_sidebar), TRUE); + + gtk_widget_show (main_widget); + gtk_notebook_append_page (GTK_NOTEBOOK (ev_sidebar->priv->notebook), main_widget, NULL); + + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + gtk_list_store_set (GTK_LIST_STORE (model), &iter, + PAGE_COLUMN_ID, page_id, + PAGE_COLUMN_TITLE, title, + -1); + + if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ev_sidebar->priv->combobox), &iter2)) { + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ev_sidebar->priv->combobox), &iter); + } +} + +void +ev_sidebar_remove_page (EvSidebar *ev_sidebar, + const gchar *page_id) +{ + GtkTreeIter iter; + GtkTreeModel *model; + + g_return_if_fail (EV_IS_SIDEBAR (ev_sidebar)); + g_return_if_fail (page_id != NULL); + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (ev_sidebar->priv->combobox)); + + if (ev_sidebar_get_iter_for_page_id (ev_sidebar, page_id, &iter)) { + GtkTreePath *path; + gint *indices; + + path = gtk_tree_model_get_path (model, &iter); + indices = gtk_tree_path_get_indices (path); + + g_assert (indices != NULL); + gtk_notebook_remove_page (GTK_NOTEBOOK (ev_sidebar->priv->notebook), indices[0]); + + gtk_list_store_remove (GTK_LIST_STORE (model), &iter); + + if (gtk_tree_model_iter_n_children (model, NULL) != 0) { + gtk_tree_path_prev (path); + + if (gtk_tree_model_get_iter (model, &iter, path)) { + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ev_sidebar->priv->combobox), &iter); + } + } else { + gtk_widget_set_sensitive (GTK_WIDGET (ev_sidebar), FALSE); + } + + gtk_tree_path_free (path); + } +} + diff --git a/src/ev-sidebar.h b/src/ev-sidebar.h new file mode 100644 index 000000000..cfb57f2eb --- /dev/null +++ b/src/ev-sidebar.h @@ -0,0 +1,76 @@ +/* ev-sidebar.h + * this file is part of evince, a gnome document viewer + * + * Copyright (C) 2004 Red Hat, Inc. + * + * Author: + * Jonathan Blandford <jrb@alum.mit.edu> + * + * Evince is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Evince is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02110-1301 USA. + * + * Thursday 03 May 2007: Bastien Nocera: Add exception clause. + * See license_change file for details. + * + */ + +#ifndef __EV_SIDEBAR_H__ +#define __EV_SIDEBAR_H__ + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +typedef struct _EvSidebar EvSidebar; +typedef struct _EvSidebarClass EvSidebarClass; +typedef struct _EvSidebarPrivate EvSidebarPrivate; + +#define EV_TYPE_SIDEBAR (ev_sidebar_get_type()) +#define EV_SIDEBAR(object) (G_TYPE_CHECK_INSTANCE_CAST((object), EV_TYPE_SIDEBAR, EvSidebar)) +#define EV_SIDEBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), EV_TYPE_SIDEBAR, EvSidebarClass)) +#define EV_IS_SIDEBAR(object) (G_TYPE_CHECK_INSTANCE_TYPE((object), EV_TYPE_SIDEBAR)) +#define EV_IS_SIDEBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), EV_TYPE_SIDEBAR)) +#define EV_SIDEBAR_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), EV_TYPE_SIDEBAR, EvSidebarClass)) + +struct _EvSidebar { + GtkVBox base_instance; + + EvSidebarPrivate *priv; +}; + +struct _EvSidebarClass { + GtkVBoxClass base_class; + + void (*closed) (EvSidebar *sidebar); +}; + +GType ev_sidebar_get_type (void); +GtkWidget *ev_sidebar_new (void); +void ev_sidebar_add_page (EvSidebar *ev_sidebar, + const gchar *page_id, + const gchar *title, + GtkWidget *main_widget); +void ev_sidebar_set_current_page + (EvSidebar *ev_sidebar, + const char *page_id); +char *ev_sidebar_get_current_page + (EvSidebar *ev_sidebar); +void ev_sidebar_remove_page (EvSidebar *ev_sidebar, + const gchar *page_id); + +G_END_DECLS + +#endif /* __EV_SIDEBAR_H__ */ + + |