# HG changeset patch
# Node ID b5a0603fc758
# Parent  99658055bb0c
diff --git a/pidgin/meson.build b/pidgin/meson.build
--- a/pidgin/meson.build
+++ b/pidgin/meson.build
@@ -67,6 +67,11 @@ libpidgin_SOURCES = [
 	'prefs/pidginnetworkprefs.c',
 	'prefs/pidginproxyprefs.c',
 ]
+if enable_vv
+	libpidgin_SOURCES += [
+		'prefs/pidginvvprefs.c',
+	]
+endif
 
 libpidgin_headers = [
 	'gtkaccount.h',
@@ -139,6 +144,11 @@ libpidgin_prefs_headers = [
 	'prefs/pidginnetworkprefs.h',
 	'prefs/pidginproxyprefs.h',
 ]
+if enable_vv
+	libpidgin_prefs_headers += [
+		'prefs/pidginvvprefs.h',
+	]
+endif
 
 libpidgin_enum_headers = [
 	'gtkaccount.h',
diff --git a/pidgin/prefs/pidginprefs.c b/pidgin/prefs/pidginprefs.c
--- a/pidgin/prefs/pidginprefs.c
+++ b/pidgin/prefs/pidginprefs.c
@@ -42,6 +42,9 @@
 #include "pidgindebug.h"
 #include "pidginprefs.h"
 #include "pidginprefsinternal.h"
+#ifdef USE_VV
+#include "pidginvvprefs.h"
+#endif
 #include <libsoup/soup.h>
 
 #define PREFS_OPTIMAL_ICON_SIZE 32
@@ -54,30 +57,6 @@ struct _PidginPrefsWindow {
 
 	/* Stack */
 	GtkWidget *stack;
-
-#ifdef USE_VV
-	/* Voice/Video page */
-	struct {
-		struct {
-			PidginPrefCombo input;
-			PidginPrefCombo output;
-			GtkWidget *level;
-			GtkWidget *threshold;
-			GtkWidget *volume;
-			GtkWidget *test;
-			GstElement *pipeline;
-		} voice;
-
-		struct {
-			PidginPrefCombo input;
-			PidginPrefCombo output;
-			GtkWidget *frame;
-			GtkWidget *sink_widget;
-			GtkWidget *test;
-			GstElement *pipeline;
-		} video;
-	} vv;
-#endif
 };
 
 /* Main dialog */
@@ -494,526 +473,30 @@ delete_prefs(GtkWidget *asdf, void *gdsa
 
 #ifdef USE_VV
 static void
-populate_vv_device_menuitems(PurpleMediaElementType type, GtkListStore *store)
-{
-	PurpleMediaManager *manager = NULL;
-	GList *devices;
-
-	gtk_list_store_clear(store);
-
-	manager = purple_media_manager_get();
-	devices = purple_media_manager_enumerate_elements(manager, type);
-	for (; devices; devices = g_list_delete_link(devices, devices)) {
-		PurpleMediaElementInfo *info = devices->data;
-		GtkTreeIter iter;
-		const gchar *name, *id;
-
-		name = purple_media_element_info_get_name(info);
-		id = purple_media_element_info_get_id(info);
-
-		gtk_list_store_append(store, &iter);
-		gtk_list_store_set(store, &iter, PIDGIN_PREF_COMBO_TEXT, name,
-		                   PIDGIN_PREF_COMBO_VALUE, id, -1);
-
-		g_object_unref(info);
-	}
-}
-
-static GstElement *
-create_test_element(PurpleMediaElementType type)
-{
-	PurpleMediaElementInfo *element_info;
-
-	element_info = purple_media_manager_get_active_element(purple_media_manager_get(), type);
-
-	g_return_val_if_fail(element_info, NULL);
-
-	return purple_media_element_info_call_create(element_info,
-		NULL, NULL, NULL);
-}
-
-static void
 vv_test_switch_page_cb(GtkStack *stack, G_GNUC_UNUSED GParamSpec *pspec,
                        gpointer data)
 {
-	PidginPrefsWindow *win = data;
+	PidginVVPrefs *vv_prefs = data;
 
 	if (!g_str_equal(gtk_stack_get_visible_child_name(stack), "vv")) {
 		/* Disable any running test pipelines. */
-		gtk_toggle_button_set_active(
-		        GTK_TOGGLE_BUTTON(win->vv.voice.test), FALSE);
-		gtk_toggle_button_set_active(
-		        GTK_TOGGLE_BUTTON(win->vv.video.test), FALSE);
-	}
-}
-
-static GstElement *
-create_voice_pipeline(void)
-{
-	GstElement *pipeline;
-	GstElement *src, *sink;
-	GstElement *volume;
-	GstElement *level;
-	GstElement *valve;
-
-	pipeline = gst_pipeline_new("voicetest");
-
-	src = create_test_element(PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC);
-	sink = create_test_element(PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SINK);
-	volume = gst_element_factory_make("volume", "volume");
-	level = gst_element_factory_make("level", "level");
-	valve = gst_element_factory_make("valve", "valve");
-
-	gst_bin_add_many(GST_BIN(pipeline), src, volume, level, valve, sink, NULL);
-	gst_element_link_many(src, volume, level, valve, sink, NULL);
-
-	purple_debug_info("gtkprefs", "create_voice_pipeline: setting pipeline "
-		"state to GST_STATE_PLAYING - it may hang here on win32\n");
-	gst_element_set_state(GST_ELEMENT(pipeline), GST_STATE_PLAYING);
-	purple_debug_info("gtkprefs", "create_voice_pipeline: state is set\n");
-
-	return pipeline;
-}
-
-static void
-on_volume_change_cb(GtkWidget *w, gdouble value, gpointer data)
-{
-	PidginPrefsWindow *win = PIDGIN_PREFS_WINDOW(data);
-	GstElement *volume;
-
-	if (!win->vv.voice.pipeline) {
-		return;
-	}
-
-	volume = gst_bin_get_by_name(GST_BIN(win->vv.voice.pipeline), "volume");
-	g_object_set(volume, "volume",
-	             gtk_scale_button_get_value(GTK_SCALE_BUTTON(w)) / 100.0, NULL);
-}
-
-static gdouble
-gst_msg_db_to_percent(GstMessage *msg, gchar *value_name)
-{
-	const GValue *list;
-	const GValue *value;
-	gdouble value_db;
-	gdouble percent;
-
-	list = gst_structure_get_value(gst_message_get_structure(msg), value_name);
-G_GNUC_BEGIN_IGNORE_DEPRECATIONS
-	value = g_value_array_get_nth(g_value_get_boxed(list), 0);
-G_GNUC_END_IGNORE_DEPRECATIONS
-	value_db = g_value_get_double(value);
-	percent = pow(10, value_db / 20);
-	return (percent > 1.0) ? 1.0 : percent;
-}
-
-static gboolean
-gst_bus_cb(GstBus *bus, GstMessage *msg, gpointer data)
-{
-	PidginPrefsWindow *win = PIDGIN_PREFS_WINDOW(data);
-
-	if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ELEMENT &&
-		gst_structure_has_name(gst_message_get_structure(msg), "level")) {
-
-		GstElement *src = GST_ELEMENT(GST_MESSAGE_SRC(msg));
-		gchar *name = gst_element_get_name(src);
-
-		if (purple_strequal(name, "level")) {
-			gdouble percent;
-			gdouble threshold;
-			GstElement *valve;
-
-			percent = gst_msg_db_to_percent(msg, "rms");
-			gtk_progress_bar_set_fraction(
-			        GTK_PROGRESS_BAR(win->vv.voice.level), percent);
-
-			percent = gst_msg_db_to_percent(msg, "decay");
-			threshold = gtk_range_get_value(GTK_RANGE(
-			                    win->vv.voice.threshold)) /
-			            100.0;
-			valve = gst_bin_get_by_name(GST_BIN(GST_ELEMENT_PARENT(src)), "valve");
-			g_object_set(valve, "drop", (percent < threshold), NULL);
-			g_object_set(win->vv.voice.level, "text",
-			             (percent < threshold) ? _("DROP") : " ",
-			             NULL);
-		}
-
-		g_free(name);
-	}
-
-	return TRUE;
-}
-
-static void
-voice_test_destroy_cb(GtkWidget *w, gpointer data)
-{
-	PidginPrefsWindow *win = PIDGIN_PREFS_WINDOW(data);
-
-	if (!win->vv.voice.pipeline) {
-		return;
-	}
-
-	gst_element_set_state(win->vv.voice.pipeline, GST_STATE_NULL);
-	g_clear_pointer(&win->vv.voice.pipeline, gst_object_unref);
-}
-
-static void
-enable_voice_test(PidginPrefsWindow *win)
-{
-	GstBus *bus;
-
-	win->vv.voice.pipeline = create_voice_pipeline();
-	bus = gst_pipeline_get_bus(GST_PIPELINE(win->vv.voice.pipeline));
-	gst_bus_add_signal_watch(bus);
-	g_signal_connect(bus, "message", G_CALLBACK(gst_bus_cb), win);
-	gst_object_unref(bus);
-}
-
-static void
-toggle_voice_test_cb(GtkToggleButton *test, gpointer data)
-{
-	PidginPrefsWindow *win = PIDGIN_PREFS_WINDOW(data);
-
-	if (gtk_toggle_button_get_active(test)) {
-		gtk_widget_set_sensitive(win->vv.voice.level, TRUE);
-		enable_voice_test(win);
-
-		g_signal_connect(win->vv.voice.volume, "value-changed",
-		                 G_CALLBACK(on_volume_change_cb), win);
-		g_signal_connect(test, "destroy",
-		                 G_CALLBACK(voice_test_destroy_cb), win);
-	} else {
-		gtk_progress_bar_set_fraction(
-		        GTK_PROGRESS_BAR(win->vv.voice.level), 0.0);
-		gtk_widget_set_sensitive(win->vv.voice.level, FALSE);
-		g_object_disconnect(win->vv.voice.volume,
-		                    "any-signal::value-changed",
-		                    G_CALLBACK(on_volume_change_cb), win, NULL);
-		g_object_disconnect(test, "any-signal::destroy",
-		                    G_CALLBACK(voice_test_destroy_cb), win,
-		                    NULL);
-		voice_test_destroy_cb(NULL, win);
+		pidgin_vv_prefs_disable_test_pipelines(vv_prefs);
 	}
 }
-
-static void
-volume_changed_cb(GtkScaleButton *button, gdouble value, gpointer data)
-{
-	purple_prefs_set_int("/purple/media/audio/volume/input", value * 100);
-}
-
-static void
-threshold_value_changed_cb(GtkScale *scale, GtkWidget *label)
-{
-	int value;
-	char *tmp;
-
-	value = (int)gtk_range_get_value(GTK_RANGE(scale));
-	tmp = g_strdup_printf(_("Silence threshold: %d%%"), value);
-	gtk_label_set_label(GTK_LABEL(label), tmp);
-	g_free(tmp);
-
-	purple_prefs_set_int("/purple/media/audio/silence_threshold", value);
-}
-
-static void
-bind_voice_test(PidginPrefsWindow *win, GtkBuilder *builder)
-{
-	GObject *test;
-	GObject *label;
-	GObject *volume;
-	GObject *threshold;
-	char *tmp;
-
-	volume = gtk_builder_get_object(builder, "vv.voice.volume");
-	win->vv.voice.volume = GTK_WIDGET(volume);
-	gtk_scale_button_set_value(GTK_SCALE_BUTTON(volume),
-			purple_prefs_get_int("/purple/media/audio/volume/input") / 100.0);
-	g_signal_connect(volume, "value-changed",
-	                 G_CALLBACK(volume_changed_cb), NULL);
-
-	label = gtk_builder_get_object(builder, "vv.voice.threshold_label");
-	tmp = g_strdup_printf(_("Silence threshold: %d%%"),
-	                      purple_prefs_get_int("/purple/media/audio/silence_threshold"));
-	gtk_label_set_text(GTK_LABEL(label), tmp);
-	g_free(tmp);
-
-	threshold = gtk_builder_get_object(builder, "vv.voice.threshold");
-	win->vv.voice.threshold = GTK_WIDGET(threshold);
-	gtk_range_set_value(GTK_RANGE(threshold),
-			purple_prefs_get_int("/purple/media/audio/silence_threshold"));
-	g_signal_connect(threshold, "value-changed",
-	                 G_CALLBACK(threshold_value_changed_cb), label);
-
-	win->vv.voice.level =
-	        GTK_WIDGET(gtk_builder_get_object(builder, "vv.voice.level"));
-
-	test = gtk_builder_get_object(builder, "vv.voice.test");
-	g_signal_connect(test, "toggled",
-	                 G_CALLBACK(toggle_voice_test_cb), win);
-	win->vv.voice.test = GTK_WIDGET(test);
-}
-
-static GstElement *
-create_video_pipeline(void)
-{
-	GstElement *pipeline;
-	GstElement *src, *sink;
-	GstElement *videoconvert;
-	GstElement *videoscale;
-
-	pipeline = gst_pipeline_new("videotest");
-	src = create_test_element(PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC);
-	sink = create_test_element(PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK);
-	videoconvert = gst_element_factory_make("videoconvert", NULL);
-	videoscale = gst_element_factory_make("videoscale", NULL);
-
-	g_object_set_data(G_OBJECT(pipeline), "sink", sink);
-
-	gst_bin_add_many(GST_BIN(pipeline), src, videoconvert, videoscale, sink,
-			NULL);
-	gst_element_link_many(src, videoconvert, videoscale, sink, NULL);
-
-	return pipeline;
-}
-
-static void
-video_test_destroy_cb(GtkWidget *w, gpointer data)
-{
-	PidginPrefsWindow *win = PIDGIN_PREFS_WINDOW(data);
-
-	if (!win->vv.video.pipeline) {
-		return;
-	}
-
-	gst_element_set_state(win->vv.video.pipeline, GST_STATE_NULL);
-	g_clear_pointer(&win->vv.video.pipeline, gst_object_unref);
-}
-
-static void
-enable_video_test(PidginPrefsWindow *win)
-{
-	GtkWidget *video = NULL;
-	GstElement *sink = NULL;
-
-	win->vv.video.pipeline = create_video_pipeline();
-
-	sink = g_object_get_data(G_OBJECT(win->vv.video.pipeline), "sink");
-	g_object_get(sink, "widget", &video, NULL);
-	gtk_widget_show(video);
-
-	g_clear_pointer(&win->vv.video.sink_widget, gtk_widget_destroy);
-	gtk_container_add(GTK_CONTAINER(win->vv.video.frame), video);
-	win->vv.video.sink_widget = video;
-
-	gst_element_set_state(GST_ELEMENT(win->vv.video.pipeline),
-	                      GST_STATE_PLAYING);
-}
-
-static void
-toggle_video_test_cb(GtkToggleButton *test, gpointer data)
-{
-	PidginPrefsWindow *win = PIDGIN_PREFS_WINDOW(data);
-
-	if (gtk_toggle_button_get_active(test)) {
-		enable_video_test(win);
-		g_signal_connect(test, "destroy",
-		                 G_CALLBACK(video_test_destroy_cb), win);
-	} else {
-		g_object_disconnect(test, "any-signal::destroy",
-		                    G_CALLBACK(video_test_destroy_cb), win,
-		                    NULL);
-		video_test_destroy_cb(NULL, win);
-	}
-}
-
-static void
-bind_video_test(PidginPrefsWindow *win, GtkBuilder *builder)
-{
-	GObject *test;
-
-	win->vv.video.frame = GTK_WIDGET(
-	        gtk_builder_get_object(builder, "vv.video.frame"));
-	test = gtk_builder_get_object(builder, "vv.video.test");
-	g_signal_connect(test, "toggled",
-	                 G_CALLBACK(toggle_video_test_cb), win);
-	win->vv.video.test = GTK_WIDGET(test);
-}
-
-static void
-vv_device_changed_cb(const gchar *name, PurplePrefType type,
-                     gconstpointer value, gpointer data)
-{
-	PidginPrefsWindow *win = PIDGIN_PREFS_WINDOW(data);
-
-	PurpleMediaManager *manager;
-	PurpleMediaElementInfo *info;
-
-	manager = purple_media_manager_get();
-	info = purple_media_manager_get_element_info(manager, value);
-	purple_media_manager_set_active_element(manager, info);
-
-	/* Refresh test viewers */
-	if (strstr(name, "audio") && win->vv.voice.pipeline) {
-		voice_test_destroy_cb(NULL, win);
-		enable_voice_test(win);
-	} else if (strstr(name, "video") && win->vv.video.pipeline) {
-		video_test_destroy_cb(NULL, win);
-		enable_video_test(win);
-	}
-}
-
-static const char *
-purple_media_type_to_preference_key(PurpleMediaElementType type)
-{
-	if (type & PURPLE_MEDIA_ELEMENT_AUDIO) {
-		if (type & PURPLE_MEDIA_ELEMENT_SRC) {
-			return PIDGIN_PREFS_ROOT "/vvconfig/audio/src/device";
-		} else if (type & PURPLE_MEDIA_ELEMENT_SINK) {
-			return PIDGIN_PREFS_ROOT "/vvconfig/audio/sink/device";
-		}
-	} else if (type & PURPLE_MEDIA_ELEMENT_VIDEO) {
-		if (type & PURPLE_MEDIA_ELEMENT_SRC) {
-			return PIDGIN_PREFS_ROOT "/vvconfig/video/src/device";
-		} else if (type & PURPLE_MEDIA_ELEMENT_SINK) {
-			return PIDGIN_PREFS_ROOT "/vvconfig/video/sink/device";
-		}
-	}
-
-	return NULL;
-}
-
-static void
-bind_vv_dropdown(PidginPrefCombo *combo, PurpleMediaElementType element_type)
-{
-	const gchar *preference_key;
-	GtkTreeModel *model;
-
-	preference_key = purple_media_type_to_preference_key(element_type);
-	model = gtk_combo_box_get_model(GTK_COMBO_BOX(combo->combo));
-	populate_vv_device_menuitems(element_type, GTK_LIST_STORE(model));
-
-	combo->type = PURPLE_PREF_STRING;
-	combo->key = preference_key;
-	pidgin_prefs_bind_dropdown(combo);
-}
-
-static void
-bind_vv_frame(PidginPrefsWindow *win, PidginPrefCombo *combo,
-              PurpleMediaElementType type)
-{
-	bind_vv_dropdown(combo, type);
-
-	purple_prefs_connect_callback(combo->combo,
-	                              purple_media_type_to_preference_key(type),
-	                              vv_device_changed_cb, win);
-	g_signal_connect_swapped(combo->combo, "destroy",
-	                         G_CALLBACK(purple_prefs_disconnect_by_handle),
-	                         combo->combo);
-
-	g_object_set_data(G_OBJECT(combo->combo), "vv_media_type",
-	                  (gpointer)type);
-	g_object_set_data(G_OBJECT(combo->combo), "vv_combo", combo);
-}
-
-static void
-device_list_changed_cb(PurpleMediaManager *manager, GtkWidget *widget)
-{
-	PidginPrefCombo *combo;
-	PurpleMediaElementType media_type;
-	guint signal_id;
-	GtkTreeModel *model;
-
-	combo = g_object_get_data(G_OBJECT(widget), "vv_combo");
-	media_type = (PurpleMediaElementType)g_object_get_data(G_OBJECT(widget),
-			"vv_media_type");
-
-	/* Block signals so pref doesn't get re-saved while changing UI. */
-	signal_id = g_signal_lookup("changed", GTK_TYPE_COMBO_BOX);
-	g_signal_handlers_block_matched(combo->combo, G_SIGNAL_MATCH_ID, signal_id,
-	                                0, NULL, NULL, NULL);
-
-	model = gtk_combo_box_get_model(GTK_COMBO_BOX(combo->combo));
-	populate_vv_device_menuitems(media_type, GTK_LIST_STORE(model));
-
-	g_signal_handlers_unblock_matched(combo->combo, G_SIGNAL_MATCH_ID,
-	                                  signal_id, 0, NULL, NULL, NULL);
-}
-
-static GtkWidget *
-vv_page(PidginPrefsWindow *win)
-{
-	GtkBuilder *builder;
-	GtkWidget *ret;
-	PurpleMediaManager *manager;
-
-	builder = gtk_builder_new_from_resource("/im/pidgin/Pidgin3/Prefs/vv.ui");
-	gtk_builder_set_translation_domain(builder, PACKAGE);
-
-	ret = GTK_WIDGET(gtk_builder_get_object(builder, "vv.page"));
-
-	manager = purple_media_manager_get();
-
-	win->vv.voice.input.combo = GTK_WIDGET(
-	        gtk_builder_get_object(builder, "vv.voice.input.combo"));
-	bind_vv_frame(win, &win->vv.voice.input,
-	              PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC);
-	g_signal_connect_object(manager, "elements-changed::audiosrc",
-	                        G_CALLBACK(device_list_changed_cb),
-	                        win->vv.voice.input.combo, 0);
-
-	win->vv.voice.output.combo = GTK_WIDGET(
-	        gtk_builder_get_object(builder, "vv.voice.output.combo"));
-	bind_vv_frame(win, &win->vv.voice.output,
-	              PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SINK);
-	g_signal_connect_object(manager, "elements-changed::audiosink",
-	                        G_CALLBACK(device_list_changed_cb),
-	                        win->vv.voice.output.combo, 0);
-
-	bind_voice_test(win, builder);
-
-	win->vv.video.input.combo = GTK_WIDGET(
-	        gtk_builder_get_object(builder, "vv.video.input.combo"));
-	bind_vv_frame(win, &win->vv.video.input,
-	              PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC);
-	g_signal_connect_object(manager, "elements-changed::videosrc",
-	                        G_CALLBACK(device_list_changed_cb),
-	                        win->vv.video.input.combo, 0);
-
-	win->vv.video.output.combo = GTK_WIDGET(
-	        gtk_builder_get_object(builder, "vv.video.output.combo"));
-	bind_vv_frame(win, &win->vv.video.output,
-	              PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK);
-	g_signal_connect_object(manager, "elements-changed::videosink",
-	                        G_CALLBACK(device_list_changed_cb),
-	                        win->vv.video.output.combo, 0);
-
-	bind_video_test(win, builder);
-
-	g_signal_connect(win->stack, "notify::visible-child",
-	                 G_CALLBACK(vv_test_switch_page_cb), win);
-
-	g_object_ref(ret);
-	g_object_unref(builder);
-
-	return ret;
-}
 #endif
 
 static void
 prefs_stack_init(PidginPrefsWindow *win)
 {
 #ifdef USE_VV
-	GtkStack *stack = GTK_STACK(win->stack);
 	GtkWidget *vv;
-#endif
 
-#ifdef USE_VV
-	vv = vv_page(win);
-	gtk_container_add_with_properties(GTK_CONTAINER(stack), vv, "name",
+	vv = pidgin_vv_prefs_new();
+	gtk_container_add_with_properties(GTK_CONTAINER(win->stack), vv, "name",
 	                                  "vv", "title", _("Voice/Video"),
 	                                  NULL);
-	g_object_unref(vv);
+	g_signal_connect(win->stack, "notify::visible-child",
+	                 G_CALLBACK(vv_test_switch_page_cb), vv);
 #endif
 }
 
diff --git a/pidgin/prefs/pidginvvprefs.h b/pidgin/prefs/pidginvvprefs.h
new file mode 100644
--- /dev/null
+++ b/pidgin/prefs/pidginvvprefs.h
@@ -0,0 +1,73 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PIDGIN_GLOBAL_HEADER_INSIDE) && !defined(PIDGIN_COMPILATION)
+# error "only <pidgin.h> may be included directly"
+#endif
+
+#ifndef PIDGIN_VV_PREFS_H
+#define PIDGIN_VV_PREFS_H
+
+#include <glib.h>
+
+#include <gtk/gtk.h>
+#include <handy.h>
+
+G_BEGIN_DECLS
+
+/**
+ * PidginVVPrefs:
+ *
+ * #PidginVVPrefs is a widget for the preferences window to let users
+ * choose and configure their voice and video settings.
+ *
+ * Since: 3.0.0
+ */
+#define PIDGIN_TYPE_VV_PREFS (pidgin_vv_prefs_get_type())
+G_DECLARE_FINAL_TYPE(PidginVVPrefs, pidgin_vv_prefs,
+                     PIDGIN, VV_PREFS, HdyPreferencesPage)
+
+/**
+ * pidgin_vv_prefs_new:
+ *
+ * Creates a new #PidginVVPrefs instance.
+ *
+ * Returns: (transfer full): The new #PidginVVPrefs instance.
+ *
+ * Since: 3.0.0
+ */
+GtkWidget *pidgin_vv_prefs_new(void);
+
+/**
+ * pidgin_vv_prefs_disable_test_pipelines:
+ * @prefs: The #PidginVVPrefs instance.
+ *
+ * Disable any test pipelines that may be playing on this widget. This may be
+ * used when switching focus or views to a different widget.
+ *
+ * Since: 3.0.0
+ */
+void pidgin_vv_prefs_disable_test_pipelines(PidginVVPrefs *prefs);
+
+G_END_DECLS
+
+#endif /* PIDGIN_VV_PREFS_H */
diff --git a/pidgin/prefs/pidginvvprefs.c b/pidgin/prefs/pidginvvprefs.c
new file mode 100644
--- /dev/null
+++ b/pidgin/prefs/pidginvvprefs.c
@@ -0,0 +1,568 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <math.h>
+
+#include <glib/gi18n-lib.h>
+
+#include <purple.h>
+
+#include <handy.h>
+
+#include "pidginvvprefs.h"
+#include "pidgincore.h"
+#include "pidginprefsinternal.h"
+
+struct _PidginVVPrefs {
+	HdyPreferencesPage parent;
+
+	struct {
+		PidginPrefCombo input;
+		PidginPrefCombo output;
+		GtkWidget *level;
+		GtkWidget *threshold_label;
+		GtkWidget *threshold;
+		GtkWidget *volume;
+		GtkWidget *test;
+		GstElement *pipeline;
+	} voice;
+
+	struct {
+		PidginPrefCombo input;
+		PidginPrefCombo output;
+		GtkWidget *frame;
+		GtkWidget *sink_widget;
+		GtkWidget *test;
+		GstElement *pipeline;
+	} video;
+};
+
+G_DEFINE_TYPE(PidginVVPrefs, pidgin_vv_prefs, HDY_TYPE_PREFERENCES_PAGE)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+populate_vv_device_menuitems(PurpleMediaElementType type, GtkListStore *store)
+{
+	PurpleMediaManager *manager = NULL;
+	GList *devices;
+
+	gtk_list_store_clear(store);
+
+	manager = purple_media_manager_get();
+	devices = purple_media_manager_enumerate_elements(manager, type);
+	for (; devices; devices = g_list_delete_link(devices, devices)) {
+		PurpleMediaElementInfo *info = devices->data;
+		GtkTreeIter iter;
+		const gchar *name, *id;
+
+		name = purple_media_element_info_get_name(info);
+		id = purple_media_element_info_get_id(info);
+
+		gtk_list_store_append(store, &iter);
+		gtk_list_store_set(store, &iter, PIDGIN_PREF_COMBO_TEXT, name,
+		                   PIDGIN_PREF_COMBO_VALUE, id, -1);
+
+		g_object_unref(info);
+	}
+}
+
+static GstElement *
+create_test_element(PurpleMediaElementType type)
+{
+	PurpleMediaElementInfo *element_info;
+
+	element_info = purple_media_manager_get_active_element(purple_media_manager_get(), type);
+
+	g_return_val_if_fail(element_info, NULL);
+
+	return purple_media_element_info_call_create(element_info,
+		NULL, NULL, NULL);
+}
+
+static GstElement *
+create_voice_pipeline(void)
+{
+	GstElement *pipeline;
+	GstElement *src, *sink;
+	GstElement *volume;
+	GstElement *level;
+	GstElement *valve;
+
+	pipeline = gst_pipeline_new("voicetest");
+
+	src = create_test_element(PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC);
+	sink = create_test_element(PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SINK);
+	volume = gst_element_factory_make("volume", "volume");
+	level = gst_element_factory_make("level", "level");
+	valve = gst_element_factory_make("valve", "valve");
+
+	gst_bin_add_many(GST_BIN(pipeline), src, volume, level, valve, sink, NULL);
+	gst_element_link_many(src, volume, level, valve, sink, NULL);
+
+	purple_debug_info("gtkprefs", "create_voice_pipeline: setting pipeline "
+		"state to GST_STATE_PLAYING - it may hang here on win32\n");
+	gst_element_set_state(GST_ELEMENT(pipeline), GST_STATE_PLAYING);
+	purple_debug_info("gtkprefs", "create_voice_pipeline: state is set\n");
+
+	return pipeline;
+}
+
+static void
+on_volume_change_cb(GtkWidget *w, gdouble value, gpointer data)
+{
+	PidginVVPrefs *prefs = PIDGIN_VV_PREFS(data);
+	GstElement *volume;
+
+	if (!prefs->voice.pipeline) {
+		return;
+	}
+
+	volume = gst_bin_get_by_name(GST_BIN(prefs->voice.pipeline), "volume");
+	g_object_set(volume, "volume",
+	             gtk_scale_button_get_value(GTK_SCALE_BUTTON(w)) / 100.0, NULL);
+}
+
+static gdouble
+gst_msg_db_to_percent(GstMessage *msg, gchar *value_name)
+{
+	const GValue *list;
+	const GValue *value;
+	gdouble value_db;
+	gdouble percent;
+
+	list = gst_structure_get_value(gst_message_get_structure(msg), value_name);
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+	value = g_value_array_get_nth(g_value_get_boxed(list), 0);
+G_GNUC_END_IGNORE_DEPRECATIONS
+	value_db = g_value_get_double(value);
+	percent = pow(10, value_db / 20);
+	return (percent > 1.0) ? 1.0 : percent;
+}
+
+static gboolean
+gst_bus_cb(GstBus *bus, GstMessage *msg, gpointer data)
+{
+	PidginVVPrefs *prefs = PIDGIN_VV_PREFS(data);
+
+	if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ELEMENT &&
+		gst_structure_has_name(gst_message_get_structure(msg), "level")) {
+
+		GstElement *src = GST_ELEMENT(GST_MESSAGE_SRC(msg));
+		gchar *name = gst_element_get_name(src);
+
+		if (purple_strequal(name, "level")) {
+			gdouble percent;
+			gdouble threshold;
+			GstElement *valve;
+
+			percent = gst_msg_db_to_percent(msg, "rms");
+			gtk_progress_bar_set_fraction(
+			        GTK_PROGRESS_BAR(prefs->voice.level), percent);
+
+			percent = gst_msg_db_to_percent(msg, "decay");
+			threshold = gtk_range_get_value(GTK_RANGE(
+			                    prefs->voice.threshold)) /
+			            100.0;
+			valve = gst_bin_get_by_name(GST_BIN(GST_ELEMENT_PARENT(src)), "valve");
+			g_object_set(valve, "drop", (percent < threshold), NULL);
+			g_object_set(prefs->voice.level, "text",
+			             (percent < threshold) ? _("DROP") : " ",
+			             NULL);
+		}
+
+		g_free(name);
+	}
+
+	return TRUE;
+}
+
+static void
+voice_test_destroy_cb(GtkWidget *w, gpointer data)
+{
+	PidginVVPrefs *prefs = PIDGIN_VV_PREFS(data);
+
+	if (!prefs->voice.pipeline) {
+		return;
+	}
+
+	gst_element_set_state(prefs->voice.pipeline, GST_STATE_NULL);
+	g_clear_pointer(&prefs->voice.pipeline, gst_object_unref);
+}
+
+static void
+enable_voice_test(PidginVVPrefs *prefs)
+{
+	GstBus *bus;
+
+	prefs->voice.pipeline = create_voice_pipeline();
+	bus = gst_pipeline_get_bus(GST_PIPELINE(prefs->voice.pipeline));
+	gst_bus_add_signal_watch(bus);
+	g_signal_connect(bus, "message", G_CALLBACK(gst_bus_cb), prefs);
+	gst_object_unref(bus);
+}
+
+static void
+toggle_voice_test_cb(GtkToggleButton *test, gpointer data)
+{
+	PidginVVPrefs *prefs = PIDGIN_VV_PREFS(data);
+
+	if (gtk_toggle_button_get_active(test)) {
+		gtk_widget_set_sensitive(prefs->voice.level, TRUE);
+		enable_voice_test(prefs);
+
+		g_signal_connect(prefs->voice.volume, "value-changed",
+		                 G_CALLBACK(on_volume_change_cb), prefs);
+		g_signal_connect(test, "destroy",
+		                 G_CALLBACK(voice_test_destroy_cb), prefs);
+	} else {
+		gtk_progress_bar_set_fraction(
+		        GTK_PROGRESS_BAR(prefs->voice.level), 0.0);
+		gtk_widget_set_sensitive(prefs->voice.level, FALSE);
+		g_object_disconnect(prefs->voice.volume,
+		                    "any-signal::value-changed",
+		                    G_CALLBACK(on_volume_change_cb), prefs, NULL);
+		g_object_disconnect(test, "any-signal::destroy",
+		                    G_CALLBACK(voice_test_destroy_cb), prefs,
+		                    NULL);
+		voice_test_destroy_cb(NULL, prefs);
+	}
+}
+
+static void
+volume_changed_cb(GtkScaleButton *button, gdouble value, gpointer data)
+{
+	purple_prefs_set_int("/purple/media/audio/volume/input", value * 100);
+}
+
+static void
+threshold_value_changed_cb(GtkScale *scale, gpointer data)
+{
+	PidginVVPrefs *prefs = data;
+	int value;
+	char *tmp;
+
+	value = (int)gtk_range_get_value(GTK_RANGE(scale));
+	tmp = g_strdup_printf(_("Silence threshold: %d%%"), value);
+	gtk_label_set_label(GTK_LABEL(prefs->voice.threshold_label), tmp);
+	g_free(tmp);
+
+	purple_prefs_set_int("/purple/media/audio/silence_threshold", value);
+}
+
+static void
+bind_voice_test(PidginVVPrefs *prefs)
+{
+	char *tmp;
+
+	gtk_scale_button_set_value(GTK_SCALE_BUTTON(prefs->voice.volume),
+			purple_prefs_get_int("/purple/media/audio/volume/input") / 100.0);
+
+	tmp = g_strdup_printf(_("Silence threshold: %d%%"),
+	                      purple_prefs_get_int("/purple/media/audio/silence_threshold"));
+	gtk_label_set_text(GTK_LABEL(prefs->voice.threshold_label), tmp);
+	g_free(tmp);
+
+	gtk_range_set_value(GTK_RANGE(prefs->voice.threshold),
+			purple_prefs_get_int("/purple/media/audio/silence_threshold"));
+}
+
+static GstElement *
+create_video_pipeline(void)
+{
+	GstElement *pipeline;
+	GstElement *src, *sink;
+	GstElement *videoconvert;
+	GstElement *videoscale;
+
+	pipeline = gst_pipeline_new("videotest");
+	src = create_test_element(PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC);
+	sink = create_test_element(PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK);
+	videoconvert = gst_element_factory_make("videoconvert", NULL);
+	videoscale = gst_element_factory_make("videoscale", NULL);
+
+	g_object_set_data(G_OBJECT(pipeline), "sink", sink);
+
+	gst_bin_add_many(GST_BIN(pipeline), src, videoconvert, videoscale, sink,
+			NULL);
+	gst_element_link_many(src, videoconvert, videoscale, sink, NULL);
+
+	return pipeline;
+}
+
+static void
+video_test_destroy_cb(GtkWidget *w, gpointer data)
+{
+	PidginVVPrefs *prefs = PIDGIN_VV_PREFS(data);
+
+	if (!prefs->video.pipeline) {
+		return;
+	}
+
+	gst_element_set_state(prefs->video.pipeline, GST_STATE_NULL);
+	g_clear_pointer(&prefs->video.pipeline, gst_object_unref);
+}
+
+static void
+enable_video_test(PidginVVPrefs *prefs)
+{
+	GtkWidget *video = NULL;
+	GstElement *sink = NULL;
+
+	prefs->video.pipeline = create_video_pipeline();
+
+	sink = g_object_get_data(G_OBJECT(prefs->video.pipeline), "sink");
+	g_object_get(sink, "widget", &video, NULL);
+	gtk_widget_show(video);
+
+	g_clear_pointer(&prefs->video.sink_widget, gtk_widget_destroy);
+	gtk_widget_set_size_request(prefs->video.frame, 400, 300);
+	gtk_container_add(GTK_CONTAINER(prefs->video.frame), video);
+	prefs->video.sink_widget = video;
+
+	gst_element_set_state(GST_ELEMENT(prefs->video.pipeline),
+	                      GST_STATE_PLAYING);
+}
+
+static void
+toggle_video_test_cb(GtkToggleButton *test, gpointer data)
+{
+	PidginVVPrefs *prefs = PIDGIN_VV_PREFS(data);
+
+	if (gtk_toggle_button_get_active(test)) {
+		enable_video_test(prefs);
+		g_signal_connect(test, "destroy",
+		                 G_CALLBACK(video_test_destroy_cb), prefs);
+	} else {
+		g_object_disconnect(test, "any-signal::destroy",
+		                    G_CALLBACK(video_test_destroy_cb), prefs,
+		                    NULL);
+		video_test_destroy_cb(NULL, prefs);
+	}
+}
+
+static void
+vv_device_changed_cb(const gchar *name, PurplePrefType type,
+                     gconstpointer value, gpointer data)
+{
+	PidginVVPrefs *prefs = PIDGIN_VV_PREFS(data);
+
+	PurpleMediaManager *manager;
+	PurpleMediaElementInfo *info;
+
+	manager = purple_media_manager_get();
+	info = purple_media_manager_get_element_info(manager, value);
+	purple_media_manager_set_active_element(manager, info);
+
+	/* Refresh test viewers */
+	if (strstr(name, "audio") && prefs->voice.pipeline) {
+		voice_test_destroy_cb(NULL, prefs);
+		enable_voice_test(prefs);
+	} else if (strstr(name, "video") && prefs->video.pipeline) {
+		video_test_destroy_cb(NULL, prefs);
+		enable_video_test(prefs);
+	}
+}
+
+static const char *
+purple_media_type_to_preference_key(PurpleMediaElementType type)
+{
+	if (type & PURPLE_MEDIA_ELEMENT_AUDIO) {
+		if (type & PURPLE_MEDIA_ELEMENT_SRC) {
+			return PIDGIN_PREFS_ROOT "/vvconfig/audio/src/device";
+		} else if (type & PURPLE_MEDIA_ELEMENT_SINK) {
+			return PIDGIN_PREFS_ROOT "/vvconfig/audio/sink/device";
+		}
+	} else if (type & PURPLE_MEDIA_ELEMENT_VIDEO) {
+		if (type & PURPLE_MEDIA_ELEMENT_SRC) {
+			return PIDGIN_PREFS_ROOT "/vvconfig/video/src/device";
+		} else if (type & PURPLE_MEDIA_ELEMENT_SINK) {
+			return PIDGIN_PREFS_ROOT "/vvconfig/video/sink/device";
+		}
+	}
+
+	return NULL;
+}
+
+static void
+bind_vv_dropdown(PidginPrefCombo *combo, PurpleMediaElementType element_type)
+{
+	const gchar *preference_key;
+	GtkTreeModel *model;
+
+	preference_key = purple_media_type_to_preference_key(element_type);
+	model = gtk_combo_box_get_model(GTK_COMBO_BOX(combo->combo));
+	populate_vv_device_menuitems(element_type, GTK_LIST_STORE(model));
+
+	combo->type = PURPLE_PREF_STRING;
+	combo->key = preference_key;
+	pidgin_prefs_bind_dropdown(combo);
+}
+
+static void
+bind_vv_frame(PidginVVPrefs *prefs, PidginPrefCombo *combo,
+              PurpleMediaElementType type)
+{
+	bind_vv_dropdown(combo, type);
+
+	purple_prefs_connect_callback(combo->combo,
+	                              purple_media_type_to_preference_key(type),
+	                              vv_device_changed_cb, prefs);
+	g_signal_connect_swapped(combo->combo, "destroy",
+	                         G_CALLBACK(purple_prefs_disconnect_by_handle),
+	                         combo->combo);
+
+	g_object_set_data(G_OBJECT(combo->combo), "vv_media_type",
+	                  (gpointer)type);
+	g_object_set_data(G_OBJECT(combo->combo), "vv_combo", combo);
+}
+
+static void
+device_list_changed_cb(PurpleMediaManager *manager, GtkWidget *widget)
+{
+	PidginPrefCombo *combo;
+	PurpleMediaElementType media_type;
+	const gchar *preference_key;
+	guint signal_id;
+	GtkTreeModel *model;
+
+	combo = g_object_get_data(G_OBJECT(widget), "vv_combo");
+	media_type = (PurpleMediaElementType)GPOINTER_TO_INT(g_object_get_data(
+			G_OBJECT(widget),
+			"vv_media_type"));
+	preference_key = purple_media_type_to_preference_key(media_type);
+
+	/* Block signals so pref doesn't get re-saved while changing UI. */
+	signal_id = g_signal_lookup("changed", GTK_TYPE_COMBO_BOX);
+	g_signal_handlers_block_matched(combo->combo, G_SIGNAL_MATCH_ID, signal_id,
+	                                0, NULL, NULL, NULL);
+
+	model = gtk_combo_box_get_model(GTK_COMBO_BOX(combo->combo));
+	populate_vv_device_menuitems(media_type, GTK_LIST_STORE(model));
+	gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo->combo),
+	                            purple_prefs_get_string(preference_key));
+
+	g_signal_handlers_unblock_matched(combo->combo, G_SIGNAL_MATCH_ID,
+	                                  signal_id, 0, NULL, NULL, NULL);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+pidgin_vv_prefs_class_init(PidginVVPrefsClass *klass)
+{
+	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+	gtk_widget_class_set_template_from_resource(
+		widget_class,
+		"/im/pidgin/Pidgin3/Prefs/vv.ui"
+	);
+
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     voice.input.combo);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     voice.output.combo);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     voice.volume);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     voice.threshold_label);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     voice.threshold);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     voice.level);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     voice.test);
+	gtk_widget_class_bind_template_callback(widget_class, volume_changed_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        threshold_value_changed_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        toggle_voice_test_cb);
+
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     video.input.combo);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     video.output.combo);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     video.frame);
+	gtk_widget_class_bind_template_child(widget_class, PidginVVPrefs,
+	                                     video.test);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        toggle_video_test_cb);
+}
+
+static void
+pidgin_vv_prefs_init(PidginVVPrefs *prefs)
+{
+	PurpleMediaManager *manager = NULL;
+
+	gtk_widget_init_template(GTK_WIDGET(prefs));
+
+	manager = purple_media_manager_get();
+
+	bind_vv_frame(prefs, &prefs->voice.input,
+	              PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC);
+	g_signal_connect_object(manager, "elements-changed::audiosrc",
+	                        G_CALLBACK(device_list_changed_cb),
+	                        prefs->voice.input.combo, 0);
+
+	bind_vv_frame(prefs, &prefs->voice.output,
+	              PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SINK);
+	g_signal_connect_object(manager, "elements-changed::audiosink",
+	                        G_CALLBACK(device_list_changed_cb),
+	                        prefs->voice.output.combo, 0);
+
+	bind_voice_test(prefs);
+
+	bind_vv_frame(prefs, &prefs->video.input,
+	              PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC);
+	g_signal_connect_object(manager, "elements-changed::videosrc",
+	                        G_CALLBACK(device_list_changed_cb),
+	                        prefs->video.input.combo, 0);
+
+	bind_vv_frame(prefs, &prefs->video.output,
+	              PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK);
+	g_signal_connect_object(manager, "elements-changed::videosink",
+	                        G_CALLBACK(device_list_changed_cb),
+	                        prefs->video.output.combo, 0);
+}
+
+/******************************************************************************
+ * API
+ *****************************************************************************/
+GtkWidget *
+pidgin_vv_prefs_new(void) {
+	return GTK_WIDGET(g_object_new(PIDGIN_TYPE_VV_PREFS, NULL));
+}
+
+void
+pidgin_vv_prefs_disable_test_pipelines(PidginVVPrefs *prefs) {
+	g_return_if_fail(PIDGIN_IS_VV_PREFS(prefs));
+
+	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(prefs->voice.test), FALSE);
+	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(prefs->video.test), FALSE);
+}
diff --git a/pidgin/resources/Prefs/vv.ui b/pidgin/resources/Prefs/vv.ui
--- a/pidgin/resources/Prefs/vv.ui
+++ b/pidgin/resources/Prefs/vv.ui
@@ -35,7 +35,7 @@ Foundation, Inc., 51 Franklin Street, Fi
     <property name="step-increment">1</property>
     <property name="page-increment">10</property>
   </object>
