https://bugs.gentoo.org/957940 https://gitlab.freedesktop.org/gstreamer/gstreamer/-/commit/47874799e328f2b4f081b623efe9d0ae059d0fd8 From 47874799e328f2b4f081b623efe9d0ae059d0fd8 Mon Sep 17 00:00:00 2001 From: Thibault Saunier Date: Sun, 28 Sep 2025 09:48:05 -0300 Subject: [PATCH] ges: Move OTIO formatter to a separate Python plugin The GES OpenTimelineIO formatter was previously embedded directly in libges using GLib resources, this was all a bit complex for not much benefit, moreover it started to crash recently. Move the formatter to a standalone Python plugin that will be loaded through the standard GStreamer Python plugin infrastructure making it all more simple. The formatter is now located in subprojects/gst-python/plugins/ges/ and will only be loaded when the Python plugin is available and opentimelineio is installed. Fixes https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/4676 Part-of: --- a/ges/ges-formatter.c +++ b/ges/ges-formatter.c @@ -40,38 +40,6 @@ #include "ges-pitivi-formatter.h" #endif -#ifdef HAS_PYTHON -#include -#include "ges-resources.h" - -/* - * We need to call dlopen() directly on macOS to workaround a macOS runtime - * linker bug. When there are nested dlopen() calls and the second dlopen() is - * called from another library (such as gmodule), @loader_path is resolved as - * @executable_path and RPATHs are read from the executable (gst-plugin-scanner) - * instead of the library itself (libgstges.dylib). This doesn't happen if the - * second dlopen() call is directly in the source code of the library. - * Previously seen at: - * https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1171#note_2290789 - */ -#ifdef G_OS_WIN32 -#include -#define ges_module_open(fname) g_module_open(fname,0) -#define ges_module_error g_module_error -#define ges_module_symbol(module,name,symbol) g_module_symbol(module,name,symbol) -#else -#include -#define ges_module_open(fname) dlopen(fname,RTLD_NOW | RTLD_GLOBAL) -#define ges_module_error dlerror -static inline gboolean -ges_module_symbol (gpointer handle, const char *name, gpointer * symbol) -{ - *symbol = dlsym (handle, name); - return *symbol != NULL; -} -#endif -#endif /* HAS_PYTHON */ - GST_DEBUG_CATEGORY_STATIC (ges_formatter_debug); #undef GST_CAT_DEFAULT #define GST_CAT_DEFAULT ges_formatter_debug @@ -578,103 +546,19 @@ _list_formatters (GType * formatters, guint n_formatters) static void load_python_formatters (void) { -#ifdef HAS_PYTHON - PyGILState_STATE state = 0; - PyObject *main_module, *main_locals; - GError *err = NULL; - GResource *resource = ges_get_resource (); - GBytes *bytes = - g_resource_lookup_data (resource, "/ges/python/gesotioformatter.py", - G_RESOURCE_LOOKUP_FLAGS_NONE, &err); - PyObject *code = NULL, *res = NULL; - gboolean we_initialized = FALSE; - gpointer has_python = NULL; - - GST_LOG ("Checking to see if libpython is already loaded"); - if (ges_module_symbol (ges_module_open (NULL), - "_Py_NoneStruct", &has_python) && has_python) { - GST_LOG ("libpython is already loaded"); - } else { - GST_LOG ("loading libpython by name: %s", PY_LIB_FNAME); - if (!ges_module_open (PY_LIB_FNAME)) { - GST_ERROR ("Couldn't load libpython. Reason: %s", ges_module_error ()); - return; - } - } - - if (!Py_IsInitialized ()) { - GST_LOG ("python wasn't already initialized"); - /* set the correct plugin for registering stuff */ - Py_Initialize (); - we_initialized = TRUE; - } else { - GST_LOG ("python was already initialized"); - state = PyGILState_Ensure (); - } - - if (!bytes) { - GST_DEBUG ("Could not load gesotioformatter: %s\n", err->message); + GstPlugin *python_plugin = gst_registry_find_plugin (gst_registry_get (), + "python"); - g_clear_error (&err); + if (python_plugin && !gst_plugin_is_loaded (python_plugin)) { + GST_DEBUG ("Loading python plugin to load python formatters"); - goto done; - } - - main_module = PyImport_AddModule ("__main__"); - if (main_module == NULL) { - GST_WARNING ("Could not add main module"); - PyErr_Print (); - PyErr_Clear (); - goto done; - } - - main_locals = PyModule_GetDict (main_module); - /* Compiling the code ourself so it has a proper filename */ - code = - Py_CompileString (g_bytes_get_data (bytes, NULL), "gesotioformatter.py", - Py_file_input); - if (PyErr_Occurred ()) { - PyErr_Print (); - PyErr_Clear (); - goto done; - } - res = PyEval_EvalCode ((gpointer) code, main_locals, main_locals); - Py_XDECREF (code); - Py_XDECREF (res); - if (PyErr_Occurred ()) { - PyObject *exception_backtrace; - PyObject *exception_type; - PyObject *exception_value, *exception_value_repr, *exception_value_str; - - PyErr_Fetch (&exception_type, &exception_value, &exception_backtrace); - PyErr_NormalizeException (&exception_type, &exception_value, - &exception_backtrace); - - exception_value_repr = PyObject_Repr (exception_value); - exception_value_str = - PyUnicode_AsEncodedString (exception_value_repr, "utf-8", "Error ~"); - GST_INFO ("Could not load OpenTimelineIO formatter: %s", - PyBytes_AS_STRING (exception_value_str)); - - Py_XDECREF (exception_type); - Py_XDECREF (exception_value); - Py_XDECREF (exception_backtrace); - - Py_XDECREF (exception_value_repr); - Py_XDECREF (exception_value_str); - PyErr_Clear (); - } - -done: - if (bytes) - g_bytes_unref (bytes); - - if (we_initialized) { - PyEval_SaveThread (); - } else { - PyGILState_Release (state); + GstPlugin *loaded_plugin = gst_plugin_load (python_plugin); + if (!loaded_plugin) { + GST_INFO ("Failed to load python plugin, not loading python formatters"); + } else { + gst_object_unref (loaded_plugin); + } } -#endif /* HAS_PYTHON */ } void --- a/ges/ges.resource +++ /dev/null @@ -1,6 +0,0 @@ - - - - python/gesotioformatter.py - - --- a/ges/meson.build +++ b/ges/meson.build @@ -168,16 +168,7 @@ parser = custom_target('gesparselex', command : [flex, '-Ppriv_ges_parse_yy', '--header-file=@OUTPUT1@', '-o', '@OUTPUT0@', '@INPUT@'] ) -ges_resources = [] -if has_python - ges_resources = gnome.compile_resources( - 'ges-resources', 'ges.resource', - source_dir: '.', - c_name: 'ges' - ) -endif - -libges = library('ges-1.0', ges_sources, parser, ges_resources, +libges = library('ges-1.0', ges_sources, parser, version : libversion, soversion : soversion, darwin_versions : osxversion, --- a/ges/python/gesotioformatter.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python -# -*- Mode: Python -*- -# vi:si:et:sw=4:sts=4:ts=4 -# -# Copyright (C) 2019 Igalia S.L -# Authors: -# Thibault Saunier -# - -import sys - -import gi -import tempfile -gi.require_version("GES", "1.0") -gi.require_version("Gst", "1.0") - -from gi.repository import GObject -from gi.repository import Gst -Gst.init(None) -from gi.repository import GES -from gi.repository import GLib -from collections import OrderedDict - -import opentimelineio as otio -otio.adapters.from_name('xges') - -class GESOtioFormatter(GES.Formatter): - def do_save_to_uri(self, timeline, uri, overwrite): - if not Gst.uri_is_valid(uri) or Gst.uri_get_protocol(uri) != "file": - Gst.error("Protocol not supported for file: %s" % uri) - return False - - with tempfile.NamedTemporaryFile(suffix=".xges") as tmpxges: - timeline.get_asset().save(timeline, "file://" + tmpxges.name, None, overwrite) - - linker = otio.media_linker.MediaLinkingPolicy.ForceDefaultLinker - otio_timeline = otio.adapters.read_from_file(tmpxges.name, "xges", media_linker_name=linker) - location = Gst.uri_get_location(uri) - out_adapter = otio.adapters.from_filepath(location) - otio.adapters.write_to_file(otio_timeline, Gst.uri_get_location(uri), out_adapter.name) - - return True - - def do_can_load_uri(self, uri): - try: - if not Gst.uri_is_valid(uri) or Gst.uri_get_protocol(uri) != "file": - return False - except GLib.Error as e: - Gst.error(str(e)) - return False - - if uri.endswith(".xges"): - return False - - try: - return otio.adapters.from_filepath(Gst.uri_get_location(uri)) is not None - except Exception as e: - Gst.info("Could not load %s -> %s" % (uri, e)) - return False - - - def do_load_from_uri(self, timeline, uri): - location = Gst.uri_get_location(uri) - in_adapter = otio.adapters.from_filepath(location) - assert(in_adapter) # can_load_uri should have ensured it is loadable - - linker = otio.media_linker.MediaLinkingPolicy.ForceDefaultLinker - otio_timeline = otio.adapters.read_from_file( - location, - in_adapter.name, - media_linker_name=linker - ) - - with tempfile.NamedTemporaryFile(suffix=".xges") as tmpxges: - otio.adapters.write_to_file(otio_timeline, tmpxges.name, "xges") - formatter = GES.Formatter.get_default().extract() - timeline.get_asset().add_formatter(formatter) - return formatter.load_from_uri(timeline, "file://" + tmpxges.name) - -GObject.type_register(GESOtioFormatter) -known_extensions_mimetype_map = [ - ("otio", "xml", "fcpxml"), - ("application/vnd.pixar.opentimelineio+json", "application/vnd.apple-xmeml+xml", "application/vnd.apple-fcp+xml") -] - -extensions = [] -for adapter in otio.plugins.ActiveManifest().adapters: - if adapter.name != 'xges': - extensions.extend(adapter.suffixes) - -extensions_mimetype_map = [[], []] -for i, ext in enumerate(known_extensions_mimetype_map[0]): - if ext in extensions: - extensions_mimetype_map[0].append(ext) - extensions_mimetype_map[1].append(known_extensions_mimetype_map[1][i]) - extensions.remove(ext) -extensions_mimetype_map[0].extend(extensions) - -GES.FormatterClass.register_metas(GESOtioFormatter, "otioformatter", - "GES Formatter using OpenTimelineIO", - ','.join(extensions_mimetype_map[0]), - ';'.join(extensions_mimetype_map[1]), 0.1, Gst.Rank.SECONDARY) -- GitLab