1 /*
2  * Configures microphones and webcams for voice and video
3  * Copyright (C) 2009 Mike Ruprecht <cmaiku@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
18  */
19 #include "internal.h"
20 
21 #include "debug.h"
22 #include "mediamanager.h"
23 #include "media-gst.h"
24 #include "version.h"
25 #include "gtkplugin.h"
26 #include "gtkutils.h"
27 #include "gtkprefs.h"
28 
29 #if GST_CHECK_VERSION(1,0,0)
30 #include <gst/video/videooverlay.h>
31 #else
32 #include <gst/interfaces/propertyprobe.h>
33 #endif
34 
35 /* container window for showing a stand-alone configurator */
36 static GtkWidget *window = NULL;
37 
38 static PurpleMediaElementInfo *old_video_src = NULL, *old_video_sink = NULL,
39 		*old_audio_src = NULL, *old_audio_sink = NULL;
40 
41 static const gchar *AUDIO_SRC_PLUGINS[] = {
42 	"alsasrc", N_("ALSA"),
43 	/* "esdmon",	"ESD", ? */
44 	"osssrc", N_("OSS"),
45 	"pulsesrc", N_("PulseAudio"),
46 	"sndiosrc", N_("sndio"),
47 	/* "audiotestsrc wave=silence", "Silence", */
48 	"audiotestsrc", N_("Test Sound"),
49 	NULL
50 };
51 
52 static const gchar *AUDIO_SINK_PLUGINS[] = {
53 	"alsasink", N_("ALSA"),
54 	"artsdsink", N_("aRts"),
55 	"esdsink", N_("ESD"),
56 	"osssink", N_("OSS"),
57 	"pulsesink", N_("PulseAudio"),
58 	"sndiosink", N_("sndio"),
59 	NULL
60 };
61 
62 static const gchar *VIDEO_SRC_PLUGINS[] = {
63 	"videotestsrc", N_("Test Input"),
64 	"dshowvideosrc", N_("DirectDraw"),
65 	"ksvideosrc", N_("KS Video"),
66 	"qcamsrc", N_("Quickcam"),
67 	"v4lsrc", N_("Video4Linux"),
68 	"v4l2src", N_("Video4Linux2"),
69 	"v4lmjpegsrc", N_("Video4Linux MJPEG"),
70 	NULL
71 };
72 
73 static const gchar *VIDEO_SINK_PLUGINS[] = {
74 	/* "aasink",	"AALib", Didn't work for me */
75 	"directdrawsink", N_("DirectDraw"),
76 	"glimagesink",	N_("OpenGL"),
77 	"ximagesink",	N_("X Window System"),
78 	"xvimagesink",	N_("X Window System (Xv)"),
79 	NULL
80 };
81 
82 static GList *
get_element_devices(const gchar * element_name)83 get_element_devices(const gchar *element_name)
84 {
85 	GList *ret = NULL;
86 	GstElement *element;
87 	GObjectClass *klass;
88 #if !GST_CHECK_VERSION(1,0,0)
89 	GstPropertyProbe *probe;
90 	const GParamSpec *pspec;
91 #endif
92 
93 	ret = g_list_prepend(ret, (gpointer)_("Default"));
94 	ret = g_list_prepend(ret, "");
95 
96 	if (purple_strequal(element_name, "<custom>") || (*element_name == '\0')) {
97 		return g_list_reverse(ret);
98 	}
99 
100 	element = gst_element_factory_make(element_name, "test");
101 	if(!element) {
102 		purple_debug_info("vvconfig", "'%s' - unable to find element\n", element_name);
103 		return g_list_reverse(ret);
104 	}
105 
106 	klass = G_OBJECT_GET_CLASS (element);
107 	if(!klass) {
108 		purple_debug_info("vvconfig", "'%s' - unable to find G_Object Class\n", element_name);
109 		return g_list_reverse(ret);
110 	}
111 
112 #if GST_CHECK_VERSION(1,0,0)
113 	purple_debug_info("vvconfig", "'%s' - gstreamer-1.0 doesn't support "
114 		"property probing\n", element_name);
115 #else
116 	if (!g_object_class_find_property(klass, "device") ||
117 			!GST_IS_PROPERTY_PROBE(element) ||
118 			!(probe = GST_PROPERTY_PROBE(element)) ||
119 			!(pspec = gst_property_probe_get_property(probe, "device"))) {
120 		purple_debug_info("vvconfig", "'%s' - no device\n", element_name);
121 	} else {
122 		gsize n;
123 		GValueArray *array;
124 
125 		/* Set autoprobe[-fps] to FALSE to avoid delays when probing. */
126 		if (g_object_class_find_property (klass, "autoprobe")) {
127 			g_object_set (G_OBJECT (element), "autoprobe", FALSE, NULL);
128 			if (g_object_class_find_property (klass, "autoprobe-fps")) {
129 				g_object_set (G_OBJECT (element), "autoprobe-fps", FALSE, NULL);
130 			}
131 		}
132 
133 		array = gst_property_probe_probe_and_get_values (probe, pspec);
134 		if (array == NULL) {
135 			purple_debug_info("vvconfig", "'%s' has no devices\n", element_name);
136 			return g_list_reverse(ret);
137 		}
138 
139 		for (n=0; n < array->n_values; ++n) {
140 			GValue *device;
141 			const gchar *name;
142 			const gchar *device_name;
143 
144 G_GNUC_BEGIN_IGNORE_DEPRECATIONS
145 			/* GValueArray is in gstreamer-0.10 API */
146 			device = g_value_array_get_nth(array, n);
147 G_GNUC_END_IGNORE_DEPRECATIONS
148 			g_object_set_property(G_OBJECT(element), "device", device);
149 			if (gst_element_set_state(element, GST_STATE_READY)
150 					!= GST_STATE_CHANGE_SUCCESS) {
151 				purple_debug_warning("vvconfig",
152 						"Error changing state of %s\n",
153 						element_name);
154 				continue;
155 			}
156 
157 			g_object_get(G_OBJECT(element), "device-name", &name, NULL);
158 			device_name = g_value_get_string(device);
159 			if (name == NULL)
160 				name = _("Unknown");
161 			purple_debug_info("vvconfig", "Found device %s : %s for %s\n",
162 					device_name, name, element_name);
163 			ret = g_list_prepend(ret, (gpointer)name);
164 			ret = g_list_prepend(ret, (gpointer)device_name);
165 			gst_element_set_state(element, GST_STATE_NULL);
166 		}
167 	}
168 #endif
169 	gst_object_unref(element);
170 
171 	return g_list_reverse(ret);
172 }
173 
174 static GList *
get_element_plugins(const gchar ** plugins)175 get_element_plugins(const gchar **plugins)
176 {
177 	GList *ret = NULL;
178 
179 	ret = g_list_prepend(ret, "Default");
180 	ret = g_list_prepend(ret, "");
181 	for (; plugins[0] && plugins[1]; plugins += 2) {
182 #if GST_CHECK_VERSION(1,0,0)
183 		if (gst_registry_check_feature_version(gst_registry_get(),
184 				plugins[0], 0, 0, 0)) {
185 #else
186 		if (gst_default_registry_check_feature_version(
187 				plugins[0], 0, 0, 0)) {
188 #endif
189 			ret = g_list_prepend(ret, (gpointer)plugins[1]);
190 			ret = g_list_prepend(ret, (gpointer)plugins[0]);
191 		}
192 	}
193 	ret = g_list_reverse(ret);
194 	return ret;
195 }
196 
197 static void
198 device_changed_cb(const gchar *name, PurplePrefType type,
199 		gconstpointer value, gpointer data)
200 {
201 	GtkSizeGroup *sg = data;
202 	GtkWidget *parent, *widget;
203 	GSList *widgets;
204 	GList *devices;
205 	GValue gvalue;
206 	gint position;
207 	gchar *label, *pref;
208 
209 	widgets = gtk_size_group_get_widgets(GTK_SIZE_GROUP(sg));
210 	for (; widgets; widgets = g_slist_next(widgets)) {
211 		const gchar *widget_name =
212 				gtk_widget_get_name(GTK_WIDGET(widgets->data));
213 		if (purple_strequal(widget_name, name)) {
214 			gchar *temp_str;
215 			gchar delimiters[3] = {0, 0, 0};
216 			const gchar *text;
217 			gint keyval, pos;
218 
219 			widget = widgets->data;
220 			/* Get label with _ from the GtkLabel */
221 			text = gtk_label_get_text(GTK_LABEL(widget));
222 			keyval = gtk_label_get_mnemonic_keyval(GTK_LABEL(widget));
223 			delimiters[0] = g_ascii_tolower(keyval);
224 			delimiters[1] = g_ascii_toupper(keyval);
225 			pos = strcspn(text, delimiters);
226 			if (pos != -1) {
227 				temp_str = g_strndup(text, pos);
228 				label = g_strconcat(temp_str, "_",
229 						text + pos, NULL);
230 				g_free(temp_str);
231 			} else {
232 				label = g_strdup(text);
233 			}
234 			break;
235 		}
236 	}
237 
238 	if (widgets == NULL)
239 		return;
240 
241 	parent = gtk_widget_get_parent(widget);
242 	widget = parent;
243 	parent = gtk_widget_get_parent(GTK_WIDGET(widget));
244 	gvalue.g_type = 0;
245 	g_value_init(&gvalue, G_TYPE_INT);
246 	gtk_container_child_get_property(GTK_CONTAINER(parent),
247 			GTK_WIDGET(widget), "position", &gvalue);
248 	position = g_value_get_int(&gvalue);
249 	g_value_unset(&gvalue);
250 	gtk_widget_destroy(widget);
251 
252 	pref = g_strdup(name);
253 	strcpy(pref + strlen(pref) - strlen("plugin"), "device");
254 	devices = get_element_devices(value);
255 	if (g_list_find_custom(devices, purple_prefs_get_string(pref),
256 			(GCompareFunc)strcmp) == NULL)
257 	{
258 		GList *next = g_list_next(devices);
259 
260 		if(next != NULL) {
261 			purple_prefs_set_string(pref, next->data);
262 		}
263 	}
264 	widget = pidgin_prefs_dropdown_from_list(parent,
265 			label, PURPLE_PREF_STRING,
266 			pref, devices);
267 	g_list_free(devices);
268 	g_signal_connect_swapped(widget, "destroy",
269 			G_CALLBACK(g_free), pref);
270 	g_free(label);
271 	gtk_misc_set_alignment(GTK_MISC(widget), 0, 0.5);
272 	gtk_widget_set_name(widget, name);
273 	gtk_size_group_add_widget(sg, widget);
274 	gtk_box_reorder_child(GTK_BOX(parent),
275 			gtk_widget_get_parent(GTK_WIDGET(widget)), position);
276 }
277 
278 static void
279 get_plugin_frame(GtkWidget *parent, GtkSizeGroup *sg,
280 		const gchar *name, const gchar *plugin_label,
281 		const gchar **plugin_strs, const gchar *plugin_pref,
282 		const gchar *device_label, const gchar *device_pref)
283 {
284 	GtkWidget *vbox, *widget;
285 	GList *plugins, *devices;
286 
287 	vbox = pidgin_make_frame(parent, name);
288 
289 	/* Setup plugin preference */
290 	plugins = get_element_plugins(plugin_strs);
291 	widget = pidgin_prefs_dropdown_from_list(vbox, plugin_label,
292 			PURPLE_PREF_STRING, plugin_pref, plugins);
293 	g_list_free(plugins);
294 	gtk_size_group_add_widget(sg, widget);
295 	gtk_misc_set_alignment(GTK_MISC(widget), 0, 0.5);
296 
297 	/* Setup device preference */
298 	devices = get_element_devices(purple_prefs_get_string(plugin_pref));
299 	if (g_list_find_custom(devices, purple_prefs_get_string(device_pref),
300 			(GCompareFunc) strcmp) == NULL)
301 	{
302 		GList *next = g_list_next(devices);
303 		if(next != NULL) {
304 			purple_prefs_set_string(device_pref, next->data);
305 		}
306 	}
307 	widget = pidgin_prefs_dropdown_from_list(vbox, device_label,
308 			PURPLE_PREF_STRING, device_pref, devices);
309 	g_list_free(devices);
310 	gtk_widget_set_name(widget, plugin_pref);
311 	gtk_size_group_add_widget(sg, widget);
312 	gtk_misc_set_alignment(GTK_MISC(widget), 0, 0.5);
313 
314 	purple_prefs_connect_callback(vbox, plugin_pref,
315 			device_changed_cb, sg);
316 	g_signal_connect_swapped(vbox, "destroy",
317 			G_CALLBACK(purple_prefs_disconnect_by_handle), vbox);
318 }
319 
320 static GtkWidget *
321 get_plugin_config_frame(PurplePlugin *plugin) {
322 	GtkWidget *notebook, *vbox_audio, *vbox_video;
323 	GtkSizeGroup *sg;
324 
325 	notebook = gtk_notebook_new();
326 	gtk_container_set_border_width(GTK_CONTAINER(notebook),
327 			PIDGIN_HIG_BORDER);
328 	gtk_widget_show(notebook);
329 
330 	vbox_audio = gtk_vbox_new(FALSE, PIDGIN_HIG_CAT_SPACE);
331 	vbox_video = gtk_vbox_new(FALSE, PIDGIN_HIG_CAT_SPACE);
332 	gtk_notebook_append_page(GTK_NOTEBOOK(notebook),
333 			vbox_audio, gtk_label_new(_("Audio")));
334 	gtk_notebook_append_page(GTK_NOTEBOOK(notebook),
335 			vbox_video, gtk_label_new(_("Video")));
336 	gtk_container_set_border_width(GTK_CONTAINER (vbox_audio),
337 			PIDGIN_HIG_BORDER);
338 	gtk_container_set_border_width(GTK_CONTAINER (vbox_video),
339 			PIDGIN_HIG_BORDER);
340 
341 	gtk_widget_show(vbox_audio);
342 	gtk_widget_show(vbox_video);
343 
344 	sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
345 
346 	get_plugin_frame(vbox_audio, sg, _("Output"), _("_Plugin"), AUDIO_SINK_PLUGINS,
347 			"/plugins/core/vvconfig/audio/sink/plugin", _("_Device"),
348 			"/plugins/core/vvconfig/audio/sink/device");
349 	get_plugin_frame(vbox_audio, sg, _("Input"), _("P_lugin"), AUDIO_SRC_PLUGINS,
350 			"/plugins/core/vvconfig/audio/src/plugin", _("D_evice"),
351 			"/plugins/core/vvconfig/audio/src/device");
352 
353 	get_plugin_frame(vbox_video, sg, _("Output"), _("_Plugin"), VIDEO_SINK_PLUGINS,
354 			"/plugins/gtk/vvconfig/video/sink/plugin", _("_Device"),
355 			"/plugins/gtk/vvconfig/video/sink/device");
356 	get_plugin_frame(vbox_video, sg, _("Input"), _("P_lugin"), VIDEO_SRC_PLUGINS,
357 			"/plugins/core/vvconfig/video/src/plugin", _("D_evice"),
358 			"/plugins/core/vvconfig/video/src/device");
359 
360 	return notebook;
361 }
362 
363 static GstElement *
364 create_video_src(PurpleMedia *media,
365 		const gchar *session_id, const gchar *participant)
366 {
367 	const gchar *plugin = purple_prefs_get_string(
368 			"/plugins/core/vvconfig/video/src/plugin");
369 	const gchar *device = purple_prefs_get_string(
370 			"/plugins/core/vvconfig/video/src/device");
371 	GstElement *ret;
372 
373 	if (plugin[0] == '\0')
374 		return purple_media_element_info_call_create(old_video_src,
375 				media, session_id, participant);
376 
377 	ret = gst_element_factory_make(plugin, "vvconfig-videosrc");
378 	if (device[0] != '\0')
379 		g_object_set(G_OBJECT(ret), "device", device, NULL);
380 	if (purple_strequal(plugin, "videotestsrc"))
381 		g_object_set(G_OBJECT(ret), "is-live", 1, NULL);
382 	return ret;
383 }
384 
385 static void
386 videosink_disable_last_sample(GstElement *sink)
387 {
388 	GObjectClass *klass = G_OBJECT_GET_CLASS(sink);
389 
390 	if (g_object_class_find_property(klass, "enable-last-sample")) {
391 		g_object_set(sink, "enable-last-sample", FALSE, NULL);
392 	}
393 }
394 
395 static void
396 autovideosink_child_added_cb(GstChildProxy *child_proxy, GObject *object,
397 #if GST_CHECK_VERSION(1,0,0)
398 		gchar *name,
399 #endif
400 		gpointer user_data)
401 {
402 	videosink_disable_last_sample(GST_ELEMENT(object));
403 }
404 
405 static GstElement *
406 create_video_sink(PurpleMedia *media,
407 		const gchar *session_id, const gchar *participant)
408 {
409 	const gchar *plugin = purple_prefs_get_string(
410 			"/plugins/gtk/vvconfig/video/sink/plugin");
411 	const gchar *device = purple_prefs_get_string(
412 			"/plugins/gtk/vvconfig/video/sink/device");
413 	GstElement *ret;
414 
415 	if (plugin[0] == '\0')
416 		return purple_media_element_info_call_create(old_video_sink,
417 				media, session_id, participant);
418 
419 	ret = gst_element_factory_make(plugin, NULL);
420 	if (device[0] != '\0')
421 		g_object_set(G_OBJECT(ret), "device", device, NULL);
422 
423 	if (purple_strequal(plugin, "autovideosink")) {
424 		g_signal_connect(ret, "child-added",
425 			G_CALLBACK(autovideosink_child_added_cb), NULL);
426 	} else {
427 		videosink_disable_last_sample(ret);
428 	}
429 
430 	return ret;
431 }
432 
433 static GstElement *
434 create_audio_src(PurpleMedia *media,
435 		const gchar *session_id, const gchar *participant)
436 {
437 	const gchar *plugin = purple_prefs_get_string(
438 			"/plugins/core/vvconfig/audio/src/plugin");
439 	const gchar *device = purple_prefs_get_string(
440 			"/plugins/core/vvconfig/audio/src/device");
441 	GstElement *ret;
442 
443 	if (plugin[0] == '\0')
444 		return purple_media_element_info_call_create(old_audio_src,
445 				media, session_id, participant);
446 
447 	ret = gst_element_factory_make(plugin, NULL);
448 	if (device[0] != '\0')
449 		g_object_set(G_OBJECT(ret), "device", device, NULL);
450 	return ret;
451 }
452 
453 static GstElement *
454 create_audio_sink(PurpleMedia *media,
455 		const gchar *session_id, const gchar *participant)
456 {
457 	const gchar *plugin = purple_prefs_get_string(
458 			"/plugins/core/vvconfig/audio/sink/plugin");
459 	const gchar *device = purple_prefs_get_string(
460 			"/plugins/core/vvconfig/audio/sink/device");
461 	GstElement *ret;
462 
463 	if (plugin[0] == '\0')
464 		return purple_media_element_info_call_create(old_audio_sink,
465 				media, session_id, participant);
466 
467 	ret = gst_element_factory_make(plugin, NULL);
468 	if (device[0] != '\0')
469 		g_object_set(G_OBJECT(ret), "device", device, NULL);
470 	return ret;
471 }
472 
473 static void
474 set_element_info_cond(PurpleMediaElementInfo *old_info,
475 		PurpleMediaElementInfo *new_info, const gchar *id)
476 {
477 	gchar *element_id = purple_media_element_info_get_id(old_info);
478 	if (purple_strequal(element_id, id))
479 		purple_media_manager_set_active_element(
480 				purple_media_manager_get(), new_info);
481 	g_free(element_id);
482 }
483 
484 static gboolean
485 plugin_load(PurplePlugin *plugin)
486 {
487 	PurpleMediaManager *manager;
488 	PurpleMediaElementInfo *video_src, *video_sink,
489 			*audio_src, *audio_sink;
490 
491 	/* Disable the plugin if the UI doesn't support VV */
492 	if (purple_media_manager_get_ui_caps(purple_media_manager_get()) ==
493 			PURPLE_MEDIA_CAPS_NONE)
494 		return FALSE;
495 
496 	purple_prefs_add_none("/plugins/core/vvconfig");
497 	purple_prefs_add_none("/plugins/core/vvconfig/audio");
498 	purple_prefs_add_none("/plugins/core/vvconfig/audio/src");
499 	purple_prefs_add_string("/plugins/core/vvconfig/audio/src/plugin", "");
500 	purple_prefs_add_string("/plugins/core/vvconfig/audio/src/device", "");
501 	purple_prefs_add_none("/plugins/core/vvconfig/audio/sink");
502 	purple_prefs_add_string("/plugins/core/vvconfig/audio/sink/plugin", "");
503 	purple_prefs_add_string("/plugins/core/vvconfig/audio/sink/device", "");
504 	purple_prefs_add_none("/plugins/core/vvconfig/video");
505 	purple_prefs_add_none("/plugins/core/vvconfig/video/src");
506 	purple_prefs_add_string("/plugins/core/vvconfig/video/src/plugin", "");
507 	purple_prefs_add_string("/plugins/core/vvconfig/video/src/device", "");
508 	purple_prefs_add_none("/plugins/gtk/vvconfig");
509 	purple_prefs_add_none("/plugins/gtk/vvconfig/video");
510 	purple_prefs_add_none("/plugins/gtk/vvconfig/video/sink");
511 	purple_prefs_add_string("/plugins/gtk/vvconfig/video/sink/plugin", "");
512 	purple_prefs_add_string("/plugins/gtk/vvconfig/video/sink/device", "");
513 
514 	video_src = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO,
515 			"id", "vvconfig-videosrc",
516 			"name", "VV Conf Plugin Video Source",
517 			"type", PURPLE_MEDIA_ELEMENT_VIDEO
518 					| PURPLE_MEDIA_ELEMENT_SRC
519 					| PURPLE_MEDIA_ELEMENT_ONE_SRC
520 					| PURPLE_MEDIA_ELEMENT_UNIQUE,
521 			"create-cb", create_video_src, NULL);
522 	video_sink = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO,
523 			"id", "vvconfig-videosink",
524 			"name", "VV Conf Plugin Video Sink",
525 			"type", PURPLE_MEDIA_ELEMENT_VIDEO
526 					| PURPLE_MEDIA_ELEMENT_SINK
527 					| PURPLE_MEDIA_ELEMENT_ONE_SINK,
528 			"create-cb", create_video_sink, NULL);
529 	audio_src = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO,
530 			"id", "vvconfig-audiosrc",
531 			"name", "VV Conf Plugin Audio Source",
532 			"type", PURPLE_MEDIA_ELEMENT_AUDIO
533 					| PURPLE_MEDIA_ELEMENT_SRC
534 					| PURPLE_MEDIA_ELEMENT_ONE_SRC
535 					| PURPLE_MEDIA_ELEMENT_UNIQUE,
536 			"create-cb", create_audio_src, NULL);
537 	audio_sink = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO,
538 			"id", "vvconfig-audiosink",
539 			"name", "VV Conf Plugin Audio Sink",
540 			"type", PURPLE_MEDIA_ELEMENT_AUDIO
541 					| PURPLE_MEDIA_ELEMENT_SINK
542 					| PURPLE_MEDIA_ELEMENT_ONE_SINK,
543 			"create-cb", create_audio_sink, NULL);
544 
545 	purple_debug_info("gtkmedia", "Registering media element types\n");
546 	manager = purple_media_manager_get();
547 
548 	old_video_src = purple_media_manager_get_active_element(manager,
549 			PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC);
550 	old_video_sink = purple_media_manager_get_active_element(manager,
551 			PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK);
552 	old_audio_src = purple_media_manager_get_active_element(manager,
553 			PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC);
554 	old_audio_sink = purple_media_manager_get_active_element(manager,
555 			PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SINK);
556 
557 	set_element_info_cond(old_video_src, video_src, "pidgindefaultvideosrc");
558 	set_element_info_cond(old_video_sink, video_sink, "pidgindefaultvideosink");
559 	set_element_info_cond(old_audio_src, audio_src, "pidgindefaultaudiosrc");
560 	set_element_info_cond(old_audio_sink, audio_sink, "pidgindefaultaudiosink");
561 
562 	return TRUE;
563 }
564 
565 static void
566 config_destroy(GtkObject *w, gpointer nul)
567 {
568 	purple_debug_info("vvconfig", "closing vv configuration window\n");
569 	window = NULL;
570 }
571 
572 static void
573 config_close(GtkObject *w, gpointer nul)
574 {
575 	gtk_widget_destroy(GTK_WIDGET(window));
576 }
577 
578 typedef GtkWidget *(*FrameCreateCb)(PurplePlugin *plugin);
579 
580 static void
581 show_config(PurplePluginAction *action)
582 {
583 	if (!window) {
584 		FrameCreateCb create_frame = action->user_data;
585 		GtkWidget *vbox = gtk_vbox_new(FALSE, PIDGIN_HIG_BORDER);
586 		GtkWidget *hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BORDER);
587 		GtkWidget *config_frame = create_frame(NULL);
588 		GtkWidget *close = gtk_button_new_from_stock(GTK_STOCK_CLOSE);
589 
590 		gtk_container_add(GTK_CONTAINER(vbox), config_frame);
591 		gtk_container_add(GTK_CONTAINER(vbox), hbox);
592 		window = pidgin_create_window(action->label,
593 			PIDGIN_HIG_BORDER, NULL, FALSE);
594 		g_signal_connect(G_OBJECT(window), "destroy",
595 			G_CALLBACK(config_destroy), NULL);
596 		g_signal_connect(G_OBJECT(close), "clicked",
597 		    G_CALLBACK(config_close), NULL);
598 		gtk_box_pack_end(GTK_BOX(hbox), close, FALSE, FALSE, PIDGIN_HIG_BORDER);
599 		gtk_container_add(GTK_CONTAINER(window), vbox);
600 		gtk_widget_show(GTK_WIDGET(close));
601 		gtk_widget_show(GTK_WIDGET(vbox));
602 		gtk_widget_show(GTK_WIDGET(hbox));
603 	}
604 	gtk_window_present(GTK_WINDOW(window));
605 }
606 
607 static GstElement *
608 create_pipeline()
609 {
610 	GstElement *pipeline = gst_pipeline_new("voicetest");
611 	GstElement *src = create_audio_src(NULL, NULL, NULL);
612 	GstElement *sink = create_audio_sink(NULL, NULL, NULL);
613 	GstElement *volume = gst_element_factory_make("volume", "volume");
614 	GstElement *level = gst_element_factory_make("level", "level");
615 	GstElement *valve = gst_element_factory_make("valve", "valve");
616 
617 	gst_bin_add_many(GST_BIN(pipeline), src, volume, level, valve, sink, NULL);
618 	gst_element_link_many(src, volume, level, valve, sink, NULL);
619 
620 	gst_element_set_state(GST_ELEMENT(pipeline), GST_STATE_PLAYING);
621 
622 	return pipeline;
623 }
624 
625 static void
626 on_volume_change_cb(GtkRange *range, GstBin *pipeline)
627 {
628 	GstElement *volume;
629 
630 	g_return_if_fail(pipeline != NULL);
631 
632 	volume = gst_bin_get_by_name(pipeline, "volume");
633 	g_object_set(volume, "volume", gtk_range_get_value(range) / 10.0, NULL);
634 }
635 
636 static gdouble
637 gst_msg_db_to_percent(GstMessage *msg, gchar *value_name)
638 {
639 	const GValue *list;
640 	const GValue *value;
641 	gdouble value_db;
642 	gdouble percent;
643 
644 	list = gst_structure_get_value(
645 				gst_message_get_structure(msg), value_name);
646 #if GST_CHECK_VERSION(1,0,0)
647 G_GNUC_BEGIN_IGNORE_DEPRECATIONS
648 	value = g_value_array_get_nth(g_value_get_boxed(list), 0);
649 G_GNUC_END_IGNORE_DEPRECATIONS
650 #else
651 	value = gst_value_list_get_value(list, 0);
652 #endif
653 	value_db = g_value_get_double(value);
654 	percent = pow(10, value_db / 20);
655 	return (percent > 1.0) ? 1.0 : percent;
656 }
657 
658 typedef struct
659 {
660 	GtkProgressBar *level;
661 	GtkRange *threshold;
662 } BusCbCtx;
663 
664 static gboolean
665 gst_bus_cb(GstBus *bus, GstMessage *msg, BusCbCtx *ctx)
666 {
667 	if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ELEMENT &&
668 		gst_structure_has_name(gst_message_get_structure(msg), "level")) {
669 
670 		GstElement *src = GST_ELEMENT(GST_MESSAGE_SRC(msg));
671 		gchar *name = gst_element_get_name(src);
672 
673 		if (purple_strequal(name, "level")) {
674 			gdouble percent;
675 			gdouble threshold;
676 			GstElement *valve;
677 
678 			percent = gst_msg_db_to_percent(msg, "rms");
679 			percent *= 5;
680 			if (percent > 1.0)
681 				percent = 1.0;
682 			gtk_progress_bar_set_fraction(ctx->level, percent);
683 
684 			percent = gst_msg_db_to_percent(msg, "decay");
685 			threshold = gtk_range_get_value(ctx->threshold) / 100.0;
686 			valve = gst_bin_get_by_name(GST_BIN(GST_ELEMENT_PARENT(src)), "valve");
687 			g_object_set(valve, "drop", (percent < threshold), NULL);
688 			g_object_set(ctx->level,
689 					"text", (percent < threshold) ? _("DROP") : " ", NULL);
690 		}
691 
692 		g_free(name);
693 	}
694 
695 	return TRUE;
696 }
697 
698 static void
699 voice_test_frame_destroy_cb(GtkObject *w, GstElement *pipeline)
700 {
701 	g_return_if_fail(GST_IS_ELEMENT(pipeline));
702 
703 	gst_element_set_state(pipeline, GST_STATE_NULL);
704 	gst_object_unref(pipeline);
705 }
706 
707 static void
708 volume_scale_destroy_cb(GtkRange *volume, gpointer nul)
709 {
710 	purple_prefs_set_int("/purple/media/audio/volume/input",
711 			gtk_range_get_value(volume));
712 }
713 
714 static gchar*
715 threshold_value_format_cb(GtkScale *scale, gdouble value)
716 {
717 	return g_strdup_printf ("%.*f%%", gtk_scale_get_digits(scale), value);
718 }
719 
720 static void
721 threshold_scale_destroy_cb(GtkRange *threshold, gpointer nul)
722 {
723 	purple_prefs_set_int("/purple/media/audio/silence_threshold",
724 			gtk_range_get_value(threshold));
725 }
726 
727 static GtkWidget *
728 get_voice_test_frame(PurplePlugin *plugin)
729 {
730 	GtkWidget *vbox = gtk_vbox_new(FALSE, PIDGIN_HIG_BORDER);
731 	GtkWidget *level = gtk_progress_bar_new();
732 	GtkWidget *volume = gtk_hscale_new_with_range(0, 100, 1);
733 	GtkWidget *threshold = gtk_hscale_new_with_range(0, 100, 1);
734 	GtkWidget *label;
735 	GtkTable *table = GTK_TABLE(gtk_table_new(2, 2, FALSE));
736 
737 	GstElement *pipeline;
738 	GstBus *bus;
739 	BusCbCtx *ctx;
740 
741 	g_object_set(vbox, "width-request", 500, NULL);
742 
743 	gtk_table_set_row_spacings(table, PIDGIN_HIG_BOX_SPACE);
744 	gtk_table_set_col_spacings(table, PIDGIN_HIG_BOX_SPACE);
745 
746 	label = gtk_label_new(_("Volume:"));
747 	g_object_set(label, "xalign", 0.0, NULL);
748 	gtk_table_attach(table, label, 0, 1, 0, 1, GTK_FILL, 0, 0, 0);
749 	gtk_table_attach_defaults(table, volume, 1, 2, 0, 1);
750 	label = gtk_label_new(_("Silence threshold:"));
751 	g_object_set(label, "xalign", 0.0, "yalign", 1.0, NULL);
752 	gtk_table_attach(table, label, 0, 1, 1, 2, GTK_FILL, GTK_FILL, 0, 0);
753 	gtk_table_attach_defaults(table, threshold, 1, 2, 1, 2);
754 
755 	gtk_container_add(GTK_CONTAINER(vbox), level);
756 	gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(table));
757 	gtk_widget_show_all(vbox);
758 
759 	pipeline = create_pipeline();
760 	bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
761 	gst_bus_add_signal_watch(bus);
762 	ctx = g_new(BusCbCtx, 1);
763 	ctx->level = GTK_PROGRESS_BAR(level);
764 	ctx->threshold = GTK_RANGE(threshold);
765 	g_signal_connect_data(bus, "message", G_CALLBACK(gst_bus_cb),
766 			ctx, (GClosureNotify)g_free, 0);
767 	gst_object_unref(bus);
768 
769 	g_signal_connect(volume, "value-changed",
770 			(GCallback)on_volume_change_cb, pipeline);
771 
772 	gtk_range_set_value(GTK_RANGE(volume),
773 			purple_prefs_get_int("/purple/media/audio/volume/input"));
774 	gtk_widget_set(volume, "draw-value", FALSE, NULL);
775 
776 	gtk_range_set_value(GTK_RANGE(threshold),
777 			purple_prefs_get_int("/purple/media/audio/silence_threshold"));
778 
779 	g_signal_connect(vbox, "destroy",
780 			G_CALLBACK(voice_test_frame_destroy_cb), pipeline);
781 	g_signal_connect(volume, "destroy",
782 			G_CALLBACK(volume_scale_destroy_cb), NULL);
783 	g_signal_connect(threshold, "format-value",
784 			G_CALLBACK(threshold_value_format_cb), NULL);
785 	g_signal_connect(threshold, "destroy",
786 			G_CALLBACK(threshold_scale_destroy_cb), NULL);
787 
788 	return vbox;
789 }
790 
791 static GList *
792 actions(PurplePlugin *plugin, gpointer context)
793 {
794 	GList *l = NULL;
795 	PurplePluginAction *act = NULL;
796 
797 	act = purple_plugin_action_new(_("Input and Output Settings"),
798 		show_config);
799 	act->user_data = get_plugin_config_frame;
800 	l = g_list_append(l, act);
801 
802 	act = purple_plugin_action_new(_("Microphone Test"),
803 		show_config);
804 	act->user_data = get_voice_test_frame;
805 	l = g_list_append(l, act);
806 
807 	return l;
808 }
809 
810 static gboolean
811 plugin_unload(PurplePlugin *plugin)
812 {
813 	PurpleMediaManager *manager = purple_media_manager_get();
814 	purple_media_manager_set_active_element(manager, old_video_src);
815 	purple_media_manager_set_active_element(manager, old_video_sink);
816 	purple_media_manager_set_active_element(manager, old_audio_src);
817 	purple_media_manager_set_active_element(manager, old_audio_sink);
818 	return TRUE;
819 }
820 
821 static PidginPluginUiInfo ui_info = {
822 	get_plugin_config_frame,
823 	0,   /* page_num (Reserved) */
824 	/* Padding */
825 	NULL,
826 	NULL,
827 	NULL,
828 	NULL
829 };
830 
831 static PurplePluginInfo info =
832 {
833 	PURPLE_PLUGIN_MAGIC,                         /**< magic          */
834 	PURPLE_MAJOR_VERSION,                        /**< major version  */
835 	PURPLE_MINOR_VERSION,                        /**< minor version  */
836 	PURPLE_PLUGIN_STANDARD,                      /**< type           */
837 	PIDGIN_PLUGIN_TYPE,                          /**< ui_requirement */
838 	0,                                           /**< flags          */
839 	NULL,                                        /**< dependencies   */
840 	PURPLE_PRIORITY_DEFAULT,                     /**< priority       */
841 
842 	"gtk-maiku-vvconfig",                        /**< id             */
843 	N_("Voice/Video Settings"),                  /**< name           */
844 	DISPLAY_VERSION,                             /**< version        */
845 	N_("Configure your microphone and webcam."), /**< summary        */
846 	N_("Configure microphone and webcam "
847 	   "settings for voice/video calls."),       /**< description    */
848 	"Mike Ruprecht <cmaiku@gmail.com>",          /**< author         */
849 	PURPLE_WEBSITE,                              /**< homepage       */
850 
851 	plugin_load,                                 /**< load           */
852 	plugin_unload,                               /**< unload         */
853 	NULL,                                        /**< destroy        */
854 
855 	&ui_info,                                    /**< ui_info        */
856 	NULL,                                        /**< extra_info     */
857 	NULL,                                        /**< prefs_info     */
858 	actions,                                     /**< actions        */
859 
860 	/* padding */
861 	NULL,
862 	NULL,
863 	NULL,
864 	NULL
865 };
866 
867 static void
868 init_plugin(PurplePlugin *plugin) {
869 }
870 
871 PURPLE_INIT_PLUGIN(vvconfig, init_plugin, info)
872