-  <object class="GtkListStore" id="vv.video.input.store">
+  <object class="GtkListStore" id="video.input.store">
     <columns>
       <!-- column-name text -->
       <column type="gchararray"/>
@@ -43,7 +43,7 @@ Foundation, Inc., 51 Franklin Street, Fi
       <column type="gchararray"/>
     </columns>
   </object>
-  <object class="GtkListStore" id="vv.video.output.store">
+  <object class="GtkListStore" id="video.output.store">
     <columns>
       <!-- column-name text -->
       <column type="gchararray"/>
@@ -51,7 +51,7 @@ Foundation, Inc., 51 Franklin Street, Fi
       <column type="gchararray"/>
     </columns>
   </object>
-  <object class="GtkListStore" id="vv.voice.input.store">
+  <object class="GtkListStore" id="voice.input.store">
     <columns>
       <!-- column-name text -->
       <column type="gchararray"/>
@@ -59,7 +59,7 @@ Foundation, Inc., 51 Franklin Street, Fi
       <column type="gchararray"/>
     </columns>
   </object>
-  <object class="GtkListStore" id="vv.voice.output.store">
+  <object class="GtkListStore" id="voice.output.store">
     <columns>
       <!-- column-name text -->
       <column type="gchararray"/>
@@ -67,95 +67,173 @@ Foundation, Inc., 51 Franklin Street, Fi
       <column type="gchararray"/>
     </columns>
   </object>
