Implement a formatter based on [OpenTimelineIO]
authorThibault Saunier <tsaunier@igalia.com>
Tue, 5 Feb 2019 18:46:49 +0000 (15:46 -0300)
committerThibault Saunier <tsaunier@igalia.com>
Fri, 26 Jul 2019 17:48:51 +0000 (13:48 -0400)
[OpenTimelineIO]: http://opentimeline.io/

ges/ges-formatter.c
ges/ges.resource [new file with mode: 0644]
ges/meson.build
ges/python/gesotioformatter.py [new file with mode: 0644]
meson.build
meson_options.txt

index 1262ae5..36cb6ba 100644 (file)
 #include "ges-formatter.h"
 #include "ges-internal.h"
 #include "ges.h"
+#ifdef HAS_PYTHON
+#include <Python.h>
+#include "ges-resources.h"
+#endif
+
+GST_DEBUG_CATEGORY_STATIC (ges_formatter_debug);
+#undef GST_CAT_DEFAULT
+#define GST_CAT_DEFAULT ges_formatter_debug
 
 /* TODO Add a GCancellable somewhere in the API */
 static void ges_extractable_interface_init (GESExtractableInterface * iface);
@@ -100,10 +108,9 @@ _register_metas (GESExtractableInterface * iface, GObjectClass * class,
   ges_meta_container_register_meta_string (container, GES_META_READ_WRITE,
       GES_META_FORMAT_VERSION, NULL);
 
-  g_clear_pointer (&fclass->name, g_free);
-  g_clear_pointer (&fclass->description, g_free);
-  g_clear_pointer (&fclass->extension, g_free);
-  g_clear_pointer (&fclass->mimetype, g_free);
+  /* We are leaking the metadata but we don't really have choice here
+   * as calling ges_init() after deinit() is allowed.
+   */
 
   return TRUE;
 }
@@ -513,11 +520,110 @@ _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;
+  GModule *libpython;
+  gpointer has_python = NULL;
+
+  GST_LOG ("Checking to see if libpython is already loaded");
+  if (g_module_symbol (g_module_open (NULL, G_MODULE_BIND_LOCAL),
+          "_Py_NoneStruct", &has_python) && has_python) {
+    GST_LOG ("libpython is already loaded");
+  } else {
+    const gchar *libpython_path =
+        PY_LIB_LOC "/libpython" PYTHON_VERSION PY_ABI_FLAGS "." PY_LIB_SUFFIX;
+    GST_LOG ("loading libpython from '%s'", libpython_path);
+    libpython = g_module_open (libpython_path, 0);
+    if (!libpython) {
+      GST_ERROR ("Couldn't g_module_open libpython. Reason: %s",
+          g_module_error ());
+      return;
+    }
+  }
+
+  if (!Py_IsInitialized ()) {
+    GST_LOG ("python wasn't 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);
+
+    g_clear_error (&err);
+
+    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 ()) {
+    PyErr_Print ();
+    PyErr_Clear ();
+  }
+
+done:
+  if (bytes)
+    g_bytes_unref (bytes);
+
+  if (we_initialized) {
+    PyEval_SaveThread ();
+  } else {
+    PyGILState_Release (state);
+  }
+#endif /* HAS_PYTHON */
+}
+
 void
 _init_formatter_assets (void)
 {
   GType *formatters;
   guint n_formatters;
+  static gsize init_debug = 0;
+
+  if (g_once_init_enter (&init_debug)) {
+
+    GST_DEBUG_CATEGORY_INIT (ges_formatter_debug, "gesformatter",
+        GST_DEBUG_FG_YELLOW, "ges formatter");
+    g_once_init_leave (&init_debug, TRUE);
+  }
+
+  load_python_formatters ();
+
 
   formatters = g_type_children (GES_TYPE_FORMATTER, &n_formatters);
   _list_formatters (formatters, n_formatters);
diff --git a/ges/ges.resource b/ges/ges.resource
new file mode 100644 (file)
index 0000000..638794e
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/ges/">
+    <file>python/gesotioformatter.py</file>
+  </gresource>
+</gresources>
index 8dace54..1bb6eb5 100644 (file)
@@ -157,7 +157,16 @@ parser = custom_target('gesparselex',
   command : [flex, '-Ppriv_ges_parse_yy', '--header-file=@OUTPUT1@', '-o', '@OUTPUT0@', '@INPUT@']
 )
 
-libges = library('ges-1.0', ges_sources, parser,
+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,
     version : libversion,
     soversion : soversion,
     darwin_versions : osxversion,
diff --git a/ges/python/gesotioformatter.py b/ges/python/gesotioformatter.py
new file mode 100644 (file)
index 0000000..2815e26
--- /dev/null
@@ -0,0 +1,107 @@
+#!/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
+
+try:
+    import opentimelineio as otio
+    otio.adapters.from_name('xges')
+except Exception as e:
+    Gst.info("Could not load OpenTimelineIO: %s" % e)
+    otio = None
+
+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)
+
+if otio is not None:
+    GObject.type_register(GESOtioFormatter)
+    known_extensions_mimetype_map = [
+        ("otio", "xml", "fcpxml"),
+        ("application/otio", "application/xmeml", "application/fcpxml")
+    ]
+
+    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)
index 84e7554..932f42f 100644 (file)
@@ -110,9 +110,6 @@ if gstvalidate_dep.found()
     cdata.set('HAVE_GST_VALIDATE', 1)
 endif
 
