From 681d5b869977c39623e5009ca0182be5fefdcf5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 17 Nov 2024 11:30:47 +0100 Subject: [PATCH 1/9] clapper: Add a function to get pipeline graph data --- src/lib/clapper/clapper-player.c | 22 ++++++++++++++++++++++ src/lib/clapper/clapper-player.h | 3 +++ 2 files changed, 25 insertions(+) diff --git a/src/lib/clapper/clapper-player.c b/src/lib/clapper/clapper-player.c index 217b1d07..e394b839 100644 --- a/src/lib/clapper/clapper-player.c +++ b/src/lib/clapper/clapper-player.c @@ -2187,6 +2187,28 @@ clapper_player_add_feature (ClapperPlayer *self, ClapperFeature *feature) clapper_features_manager_add_feature (self->features_manager, feature, GST_OBJECT (self)); } +/** + * clapper_player_make_pipeline_graph: + * @player: a #ClapperPlayer + * @details: a #GstDebugGraphDetails level + * + * Make current #GStreamer pipeline graph in `graphviz` dot format. + * + * Applications can use tools like `graphviz` to display returned + * data or just save it to a file as-is for the user to do it manually. + * + * Returns: (transfer full): current pipeline description in dot format. + * + * Since: 0.10 + */ +gchar * +clapper_player_make_pipeline_graph (ClapperPlayer *self, GstDebugGraphDetails details) +{ + g_return_val_if_fail (CLAPPER_IS_PLAYER (self), NULL); + + return gst_debug_bin_to_dot_data (GST_BIN (self->playbin), details); +} + static void clapper_player_thread_start (ClapperThreadedObject *threaded_object) { diff --git a/src/lib/clapper/clapper-player.h b/src/lib/clapper/clapper-player.h index c47b1a78..48547168 100644 --- a/src/lib/clapper/clapper-player.h +++ b/src/lib/clapper/clapper-player.h @@ -204,4 +204,7 @@ void clapper_player_seek_custom (ClapperPlayer *player, gdouble position, Clappe CLAPPER_API void clapper_player_add_feature (ClapperPlayer *player, ClapperFeature *feature); +CLAPPER_API +gchar * clapper_player_make_pipeline_graph (ClapperPlayer *player, GstDebugGraphDetails details); + G_END_DECLS From dca8fbd33646383426e0b67db8d13b30eac26381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 16 Feb 2025 17:35:58 +0100 Subject: [PATCH 2/9] clapper-app: Add GStreamer pipeline preview Allow to preview GStreamer pipeline while playing content. This makes it easier to check what is used underneath. --- meson.build | 6 ++ meson_options.txt | 5 + src/bin/clapper-app/clapper-app-application.c | 55 +++++++++++ src/bin/clapper-app/clapper-app-info-window.c | 7 ++ src/bin/clapper-app/clapper-app-utils.c | 93 +++++++++++++++++++ src/bin/clapper-app/clapper-app-utils.h | 3 + src/bin/clapper-app/meson.build | 11 +++ .../clapper-app/ui/clapper-app-info-window.ui | 11 +++ 8 files changed, 191 insertions(+) diff --git a/meson.build b/meson.build index b8765d65..4c8d9b4a 100644 --- a/meson.build +++ b/meson.build @@ -89,6 +89,12 @@ libadwaita_dep = dependency('libadwaita-1', peas_dep = dependency('libpeas-2', required: false, ) +cgraph_dep = dependency('libcgraph', + required: false, +) +gvc_dep = dependency('libgvc', + required: false, +) cc = meson.get_compiler('c') libm = cc.find_library('m', required: false) diff --git a/meson_options.txt b/meson_options.txt index 7adf225d..d7b8adcb 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -41,6 +41,11 @@ option('enhancers-loader', value: 'enabled', description: 'Ability to load libpeas based plugins that enhance capabilities' ) +option('pipeline-preview', + type: 'feature', + value: 'auto', + description: 'Ability to preview GStreamer pipeline in clapper-app' +) # Features option('discoverer', diff --git a/src/bin/clapper-app/clapper-app-application.c b/src/bin/clapper-app/clapper-app-application.c index 0e5ab8ba..eae3e248 100644 --- a/src/bin/clapper-app/clapper-app-application.c +++ b/src/bin/clapper-app/clapper-app-application.c @@ -395,6 +395,59 @@ show_info (GSimpleAction *action, GVariant *param, gpointer user_data) gtk_window_present (GTK_WINDOW (info_window)); } +static void +_show_pipeline_cb (GtkFileLauncher *launcher, + GAsyncResult *res, gpointer user_data G_GNUC_UNUSED) +{ + GError *error = NULL; + + if (!gtk_file_launcher_launch_finish (launcher, res, &error)) { + if (error->domain != GTK_DIALOG_ERROR || error->code != GTK_DIALOG_ERROR_DISMISSED) { + GST_ERROR ("Could not launch pipeline preview, reason: %s", + GST_STR_NULL (error->message)); + } + g_error_free (error); + } +} + +static void +show_pipeline (GSimpleAction *action, GVariant *param, gpointer user_data) +{ + GtkApplication *gtk_app = GTK_APPLICATION (user_data); + GtkWindow *window; + GtkFileLauncher *launcher; + GFile *svg_file; + GError *error = NULL; + + window = gtk_application_get_active_window (gtk_app); + + while (window && !CLAPPER_APP_IS_WINDOW (window)) + window = gtk_window_get_transient_for (window); + + if (G_UNLIKELY (window == NULL)) + return; + + if (!(svg_file = clapper_app_utils_create_pipeline_svg_file ( + clapper_app_window_get_player (CLAPPER_APP_WINDOW (window)), &error))) { + GST_ERROR ("Could not create pipeline graph file, reason: %s", + GST_STR_NULL (error->message)); + g_error_free (error); + + return; + } + + launcher = gtk_file_launcher_new (svg_file); + g_object_unref (svg_file); + +#if GTK_CHECK_VERSION(4,12,0) + gtk_file_launcher_set_always_ask (launcher, TRUE); +#endif + + gtk_file_launcher_launch (launcher, window, NULL, + (GAsyncReadyCallback) _show_pipeline_cb, NULL); + g_object_unref (launcher); +} + static void show_about (GSimpleAction *action, GVariant *param, gpointer user_data) { @@ -712,6 +765,7 @@ clapper_app_application_constructed (GObject *object) { "clear-queue", clear_queue, NULL, NULL, NULL }, { "new-window", new_window, NULL, NULL, NULL }, { "info", show_info, NULL, NULL, NULL }, + { "pipeline", show_pipeline, NULL, NULL, NULL }, { "preferences", show_preferences, NULL, NULL, NULL }, { "about", show_about, NULL, NULL, NULL }, }; @@ -720,6 +774,7 @@ clapper_app_application_constructed (GObject *object) { "app.add-uri", { "u", NULL, NULL }}, { "app.new-window", { "n", NULL, NULL }}, { "app.info", { "i", NULL, NULL }}, + { "app.pipeline", { "p", NULL, NULL }}, { "app.preferences", { "comma", NULL, NULL }}, { "app.about", { "F1", NULL, NULL }}, { "win.toggle-fullscreen", { "F11", "f", NULL }}, diff --git a/src/bin/clapper-app/clapper-app-info-window.c b/src/bin/clapper-app/clapper-app-info-window.c index 390e1ede..c1d2f9a1 100644 --- a/src/bin/clapper-app/clapper-app-info-window.c +++ b/src/bin/clapper-app/clapper-app-info-window.c @@ -36,6 +36,8 @@ struct _ClapperAppInfoWindow GtkWidget *astreams_list; GtkWidget *sstreams_list; + GtkWidget *pipeline_button; + ClapperPlayer *player; }; @@ -174,6 +176,10 @@ clapper_app_info_window_init (ClapperAppInfoWindow *self) gtk_widget_remove_css_class (self->vstreams_list, "view"); gtk_widget_remove_css_class (self->astreams_list, "view"); gtk_widget_remove_css_class (self->sstreams_list, "view"); + +#ifdef HAVE_GRAPHVIZ + gtk_widget_set_visible (self->pipeline_button, TRUE); +#endif } static void @@ -256,6 +262,7 @@ clapper_app_info_window_class_init (ClapperAppInfoWindowClass *klass) gtk_widget_class_bind_template_child (widget_class, ClapperAppInfoWindow, vstreams_list); gtk_widget_class_bind_template_child (widget_class, ClapperAppInfoWindow, astreams_list); gtk_widget_class_bind_template_child (widget_class, ClapperAppInfoWindow, sstreams_list); + gtk_widget_class_bind_template_child (widget_class, ClapperAppInfoWindow, pipeline_button); gtk_widget_class_bind_template_callback (widget_class, media_duration_closure); gtk_widget_class_bind_template_callback (widget_class, playback_element_name_closure); diff --git a/src/bin/clapper-app/clapper-app-utils.c b/src/bin/clapper-app/clapper-app-utils.c index 74b1308e..c03b6c0f 100644 --- a/src/bin/clapper-app/clapper-app-utils.c +++ b/src/bin/clapper-app/clapper-app-utils.c @@ -23,6 +23,11 @@ #include "clapper-app-utils.h" #include "clapper-app-media-item-box.h" +#ifdef HAVE_GRAPHVIZ +#include +#include +#endif + #ifdef G_OS_WIN32 #include #ifdef HAVE_WIN_PROCESS_THREADS_API @@ -527,3 +532,91 @@ clapper_app_utils_make_element (const gchar *string) return gst_element_factory_make (string, NULL); } + +#ifdef HAVE_GRAPHVIZ +static GFile * +_create_tmp_subdir (const gchar *subdir) +{ + GFile *tmp_dir; + GError *error = NULL; + + tmp_dir = g_file_new_build_filename ( + g_get_tmp_dir (), "." CLAPPER_APP_ID, subdir, NULL); + + if (!g_file_make_directory_with_parents (tmp_dir, NULL, &error)) { + if (error->domain != G_IO_ERROR || error->code != G_IO_ERROR_EXISTS) { + GST_ERROR ("Could not create temp dir, reason: %s", + GST_STR_NULL (error->message)); + g_clear_object (&tmp_dir); // return NULL + } + g_error_free (error); + } + + return tmp_dir; +} +#endif + +GFile * +clapper_app_utils_create_pipeline_svg_file (ClapperPlayer *player, GError **error) +{ + GFile *tmp_file = NULL; + +#ifdef HAVE_GRAPHVIZ + GFile *tmp_subdir; + Agraph_t *graph; + GVC_t *gvc; + gchar *path, *template, *dot_data, *img_data = NULL; + gint fd; + guint size = 0; + + tmp_subdir = _create_tmp_subdir ("pipelines"); + if (G_UNLIKELY (tmp_subdir == NULL)) + return NULL; + + path = g_file_get_path (tmp_subdir); + g_object_unref (tmp_subdir); + + template = g_build_filename (path, "pipeline-XXXXXX.svg", NULL); + g_free (path); + + fd = g_mkstemp (template); // Modifies template to actual filename + + if (G_UNLIKELY (fd == -1)) { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Could not open temp file for writing"); + g_free (template); + + return NULL; + } + + dot_data = clapper_player_make_pipeline_graph (player, GST_DEBUG_GRAPH_SHOW_ALL); + graph = agmemread (dot_data); + + gvc = gvContext (); + gvLayout (gvc, graph, "dot"); + gvRenderData (gvc, graph, "svg", &img_data, &size); + + agclose (graph); + gvFreeContext (gvc); + g_free (dot_data); + + if (write (fd, img_data, size) != -1) { + tmp_file = g_file_new_for_path (template); + } else { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Could not write data to temp file"); + } + + /* Always close the file IO */ + if (G_UNLIKELY (close (fd) == -1)) + GST_ERROR ("Could not close temp file!"); + + g_free (img_data); + g_free (template); +#else + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Cannot create graph file when compiled without Graphviz"); +#endif + + return tmp_file; +} diff --git a/src/bin/clapper-app/clapper-app-utils.h b/src/bin/clapper-app/clapper-app-utils.h index f0332d35..cfb16e77 100644 --- a/src/bin/clapper-app/clapper-app-utils.h +++ b/src/bin/clapper-app/clapper-app-utils.h @@ -84,4 +84,7 @@ void clapper_app_utils_iterate_plugin_feature_ranks (GSettings *settings, Clappe G_GNUC_INTERNAL GstElement * clapper_app_utils_make_element (const gchar *string); +G_GNUC_INTERNAL +GFile * clapper_app_utils_create_pipeline_svg_file (ClapperPlayer *player, GError **error); + G_END_DECLS diff --git a/src/bin/clapper-app/meson.build b/src/bin/clapper-app/meson.build index 220b66c0..8d8d7a5f 100644 --- a/src/bin/clapper-app/meson.build +++ b/src/bin/clapper-app/meson.build @@ -94,6 +94,17 @@ clapperapp_c_args = [ '-DGST_USE_UNSTABLE_API', ] +pp_option = get_option('pipeline-preview') + +if not pp_option.disabled() + if cgraph_dep.found() and gvc_dep.found() + clapperapp_c_args += ['-DHAVE_GRAPHVIZ'] + clapperapp_deps += [cgraph_dep, gvc_dep] + elif pp_option.enabled() + error('pipeline-preview option was enabled, but required dependencies were not found') + endif +endif + is_windows = ['windows'].contains(host_machine.system()) if is_windows diff --git a/src/bin/clapper-app/ui/clapper-app-info-window.ui b/src/bin/clapper-app/ui/clapper-app-info-window.ui index b64353b0..e8c70a95 100644 --- a/src/bin/clapper-app/ui/clapper-app-info-window.ui +++ b/src/bin/clapper-app/ui/clapper-app-info-window.ui @@ -232,6 +232,17 @@ + + + center + Show Pipeline + app.pipeline + false + + + From b8a4a90b6ca2054ecdf6d75f1aa02afff8174cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Tue, 18 Feb 2025 18:30:03 +0100 Subject: [PATCH 3/9] clapper-app: Delete temp dir on exit Pipeline preview creates temp files, so delete whole directory recursively on app exit. Since these files are used through launcher/portal with another app, we do not know when user finished using them, thus we cannot detect and remove them earlier. --- src/bin/clapper-app/clapper-app-utils.c | 66 ++++++++++++++++++++++++- src/bin/clapper-app/clapper-app-utils.h | 3 ++ src/bin/clapper-app/main.c | 2 + 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/bin/clapper-app/clapper-app-utils.c b/src/bin/clapper-app/clapper-app-utils.c index c03b6c0f..135feffc 100644 --- a/src/bin/clapper-app/clapper-app-utils.c +++ b/src/bin/clapper-app/clapper-app-utils.c @@ -533,6 +533,19 @@ clapper_app_utils_make_element (const gchar *string) return gst_element_factory_make (string, NULL); } +/* + * _get_tmp_dir: + * @subdir: (nullable): an optional subdirectory + * + * Returns: (transfer full): a newly constructed #GFile + */ +static inline GFile * +_get_tmp_dir (const gchar *subdir) +{ + return g_file_new_build_filename ( + g_get_tmp_dir (), "." CLAPPER_APP_ID, subdir, NULL); +} + #ifdef HAVE_GRAPHVIZ static GFile * _create_tmp_subdir (const gchar *subdir) @@ -540,8 +553,7 @@ _create_tmp_subdir (const gchar *subdir) GFile *tmp_dir; GError *error = NULL; - tmp_dir = g_file_new_build_filename ( - g_get_tmp_dir (), "." CLAPPER_APP_ID, subdir, NULL); + tmp_dir = _get_tmp_dir (subdir); if (!g_file_make_directory_with_parents (tmp_dir, NULL, &error)) { if (error->domain != G_IO_ERROR || error->code != G_IO_ERROR_EXISTS) { @@ -620,3 +632,53 @@ clapper_app_utils_create_pipeline_svg_file (ClapperPlayer *player, GError **erro return tmp_file; } + +static gboolean +_delete_dir_recursive (GFile *dir, GError **error) +{ + GFileEnumerator *dir_enum; + + if ((dir_enum = g_file_enumerate_children (dir, + G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, error))) { + while (TRUE) { + GFileInfo *info = NULL; + GFile *child = NULL; + + if (!g_file_enumerator_iterate (dir_enum, &info, + &child, NULL, error) || !info) + break; + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { + if (!_delete_dir_recursive (child, error)) + break; + } else if (!g_file_delete (child, NULL, error)) { + break; + } + } + + g_object_unref (dir_enum); + } + + if (*error != NULL) + return FALSE; + + return g_file_delete (dir, NULL, error); +} + +void +clapper_app_utils_delete_tmp_dir (void) +{ + GFile *tmp_dir = _get_tmp_dir (NULL); + GError *error = NULL; + + if (!_delete_dir_recursive (tmp_dir, &error)) { + if (error->domain != G_IO_ERROR || error->code != G_IO_ERROR_NOT_FOUND) { + GST_ERROR ("Could not remove temp dir, reason: %s", + GST_STR_NULL (error->message)); + } + g_error_free (error); + } + + g_object_unref (tmp_dir); +} diff --git a/src/bin/clapper-app/clapper-app-utils.h b/src/bin/clapper-app/clapper-app-utils.h index cfb16e77..90771021 100644 --- a/src/bin/clapper-app/clapper-app-utils.h +++ b/src/bin/clapper-app/clapper-app-utils.h @@ -87,4 +87,7 @@ GstElement * clapper_app_utils_make_element (const gchar *string); G_GNUC_INTERNAL GFile * clapper_app_utils_create_pipeline_svg_file (ClapperPlayer *player, GError **error); +G_GNUC_INTERNAL +void clapper_app_utils_delete_tmp_dir (void); + G_END_DECLS diff --git a/src/bin/clapper-app/main.c b/src/bin/clapper-app/main.c index 63ca4165..865603a9 100644 --- a/src/bin/clapper-app/main.c +++ b/src/bin/clapper-app/main.c @@ -74,5 +74,7 @@ main (gint argc, gchar **argv) clapper_app_utils_win_hi_res_clock_stop (resolution); #endif + clapper_app_utils_delete_tmp_dir (); + return status; } From 224326857b15ad5f7fccf544f08cd3bdb0772dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Tue, 18 Feb 2025 21:32:35 +0100 Subject: [PATCH 4/9] clapper-app: Create pipeline preview file async --- src/bin/clapper-app/clapper-app-application.c | 93 +++++++++++++++---- src/bin/clapper-app/clapper-app-application.h | 1 + src/bin/clapper-app/clapper-app-utils.c | 67 ++++++++----- src/bin/clapper-app/clapper-app-utils.h | 2 +- 4 files changed, 120 insertions(+), 43 deletions(-) diff --git a/src/bin/clapper-app/clapper-app-application.c b/src/bin/clapper-app/clapper-app-application.c index eae3e248..288ae041 100644 --- a/src/bin/clapper-app/clapper-app-application.c +++ b/src/bin/clapper-app/clapper-app-application.c @@ -41,6 +41,7 @@ struct _ClapperAppApplication GtkApplication parent; GSettings *settings; + GCancellable *cancellable; gboolean need_init_state; }; @@ -61,6 +62,12 @@ struct ClapperPluginData struct ClapperPluginFeatureData features[10]; }; +typedef struct +{ + ClapperAppApplication *app; + guint id; +} ClapperAppWindowData; + typedef struct { const gchar *action; @@ -396,7 +403,7 @@ show_info (GSimpleAction *action, GVariant *param, gpointer user_data) } static void -_show_pipeline_cb (GtkFileLauncher *launcher, +_launch_pipeline_cb (GtkFileLauncher *launcher, GAsyncResult *res, gpointer user_data G_GNUC_UNUSED) { GError *error = NULL; @@ -411,14 +418,52 @@ _show_pipeline_cb (GtkFileLauncher *launcher, } static void -show_pipeline (GSimpleAction *action, GVariant *param, gpointer user_data) +_show_pipeline_cb (GObject *source G_GNUC_UNUSED, + GAsyncResult *res, ClapperAppWindowData *win_data) { - GtkApplication *gtk_app = GTK_APPLICATION (user_data); + GTask *task = G_TASK (res); GtkWindow *window; - GtkFileLauncher *launcher; GFile *svg_file; GError *error = NULL; + svg_file = (GFile *) g_task_propagate_pointer (task, &error); + + if (error) { + if (error->domain != G_IO_ERROR || error->code != G_IO_ERROR_CANCELLED) { + GST_ERROR ("Could not create pipeline graph file, reason: %s", + GST_STR_NULL (error->message)); + } + g_error_free (error); + g_free (win_data); + + return; + } + + if ((window = gtk_application_get_window_by_id ( + GTK_APPLICATION (win_data->app), win_data->id))) { + GtkFileLauncher *launcher = gtk_file_launcher_new (svg_file); + +#if GTK_CHECK_VERSION(4,12,0) + gtk_file_launcher_set_always_ask (launcher, TRUE); +#endif + + gtk_file_launcher_launch (launcher, window, NULL, + (GAsyncReadyCallback) _launch_pipeline_cb, NULL); + g_object_unref (launcher); + } + + g_object_unref (svg_file); + g_free (win_data); +} + +static void +show_pipeline (GSimpleAction *action, GVariant *param, gpointer user_data) +{ + ClapperAppApplication *self = CLAPPER_APP_APPLICATION_CAST (user_data); + GtkApplication *gtk_app = GTK_APPLICATION (self); + GtkWindow *window; + ClapperAppWindowData *win_data; + window = gtk_application_get_active_window (gtk_app); while (window && !CLAPPER_APP_IS_WINDOW (window)) @@ -427,25 +472,19 @@ show_pipeline (GSimpleAction *action, GVariant *param, gpointer user_data) if (G_UNLIKELY (window == NULL)) return; - if (!(svg_file = clapper_app_utils_create_pipeline_svg_file ( - clapper_app_window_get_player (CLAPPER_APP_WINDOW (window)), &error))) { - GST_ERROR ("Could not create pipeline graph file, reason: %s", - GST_STR_NULL (error->message)); - g_error_free (error); - - return; + if (self->cancellable) { + g_cancellable_cancel (self->cancellable); + g_object_unref (self->cancellable); } + self->cancellable = g_cancellable_new (); - launcher = gtk_file_launcher_new (svg_file); - g_object_unref (svg_file); + win_data = g_new (ClapperAppWindowData, 1); + win_data->app = self; + win_data->id = gtk_application_window_get_id (GTK_APPLICATION_WINDOW (window)); -#if GTK_CHECK_VERSION(4,12,0) - gtk_file_launcher_set_always_ask (launcher, TRUE); -#endif - - gtk_file_launcher_launch (launcher, window, NULL, - (GAsyncReadyCallback) _show_pipeline_cb, NULL); - g_object_unref (launcher); + clapper_app_utils_create_pipeline_svg_file_async ( + clapper_app_window_get_player (CLAPPER_APP_WINDOW (window)), + self->cancellable, (GAsyncReadyCallback) _show_pipeline_cb, win_data); } static void @@ -809,6 +848,19 @@ clapper_app_application_constructed (GObject *object) G_OBJECT_CLASS (parent_class)->constructed (object); } +static void +clapper_app_application_dispose (GObject *object) +{ + ClapperAppApplication *self = CLAPPER_APP_APPLICATION_CAST (object); + + if (self->cancellable) { + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + } + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + static void clapper_app_application_finalize (GObject *object) { @@ -832,6 +884,7 @@ clapper_app_application_class_init (ClapperAppApplicationClass *klass) "Clapper App Application"); gobject_class->constructed = clapper_app_application_constructed; + gobject_class->dispose = clapper_app_application_dispose; gobject_class->finalize = clapper_app_application_finalize; gtk_application_class->window_removed = clapper_app_application_window_removed; diff --git a/src/bin/clapper-app/clapper-app-application.h b/src/bin/clapper-app/clapper-app-application.h index 17a7e1c1..8edfc585 100644 --- a/src/bin/clapper-app/clapper-app-application.h +++ b/src/bin/clapper-app/clapper-app-application.h @@ -19,6 +19,7 @@ #include #include +#include #include G_BEGIN_DECLS diff --git a/src/bin/clapper-app/clapper-app-utils.c b/src/bin/clapper-app/clapper-app-utils.c index 135feffc..3da4a28d 100644 --- a/src/bin/clapper-app/clapper-app-utils.c +++ b/src/bin/clapper-app/clapper-app-utils.c @@ -548,42 +548,42 @@ _get_tmp_dir (const gchar *subdir) #ifdef HAVE_GRAPHVIZ static GFile * -_create_tmp_subdir (const gchar *subdir) +_create_tmp_subdir (const gchar *subdir, GCancellable *cancellable, GError **error) { GFile *tmp_dir; - GError *error = NULL; + GError *my_error = NULL; tmp_dir = _get_tmp_dir (subdir); - if (!g_file_make_directory_with_parents (tmp_dir, NULL, &error)) { - if (error->domain != G_IO_ERROR || error->code != G_IO_ERROR_EXISTS) { - GST_ERROR ("Could not create temp dir, reason: %s", - GST_STR_NULL (error->message)); + if (!g_file_make_directory_with_parents (tmp_dir, cancellable, &my_error)) { + if (my_error->domain != G_IO_ERROR || my_error->code != G_IO_ERROR_EXISTS) { + *error = g_error_copy (my_error); g_clear_object (&tmp_dir); // return NULL } - g_error_free (error); + g_error_free (my_error); } return tmp_dir; } #endif -GFile * -clapper_app_utils_create_pipeline_svg_file (ClapperPlayer *player, GError **error) +static void +_create_pipeline_svg_file_in_thread (GTask *task, GObject *source G_GNUC_UNUSED, + ClapperPlayer *player, GCancellable *cancellable) { GFile *tmp_file = NULL; + GError *error = NULL; #ifdef HAVE_GRAPHVIZ GFile *tmp_subdir; Agraph_t *graph; GVC_t *gvc; - gchar *path, *template, *dot_data, *img_data = NULL; + gchar *path, *template = NULL, *dot_data = NULL, *img_data = NULL; gint fd; guint size = 0; - tmp_subdir = _create_tmp_subdir ("pipelines"); - if (G_UNLIKELY (tmp_subdir == NULL)) - return NULL; + if (!(tmp_subdir = _create_tmp_subdir ("pipelines", cancellable, &error))) + goto finish; path = g_file_get_path (tmp_subdir); g_object_unref (tmp_subdir); @@ -594,14 +594,16 @@ clapper_app_utils_create_pipeline_svg_file (ClapperPlayer *player, GError **erro fd = g_mkstemp (template); // Modifies template to actual filename if (G_UNLIKELY (fd == -1)) { - g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + g_set_error (&error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Could not open temp file for writing"); - g_free (template); - - return NULL; + goto finish; } dot_data = clapper_player_make_pipeline_graph (player, GST_DEBUG_GRAPH_SHOW_ALL); + + if (g_cancellable_is_cancelled (cancellable)) + goto close_and_finish; + graph = agmemread (dot_data); gvc = gvContext (); @@ -610,27 +612,48 @@ clapper_app_utils_create_pipeline_svg_file (ClapperPlayer *player, GError **erro agclose (graph); gvFreeContext (gvc); - g_free (dot_data); + + if (g_cancellable_is_cancelled (cancellable)) + goto close_and_finish; if (write (fd, img_data, size) != -1) { tmp_file = g_file_new_for_path (template); } else { - g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + g_set_error (&error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Could not write data to temp file"); } +close_and_finish: /* Always close the file IO */ if (G_UNLIKELY (close (fd) == -1)) GST_ERROR ("Could not close temp file!"); - g_free (img_data); +finish: g_free (template); + g_free (dot_data); + g_free (img_data); #else - g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + g_set_error (&error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Cannot create graph file when compiled without Graphviz"); #endif - return tmp_file; + if (tmp_file) + g_task_return_pointer (task, tmp_file, (GDestroyNotify) g_object_unref); + else + g_task_return_error (task, error); +} + +void +clapper_app_utils_create_pipeline_svg_file_async (ClapperPlayer *player, + GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) +{ + GTask *task; + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_task_data (task, gst_object_ref (player), (GDestroyNotify) gst_object_unref); + g_task_run_in_thread (task, (GTaskThreadFunc) _create_pipeline_svg_file_in_thread); + + g_object_unref (task); } static gboolean diff --git a/src/bin/clapper-app/clapper-app-utils.h b/src/bin/clapper-app/clapper-app-utils.h index 90771021..d02b6bc5 100644 --- a/src/bin/clapper-app/clapper-app-utils.h +++ b/src/bin/clapper-app/clapper-app-utils.h @@ -85,7 +85,7 @@ G_GNUC_INTERNAL GstElement * clapper_app_utils_make_element (const gchar *string); G_GNUC_INTERNAL -GFile * clapper_app_utils_create_pipeline_svg_file (ClapperPlayer *player, GError **error); +void clapper_app_utils_create_pipeline_svg_file_async (ClapperPlayer *player, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); G_GNUC_INTERNAL void clapper_app_utils_delete_tmp_dir (void); From 160dbce2652f0242b912b936bffe18f30ca08116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Wed, 19 Feb 2025 16:57:37 +0100 Subject: [PATCH 5/9] clapper-app: Add "show pipeline" key shortcut to help overlay --- src/bin/clapper-app/ui/clapper-app-help-overlay.ui | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/bin/clapper-app/ui/clapper-app-help-overlay.ui b/src/bin/clapper-app/ui/clapper-app-help-overlay.ui index 57c51527..9ee14cbd 100644 --- a/src/bin/clapper-app/ui/clapper-app-help-overlay.ui +++ b/src/bin/clapper-app/ui/clapper-app-help-overlay.ui @@ -26,6 +26,12 @@ <Ctrl>question + + + Show pipeline + <Ctrl><Shift>p + + Toggle fullscreen From 5bd6ba89488ba268537a112583f576ff00f3849d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Wed, 19 Feb 2025 18:11:56 +0100 Subject: [PATCH 6/9] meson: Add "pipeline-preview" to printed summary --- meson.build | 5 +++++ src/bin/clapper-app/meson.build | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/meson.build b/meson.build index 4c8d9b4a..bc65f752 100644 --- a/meson.build +++ b/meson.build @@ -166,3 +166,8 @@ if build_clapper summary(name, clapper_available_features.contains(name) ? 'Yes' : 'No', section: 'Features') endforeach endif +if build_clapperapp + foreach name : clapperapp_possible_functionalities + summary(name, clapperapp_available_functionalities.contains(name) ? 'Yes' : 'No', section: 'Functionalities') + endforeach +endif diff --git a/src/bin/clapper-app/meson.build b/src/bin/clapper-app/meson.build index 8d8d7a5f..b08baa4c 100644 --- a/src/bin/clapper-app/meson.build +++ b/src/bin/clapper-app/meson.build @@ -94,12 +94,18 @@ clapperapp_c_args = [ '-DGST_USE_UNSTABLE_API', ] +clapperapp_possible_functionalities = [ + 'pipeline-preview', +] +clapperapp_available_functionalities = [] + pp_option = get_option('pipeline-preview') if not pp_option.disabled() if cgraph_dep.found() and gvc_dep.found() clapperapp_c_args += ['-DHAVE_GRAPHVIZ'] clapperapp_deps += [cgraph_dep, gvc_dep] + clapperapp_available_functionalities += 'pipeline-preview' elif pp_option.enabled() error('pipeline-preview option was enabled, but required dependencies were not found') endif From a75184a471641d39b943424d2571dd2039845b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Wed, 19 Feb 2025 18:50:42 +0100 Subject: [PATCH 7/9] flatpak: Build graphviz Needed for pipeline preview app functionality --- .../com.github.rafostar.Clapper-nightly.json | 1 + pkgs/flatpak/com.github.rafostar.Clapper.json | 1 + pkgs/flatpak/testing/graphviz.json | 38 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 pkgs/flatpak/testing/graphviz.json diff --git a/pkgs/flatpak/com.github.rafostar.Clapper-nightly.json b/pkgs/flatpak/com.github.rafostar.Clapper-nightly.json index 0516efcb..2fcbee17 100644 --- a/pkgs/flatpak/com.github.rafostar.Clapper-nightly.json +++ b/pkgs/flatpak/com.github.rafostar.Clapper-nightly.json @@ -66,6 +66,7 @@ "testing/dav1d.json", "testing/gstreamer.json", "testing/gst-plugins-rs.json", + "testing/graphviz.json", { "name": "clapper", "buildsystem": "meson", diff --git a/pkgs/flatpak/com.github.rafostar.Clapper.json b/pkgs/flatpak/com.github.rafostar.Clapper.json index 2f7b6c71..0d7abff2 100644 --- a/pkgs/flatpak/com.github.rafostar.Clapper.json +++ b/pkgs/flatpak/com.github.rafostar.Clapper.json @@ -51,6 +51,7 @@ "flathub/lib/libmicrodns.json", "flathub/lib/libpeas.json", "flathub/gstreamer-1.0/gstreamer.json", + "testing/graphviz.json", { "name": "clapper", "buildsystem": "meson", diff --git a/pkgs/flatpak/testing/graphviz.json b/pkgs/flatpak/testing/graphviz.json new file mode 100644 index 00000000..a9f0631a --- /dev/null +++ b/pkgs/flatpak/testing/graphviz.json @@ -0,0 +1,38 @@ +{ + "name": "graphviz", + "buildsystem": "autotools", + "config-opts": [ + "--without-x", + "--without-gtk", + "--without-gtkgl", + "--without-gtkglext", + "--without-gdk", + "--without-gdk-pixbuf", + "--without-qt", + "--disable-static", + "--disable-sharp", + "--disable-go", + "--disable-guile", + "--disable-java", + "--disable-javascript", + "--disable-lua", + "--disable-perl", + "--disable-php", + "--disable-python", + "--disable-r", + "--disable-ruby", + "--disable-tcl" + ], + "cleanup": [ + "/bin", + "/share/man", + "/share/graphviz" + ], + "sources": [ + { + "type": "archive", + "url": "https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/12.2.1/graphviz-12.2.1.tar.gz", + "sha256": "242bc18942eebda6db4039f108f387ec97856fc91ba47f21e89341c34b554df8" + } + ] +} From 366315077e870bb2cdc9047e8515516f89a9f842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 23 Feb 2025 17:28:21 +0100 Subject: [PATCH 8/9] clapper-app: Use user data dir for storing pipeline exports System tmp directory does not work within containers such as Flatpak for our usage with file launcher, so make our own temp in app data dir --- src/bin/clapper-app/clapper-app-utils.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bin/clapper-app/clapper-app-utils.c b/src/bin/clapper-app/clapper-app-utils.c index 3da4a28d..447696ed 100644 --- a/src/bin/clapper-app/clapper-app-utils.c +++ b/src/bin/clapper-app/clapper-app-utils.c @@ -542,8 +542,10 @@ clapper_app_utils_make_element (const gchar *string) static inline GFile * _get_tmp_dir (const gchar *subdir) { + /* XXX: System tmp directory does not work within containers such as Flatpak + * for our usage with file launcher, so make our own temp in app data dir */ return g_file_new_build_filename ( - g_get_tmp_dir (), "." CLAPPER_APP_ID, subdir, NULL); + g_get_user_data_dir (), CLAPPER_APP_ID, "tmp", subdir, NULL); } #ifdef HAVE_GRAPHVIZ From d09824fb1af20d8f38c1e341b8233be92a2806df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sat, 26 Apr 2025 21:25:13 +0200 Subject: [PATCH 9/9] clapper-app: Show error dialog on pipeline preview failure --- src/bin/clapper-app/clapper-app-application.c | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/bin/clapper-app/clapper-app-application.c b/src/bin/clapper-app/clapper-app-application.c index 288ae041..acc882b1 100644 --- a/src/bin/clapper-app/clapper-app-application.c +++ b/src/bin/clapper-app/clapper-app-application.c @@ -324,6 +324,19 @@ _assemble_initial_state (GtkWindow *window) g_object_unref (builder); } +static void +_show_error_dialog (GError *error, GtkWindow *parent) +{ + AdwDialog *dialog; + + dialog = adw_alert_dialog_new ("Error", error->message); + adw_alert_dialog_add_response (ADW_ALERT_DIALOG (dialog), "close", _("Close")); + adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (dialog), "close"); + adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (dialog), "close"); + + adw_dialog_present (dialog, GTK_WIDGET (parent)); +} + static void add_files (GSimpleAction *action, GVariant *param, gpointer user_data) { @@ -404,17 +417,24 @@ show_info (GSimpleAction *action, GVariant *param, gpointer user_data) static void _launch_pipeline_cb (GtkFileLauncher *launcher, - GAsyncResult *res, gpointer user_data G_GNUC_UNUSED) + GAsyncResult *res, ClapperAppWindowData *win_data) { GError *error = NULL; if (!gtk_file_launcher_launch_finish (launcher, res, &error)) { if (error->domain != GTK_DIALOG_ERROR || error->code != GTK_DIALOG_ERROR_DISMISSED) { + GtkWindow *window; + GST_ERROR ("Could not launch pipeline preview, reason: %s", GST_STR_NULL (error->message)); + + if ((window = gtk_application_get_window_by_id ( + GTK_APPLICATION (win_data->app), win_data->id))) + _show_error_dialog (error, window); } g_error_free (error); } + g_free (win_data); } static void @@ -427,11 +447,15 @@ _show_pipeline_cb (GObject *source G_GNUC_UNUSED, GError *error = NULL; svg_file = (GFile *) g_task_propagate_pointer (task, &error); + window = gtk_application_get_window_by_id ( + GTK_APPLICATION (win_data->app), win_data->id); if (error) { if (error->domain != G_IO_ERROR || error->code != G_IO_ERROR_CANCELLED) { GST_ERROR ("Could not create pipeline graph file, reason: %s", GST_STR_NULL (error->message)); + if (window) + _show_error_dialog (error, window); } g_error_free (error); g_free (win_data); @@ -439,8 +463,7 @@ _show_pipeline_cb (GObject *source G_GNUC_UNUSED, return; } - if ((window = gtk_application_get_window_by_id ( - GTK_APPLICATION (win_data->app), win_data->id))) { + if (window) { GtkFileLauncher *launcher = gtk_file_launcher_new (svg_file); #if GTK_CHECK_VERSION(4,12,0) @@ -448,12 +471,13 @@ _show_pipeline_cb (GObject *source G_GNUC_UNUSED, #endif gtk_file_launcher_launch (launcher, window, NULL, - (GAsyncReadyCallback) _launch_pipeline_cb, NULL); + (GAsyncReadyCallback) _launch_pipeline_cb, win_data); g_object_unref (launcher); + } else { + g_free (win_data); } g_object_unref (svg_file); - g_free (win_data); } static void