-  <object class="GtkBox" id="vv.page">
+  <template class="PidginVVPrefs" parent="HdyPreferencesPage">
     <property name="visible">True</property>
     <property name="can-focus">False</property>
-    <property name="border-width">12</property>
-    <property name="orientation">vertical</property>
-    <property name="spacing">18</property>
     <child>
-      <object class="GtkBox">
+      <object class="HdyPreferencesGroup">
         <property name="visible">True</property>
         <property name="can-focus">False</property>
-        <property name="spacing">6</property>
+        <property name="title" translatable="yes">Audio</property>
         <child>
-          <object class="GtkFrame">
+          <object class="GtkAlignment">
             <property name="visible">True</property>
             <property name="can-focus">False</property>
-            <property name="label-xalign">0</property>
-            <property name="shadow-type">none</property>
+            <property name="left-padding">12</property>
             <child>
-              <object class="GtkAlignment">
+              <object class="GtkBox">
                 <property name="visible">True</property>
                 <property name="can-focus">False</property>
-                <property name="left-padding">12</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkFrame">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="label-xalign">0</property>
+                    <property name="shadow-type">none</property>
+                    <child>
+                      <object class="GtkAlignment">
+                        <property name="visible">True</property>
+                        <property name="can-focus">False</property>
+                        <property name="left-padding">12</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="can-focus">False</property>
+                            <property name="spacing">6</property>
+                            <child>
+                              <object class="GtkLabel" id="label1">
+                                <property name="visible">True</property>
+                                <property name="can-focus">False</property>
+                                <property name="label" translatable="yes" context="Device for Audio Input">Device</property>
+                                <property name="xalign">0</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkComboBox" id="voice.input.combo">
+                                <property name="visible">True</property>
+                                <property name="can-focus">False</property>
+                                <property name="id-column">1</property>
+                                <property name="model">voice.input.store</property>
+                                <child>
+                                  <object class="GtkCellRendererText"/>
+                                  <attributes>
+                                    <attribute name="text">0</attribute>
+                                  </attributes>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child type="label">
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can-focus">False</property>
+                        <property name="label" translatable="yes" context="Input for Audio">Input</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkFrame">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="label-xalign">0</property>
+                    <property name="shadow-type">none</property>
+                    <child>
+                      <object class="GtkAlignment">
+                        <property name="visible">True</property>
+                        <property name="can-focus">False</property>
+                        <property name="left-padding">12</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="can-focus">False</property>
+                            <property name="spacing">6</property>
+                            <child>
+                              <object class="GtkLabel" id="label2">
+                                <property name="visible">True</property>
+                                <property name="can-focus">False</property>
+                                <property name="label" translatable="yes" context="Device for Audio Output">Device</property>
+                                <property name="xalign">0</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkComboBox" id="voice.output.combo">
+                                <property name="visible">True</property>
+                                <property name="can-focus">False</property>
+                                <property name="id-column">1</property>
+                                <property name="model">voice.output.store</property>
+                                <child>
+                                  <object class="GtkCellRendererText"/>
+                                  <attributes>
+                                    <attribute name="text">0</attribute>
+                                  </attributes>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child type="label">
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can-focus">False</property>
+                        <property name="label" translatable="yes" context="Output for Audio">Output</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
                 <child>
                   <object class="GtkBox">
                     <property name="visible">True</property>
                     <property name="can-focus">False</property>
