diff --git a/meson.build b/meson.build index b8765d65..bc65f752 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) @@ -160,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/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/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" + } + ] +} diff --git a/src/bin/clapper-app/clapper-app-application.c b/src/bin/clapper-app/clapper-app-application.c index fbea6a02..3d682fe9 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; @@ -317,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) { @@ -395,6 +415,102 @@ show_info (GSimpleAction *action, GVariant *param, gpointer user_data) gtk_window_present (GTK_WINDOW (info_window)); } +static void +_launch_pipeline_cb (GtkFileLauncher *launcher, + 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 +_show_pipeline_cb (GObject *source G_GNUC_UNUSED, + GAsyncResult *res, ClapperAppWindowData *win_data) +{ + GTask *task = G_TASK (res); + GtkWindow *window; + GFile *svg_file; + 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); + + return; + } + + if (window) { + 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, win_data); + g_object_unref (launcher); + } else { + g_free (win_data); + } + + g_object_unref (svg_file); +} + +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)) + window = gtk_window_get_transient_for (window); + + if (G_UNLIKELY (window == NULL)) + return; + + if (self->cancellable) { + g_cancellable_cancel (self->cancellable); + g_object_unref (self->cancellable); + } + self->cancellable = g_cancellable_new (); + + win_data = g_new (ClapperAppWindowData, 1); + win_data->app = self; + win_data->id = gtk_application_window_get_id (GTK_APPLICATION_WINDOW (window)); + + 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 show_about (GSimpleAction *action, GVariant *param, gpointer user_data) { @@ -714,6 +830,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 }, }; @@ -722,6 +839,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 }}, @@ -756,6 +874,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) { @@ -779,6 +910,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-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..447696ed 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,178 @@ 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) +{ + /* 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_user_data_dir (), CLAPPER_APP_ID, "tmp", subdir, NULL); +} + +#ifdef HAVE_GRAPHVIZ +static GFile * +_create_tmp_subdir (const gchar *subdir, GCancellable *cancellable, GError **error) +{ + GFile *tmp_dir; + GError *my_error = NULL; + + tmp_dir = _get_tmp_dir (subdir); + + 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 (my_error); + } + + return tmp_dir; +} +#endif + +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 = NULL, *dot_data = NULL, *img_data = NULL; + gint fd; + guint size = 0; + + if (!(tmp_subdir = _create_tmp_subdir ("pipelines", cancellable, &error))) + goto finish; + + 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"); + 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 (); + gvLayout (gvc, graph, "dot"); + gvRenderData (gvc, graph, "svg", &img_data, &size); + + agclose (graph); + gvFreeContext (gvc); + + 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, + "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!"); + +finish: + g_free (template); + g_free (dot_data); + g_free (img_data); +#else + g_set_error (&error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Cannot create graph file when compiled without Graphviz"); +#endif + + 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 +_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 f0332d35..d02b6bc5 100644 --- a/src/bin/clapper-app/clapper-app-utils.h +++ b/src/bin/clapper-app/clapper-app-utils.h @@ -84,4 +84,10 @@ 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 +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); + 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; } diff --git a/src/bin/clapper-app/meson.build b/src/bin/clapper-app/meson.build index 96021cee..50c29f03 100644 --- a/src/bin/clapper-app/meson.build +++ b/src/bin/clapper-app/meson.build @@ -94,6 +94,23 @@ 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 +endif + is_windows = ['windows'].contains(host_machine.system()) if is_windows 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 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 + + + 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