Files
clapper/src/bin/clapper-app/clapper-app-utils.c
Rafał Dzięgiel 366315077e 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
2025-04-26 18:33:02 +02:00

710 lines
17 KiB
C

/* Clapper Application
* Copyright (C) 2024 Rafał Dzięgiel <rafostar.github@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include "clapper-app-utils.h"
#include "clapper-app-media-item-box.h"
#ifdef HAVE_GRAPHVIZ
#include <graphviz/cgraph.h>
#include <graphviz/gvc.h>
#endif
#ifdef G_OS_WIN32
#include <windows.h>
#ifdef HAVE_WIN_PROCESS_THREADS_API
#include <processthreadsapi.h>
#endif
#ifdef HAVE_WIN_TIME_API
#include <timeapi.h>
#endif
#endif
#define GST_CAT_DEFAULT clapper_app_utils_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
void
clapper_app_utils_debug_init (void)
{
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperapputils", 0,
"Clapper App Utils");
}
/* Windows specific functions */
#ifdef G_OS_WIN32
/*
* clapper_app_utils_win_enforce_hi_res_clock:
*
* Enforce high resolution clock by explicitly disabling Windows
* timer resolution power throttling. When disabled, system remembers
* and honors any previous timer resolution request by the process.
*
* By default, Windows 11 may automatically ignore the timer
* resolution requests in certain scenarios.
*/
void
clapper_app_utils_win_enforce_hi_res_clock (void)
{
#ifdef HAVE_WIN_PROCESS_THREADS_API
PROCESS_POWER_THROTTLING_STATE PowerThrottling;
gboolean success;
RtlZeroMemory (&PowerThrottling, sizeof (PowerThrottling));
PowerThrottling.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
PowerThrottling.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
PowerThrottling.StateMask = 0; // Always honor timer resolution requests
success = (gboolean) SetProcessInformation(
GetCurrentProcess (),
ProcessPowerThrottling,
&PowerThrottling,
sizeof (PowerThrottling));
/* Not an error. Older Windows does not have this functionality, but
* also honor hi-res clock by default anyway, so do not print then. */
GST_INFO ("Windows hi-res clock support is %senforced",
(success) ? "" : "NOT ");
#endif
}
/*
* clapper_app_utils_win_hi_res_clock_start:
*
* Start Windows high resolution clock which will improve
* accuracy of various Windows timer APIs and precision
* of #GstSystemClock during playback.
*
* On Windows 10 version 2004 (and older), this function affects
* a global Windows setting. On any other (newer) version this
* will only affect a single process.
*
* Returns: Timer resolution period value.
*/
guint
clapper_app_utils_win_hi_res_clock_start (void)
{
guint resolution = 0;
#ifdef HAVE_WIN_TIME_API
TIMECAPS time_caps;
MMRESULT res;
if ((res = timeGetDevCaps (&time_caps, sizeof (TIMECAPS))) != TIMERR_NOERROR) {
GST_WARNING ("Could not query timer resolution, code: %u", res);
return 0;
}
resolution = MIN (MAX (time_caps.wPeriodMin, 1), time_caps.wPeriodMax);
if ((res = timeBeginPeriod (resolution)) != TIMERR_NOERROR) {
GST_WARNING ("Could not request timer resolution, code: %u", res);
return 0;
}
GST_INFO ("Started Windows hi-res clock, precision: %ums", resolution);
#endif
return resolution;
}
/*
* clapper_app_utils_win_hi_res_clock_stop:
* @resolution: started resolution value (non-zero)
*
* Stop previously started Microsoft Windows high resolution clock.
*/
void
clapper_app_utils_win_hi_res_clock_stop (guint resolution)
{
#ifdef HAVE_WIN_TIME_API
MMRESULT res;
if ((res = timeEndPeriod (resolution)) == TIMERR_NOERROR)
GST_INFO ("Stopped Windows hi-res clock");
else
GST_ERROR ("Could not stop hi-res clock, code: %u", res);
#endif
}
/* Extensions are used only on Windows */
const gchar *const *
clapper_app_utils_get_extensions (void)
{
static const gchar *const all_extensions[] = {
"avi", "claps", "m2ts", "mkv", "mov",
"mp4", "webm", "wmv", NULL
};
return all_extensions;
}
const gchar *const *
clapper_app_utils_get_subtitles_extensions (void)
{
static const gchar *const subs_extensions[] = {
"srt", "vtt", NULL
};
return subs_extensions;
}
#endif // G_OS_WIN32
const gchar *const *
clapper_app_utils_get_mime_types (void)
{
static const gchar *const all_mime_types[] = {
"video/*",
"audio/*",
"application/claps",
"application/x-subrip",
"text/x-ssa",
NULL
};
return all_mime_types;
}
const gchar *const *
clapper_app_utils_get_subtitles_mime_types (void)
{
static const gchar *const subs_mime_types[] = {
"application/x-subrip",
"text/x-ssa",
NULL
};
return subs_mime_types;
}
void
clapper_app_utils_parse_progression (ClapperQueueProgressionMode mode,
const gchar **icon, const gchar **label)
{
const gchar *const icon_names[] = {
"action-unavailable-symbolic",
"media-playlist-consecutive-symbolic",
"media-playlist-repeat-song-symbolic",
"media-playlist-repeat-symbolic",
"media-playlist-shuffle-symbolic",
NULL
};
const gchar *const labels[] = {
_("No progression"),
_("Consecutive"),
_("Repeat item"),
_("Carousel"),
_("Shuffle"),
NULL
};
*icon = icon_names[mode];
*label = labels[mode];
}
gboolean
clapper_app_utils_is_subtitles_file (GFile *file)
{
GFileInfo *info;
gboolean is_subs = FALSE;
if ((info = g_file_query_info (file,
G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE,
G_FILE_QUERY_INFO_NONE,
NULL, NULL))) {
const gchar *content_type = NULL;
if (g_file_info_has_attribute (info,
G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE)) {
content_type = g_file_info_get_content_type (info);
} else if (g_file_info_has_attribute (info,
G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE)) {
content_type = g_file_info_get_attribute_string (info,
G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE);
}
is_subs = (content_type && g_strv_contains (
clapper_app_utils_get_subtitles_mime_types (),
content_type));
g_object_unref (info);
}
return is_subs;
}
gboolean
clapper_app_utils_value_for_item_is_valid (const GValue *value)
{
if (G_VALUE_HOLDS (value, GTK_TYPE_WIDGET))
return CLAPPER_APP_IS_MEDIA_ITEM_BOX (g_value_get_object (value));
if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)
|| G_VALUE_HOLDS (value, G_TYPE_FILE))
return TRUE;
if (G_VALUE_HOLDS (value, G_TYPE_STRING))
return gst_uri_is_valid (g_value_get_string (value));
return FALSE;
}
gboolean
clapper_app_utils_files_from_list_model (GListModel *files_model, GFile ***files, gint *n_files)
{
guint i, len = g_list_model_get_n_items (files_model);
if (G_UNLIKELY (len == 0 || len > G_MAXINT))
return FALSE;
*files = g_new (GFile *, len + 1);
if (n_files)
*n_files = (gint) len;
for (i = 0; i < len; ++i) {
(*files)[i] = g_list_model_get_item (files_model, i);
}
(*files)[i] = NULL;
return TRUE;
}
gboolean
clapper_app_utils_files_from_slist (GSList *file_list, GFile ***files, gint *n_files)
{
GSList *fl;
guint len, i = 0;
len = g_slist_length (file_list);
if (G_UNLIKELY (len == 0 || len > G_MAXINT))
return FALSE;
*files = g_new (GFile *, len + 1);
if (n_files)
*n_files = (gint) len;
for (fl = file_list; fl != NULL; fl = fl->next) {
(*files)[i] = (GFile *) g_object_ref (fl->data);
i++;
}
(*files)[i] = NULL;
return TRUE;
}
gboolean
clapper_app_utils_files_from_string (const gchar *string, GFile ***files, gint *n_files)
{
GSList *slist = NULL;
gchar **uris = g_strsplit (string, "\n", 0);
guint i;
gboolean success;
for (i = 0; uris[i]; ++i) {
const gchar *uri = uris[i];
if (!gst_uri_is_valid (uri))
continue;
slist = g_slist_append (slist, g_file_new_for_uri (uri));
}
g_strfreev (uris);
if (!slist)
return FALSE;
success = clapper_app_utils_files_from_slist (slist, files, n_files);
g_slist_free_full (slist, g_object_unref);
return success;
}
gboolean
clapper_app_utils_files_from_command_line (GApplicationCommandLine *cmd_line, GFile ***files, gint *n_files)
{
GSList *slist = NULL;
gchar **argv;
gint i, argc = 0;
gboolean success;
argv = g_application_command_line_get_arguments (cmd_line, &argc);
for (i = 1; i < argc; ++i)
slist = g_slist_append (slist, g_application_command_line_create_file_for_arg (cmd_line, argv[i]));
g_strfreev (argv);
if (!slist)
return FALSE;
success = clapper_app_utils_files_from_slist (slist, files, n_files);
g_slist_free_full (slist, g_object_unref);
return success;
}
static inline gboolean
_files_from_file (GFile *file, GFile ***files, gint *n_files)
{
*files = g_new (GFile *, 2);
(*files)[0] = g_object_ref (file);
(*files)[1] = NULL;
if (n_files)
*n_files = 1;
return TRUE;
}
gboolean
clapper_app_utils_files_from_value (const GValue *value, GFile ***files, gint *n_files)
{
if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) {
return clapper_app_utils_files_from_slist (
(GSList *) g_value_get_boxed (value), files, n_files);
} else if (G_VALUE_HOLDS (value, G_TYPE_FILE)) {
return _files_from_file (
(GFile *) g_value_get_object (value), files, n_files);
} else if (G_VALUE_HOLDS (value, G_TYPE_STRING)) {
return clapper_app_utils_files_from_string (
g_value_get_string (value), files, n_files);
}
return FALSE;
}
void
clapper_app_utils_files_free (GFile **files)
{
gint i;
for (i = 0; files[i]; ++i)
g_object_unref (files[i]);
g_free (files);
}
static inline gboolean
_parse_feature_name (gchar *str, const gchar **feature_name)
{
if (!str)
return FALSE;
g_strstrip (str);
if (str[0] == '\0')
return FALSE;
*feature_name = str;
return TRUE;
}
static inline gboolean
_parse_feature_rank (gchar *str, GstRank *rank)
{
if (!str)
return FALSE;
g_strstrip (str);
if (str[0] == '\0')
return FALSE;
if (g_ascii_isdigit (str[0])) {
gulong l;
gchar *endptr;
l = strtoul (str, &endptr, 10);
if (endptr > str && endptr[0] == 0) {
*rank = (GstRank) l;
} else {
return FALSE;
}
} else if (g_ascii_strcasecmp (str, "NONE") == 0) {
*rank = GST_RANK_NONE;
} else if (g_ascii_strcasecmp (str, "MARGINAL") == 0) {
*rank = GST_RANK_MARGINAL;
} else if (g_ascii_strcasecmp (str, "SECONDARY") == 0) {
*rank = GST_RANK_SECONDARY;
} else if (g_ascii_strcasecmp (str, "PRIMARY") == 0) {
*rank = GST_RANK_PRIMARY;
} else if (g_ascii_strcasecmp (str, "MAX") == 0) {
*rank = (GstRank) G_MAXINT;
} else {
return FALSE;
}
return TRUE;
}
void
clapper_app_utils_iterate_plugin_feature_ranks (GSettings *settings,
ClapperAppUtilsIterRanks callback, gpointer user_data)
{
gchar **split, **walk, *stored_overrides;
const gchar *env_overrides;
gboolean from_env = FALSE;
stored_overrides = g_settings_get_string (settings, "plugin-feature-ranks");
env_overrides = g_getenv ("GST_PLUGIN_FEATURE_RANK");
/* Iterate from GSettings, then from ENV */
parse_overrides:
split = g_strsplit ((from_env) ? env_overrides : stored_overrides, ",", 0);
for (walk = split; *walk; walk++) {
gchar **values;
if (!strchr (*walk, ':'))
continue;
values = g_strsplit (*walk, ":", 2);
if (g_strv_length (values) == 2) {
GstRank rank;
const gchar *feature_name;
if (_parse_feature_name (values[0], &feature_name)
&& _parse_feature_rank (values[1], &rank))
callback (feature_name, rank, from_env, user_data);
}
g_strfreev (values);
}
g_strfreev (split);
if (!from_env && env_overrides) {
from_env = TRUE;
goto parse_overrides;
}
g_free (stored_overrides);
}
GstElement *
clapper_app_utils_make_element (const gchar *string)
{
gchar *char_loc;
if (strcmp (string, "none") == 0)
return NULL;
char_loc = strchr (string, ' ');
if (char_loc) {
GstElement *element;
GError *error = NULL;
element = gst_parse_bin_from_description (string, TRUE, &error);
if (error) {
GST_ERROR ("Bin parse error: \"%s\", reason: %s", string, error->message);
g_error_free (error);
}
return element;
}
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);
}