-                    <property name="orientation">vertical</property>
                     <property name="spacing">6</property>
                     <child>
-                      <object class="GtkFrame">
+                      <object class="GtkLabel">
                         <property name="visible">True</property>
                         <property name="can-focus">False</property>
-                        <property name="label-xalign">0</property>
-                        <property name="shadow-type">none</property>
-                        <child>
-                          <object class="GtkAlignment">
-                            <property name="visible">True</property>
-                            <property name="can-focus">False</property>
-                            <property name="left-padding">12</property>
-                            <child>
-                              <object class="GtkBox">
-                                <property name="visible">True</property>
-                                <property name="can-focus">False</property>
-                                <property name="spacing">6</property>
-                                <child>
-                                  <object class="GtkLabel" id="label1">
-                                    <property name="visible">True</property>
-                                    <property name="can-focus">False</property>
-                                    <property name="label" translatable="yes" context="Device for Audio Input">Device</property>
-                                    <property name="xalign">0</property>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">0</property>
-                                  </packing>
-                                </child>
-                                <child>
-                                  <object class="GtkComboBox" id="vv.voice.input.combo">
-                                    <property name="visible">True</property>
-                                    <property name="can-focus">False</property>
-                                    <property name="model">vv.voice.input.store</property>
-                                    <child>
-                                      <object class="GtkCellRendererText"/>
-                                      <attributes>
-                                        <attribute name="text">0</attribute>
-                                      </attributes>
-                                    </child>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">1</property>
-                                  </packing>
-                                </child>
-                              </object>
-                            </child>
-                          </object>
-                        </child>
-                        <child type="label">
-                          <object class="GtkLabel">
-                            <property name="visible">True</property>
-                            <property name="can-focus">False</property>
-                            <property name="label" translatable="yes" context="Input for Audio">Input</property>
-                            <attributes>
-                              <attribute name="weight" value="bold"/>
-                            </attributes>
-                          </object>
-                        </child>
+                        <property name="label" translatable="yes">Volume:</property>
                       </object>
                       <packing>
                         <property name="expand">False</property>
