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 <tsaunier@igalia.com>
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: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/9759>
--- a/ges/ges-formatter.c
+++ b/ges/ges-formatter.c
@@ -40,38 +40,6 @@
 #include "ges-pitivi-formatter.h"
 #endif
 
-#ifdef HAS_PYTHON
-#include <Python.h>
-#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 <gmodule.h>
-#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 <dlfcn.h>
-#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 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<gresources>
-  <gresource prefix="/ges/">
-    <file>python/gesotioformatter.py</file>
-  </gresource>
-</gresources>
--- 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 <tsaunier@igalia.com>
-#
-
-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

