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