-configure_file(output : 'config.h', configuration : cdata)
-
-
 gir = find_program('g-ir-scanner', required : get_option('introspection'))
 gnome = import('gnome')
 
@@ -128,6 +125,72 @@ gir_init_section = [ '--add-init-section=' + \
     'gst_init(NULL,NULL);' + \
     'ges_init();', '--quiet']
 
+has_python = false
+if build_gir
+  pymod = import('python')
+  python = pymod.find_installation(required: get_option('python'))
+  python_dep = python.dependency(required : get_option('python'))
+  if python_dep.found()
+    python_abi_flags = python.get_variable('ABIFLAGS', '')
+    pylib_loc = get_option('libpython-dir')
+
+    error_msg = ''
+    if not cc.compiles('#include <Python.h>', dependencies: [python_dep])
+      error_msg = 'Could not compile a simple program against python'
+    elif pylib_loc == ''
+      check_path_exists = 'import os, sys; assert(os.path.exists(sys.argv[1]))'
+      pylib_loc = python.get_variable('LIBPL', '')
+      if host_machine.system() != 'windows'
+        pylib_ldlibrary = python.get_variable('LDLIBRARY', '')
+        if host_machine.system() == 'darwin'
+          # OSX is a pain. Python as shipped by apple installs libpython in /usr/lib
+          # so we hardcode that. Other systems can use -Dlibpythondir to
+          # override this.
+          pylib_loc = '/usr/lib'
+        else
+          if run_command(python, '-c', check_path_exists, join_paths(pylib_loc, pylib_ldlibrary)).returncode() != 0
+            # Workaround for Fedora
+            pylib_loc = python.get_variable('LIBDIR', '')
+            message('pylib_loc = @0@'.format(pylib_loc))
+          endif
+        endif
+
+        res = run_command(python, '-c', check_path_exists, join_paths(pylib_loc, pylib_ldlibrary))
+        if res.returncode() != 0
+          error_msg = '@0@ doesn\' exist, can\'t use python'.format(join_paths(pylib_loc, pylib_ldlibrary))
+        endif
+      endif
+      if error_msg == ''
+        pylib_suffix = 'so'
+        if host_machine.system() == 'windows'
+          pylib_suffix = 'dll'
+        elif host_machine.system() == 'darwin'
+          pylib_suffix = 'dylib'
+        endif
+
+        gmodule_dep = dependency('gmodule-2.0')
+        libges_deps = libges_deps + [python_dep, gmodule_dep]
+        has_python = true
+        message('python_abi_flags = @0@'.format(python_abi_flags))
+        message('pylib_loc = @0@'.format(pylib_loc))
+        cdata.set('HAS_PYTHON', true)
+        cdata.set('PY_LIB_LOC', '"@0@"'.format(pylib_loc))
+        cdata.set('PY_ABI_FLAGS', '"@0@"'.format(python_abi_flags))
+        cdata.set('PY_LIB_SUFFIX', '"@0@"'.format(pylib_suffix))
+        cdata.set('PYTHON_VERSION', '"@0@"'.format(python_dep.version()))
+      else
+          if get_option('python').enabled()
+            error(error_msg)
+          else
+            message(error_msg)
+          endif
+      endif
+    endif
+  endif
+endif
+
+configure_file(output : 'config.h', configuration : cdata)
+
 ges_c_args = ['-DHAVE_CONFIG_H', '-DG_LOG_DOMAIN="GES"']
 plugins_install_dir = '@0@/gstreamer-1.0'.format(get_option('libdir'))
 
index 57417e0..26534e8 100644 (file)
@@ -8,3 +8,7 @@ option('xptv', type : 'feature', value : 'auto',
        description : 'Build the deprecated xptv formater')
 option('doc', type : 'feature', value : 'auto', yield: true,
        description: 'Enable documentation.')
+option('python', type : 'feature', value : 'auto', yield: true,
+       description: 'Enable python formatters.')
+option('libpython-dir', type : 'string', value : '',
+        description: 'Path to find libpythonXX.so')