diff options
author | Guillaume Desmottes <guillaume.desmottes@collabora.co.uk> | 2012-02-28 19:07:03 +0800 |
---|---|---|
committer | Guillaume Desmottes <guillaume.desmottes@collabora.co.uk> | 2012-03-01 23:49:06 +0800 |
commit | 4110a4bc8b8c2a63e94615594eba3b109dcf97e4 (patch) | |
tree | d06236f38b94639591d0b89f25c19e543a52ccca /src | |
parent | afb9276b8a497e7b9275fb53abafd0cdb18d55fc (diff) | |
download | gsoc2013-empathy-4110a4bc8b8c2a63e94615594eba3b109dcf97e4.tar gsoc2013-empathy-4110a4bc8b8c2a63e94615594eba3b109dcf97e4.tar.gz gsoc2013-empathy-4110a4bc8b8c2a63e94615594eba3b109dcf97e4.tar.bz2 gsoc2013-empathy-4110a4bc8b8c2a63e94615594eba3b109dcf97e4.tar.lz gsoc2013-empathy-4110a4bc8b8c2a63e94615594eba3b109dcf97e4.tar.xz gsoc2013-empathy-4110a4bc8b8c2a63e94615594eba3b109dcf97e4.tar.zst gsoc2013-empathy-4110a4bc8b8c2a63e94615594eba3b109dcf97e4.zip |
Revert "Drop empathy-av"
This reverts commit ba0a893b1d31cdb234bae72845e840a895b9d62a.
Conflicts:
src/Makefile.am
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 29 | ||||
-rw-r--r-- | src/empathy-av.c | 172 | ||||
-rw-r--r-- | src/empathy-streamed-media-factory.c | 269 | ||||
-rw-r--r-- | src/empathy-streamed-media-factory.h | 69 | ||||
-rw-r--r-- | src/empathy-streamed-media-handler.c | 903 | ||||
-rw-r--r-- | src/empathy-streamed-media-handler.h | 106 | ||||
-rw-r--r-- | src/empathy-streamed-media-window-fullscreen.c | 309 | ||||
-rw-r--r-- | src/empathy-streamed-media-window-fullscreen.h | 77 | ||||
-rw-r--r-- | src/empathy-streamed-media-window.c | 3202 | ||||
-rw-r--r-- | src/empathy-streamed-media-window.h | 65 | ||||
-rw-r--r-- | src/empathy-streamed-media-window.ui | 861 |
11 files changed, 6062 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 1857eb578..ffc74e7fd 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -52,6 +52,7 @@ bin_PROGRAMS = \ libexec_PROGRAMS = \ empathy-auth-client \ + empathy-av \ empathy-chat empathy_accounts_SOURCES = \ @@ -68,6 +69,33 @@ empathy_debugger_SOURCES = \ empathy-debugger.c \ $(NULL) +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) + empathy_auth_client_SOURCES = \ empathy-auth-client.c \ $(NULL) @@ -171,6 +199,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-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..a3c539773 --- /dev/null +++ b/src/empathy-streamed-media-handler.c @@ -0,0 +1,903 @@ +/* + * 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-farsight/channel.h> +#include <telepathy-farsight/stream.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, + REQUEST_RESOURCE, + 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[REQUEST_RESOURCE] = + g_signal_new ("request-resource", 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_STREAM); + + 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, "farsight-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, "farsight-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, "farsight-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_session_created_cb (TfChannel *tfchannel, + FsConference *conference, FsParticipant *participant, + EmpathyStreamedMediaHandler *self) +{ + g_signal_emit (G_OBJECT (self), signals[CONFERENCE_ADDED], 0, + GST_ELEMENT (conference)); +} + +static gboolean +src_pad_added_error_idle (gpointer data) +{ + TfStream *stream = data; + + tf_stream_error (stream, TP_MEDIA_STREAM_ERROR_MEDIA_ERROR, + "Could not link sink"); + g_object_unref (stream); + + return FALSE; +} + +static void +empathy_streamed_media_handler_tf_stream_src_pad_added_cb (TfStream *stream, + GstPad *pad, FsCodec *codec, EmpathyStreamedMediaHandler *handler) +{ + guint media_type; + gboolean retval; + + g_object_get (stream, "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 (stream)); +} + + +static gboolean +empathy_streamed_media_handler_tf_stream_request_resource_cb (TfStream *stream, + guint direction, EmpathyTpStreamedMedia *call) +{ + gboolean ret; + guint media_type; + + g_object_get (G_OBJECT (stream), "media-type", &media_type, NULL); + + g_signal_emit (G_OBJECT (call), + signals[REQUEST_RESOURCE], 0, media_type, direction, &ret); + + return ret; +} + +static void +empathy_streamed_media_handler_tf_stream_closed_cb (TfStream *stream, + EmpathyStreamedMediaHandler *handler) +{ + g_signal_emit (handler, signals[STREAM_CLOSED], 0, stream); +} + +static void +empathy_streamed_media_handler_tf_channel_stream_created_cb (TfChannel *tfchannel, + TfStream *stream, EmpathyStreamedMediaHandler *handler) +{ + guint media_type; + GstPad *spad; + gboolean retval; + FsStream *fs_stream; + GList *codecs; + FsSession *session; + FsCodec *codec; + + g_signal_connect (stream, "src-pad-added", + G_CALLBACK (empathy_streamed_media_handler_tf_stream_src_pad_added_cb), handler); + g_signal_connect (stream, "request-resource", + G_CALLBACK (empathy_streamed_media_handler_tf_stream_request_resource_cb), + handler); + g_signal_connect (stream, "closed", + G_CALLBACK (empathy_streamed_media_handler_tf_stream_closed_cb), handler); + + g_object_get (stream, "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_stream_error (stream, TP_MEDIA_STREAM_ERROR_MEDIA_ERROR, + "Could not link source"); + + /* Get sending codec */ + g_object_get (stream, "farsight-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); + + /* Get receiving codec */ + g_object_get (stream, "farsight-stream", &fs_stream, NULL); + g_object_get (fs_stream, "current-recv-codecs", &codecs, NULL); + + update_receiving_codec (handler, codecs, fs_stream); + + fs_codec_list_destroy (codecs); + tp_clear_object (&fs_stream); + + 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 (EmpathyStreamedMediaHandler *self) +{ + EmpathyStreamedMediaHandlerPriv *priv = GET_PRIV (self); + TpChannel *channel; + + g_object_get (priv->call, "channel", &channel, NULL); + + g_assert (channel != NULL); + + priv->tfchannel = tf_channel_new (channel); + + /* Set up the telepathy farsight channel */ + g_signal_connect (priv->tfchannel, "session-created", + G_CALLBACK (empathy_streamed_media_handler_tf_channel_session_created_cb), self); + g_signal_connect (priv->tfchannel, "stream-created", + G_CALLBACK (empathy_streamed_media_handler_tf_channel_stream_created_cb), self); + g_signal_connect (priv->tfchannel, "closed", + G_CALLBACK (empathy_streamed_media_handler_tf_channel_closed_cb), self); + + g_object_unref (channel); +} + +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"); + + empathy_streamed_media_handler_start_tpfs (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) + { + empathy_streamed_media_handler_start_tpfs (handler); + empathy_tp_streamed_media_accept_incoming_call (priv->call); + 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..ab655b8e4 --- /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 <gst/farsight/fs-conference-iface.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..df3fbab00 --- /dev/null +++ b/src/empathy-streamed-media-window.c @@ -0,0 +1,3202 @@ +/* + * 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-farsight/channel.h> +#include <telepathy-glib/util.h> + +#include <gst/farsight/fs-element-added-notifier.h> +#include <gst/farsight/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_request_resource_cb (EmpathyStreamedMediaHandler *handler, + FsMediaType type, FsStreamDirection direction, gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + + if (type != FS_MEDIA_TYPE_VIDEO) + return TRUE; + + if (direction == FS_DIRECTION_RECV) + 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, + TfStream *stream, gpointer user_data) +{ + EmpathyStreamedMediaWindow *self = EMPATHY_STREAMED_MEDIA_WINDOW (user_data); + EmpathyStreamedMediaWindowPriv *priv = GET_PRIV (self); + guint media_type; + + g_object_get (stream, "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, "request-resource", + G_CALLBACK (empathy_streamed_media_window_request_resource_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> |