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] 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 + + +