Re: [RFC] Screen share window/monitor selection

David Woodhouse <[email protected]> Thu, 26 Apr 2018 21:32:30 +0100
Newsgroups gmane.comp.gnome.gaim.devel
Message-ID <[email protected]>
On Mon, 2018-04-23 at 16:20 +0100, David Woodhouse wrote:
> 
> Now it actually works on the UI side too. I can select a monitor or a
> specific window to be shared. There is some X-specific magic here to
> identify a window by its xid; Windows support is blocked on the fact
> that there isn't a GStreamer source element equivalent to ximagesrc.
> You *can* share a videotestsrc from Windows though ;)

Someone pointed me at gdiscreencapsrc. Now it should do Windows too.
Although I have so far failed to get the Windows build going with
mingw32-configure. I'll keep working on that.

There's an avfvideosrc which Adium can use too, but does Adium have V/V
support at all anyway?

_______________________________________________
Devel mailing list
[email protected]
https://pidgin.im/cgi-bin/mailman/listinfo/devel
0001-Outbound-screenshare-window-monitor-selection.patch (text/x-patch, 17 KB)
From 52df3e6581cbbabc912a706ec267398c3c5ee2ac Mon Sep 17 00:00:00 2001
From: David Woodhouse <[email protected]>
Date: Mon, 23 Apr 2018 16:37:00 +0100
Subject: [PATCH] Outbound screenshare window/monitor selection
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add a purple_request_screenshare_media() method. The UI will interact
with the user to select a monitor / window / region to share, depending
on the capabilities of the platform, and will invoke the callback with
a PurpleMediaElementInfo which represents the desired GStreamer src
element. If the UI request is cancelled, the callback is invoked with
NULL.

The PRPL can then use g_object_set_data(media, "src-element", info)
before calling purple_media_add_stream(media, …PURPLE_MEDIA_SEND_VIDEO…),
and the selected source element will be used for that session.
---
 libpurple/request.c |  25 ++++
 libpurple/request.h |  33 ++++-
 pidgin/gtkmain.c    |  10 +-
 pidgin/gtkrequest.c | 360 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 425 insertions(+), 3 deletions(-)

diff --git a/libpurple/request.c b/libpurple/request.c
index d61d241..da2b5ca 100644
--- a/libpurple/request.c
+++ b/libpurple/request.c
@@ -1501,6 +1501,31 @@ purple_request_folder(void *handle, const char *title, const char *dirname,
 	return NULL;
 }
 
+void *
+purple_request_screenshare_media(void *handle, const char *title,
+				 const char *primary, const char *secondary,
+				 PurpleAccount *account, GCallback cb,
+				 void *user_data)
+{
+	PurpleRequestUiOps *ops;
+
+	ops = purple_request_get_ui_ops();
+
+	if (ops != NULL && ops->request_screenshare_media != NULL) {
+		PurpleRequestInfo *info;
+
+		info            = g_new0(PurpleRequestInfo, 1);
+		info->type      = PURPLE_REQUEST_SCREENSHARE;
+		info->handle    = handle;
+		info->ui_handle = ops->request_screenshare_media(title, primary, secondary,
+								 account, cb, user_data);
+		handles = g_list_append(handles, info);
+		return info->ui_handle;
+	}
+
+	return NULL;
+}
+
 static void
 purple_request_close_info(PurpleRequestInfo *info)
 {
diff --git a/libpurple/request.h b/libpurple/request.h
index 8035ef8..282c909 100644
--- a/libpurple/request.h
+++ b/libpurple/request.h
@@ -47,7 +47,8 @@ typedef enum
 	PURPLE_REQUEST_ACTION,     /**< Action request.            */
 	PURPLE_REQUEST_FIELDS,     /**< Multiple fields request.   */
 	PURPLE_REQUEST_FILE,       /**< File open or save request. */
-	PURPLE_REQUEST_FOLDER      /**< Folder selection request.  */
+	PURPLE_REQUEST_FOLDER,     /**< Folder selection request.  */
+	PURPLE_REQUEST_SCREENSHARE /**< Screenshare media request. */
 
 } PurpleRequestType;
 
@@ -246,9 +247,12 @@ typedef struct
 	                        void *user_data,
 	                        size_t action_count, va_list actions);
 
+	void *(*request_screenshare_media)(const char *title, const char *primary,
+				const char *secondary, PurpleAccount *account,
+				GCallback cb, void *user_data);
+
 	void (*_purple_reserved1)(void);
 	void (*_purple_reserved2)(void);