@@ -164,64 +242,31 @@ Foundation, Inc., 51 Franklin Street, Fi
                       </packing>
                     </child>
                     <child>
-                      <object class="GtkFrame">
+                      <object class="GtkVolumeButton" id="voice.volume">
                         <property name="visible">True</property>
-                        <property name="can-focus">False</property>
-                        <property name="label-xalign">0</property>
-                        <property name="shadow-type">none</property>
-                        <child>
-                          <object class="GtkAlignment">
-                            <property name="visible">True</property>
-                            <property name="can-focus">False</property>
-                            <property name="left-padding">12</property>
-                            <child>
-                              <object class="GtkBox">
-                                <property name="visible">True</property>
-                                <property name="can-focus">False</property>
-                                <property name="spacing">6</property>
-                                <child>
-                                  <object class="GtkLabel" id="label2">
-                                    <property name="visible">True</property>
-                                    <property name="can-focus">False</property>
-                                    <property name="label" translatable="yes" context="Device for Audio Output">Device</property>
-                                    <property name="xalign">0</property>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">0</property>
-                                  </packing>
-                                </child>
-                                <child>
-                                  <object class="GtkComboBox" id="vv.voice.output.combo">
-                                    <property name="visible">True</property>
-                                    <property name="can-focus">False</property>
-                                    <property name="model">vv.voice.output.store</property>
-                                    <child>
-                                      <object class="GtkCellRendererText"/>
-                                      <attributes>
-                                        <attribute name="text">0</attribute>
-                                      </attributes>
-                                    </child>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">1</property>
-                                  </packing>
-                                </child>
-                              </object>
-                            </child>
+                        <property name="can-focus">True</property>
+                        <property name="focus-on-click">False</property>
+                        <property name="receives-default">True</property>
+                        <property name="relief">none</property>
+                        <property name="orientation">vertical</property>
+                        <property name="adjustment">adjustment2</property>
+                        <signal name="value-changed" handler="volume_changed_cb" swapped="no"/>
+                        <child internal-child="plus_button">
+                          <object class="GtkButton">
+                            <property name="can-focus">True</property>
+                            <property name="receives-default">True</property>
+                            <property name="halign">center</property>
+                            <property name="valign">center</property>
+                            <property name="relief">none</property>
                           </object>
                         </child>
