]> git.0d.be Git - empathy.git/blob - src/empathy-call-window.c
Merge branch 'move-video-preview-656268'
[empathy.git] / src / empathy-call-window.c
1 /*
2  * empathy-call-window.c - Source for EmpathyCallWindow
3  * Copyright (C) 2008-2011 Collabora Ltd.
4  * @author Sjoerd Simons <sjoerd.simons@collabora.co.uk>
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19  */
20
21
22 #include <stdio.h>
23 #include <stdlib.h>
24
25 #include <math.h>
26
27 #include <gdk/gdkkeysyms.h>
28 #include <gst/gst.h>
29 #include <gtk/gtk.h>
30 #include <glib/gi18n.h>
31
32 #include <clutter/clutter.h>
33 #include <clutter-gtk/clutter-gtk.h>
34 #include <clutter-gst/clutter-gst.h>
35
36 #include <telepathy-glib/util.h>
37 #include <telepathy-farstream/telepathy-farstream.h>
38 #include <telepathy-glib/util.h>
39
40 #include <gst/farsight/fs-element-added-notifier.h>
41 #include <gst/farsight/fs-utils.h>
42
43 #include <libempathy/empathy-camera-monitor.h>
44 #include <libempathy/empathy-gsettings.h>
45 #include <libempathy/empathy-tp-contact-factory.h>
46 #include <libempathy/empathy-utils.h>
47
48 #include <libempathy-gtk/empathy-avatar-image.h>
49 #include <libempathy-gtk/empathy-ui-utils.h>
50 #include <libempathy-gtk/empathy-sound-manager.h>
51 #include <libempathy-gtk/empathy-geometry.h>
52 #include <libempathy-gtk/empathy-images.h>
53
54 #define DEBUG_FLAG EMPATHY_DEBUG_VOIP
55 #include <libempathy/empathy-debug.h>
56
57 #include "empathy-call-window.h"
58 #include "empathy-call-window-fullscreen.h"
59 #include "empathy-call-factory.h"
60 #include "empathy-video-widget.h"
61 #include "empathy-about-dialog.h"
62 #include "empathy-audio-src.h"
63 #include "empathy-audio-sink.h"
64 #include "empathy-video-src.h"
65 #include "empathy-mic-menu.h"
66 #include "empathy-preferences.h"
67 #include "empathy-rounded-actor.h"
68 #include "empathy-camera-menu.h"
69
70 #define CONTENT_HBOX_BORDER_WIDTH 6
71 #define CONTENT_HBOX_SPACING 3
72 #define CONTENT_HBOX_CHILDREN_PACKING_PADDING 3
73
74 #define SELF_VIDEO_SECTION_WIDTH 120
75 #define SELF_VIDEO_SECTION_HEIGTH 90
76 #define SELF_VIDEO_SECTION_MARGIN 10
77
78 #define FLOATING_TOOLBAR_OPACITY 192
79 #define FLOATING_TOOLBAR_WIDTH 280
80 #define FLOATING_TOOLBAR_HEIGHT 36
81 #define FLOATING_TOOLBAR_SPACING 20
82
83 /* The avatar's default width and height are set to the same value because we
84    want a square icon. */
85 #define REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH EMPATHY_VIDEO_WIDGET_DEFAULT_HEIGHT
86 #define REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT \
87   EMPATHY_VIDEO_WIDGET_DEFAULT_HEIGHT
88
89 #define SMALL_TOOLBAR_SIZE 36
90
91 /* If an video input error occurs, the error message will start with "v4l" */
92 #define VIDEO_INPUT_ERROR_PREFIX "v4l"
93
94 /* The time interval in milliseconds between 2 outgoing rings */
95 #define MS_BETWEEN_RING 500
96
97 G_DEFINE_TYPE(EmpathyCallWindow, empathy_call_window, GTK_TYPE_WINDOW)
98
99 enum {
100   PROP_CALL_HANDLER = 1,
101 };
102
103 typedef enum {
104   CONNECTING,
105   CONNECTED,
106   HELD,
107   DISCONNECTED,
108   REDIALING
109 } CallState;
110
111 typedef enum {
112   CAMERA_STATE_OFF = 0,
113   CAMERA_STATE_ON,
114 } CameraState;
115
116 typedef enum {
117   PREVIEW_POS_NONE,
118   PREVIEW_POS_TOP_LEFT,
119   PREVIEW_POS_TOP_RIGHT,
120   PREVIEW_POS_BOTTOM_LEFT,
121   PREVIEW_POS_BOTTOM_RIGHT,
122 } PreviewPosition;
123
124 struct _EmpathyCallWindowPriv
125 {
126   gboolean dispose_has_run;
127   EmpathyCallHandler *handler;
128
129   EmpathyContact *contact;
130
131   EmpathyCameraMonitor *camera_monitor;
132
133   guint call_state;
134   gboolean outgoing;
135
136   GtkUIManager *ui_manager;
137   GtkWidget *errors_vbox;
138   /* widget displays the video received from the remote user. This widget is
139    * alive only during call. */
140   ClutterActor *video_output;
141   ClutterActor *video_preview;
142   ClutterActor *preview_hidden_button;
143   ClutterActor *preview_rectangle1;
144   ClutterActor *preview_rectangle2;
145   ClutterActor *preview_rectangle3;
146   ClutterActor *preview_rectangle4;
147   GtkWidget *video_container;
148   GtkWidget *remote_user_avatar_widget;
149   GtkWidget *remote_user_avatar_toolbar;
150   GtkWidget *remote_user_name_toolbar;
151   GtkWidget *status_label;
152   GtkWidget *hangup_button;
153   GtkWidget *audio_call_button;
154   GtkWidget *video_call_button;
155   GtkWidget *mic_button;
156   GtkWidget *camera_button;
157   GtkWidget *dialpad_button;
158   GtkWidget *toolbar;
159   GtkWidget *bottom_toolbar;
160   ClutterActor *floating_toolbar;
161   GtkWidget *pane;
162   GtkAction *menu_fullscreen;
163   GtkAction *menu_swap_camera;
164
165   ClutterState *transitions;
166
167   /* The box that contains self and remote avatar and video
168      input/output. When we redial, we destroy and re-create the box */
169   ClutterActor *video_box;
170   ClutterLayoutManager *video_layout;
171
172   /* Coordinates of the preview drag event's start. */
173   PreviewPosition preview_pos;
174   gfloat event_x;
175   gfloat event_y;
176
177   /* We keep a reference on the hbox which contains the main content so we can
178      easilly repack everything when toggling fullscreen */
179   GtkWidget *content_hbox;
180
181   gulong video_output_motion_handler_id;
182   guint bus_message_source_id;
183
184   gdouble volume;
185
186   GtkWidget *dtmf_panel;
187
188   /* Details vbox */
189   GtkWidget *details_vbox;
190   GtkWidget *vcodec_encoding_label;
191   GtkWidget *acodec_encoding_label;
192   GtkWidget *vcodec_decoding_label;
193   GtkWidget *acodec_decoding_label;
194
195   GtkWidget *audio_remote_candidate_label;
196   GtkWidget *audio_local_candidate_label;
197   GtkWidget *video_remote_candidate_label;
198   GtkWidget *video_local_candidate_label;
199   GtkWidget *video_remote_candidate_info_img;
200   GtkWidget *video_local_candidate_info_img;
201   GtkWidget *audio_remote_candidate_info_img;
202   GtkWidget *audio_local_candidate_info_img;
203
204   GstElement *video_input;
205   GstElement *video_preview_sink;
206   GstElement *video_output_sink;
207   GstElement *audio_input;
208   GstElement *audio_output;
209   GstElement *pipeline;
210   GstElement *video_tee;
211
212   GstElement *funnel;
213
214   GList *notifiers;
215
216   GTimer *timer;
217   guint timer_id;
218
219   GMutex *lock;
220   gboolean call_started;
221   gboolean sending_video;
222   CameraState camera_state;
223
224   EmpathyCallWindowFullscreen *fullscreen;
225   gboolean is_fullscreen;
226
227   gboolean got_video;
228   guint got_video_src;
229
230   guint inactivity_src;
231
232   /* Those fields represent the state of the window before it actually was in
233      fullscreen mode. */
234   gboolean dialpad_was_visible_before_fs;
235   gint original_width_before_fs;
236   gint original_height_before_fs;
237
238   gint x, y, w, h, dialpad_width;
239   gboolean maximized;
240
241   /* TRUE if the call should be started when the pipeline is playing */
242   gboolean start_call_when_playing;
243   /* TRUE if we requested to set the pipeline in the playing state */
244   gboolean pipeline_playing;
245
246   EmpathySoundManager *sound_mgr;
247
248   GSettings *settings;
249   EmpathyMicMenu *mic_menu;
250   EmpathyCameraMenu *camera_menu;
251 };
252
253 #define GET_PRIV(o) (EMPATHY_CALL_WINDOW (o)->priv)
254
255 static void empathy_call_window_realized_cb (GtkWidget *widget,
256   EmpathyCallWindow *window);
257
258 static gboolean empathy_call_window_delete_cb (GtkWidget *widget,
259   GdkEvent *event, EmpathyCallWindow *window);
260
261 static gboolean empathy_call_window_state_event_cb (GtkWidget *widget,
262   GdkEventWindowState *event, EmpathyCallWindow *window);
263
264 static void empathy_call_window_set_send_video (EmpathyCallWindow *window,
265   CameraState state);
266
267 static void empathy_call_window_mic_toggled_cb (
268   GtkToggleToolButton *toggle, EmpathyCallWindow *window);
269
270 static void empathy_call_window_hangup_cb (gpointer object,
271   EmpathyCallWindow *window);
272
273 static void empathy_call_window_fullscreen_cb (gpointer object,
274   EmpathyCallWindow *window);
275
276 static void empathy_call_window_fullscreen_toggle (EmpathyCallWindow *window);
277
278 static gboolean empathy_call_window_video_button_press_cb (
279   GtkWidget *video_output, GdkEventButton *event, EmpathyCallWindow *window);
280
281 static gboolean empathy_call_window_key_press_cb (GtkWidget *video_output,
282   GdkEventKey *event, EmpathyCallWindow *window);
283
284 static gboolean empathy_call_window_video_output_motion_notify (
285   GtkWidget *widget, GdkEventMotion *event, EmpathyCallWindow *window);
286
287 static void empathy_call_window_video_menu_popup (EmpathyCallWindow *window,
288   guint button);
289
290 static void empathy_call_window_dialpad_cb (GtkToggleToolButton *button,
291   EmpathyCallWindow *window);
292
293 static void empathy_call_window_restart_call (EmpathyCallWindow *window);
294
295 static void empathy_call_window_status_message (EmpathyCallWindow *window,
296   gchar *message);
297
298 static gboolean empathy_call_window_bus_message (GstBus *bus,
299   GstMessage *message, gpointer user_data);
300
301 static void
302 empathy_call_window_volume_changed_cb (GtkScaleButton *button,
303   gdouble value, EmpathyCallWindow *window);
304
305 static void
306 empathy_call_window_show_hangup_button (EmpathyCallWindow *self,
307     gboolean show)
308 {
309   gtk_widget_set_visible (self->priv->hangup_button, show);
310   gtk_widget_set_visible (self->priv->audio_call_button, !show);
311   gtk_widget_set_visible (self->priv->video_call_button, !show);
312 }
313
314 static void
315 empathy_call_window_audio_call_cb (GtkToggleToolButton *button,
316     EmpathyCallWindow *self)
317 {
318   g_object_set (self->priv->handler, "initial-video", FALSE, NULL);
319   empathy_call_window_restart_call (self);
320 }
321
322 static void
323 empathy_call_window_video_call_cb (GtkToggleToolButton *button,
324     EmpathyCallWindow *self)
325 {
326   empathy_call_window_set_send_video (self, CAMERA_STATE_ON);
327   g_object_set (self->priv->handler, "initial-video", TRUE, NULL);
328   empathy_call_window_restart_call (self);
329 }
330
331 static void
332 dtmf_button_pressed_cb (GtkButton *button, EmpathyCallWindow *window)
333 {
334   EmpathyCallWindowPriv *priv = GET_PRIV (window);
335   TpyCallChannel *call;
336   GQuark button_quark;
337   TpDTMFEvent event;
338
339   g_object_get (priv->handler, "call-channel", &call, NULL);
340
341   button_quark = g_quark_from_static_string (EMPATHY_DTMF_BUTTON_ID);
342   event = GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (button),
343     button_quark));
344
345   tpy_call_channel_dtmf_start_tone (call, event);
346
347   g_object_unref (call);
348 }
349
350 static void
351 dtmf_button_released_cb (GtkButton *button, EmpathyCallWindow *window)
352 {
353   EmpathyCallWindowPriv *priv = GET_PRIV (window);
354   TpyCallChannel *call;
355
356   g_object_get (priv->handler, "call-channel", &call, NULL);
357
358   tpy_call_channel_dtmf_stop_tone (call);
359
360   g_object_unref (call);
361 }
362
363 static void
364 empathy_call_window_mic_volume_changed (EmpathyCallWindow *self)
365 {
366   EmpathyCallWindowPriv *priv = GET_PRIV (self);
367   gdouble volume;
368
369   volume = g_settings_get_double (priv->settings,
370       EMPATHY_PREFS_CALL_SOUND_VOLUME) / 100.0;
371
372   /* Don't store the volume because of muting */
373   if (volume > 0 || gtk_toggle_tool_button_get_active (
374         GTK_TOGGLE_TOOL_BUTTON (priv->mic_button)))
375     priv->volume = volume;
376
377   /* Ensure that the toggle button is active if the volume is > 0 and inactive
378    * if it's smaller than 0 */
379   if ((volume > 0) != gtk_toggle_tool_button_get_active (
380         GTK_TOGGLE_TOOL_BUTTON (priv->mic_button)))
381     gtk_toggle_tool_button_set_active (
382       GTK_TOGGLE_TOOL_BUTTON (priv->mic_button), volume > 0);
383
384   empathy_audio_src_set_volume (EMPATHY_GST_AUDIO_SRC (priv->audio_input),
385     volume);
386 }
387
388 static void
389 empathy_call_window_prefs_volume_changed_cb (GSettings *settings,
390     gchar *key,
391     EmpathyCallWindow *self)
392 {
393   empathy_call_window_mic_volume_changed (self);
394 }
395
396 static void
397 empathy_call_window_show_video_output (EmpathyCallWindow *self,
398     gboolean show)
399 {
400   if (self->priv->video_output != NULL)
401     g_object_set (self->priv->video_output, "visible", show, NULL);
402
403   gtk_widget_set_visible (self->priv->remote_user_avatar_widget, !show);
404
405   clutter_actor_raise_top (self->priv->floating_toolbar);
406 }
407
408 static void
409 create_video_output_widget (EmpathyCallWindow *self)
410 {
411   EmpathyCallWindowPriv *priv = GET_PRIV (self);
412
413   g_assert (priv->video_output == NULL);
414   g_assert (priv->pipeline != NULL);
415
416   priv->video_output = clutter_texture_new ();
417
418   clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (priv->video_output),
419       TRUE);
420
421   priv->video_output_sink = clutter_gst_video_sink_new (
422       CLUTTER_TEXTURE (priv->video_output));
423
424   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_box),
425       priv->video_output);
426
427   gtk_widget_add_events (priv->video_container,
428       GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK);
429   g_signal_connect (G_OBJECT (priv->video_container), "button-press-event",
430       G_CALLBACK (empathy_call_window_video_button_press_cb), self);
431 }
432
433 static void
434 create_video_input (EmpathyCallWindow *self)
435 {
436   EmpathyCallWindowPriv *priv = GET_PRIV (self);
437
438   g_assert (priv->video_input == NULL);
439   priv->video_input = empathy_video_src_new ();
440   gst_object_ref (priv->video_input);
441   gst_object_sink (priv->video_input);
442 }
443
444 static void
445 create_audio_input (EmpathyCallWindow *self)
446 {
447   EmpathyCallWindowPriv *priv = GET_PRIV (self);
448
449   g_assert (priv->audio_input == NULL);
450   priv->audio_input = empathy_audio_src_new ();
451   gst_object_ref (priv->audio_input);
452   gst_object_sink (priv->audio_input);
453 }
454
455 static void
456 add_video_preview_to_pipeline (EmpathyCallWindow *self)
457 {
458   EmpathyCallWindowPriv *priv = GET_PRIV (self);
459   GstElement *preview;
460
461   g_assert (priv->video_preview != NULL);
462   g_assert (priv->pipeline != NULL);
463   g_assert (priv->video_input != NULL);
464   g_assert (priv->video_tee != NULL);
465
466   preview = priv->video_preview_sink;
467
468   if (!gst_bin_add (GST_BIN (priv->pipeline), priv->video_input))
469     {
470       g_warning ("Could not add video input to pipeline");
471       return;
472     }
473
474   if (!gst_bin_add (GST_BIN (priv->pipeline), preview))
475     {
476       g_warning ("Could not add video preview to pipeline");
477       return;
478     }
479
480   if (!gst_element_link (priv->video_input, priv->video_tee))
481     {
482       g_warning ("Could not link video input to video tee");
483       return;
484     }
485
486   if (!gst_element_link (priv->video_tee, preview))
487     {
488       g_warning ("Could not link video tee to video preview");
489       return;
490     }
491 }
492
493 static void
494 empathy_call_window_disable_camera_cb (GtkAction *action,
495     EmpathyCallWindow *self)
496 {
497   clutter_actor_destroy (self->priv->preview_hidden_button);
498
499   gtk_toggle_tool_button_set_active (
500       GTK_TOGGLE_TOOL_BUTTON (self->priv->camera_button), FALSE);
501 }
502
503 static void
504 empathy_call_window_minimise_camera_cb (GtkAction *action,
505     EmpathyCallWindow *self)
506 {
507   clutter_actor_hide (self->priv->video_preview);
508   clutter_actor_show (self->priv->preview_hidden_button);
509 }
510
511 static void
512 empathy_call_window_maximise_camera_cb (GtkAction *action,
513     EmpathyCallWindow *self)
514 {
515   clutter_actor_show (self->priv->video_preview);
516   clutter_actor_hide (self->priv->preview_hidden_button);
517 }
518
519 static void
520 empathy_call_window_swap_camera_cb (GtkAction *action,
521     EmpathyCallWindow *self)
522 {
523   const GList *cameras, *l;
524   gchar *current_cam;
525
526   DEBUG ("Swapping the camera");
527
528   cameras = empathy_camera_monitor_get_cameras (self->priv->camera_monitor);
529   current_cam = empathy_video_src_dup_device (
530       EMPATHY_GST_VIDEO_SRC (self->priv->video_input));
531
532   for (l = cameras; l != NULL; l = l->next)
533     {
534       EmpathyCamera *camera = l->data;
535
536       if (!tp_strdiff (camera->device, current_cam))
537         {
538           EmpathyCamera *next;
539
540           if (l->next != NULL)
541             next = l->next->data;
542           else
543             next = cameras->data;
544
545           /* EmpathyCameraMenu will update itself and do the actual change
546            * for us */
547           g_settings_set_string (self->priv->settings,
548               EMPATHY_PREFS_CALL_CAMERA_DEVICE,
549               next->device);
550
551           break;
552         }
553     }
554
555   g_free (current_cam);
556 }
557
558 static void
559 empathy_call_window_camera_added_cb (EmpathyCameraMonitor *monitor,
560     EmpathyCamera *camera,
561     EmpathyCallWindow *self)
562 {
563   const GList *cameras = empathy_camera_monitor_get_cameras (monitor);
564
565   gtk_action_set_visible (self->priv->menu_swap_camera,
566       g_list_length ((GList *) cameras) >= 2);
567 }
568
569 static void
570 empathy_call_window_camera_removed_cb (EmpathyCameraMonitor *monitor,
571     EmpathyCamera *camera,
572     EmpathyCallWindow *self)
573 {
574   const GList *cameras = empathy_camera_monitor_get_cameras (monitor);
575
576   gtk_action_set_visible (self->priv->menu_swap_camera,
577       g_list_length ((GList *) cameras) >= 2);
578 }
579
580 static void
581 empathy_call_window_preview_button_clicked_cb (GtkButton *button,
582     EmpathyCallWindow *self)
583 {
584   GtkWidget *menu;
585
586   menu = gtk_ui_manager_get_widget (self->priv->ui_manager,
587       "/preview-menu");
588   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
589       0, gtk_get_current_event_time ());
590   gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE);
591 }
592
593 static void
594 empathy_call_window_preview_hidden_button_clicked_cb (GtkButton *button,
595     EmpathyCallWindow *self)
596 {
597   GtkWidget *menu;
598
599   menu = gtk_ui_manager_get_widget (self->priv->ui_manager,
600       "/preview-hidden-menu");
601   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
602       0, gtk_get_current_event_time ());
603   gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE);
604 }
605
606 static ClutterActor *
607 empathy_call_window_create_preview_rectangle (EmpathyCallWindow *self,
608     ClutterBinAlignment x,
609     ClutterBinAlignment y)
610 {
611   ClutterLayoutManager *layout1, *layout2;
612   ClutterActor *rectangle;
613   ClutterActor *box1, *box2;
614
615   layout1 = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
616       CLUTTER_BIN_ALIGNMENT_START);
617
618   box1 = clutter_box_new (layout1);
619
620   rectangle = clutter_rectangle_new_with_color (
621       CLUTTER_COLOR_Transparent);
622
623   clutter_rectangle_set_border_width (CLUTTER_RECTANGLE (rectangle),
624       1);
625
626   clutter_actor_set_size (box1,
627       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
628       SELF_VIDEO_SECTION_HEIGTH + 2 * SELF_VIDEO_SECTION_MARGIN +
629       FLOATING_TOOLBAR_HEIGHT + FLOATING_TOOLBAR_SPACING);
630
631   layout2 = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
632       CLUTTER_BIN_ALIGNMENT_CENTER);
633
634   /* We have a box with the margins and the video in the middle inside
635    * a bigger box with an extra bottom margin so we're not on top of
636    * the floating toolbar. */
637   box2 = clutter_box_new (layout2);
638
639   clutter_actor_set_size (box2,
640       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
641       SELF_VIDEO_SECTION_HEIGTH + 2 * SELF_VIDEO_SECTION_MARGIN);
642
643   clutter_actor_set_size (rectangle,
644       SELF_VIDEO_SECTION_WIDTH + 5, SELF_VIDEO_SECTION_HEIGTH + 5);
645
646   clutter_container_add_actor (CLUTTER_CONTAINER (box1), box2);
647   clutter_container_add_actor (CLUTTER_CONTAINER (box2), rectangle);
648
649   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (self->priv->video_layout),
650       box1, x, y);
651
652   clutter_actor_hide (rectangle);
653
654   return rectangle;
655 }
656
657 static void
658 empathy_call_window_create_preview_rectangles (EmpathyCallWindow *self)
659 {
660   self->priv->preview_rectangle1 =
661       empathy_call_window_create_preview_rectangle (self,
662           CLUTTER_BIN_ALIGNMENT_START, CLUTTER_BIN_ALIGNMENT_START);
663   self->priv->preview_rectangle2 =
664       empathy_call_window_create_preview_rectangle (self,
665           CLUTTER_BIN_ALIGNMENT_START, CLUTTER_BIN_ALIGNMENT_END);
666   self->priv->preview_rectangle3 =
667       empathy_call_window_create_preview_rectangle (self,
668           CLUTTER_BIN_ALIGNMENT_END, CLUTTER_BIN_ALIGNMENT_START);
669   self->priv->preview_rectangle4 =
670       empathy_call_window_create_preview_rectangle (self,
671           CLUTTER_BIN_ALIGNMENT_END, CLUTTER_BIN_ALIGNMENT_END);
672 }
673
674 static void
675 empathy_call_window_show_preview_rectangles (EmpathyCallWindow *self,
676     gboolean show)
677 {
678   g_object_set (self->priv->preview_rectangle1, "visible", show, NULL);
679   g_object_set (self->priv->preview_rectangle2, "visible", show, NULL);
680   g_object_set (self->priv->preview_rectangle3, "visible", show, NULL);
681   g_object_set (self->priv->preview_rectangle4, "visible", show, NULL);
682 }
683
684 static PreviewPosition
685 empathy_call_window_get_preview_position (EmpathyCallWindow *self,
686     gfloat event_x,
687     gfloat event_y)
688 {
689   ClutterGeometry box;
690   PreviewPosition pos = PREVIEW_POS_NONE;
691
692   clutter_actor_get_geometry (self->priv->video_box, &box);
693
694   if (0 + SELF_VIDEO_SECTION_MARGIN <= event_x &&
695       event_x <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_WIDTH) &&
696       0 + SELF_VIDEO_SECTION_MARGIN <= event_y &&
697       event_y <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_HEIGTH))
698     {
699       pos = PREVIEW_POS_TOP_LEFT;
700     }
701   else if (box.width - SELF_VIDEO_SECTION_MARGIN >= event_x &&
702       event_x >= (box.width - SELF_VIDEO_SECTION_MARGIN - (gint) SELF_VIDEO_SECTION_WIDTH) &&
703       0 + SELF_VIDEO_SECTION_MARGIN <= event_y &&
704       event_y <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_HEIGTH))
705     {
706       pos = PREVIEW_POS_TOP_RIGHT;
707     }
708   else if (0 + SELF_VIDEO_SECTION_MARGIN <= event_x &&
709       event_x <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_WIDTH) &&
710       box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING >= event_y &&
711       event_y >= (box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING - (gint) SELF_VIDEO_SECTION_HEIGTH))
712     {
713       pos = PREVIEW_POS_BOTTOM_LEFT;
714     }
715   else if (box.width - SELF_VIDEO_SECTION_MARGIN >= event_x &&
716       event_x >= (box.width - SELF_VIDEO_SECTION_MARGIN - (gint) SELF_VIDEO_SECTION_WIDTH) &&
717       box.height - SELF_VIDEO_SECTION_MARGIN - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING >= event_y &&
718       event_y >= (box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING - (gint) SELF_VIDEO_SECTION_HEIGTH))
719     {
720       pos = PREVIEW_POS_BOTTOM_RIGHT;
721     }
722
723   return pos;
724 }
725
726 static ClutterActor *
727 empathy_call_window_get_preview_rectangle (EmpathyCallWindow *self,
728     PreviewPosition pos)
729 {
730   ClutterActor *rectangle;
731
732   switch (pos)
733     {
734       case PREVIEW_POS_TOP_LEFT:
735         rectangle = self->priv->preview_rectangle1;
736         break;
737       case PREVIEW_POS_TOP_RIGHT:
738         rectangle = self->priv->preview_rectangle3;
739         break;
740       case PREVIEW_POS_BOTTOM_LEFT:
741         rectangle = self->priv->preview_rectangle2;
742         break;
743       case PREVIEW_POS_BOTTOM_RIGHT:
744         rectangle = self->priv->preview_rectangle4;
745         break;
746       default:
747         rectangle = NULL;
748     }
749
750   return rectangle;
751 }
752
753 static void
754 empathy_call_window_move_video_preview (EmpathyCallWindow *self,
755     PreviewPosition pos)
756 {
757   ClutterBinLayout *layout = CLUTTER_BIN_LAYOUT (self->priv->video_layout);
758
759   DEBUG ("moving the video preview to %d", pos);
760
761   self->priv->preview_pos = pos;
762
763   switch (pos)
764     {
765       case PREVIEW_POS_TOP_LEFT:
766         clutter_bin_layout_set_alignment (layout,
767             self->priv->video_preview,
768             CLUTTER_BIN_ALIGNMENT_START,
769             CLUTTER_BIN_ALIGNMENT_START);
770         break;
771       case PREVIEW_POS_TOP_RIGHT:
772         clutter_bin_layout_set_alignment (layout,
773             self->priv->video_preview,
774             CLUTTER_BIN_ALIGNMENT_END,
775             CLUTTER_BIN_ALIGNMENT_START);
776         break;
777       case PREVIEW_POS_BOTTOM_LEFT:
778         clutter_bin_layout_set_alignment (layout,
779             self->priv->video_preview,
780             CLUTTER_BIN_ALIGNMENT_START,
781             CLUTTER_BIN_ALIGNMENT_END);
782         break;
783       case PREVIEW_POS_BOTTOM_RIGHT:
784         clutter_bin_layout_set_alignment (layout,
785             self->priv->video_preview,
786             CLUTTER_BIN_ALIGNMENT_END,
787             CLUTTER_BIN_ALIGNMENT_END);
788         break;
789       default:
790         g_warn_if_reached ();
791     }
792 }
793
794 static void
795 empathy_call_window_highlight_preview_rectangle (EmpathyCallWindow *self,
796     PreviewPosition pos)
797 {
798   ClutterActor *rectangle;
799
800   rectangle = empathy_call_window_get_preview_rectangle (self, pos);
801
802   clutter_rectangle_set_border_width (CLUTTER_RECTANGLE (rectangle), 3);
803   clutter_rectangle_set_border_color (CLUTTER_RECTANGLE (rectangle),
804       CLUTTER_COLOR_Red);
805 }
806
807 static void
808 empathy_call_window_darken_preview_rectangle (EmpathyCallWindow *self,
809     ClutterActor *rectangle)
810 {
811   clutter_rectangle_set_border_width (CLUTTER_RECTANGLE (rectangle), 1);
812   clutter_rectangle_set_border_color (CLUTTER_RECTANGLE (rectangle),
813       CLUTTER_COLOR_Black);
814 }
815
816 static void
817 empathy_call_window_darken_preview_rectangles (EmpathyCallWindow *self)
818 {
819   ClutterActor *rectangle;
820
821   rectangle = empathy_call_window_get_preview_rectangle (self,
822       self->priv->preview_pos);
823
824   /* We don't want to darken the rectangle where the preview
825    * currently is. */
826
827   if (self->priv->preview_rectangle1 != rectangle)
828     empathy_call_window_darken_preview_rectangle (self,
829         self->priv->preview_rectangle1);
830
831   if (self->priv->preview_rectangle2 != rectangle)
832     empathy_call_window_darken_preview_rectangle (self,
833         self->priv->preview_rectangle2);
834
835   if (self->priv->preview_rectangle3 != rectangle)
836     empathy_call_window_darken_preview_rectangle (self,
837         self->priv->preview_rectangle3);
838
839   if (self->priv->preview_rectangle4 != rectangle)
840     empathy_call_window_darken_preview_rectangle (self,
841         self->priv->preview_rectangle4);
842 }
843
844 static void
845 empathy_call_window_preview_on_drag_begin_cb (ClutterDragAction *action,
846     ClutterActor *actor,
847     gfloat event_x,
848     gfloat event_y,
849     ClutterModifierType modifiers,
850     EmpathyCallWindow *self)
851 {
852   empathy_call_window_show_preview_rectangles (self, TRUE);
853   empathy_call_window_darken_preview_rectangles (self);
854
855   self->priv->event_x = event_x;
856   self->priv->event_y = event_y;
857 }
858
859 static void
860 empathy_call_window_preview_on_drag_end_cb (ClutterDragAction *action,
861     ClutterActor *actor,
862     gfloat event_x,
863     gfloat event_y,
864     ClutterModifierType modifiers,
865     EmpathyCallWindow *self)
866 {
867   PreviewPosition pos;
868
869   pos = empathy_call_window_get_preview_position (self, event_x, event_y);
870
871   if (pos != PREVIEW_POS_NONE)
872     empathy_call_window_move_video_preview (self, pos);
873
874   empathy_call_window_show_preview_rectangles (self, FALSE);
875 }
876
877 static void
878 empathy_call_window_preview_on_drag_motion_cb (ClutterDragAction *action,
879     ClutterActor *actor,
880     gfloat delta_x,
881     gfloat delta_y,
882     EmpathyCallWindow *self)
883 {
884   PreviewPosition pos;
885
886   pos = empathy_call_window_get_preview_position (self,
887       self->priv->event_x - delta_x, self->priv->event_y + delta_y);
888
889   if (pos != PREVIEW_POS_NONE)
890     empathy_call_window_highlight_preview_rectangle (self, pos);
891   else
892     empathy_call_window_darken_preview_rectangles (self);
893 }
894
895 static gboolean
896 empathy_call_window_preview_enter_event_cb (ClutterActor *actor,
897     ClutterCrossingEvent *event,
898     EmpathyCallWindow *self)
899 {
900   ClutterActor *rectangle;
901   PreviewPosition pos;
902
903   pos = empathy_call_window_get_preview_position (self, event->x, event->y);
904   rectangle = empathy_call_window_get_preview_rectangle (self, pos);
905
906   empathy_call_window_highlight_preview_rectangle (self, pos);
907
908   clutter_actor_show (rectangle);
909
910   return FALSE;
911 }
912
913 static gboolean
914 empathy_call_window_preview_leave_event_cb (ClutterActor *actor,
915     ClutterCrossingEvent *event,
916     EmpathyCallWindow *self)
917 {
918   ClutterActor *rectangle;
919
920   rectangle = empathy_call_window_get_preview_rectangle (self,
921       self->priv->preview_pos);
922
923   empathy_call_window_darken_preview_rectangle (self, rectangle);
924
925   clutter_actor_hide (rectangle);
926
927   return FALSE;
928 }
929
930 static void
931 create_video_preview (EmpathyCallWindow *self)
932 {
933   EmpathyCallWindowPriv *priv = GET_PRIV (self);
934   ClutterLayoutManager *layout, *layout_center;
935   ClutterActor *preview;
936   ClutterActor *box;
937   ClutterActor *b;
938   ClutterAction *action;
939   GtkWidget *button;
940
941   g_assert (priv->video_preview == NULL);
942
943   preview = clutter_texture_new ();
944   clutter_actor_set_size (preview,
945       SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGTH);
946   priv->video_preview_sink = clutter_gst_video_sink_new (
947       CLUTTER_TEXTURE (preview));
948
949   /* Flip the video preview */
950   clutter_actor_set_rotation (preview,
951       CLUTTER_Y_AXIS,
952       180,
953       SELF_VIDEO_SECTION_WIDTH * 0.5,
954       0.0,
955       0.0);
956
957   /* Add a little offset to the video preview */
958   layout = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
959       CLUTTER_BIN_ALIGNMENT_START);
960   priv->video_preview = clutter_box_new (layout);
961   clutter_actor_set_size (priv->video_preview,
962       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
963       SELF_VIDEO_SECTION_HEIGTH + 2 * SELF_VIDEO_SECTION_MARGIN +
964       FLOATING_TOOLBAR_HEIGHT + FLOATING_TOOLBAR_SPACING);
965
966   /* We have a box with the margins and the video in the middle inside
967    * a bigger box with an extra bottom margin so we're not on top of
968    * the floating toolbar. */
969   layout_center = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
970       CLUTTER_BIN_ALIGNMENT_CENTER);
971   box = clutter_box_new (layout_center);
972   clutter_actor_set_size (box,
973       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
974       SELF_VIDEO_SECTION_HEIGTH + 2 * SELF_VIDEO_SECTION_MARGIN);
975
976   clutter_container_add_actor (CLUTTER_CONTAINER (box), preview);
977   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_preview), box);
978
979   g_object_set (priv->video_preview_sink,
980       "sync", FALSE,
981       "async", TRUE,
982       NULL);
983
984   /* Translators: this is an "Info" label. It should be as short
985    * as possible. */
986   button = gtk_button_new_with_label (_("i"));
987   b = gtk_clutter_actor_new_with_contents (button);
988
989   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (layout_center),
990       b,
991       CLUTTER_BIN_ALIGNMENT_END,
992       CLUTTER_BIN_ALIGNMENT_END);
993
994   g_signal_connect (button, "clicked",
995       G_CALLBACK (empathy_call_window_preview_button_clicked_cb),
996       self);
997
998   /* Translators: this is an "Info" label. It should be as short
999    * as possible. */
1000   button = gtk_button_new_with_label (_("i"));
1001   priv->preview_hidden_button =
1002       gtk_clutter_actor_new_with_contents (button);
1003
1004   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (priv->video_layout),
1005       priv->preview_hidden_button,
1006       CLUTTER_BIN_ALIGNMENT_START,
1007       CLUTTER_BIN_ALIGNMENT_END);
1008
1009   self->priv->preview_pos = PREVIEW_POS_BOTTOM_LEFT;
1010
1011   clutter_actor_hide (priv->preview_hidden_button);
1012
1013   g_signal_connect (button, "clicked",
1014       G_CALLBACK (empathy_call_window_preview_hidden_button_clicked_cb),
1015       self);
1016
1017   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (priv->video_layout),
1018       priv->video_preview,
1019       CLUTTER_BIN_ALIGNMENT_START,
1020       CLUTTER_BIN_ALIGNMENT_END);
1021
1022   action = clutter_drag_action_new ();
1023   g_signal_connect (action, "drag-begin",
1024       G_CALLBACK (empathy_call_window_preview_on_drag_begin_cb), self);
1025   g_signal_connect (action, "drag-end",
1026       G_CALLBACK (empathy_call_window_preview_on_drag_end_cb), self);
1027   g_signal_connect (action, "drag-motion",
1028       G_CALLBACK (empathy_call_window_preview_on_drag_motion_cb), self);
1029
1030   g_signal_connect (preview, "enter-event",
1031       G_CALLBACK (empathy_call_window_preview_enter_event_cb), self);
1032   g_signal_connect (preview, "leave-event",
1033       G_CALLBACK (empathy_call_window_preview_leave_event_cb), self);
1034
1035   clutter_actor_add_action (preview, action);
1036   clutter_actor_set_reactive (preview, TRUE);
1037 }
1038
1039 static void
1040 play_camera (EmpathyCallWindow *window,
1041     gboolean play)
1042 {
1043   EmpathyCallWindowPriv *priv = GET_PRIV (window);
1044   GstElement *preview;
1045   GstState state;
1046
1047   if (priv->video_preview == NULL)
1048     {
1049       create_video_preview (window);
1050       add_video_preview_to_pipeline (window);
1051     }
1052
1053   if (play)
1054     state = GST_STATE_PLAYING;
1055   else
1056     state = GST_STATE_NULL;
1057
1058   preview = priv->video_preview_sink;
1059
1060   gst_element_set_state (preview, state);
1061   gst_element_set_state (priv->video_input, state);
1062   gst_element_set_state (priv->video_tee, state);
1063 }
1064
1065 static void
1066 display_video_preview (EmpathyCallWindow *self,
1067     gboolean display)
1068 {
1069   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1070
1071   if (display)
1072     {
1073       /* Display the video preview */
1074       DEBUG ("Show video preview");
1075
1076       play_camera (self, TRUE);
1077       clutter_actor_show (priv->video_preview);
1078       clutter_actor_raise_top (priv->floating_toolbar);
1079     }
1080   else
1081     {
1082       /* Hide the video preview */
1083       DEBUG ("Hide video preview");
1084
1085       if (priv->video_preview != NULL)
1086         {
1087           clutter_actor_hide (priv->video_preview);
1088           play_camera (self, FALSE);
1089         }
1090     }
1091 }
1092
1093 static void
1094 empathy_call_window_set_state_connecting (EmpathyCallWindow *window)
1095 {
1096   EmpathyCallWindowPriv *priv = GET_PRIV (window);
1097
1098   empathy_call_window_status_message (window, _("Connecting…"));
1099   priv->call_state = CONNECTING;
1100
1101   if (priv->outgoing)
1102     empathy_sound_manager_start_playing (priv->sound_mgr, GTK_WIDGET (window),
1103         EMPATHY_SOUND_PHONE_OUTGOING, MS_BETWEEN_RING);
1104 }
1105
1106 static void
1107 disable_camera (EmpathyCallWindow *self)
1108 {
1109   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1110
1111   if (priv->camera_state == CAMERA_STATE_OFF)
1112     return;
1113
1114   DEBUG ("Disable camera");
1115
1116   display_video_preview (self, FALSE);
1117
1118   if (priv->camera_state == CAMERA_STATE_ON)
1119     empathy_call_window_set_send_video (self, CAMERA_STATE_OFF);
1120
1121   priv->camera_state = CAMERA_STATE_OFF;
1122 }
1123
1124 static void
1125 enable_camera (EmpathyCallWindow *self)
1126 {
1127   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1128
1129   if (priv->camera_state == CAMERA_STATE_ON)
1130     return;
1131
1132   if (priv->video_input == NULL)
1133     {
1134       DEBUG ("Can't enable camera, no input");
1135       return;
1136     }
1137
1138   DEBUG ("Enable camera");
1139
1140   empathy_call_window_set_send_video (self, CAMERA_STATE_ON);
1141
1142   priv->camera_state = CAMERA_STATE_ON;
1143 }
1144
1145 static void
1146 empathy_call_window_camera_toggled_cb (GtkToggleToolButton *toggle,
1147   EmpathyCallWindow *self)
1148 {
1149   if (gtk_toggle_tool_button_get_active (toggle))
1150     enable_camera (self);
1151   else
1152     disable_camera (self);
1153 }
1154
1155 static void
1156 create_pipeline (EmpathyCallWindow *self)
1157 {
1158   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1159   GstBus *bus;
1160
1161   g_assert (priv->pipeline == NULL);
1162
1163   priv->pipeline = gst_pipeline_new (NULL);
1164   priv->pipeline_playing = FALSE;
1165
1166   priv->video_tee = gst_element_factory_make ("tee", NULL);
1167   gst_object_ref (priv->video_tee);
1168   gst_object_sink (priv->video_tee);
1169
1170   gst_bin_add (GST_BIN (priv->pipeline), priv->video_tee);
1171
1172   bus = gst_pipeline_get_bus (GST_PIPELINE (priv->pipeline));
1173   priv->bus_message_source_id = gst_bus_add_watch (bus,
1174       empathy_call_window_bus_message, self);
1175
1176   g_object_unref (bus);
1177 }
1178
1179 static void
1180 empathy_call_window_settings_cb (GtkAction *action,
1181     EmpathyCallWindow *self)
1182 {
1183   gchar *args = g_strdup_printf ("-p %s",
1184       empathy_preferences_tab_to_string (EMPATHY_PREFERENCES_TAB_CALLS));
1185
1186   empathy_launch_program (BIN_DIR, "empathy", args);
1187
1188   g_free (args);
1189 }
1190
1191 static void
1192 empathy_call_window_contents_cb (GtkAction *action,
1193     EmpathyCallWindow *self)
1194 {
1195   empathy_url_show (GTK_WIDGET (self), "ghelp:empathy?audio-video");
1196 }
1197
1198 static void
1199 empathy_call_window_debug_cb (GtkAction *action,
1200     EmpathyCallWindow *self)
1201 {
1202   empathy_launch_program (BIN_DIR, "empathy-debugger", "-s Empathy.Call");
1203 }
1204
1205 static void
1206 empathy_call_window_about_cb (GtkAction *action,
1207     EmpathyCallWindow *self)
1208 {
1209   empathy_about_dialog_new (GTK_WINDOW (self));
1210 }
1211
1212 static gboolean
1213 empathy_call_window_toolbar_timeout (gpointer data)
1214 {
1215   EmpathyCallWindow *self = data;
1216
1217   clutter_state_set_state (self->priv->transitions, "fade-out");
1218
1219   return TRUE;
1220 }
1221
1222 static gboolean
1223 empathy_call_window_motion_notify_cb (GtkWidget *widget,
1224     GdkEvent *event,
1225     EmpathyCallWindow *self)
1226 {
1227   clutter_state_set_state (self->priv->transitions, "fade-in");
1228
1229   if (self->priv->inactivity_src > 0)
1230     g_source_remove (self->priv->inactivity_src);
1231
1232   self->priv->inactivity_src = g_timeout_add_seconds (3,
1233       empathy_call_window_toolbar_timeout, self);
1234
1235   return FALSE;
1236 }
1237
1238 static gboolean
1239 empathy_call_window_configure_event_cb (GtkWidget *widget,
1240     GdkEvent  *event,
1241     EmpathyCallWindow *self)
1242 {
1243   GdkWindow *gdk_window;
1244   GdkWindowState window_state;
1245
1246   gtk_window_get_position (GTK_WINDOW (self), &self->priv->x, &self->priv->y);
1247   gtk_window_get_size (GTK_WINDOW (self), &self->priv->w, &self->priv->h);
1248
1249   gtk_widget_get_preferred_width (self->priv->dtmf_panel,
1250       &self->priv->dialpad_width, NULL);
1251
1252   gdk_window = gtk_widget_get_window (widget);
1253   window_state = gdk_window_get_state (gdk_window);
1254   self->priv->maximized = (window_state & GDK_WINDOW_STATE_MAXIMIZED);
1255
1256   return FALSE;
1257 }
1258
1259 static void
1260 empathy_call_window_destroyed_cb (GtkWidget *object,
1261     EmpathyCallWindow *self)
1262 {
1263   if (gtk_widget_get_visible (self->priv->dtmf_panel))
1264     {
1265       /* Save the geometry as if the dialpad was hidden. */
1266       empathy_geometry_save_values (GTK_WINDOW (self),
1267           self->priv->x, self->priv->y,
1268           self->priv->w - self->priv->dialpad_width, self->priv->h,
1269           self->priv->maximized);
1270     }
1271 }
1272
1273 static void
1274 empathy_call_window_stage_allocation_changed_cb (ClutterActor *stage,
1275     GParamSpec *pspec,
1276     ClutterBindConstraint *constraint)
1277 {
1278   ClutterActorBox allocation;
1279
1280   clutter_actor_get_allocation_box (stage, &allocation);
1281
1282   clutter_bind_constraint_set_offset (constraint,
1283       allocation.y2 - allocation.y1 -
1284       FLOATING_TOOLBAR_SPACING - FLOATING_TOOLBAR_HEIGHT);
1285 }
1286
1287 static void
1288 empathy_call_window_init (EmpathyCallWindow *self)
1289 {
1290   EmpathyCallWindowPriv *priv;
1291   GtkBuilder *gui;
1292   GtkWidget *top_vbox;
1293   gchar *filename;
1294   ClutterConstraint *constraint;
1295   ClutterActor *remote_avatar;
1296   GtkStyleContext *context;
1297   GdkRGBA rgba;
1298   ClutterColor bg;
1299
1300   priv = self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1301     EMPATHY_TYPE_CALL_WINDOW, EmpathyCallWindowPriv);
1302
1303   filename = empathy_file_lookup ("empathy-call-window.ui", "src");
1304   gui = empathy_builder_get_file (filename,
1305     "call_window_vbox", &top_vbox,
1306     "errors_vbox", &priv->errors_vbox,
1307     "pane", &priv->pane,
1308     "remote_user_name_toolbar", &priv->remote_user_name_toolbar,
1309     "remote_user_avatar_toolbar", &priv->remote_user_avatar_toolbar,
1310     "status_label", &priv->status_label,
1311     "audiocall", &priv->audio_call_button,
1312     "videocall", &priv->video_call_button,
1313     "microphone", &priv->mic_button,
1314     "camera", &priv->camera_button,
1315     "hangup", &priv->hangup_button,
1316     "dialpad", &priv->dialpad_button,
1317     "toolbar", &priv->toolbar,
1318     "bottom_toolbar", &priv->bottom_toolbar,
1319     "ui_manager", &priv->ui_manager,
1320     "menufullscreen", &priv->menu_fullscreen,
1321     "menupreviewswap", &priv->menu_swap_camera,
1322     "details_vbox",  &priv->details_vbox,
1323     "vcodec_encoding_label", &priv->vcodec_encoding_label,
1324     "acodec_encoding_label", &priv->acodec_encoding_label,
1325     "acodec_decoding_label", &priv->acodec_decoding_label,
1326     "vcodec_decoding_label", &priv->vcodec_decoding_label,
1327     "audio_remote_candidate_label", &priv->audio_remote_candidate_label,
1328     "audio_local_candidate_label", &priv->audio_local_candidate_label,
1329     "video_remote_candidate_label", &priv->video_remote_candidate_label,
1330     "video_local_candidate_label", &priv->video_local_candidate_label,
1331     "video_remote_candidate_info_img", &priv->video_remote_candidate_info_img,
1332     "video_local_candidate_info_img", &priv->video_local_candidate_info_img,
1333     "audio_remote_candidate_info_img", &priv->audio_remote_candidate_info_img,
1334     "audio_local_candidate_info_img", &priv->audio_local_candidate_info_img,
1335     NULL);
1336   g_free (filename);
1337
1338   empathy_builder_connect (gui, self,
1339     "hangup", "clicked", empathy_call_window_hangup_cb,
1340     "audiocall", "clicked", empathy_call_window_audio_call_cb,
1341     "videocall", "clicked", empathy_call_window_video_call_cb,
1342     "volume", "value-changed", empathy_call_window_volume_changed_cb,
1343     "microphone", "toggled", empathy_call_window_mic_toggled_cb,
1344     "camera", "toggled", empathy_call_window_camera_toggled_cb,
1345     "dialpad", "toggled", empathy_call_window_dialpad_cb,
1346     "menufullscreen", "activate", empathy_call_window_fullscreen_cb,
1347     "menusettings", "activate", empathy_call_window_settings_cb,
1348     "menucontents", "activate", empathy_call_window_contents_cb,
1349     "menudebug", "activate", empathy_call_window_debug_cb,
1350     "menuabout", "activate", empathy_call_window_about_cb,
1351     "menupreviewdisable", "activate", empathy_call_window_disable_camera_cb,
1352     "menupreviewminimise", "activate", empathy_call_window_minimise_camera_cb,
1353     "menupreviewmaximise", "activate", empathy_call_window_maximise_camera_cb,
1354     "menupreviewswap", "activate", empathy_call_window_swap_camera_cb,
1355     NULL);
1356
1357   gtk_action_set_sensitive (priv->menu_fullscreen, FALSE);
1358
1359   priv->camera_monitor = empathy_camera_monitor_dup_singleton ();
1360
1361   g_object_bind_property (priv->camera_monitor, "available",
1362       priv->camera_button, "sensitive",
1363       G_BINDING_SYNC_CREATE);
1364
1365   g_signal_connect (priv->camera_monitor, "added",
1366       G_CALLBACK (empathy_call_window_camera_added_cb), self);
1367   g_signal_connect (priv->camera_monitor, "removed",
1368       G_CALLBACK (empathy_call_window_camera_removed_cb), self);
1369
1370   priv->lock = g_mutex_new ();
1371
1372   gtk_container_add (GTK_CONTAINER (self), top_vbox);
1373
1374   priv->content_hbox = gtk_hbox_new (FALSE, CONTENT_HBOX_SPACING);
1375   gtk_container_set_border_width (GTK_CONTAINER (priv->content_hbox),
1376                                   CONTENT_HBOX_BORDER_WIDTH);
1377   gtk_box_pack_start (GTK_BOX (priv->pane), priv->content_hbox,
1378       TRUE, TRUE, 0);
1379
1380   /* avatar/video box */
1381   priv->video_layout = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
1382       CLUTTER_BIN_ALIGNMENT_CENTER);
1383
1384   priv->video_box = clutter_box_new (priv->video_layout);
1385
1386   priv->video_container = gtk_clutter_embed_new ();
1387
1388   /* Set the background color to that of the rest of the window */
1389   context = gtk_widget_get_style_context (priv->content_hbox);
1390   gtk_style_context_get_background_color (context,
1391       GTK_STATE_FLAG_NORMAL, &rgba);
1392   bg.red = CLAMP (rgba.red * 255.0, 0, 255);
1393   bg.green = CLAMP (rgba.green * 255.0, 0, 255);
1394   bg.blue = CLAMP (rgba.blue * 255.0, 0, 255);
1395   bg.alpha = CLAMP (rgba.alpha * 255.0, 0, 255);
1396   clutter_stage_set_color (
1397       CLUTTER_STAGE (gtk_clutter_embed_get_stage (
1398           GTK_CLUTTER_EMBED (priv->video_container))),
1399       &bg);
1400
1401   clutter_container_add (
1402       CLUTTER_CONTAINER (gtk_clutter_embed_get_stage (
1403           GTK_CLUTTER_EMBED (priv->video_container))),
1404       priv->video_box,
1405       NULL);
1406
1407   constraint = clutter_bind_constraint_new (
1408       gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)),
1409       CLUTTER_BIND_SIZE, 0);
1410   clutter_actor_add_constraint (priv->video_box, constraint);
1411
1412   priv->remote_user_avatar_widget = gtk_image_new ();
1413   remote_avatar = gtk_clutter_actor_new_with_contents (
1414       priv->remote_user_avatar_widget);
1415
1416   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_box),
1417       remote_avatar);
1418
1419   empathy_call_window_create_preview_rectangles (self);
1420
1421   gtk_box_pack_start (GTK_BOX (priv->content_hbox),
1422       priv->video_container, TRUE, TRUE,
1423       CONTENT_HBOX_CHILDREN_PACKING_PADDING);
1424
1425   create_pipeline (self);
1426   create_video_output_widget (self);
1427   create_audio_input (self);
1428   create_video_input (self);
1429
1430   priv->floating_toolbar = empathy_rounded_actor_new ();
1431
1432   gtk_widget_reparent (priv->bottom_toolbar,
1433       gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (priv->floating_toolbar)));
1434
1435   constraint = clutter_bind_constraint_new (
1436       gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)),
1437       CLUTTER_BIND_Y, 0);
1438
1439   clutter_actor_add_constraint (priv->floating_toolbar, constraint);
1440
1441   g_signal_connect (
1442       gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)),
1443       "notify::allocation",
1444       G_CALLBACK (empathy_call_window_stage_allocation_changed_cb),
1445       constraint);
1446
1447   clutter_actor_set_size (priv->floating_toolbar,
1448       FLOATING_TOOLBAR_WIDTH, FLOATING_TOOLBAR_HEIGHT);
1449   clutter_actor_set_opacity (priv->floating_toolbar, FLOATING_TOOLBAR_OPACITY);
1450
1451   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (priv->video_layout),
1452       priv->floating_toolbar,
1453       CLUTTER_BIN_ALIGNMENT_CENTER,
1454       CLUTTER_BIN_ALIGNMENT_END);
1455
1456   clutter_actor_raise_top (priv->floating_toolbar);
1457
1458   /* Transitions for the floating toolbar */
1459   priv->transitions = clutter_state_new ();
1460
1461   /* all transitions last for 2s */
1462   clutter_state_set_duration (priv->transitions, NULL, NULL, 2000);
1463
1464   /* transition from any state to "fade-out" state */
1465   clutter_state_set (priv->transitions, NULL, "fade-out",
1466       priv->floating_toolbar,
1467       "opacity", CLUTTER_EASE_OUT_QUAD, 0,
1468       NULL);
1469
1470   /* transition from any state to "fade-in" state */
1471   clutter_state_set (priv->transitions, NULL, "fade-in",
1472       priv->floating_toolbar,
1473       "opacity", CLUTTER_EASE_OUT_QUAD, FLOATING_TOOLBAR_OPACITY,
1474       NULL);
1475
1476   /* put the actor into the "fade-in" state with no animation */
1477   clutter_state_warp_to_state (priv->transitions, "fade-in");
1478
1479   /* The call will be started as soon the pipeline is playing */
1480   priv->start_call_when_playing = TRUE;
1481
1482   priv->dtmf_panel = empathy_create_dtmf_dialpad (G_OBJECT (self),
1483       G_CALLBACK (dtmf_button_pressed_cb),
1484       G_CALLBACK (dtmf_button_released_cb));
1485
1486   gtk_box_pack_start (GTK_BOX (priv->pane), priv->dtmf_panel,
1487       FALSE, FALSE, 0);
1488
1489   gtk_box_pack_start (GTK_BOX (priv->pane), priv->details_vbox,
1490       FALSE, FALSE, 0);
1491
1492   gtk_widget_set_sensitive (priv->dtmf_panel, FALSE);
1493
1494   gtk_widget_show_all (top_vbox);
1495
1496   gtk_widget_hide (priv->dtmf_panel);
1497   gtk_widget_hide (priv->details_vbox);
1498
1499   priv->fullscreen = empathy_call_window_fullscreen_new (self);
1500
1501   empathy_call_window_fullscreen_set_video_widget (priv->fullscreen,
1502       priv->video_container);
1503
1504   /* We hide the bottom toolbar after 3s of inactivity and show it
1505    * again on mouse movement */
1506   priv->inactivity_src = g_timeout_add_seconds (3,
1507       empathy_call_window_toolbar_timeout, self);
1508
1509   g_signal_connect (G_OBJECT (priv->fullscreen->leave_fullscreen_button),
1510       "clicked", G_CALLBACK (empathy_call_window_fullscreen_cb), self);
1511
1512   g_signal_connect (G_OBJECT (self), "realize",
1513     G_CALLBACK (empathy_call_window_realized_cb), self);
1514
1515   g_signal_connect (G_OBJECT (self), "delete-event",
1516     G_CALLBACK (empathy_call_window_delete_cb), self);
1517
1518   g_signal_connect (G_OBJECT (self), "window-state-event",
1519     G_CALLBACK (empathy_call_window_state_event_cb), self);
1520
1521   g_signal_connect (G_OBJECT (self), "key-press-event",
1522       G_CALLBACK (empathy_call_window_key_press_cb), self);
1523
1524   g_signal_connect (self, "motion-notify-event",
1525       G_CALLBACK (empathy_call_window_motion_notify_cb), self);
1526
1527   priv->timer = g_timer_new ();
1528
1529   g_object_ref (priv->ui_manager);
1530   g_object_unref (gui);
1531
1532   priv->sound_mgr = empathy_sound_manager_dup_singleton ();
1533   priv->mic_menu = empathy_mic_menu_new (self);
1534   priv->camera_menu = empathy_camera_menu_new (self);
1535
1536   empathy_call_window_show_hangup_button (self, TRUE);
1537
1538   priv->settings = g_settings_new (EMPATHY_PREFS_CALL_SCHEMA);
1539
1540   /* Retrieve initial volume */
1541   priv->volume = g_settings_get_double (priv->settings,
1542       EMPATHY_PREFS_CALL_SOUND_VOLUME) / 100.0;
1543
1544   g_signal_connect (priv->settings, "changed::"EMPATHY_PREFS_CALL_SOUND_VOLUME,
1545       G_CALLBACK (empathy_call_window_prefs_volume_changed_cb), self);
1546
1547   empathy_geometry_bind (GTK_WINDOW (self), "call-window");
1548   /* These signals are used to track the window position and save it
1549    * when the window is destroyed. We need to do this as we don't want
1550    * the window geometry to be saved with the dialpad taken into account. */
1551   g_signal_connect (self, "destroy",
1552       G_CALLBACK (empathy_call_window_destroyed_cb), self);
1553   g_signal_connect (self, "configure-event",
1554       G_CALLBACK (empathy_call_window_configure_event_cb), self);
1555   g_signal_connect (self, "window-state-event",
1556       G_CALLBACK (empathy_call_window_configure_event_cb), self);
1557
1558   /* Don't display labels in both toolbars */
1559   gtk_toolbar_set_style (GTK_TOOLBAR (priv->toolbar), GTK_TOOLBAR_ICONS);
1560 }
1561
1562 /* Instead of specifying a width and a height, we specify only one size. That's
1563    because we want a square avatar icon.  */
1564 static void
1565 init_contact_avatar_with_size (EmpathyContact *contact,
1566     GtkWidget *image_widget,
1567     gint size)
1568 {
1569   GdkPixbuf *pixbuf_avatar = NULL;
1570
1571   if (contact != NULL)
1572     {
1573       pixbuf_avatar = empathy_pixbuf_avatar_from_contact_scaled (contact,
1574         size, size);
1575     }
1576
1577   if (pixbuf_avatar == NULL)
1578     {
1579       pixbuf_avatar = empathy_pixbuf_from_icon_name_sized (
1580           EMPATHY_IMAGE_AVATAR_DEFAULT, size);
1581     }
1582
1583   gtk_image_set_from_pixbuf (GTK_IMAGE (image_widget), pixbuf_avatar);
1584
1585   if (pixbuf_avatar != NULL)
1586     g_object_unref (pixbuf_avatar);
1587 }
1588
1589 static void
1590 set_window_title (EmpathyCallWindow *self)
1591 {
1592   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1593   gchar *tmp;
1594
1595   if (priv->contact != NULL)
1596     {
1597       /* translators: Call is a noun and %s is the contact name. This string
1598        * is used in the window title */
1599       tmp = g_strdup_printf (_("Call with %s"),
1600           empathy_contact_get_alias (priv->contact));
1601       gtk_window_set_title (GTK_WINDOW (self), tmp);
1602       g_free (tmp);
1603     }
1604   else
1605     {
1606       g_warning ("Unknown remote contact!");
1607     }
1608 }
1609
1610 static void
1611 set_remote_user_name (EmpathyCallWindow *self,
1612   EmpathyContact *contact)
1613 {
1614   const gchar *alias = empathy_contact_get_alias (contact);
1615   const gchar *status = empathy_contact_get_status (contact);
1616   gchar *label;
1617
1618   label = g_strdup_printf ("%s\n<small>%s</small>", alias, status);
1619   gtk_label_set_markup (GTK_LABEL (self->priv->remote_user_name_toolbar),
1620       label);
1621   g_free (label);
1622 }
1623
1624 static void
1625 contact_name_changed_cb (EmpathyContact *contact,
1626     GParamSpec *pspec,
1627     EmpathyCallWindow *self)
1628 {
1629   set_window_title (self);
1630   set_remote_user_name (self, contact);
1631 }
1632
1633 static void
1634 contact_presence_changed_cb (EmpathyContact *contact,
1635     GParamSpec *pspec,
1636     EmpathyCallWindow *self)
1637 {
1638   set_remote_user_name (self, contact);
1639 }
1640
1641 static void
1642 contact_avatar_changed_cb (EmpathyContact *contact,
1643     GParamSpec *pspec,
1644     EmpathyCallWindow *self)
1645 {
1646   int size;
1647   GtkAllocation allocation;
1648   GtkWidget *avatar_widget;
1649
1650   avatar_widget = self->priv->remote_user_avatar_widget;
1651
1652   gtk_widget_get_allocation (avatar_widget, &allocation);
1653   size = allocation.height;
1654
1655   if (size == 0)
1656     {
1657       /* the widget is not allocated yet, set a default size */
1658       size = MIN (REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT,
1659           REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH);
1660     }
1661
1662   init_contact_avatar_with_size (contact, avatar_widget, size);
1663
1664   avatar_widget = self->priv->remote_user_avatar_toolbar;
1665
1666   gtk_widget_get_allocation (avatar_widget, &allocation);
1667   size = allocation.height;
1668
1669   if (size == 0)
1670     {
1671       /* the widget is not allocated yet, set a default size */
1672       size = SMALL_TOOLBAR_SIZE;
1673     }
1674
1675   init_contact_avatar_with_size (contact, avatar_widget, size);
1676 }
1677
1678 static void
1679 empathy_call_window_setup_avatars (EmpathyCallWindow *self,
1680     EmpathyCallHandler *handler)
1681 {
1682   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1683
1684   tp_g_signal_connect_object (priv->contact, "notify::name",
1685       G_CALLBACK (contact_name_changed_cb), self, 0);
1686   tp_g_signal_connect_object (priv->contact, "notify::avatar",
1687     G_CALLBACK (contact_avatar_changed_cb), self, 0);
1688   tp_g_signal_connect_object (priv->contact, "notify::presence",
1689       G_CALLBACK (contact_presence_changed_cb), self, 0);
1690
1691   set_window_title (self);
1692   set_remote_user_name (self, priv->contact);
1693
1694   init_contact_avatar_with_size (priv->contact,
1695       priv->remote_user_avatar_widget,
1696       MIN (REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH,
1697           REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT));
1698
1699   init_contact_avatar_with_size (priv->contact,
1700       priv->remote_user_avatar_toolbar,
1701       SMALL_TOOLBAR_SIZE);
1702
1703   /* The remote avatar is shown by default and will be hidden when we receive
1704      video from the remote side. */
1705   clutter_actor_hide (priv->video_output);
1706   gtk_widget_show (priv->remote_user_avatar_widget);
1707 }
1708
1709 static void
1710 update_send_codec (EmpathyCallWindow *self,
1711     gboolean audio)
1712 {
1713   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1714   FsCodec *codec;
1715   GtkWidget *widget;
1716   gchar *tmp;
1717
1718   if (audio)
1719     {
1720       codec = empathy_call_handler_get_send_audio_codec (priv->handler);
1721       widget = priv->acodec_encoding_label;
1722     }
1723   else
1724     {
1725       codec = empathy_call_handler_get_send_video_codec (priv->handler);
1726       widget = priv->vcodec_encoding_label;
1727     }
1728
1729   if (codec == NULL)
1730     return;
1731
1732   tmp = g_strdup_printf ("%s/%u", codec->encoding_name, codec->clock_rate);
1733   gtk_label_set_text (GTK_LABEL (widget), tmp);
1734   g_free (tmp);
1735 }
1736
1737 static void
1738 send_audio_codec_notify_cb (GObject *object,
1739     GParamSpec *pspec,
1740     gpointer user_data)
1741 {
1742   EmpathyCallWindow *self = user_data;
1743
1744   update_send_codec (self, TRUE);
1745 }
1746
1747 static void
1748 send_video_codec_notify_cb (GObject *object,
1749     GParamSpec *pspec,
1750     gpointer user_data)
1751 {
1752   EmpathyCallWindow *self = user_data;
1753
1754   update_send_codec (self, FALSE);
1755 }
1756
1757 static void
1758 update_recv_codec (EmpathyCallWindow *self,
1759     gboolean audio)
1760 {
1761   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1762   GList *codecs, *l;
1763   GtkWidget *widget;
1764   GString *str = NULL;
1765
1766   if (audio)
1767     {
1768       codecs = empathy_call_handler_get_recv_audio_codecs (priv->handler);
1769       widget = priv->acodec_decoding_label;
1770     }
1771   else
1772     {
1773       codecs = empathy_call_handler_get_recv_video_codecs (priv->handler);
1774       widget = priv->vcodec_decoding_label;
1775     }
1776
1777   if (codecs == NULL)
1778     return;
1779
1780   for (l = codecs; l != NULL; l = g_list_next (l))
1781     {
1782       FsCodec *codec = l->data;
1783
1784       if (str == NULL)
1785         str = g_string_new (NULL);
1786       else
1787         g_string_append (str, ", ");
1788
1789       g_string_append_printf (str, "%s/%u", codec->encoding_name,
1790           codec->clock_rate);
1791     }
1792
1793   gtk_label_set_text (GTK_LABEL (widget), str->str);
1794   g_string_free (str, TRUE);
1795 }
1796
1797 static void
1798 recv_audio_codecs_notify_cb (GObject *object,
1799     GParamSpec *pspec,
1800     gpointer user_data)
1801 {
1802   EmpathyCallWindow *self = user_data;
1803
1804   update_recv_codec (self, TRUE);
1805 }
1806
1807 static void
1808 recv_video_codecs_notify_cb (GObject *object,
1809     GParamSpec *pspec,
1810     gpointer user_data)
1811 {
1812   EmpathyCallWindow *self = user_data;
1813
1814   update_recv_codec (self, FALSE);
1815 }
1816
1817 static const gchar *
1818 candidate_type_to_str (FsCandidate *candidate)
1819 {
1820   switch (candidate->type)
1821     {
1822       case FS_CANDIDATE_TYPE_HOST:
1823         return "host";
1824       case FS_CANDIDATE_TYPE_SRFLX:
1825         return "server reflexive";
1826       case FS_CANDIDATE_TYPE_PRFLX:
1827         return "peer reflexive";
1828       case FS_CANDIDATE_TYPE_RELAY:
1829         return "relay";
1830       case FS_CANDIDATE_TYPE_MULTICAST:
1831         return "multicast";
1832     }
1833
1834   return NULL;
1835 }
1836
1837 static const gchar *
1838 candidate_type_to_desc (FsCandidate *candidate)
1839 {
1840   switch (candidate->type)
1841     {
1842       case FS_CANDIDATE_TYPE_HOST:
1843         return _("The IP address as seen by the machine");
1844       case FS_CANDIDATE_TYPE_SRFLX:
1845         return _("The IP address as seen by a server on the Internet");
1846       case FS_CANDIDATE_TYPE_PRFLX:
1847         return _("The IP address of the peer as seen by the other side");
1848       case FS_CANDIDATE_TYPE_RELAY:
1849         return _("The IP address of a relay server");
1850       case FS_CANDIDATE_TYPE_MULTICAST:
1851         return _("The IP address of the multicast group");
1852     }
1853
1854   return NULL;
1855 }
1856
1857 static void
1858 update_candidat_widget (EmpathyCallWindow *self,
1859     GtkWidget *label,
1860     GtkWidget *img,
1861     FsCandidate *candidate)
1862 {
1863   gchar *str;
1864
1865   g_assert (candidate != NULL);
1866   str = g_strdup_printf ("%s %u (%s)", candidate->ip,
1867       candidate->port, candidate_type_to_str (candidate));
1868
1869   gtk_label_set_text (GTK_LABEL (label), str);
1870   gtk_widget_set_tooltip_text (img, candidate_type_to_desc (candidate));
1871
1872   g_free (str);
1873 }
1874
1875 static void
1876 candidates_changed_cb (GObject *object,
1877     FsMediaType type,
1878     EmpathyCallWindow *self)
1879 {
1880   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1881   FsCandidate *candidate = NULL;
1882
1883   if (type == FS_MEDIA_TYPE_VIDEO)
1884     {
1885       /* Update remote candidate */
1886       candidate = empathy_call_handler_get_video_remote_candidate (
1887           priv->handler);
1888
1889       update_candidat_widget (self, priv->video_remote_candidate_label,
1890           priv->video_remote_candidate_info_img, candidate);
1891
1892       /* Update local candidate */
1893       candidate = empathy_call_handler_get_video_local_candidate (
1894           priv->handler);
1895
1896       update_candidat_widget (self, priv->video_local_candidate_label,
1897           priv->video_local_candidate_info_img, candidate);
1898     }
1899   else
1900     {
1901       /* Update remote candidate */
1902       candidate = empathy_call_handler_get_audio_remote_candidate (
1903           priv->handler);
1904
1905       update_candidat_widget (self, priv->audio_remote_candidate_label,
1906           priv->audio_remote_candidate_info_img, candidate);
1907
1908       /* Update local candidate */
1909       candidate = empathy_call_handler_get_audio_local_candidate (
1910           priv->handler);
1911
1912       update_candidat_widget (self, priv->audio_local_candidate_label,
1913           priv->audio_local_candidate_info_img, candidate);
1914     }
1915 }
1916
1917 static void
1918 empathy_call_window_constructed (GObject *object)
1919 {
1920   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (object);
1921   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1922   TpyCallChannel *call;
1923
1924   g_assert (priv->handler != NULL);
1925
1926   g_object_get (priv->handler, "call-channel", &call, NULL);
1927   priv->outgoing = (call == NULL);
1928   if (call != NULL)
1929     g_object_unref (call);
1930
1931   g_object_get (priv->handler, "target-contact", &priv->contact, NULL);
1932   g_assert (priv->contact != NULL);
1933
1934   empathy_call_window_setup_avatars (self, priv->handler);
1935   empathy_call_window_set_state_connecting (self);
1936
1937   if (!empathy_call_handler_has_initial_video (priv->handler))
1938     {
1939       gtk_toggle_tool_button_set_active (
1940           GTK_TOGGLE_TOOL_BUTTON (priv->camera_button), FALSE);
1941     }
1942   /* If call has InitialVideo, the preview will be started once the call has
1943    * been started (start_call()). */
1944
1945   update_send_codec (self, TRUE);
1946   update_send_codec (self, FALSE);
1947   update_recv_codec (self, TRUE);
1948   update_recv_codec (self, FALSE);
1949
1950   tp_g_signal_connect_object (priv->handler, "notify::send-audio-codec",
1951       G_CALLBACK (send_audio_codec_notify_cb), self, 0);
1952   tp_g_signal_connect_object (priv->handler, "notify::send-video-codec",
1953       G_CALLBACK (send_video_codec_notify_cb), self, 0);
1954   tp_g_signal_connect_object (priv->handler, "notify::recv-audio-codecs",
1955       G_CALLBACK (recv_audio_codecs_notify_cb), self, 0);
1956   tp_g_signal_connect_object (priv->handler, "notify::recv-video-codecs",
1957       G_CALLBACK (recv_video_codecs_notify_cb), self, 0);
1958
1959   tp_g_signal_connect_object (priv->handler, "candidates-changed",
1960       G_CALLBACK (candidates_changed_cb), self, 0);
1961 }
1962
1963 static void empathy_call_window_dispose (GObject *object);
1964 static void empathy_call_window_finalize (GObject *object);
1965
1966 static void
1967 empathy_call_window_set_property (GObject *object,
1968   guint property_id, const GValue *value, GParamSpec *pspec)
1969 {
1970   EmpathyCallWindowPriv *priv = GET_PRIV (object);
1971
1972   switch (property_id)
1973     {
1974       case PROP_CALL_HANDLER:
1975         priv->handler = g_value_dup_object (value);
1976         break;
1977       default:
1978         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
1979     }
1980 }
1981
1982 static void
1983 empathy_call_window_get_property (GObject *object,
1984   guint property_id, GValue *value, GParamSpec *pspec)
1985 {
1986   EmpathyCallWindowPriv *priv = GET_PRIV (object);
1987
1988   switch (property_id)
1989     {
1990       case PROP_CALL_HANDLER:
1991         g_value_set_object (value, priv->handler);
1992         break;
1993       default:
1994         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
1995     }
1996 }
1997
1998 static void
1999 empathy_call_window_class_init (
2000   EmpathyCallWindowClass *empathy_call_window_class)
2001 {
2002   GObjectClass *object_class = G_OBJECT_CLASS (empathy_call_window_class);
2003   GParamSpec *param_spec;
2004
2005   g_type_class_add_private (empathy_call_window_class,
2006     sizeof (EmpathyCallWindowPriv));
2007
2008   object_class->constructed = empathy_call_window_constructed;
2009   object_class->set_property = empathy_call_window_set_property;
2010   object_class->get_property = empathy_call_window_get_property;
2011
2012   object_class->dispose = empathy_call_window_dispose;
2013   object_class->finalize = empathy_call_window_finalize;
2014
2015   param_spec = g_param_spec_object ("handler",
2016     "handler", "The call handler",
2017     EMPATHY_TYPE_CALL_HANDLER,
2018     G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
2019   g_object_class_install_property (object_class,
2020     PROP_CALL_HANDLER, param_spec);
2021 }
2022
2023 void
2024 empathy_call_window_dispose (GObject *object)
2025 {
2026   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (object);
2027   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2028
2029   if (priv->dispose_has_run)
2030     return;
2031
2032   priv->dispose_has_run = TRUE;
2033
2034   if (priv->handler != NULL)
2035     {
2036       empathy_call_handler_stop_call (priv->handler);
2037       tp_clear_object (&priv->handler);
2038     }
2039
2040   if (priv->bus_message_source_id != 0)
2041     {
2042       g_source_remove (priv->bus_message_source_id);
2043       priv->bus_message_source_id = 0;
2044     }
2045
2046   if (priv->got_video_src > 0)
2047     {
2048       g_source_remove (priv->got_video_src);
2049       priv->got_video_src = 0;
2050     }
2051
2052   if (priv->inactivity_src > 0)
2053     {
2054       g_source_remove (priv->inactivity_src);
2055       priv->inactivity_src = 0;
2056     }
2057
2058   tp_clear_object (&priv->pipeline);
2059   tp_clear_object (&priv->video_input);
2060   tp_clear_object (&priv->audio_input);
2061   tp_clear_object (&priv->video_tee);
2062   tp_clear_object (&priv->ui_manager);
2063   tp_clear_object (&priv->fullscreen);
2064   tp_clear_object (&priv->camera_monitor);
2065   tp_clear_object (&priv->settings);
2066   tp_clear_object (&priv->sound_mgr);
2067   tp_clear_object (&priv->mic_menu);
2068   tp_clear_object (&priv->camera_menu);
2069
2070   g_list_free_full (priv->notifiers, g_object_unref);
2071
2072   if (priv->timer_id != 0)
2073     g_source_remove (priv->timer_id);
2074   priv->timer_id = 0;
2075
2076   if (priv->contact != NULL)
2077     {
2078       g_signal_handlers_disconnect_by_func (priv->contact,
2079           contact_name_changed_cb, self);
2080       priv->contact = NULL;
2081     }
2082
2083   G_OBJECT_CLASS (empathy_call_window_parent_class)->dispose (object);
2084 }
2085
2086 static void
2087 disconnect_video_output_motion_handler (EmpathyCallWindow *self)
2088 {
2089   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2090
2091   if (priv->video_output_motion_handler_id != 0)
2092     {
2093       g_signal_handler_disconnect (G_OBJECT (priv->video_container),
2094           priv->video_output_motion_handler_id);
2095       priv->video_output_motion_handler_id = 0;
2096     }
2097 }
2098
2099 void
2100 empathy_call_window_finalize (GObject *object)
2101 {
2102   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (object);
2103   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2104
2105   disconnect_video_output_motion_handler (self);
2106
2107   /* free any data held directly by the object here */
2108   g_mutex_free (priv->lock);
2109
2110   g_timer_destroy (priv->timer);
2111
2112   G_OBJECT_CLASS (empathy_call_window_parent_class)->finalize (object);
2113 }
2114
2115
2116 EmpathyCallWindow *
2117 empathy_call_window_new (EmpathyCallHandler *handler)
2118 {
2119   return EMPATHY_CALL_WINDOW (
2120     g_object_new (EMPATHY_TYPE_CALL_WINDOW, "handler", handler, NULL));
2121 }
2122
2123 static void
2124 empathy_call_window_conference_added_cb (EmpathyCallHandler *handler,
2125   GstElement *conference, gpointer user_data)
2126 {
2127   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2128   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2129   FsElementAddedNotifier *notifier;
2130   GKeyFile *keyfile;
2131
2132   DEBUG ("Conference added");
2133
2134   /* Add notifier to set the various element properties as needed */
2135   notifier = fs_element_added_notifier_new ();
2136   keyfile = fs_utils_get_default_element_properties (conference);
2137
2138   if (keyfile != NULL)
2139     fs_element_added_notifier_set_properties_from_keyfile (notifier, keyfile);
2140
2141   fs_element_added_notifier_add (notifier, GST_BIN (priv->pipeline));
2142
2143   priv->notifiers = g_list_prepend (priv->notifiers, notifier);
2144
2145   gst_bin_add (GST_BIN (priv->pipeline), conference);
2146   gst_element_set_state (conference, GST_STATE_PLAYING);
2147 }
2148
2149 static void
2150 empathy_call_window_conference_removed_cb (EmpathyCallHandler *handler,
2151   GstElement *conference, gpointer user_data)
2152 {
2153   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2154   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2155
2156   gst_bin_remove (GST_BIN (priv->pipeline), conference);
2157   gst_element_set_state (conference, GST_STATE_NULL);
2158 }
2159
2160 static gboolean
2161 empathy_call_window_reset_pipeline (EmpathyCallWindow *self)
2162 {
2163   GstStateChangeReturn state_change_return;
2164   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2165
2166   if (priv->pipeline == NULL)
2167     return TRUE;
2168
2169   if (priv->bus_message_source_id != 0)
2170     {
2171       g_source_remove (priv->bus_message_source_id);
2172       priv->bus_message_source_id = 0;
2173     }
2174
2175   state_change_return = gst_element_set_state (priv->pipeline, GST_STATE_NULL);
2176
2177   if (state_change_return == GST_STATE_CHANGE_SUCCESS ||
2178         state_change_return == GST_STATE_CHANGE_NO_PREROLL)
2179     {
2180       if (priv->pipeline != NULL)
2181         g_object_unref (priv->pipeline);
2182       priv->pipeline = NULL;
2183
2184       if (priv->audio_output != NULL)
2185         g_object_unref (priv->audio_output);
2186       priv->audio_output = NULL;
2187
2188       if (priv->video_tee != NULL)
2189         g_object_unref (priv->video_tee);
2190       priv->video_tee = NULL;
2191
2192       if (priv->video_preview != NULL)
2193         clutter_actor_destroy (priv->video_preview);
2194       priv->video_preview = NULL;
2195
2196       priv->funnel = NULL;
2197
2198       create_pipeline (self);
2199       /* Call will be started when user will hit the 'redial' button */
2200       priv->start_call_when_playing = FALSE;
2201       gst_element_set_state (priv->pipeline, GST_STATE_PAUSED);
2202
2203       return TRUE;
2204     }
2205   else
2206     {
2207       g_message ("Error: could not destroy pipeline. Closing call window");
2208       gtk_widget_destroy (GTK_WIDGET (self));
2209
2210       return FALSE;
2211     }
2212 }
2213
2214 static void
2215 reset_details_pane (EmpathyCallWindow *self)
2216 {
2217   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2218
2219   gtk_label_set_text (GTK_LABEL (priv->vcodec_encoding_label), _("Unknown"));
2220   gtk_label_set_text (GTK_LABEL (priv->acodec_encoding_label), _("Unknown"));
2221   gtk_label_set_text (GTK_LABEL (priv->vcodec_decoding_label), _("Unknown"));
2222   gtk_label_set_text (GTK_LABEL (priv->acodec_decoding_label), _("Unknown"));
2223 }
2224
2225 static gboolean
2226 empathy_call_window_disconnected (EmpathyCallWindow *self,
2227     gboolean restart)
2228 {
2229   gboolean could_disconnect = FALSE;
2230   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2231   gboolean could_reset_pipeline;
2232
2233   /* Leave full screen mode if needed */
2234   gtk_window_unfullscreen (GTK_WINDOW (self));
2235
2236   gtk_action_set_sensitive (priv->menu_fullscreen, FALSE);
2237   gtk_widget_set_sensitive (priv->dtmf_panel, FALSE);
2238
2239   could_reset_pipeline = empathy_call_window_reset_pipeline (self);
2240
2241   if (priv->call_state == CONNECTING)
2242       empathy_sound_manager_stop (priv->sound_mgr, EMPATHY_SOUND_PHONE_OUTGOING);
2243
2244   if (priv->call_state != REDIALING)
2245     priv->call_state = DISCONNECTED;
2246
2247   if (could_reset_pipeline)
2248     {
2249       g_mutex_lock (priv->lock);
2250
2251       g_timer_stop (priv->timer);
2252
2253       if (priv->timer_id != 0)
2254         g_source_remove (priv->timer_id);
2255       priv->timer_id = 0;
2256
2257       g_mutex_unlock (priv->lock);
2258
2259       if (!restart)
2260         /* We are about to destroy the window, no need to update it or create
2261          * a video preview */
2262         return TRUE;
2263
2264       empathy_call_window_status_message (self, _("Disconnected"));
2265
2266       empathy_call_window_show_hangup_button (self, FALSE);
2267
2268       /* Unsensitive the camera and mic button */
2269       gtk_widget_set_sensitive (priv->camera_button, FALSE);
2270       gtk_widget_set_sensitive (priv->mic_button, FALSE);
2271
2272       /* Be sure that the mic button is enabled */
2273       gtk_toggle_tool_button_set_active (
2274           GTK_TOGGLE_TOOL_BUTTON (priv->mic_button), TRUE);
2275
2276       if (priv->camera_state == CAMERA_STATE_ON)
2277         {
2278           /* Restart the preview with the new pipeline. */
2279           display_video_preview (self, TRUE);
2280         }
2281
2282       /* destroy the video output; it will be recreated when we'll redial */
2283       disconnect_video_output_motion_handler (self);
2284       if (priv->video_output != NULL)
2285         clutter_actor_destroy (priv->video_output);
2286       priv->video_output = NULL;
2287       if (priv->got_video_src > 0)
2288         {
2289           g_source_remove (priv->got_video_src);
2290           priv->got_video_src = 0;
2291         }
2292
2293       gtk_widget_show (priv->remote_user_avatar_widget);
2294
2295       reset_details_pane (self);
2296
2297       priv->sending_video = FALSE;
2298       priv->call_started = FALSE;
2299
2300       could_disconnect = TRUE;
2301
2302       /* TODO: display the self avatar of the preview (depends if the "Always
2303        * Show Video Preview" is enabled or not) */
2304     }
2305
2306   return could_disconnect;
2307 }
2308
2309
2310 static void
2311 empathy_call_window_channel_closed_cb (EmpathyCallHandler *handler,
2312     gpointer user_data)
2313 {
2314   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2315   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2316
2317   if (empathy_call_window_disconnected (self, TRUE) &&
2318       priv->call_state == REDIALING)
2319       empathy_call_window_restart_call (self);
2320 }
2321
2322 static gboolean
2323 empathy_call_window_sink_removed_cb (EmpathyCallHandler *handler,
2324     GstPad *sink,
2325     FsMediaType media_type,
2326     EmpathyCallWindow *self)
2327 {
2328   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2329
2330   DEBUG ("removing content");
2331
2332   /*
2333    * This assumes that there is only one video stream per channel...
2334    */
2335
2336   if ((guint) media_type == FS_MEDIA_TYPE_VIDEO)
2337     {
2338       if (priv->funnel != NULL)
2339         {
2340           GstElement *output;
2341
2342           output = priv->video_output_sink;
2343
2344           gst_element_set_state (output, GST_STATE_NULL);
2345           gst_element_set_state (priv->funnel, GST_STATE_NULL);
2346
2347           gst_bin_remove (GST_BIN (priv->pipeline), output);
2348           gst_bin_remove (GST_BIN (priv->pipeline), priv->funnel);
2349           priv->funnel = NULL;
2350           return TRUE;
2351         }
2352     }
2353   else if (media_type == FS_MEDIA_TYPE_AUDIO)
2354     {
2355       if (priv->audio_output != NULL)
2356         {
2357           gst_element_set_state (priv->audio_output, GST_STATE_NULL);
2358
2359           gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_output);
2360           priv->audio_output = NULL;
2361           return TRUE;
2362         }
2363     }
2364
2365   return FALSE;
2366 }
2367
2368 /* Called with global lock held */
2369 static GstPad *
2370 empathy_call_window_get_video_sink_pad (EmpathyCallWindow *self)
2371 {
2372   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2373   GstPad *pad;
2374   GstElement *output;
2375
2376   if (priv->funnel == NULL)
2377     {
2378       output = priv->video_output_sink;
2379
2380       priv->funnel = gst_element_factory_make ("fsfunnel", NULL);
2381
2382       if (!priv->funnel)
2383         {
2384           g_warning ("Could not create fsfunnel");
2385           return NULL;
2386         }
2387
2388       if (!gst_bin_add (GST_BIN (priv->pipeline), priv->funnel))
2389         {
2390           gst_object_unref (priv->funnel);
2391           priv->funnel = NULL;
2392           g_warning ("Could  not add funnel to pipeline");
2393           return NULL;
2394         }
2395
2396       if (!gst_bin_add (GST_BIN (priv->pipeline), output))
2397         {
2398           g_warning ("Could not add the video output widget to the pipeline");
2399           goto error;
2400         }
2401
2402       if (!gst_element_link (priv->funnel, output))
2403         {
2404           g_warning ("Could not link output sink to funnel");
2405           goto error_output_added;
2406         }
2407
2408       if (gst_element_set_state (output, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE)
2409         {
2410           g_warning ("Could not start video sink");
2411           goto error_output_added;
2412         }
2413
2414       if (gst_element_set_state (priv->funnel, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE)
2415         {
2416           g_warning ("Could not start funnel");
2417           goto error_output_added;
2418         }
2419     }
2420
2421   pad = gst_element_get_request_pad (priv->funnel, "sink%d");
2422
2423   if (!pad)
2424     g_warning ("Could not get request pad from funnel");
2425
2426   return pad;
2427
2428
2429  error_output_added:
2430
2431   gst_element_set_locked_state (priv->funnel, TRUE);
2432   gst_element_set_locked_state (output, TRUE);
2433
2434   gst_element_set_state (priv->funnel, GST_STATE_NULL);
2435   gst_element_set_state (output, GST_STATE_NULL);
2436
2437   gst_bin_remove (GST_BIN (priv->pipeline), output);
2438   gst_element_set_locked_state (output, FALSE);
2439
2440  error:
2441
2442   gst_bin_remove (GST_BIN (priv->pipeline), priv->funnel);
2443   priv->funnel = NULL;
2444
2445   return NULL;
2446 }
2447
2448 /* Called with global lock held */
2449 static GstPad *
2450 empathy_call_window_get_audio_sink_pad (EmpathyCallWindow *self)
2451 {
2452   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2453   GstPad *pad;
2454   GstPadTemplate *template;
2455
2456   if (priv->audio_output == NULL)
2457     {
2458       priv->audio_output = empathy_audio_sink_new ();
2459       g_object_ref_sink (priv->audio_output);
2460
2461       if (!gst_bin_add (GST_BIN (priv->pipeline), priv->audio_output))
2462         {
2463           g_warning ("Could not add audio sink to pipeline");
2464           g_object_unref (priv->audio_output);
2465           goto error_add_output;
2466         }
2467
2468       if (gst_element_set_state (priv->audio_output, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE)
2469         {
2470           g_warning ("Could not start audio sink");
2471           goto error;
2472         }
2473     }
2474
2475   template = gst_element_class_get_pad_template (
2476     GST_ELEMENT_GET_CLASS (priv->audio_output), "sink%d");
2477
2478   pad = gst_element_request_pad (priv->audio_output,
2479     template, NULL, NULL);
2480
2481   if (pad == NULL)
2482     {
2483       g_warning ("Could not get sink pad from sink");
2484       return NULL;
2485     }
2486
2487   return pad;
2488
2489 error:
2490   gst_element_set_locked_state (priv->audio_output, TRUE);
2491   gst_element_set_state (priv->audio_output, GST_STATE_NULL);
2492   gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_output);
2493   priv->audio_output = NULL;
2494
2495 error_add_output:
2496
2497   return NULL;
2498 }
2499
2500 static gboolean
2501 empathy_call_window_update_timer (gpointer user_data)
2502 {
2503   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2504   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2505   const gchar *status;
2506   gchar *str;
2507   gdouble time_;
2508
2509   time_ = g_timer_elapsed (priv->timer, NULL);
2510
2511   if (priv->call_state == HELD)
2512     status = _("On hold");
2513   else if (!gtk_toggle_tool_button_get_active (
2514       GTK_TOGGLE_TOOL_BUTTON (priv->mic_button)))
2515     status = _("Mute");
2516   else
2517     status = _("Duration");
2518
2519   /* Translators: 'status - minutes:seconds' the caller has been connected */
2520   str = g_strdup_printf (_("%s â€” %d:%02dm"),
2521       status,
2522       (int) time_ / 60, (int) time_ % 60);
2523   empathy_call_window_status_message (self, str);
2524   g_free (str);
2525
2526   return TRUE;
2527 }
2528
2529 #if 0
2530 static void
2531 display_error (EmpathyCallWindow *self,
2532     TpyCallChannel *call,
2533     const gchar *img,
2534     const gchar *title,
2535     const gchar *desc,
2536     const gchar *details)
2537 {
2538   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2539   GtkWidget *info_bar;
2540   GtkWidget *content_area;
2541   GtkWidget *hbox;
2542   GtkWidget *vbox;
2543   GtkWidget *image;
2544   GtkWidget *label;
2545   gchar *txt;
2546
2547   /* Create info bar */
2548   info_bar = gtk_info_bar_new_with_buttons (GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
2549       NULL);
2550
2551   gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_WARNING);
2552
2553   content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
2554
2555   /* hbox containing the image and the messages vbox */
2556   hbox = gtk_hbox_new (FALSE, 3);
2557   gtk_container_add (GTK_CONTAINER (content_area), hbox);
2558
2559   /* Add image */
2560   image = gtk_image_new_from_icon_name (img, GTK_ICON_SIZE_DIALOG);
2561   gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
2562
2563   /* vbox containing the main message and the details expander */
2564   vbox = gtk_vbox_new (FALSE, 3);
2565   gtk_box_pack_start (GTK_BOX (hbox), vbox, TRUE, TRUE, 0);
2566
2567   /* Add text */
2568   txt = g_strdup_printf ("<b>%s</b>\n%s", title, desc);
2569
2570   label = gtk_label_new (NULL);
2571   gtk_label_set_markup (GTK_LABEL (label), txt);
2572   gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
2573   gtk_misc_set_alignment (GTK_MISC (label), 0, 0);
2574   g_free (txt);
2575
2576   gtk_box_pack_start (GTK_BOX (vbox), label, TRUE, TRUE, 0);
2577
2578   /* Add details */
2579   if (details != NULL)
2580     {
2581       GtkWidget *expander;
2582
2583       expander = gtk_expander_new (_("Technical Details"));
2584
2585       txt = g_strdup_printf ("<i>%s</i>", details);
2586
2587       label = gtk_label_new (NULL);
2588       gtk_label_set_markup (GTK_LABEL (label), txt);
2589       gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
2590       gtk_misc_set_alignment (GTK_MISC (label), 0, 0);
2591       g_free (txt);
2592
2593       gtk_container_add (GTK_CONTAINER (expander), label);
2594       gtk_box_pack_start (GTK_BOX (vbox), expander, TRUE, TRUE, 0);
2595     }
2596
2597   g_signal_connect (info_bar, "response",
2598       G_CALLBACK (gtk_widget_destroy), NULL);
2599
2600   gtk_box_pack_start (GTK_BOX (priv->errors_vbox), info_bar,
2601       FALSE, FALSE, CONTENT_HBOX_CHILDREN_PACKING_PADDING);
2602   gtk_widget_show_all (info_bar);
2603 }
2604
2605 static gchar *
2606 media_stream_error_to_txt (EmpathyCallWindow *self,
2607     TpyCallChannel *call,
2608     gboolean audio,
2609     TpMediaStreamError error)
2610 {
2611   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2612   const gchar *cm = NULL;
2613   gchar *url;
2614   gchar *result;
2615
2616   switch (error)
2617     {
2618       case TP_MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED:
2619         if (audio)
2620           return g_strdup_printf (
2621               _("%s's software does not understand any of the audio formats "
2622                 "supported by your computer"),
2623             empathy_contact_get_alias (priv->contact));
2624         else
2625           return g_strdup_printf (
2626               _("%s's software does not understand any of the video formats "
2627                 "supported by your computer"),
2628             empathy_contact_get_alias (priv->contact));
2629
2630       case TP_MEDIA_STREAM_ERROR_CONNECTION_FAILED:
2631         return g_strdup_printf (
2632             _("Can't establish a connection to %s. "
2633               "One of you might be on a network that does not allow "
2634               "direct connections."),
2635           empathy_contact_get_alias (priv->contact));
2636
2637       case TP_MEDIA_STREAM_ERROR_NETWORK_ERROR:
2638           return g_strdup (_("There was a failure on the network"));
2639
2640       case TP_MEDIA_STREAM_ERROR_NO_CODECS:
2641         if (audio)
2642           return g_strdup (_("The audio formats necessary for this call "
2643                 "are not installed on your computer"));
2644         else
2645           return g_strdup (_("The video formats necessary for this call "
2646                 "are not installed on your computer"));
2647
2648       case TP_MEDIA_STREAM_ERROR_INVALID_CM_BEHAVIOR:
2649         tp_connection_parse_object_path (
2650             tp_channel_borrow_connection (TP_CHANNEL (call)),
2651             NULL, &cm);
2652
2653         url = g_strdup_printf ("http://bugs.freedesktop.org/enter_bug.cgi?"
2654             "product=Telepathy&amp;component=%s", cm);
2655
2656         result = g_strdup_printf (
2657             _("Something unexpected happened in a Telepathy component. "
2658               "Please <a href=\"%s\">report this bug</a> and attach "
2659               "logs gathered from the 'Debug' window in the Help menu."), url);
2660
2661         g_free (url);
2662         g_free (cm);
2663         return result;
2664
2665       case TP_MEDIA_STREAM_ERROR_MEDIA_ERROR:
2666         return g_strdup (_("There was a failure in the call engine"));
2667
2668       case TP_MEDIA_STREAM_ERROR_EOS:
2669         return g_strdup (_("The end of the stream was reached"));
2670
2671       case TP_MEDIA_STREAM_ERROR_UNKNOWN:
2672       default:
2673         return NULL;
2674     }
2675 }
2676
2677 static void
2678 empathy_call_window_stream_error (EmpathyCallWindow *self,
2679     TpyCallChannel *call,
2680     gboolean audio,
2681     guint code,
2682     const gchar *msg,
2683     const gchar *icon,
2684     const gchar *title)
2685 {
2686   gchar *desc;
2687
2688   desc = media_stream_error_to_txt (self, call, audio, code);
2689   if (desc == NULL)
2690     {
2691       /* No description, use the error message. That's not great as it's not
2692        * localized but it's better than nothing. */
2693       display_error (self, call, icon, title, msg, NULL);
2694     }
2695   else
2696     {
2697       display_error (self, call, icon, title, desc, msg);
2698       g_free (desc);
2699     }
2700 }
2701
2702 static void
2703 empathy_call_window_audio_stream_error (TpyCallChannel *call,
2704     guint code,
2705     const gchar *msg,
2706     EmpathyCallWindow *self)
2707 {
2708   empathy_call_window_stream_error (self, call, TRUE, code, msg,
2709       "gnome-stock-mic", _("Can't establish audio stream"));
2710 }
2711
2712 static void
2713 empathy_call_window_video_stream_error (TpyCallChannel *call,
2714     guint code,
2715     const gchar *msg,
2716     EmpathyCallWindow *self)
2717 {
2718   empathy_call_window_stream_error (self, call, FALSE, code, msg,
2719       "camera-web", _("Can't establish video stream"));
2720 }
2721 #endif
2722
2723 static void
2724 empathy_call_window_state_changed_cb (EmpathyCallHandler *handler,
2725     TpyCallState state,
2726     EmpathyCallWindow *self)
2727 {
2728   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2729   TpyCallChannel *call;
2730   gboolean can_send_video;
2731
2732   if (state != TPY_CALL_STATE_ACCEPTED)
2733     return;
2734
2735   if (priv->call_state == CONNECTED)
2736     return;
2737
2738   g_timer_start (priv->timer);
2739   priv->call_state = CONNECTED;
2740
2741   empathy_sound_manager_stop (priv->sound_mgr, EMPATHY_SOUND_PHONE_OUTGOING);
2742
2743   can_send_video = priv->video_input != NULL &&
2744     empathy_contact_can_voip_video (priv->contact) &&
2745     empathy_camera_monitor_get_available (priv->camera_monitor);
2746
2747   g_object_get (priv->handler, "call-channel", &call, NULL);
2748
2749   if (tpy_call_channel_has_dtmf (call))
2750     gtk_widget_set_sensitive (priv->dtmf_panel, TRUE);
2751
2752   if (priv->video_input == NULL)
2753     empathy_call_window_set_send_video (self, CAMERA_STATE_OFF);
2754
2755   gtk_widget_set_sensitive (priv->camera_button, can_send_video);
2756
2757   empathy_call_window_show_hangup_button (self, TRUE);
2758
2759   gtk_widget_set_sensitive (priv->mic_button, TRUE);
2760
2761   clutter_actor_hide (priv->video_output);
2762   gtk_widget_show (priv->remote_user_avatar_widget);
2763
2764   g_object_unref (call);
2765
2766   g_mutex_lock (priv->lock);
2767
2768   priv->timer_id = g_timeout_add_seconds (1,
2769     empathy_call_window_update_timer, self);
2770
2771   g_mutex_unlock (priv->lock);
2772
2773   empathy_call_window_update_timer (self);
2774
2775   gtk_action_set_sensitive (priv->menu_fullscreen, TRUE);
2776 }
2777
2778 static gboolean
2779 empathy_call_window_show_video_output_cb (gpointer user_data)
2780 {
2781   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2782
2783   if (self->priv->video_output != NULL)
2784     {
2785       gtk_widget_hide (self->priv->remote_user_avatar_widget);
2786       clutter_actor_show (self->priv->video_output);
2787       clutter_actor_raise_top (self->priv->floating_toolbar);
2788     }
2789
2790   return FALSE;
2791 }
2792
2793 static gboolean
2794 empathy_call_window_check_video_cb (gpointer data)
2795 {
2796   EmpathyCallWindow *self = data;
2797
2798   if (self->priv->got_video)
2799     {
2800       self->priv->got_video = FALSE;
2801       return TRUE;
2802     }
2803
2804   /* No video in the last N seconds, display the remote avatar */
2805   empathy_call_window_show_video_output (self, FALSE);
2806
2807   return TRUE;
2808 }
2809
2810 /* Called from the streaming thread */
2811 static gboolean
2812 empathy_call_window_video_probe_cb (GstPad *pad,
2813     GstMiniObject *mini_obj,
2814     EmpathyCallWindow *self)
2815 {
2816   /* Ignore events */
2817   if (GST_IS_EVENT (mini_obj))
2818     return TRUE;
2819
2820   if (G_UNLIKELY (!self->priv->got_video))
2821     {
2822       /* show the remote video */
2823       g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
2824           empathy_call_window_show_video_output_cb,
2825           g_object_ref (self), g_object_unref);
2826
2827       self->priv->got_video = TRUE;
2828     }
2829
2830   return TRUE;
2831 }
2832
2833 /* Called from the streaming thread */
2834 static gboolean
2835 empathy_call_window_src_added_cb (EmpathyCallHandler *handler,
2836   GstPad *src, guint media_type, gpointer user_data)
2837 {
2838   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2839   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2840   gboolean retval = FALSE;
2841
2842   GstPad *pad;
2843
2844   g_mutex_lock (priv->lock);
2845
2846   switch (media_type)
2847     {
2848       case TP_MEDIA_STREAM_TYPE_AUDIO:
2849         pad = empathy_call_window_get_audio_sink_pad (self);
2850         break;
2851       case TP_MEDIA_STREAM_TYPE_VIDEO:
2852         g_idle_add (empathy_call_window_show_video_output_cb, self);
2853         pad = empathy_call_window_get_video_sink_pad (self);
2854
2855         gst_pad_add_data_probe (src,
2856             G_CALLBACK (empathy_call_window_video_probe_cb), self);
2857         if (priv->got_video_src > 0)
2858           g_source_remove (priv->got_video_src);
2859         priv->got_video_src = g_timeout_add_seconds (5,
2860             empathy_call_window_check_video_cb, self);
2861         break;
2862       default:
2863         g_assert_not_reached ();
2864     }
2865
2866   if (pad == NULL)
2867     goto out;
2868
2869   if (GST_PAD_LINK_FAILED (gst_pad_link (src, pad)))
2870       g_warning ("Could not link %s sink pad",
2871           media_type == TP_MEDIA_STREAM_TYPE_AUDIO ? "audio" : "video");
2872   else
2873       retval = TRUE;
2874
2875   gst_object_unref (pad);
2876
2877  out:
2878
2879   /* If no sink could be linked, try to add fakesink to prevent the whole call
2880    * aborting */
2881
2882   if (!retval)
2883     {
2884       GstElement *fakesink = gst_element_factory_make ("fakesink", NULL);
2885
2886       if (gst_bin_add (GST_BIN (priv->pipeline), fakesink))
2887         {
2888           GstPad *sinkpad = gst_element_get_static_pad (fakesink, "sink");
2889           if (gst_element_set_state (fakesink, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE ||
2890               GST_PAD_LINK_FAILED (gst_pad_link (src, sinkpad)))
2891             {
2892               gst_element_set_locked_state (fakesink, TRUE);
2893               gst_element_set_state (fakesink, GST_STATE_NULL);
2894               gst_bin_remove (GST_BIN (priv->pipeline), fakesink);
2895             }
2896           else
2897             {
2898               DEBUG ("Could not link real sink, linked fakesink instead");
2899             }
2900           gst_object_unref (sinkpad);
2901         }
2902       else
2903         {
2904           gst_object_unref (fakesink);
2905         }
2906     }
2907
2908
2909   g_mutex_unlock (priv->lock);
2910
2911   return TRUE;
2912 }
2913
2914 static gboolean
2915 empathy_call_window_sink_added_cb (EmpathyCallHandler *handler,
2916   GstPad *sink, FsMediaType media_type, gpointer user_data)
2917 {
2918   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2919   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2920   GstPad *pad;
2921   gboolean retval = FALSE;
2922
2923   switch (media_type)
2924     {
2925       case FS_MEDIA_TYPE_AUDIO:
2926         if (!gst_bin_add (GST_BIN (priv->pipeline), priv->audio_input))
2927           {
2928             g_warning ("Could not add audio source to pipeline");
2929             break;
2930           }
2931
2932         pad = gst_element_get_static_pad (priv->audio_input, "src");
2933         if (!pad)
2934           {
2935             gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input);
2936             g_warning ("Could not get source pad from audio source");
2937             break;
2938           }
2939
2940         if (GST_PAD_LINK_FAILED (gst_pad_link (pad, sink)))
2941           {
2942             gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input);
2943             g_warning ("Could not link audio source to farsight");
2944             break;
2945           }
2946
2947         if (gst_element_set_state (priv->audio_input, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE)
2948           {
2949             g_warning ("Could not start audio source");
2950             gst_element_set_state (priv->audio_input, GST_STATE_NULL);
2951             gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input);
2952             break;
2953           }
2954
2955         retval = TRUE;
2956         break;
2957       case FS_MEDIA_TYPE_VIDEO:
2958         if (priv->video_tee != NULL)
2959           {
2960             pad = gst_element_get_request_pad (priv->video_tee, "src%d");
2961             if (GST_PAD_LINK_FAILED (gst_pad_link (pad, sink)))
2962               {
2963                 g_warning ("Could not link video source input pipeline");
2964                 break;
2965               }
2966             gst_object_unref (pad);
2967           }
2968
2969         retval = TRUE;
2970         break;
2971       default:
2972         g_assert_not_reached ();
2973     }
2974
2975   return retval;
2976 }
2977
2978 static void
2979 empathy_call_window_remove_video_input (EmpathyCallWindow *self)
2980 {
2981   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2982   GstElement *preview;
2983
2984   disable_camera (self);
2985
2986   DEBUG ("remove video input");
2987   preview = priv->video_preview_sink;
2988
2989   gst_element_set_state (priv->video_input, GST_STATE_NULL);
2990   gst_element_set_state (priv->video_tee, GST_STATE_NULL);
2991   gst_element_set_state (preview, GST_STATE_NULL);
2992
2993   gst_bin_remove_many (GST_BIN (priv->pipeline), priv->video_input,
2994     preview, NULL);
2995
2996   g_object_unref (priv->video_input);
2997   priv->video_input = NULL;
2998   g_object_unref (priv->video_tee);
2999   priv->video_tee = NULL;
3000   clutter_actor_destroy (priv->video_preview);
3001   priv->video_preview = NULL;
3002
3003   gtk_widget_set_sensitive (priv->camera_button, FALSE);
3004 }
3005
3006 static void
3007 start_call (EmpathyCallWindow *self)
3008 {
3009   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3010
3011   priv->call_started = TRUE;
3012   empathy_call_handler_start_call (priv->handler,
3013       gtk_get_current_event_time ());
3014
3015   if (empathy_call_handler_has_initial_video (priv->handler))
3016     {
3017       TpyCallChannel *call;
3018       TpySendingState s;
3019
3020       g_object_get (priv->handler, "call-channel", &call, NULL);
3021       s = tpy_call_channel_get_video_state (call);
3022
3023       if (s == TPY_SENDING_STATE_PENDING_SEND ||
3024           s == TPY_SENDING_STATE_SENDING)
3025         {
3026           /* Enable 'send video' buttons and display the preview */
3027           gtk_toggle_tool_button_set_active (
3028             GTK_TOGGLE_TOOL_BUTTON (priv->camera_button), TRUE);
3029         }
3030       else
3031         {
3032           gtk_toggle_tool_button_set_active (
3033             GTK_TOGGLE_TOOL_BUTTON (priv->camera_button), FALSE);
3034
3035           if (priv->video_preview == NULL)
3036             {
3037               create_video_preview (self);
3038               add_video_preview_to_pipeline (self);
3039             }
3040         }
3041
3042       g_object_unref (call);
3043     }
3044 }
3045
3046 static gboolean
3047 empathy_call_window_bus_message (GstBus *bus, GstMessage *message,
3048   gpointer user_data)
3049 {
3050   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
3051   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3052   GstState newstate;
3053
3054   empathy_call_handler_bus_message (priv->handler, bus, message);
3055
3056   switch (GST_MESSAGE_TYPE (message))
3057     {
3058       case GST_MESSAGE_STATE_CHANGED:
3059         if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->video_input))
3060           {
3061             gst_message_parse_state_changed (message, NULL, &newstate, NULL);
3062           }
3063         if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->pipeline) &&
3064             !priv->call_started)
3065           {
3066             gst_message_parse_state_changed (message, NULL, &newstate, NULL);
3067             if (newstate == GST_STATE_PAUSED)
3068               {
3069                 gst_element_set_state (priv->pipeline, GST_STATE_PLAYING);
3070                 priv->pipeline_playing = TRUE;
3071
3072                 if (priv->start_call_when_playing)
3073                   start_call (self);
3074               }
3075           }
3076         break;
3077       case GST_MESSAGE_ERROR:
3078         {
3079           GError *error = NULL;
3080           GstElement *gst_error;
3081           gchar *debug;
3082
3083           gst_message_parse_error (message, &error, &debug);
3084           gst_error = GST_ELEMENT (GST_MESSAGE_SRC (message));
3085
3086           g_message ("Element error: %s -- %s\n", error->message, debug);
3087
3088           if (g_str_has_prefix (gst_element_get_name (gst_error),
3089                 VIDEO_INPUT_ERROR_PREFIX))
3090             {
3091               /* Remove the video input and continue */
3092               if (priv->video_input != NULL)
3093                 empathy_call_window_remove_video_input (self);
3094               gst_element_set_state (priv->pipeline, GST_STATE_PLAYING);
3095             }
3096           else
3097             {
3098               empathy_call_window_disconnected (self, TRUE);
3099             }
3100           g_error_free (error);
3101           g_free (debug);
3102         }
3103       case GST_MESSAGE_UNKNOWN:
3104       case GST_MESSAGE_EOS:
3105       case GST_MESSAGE_WARNING:
3106       case GST_MESSAGE_INFO:
3107       case GST_MESSAGE_TAG:
3108       case GST_MESSAGE_BUFFERING:
3109       case GST_MESSAGE_STATE_DIRTY:
3110       case GST_MESSAGE_STEP_DONE:
3111       case GST_MESSAGE_CLOCK_PROVIDE:
3112       case GST_MESSAGE_CLOCK_LOST:
3113       case GST_MESSAGE_NEW_CLOCK:
3114       case GST_MESSAGE_STRUCTURE_CHANGE:
3115       case GST_MESSAGE_STREAM_STATUS:
3116       case GST_MESSAGE_APPLICATION:
3117       case GST_MESSAGE_ELEMENT:
3118       case GST_MESSAGE_SEGMENT_START:
3119       case GST_MESSAGE_SEGMENT_DONE:
3120       case GST_MESSAGE_DURATION:
3121       case GST_MESSAGE_ANY:
3122       default:
3123         break;
3124     }
3125
3126   return TRUE;
3127 }
3128
3129 static void
3130 empathy_call_window_members_changed_cb (TpyCallChannel *call,
3131     GHashTable *members,
3132     EmpathyCallWindow *self)
3133 {
3134   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3135   GHashTableIter iter;
3136   gpointer key, value;
3137   gboolean held = FALSE;
3138
3139   g_hash_table_iter_init (&iter, members);
3140   while (g_hash_table_iter_next (&iter, &key, &value))
3141     {
3142       if (GPOINTER_TO_INT (value) & TPY_CALL_MEMBER_FLAG_HELD)
3143         {
3144           /* This assumes this is a 1-1 call, otherwise one participant
3145            * putting the call on hold wouldn't mean the call is on hold
3146            * for everyone. */
3147           held = TRUE;
3148           break;
3149         }
3150     }
3151
3152   if (held)
3153     priv->call_state = HELD;
3154   else if (priv->call_state == HELD)
3155     priv->call_state = CONNECTED;
3156 }
3157
3158 static void
3159 call_handler_notify_call_cb (EmpathyCallHandler *handler,
3160     GParamSpec *spec,
3161     EmpathyCallWindow *self)
3162 {
3163   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3164   TpyCallChannel *call;
3165
3166   g_object_get (priv->handler, "call-channel", &call, NULL);
3167   if (call == NULL)
3168     return;
3169
3170 /* FIXME
3171   tp_g_signal_connect_object (call, "audio-stream-error",
3172       G_CALLBACK (empathy_call_window_audio_stream_error), self, 0);
3173   tp_g_signal_connect_object (call, "video-stream-error",
3174       G_CALLBACK (empathy_call_window_video_stream_error), self, 0);
3175 */
3176
3177   tp_g_signal_connect_object (call, "members-changed",
3178       G_CALLBACK (empathy_call_window_members_changed_cb), self, 0);
3179
3180   g_object_unref (call);
3181 }
3182
3183 static void
3184 empathy_call_window_realized_cb (GtkWidget *widget, EmpathyCallWindow *window)
3185 {
3186   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3187   TpyCallChannel *call;
3188   gint width;
3189
3190   /* Make the hangup button twice as wide */
3191   width = gtk_widget_get_allocated_width (priv->hangup_button);
3192   gtk_widget_set_size_request (priv->hangup_button, width * 2, -1);
3193
3194   g_signal_connect (priv->handler, "state-changed",
3195     G_CALLBACK (empathy_call_window_state_changed_cb), window);
3196   g_signal_connect (priv->handler, "conference-added",
3197     G_CALLBACK (empathy_call_window_conference_added_cb), window);
3198   g_signal_connect (priv->handler, "conference-removed",
3199     G_CALLBACK (empathy_call_window_conference_removed_cb), window);
3200   g_signal_connect (priv->handler, "closed",
3201     G_CALLBACK (empathy_call_window_channel_closed_cb), window);
3202   g_signal_connect (priv->handler, "src-pad-added",
3203     G_CALLBACK (empathy_call_window_src_added_cb), window);
3204   g_signal_connect (priv->handler, "sink-pad-added",
3205     G_CALLBACK (empathy_call_window_sink_added_cb), window);
3206   g_signal_connect (priv->handler, "sink-pad-removed",
3207     G_CALLBACK (empathy_call_window_sink_removed_cb), window);
3208
3209   g_object_get (priv->handler, "call-channel", &call, NULL);
3210   if (call != NULL)
3211     {
3212       call_handler_notify_call_cb (priv->handler, NULL, window);
3213       g_object_unref (call);
3214     }
3215   else
3216     {
3217       /* call-channel doesn't exist yet, we'll connect signals once it has been
3218        * set */
3219       g_signal_connect (priv->handler, "notify::call-channel",
3220         G_CALLBACK (call_handler_notify_call_cb), window);
3221     }
3222
3223   gst_element_set_state (priv->pipeline, GST_STATE_PAUSED);
3224 }
3225
3226 static gboolean
3227 empathy_call_window_delete_cb (GtkWidget *widget, GdkEvent*event,
3228   EmpathyCallWindow *window)
3229 {
3230   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3231
3232   if (priv->pipeline != NULL)
3233     {
3234       if (priv->bus_message_source_id != 0)
3235         {
3236           g_source_remove (priv->bus_message_source_id);
3237           priv->bus_message_source_id = 0;
3238         }
3239
3240       gst_element_set_state (priv->pipeline, GST_STATE_NULL);
3241     }
3242
3243   if (priv->call_state == CONNECTING)
3244     empathy_sound_manager_stop (priv->sound_mgr, EMPATHY_SOUND_PHONE_OUTGOING);
3245
3246   return FALSE;
3247 }
3248
3249 static void
3250 show_controls (EmpathyCallWindow *window, gboolean set_fullscreen)
3251 {
3252   GtkWidget *menu;
3253   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3254
3255   menu = gtk_ui_manager_get_widget (priv->ui_manager,
3256             "/menubar1");
3257
3258   if (set_fullscreen)
3259     {
3260       gtk_widget_hide (priv->dtmf_panel);
3261       gtk_widget_hide (menu);
3262       gtk_widget_hide (priv->toolbar);
3263     }
3264   else
3265     {
3266       if (priv->dialpad_was_visible_before_fs)
3267         gtk_widget_show (priv->dtmf_panel);
3268
3269       gtk_widget_show (menu);
3270       gtk_widget_show (priv->toolbar);
3271
3272       gtk_window_resize (GTK_WINDOW (window), priv->original_width_before_fs,
3273           priv->original_height_before_fs);
3274     }
3275 }
3276
3277 static void
3278 show_borders (EmpathyCallWindow *window, gboolean set_fullscreen)
3279 {
3280   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3281
3282   gtk_container_set_border_width (GTK_CONTAINER (priv->content_hbox),
3283       set_fullscreen ? 0 : CONTENT_HBOX_BORDER_WIDTH);
3284   gtk_box_set_spacing (GTK_BOX (priv->content_hbox),
3285       set_fullscreen ? 0 : CONTENT_HBOX_SPACING);
3286
3287   if (priv->video_output != NULL)
3288     {
3289 #if 0
3290       gtk_box_set_child_packing (GTK_BOX (priv->content_hbox),
3291           priv->video_output, TRUE, TRUE,
3292           set_fullscreen ? 0 : CONTENT_HBOX_CHILDREN_PACKING_PADDING,
3293           GTK_PACK_START);
3294 #endif
3295     }
3296 }
3297
3298 static gboolean
3299 empathy_call_window_state_event_cb (GtkWidget *widget,
3300   GdkEventWindowState *event, EmpathyCallWindow *window)
3301 {
3302   if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN)
3303     {
3304       EmpathyCallWindowPriv *priv = GET_PRIV (window);
3305       gboolean set_fullscreen = event->new_window_state &
3306         GDK_WINDOW_STATE_FULLSCREEN;
3307
3308       if (set_fullscreen)
3309         {
3310           gboolean dialpad_was_visible;
3311           GtkAllocation allocation;
3312           gint original_width, original_height;
3313
3314           gtk_widget_get_allocation (GTK_WIDGET (window), &allocation);
3315           original_width = allocation.width;
3316           original_height = allocation.height;
3317
3318           g_object_get (priv->dtmf_panel,
3319               "visible", &dialpad_was_visible,
3320               NULL);
3321
3322           priv->dialpad_was_visible_before_fs = dialpad_was_visible;
3323           priv->original_width_before_fs = original_width;
3324           priv->original_height_before_fs = original_height;
3325
3326           if (priv->video_output_motion_handler_id == 0 &&
3327                 priv->video_output != NULL)
3328             {
3329               priv->video_output_motion_handler_id = g_signal_connect (
3330                   G_OBJECT (priv->video_container), "motion-notify-event",
3331                   G_CALLBACK (empathy_call_window_video_output_motion_notify),
3332                   window);
3333             }
3334         }
3335       else
3336         {
3337           disconnect_video_output_motion_handler (window);
3338         }
3339
3340       empathy_call_window_fullscreen_set_fullscreen (priv->fullscreen,
3341           set_fullscreen);
3342       show_controls (window, set_fullscreen);
3343       show_borders (window, set_fullscreen);
3344       gtk_action_set_stock_id (priv->menu_fullscreen,
3345           (set_fullscreen ? "gtk-leave-fullscreen" : "gtk-fullscreen"));
3346       priv->is_fullscreen = set_fullscreen;
3347   }
3348
3349   return FALSE;
3350 }
3351
3352 static void
3353 empathy_call_window_show_dialpad (EmpathyCallWindow *window,
3354     gboolean active)
3355 {
3356   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3357   int w, h, dialpad_width;
3358   GtkAllocation allocation;
3359
3360   gtk_widget_get_allocation (GTK_WIDGET (window), &allocation);
3361   w = allocation.width;
3362   h = allocation.height;
3363
3364   gtk_widget_get_preferred_width (priv->dtmf_panel, &dialpad_width, NULL);
3365
3366   if (active)
3367     {
3368       gtk_widget_show (priv->dtmf_panel);
3369       w += dialpad_width;
3370     }
3371   else
3372     {
3373       w -= dialpad_width;
3374       gtk_widget_hide (priv->dtmf_panel);
3375     }
3376
3377   if (w > 0 && h > 0)
3378     gtk_window_resize (GTK_WINDOW (window), w, h);
3379 }
3380
3381 static void
3382 empathy_call_window_set_send_video (EmpathyCallWindow *window,
3383   CameraState state)
3384 {
3385   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3386   TpyCallChannel *call;
3387
3388   priv->sending_video = (state == CAMERA_STATE_ON);
3389
3390   if (state == CAMERA_STATE_ON)
3391     {
3392       /* When we start sending video, we want to show the video preview by
3393          default. */
3394       display_video_preview (window, TRUE);
3395     }
3396   else
3397     {
3398       display_video_preview (window, FALSE);
3399     }
3400
3401   if (priv->call_state != CONNECTED)
3402     return;
3403
3404   g_object_get (priv->handler, "call-channel", &call, NULL);
3405   DEBUG ("%s sending video", priv->sending_video ? "start": "stop");
3406   tpy_call_channel_send_video (call, priv->sending_video);
3407   g_object_unref (call);
3408 }
3409
3410 static void
3411 empathy_call_window_mic_toggled_cb (GtkToggleToolButton *toggle,
3412   EmpathyCallWindow *self)
3413 {
3414   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3415   gboolean active;
3416
3417   active = (gtk_toggle_tool_button_get_active (toggle));
3418
3419   /* We don't want the settings callback to react to this change to avoid
3420    * a loop. */
3421   g_signal_handlers_block_by_func (priv->settings,
3422       empathy_call_window_prefs_volume_changed_cb, self);
3423
3424   if (active)
3425     {
3426       g_settings_set_double (priv->settings, EMPATHY_PREFS_CALL_SOUND_VOLUME,
3427           priv->volume * 100);
3428     }
3429   else
3430     {
3431       /* TODO, Instead of setting the input volume to 0 we should probably
3432        * stop sending but this would cause the audio call to drop if both
3433        * sides mute at the same time on certain CMs AFAIK. Need to revisit this
3434        * in the future. GNOME #574574
3435        */
3436       g_settings_set_double (priv->settings, EMPATHY_PREFS_CALL_SOUND_VOLUME,
3437           0);
3438     }
3439
3440     g_signal_handlers_unblock_by_func (priv->settings,
3441       empathy_call_window_prefs_volume_changed_cb, self);
3442 }
3443
3444 static void
3445 empathy_call_window_hangup_cb (gpointer object,
3446     EmpathyCallWindow *self)
3447 {
3448   empathy_call_handler_stop_call (self->priv->handler);
3449
3450   empathy_call_window_disconnected (self, TRUE);
3451 }
3452
3453 static void
3454 empathy_call_window_restart_call (EmpathyCallWindow *window)
3455 {
3456   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3457
3458   /* Remove error info bars */
3459   gtk_container_forall (GTK_CONTAINER (priv->errors_vbox),
3460       (GtkCallback) gtk_widget_destroy, NULL);
3461
3462   create_video_output_widget (window);
3463
3464   /* While the call was disconnected, the input volume might have changed.
3465    * However, since the audio_input source was destroyed, its volume has not
3466    * been updated during that time. That's why we manually update it here */
3467   empathy_call_window_mic_volume_changed (window);
3468
3469   priv->outgoing = TRUE;
3470   empathy_call_window_set_state_connecting (window);
3471
3472   if (priv->pipeline_playing)
3473     start_call (window);
3474   else
3475     /* call will be started when the pipeline is ready */
3476     priv->start_call_when_playing = TRUE;
3477
3478   empathy_call_window_setup_avatars (window, priv->handler);
3479
3480   empathy_call_window_show_hangup_button (window, TRUE);
3481 }
3482
3483 static void
3484 empathy_call_window_dialpad_cb (GtkToggleToolButton *button,
3485     EmpathyCallWindow *window)
3486 {
3487   gboolean active;
3488
3489   active = gtk_toggle_tool_button_get_active (button);
3490
3491   empathy_call_window_show_dialpad (window, active);
3492 }
3493
3494 static void
3495 empathy_call_window_fullscreen_cb (gpointer object,
3496                                    EmpathyCallWindow *window)
3497 {
3498   empathy_call_window_fullscreen_toggle (window);
3499 }
3500
3501 static void
3502 empathy_call_window_fullscreen_toggle (EmpathyCallWindow *window)
3503 {
3504   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3505
3506   if (priv->is_fullscreen)
3507     gtk_window_unfullscreen (GTK_WINDOW (window));
3508   else
3509     gtk_window_fullscreen (GTK_WINDOW (window));
3510 }
3511
3512 static gboolean
3513 empathy_call_window_video_button_press_cb (GtkWidget *video_preview,
3514   GdkEventButton *event, EmpathyCallWindow *window)
3515 {
3516   if (event->button == 3 && event->type == GDK_BUTTON_PRESS)
3517     {
3518       empathy_call_window_video_menu_popup (window, event->button);
3519       return TRUE;
3520     }
3521
3522   return FALSE;
3523 }
3524
3525 static gboolean
3526 empathy_call_window_key_press_cb (GtkWidget *video_output,
3527   GdkEventKey *event, EmpathyCallWindow *window)
3528 {
3529   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3530
3531   if (priv->is_fullscreen && event->keyval == GDK_KEY_Escape)
3532     {
3533       /* Since we are in fullscreen mode, toggling will bring us back to
3534          normal mode. */
3535       empathy_call_window_fullscreen_toggle (window);
3536       return TRUE;
3537     }
3538
3539   return FALSE;
3540 }
3541
3542 static gboolean
3543 empathy_call_window_video_output_motion_notify (GtkWidget *widget,
3544     GdkEventMotion *event, EmpathyCallWindow *window)
3545 {
3546   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3547
3548   if (priv->is_fullscreen)
3549     {
3550       empathy_call_window_fullscreen_show_popup (priv->fullscreen);
3551
3552       /* Show the bottom toolbar */
3553       empathy_call_window_motion_notify_cb (NULL, NULL, window);
3554       return TRUE;
3555     }
3556   return FALSE;
3557 }
3558
3559 static void
3560 empathy_call_window_video_menu_popup (EmpathyCallWindow *window,
3561   guint button)
3562 {
3563   GtkWidget *menu;
3564   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3565
3566   menu = gtk_ui_manager_get_widget (priv->ui_manager,
3567             "/video-popup");
3568   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
3569       button, gtk_get_current_event_time ());
3570   gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE);
3571 }
3572
3573 static void
3574 empathy_call_window_status_message (EmpathyCallWindow *self,
3575   gchar *message)
3576 {
3577   gtk_label_set_label (GTK_LABEL (self->priv->status_label), message);
3578 }
3579
3580 static void
3581 empathy_call_window_volume_changed_cb (GtkScaleButton *button,
3582   gdouble value, EmpathyCallWindow *window)
3583 {
3584   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3585
3586   if (priv->audio_output == NULL)
3587     return;
3588
3589   empathy_audio_sink_set_volume (EMPATHY_GST_AUDIO_SINK (priv->audio_output),
3590     value);
3591 }
3592
3593 GtkUIManager *
3594 empathy_call_window_get_ui_manager (EmpathyCallWindow *window)
3595 {
3596   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3597
3598   return priv->ui_manager;
3599 }
3600
3601 EmpathyGstAudioSrc *
3602 empathy_call_window_get_audio_src (EmpathyCallWindow *window)
3603 {
3604   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3605
3606   return (EmpathyGstAudioSrc *) priv->audio_input;
3607 }
3608
3609 EmpathyGstVideoSrc *
3610 empathy_call_window_get_video_src (EmpathyCallWindow *self)
3611 {
3612   return EMPATHY_GST_VIDEO_SRC (self->priv->video_input);
3613 }