-	void (*_purple_reserved3)(void);
 } PurpleRequestUiOps;
 
 typedef void (*PurpleRequestInputCb)(void *, const char *);
@@ -261,6 +265,7 @@ typedef void (*PurpleRequestActionCb)(void *, int);
 typedef void (*PurpleRequestChoiceCb)(void *, int);
 typedef void (*PurpleRequestFieldsCb)(void *, PurpleRequestFields *fields);
 typedef void (*PurpleRequestFileCb)(void *, const char *filename);
+typedef void (*PurpleRequestScreenshareCb)(void *, GObject *info);
 
 #ifdef __cplusplus
 extern "C" {
@@ -1576,6 +1581,30 @@ void *purple_request_folder(void *handle, const char *title, const char *dirname
 	PurpleAccount *account, const char *who, PurpleConversation *conv,
 	void *user_data);
 
+
+/**
+ * Displays a dialog allowing the user to select a window/monitor etc. for
+ * screen sharing. Returns a #PurpleMediaElementInfo to the callback or @c
+ * NULL if the request is cancelled.
+ *
+ * @param handle      The plugin or connection handle.  For some things this
+ *                    is <em>extremely</em> important.  See the comments on
+ *                    purple_request_input().
+ * @param title       The title of the message, or @c NULL if it should have
+ *                    no title.
+ * @param primary     The main point of the message, or @c NULL if you're
+ *                    feeling enigmatic.
+ * @param secondary   Secondary information, or @c NULL if there is none.
+ * @param cb          The callback for the @c OK button.
+ * @param user_data   The data to pass to the callback.
+ *
+ * @return A UI-specific handle.
+ */
+void *purple_request_screenshare_media(void *handle, const char *title,
+				       const char *primary, const char *secondary,
+				       PurpleAccount *account, GCallback cb,
+				       void *user_data);
+
 /*@}*/
 
 /**************************************************************************/
diff --git a/pidgin/gtkmain.c b/pidgin/gtkmain.c
index 13aa1de..9418271 100644
--- a/pidgin/gtkmain.c
+++ b/pidgin/gtkmain.c
@@ -70,13 +70,16 @@
 #include "pidginstock.h"
 #include "gtkwhiteboard.h"
 
+#ifdef HAVE_X11
+#include <X11/Xlib.h>
+#endif
+
 #ifdef HAVE_SIGNAL_H
 # include <signal.h>
 #endif
 
 #include <getopt.h>
 
-
 #ifdef HAVE_SIGNAL_H
 
 /*
@@ -749,6 +752,11 @@ int main(int argc, char *argv[])
 	gtk_rc_add_default_file(search_path);
 	g_free(search_path);
 
+#if defined(HAVE_X11) && defined(USE_VV)
+	/* GStreamer elements such as ximagesrc may require this */
+	XInitThreads();
+#endif
+
 	gui_check = gtk_init_check(&argc, &argv);
 	if (!gui_check) {
 		char *display = gdk_get_display();
diff --git a/pidgin/gtkrequest.c b/pidgin/gtkrequest.c
index 397e09a..c51eb22 100644
--- a/pidgin/gtkrequest.c
+++ b/pidgin/gtkrequest.c
@@ -36,6 +36,9 @@
 #include "gtkutils.h"
 #include "pidginstock.h"
 #include "gtkblist.h"
+#ifdef USE_VV
+#include "media-gst.h"
+#endif
 
 #include <gdk/gdkkeysyms.h>
 
@@ -1703,6 +1706,359 @@ pidgin_request_folder(const char *title, const char *dirname,
 	return (void *)data;
 }
 
+#ifdef USE_VV
+static GstElement *create_screensrc_cb(PurpleMedia *media, const gchar *session_id,
+				       const gchar *participant);
+
+#ifdef HAVE_X11
+static gboolean
+grab_event (GtkWidget *child, GdkEvent *event, PidginRequestData *data)
+{
+	GdkScreen *screen = gdk_screen_get_default();
+	GObject *info;
+	GdkWindow *gdkroot = gdk_get_default_root_window();
+	Window xroot = GDK_WINDOW_XID(gdkroot), xwindow, parent, *children;
+	unsigned int nchildren, xmask;
+	Display *xdisplay = GDK_SCREEN_XDISPLAY(screen);
+	int rootx, rooty, winx, winy;
+
+	if (event->type != GDK_BUTTON_PRESS)
+		return FALSE;
+
+	XQueryPointer(xdisplay, xroot, &xroot, &xwindow, &rootx, &rooty, &winx, &winy, &xmask);
+
+	gdk_pointer_ungrab(GDK_CURRENT_TIME);
+
+	/* Find WM window (direct child of root) */
+	while (1) {
+		if (!XQueryTree(xdisplay, xwindow, &xroot, &parent, &children, &nchildren))
+			break;
+
+		if (nchildren)
+			XFree(children);
+
+		if (xroot == parent)
+			break;
+
+		xwindow = parent;
+	}
+
+	generic_response_start(data);
+
+	if (data->cbs[0] != NULL) {
+		info = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO,
+				    "id", "screenshare-window",
+				    "name", "Screen share single window",
+				    "type", PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC |
+				    PURPLE_MEDIA_ELEMENT_ONE_SRC,
+				    "create-cb", create_screensrc_cb, NULL);
+		g_object_set_data(info, "window-id", GUINT_TO_POINTER(xwindow));
+		((PurpleRequestScreenshareCb)data->cbs[0])(data->user_data, info);
+	}
+
+	purple_request_close(PURPLE_REQUEST_SCREENSHARE, data);
+
+	return FALSE;
+}
+
+static void
+screenshare_window_cb(GtkWidget *button, PidginRequestData *data)
+{
+	GdkCursor *cursor;
+	GdkWindow *gdkwin = gtk_widget_get_window(GTK_WIDGET(data->dialog));
+
+	if (!GTK_WIDGET_HAS_FOCUS(button))
+		gtk_widget_grab_focus(button);
+
+	gtk_widget_add_events(GTK_WIDGET(data->dialog),
+			      GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
+	g_signal_connect(data->dialog, "event", G_CALLBACK(grab_event), data);
+
+	cursor = gdk_cursor_new(GDK_CROSSHAIR);
+	gdk_pointer_grab(gdkwin, FALSE,
+			 GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK,
+			 NULL, cursor, GDK_CURRENT_TIME);
+}
+
+static GstElement *create_screensrc_cb(PurpleMedia *media, const gchar *session_id,
+				       const gchar *participant)
+{
+	GObject *info;
+	GstElement *ret;
+
+	ret = gst_element_factory_make("ximagesrc", NULL);
+	g_object_set(ret, "use-damage", 0, NULL);
+
+	info = g_object_get_data(G_OBJECT(media), "src-element");
+	if (info) {
+		Window xid = GPOINTER_TO_UINT(g_object_get_data(info, "window-id"));
+		int monitor_no = GPOINTER_TO_INT(g_object_get_data(info, "monitor-no"));
+		if (xid) {
+			g_object_set(ret, "xid", xid, NULL);
+		} else if (monitor_no >= 0) {
+			GdkScreen *screen = gdk_screen_get_default();
+			GdkRectangle geom;
+
+			gdk_screen_get_monitor_geometry(screen, monitor_no, &geom);
+			g_object_set(ret, "startx", geom.x, "starty", geom.y,
+				     "endx", geom.x + geom.width - 1,
+				     "endy", geom.y + geom.height - 1, NULL);
+		}
+	}
+
+	return ret;
+}
+#elif defined (_WIN32)
+static GstElement *create_screensrc_cb(PPurpleMedia *media, const gchar *session_id,
+				       const gchar *participant)
+{
+	GObject *info;
+	GstElement *ret;
+
+	ret = gst_element_factory_make("gdiscreencapsrc", NULL);
+	g_object_set(ret, "cursor", TRUE);
+
+	info = g_object_get_data(G_OBJECT(media), "src-element");
+	if (info) {
+		int monitor_no = GPOINTER_TO_INT(g_object_get_data(info, "monitor-no"));
+		if (monitor_no >= 0)
+			g_object_set(ret, "monitor", monitor_no);
+	}
+
+	return ret;
+}
+#else
+/* We don't actually need to break the build just because we can't do
+ * screencap, but gtkmedia.c is going to break the USE_VV build if it
+ * isn't WIN32 or X11 anyway, so we might as well. */
+#error "Unsupported windowing system"
+#endif
+
+static void
+screenshare_monitor_cb(GtkWidget *button, PidginRequestData *data)
+{
+	GtkWidget *radio;
+	GObject *info;
+	int monitor_no = -1;
+
+	generic_response_start(data);
+
+	if (!GTK_WIDGET_HAS_FOCUS(button))
+		gtk_widget_grab_focus(button);
+
+	radio = g_object_get_data(G_OBJECT(data->dialog), "radio");
+	if (radio) {
+		GSList *group = gtk_radio_button_get_group(GTK_RADIO_BUTTON(radio));
+
+		while (group) {
+			if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(group->data))) {
+				monitor_no = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(group->data),
+									       "monitor-no"));
+				break;
+			}
+			group = group->next;
+		}
+	}
+	if (data->cbs[0] != NULL) {
+		info = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO,
+				    "id", "screenshare-monitor",
+				    "name", "Screen share monitor",
+				    "type", PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC |
+				    PURPLE_MEDIA_ELEMENT_ONE_SRC,
+				    "create-cb", create_screensrc_cb, NULL);
+		g_object_set_data(info, "monitor-no", GINT_TO_POINTER(monitor_no));
+		((PurpleRequestScreenshareCb)data->cbs[0])(data->user_data, info);
+	}
+
+	purple_request_close(PURPLE_REQUEST_SCREENSHARE, data);
+}
+
+static GstElement *create_videotest_cb(PurpleMedia *media, const gchar *session_id,
+				       const gchar *participant)
+{
+	return gst_element_factory_make("videotestsrc", NULL);
+}
+
+static void
+screenshare_videotest_cb(GtkWidget *button, PidginRequestData *data)
+{
+	GObject *info;
+
+	generic_response_start(data);
+
+	if (!GTK_WIDGET_HAS_FOCUS(button))
+		gtk_widget_grab_focus(button);
+
+	if (data->cbs[0] != NULL) {
+		info = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO,
+				    "id", "screenshare-videotestsrc",
+				    "name", "Screen share test source",
+				    "type", PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC |
+				    PURPLE_MEDIA_ELEMENT_ONE_SRC,
+				    "create-cb", create_videotest_cb, NULL);
+		((PurpleRequestScreenshareCb)data->cbs[0])(data->user_data, info);
+	}
+
+	purple_request_close(PURPLE_REQUEST_SCREENSHARE, data);
+}
+
+static void
+screenshare_cancel_cb(GtkWidget *button, PidginRequestData *data)
+{
+	generic_response_start(data);
+
+	if (data->cbs[0] != NULL)
+		((PurpleRequestScreenshareCb)data->cbs[0])(data->user_data, NULL);
+
+	purple_request_close(PURPLE_REQUEST_SCREENSHARE, data);
+}
+
+static gboolean
+destroy_screenshare_cb(GtkWidget *dialog, GdkEvent *event,
+		       PidginRequestData *data)
+{
+	screenshare_cancel_cb(NULL, data);
+	return FALSE;
+}
+
+static void *pidgin_request_screenshare_media(const char *title, const char *primary,
+					      const char *secondary, PurpleAccount *account,
+					      GCallback cb, void *user_data)
+{
+	PidginRequestData *data;
+	GtkWidget *dialog;
+	GtkWidget *vbox;
+	GtkWidget *hbox;
+	GtkWidget *label;
+	GtkWidget *button;
+	GtkWidget *radio = NULL;
+	GdkScreen *screen;
+	char *label_text;
+	char *primary_esc, *secondary_esc;
+
+	data            = g_new0(PidginRequestData, 1);
+	data->type      = PURPLE_REQUEST_SCREENSHARE;
+	data->user_data = user_data;
+
+	data->cb_count = 1;
+	data->cbs = g_new0(GCallback, 1);
+	data->cbs[0] = cb;
+
+	/* Create the dialog. */
+	data->dialog = dialog = gtk_dialog_new();
+
+	if (title != NULL)
+		gtk_window_set_title(GTK_WINDOW(dialog), title);
+#ifdef _WIN32
+	else
+		gtk_window_set_title(GTK_WINDOW(dialog), PIDGIN_ALERT_TITLE);
+#endif
+
+	button = pidgin_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_CANCEL,
+					  G_CALLBACK(screenshare_cancel_cb), data);
+	GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
+
+	if (g_getenv("PIDGIN_SHARE_VIDEOTEST") != NULL) {
+		button = pidgin_dialog_add_button(GTK_DIALOG(dialog), _("Test image"),
+						  G_CALLBACK(screenshare_videotest_cb), data);
+		GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
+		gtk_window_set_default(GTK_WINDOW(dialog), button);
+	}
+
+#ifdef HAVE_X11
+	button = pidgin_dialog_add_button(GTK_DIALOG(dialog), _("Select window"),
+					  G_CALLBACK(screenshare_window_cb), data);
+	GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
+	gtk_window_set_default(GTK_WINDOW(dialog), button);
+#endif
+
+	button = pidgin_dialog_add_button(GTK_DIALOG(dialog), _("Use monitor"),
+					  G_CALLBACK(screenshare_monitor_cb), data);
+	GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
+	gtk_window_set_default(GTK_WINDOW(dialog), button);
+
+	g_signal_connect(G_OBJECT(dialog), "delete_event",
+					 G_CALLBACK(destroy_screenshare_cb), data);
+
+	/* Setup the dialog */
+	gtk_container_set_border_width(GTK_CONTAINER(dialog), PIDGIN_HIG_BORDER/2);
+	gtk_container_set_border_width(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), PIDGIN_HIG_BORDER/2);
+	gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
+	gtk_dialog_set_has_separator(GTK_DIALOG(dialog), FALSE);
+	gtk_box_set_spacing(GTK_BOX(GTK_DIALOG(dialog)->vbox), PIDGIN_HIG_BORDER);
+
+	/* Setup the main horizontal box */
+	hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BORDER);
+	gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), hbox);
+
+
+	/* Vertical box */
+	vbox = gtk_vbox_new(FALSE, PIDGIN_HIG_BORDER);
+	gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 0);
+
+	pidgin_widget_decorate_account(hbox, account);
+
+	/* Descriptive label */
+	primary_esc = (primary != NULL) ? g_markup_escape_text(primary, -1) : NULL;
+	secondary_esc = (secondary != NULL) ? g_markup_escape_text(secondary, -1) : NULL;
+	label_text = g_strdup_printf((primary ? "<span weight=\"bold\" size=\"larger\">"
+								 "%s</span>%s%s" : "%s%s%s"),
+								 (primary ? primary_esc : ""),
+								 ((primary && secondary) ? "\n\n" : ""),
+								 (secondary ? secondary_esc : ""));
+	g_free(primary_esc);
+	g_free(secondary_esc);
+
+	label = gtk_label_new(NULL);
+
+	gtk_label_set_markup(GTK_LABEL(label), label_text);
+	gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
+	gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
+	gtk_label_set_selectable(GTK_LABEL(label), TRUE);
+	gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, TRUE, 0);
+
+	g_free(label_text);
+
+	screen = gdk_screen_get_default();
+	if (screen) {
+		int nr_monitors = gdk_screen_get_n_monitors(screen);
+		int primary = gdk_screen_get_primary_monitor(screen);
+		int i;
+
+		for (i = 0; i < nr_monitors; i++) {
+			GdkRectangle geom;
+			gchar *name;
+			gchar *label;
+
+			name = gdk_screen_get_monitor_plug_name(screen, i);
+			gdk_screen_get_monitor_geometry(screen, i, &geom);
+
+			label = g_strdup_printf(_("%s (%d✕%d @ %d,%d)"),
+						name ? name : _("Unknown output"),
+						geom.width, geom.height,
+						geom.x, geom.y);
+			radio = gtk_radio_button_new_with_label_from_widget((GtkRadioButton *)radio, label);
+			g_object_set_data(G_OBJECT(radio), "monitor-no", GINT_TO_POINTER(i));
+			gtk_box_pack_start(GTK_BOX(vbox), radio, FALSE, FALSE, 0);
+			if (i == primary)
+			       gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
+
+			g_free(label);
+			g_free(name);
+		}
+		g_object_set_data(G_OBJECT(dialog), "radio", radio);
+	}
+
+	/* Show everything. */
+	pidgin_auto_parent_window(dialog);
+
+	gtk_widget_show_all(dialog);
+
+	return data;
+
+}
+#endif /* USE_VV */
+
 static void
 pidgin_close_request(PurpleRequestType type, void *ui_handle)
 {
@@ -1730,7 +2086,11 @@ static PurpleRequestUiOps ops =
 	pidgin_close_request,
 	pidgin_request_folder,
 	pidgin_request_action_with_icon,
+#ifdef USE_VV
+	pidgin_request_screenshare_media,
+#else
 	NULL,
+#endif
 	NULL,
 	NULL
 };
-- 
2.7.4
smime.p7s (application/x-pkcs7-signature, 5.1 KB) - not displayed