-                        <child type="label">
-                          <object class="GtkLabel">
-                            <property name="visible">True</property>
-                            <property name="can-focus">False</property>
-                            <property name="label" translatable="yes" context="Output for Audio">Output</property>
-                            <attributes>
-                              <attribute name="weight" value="bold"/>
-                            </attributes>
+                        <child internal-child="minus_button">
+                          <object class="GtkButton">
+                            <property name="can-focus">True</property>
+                            <property name="receives-default">True</property>
+                            <property name="halign">center</property>
+                            <property name="valign">center</property>
+                            <property name="relief">none</property>
                           </object>
                         </child>
                       </object>
@@ -231,355 +276,277 @@ Foundation, Inc., 51 Franklin Street, Fi
                         <property name="position">1</property>
                       </packing>
                     </child>
-                    <child>
-                      <object class="GtkBox">
-                        <property name="visible">True</property>
-                        <property name="can-focus">False</property>
-                        <property name="spacing">6</property>
-                        <child>
-                          <object class="GtkLabel">
-                            <property name="visible">True</property>
-                            <property name="can-focus">False</property>
-                            <property name="label" translatable="yes">Volume:</property>
-                          </object>
-                          <packing>
-                            <property name="expand">False</property>
-                            <property name="fill">True</property>
-                            <property name="position">0</property>
-                          </packing>
-                        </child>
-                        <child>
-                          <object class="GtkVolumeButton" id="vv.voice.volume">
-                            <property name="visible">True</property>
-                            <property name="can-focus">True</property>
-                            <property name="focus-on-click">False</property>
-                            <property name="receives-default">True</property>
-                            <property name="relief">none</property>
-                            <property name="orientation">vertical</property>
-                            <property name="adjustment">adjustment2</property>
-                            <property name="icons">audio-volume-muted-symbolic
-audio-volume-high-symbolic
-audio-volume-low-symbolic
-audio-volume-medium-symbolic</property>
-                            <child internal-child="plus_button">
-                              <object class="GtkButton">
-                                <property name="can-focus">True</property>
-                                <property name="receives-default">True</property>
-                                <property name="halign">center</property>
-                                <property name="valign">center</property>
-                                <property name="relief">none</property>
-                              </object>
-                            </child>
-                            <child internal-child="minus_button">
-                              <object class="GtkButton">
-                                <property name="can-focus">True</property>
-                                <property name="receives-default">True</property>
-                                <property name="halign">center</property>
-                                <property name="valign">center</property>
-                                <property name="relief">none</property>
-                              </object>
-                            </child>
-                          </object>
-                          <packing>
-                            <property name="expand">False</property>
-                            <property name="fill">True</property>
-                            <property name="position">1</property>
-                          </packing>
-                        </child>
-                      </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                        <property name="position">2</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkLabel" id="vv.voice.threshold_label">
-                        <property name="visible">True</property>
-                        <property name="can-focus">False</property>
-                        <property name="label" translatable="yes">Silence threshold:</property>
-                        <property name="xalign">0</property>
-                      </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                        <property name="position">3</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkScale" id="vv.voice.threshold">
-                        <property name="visible">True</property>
-                        <property name="can-focus">True</property>
-                        <property name="adjustment">adjustment1</property>
-                        <property name="round-digits">0</property>
-                        <property name="digits">0</property>
-                        <property name="draw-value">False</property>
-                      </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                        <property name="position">4</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkToggleButton" id="vv.voice.test">
-                        <property name="label" translatable="yes">Test Audio</property>
-                        <property name="visible">True</property>
-                        <property name="can-focus">True</property>
-                        <property name="receives-default">True</property>
-                      </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                        <property name="position">5</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkProgressBar" id="vv.voice.level">
-                        <property name="visible">True</property>
-                        <property name="sensitive">False</property>
-                        <property name="can-focus">False</property>
-                      </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                        <property name="position">6</property>
-                      </packing>
-                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="voice.threshold_label">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="label" translatable="yes">Silence threshold:</property>
+                    <property name="xalign">0</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkScale" id="voice.threshold">
+                    <property name="visible">True</property>
+                    <property name="can-focus">True</property>
+                    <property name="adjustment">adjustment1</property>
+                    <property name="round-digits">0</property>
+                    <property name="digits">0</property>
+                    <property name="draw-value">False</property>
+                    <signal name="value-changed" handler="threshold_value_changed_cb" object="PidginVVPrefs" swapped="no"/>
                   </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">4</property>
