diff options
-rw-r--r-- | configure.ac | 2 | ||||
-rw-r--r-- | src/Makefile.am | 8 | ||||
-rw-r--r-- | src/empathy-audio-src.c | 496 | ||||
-rw-r--r-- | src/empathy-audio-src.h | 21 | ||||
-rw-r--r-- | src/empathy-call-window.c | 21 | ||||
-rw-r--r-- | src/empathy-call-window.h | 5 | ||||
-rw-r--r-- | src/empathy-call-window.ui | 16 | ||||
-rw-r--r-- | src/empathy-mic-menu.c | 420 | ||||
-rw-r--r-- | src/empathy-mic-menu.h | 56 |
9 files changed, 1021 insertions, 24 deletions
diff --git a/configure.ac b/configure.ac index 70bc999b4..8646a1aa1 100644 --- a/configure.ac +++ b/configure.ac @@ -174,6 +174,8 @@ PKG_CHECK_MODULES(EMPATHY, libcanberra-gtk3 >= $LIBCANBERRA_GTK_REQUIRED libnotify >= $LIBNOTIFY_REQUIRED gcr-3 >= $GCR_REQUIRED + libpulse + libpulse-mainloop-glib ]) PKG_CHECK_MODULES(YELL, [telepathy-yell]) diff --git a/src/Makefile.am b/src/Makefile.am index 2a34494eb..c232652c1 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -168,7 +168,9 @@ empathy_call_SOURCES = \ empathy-video-widget.c \ empathy-video-widget.h \ ev-sidebar.c \ - ev-sidebar.h + ev-sidebar.h \ + empathy-mic-menu.c \ + empathy-mic-menu.h nodist_empathy_call_SOURCES = $(BUILT_SOURCES) @@ -262,10 +264,10 @@ dist_man_MANS = \ empathy.1 \ empathy-accounts.1 -src-marshal.list: $(empathy_SOURCES) Makefile.am +src-marshal.list: $(empathy_SOURCES) $(empathy_call_SOURCES) Makefile.am $(AM_V_GEN)( cd $(srcdir) && \ sed -n -e 's/.*src_marshal_\([[:upper:][:digit:]]*__[[:upper:][:digit:]_]*\).*/\1/p' \ - $(empathy_SOURCES) $(empathy_av_SOURCES) ) \ + $(empathy_SOURCES) $(empathy_av_SOURCES) $(empathy_call_SOURCES) ) \ | sed -e 's/__/:/' -e 'y/_/,/' | sort -u > $@.tmp @if cmp -s $@.tmp $@; then \ rm $@.tmp; \ diff --git a/src/empathy-audio-src.c b/src/empathy-audio-src.c index a3416f2ea..642628d7b 100644 --- a/src/empathy-audio-src.c +++ b/src/empathy-audio-src.c @@ -22,9 +22,18 @@ #include <stdio.h> #include <stdlib.h> -#include <gst/farsight/fs-element-added-notifier.h> +#include <pulse/pulseaudio.h> +#include <pulse/glib-mainloop.h> + +#include <libempathy/empathy-utils.h> + #include "empathy-audio-src.h" +#include "src-marshal.h" + +#define DEBUG_FLAG EMPATHY_DEBUG_VOIP +#include <libempathy/empathy-debug.h> + G_DEFINE_TYPE(EmpathyGstAudioSrc, empathy_audio_src, GST_TYPE_BIN) /* signal enum */ @@ -32,6 +41,8 @@ enum { PEAK_LEVEL_CHANGED, RMS_LEVEL_CHANGED, + MICROPHONE_ADDED, + MICROPHONE_REMOVED, LAST_SIGNAL }; @@ -41,6 +52,7 @@ enum { PROP_VOLUME = 1, PROP_RMS_LEVEL, PROP_PEAK_LEVEL, + PROP_MICROPHONE, }; /* private structure */ @@ -52,7 +64,15 @@ struct _EmpathyGstAudioSrcPrivate GstElement *src; GstElement *volume; GstElement *level; - FsElementAddedNotifier *notifier; + + pa_glib_mainloop *loop; + pa_context *context; + GQueue *operations; + + /* 0 if not known yet */ + guint source_output_idx; + /* G_MAXUINT if not known yet */ + guint source_idx; gdouble peak_level; gdouble rms_level; @@ -65,46 +85,315 @@ struct _EmpathyGstAudioSrcPrivate (G_TYPE_INSTANCE_GET_PRIVATE ((o), EMPATHY_TYPE_GST_AUDIO_SRC, \ EmpathyGstAudioSrcPrivate)) +static gboolean +empathy_audio_src_supports_changing_mic (EmpathyGstAudioSrc *self) +{ + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (self); + GObjectClass *object_class; + + object_class = G_OBJECT_GET_CLASS (priv->src); + + return (g_object_class_find_property (object_class, + "source-output-index") != NULL); +} + +typedef void (*OperationFunc) (EmpathyGstAudioSrc *, GSimpleAsyncResult *); + +typedef struct +{ + OperationFunc func; + GSimpleAsyncResult *result; +} Operation; + +static Operation * +operation_new (OperationFunc func, + GSimpleAsyncResult *result) +{ + Operation *o = g_slice_new0 (Operation); + + o->func = func; + o->result = result; + + return o; +} + +static void +operation_free (Operation *o, + gboolean cancelled) +{ + if (cancelled) + { + g_simple_async_result_set_error (o->result, + G_IO_ERROR, G_IO_ERROR_CANCELLED, + "The audio source was disposed"); + g_simple_async_result_complete (o->result); + g_object_unref (o->result); + } + + g_slice_free (Operation, o); +} + +static void +operation_get_microphones_free (gpointer data) +{ + GQueue *queue = data; + GList *l; + + for (l = queue->head; l != NULL; l = l->next) + { + EmpathyAudioSrcMicrophone *mic = l->data; + + g_free (mic->name); + g_free (mic->description); + g_slice_free (EmpathyAudioSrcMicrophone, mic); + } + + g_queue_free (queue); +} + +static void +operation_get_microphones_cb (pa_context *context, + const pa_source_info *info, + int eol, + void *userdata) +{ + GSimpleAsyncResult *result = userdata; + EmpathyAudioSrcMicrophone *mic; + GQueue *queue; + + if (eol) + { + g_simple_async_result_complete (result); + g_object_unref (result); + return; + } + + mic = g_slice_new0 (EmpathyAudioSrcMicrophone); + mic->index = info->index; + mic->name = g_strdup (info->name); + mic->description = g_strdup (info->description); + mic->is_monitor = (info->monitor_of_sink != PA_INVALID_INDEX); + + /* add it to the queue */ + queue = g_simple_async_result_get_op_res_gpointer (result); + g_queue_push_tail (queue, mic); +} + +static void +operation_get_microphones (EmpathyGstAudioSrc *self, + GSimpleAsyncResult *result) +{ + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (self); + + g_assert_cmpuint (pa_context_get_state (priv->context), ==, PA_CONTEXT_READY); + + g_simple_async_result_set_op_res_gpointer (result, g_queue_new (), + operation_get_microphones_free); + + pa_context_get_source_info_list (priv->context, + operation_get_microphones_cb, result); +} + +static void +operation_change_microphone_cb (pa_context *context, + int success, + void *userdata) +{ + GSimpleAsyncResult *result = userdata; + + if (!success) + { + g_simple_async_result_set_error (result, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to change microphone. Reason unknown."); + } + + g_simple_async_result_complete (result); + g_object_unref (result); +} + +static void +operation_change_microphone (EmpathyGstAudioSrc *self, + GSimpleAsyncResult *result) +{ + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (self); + guint source_output_idx, microphone; + + g_object_get (priv->src, "source-output-index", &source_output_idx, NULL); + + g_assert_cmpuint (pa_context_get_state (priv->context), ==, PA_CONTEXT_READY); + g_assert_cmpuint (source_output_idx, !=, PA_INVALID_INDEX); + + microphone = GPOINTER_TO_UINT ( + g_simple_async_result_get_op_res_gpointer (result)); + + pa_context_move_source_output_by_index (priv->context, source_output_idx, microphone, + operation_change_microphone_cb, result); +} + +static void +operations_run (EmpathyGstAudioSrc *self) +{ + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (self); + pa_context_state_t state = pa_context_get_state (priv->context); + GList *l; + + if (state != PA_CONTEXT_READY) + return; + + for (l = priv->operations->head; l != NULL; l = l->next) + { + Operation *o = l->data; + + o->func (self, o->result); + + operation_free (o, FALSE); + } + + g_queue_clear (priv->operations); +} + +static void +empathy_audio_src_source_output_info_cb (pa_context *context, + const pa_source_output_info *info, + int eol, + void *userdata) +{ + EmpathyGstAudioSrc *self = userdata; + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (self); + + if (eol) + return; + + /* There should only be one call here. */ + + if (priv->source_idx == info->source) + return; + + priv->source_idx = info->source; + g_object_notify (G_OBJECT (self), "microphone"); +} + +static void +empathy_audio_src_source_info_cb (pa_context *context, + const pa_source_info *info, + int eol, + void *userdata) +{ + EmpathyGstAudioSrc *self = userdata; + gboolean is_monitor; + + if (eol) + return; + + is_monitor = (info->monitor_of_sink != PA_INVALID_INDEX); + + g_signal_emit (self, signals[MICROPHONE_ADDED], 0, + info->index, info->name, info->description, is_monitor); +} + static void -empathy_audio_src_element_added_cb (FsElementAddedNotifier *notifier, - GstBin *bin, GstElement *element, EmpathyGstAudioSrc *self) +empathy_audio_src_pa_event_cb (pa_context *context, + pa_subscription_event_type_t type, + uint32_t idx, + void *userdata) { + EmpathyGstAudioSrc *self = userdata; EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (self); - if (g_object_class_find_property (G_OBJECT_GET_CLASS (element), "volume")) + if ((type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT + && (type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE + && idx == priv->source_output_idx) + { + /* Microphone in the source output has changed */ + pa_context_get_source_output_info (context, idx, + empathy_audio_src_source_output_info_cb, self); + } + else if ((type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE + && (type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) + { + /* A mic has been removed */ + g_signal_emit (self, signals[MICROPHONE_REMOVED], 0, idx); + } + else if ((type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE + && (type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) { - gdouble volume; + /* A mic has been plugged in */ + pa_context_get_source_info_by_index (context, idx, + empathy_audio_src_source_info_cb, self); + } +} - volume = empathy_audio_src_get_volume (self); - empathy_audio_src_set_volume (self, 1.0); +static void +empathy_audio_src_pa_subscribe_cb (pa_context *context, + int success, + void *userdata) +{ + if (!success) + DEBUG ("Failed to subscribe to PulseAudio events"); +} - if (priv->volume != NULL) - g_object_unref (priv->volume); - priv->volume = g_object_ref (element); +static void +empathy_audio_src_pa_state_change_cb (pa_context *context, + void *userdata) +{ + EmpathyGstAudioSrc *self = userdata; + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (self); + pa_context_state_t state = pa_context_get_state (priv->context); - if (volume != 1.0) - empathy_audio_src_set_volume (self, volume); + if (state == PA_CONTEXT_READY) + { + /* Listen to pulseaudio events so we know when sources are + * added and when the microphone is changed. */ + pa_context_set_subscribe_callback (priv->context, + empathy_audio_src_pa_event_cb, self); + pa_context_subscribe (priv->context, + PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT, + empathy_audio_src_pa_subscribe_cb, NULL); + + operations_run (self); } } static void +empathy_audio_src_source_output_index_notify (GObject *object, + GParamSpec *pspec, + EmpathyGstAudioSrc *self) +{ + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (self); + guint source_output_idx = PA_INVALID_INDEX; + + g_object_get (priv->src, "source-output-index", &source_output_idx, NULL); + + if (source_output_idx == PA_INVALID_INDEX) + return; + + if (priv->source_output_idx == source_output_idx) + return; + + /* It's actually changed. */ + priv->source_output_idx = source_output_idx; + + pa_context_get_source_output_info (priv->context, source_output_idx, + empathy_audio_src_source_output_info_cb, self); +} + +static void empathy_audio_src_init (EmpathyGstAudioSrc *obj) { EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (obj); GstPad *ghost, *src; + const gchar *src_element; priv->peak_level = -G_MAXDOUBLE; priv->lock = g_mutex_new (); - priv->notifier = fs_element_added_notifier_new (); - g_signal_connect (priv->notifier, "element-added", - G_CALLBACK (empathy_audio_src_element_added_cb), obj); + src_element = g_getenv ("EMPATHY_AUDIO_SRC"); + if (src_element == NULL) + src_element = "pulsesrc"; - priv->src = gst_element_factory_make ("gconfaudiosrc", NULL); + priv->src = gst_element_factory_make (src_element, NULL); gst_bin_add (GST_BIN (obj), priv->src); - fs_element_added_notifier_add (priv->notifier, GST_BIN (priv->src)); - priv->volume = gst_element_factory_make ("volume", NULL); g_object_ref (priv->volume); @@ -121,6 +410,27 @@ empathy_audio_src_init (EmpathyGstAudioSrc *obj) gst_element_add_pad (GST_ELEMENT (obj), ghost); gst_object_unref (G_OBJECT (src)); + + /* PulseAudio stuff: We need to create a dummy pa_glib_mainloop* so + * Pulse can use the mainloop that GTK has created for us. */ + priv->loop = pa_glib_mainloop_new (NULL); + priv->context = pa_context_new (pa_glib_mainloop_get_api (priv->loop), + "EmpathyAudioSrc"); + + /* Listen to changes to GstPulseSrc:source-output-index so we know when + * it's no longer PA_INVALID_INDEX (starting for the first time) or if it + * changes (READY->NULL->READY...) */ + g_signal_connect (priv->src, "notify::source-output-index", + G_CALLBACK (empathy_audio_src_source_output_index_notify), + obj); + + /* Finally listen for state changes so we know when we've + * connected. */ + pa_context_set_state_callback (priv->context, + empathy_audio_src_pa_state_change_cb, obj); + pa_context_connect (priv->context, NULL, 0, NULL); + + priv->operations = g_queue_new (); } static void empathy_audio_src_dispose (GObject *object); @@ -168,6 +478,9 @@ empathy_audio_src_get_property (GObject *object, g_value_set_double (value, priv->rms_level); g_mutex_unlock (priv->lock); break; + case PROP_MICROPHONE: + g_value_set_uint (value, priv->source_idx); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -201,7 +514,12 @@ empathy_audio_src_class_init (EmpathyGstAudioSrcClass param_spec = g_param_spec_double ("peak-level", "peak level", "peak level", -G_MAXDOUBLE, G_MAXDOUBLE, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); - g_object_class_install_property (object_class, PROP_VOLUME, param_spec); + g_object_class_install_property (object_class, PROP_PEAK_LEVEL, param_spec); + + param_spec = g_param_spec_uint ("microphone", "microphone", "microphone", + 0, G_MAXUINT, G_MAXUINT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_MICROPHONE, param_spec); signals[PEAK_LEVEL_CHANGED] = g_signal_new ("peak-level-changed", G_TYPE_FROM_CLASS (empathy_audio_src_class), @@ -214,7 +532,7 @@ empathy_audio_src_class_init (EmpathyGstAudioSrcClass param_spec = g_param_spec_double ("rms-level", "RMS level", "RMS level", -G_MAXDOUBLE, G_MAXDOUBLE, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); - g_object_class_install_property (object_class, PROP_VOLUME, param_spec); + g_object_class_install_property (object_class, PROP_RMS_LEVEL, param_spec); signals[RMS_LEVEL_CHANGED] = g_signal_new ("rms-level-changed", @@ -224,6 +542,22 @@ empathy_audio_src_class_init (EmpathyGstAudioSrcClass NULL, NULL, g_cclosure_marshal_VOID__DOUBLE, G_TYPE_NONE, 1, G_TYPE_DOUBLE); + + signals[MICROPHONE_ADDED] = g_signal_new ("microphone-added", + G_TYPE_FROM_CLASS (empathy_audio_src_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + _src_marshal_VOID__UINT_STRING_STRING_BOOLEAN, + G_TYPE_NONE, 4, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN); + + signals[MICROPHONE_REMOVED] = g_signal_new ("microphone-removed", + G_TYPE_FROM_CLASS (empathy_audio_src_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); } void @@ -242,6 +576,14 @@ empathy_audio_src_dispose (GObject *object) priv->idle_id = 0; + if (priv->context != NULL) + pa_context_unref (priv->context); + priv->context = NULL; + + if (priv->loop != NULL) + pa_glib_mainloop_free (priv->loop); + priv->loop = NULL; + /* release any references held by the object here */ if (G_OBJECT_CLASS (empathy_audio_src_parent_class)->dispose) @@ -257,6 +599,10 @@ empathy_audio_src_finalize (GObject *object) /* free any data held directly by the object here */ g_mutex_free (priv->lock); + g_queue_foreach (priv->operations, (GFunc) operation_free, + GUINT_TO_POINTER (TRUE)); + g_queue_free (priv->operations); + G_OBJECT_CLASS (empathy_audio_src_parent_class)->finalize (object); } @@ -384,4 +730,112 @@ empathy_audio_src_get_volume (EmpathyGstAudioSrc *src) return volume; } +void +empathy_audio_src_get_microphones_async (EmpathyGstAudioSrc *src, + GAsyncReadyCallback callback, + gpointer user_data) +{ + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (src); + Operation *operation; + GSimpleAsyncResult *simple; + + simple = g_simple_async_result_new (G_OBJECT (src), callback, user_data, + empathy_audio_src_get_microphones_async); + + /* If we can't change mic let's not pretend we can by returning the + * list of available mics. */ + if (!empathy_audio_src_supports_changing_mic (src)) + { + g_simple_async_result_set_error (simple, G_IO_ERROR, G_IO_ERROR_FAILED, + "pulsesrc is not new enough to support changing microphone"); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + return; + } + + operation = operation_new (operation_get_microphones, simple); + g_queue_push_tail (priv->operations, operation); + + /* gogogogo */ + operations_run (src); +} + +const GList * +empathy_audio_src_get_microphones_finish (EmpathyGstAudioSrc *src, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result); + GQueue *queue; + + if (g_simple_async_result_propagate_error (simple, error)) + return NULL; + + g_return_val_if_fail (g_simple_async_result_is_valid (result, + G_OBJECT (src), empathy_audio_src_get_microphones_async), + NULL); + + queue = g_simple_async_result_get_op_res_gpointer (simple); + return queue->head; +} +guint +empathy_audio_src_get_microphone (EmpathyGstAudioSrc *src) +{ + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (src); + + return priv->source_idx; +} + +void +empathy_audio_src_change_microphone_async (EmpathyGstAudioSrc *src, + guint microphone, + GAsyncReadyCallback callback, + gpointer user_data) +{ + EmpathyGstAudioSrcPrivate *priv = EMPATHY_GST_AUDIO_SRC_GET_PRIVATE (src); + guint source_output_idx; + GSimpleAsyncResult *simple; + Operation *operation; + + simple = g_simple_async_result_new (G_OBJECT (src), callback, user_data, + empathy_audio_src_change_microphone_async); + + if (!empathy_audio_src_supports_changing_mic (src)) + { + g_simple_async_result_set_error (simple, G_IO_ERROR, G_IO_ERROR_FAILED, + "pulsesrc is not new enough to support changing microphone"); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + return; + } + + g_object_get (priv->src, "source-output-index", &source_output_idx, NULL); + + if (source_output_idx == PA_INVALID_INDEX) + { + g_simple_async_result_set_error (simple, G_IO_ERROR, G_IO_ERROR_FAILED, + "pulsesrc is not yet PLAYING"); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + return; + } + + g_simple_async_result_set_op_res_gpointer (simple, + GUINT_TO_POINTER (microphone), NULL); + + operation = operation_new (operation_change_microphone, simple); + g_queue_push_tail (priv->operations, operation); + + /* gogogogo */ + operations_run (src); +} + +gboolean +empathy_audio_src_change_microphone_finish (EmpathyGstAudioSrc *src, + GAsyncResult *result, + GError **error) +{ + empathy_implement_finish_void (src, + empathy_audio_src_change_microphone_async); +} diff --git a/src/empathy-audio-src.h b/src/empathy-audio-src.h index 4bca31b61..05e3c46cc 100644 --- a/src/empathy-audio-src.h +++ b/src/empathy-audio-src.h @@ -23,6 +23,7 @@ #include <glib-object.h> #include <gst/gst.h> +#include <gio/gio.h> G_BEGIN_DECLS @@ -61,6 +62,26 @@ GstElement *empathy_audio_src_new (void); void empathy_audio_src_set_volume (EmpathyGstAudioSrc *src, gdouble volume); gdouble empathy_audio_src_get_volume (EmpathyGstAudioSrc *src); +typedef struct +{ + guint index; + gchar *name; + gchar *description; + gboolean is_monitor; +} EmpathyAudioSrcMicrophone; + +void empathy_audio_src_get_microphones_async (EmpathyGstAudioSrc *src, + GAsyncReadyCallback callback, gpointer user_data); +const GList * empathy_audio_src_get_microphones_finish (EmpathyGstAudioSrc *src, + GAsyncResult *result, GError **error); + +guint empathy_audio_src_get_microphone (EmpathyGstAudioSrc *src); + +void empathy_audio_src_change_microphone_async (EmpathyGstAudioSrc *src, + guint microphone, GAsyncReadyCallback callback, gpointer user_data); +gboolean empathy_audio_src_change_microphone_finish (EmpathyGstAudioSrc *src, + GAsyncResult *result, GError **error); + G_END_DECLS #endif /* #ifndef __EMPATHY_GST_AUDIO_SRC_H__*/ diff --git a/src/empathy-call-window.c b/src/empathy-call-window.c index 29d3c4463..5ed4e93d3 100644 --- a/src/empathy-call-window.c +++ b/src/empathy-call-window.c @@ -61,6 +61,7 @@ #include "empathy-audio-src.h" #include "empathy-audio-sink.h" #include "empathy-video-src.h" +#include "empathy-mic-menu.h" #define CONTENT_HBOX_BORDER_WIDTH 6 #define CONTENT_HBOX_SPACING 3 @@ -212,6 +213,7 @@ struct _EmpathyCallWindowPriv EmpathySoundManager *sound_mgr; GSettings *settings; + EmpathyMicMenu *mic_menu; }; #define GET_PRIV(o) (EMPATHY_CALL_WINDOW (o)->priv) @@ -939,6 +941,7 @@ empathy_call_window_init (EmpathyCallWindow *self) g_object_unref (gui); priv->sound_mgr = empathy_sound_manager_dup_singleton (); + priv->mic_menu = empathy_mic_menu_new (self); empathy_call_window_show_hangup_button (self, TRUE); @@ -1473,6 +1476,8 @@ empathy_call_window_dispose (GObject *object) tp_clear_object (&priv->sound_mgr); + tp_clear_object (&priv->mic_menu); + G_OBJECT_CLASS (empathy_call_window_parent_class)->dispose (object); } @@ -2964,3 +2969,19 @@ empathy_call_window_volume_changed_cb (GtkScaleButton *button, empathy_audio_sink_set_volume (EMPATHY_GST_AUDIO_SINK (priv->audio_output), value); } + +GtkUIManager * +empathy_call_window_get_ui_manager (EmpathyCallWindow *window) +{ + EmpathyCallWindowPriv *priv = GET_PRIV (window); + + return priv->ui_manager; +} + +EmpathyGstAudioSrc * +empathy_call_window_get_audio_src (EmpathyCallWindow *window) +{ + EmpathyCallWindowPriv *priv = GET_PRIV (window); + + return (EmpathyGstAudioSrc *) priv->audio_input; +} diff --git a/src/empathy-call-window.h b/src/empathy-call-window.h index 11237fff6..357d6f160 100644 --- a/src/empathy-call-window.h +++ b/src/empathy-call-window.h @@ -25,6 +25,7 @@ #include <gtk/gtk.h> #include "empathy-call-handler.h" +#include "empathy-audio-src.h" G_BEGIN_DECLS @@ -62,6 +63,10 @@ GType empathy_call_window_get_type (void); EmpathyCallWindow *empathy_call_window_new (EmpathyCallHandler *handler); +GtkUIManager *empathy_call_window_get_ui_manager (EmpathyCallWindow *window); + +EmpathyGstAudioSrc *empathy_call_window_get_audio_src (EmpathyCallWindow *window); + G_END_DECLS #endif /* #ifndef __EMPATHY_CALL_WINDOW_H__*/ diff --git a/src/empathy-call-window.ui b/src/empathy-call-window.ui index d18260045..88127e1ad 100644 --- a/src/empathy-call-window.ui +++ b/src/empathy-call-window.ui @@ -18,6 +18,19 @@ </object> </child> <child> + <object class="GtkAction" id="edit"> + <property name="name">edit</property> + <property name="label" translatable="yes">_Edit</property> + </object> + </child> + <child> + <object class="GtkAction" id="menumicrophone"> + <property name="label" translatable="yes">_Microphone</property> + <property name="name">menumicrophone</property> + <property name="icon_name">gnome-stock-mic</property> + </object> + </child> + <child> <object class="GtkAction" id="view"> <property name="name">view</property> <property name="label" translatable="yes">_View</property> @@ -55,6 +68,9 @@ <menu action="call"> <menuitem action="menuhangup"/> </menu> + <menu action="edit"> + <menu action="menumicrophone"/> + </menu> <menu action="view"> <menuitem action="menufullscreen"/> </menu> diff --git a/src/empathy-mic-menu.c b/src/empathy-mic-menu.c new file mode 100644 index 000000000..dc3d0f006 --- /dev/null +++ b/src/empathy-mic-menu.c @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2011 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * GtkAction code based on gnome-terminal's TerminalTabsMenu object. + * Thanks guys! + */ + +#include <config.h> + +#include <gtk/gtk.h> + +#include "empathy-mic-menu.h" + +#define DEBUG_FLAG EMPATHY_DEBUG_VOIP +#include <libempathy/empathy-debug.h> + +struct _EmpathyMicMenuPrivate +{ + /* Borrowed ref; the call window actually owns us. */ + EmpathyCallWindow *window; + + /* Given away ref; the call window's UI manager now owns this. */ + GtkActionGroup *action_group; + + /* An invisible radio action so new microphones are always in the + * same radio group. */ + GtkAction *anchor_action; + + /* The merge ID used with the UI manager. We need to keep this + * around so in _clean we can remove all the items we've added + * before and start again. */ + guint ui_id; + + /* TRUE if we're in _update and so calling _set_active. */ + gboolean in_update; + + /* Queue of GtkRadioActions. */ + GQueue *microphones; +}; + +G_DEFINE_TYPE (EmpathyMicMenu, empathy_mic_menu, G_TYPE_OBJECT); + +#define MONITOR_KEY "empathy-mic-menu-is-monitor" + +enum +{ + PROP_WINDOW = 1, +}; + +static void empathy_mic_menu_update (EmpathyMicMenu *self); + +static void +empathy_mic_menu_init (EmpathyMicMenu *self) +{ + EmpathyMicMenuPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self, + EMPATHY_TYPE_MIC_MENU, EmpathyMicMenuPrivate); + + self->priv = priv; +} + +static void +empathy_mic_menu_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + EmpathyMicMenu *self = EMPATHY_MIC_MENU (object); + EmpathyMicMenuPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_WINDOW: + priv->window = g_value_get_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +empathy_mic_menu_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EmpathyMicMenu *self = EMPATHY_MIC_MENU (object); + EmpathyMicMenuPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_WINDOW: + g_value_set_object (value, priv->window); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +empathy_mic_menu_clean (EmpathyMicMenu *self) +{ + EmpathyMicMenuPrivate *priv = self->priv; + GtkUIManager *ui_manager; + + if (priv->ui_id == 0) + return; + + ui_manager = empathy_call_window_get_ui_manager (priv->window); + + gtk_ui_manager_remove_ui (ui_manager, priv->ui_id); + gtk_ui_manager_ensure_update (ui_manager); + priv->ui_id = 0; +} + +static void +empathy_mic_menu_change_mic_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EmpathyGstAudioSrc *audio = EMPATHY_GST_AUDIO_SRC (source_object); + EmpathyMicMenu *self = user_data; + GError *error = NULL; + + if (!empathy_audio_src_change_microphone_finish (audio, result, &error)) + { + DEBUG ("Failed to change microphone: %s", error->message); + g_clear_error (&error); + + /* We call update here because if this change operation failed + * and we don't update the menu items, it'll point to the wrong + * device. We don't want to call it if the change was successful + * because we'll get the notify::microphone signal fired in a + * bit and the current value hasn't changed so it'd keep jumping + * between these values like there's no tomorrow, etc. */ + empathy_mic_menu_update (self); + } +} + +static void +empathy_mic_menu_activate_cb (GtkToggleAction *action, + EmpathyMicMenu *self) +{ + EmpathyMicMenuPrivate *priv = self->priv; + EmpathyGstAudioSrc *audio; + gint value; + + if (priv->in_update) + return; + + audio = empathy_call_window_get_audio_src (priv->window); + + g_object_get (action, "value", &value, NULL); + + empathy_audio_src_change_microphone_async (audio, value, + empathy_mic_menu_change_mic_cb, self); +} + +static void +empathy_mic_menu_update (EmpathyMicMenu *self) +{ + EmpathyMicMenuPrivate *priv = self->priv; + GList *l; + GtkUIManager *ui_manager; + EmpathyGstAudioSrc *audio; + guint current_mic; + + ui_manager = empathy_call_window_get_ui_manager (priv->window); + + audio = empathy_call_window_get_audio_src (priv->window); + current_mic = empathy_audio_src_get_microphone (audio); + + empathy_mic_menu_clean (self); + priv->ui_id = gtk_ui_manager_new_merge_id (ui_manager); + + for (l = priv->microphones->head; l != NULL; l = l->next) + { + GtkRadioAction *action = l->data; + const gchar *name = gtk_action_get_name (GTK_ACTION (action)); + gint value; + gboolean active; + + g_object_get (action, "value", &value, NULL); + + active = (value == (gint) current_mic); + + if (active) + { + priv->in_update = TRUE; + gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), TRUE); + priv->in_update = FALSE; + } + + /* If action is a monitor then don't show it in the UI, BUT do + * display it regardless if it is the current device. This is so + * we don't have a rubbish UI by showing monitor devices in + * Empathy, but still show the correct device when someone plays + * with pavucontrol. */ + if (g_object_get_data (G_OBJECT (action), MONITOR_KEY) != NULL + && !active) + continue; + + gtk_ui_manager_add_ui (ui_manager, priv->ui_id, + /* TODO: this should probably be passed from the call + * window, seeing that it's a reference to + * empathy-call-window.ui. */ + "/menubar1/edit/menumicrophone", + name, name, GTK_UI_MANAGER_MENUITEM, FALSE); + } +} + +static void +empathy_mic_menu_add_microphone (EmpathyMicMenu *self, + const gchar *name, + const gchar *description, + guint source_idx, + gboolean is_monitor) +{ + EmpathyMicMenuPrivate *priv = self->priv; + GtkRadioAction *action; + GSList *group; + + action = gtk_radio_action_new (name, description, NULL, NULL, source_idx); + gtk_action_group_add_action_with_accel (priv->action_group, + GTK_ACTION (action), NULL); + + /* Set MONITOR_KEY on the action to non-NULL if it's a monitor + * because we don't want to show monitors if we can help it. */ + if (is_monitor) + { + g_object_set_data (G_OBJECT (action), MONITOR_KEY, + GUINT_TO_POINTER (TRUE)); + } + + group = gtk_radio_action_get_group (GTK_RADIO_ACTION (priv->anchor_action)); + gtk_radio_action_set_group (GTK_RADIO_ACTION (action), group); + + g_queue_push_tail (priv->microphones, action); + + g_signal_connect (action, "activate", + G_CALLBACK (empathy_mic_menu_activate_cb), self); +} + +static void +empathy_mic_menu_notify_microphone_cb (EmpathyGstAudioSrc *audio, + GParamSpec *pspec, + EmpathyMicMenu *self) +{ + empathy_mic_menu_update (self); +} + +static void +empathy_mic_menu_microphone_added_cb (EmpathyGstAudioSrc *audio, + guint source_idx, + const gchar *name, + const gchar *description, + gboolean is_monitor, + EmpathyMicMenu *self) +{ + empathy_mic_menu_add_microphone (self, name, description, + source_idx, is_monitor); + + empathy_mic_menu_update (self); +} + +static void +empathy_mic_menu_microphone_removed_cb (EmpathyGstAudioSrc *audio, + guint source_idx, + EmpathyMicMenu *self) +{ + EmpathyMicMenuPrivate *priv = self->priv; + GList *l; + + for (l = priv->microphones->head; l != NULL; l = l->next) + { + GtkRadioAction *action = l->data; + gint value; + + g_object_get (action, "value", &value, NULL); + + if (value != (gint) source_idx) + { + action = NULL; + continue; + } + + g_signal_handlers_disconnect_by_func (action, + G_CALLBACK (empathy_mic_menu_activate_cb), self); + + gtk_action_group_remove_action (priv->action_group, GTK_ACTION (action)); + g_queue_remove (priv->microphones, action); + break; + } + + empathy_mic_menu_update (self); +} + +static void +empathy_mic_menu_get_microphones_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EmpathyGstAudioSrc *audio = EMPATHY_GST_AUDIO_SRC (source_object); + EmpathyMicMenu *self = user_data; + GError *error = NULL; + const GList *mics = NULL; + + mics = empathy_audio_src_get_microphones_finish (audio, result, &error); + + if (error != NULL) + { + DEBUG ("Failed to get microphone list: %s", error->message); + g_clear_error (&error); + return; + } + + for (; mics != NULL; mics = mics->next) + { + EmpathyAudioSrcMicrophone *mic = mics->data; + + empathy_mic_menu_add_microphone (self, mic->name, + mic->description, mic->index, mic->is_monitor); + } + + empathy_mic_menu_update (self); +} + +static void +empathy_mic_menu_constructed (GObject *obj) +{ + EmpathyMicMenu *self = EMPATHY_MIC_MENU (obj); + EmpathyMicMenuPrivate *priv = self->priv; + GtkUIManager *ui_manager; + EmpathyGstAudioSrc *audio; + + g_assert (EMPATHY_IS_CALL_WINDOW (priv->window)); + + ui_manager = empathy_call_window_get_ui_manager (priv->window); + audio = empathy_call_window_get_audio_src (priv->window); + + g_assert (GTK_IS_UI_MANAGER (ui_manager)); + g_assert (EMPATHY_IS_GST_AUDIO_SRC (audio)); + + /* Okay let's go go go. */ + + priv->action_group = gtk_action_group_new ("EmpathyMicMenu"); + gtk_ui_manager_insert_action_group (ui_manager, priv->action_group, -1); + /* the UI manager now owns this */ + g_object_unref (priv->action_group); + + priv->anchor_action = g_object_new (GTK_TYPE_RADIO_ACTION, + "name", "EmpathyMicMenuAnchorAction", + NULL); + gtk_action_group_add_action (priv->action_group, priv->anchor_action); + g_object_unref (priv->anchor_action); + + tp_g_signal_connect_object (audio, "notify::microphone", + G_CALLBACK (empathy_mic_menu_notify_microphone_cb), self, 0); + tp_g_signal_connect_object (audio, "microphone-added", + G_CALLBACK (empathy_mic_menu_microphone_added_cb), self, 0); + tp_g_signal_connect_object (audio, "microphone-removed", + G_CALLBACK (empathy_mic_menu_microphone_removed_cb), self, 0); + + priv->microphones = g_queue_new (); + + empathy_audio_src_get_microphones_async (audio, + empathy_mic_menu_get_microphones_cb, self); +} + +static void +empathy_mic_menu_dispose (GObject *obj) +{ + EmpathyMicMenu *self = EMPATHY_MIC_MENU (obj); + EmpathyMicMenuPrivate *priv = self->priv; + + if (priv->microphones != NULL) + g_queue_free (priv->microphones); + priv->microphones = NULL; + + G_OBJECT_CLASS (empathy_mic_menu_parent_class)->dispose (obj); +} + +static void +empathy_mic_menu_class_init (EmpathyMicMenuClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = empathy_mic_menu_set_property; + object_class->get_property = empathy_mic_menu_get_property; + object_class->constructed = empathy_mic_menu_constructed; + object_class->dispose = empathy_mic_menu_dispose; + + g_object_class_install_property (object_class, PROP_WINDOW, + g_param_spec_object ("window", "window", "window", + EMPATHY_TYPE_CALL_WINDOW, + G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY)); + + g_type_class_add_private (object_class, sizeof (EmpathyMicMenuPrivate)); +} + +EmpathyMicMenu * +empathy_mic_menu_new (EmpathyCallWindow *window) +{ + return g_object_new (EMPATHY_TYPE_MIC_MENU, + "window", window, + NULL); +} diff --git a/src/empathy-mic-menu.h b/src/empathy-mic-menu.h new file mode 100644 index 000000000..87691ae6d --- /dev/null +++ b/src/empathy-mic-menu.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __EMPATHY_MIC_MENU_H__ +#define __EMPATHY_MIC_MENU_H__ + +#include <glib-object.h> + +#include "empathy-call-window.h" + +G_BEGIN_DECLS + +#define EMPATHY_TYPE_MIC_MENU (empathy_mic_menu_get_type ()) +#define EMPATHY_MIC_MENU(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), EMPATHY_TYPE_MIC_MENU, EmpathyMicMenu)) +#define EMPATHY_MIC_MENU_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), EMPATHY_TYPE_MIC_MENU, EmpathyMicMenuClass)) +#define EMPATHY_IS_MIC_MENU(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), EMPATHY_TYPE_MIC_MENU)) +#define EMPATHY_IS_MIC_MENU_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), EMPATHY_TYPE_MIC_MENU)) +#define EMPATHY_MIC_MENU_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EMPATHY_TYPE_MIC_MENU, EmpathyMicMenuClass)) + +typedef struct _EmpathyMicMenu EmpathyMicMenu; +typedef struct _EmpathyMicMenuPrivate EmpathyMicMenuPrivate; +typedef struct _EmpathyMicMenuClass EmpathyMicMenuClass; + +struct _EmpathyMicMenu +{ + GObject parent; + EmpathyMicMenuPrivate *priv; +}; + +struct _EmpathyMicMenuClass +{ + GObjectClass parent_class; +}; + +GType empathy_mic_menu_get_type (void) G_GNUC_CONST; + +EmpathyMicMenu * empathy_mic_menu_new (EmpathyCallWindow *window); + +G_END_DECLS + +#endif /* __EMPATHY_MIC_MENU_H__ */ |