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