+                  </packing>
                 </child>
-              </object>
-            </child>
-            <child type="label">
-              <object class="GtkLabel">
-                <property name="visible">True</property>
-                <property name="can-focus">False</property>
-                <property name="label" translatable="yes">Audio</property>
-                <attributes>
-                  <attribute name="weight" value="bold"/>
-                </attributes>
+                <child>
+                  <object class="GtkToggleButton" id="voice.test">
+                    <property name="label" translatable="yes">Test Audio</property>
+                    <property name="visible">True</property>
+                    <property name="can-focus">True</property>
+                    <property name="receives-default">True</property>
+                    <signal name="toggled" handler="toggle_voice_test_cb" object="PidginVVPrefs" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">5</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkProgressBar" id="voice.level">
+                    <property name="visible">True</property>
+                    <property name="sensitive">False</property>
+                    <property name="can-focus">False</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">6</property>
+                  </packing>
+                </child>
               </object>
             </child>
           </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-            <property name="position">0</property>
-          </packing>
         </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="HdyPreferencesGroup">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="title" translatable="yes">Video</property>
         <child>
-          <object class="GtkFrame">
+          <object class="GtkAlignment">
             <property name="visible">True</property>
             <property name="can-focus">False</property>
-            <property name="label-xalign">0</property>
-            <property name="shadow-type">none</property>
+            <property name="left-padding">12</property>
             <child>
-              <object class="GtkAlignment">
+              <object class="GtkBox">
                 <property name="visible">True</property>
                 <property name="can-focus">False</property>
-                <property name="left-padding">12</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">6</property>
                 <child>
-                  <object class="GtkBox">
+                  <object class="GtkFrame">
                     <property name="visible">True</property>
                     <property name="can-focus">False</property>
