Re: [RFC] Screen share window/monitor selection

David Woodhouse <[email protected]> Mon, 23 Apr 2018 16:20:46 +0100
Newsgroups gmane.comp.gnome.gaim.devel
Message-ID <[email protected]>
On Fri, 2018-04-20 at 16:34 +0100, David Woodhouse wrote:
> This adds a method to PurpleRequestUiOps to request a
> PurpleMediaElementInfo as a source for screen sharing.
> 
> The UI interacts with the user to select the window/monitor to be
> shared, and the UI is responsible for providing the appropriate
> GStreamer element.
> 
> The Pidgin side needs finishing — currently I'm using videotestsrc and
> a nasty hack to test with ximagesrc, but actually I think I'm going to
> end up with an appsrc which is fed with frames by
> gdk_pixbuf_get_from_window() or something like that. Done properly,
> that will work on Windows too.
> 
> For now I'm soliciting feedback on the basic approach and the libpurple
> side of things.
> 
> This works on top of my patch in PR#322 which allows the PRPL to
> specify a PurpleMediaElementInfo to use instead of the default for the
> media type, by using g_object_set_data(media, "src-element", info)
> before adding streams.

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 ;)

Once PR#322 is merged, I'll make a separate PR for this as it wants its
own review.

_______________________________________________
Devel mailing list
[email protected]
https://pidgin.im/cgi-bin/mailman/listinfo/devel
pidgin-screenshare.patch (text/x-patch, 14.5 KB)
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/gtkrequest.c b/pidgin/gtkrequest.c
index 397e09a..840a295 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>
 
@@ -77,6 +80,11 @@ typedef struct
 
 		} file;
 
+		struct
+		{
+			gint x, y;
+		} screen;
+
 	} u;
 
 } PidginRequestData;
@@ -1703,6 +1711,332 @@ pidgin_request_folder(const char *title, const char *dirname,
 	return (void *)data;
 }
 
+#ifdef USE_VV
+#ifdef HAVE_X11
+static GstElement *create_ximagesrc_cb(PurpleMedia *media, const gchar *session_id,
+				       const gchar *participant)
+{
+	GObject *info;
+	GstElement *ret;
+
+	XInitThreads(); /* Don't even ask */
+	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;
+}
+
+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_ximagesrc_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 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_ximagesrc_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);
+	gdk_window_get_origin(gdkwin, &data->u.screen.x, &data->u.screen.y);
+}
+#endif /* HAVE_X11 */
+
+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);
+
+	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);
+
+	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);
+#endif
+
+	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 +2064,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
 };
smime.p7s (application/x-pkcs7-signature, 5.1 KB) - not displayed