From af25561e0f4e5aa2c3460ce7448b0c580f28532a Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 28 Sep 2021 21:04:28 -0300 Subject: [PATCH] Implement screencast stream restoration Handle receiving 'restore_data' and 'persist_mode'. For monitors, use the match string introduced by the previous commit to identify individual monitors. For windows, do a strict check on the app id, and a best-match approach to window titles. Virtual monitors don't have any particular data attached to them, so they are merely stored as "virtual" and restored. --- src/screencast.c | 327 +++++++++++++++++++++++++++++++++++++++- src/screencast.h | 7 + src/screencastdialog.c | 14 +- src/screencastdialog.h | 5 +- src/screencastwidget.c | 23 +++ src/screencastwidget.h | 6 + src/screencastwidget.ui | 8 + src/utils.c | 77 ++++++++++ src/utils.h | 3 + 9 files changed, 456 insertions(+), 14 deletions(-) diff --git a/src/screencast.c b/src/screencast.c index 2713d26..55a3e5c 100644 --- a/src/screencast.c +++ b/src/screencast.c @@ -33,8 +33,15 @@ #include "externalwindow.h" #include "request.h" #include "session.h" +#include "shellintrospect.h" #include "utils.h" +#define RESTORE_FORMAT_VERSION 1 +#define RESTORE_VARIANT_TYPE "(xxa(uuv))" +#define MONITOR_TYPE "s" +#define WINDOW_TYPE "(ss)" +#define VIRTUAL_TYPE "b" + typedef struct _ScreenCastDialogHandle ScreenCastDialogHandle; typedef struct _ScreenCastSession @@ -49,6 +56,13 @@ typedef struct _ScreenCastSession ScreenCastSelection select; + ScreenCastPersistMode persist_mode; + GPtrArray *streams_to_restore; + struct { + GVariant *data; + int64_t creation_time; + } restored; + GDBusMethodInvocation *start_invocation; ScreenCastDialogHandle *dialog_handle; } ScreenCastSession; @@ -99,6 +113,68 @@ screen_cast_dialog_handle_close (ScreenCastDialogHandle *dialog_handle) screen_cast_dialog_handle_free (dialog_handle); } +static GVariant * +serialize_streams_as_restore_data (ScreenCastSession *screen_cast_session, + GPtrArray *streams) +{ + GVariantBuilder restore_data_builder; + GVariantBuilder impl_builder; + int64_t creation_time; + int64_t last_used_time; + guint i; + + if (!streams || streams->len == 0) + return NULL; + + last_used_time = g_get_real_time (); + if (screen_cast_session->restored.creation_time != -1) + creation_time = screen_cast_session->restored.creation_time; + else + creation_time = g_get_real_time (); + + g_variant_builder_init (&impl_builder, G_VARIANT_TYPE (RESTORE_VARIANT_TYPE)); + g_variant_builder_add (&impl_builder, "x", creation_time); + g_variant_builder_add (&impl_builder, "x", last_used_time); + + g_variant_builder_open (&impl_builder, G_VARIANT_TYPE ("a(uuv)")); + for (i = 0; i < streams->len; i++) + { + ScreenCastStreamInfo *info = g_ptr_array_index (streams, i); + GVariant *stream_variant; + Monitor *monitor; + Window *window; + + switch (info->type) + { + case SCREEN_CAST_SOURCE_TYPE_MONITOR: + monitor = info->data.monitor; + stream_variant = g_variant_new (MONITOR_TYPE, + monitor_get_match_string (monitor)); + break; + + case SCREEN_CAST_SOURCE_TYPE_WINDOW: + window = info->data.window; + stream_variant = g_variant_new (WINDOW_TYPE, + window_get_app_id (window), + window_get_title (window)); + break; + } + + g_variant_builder_add (&impl_builder, + "(uuv)", + i, + info->type, + stream_variant); + } + g_variant_builder_close (&impl_builder); + + g_variant_builder_init (&restore_data_builder, G_VARIANT_TYPE ("(suv)")); + g_variant_builder_add (&restore_data_builder, "s", "GNOME"); + g_variant_builder_add (&restore_data_builder, "u", RESTORE_FORMAT_VERSION); + g_variant_builder_add (&restore_data_builder, "v", g_variant_builder_end (&impl_builder)); + return g_variant_builder_end (&restore_data_builder); +} + static void cancel_start_session (ScreenCastSession *screen_cast_session, int response) @@ -142,6 +218,22 @@ on_gnome_screen_cast_session_ready (GnomeScreenCastSession *gnome_screen_cast_se "streams", g_variant_builder_end (&streams_builder)); + if (screen_cast_session->persist_mode != SCREEN_CAST_PERSIST_MODE_NONE) + { + g_autoptr(GPtrArray) streams = g_steal_pointer (&screen_cast_session->streams_to_restore); + GVariant *restore_data; + + restore_data = serialize_streams_as_restore_data (screen_cast_session, streams); + + if (restore_data) + { + g_variant_builder_add (&results_builder, "{sv}", "persist_mode", + g_variant_new_uint32 (screen_cast_session->persist_mode)); + g_variant_builder_add (&results_builder, "{sv}", "restore_data", + g_variant_new_variant (restore_data)); + } + } + xdp_impl_screen_cast_complete_start (XDP_IMPL_SCREEN_CAST (impl), screen_cast_session->start_invocation, 0, g_variant_builder_end (&results_builder)); @@ -178,6 +270,9 @@ start_session (ScreenCastSession *screen_cast_session, G_CALLBACK (on_gnome_screen_cast_session_closed), screen_cast_session); + if (screen_cast_session->persist_mode != SCREEN_CAST_PERSIST_MODE_NONE) + screen_cast_session->streams_to_restore = g_ptr_array_ref (streams); + if (!gnome_screen_cast_session_record_selections (gnome_screen_cast_session, streams, &screen_cast_session->select, @@ -193,6 +288,7 @@ start_session (ScreenCastSession *screen_cast_session, static void on_screen_cast_dialog_done_cb (GtkWidget *widget, int dialog_response, + ScreenCastPersistMode persist_mode, GPtrArray *streams, ScreenCastDialogHandle *dialog_handle) { @@ -218,8 +314,12 @@ on_screen_cast_dialog_done_cb (GtkWidget *widget, if (response == 0) { + ScreenCastSession *screen_cast_session = dialog_handle->session; g_autoptr(GError) error = NULL; + screen_cast_session->persist_mode = MIN (screen_cast_session->persist_mode, + persist_mode); + if (!start_session (dialog_handle->session, streams, &error)) { g_warning ("Failed to start session: %s", error->message); @@ -269,7 +369,8 @@ create_screen_cast_dialog (ScreenCastSession *session, g_object_ref_sink (fake_parent); dialog = GTK_WIDGET (screen_cast_dialog_new (request->app_id, - &session->select)); + &session->select, + session->persist_mode)); gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (fake_parent)); gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); @@ -295,6 +396,161 @@ create_screen_cast_dialog (ScreenCastSession *session, return dialog_handle; } +static Monitor * +find_monitor_by_string (const char *monitor_string) +{ + DisplayStateTracker *display_state_tracker = display_state_tracker_get (); + GList *l; + + for (l = display_state_tracker_get_logical_monitors (display_state_tracker); + l; + l = l->next) + { + LogicalMonitor *logical_monitor = l->data; + GList *monitors; + + for (monitors = logical_monitor_get_monitors (logical_monitor); + monitors; + monitors = monitors->next) + { + Monitor *monitor = monitors->data; + + if (g_strcmp0 (monitor_get_match_string (monitor), monitor_string) == 0) + return monitor; + } + } + + return NULL; +} + +static Window * +find_best_window_by_app_id_and_title (const char *app_id, + const char *title) +{ + ShellIntrospect *shell_introspect = shell_introspect_get (); + Window *best_match; + glong best_match_distance; + GList *l; + + best_match = NULL; + best_match_distance = G_MAXLONG; + + for (l = shell_introspect_get_windows (shell_introspect); l; l = l->next) + { + Window *window = l->data; + glong distance; + + if (g_strcmp0 (window_get_app_id (window), app_id) != 0) + continue; + + distance = str_distance (window_get_title (window), title); + + if (distance == 0) + return window; + + if (distance < best_match_distance) + { + best_match = window; + best_match_distance = distance; + } + } + + return best_match; +} + +static gboolean +restore_stream_from_data (ScreenCastSession *screen_cast_session) + +{ + ScreenCastStreamInfo *info; + g_autoptr(GVariantIter) array_iter = NULL; + g_autoptr(GPtrArray) streams = NULL; + g_autoptr(GError) error = NULL; + ScreenCastSourceType source_type; + GVariant *data; + uint32_t id; + int64_t creation_time; + int64_t last_used_time; + + if (!screen_cast_session->restored.data) + return FALSE; + + streams = g_ptr_array_new_with_free_func (g_free); + + g_variant_get (screen_cast_session->restored.data, + RESTORE_VARIANT_TYPE, + &creation_time, + &last_used_time, + &array_iter); + + while (g_variant_iter_next (array_iter, "(uuv)", &id, &source_type, &data)) + { + switch (source_type) + { + case SCREEN_CAST_SOURCE_TYPE_MONITOR: + { + if (!(screen_cast_session->select.source_types & SCREEN_CAST_SOURCE_TYPE_MONITOR) || + !g_variant_check_format_string (data, MONITOR_TYPE, FALSE)) + goto fail; + + const char *match_string = g_variant_get_string (data, NULL); + Monitor *monitor = find_monitor_by_string (match_string); + + if (!monitor) + goto fail; + + info = g_new0 (ScreenCastStreamInfo, 1); + info->type = SCREEN_CAST_SOURCE_TYPE_MONITOR; + info->data.monitor = monitor; + g_ptr_array_add (streams, info); + } + break; + + case SCREEN_CAST_SOURCE_TYPE_WINDOW: + { + if (!(screen_cast_session->select.source_types & SCREEN_CAST_SOURCE_TYPE_WINDOW) || + !g_variant_check_format_string (data, WINDOW_TYPE, FALSE)) + goto fail; + + const char *app_id = NULL; + const char *title = NULL; + Window *window; + + g_variant_get (data, "(&s&s)", &app_id, &title); + + window = find_best_window_by_app_id_and_title (app_id, title); + + if (!window) + goto fail; + + info = g_new0 (ScreenCastStreamInfo, 1); + info->type = SCREEN_CAST_SOURCE_TYPE_WINDOW; + info->data.window = window; + g_ptr_array_add (streams, info); + } + break; + + default: + goto fail; + } + } + + screen_cast_session->restored.creation_time = creation_time; + + start_session (screen_cast_session, streams, &error); + + if (error) + { + g_warning ("Error restoring stream from session: %s", error->message); + return FALSE; + } + + return TRUE; + +fail: + return FALSE; +} + static gboolean handle_start (XdpImplScreenCast *object, GDBusMethodInvocation *invocation, @@ -307,7 +563,6 @@ handle_start (XdpImplScreenCast *object, const char *sender; g_autoptr(Request) request = NULL; ScreenCastSession *screen_cast_session; - ScreenCastDialogHandle *dialog_handle; GVariantBuilder results_builder; sender = g_dbus_method_invocation_get_sender (invocation); @@ -329,14 +584,19 @@ handle_start (XdpImplScreenCast *object, goto err; } - dialog_handle = create_screen_cast_dialog (screen_cast_session, - invocation, - request, - arg_parent_window); + screen_cast_session->start_invocation = invocation; + if (!restore_stream_from_data (screen_cast_session)) + { + ScreenCastDialogHandle *dialog_handle; - screen_cast_session->start_invocation = invocation; - screen_cast_session->dialog_handle = dialog_handle; + dialog_handle = create_screen_cast_dialog (screen_cast_session, + invocation, + request, + arg_parent_window); + + screen_cast_session->dialog_handle = dialog_handle; + } return TRUE; @@ -356,6 +616,9 @@ handle_select_sources (XdpImplScreenCast *object, const char *arg_app_id, GVariant *arg_options) { + g_autofree gchar *provider = NULL; + g_autoptr(GVariant) restore_data = NULL; + ScreenCastSession *screen_cast_session; Session *session; int response; uint32_t types; @@ -364,6 +627,7 @@ handle_select_sources (XdpImplScreenCast *object, ScreenCastSelection select; GVariantBuilder results_builder; GVariant *results; + uint32_t version; session = lookup_session (arg_session_handle); if (!session) @@ -427,6 +691,21 @@ handle_select_sources (XdpImplScreenCast *object, response = 2; } + screen_cast_session = (ScreenCastSession *)session; + g_variant_lookup (arg_options, "persist_mode", "u", &screen_cast_session->persist_mode); + + if (g_variant_lookup (arg_options, "restore_data", "(suv)", &provider, &version, &restore_data)) + { + if (!g_variant_check_format_string (restore_data, "(suv)", FALSE)) + { + g_warning ("Cannot parse restore data, ignoring"); + goto out; + } + + if (g_strcmp0 (provider, "GNOME") == 0 && version == RESTORE_FORMAT_VERSION) + screen_cast_session->restored.data = g_variant_ref (restore_data); + } + out: g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); results = g_variant_builder_end (&results_builder); @@ -567,6 +846,8 @@ screen_cast_session_finalize (GObject *object) { ScreenCastSession *screen_cast_session = (ScreenCastSession *)object; + g_clear_pointer (&screen_cast_session->streams_to_restore, g_ptr_array_unref); + g_clear_pointer (&screen_cast_session->restored.data, g_variant_unref); g_clear_object (&screen_cast_session->gnome_screen_cast_session); G_OBJECT_CLASS (screen_cast_session_parent_class)->finalize (object); @@ -575,6 +856,8 @@ screen_cast_session_finalize (GObject *object) static void screen_cast_session_init (ScreenCastSession *screen_cast_session) { + screen_cast_session->persist_mode = SCREEN_CAST_PERSIST_MODE_NONE; + screen_cast_session->restored.creation_time = -1; } static void @@ -594,6 +877,14 @@ gboolean screen_cast_init (GDBusConnection *connection, GError **error) { + /* + * Ensure ShellIntrospect and DisplayStateTracker are initialized before + * any screencast session is created to avoid race conditions when restoring + * previous streams. + */ + display_state_tracker_get (); + shell_introspect_get (); + impl_connection = connection; gnome_screen_cast = gnome_screen_cast_new (connection); diff --git a/src/screencast.h b/src/screencast.h index a9be16b..d78066e 100644 --- a/src/screencast.h +++ b/src/screencast.h @@ -38,6 +38,13 @@ typedef enum _ScreenCastCursorMode SCREEN_CAST_CURSOR_MODE_METADATA = 4, } ScreenCastCursorMode; +typedef enum _ScreenCastPersistMode +{ + SCREEN_CAST_PERSIST_MODE_NONE = 0, + SCREEN_CAST_PERSIST_MODE_TRANSIENT = 1, + SCREEN_CAST_PERSIST_MODE_PERSISTENT = 2, +} ScreenCastPersistMode; + typedef struct _ScreenCastSelection { gboolean multiple; diff --git a/src/screencastdialog.c b/src/screencastdialog.c index 3e3b064..d80329e 100644 --- a/src/screencastdialog.c +++ b/src/screencastdialog.c @@ -59,6 +59,7 @@ static void button_clicked (GtkWidget *button, ScreenCastDialog *dialog) { + ScreenCastPersistMode persist_mode; g_autoptr(GPtrArray) streams = NULL; int response; @@ -71,14 +72,16 @@ button_clicked (GtkWidget *button, response = GTK_RESPONSE_OK; streams = screen_cast_widget_get_selected_streams (screen_cast_widget); + persist_mode = screen_cast_widget_get_persist_mode (screen_cast_widget); } else { response = GTK_RESPONSE_CANCEL; + persist_mode = SCREEN_CAST_PERSIST_MODE_NONE; streams = NULL; } - g_signal_emit (dialog, signals[DONE], 0, response, streams); + g_signal_emit (dialog, signals[DONE], 0, response, persist_mode, streams); } static void @@ -120,7 +123,8 @@ screen_cast_dialog_class_init (ScreenCastDialogClass *klass) 0, NULL, NULL, NULL, - G_TYPE_NONE, 2, + G_TYPE_NONE, 3, + G_TYPE_INT, G_TYPE_INT, G_TYPE_PTR_ARRAY); @@ -143,8 +147,9 @@ screen_cast_dialog_init (ScreenCastDialog *dialog) } ScreenCastDialog * -screen_cast_dialog_new (const char *app_id, - ScreenCastSelection *select) +screen_cast_dialog_new (const char *app_id, + ScreenCastSelection *select, + ScreenCastPersistMode persist_mode) { ScreenCastDialog *dialog; ScreenCastWidget *screen_cast_widget; @@ -155,6 +160,7 @@ screen_cast_dialog_new (const char *app_id, screen_cast_widget_set_allow_multiple (screen_cast_widget, select->multiple); screen_cast_widget_set_source_types (screen_cast_widget, select->source_types); + screen_cast_widget_set_persist_mode (screen_cast_widget, persist_mode); return dialog; } diff --git a/src/screencastdialog.h b/src/screencastdialog.h index 1fca470..c132ecf 100644 --- a/src/screencastdialog.h +++ b/src/screencastdialog.h @@ -26,5 +26,6 @@ G_DECLARE_FINAL_TYPE (ScreenCastDialog, screen_cast_dialog, SCREEN_CAST, DIALOG, GtkWindow) -ScreenCastDialog * screen_cast_dialog_new (const char *app_id, - ScreenCastSelection *select); +ScreenCastDialog * screen_cast_dialog_new (const char *app_id, + ScreenCastSelection *select, + ScreenCastPersistMode persist_mode); diff --git a/src/screencastwidget.c b/src/screencastwidget.c index 3119245..c100ad9 100644 --- a/src/screencastwidget.c +++ b/src/screencastwidget.c @@ -51,6 +51,9 @@ struct _ScreenCastWidget GtkWidget *window_list; GtkWidget *window_list_scrolled; + GtkCheckButton *persist_check; + ScreenCastPersistMode persist_mode; + DisplayStateTracker *display_state_tracker; gulong monitors_changed_handler_id; @@ -454,6 +457,7 @@ screen_cast_widget_class_init (ScreenCastWidgetClass *klass) G_TYPE_BOOLEAN); gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/portal/desktop/gnome/screencastwidget.ui"); + gtk_widget_class_bind_template_child (widget_class, ScreenCastWidget, persist_check); gtk_widget_class_bind_template_child (widget_class, ScreenCastWidget, source_type_switcher); gtk_widget_class_bind_template_child (widget_class, ScreenCastWidget, source_type); gtk_widget_class_bind_template_child (widget_class, ScreenCastWidget, monitor_selection); @@ -635,3 +639,22 @@ screen_cast_widget_get_selected_streams (ScreenCastWidget *self) return g_steal_pointer (&streams); } + +void +screen_cast_widget_set_persist_mode (ScreenCastWidget *screen_cast_widget, + ScreenCastPersistMode persist_mode) +{ + screen_cast_widget->persist_mode = persist_mode; + + gtk_widget_set_visible (GTK_WIDGET (screen_cast_widget->persist_check), + persist_mode != SCREEN_CAST_PERSIST_MODE_NONE); +} + +ScreenCastPersistMode +screen_cast_widget_get_persist_mode (ScreenCastWidget *screen_cast_widget) +{ + if (!gtk_check_button_get_active (screen_cast_widget->persist_check)) + return SCREEN_CAST_PERSIST_MODE_NONE; + + return screen_cast_widget->persist_mode; +} diff --git a/src/screencastwidget.h b/src/screencastwidget.h index ad95903..a963f99 100644 --- a/src/screencastwidget.h +++ b/src/screencastwidget.h @@ -40,3 +40,9 @@ void screen_cast_widget_set_source_types (ScreenCastWidget *screen_cast_widg ScreenCastSourceType source_types); GPtrArray *screen_cast_widget_get_selected_streams (ScreenCastWidget *self); + +void screen_cast_widget_set_persist_mode (ScreenCastWidget *screen_cast_widget, + ScreenCastPersistMode persist_mode); + +ScreenCastPersistMode +screen_cast_widget_get_persist_mode (ScreenCastWidget *screen_cast_widget); diff --git a/src/screencastwidget.ui b/src/screencastwidget.ui index fb83b94..0a9028e 100644 --- a/src/screencastwidget.ui +++ b/src/screencastwidget.ui @@ -152,5 +152,13 @@ + + + + + True + Remember this selection + + diff --git a/src/utils.c b/src/utils.c index b7dd472..5e0485c 100644 --- a/src/utils.c +++ b/src/utils.c @@ -45,3 +45,80 @@ xdg_desktop_portal_error_quark (void) G_N_ELEMENTS (xdg_desktop_portal_error_entries)); return (GQuark) quark_volatile; } + +glong +str_distance (const char *a, + const char *b) +{ + g_autofree gint *v0 = NULL; + g_autofree gint *v1 = NULL; + const gchar *s; + const gchar *t; + gunichar sc; + gunichar tc; + glong b_char_len; + glong cost; + glong i; + glong j; + + /* + * Handle degenerate cases. + */ + if (g_strcmp0 (a, b) == 0) + return 0; + else if (!*a) + return g_utf8_strlen (a, -1); + else if (!*b) + return g_utf8_strlen (a, -1); + + b_char_len = g_utf8_strlen (b, -1); + + /* + * Create two vectors to hold our states. + */ + + v0 = g_new0 (gint, b_char_len + 1); + v1 = g_new0 (gint, b_char_len + 1); + + /* + * initialize v0 (the previous row of distances). + * this row is A[0][i]: edit distance for an empty a. + * the distance is just the number of characters to delete from b. + */ + for (i = 0; i < b_char_len + 1; i++) + v0[i] = i; + + for (i = 0, s = a; s && *s; i++, s = g_utf8_next_char(s)) + { + /* + * Calculate v1 (current row distances) from the previous row v0. + */ + + sc = g_utf8_get_char(s); + + /* + * first element of v1 is A[i+1][0] + * + * edit distance is delete (i+1) chars from a to match empty + * b. + */ + v1[0] = i + 1; + + /* + * use formula to fill in the rest of the row. + */ + for (j = 0, t = b; t && *t; j++, t = g_utf8_next_char(t)) + { + tc = g_utf8_get_char(t); + cost = (sc == tc) ? 0 : 1; + v1[j+1] = MIN (v1[j] + 1, MIN (v0[j+1] + 1, v0[j] + cost)); + } + + /* + * copy v1 (current row) to v0 (previous row) for next iteration. + */ + memcpy (v0, v1, sizeof(gint) * b_char_len); + } + + return v1[b_char_len]; +} diff --git a/src/utils.h b/src/utils.h index 5fdfda9..fa3f1b0 100644 --- a/src/utils.h +++ b/src/utils.h @@ -37,3 +37,6 @@ typedef enum { #define XDG_DESKTOP_PORTAL_ERROR xdg_desktop_portal_error_quark () GQuark xdg_desktop_portal_error_quark (void); + +glong str_distance (const char *a, + const char *b);