-                    <property name="orientation">vertical</property>
-                    <property name="spacing">6</property>
+                    <property name="label-xalign">0</property>
+                    <property name="shadow-type">none</property>
                     <child>
-                      <object class="GtkFrame">
+                      <object class="GtkAlignment">
                         <property name="visible">True</property>
                         <property name="can-focus">False</property>
-                        <property name="label-xalign">0</property>
-                        <property name="shadow-type">none</property>
+                        <property name="left-padding">12</property>
                         <child>
-                          <object class="GtkAlignment">
+                          <object class="GtkBox">
                             <property name="visible">True</property>
                             <property name="can-focus">False</property>
-                            <property name="left-padding">12</property>
+                            <property name="spacing">6</property>
                             <child>
-                              <object class="GtkBox">
+                              <object class="GtkLabel" id="label3">
                                 <property name="visible">True</property>
                                 <property name="can-focus">False</property>
-                                <property name="spacing">6</property>
-                                <child>
-                                  <object class="GtkLabel" id="label3">
-                                    <property name="visible">True</property>
-                                    <property name="can-focus">False</property>
-                                    <property name="label" translatable="yes" context="Device for Video Input">Device</property>
-                                    <property name="xalign">0</property>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">0</property>
-                                  </packing>
-                                </child>
+                                <property name="label" translatable="yes" context="Device for Video Input">Device</property>
+                                <property name="xalign">0</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkComboBox" id="video.input.combo">
+                                <property name="visible">True</property>
+                                <property name="can-focus">False</property>
+                                <property name="id-column">1</property>
+                                <property name="model">video.input.store</property>
                                 <child>
-                                  <object class="GtkComboBox" id="vv.video.input.combo">
-                                    <property name="visible">True</property>
-                                    <property name="can-focus">False</property>
-                                    <property name="model">vv.video.input.store</property>
-                                    <child>
-                                      <object class="GtkCellRendererText"/>
-                                      <attributes>
-                                        <attribute name="text">0</attribute>
-                                      </attributes>
-                                    </child>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">1</property>
-                                  </packing>
+                                  <object class="GtkCellRendererText"/>
+                                  <attributes>
+                                    <attribute name="text">0</attribute>
+                                  </attributes>
                                 </child>
                               </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
                             </child>
                           </object>
                         </child>
-                        <child type="label">
-                          <object class="GtkLabel">
-                            <property name="visible">True</property>
-                            <property name="can-focus">False</property>
-                            <property name="label" translatable="yes" context="Input for Video">Input</property>
-                            <attributes>
-                              <attribute name="weight" value="bold"/>
-                            </attributes>
-                          </object>
-                        </child>
                       </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                        <property name="position">0</property>
-                      </packing>
                     </child>
-                    <child>
-                      <object class="GtkFrame">
+                    <child type="label">
+                      <object class="GtkLabel">
                         <property name="visible">True</property>
                         <property name="can-focus">False</property>
-                        <property name="label-xalign">0</property>
-                        <property name="shadow-type">none</property>
+                        <property name="label" translatable="yes" context="Input for Video">Input</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkFrame">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="label-xalign">0</property>
+                    <property name="shadow-type">none</property>
+                    <child>
+                      <object class="GtkAlignment">
+                        <property name="visible">True</property>
+                        <property name="can-focus">False</property>
+                        <property name="left-padding">12</property>
                         <child>
-                          <object class="GtkAlignment">
+                          <object class="GtkBox">
                             <property name="visible">True</property>
                             <property name="can-focus">False</property>
-                            <property name="left-padding">12</property>
+                            <property name="spacing">6</property>
                             <child>
-                              <object class="GtkBox">
+                              <object class="GtkLabel" id="label4">
                                 <property name="visible">True</property>
                                 <property name="can-focus">False</property>
-                                <property name="spacing">6</property>
-                                <child>
-                                  <object class="GtkLabel" id="label4">
-                                    <property name="visible">True</property>
-                                    <property name="can-focus">False</property>
-                                    <property name="label" translatable="yes" context="Device for Video Output">Device</property>
-                                    <property name="xalign">0</property>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">0</property>
-                                  </packing>
-                                </child>
+                                <property name="label" translatable="yes" context="Device for Video Output">Device</property>
+                                <property name="xalign">0</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkComboBox" id="video.output.combo">
+                                <property name="visible">True</property>
+                                <property name="can-focus">False</property>
+                                <property name="id-column">1</property>
+                                <property name="model">video.output.store</property>
                                 <child>
-                                  <object class="GtkComboBox" id="vv.video.output.combo">
-                                    <property name="visible">True</property>
-                                    <property name="can-focus">False</property>
-                                    <property name="model">vv.video.output.store</property>
-                                    <child>
-                                      <object class="GtkCellRendererText"/>
-                                      <attributes>
-                                        <attribute name="text">0</attribute>
-                                      </attributes>
-                                    </child>
-                                  </object>
-                                  <packing>
-                                    <property name="expand">False</property>
-                                    <property name="fill">True</property>
-                                    <property name="position">1</property>
-                                  </packing>
+                                  <object class="GtkCellRendererText"/>
+                                  <attributes>
+                                    <attribute name="text">0</attribute>
+                                  </attributes>
                                 </child>
                               </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
                             </child>
                           </object>
                         </child>
-                        <child type="label">
-                          <object class="GtkLabel">
-                            <property name="visible">True</property>
-                            <property name="can-focus">False</property>
-                            <property name="label" translatable="yes" context="Output for Video">Output</property>
-                            <attributes>
-                              <attribute name="weight" value="bold"/>
-                            </attributes>
-                          </object>
-                        </child>
                       </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                        <property name="position">1</property>
-                      </packing>
                     </child>
-                    <child>
-                      <object class="GtkAspectFrame" id="vv.video.frame">
+                    <child type="label">
+                      <object class="GtkLabel">
                         <property name="visible">True</property>
                         <property name="can-focus">False</property>
-                        <property name="label-xalign">0</property>
-                        <property name="shadow-type">none</property>
-                        <property name="ratio">1.3300000429153442</property>
-                        <child>
-                          <placeholder/>
-                        </child>
+                        <property name="label" translatable="yes" context="Output for Video">Output</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
                       </object>
-                      <packing>
-                        <property name="expand">True</property>
-                        <property name="fill">True</property>
-                        <property name="position">2</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkToggleButton" id="vv.video.test">
-                        <property name="label" translatable="yes">Test Video</property>
-                        <property name="visible">True</property>
-                        <property name="can-focus">True</property>
-                        <property name="receives-default">True</property>
-                      </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                        <property name="position">3</property>
-                      </packing>
                     </child>
                   </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkAspectFrame" id="video.frame">
+                    <property name="visible">True</property>
+                    <property name="can-focus">False</property>
+                    <property name="label-xalign">0</property>
+                    <property name="shadow-type">none</property>
+                    <property name="ratio">1.33</property>
+                    <child>
+                      <placeholder/>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkToggleButton" id="video.test">
+                    <property name="label" translatable="yes">Test Video</property>
+                    <property name="visible">True</property>
+                    <property name="can-focus">True</property>
+                    <property name="receives-default">True</property>
+                    <signal name="toggled" handler="toggle_video_test_cb" object="PidginVVPrefs" swapped="no"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">3</property>
+                  </packing>
                 </child>
               </object>
             </child>
-            <child type="label">
-              <object class="GtkLabel">
-                <property name="visible">True</property>
-                <property name="can-focus">False</property>
-                <property name="label" translatable="yes">Video</property>
-                <attributes>
-                  <attribute name="weight" value="bold"/>
-                </attributes>
-              </object>
-            </child>
           </object>
-          <packing>
-            <property name="expand">True</property>
-            <property name="fill">True</property>
-            <property name="position">1</property>
-          </packing>
         </child>
       </object>
       <packing>
         <property name="expand">True</property>
         <property name="fill">True</property>
-        <property name="position">0</property>
+        <property name="position">1</property>
       </packing>
     </child>
-  </object>
-  <object class="GtkSizeGroup" id="vv.sg">
+  </template>
+  <object class="GtkSizeGroup" id="sg">
     <widgets>
       <widget name="label1"/>
       <widget name="label2"/>
diff --git a/po/POTFILES.in b/po/POTFILES.in
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -394,6 +394,7 @@ pidgin/prefs/pidgincredentialspage.c
 pidgin/prefs/pidginnetworkpage.c
 pidgin/prefs/pidginprefs.c
 pidgin/prefs/pidginproxyprefs.c
+pidgin/prefs/pidginvvprefs.c
 pidgin/resources/About/about.ui
 pidgin/resources/Accounts/actionsmenu.ui
 pidgin/resources/Accounts/chooser.ui
