Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: DynamicEffects: Add code for Linux with tests #345

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from

Conversation

kirillmakhonin
Copy link

This PR adds possibility to develop and integrate effects for libopenshot as a shared library.
Right now it works only for Linux, on other platform this method does not do anything (returns NULL).

As an example of "plugin" I've copied Blur effect with name SuperBlur.

From user-perspective, this feature is needed to have an ability create granular "plugins" w/o entire libopenshot compilation.

Later, this system needs to be improved to:

  1. Make an ability to contain more then one effect in library
  2. Add version check during plugin loading (to be sure that plugin was developed for correct version of libopenshot)
  3. Add after-loading of plugins if path to them is provided in env. variable

@ferdnyc
Copy link
Contributor

ferdnyc commented Oct 9, 2019

@kirillmakhonin Wow! This looks really interesting, I'm excited to check it out. I love the idea of making libopenshot more modular, period, and breaking out the effects seems like one of the more natural ways to do that. I can even imagine porting all of our built-in effects to build as runtime-loaded modules, perhaps, and doing away with the "built-in vs. external" divide entirely.

But for now, this sounds like a perfect first step. I'll give it a spin and post more feedback once I've had a chance to digest it more fully, and then hopefully a review some time in the next couple of days.

Some thoughts on your improvements list, for starters:

Later, this system needs to be improved to:

  1. Make an ability to contain more then one effect in library

Eh... I mean, maybe? If there are effects that might benefit from sharing code, I can see there being an advantage to that. (But it's not like any of our EXISTING effects share even a single line of code, as things stand today.) Unless there's some practical advantage to building multi-effect libraries, though, my knee-jerk inclination is to keep each one in its own module. Linux's libdl, at least, is pretty efficient about loading multiple objects. But maybe I'll reconsider that or be talked out of it later, currently it's just a snap judgement.

  1. Add version check during plugin loading (to be sure that plugin was developed for correct version of libopenshot)

Yeah, versioning will definitely be important here — potentially of both the source (versioned for API changes) and the objects. (libopenshot should probably require that the objects export some build-ident signature that matches its own copy, so it only loads modules if they were compiled for that exact libopenshot.so. No copying effect modules from a different install or anything, at least not in the beginning.)

  1. Add after-loading of plugins if path to them is provided in env. variable

Best thing might be for us to define a Settings variable that holds the plugin path, that'll provide callers with an already-established API for configuring it. And supporting an envvar for runtime overrides (or additions?) makes sense as well.

Some other possible items to consider:

  1. I think we'll need a way to generate at least Python bindings for these, as well, if they're to be usable within OpenShot itself (which would be ideal).

    I've been toying with the idea of moving the effects into their own openshot::effect namespace, and breaking them out into a separate Python submodule. (Simply because the SWIG-generated C++ source for the Python bindings is a single .cpp file that currently clocks in at 2.3 MEGABYTES, which is just insane. The effects seemed the most obvious and natural place to start breaking that apart a bit.) This might just be one more reason to do that, because if the effects are openshot.effect.Blur / openshot.effect.Crop, etc., then it's easy enough to create SWIG interface files for more modules. We'd have to figure out the logistics of loading those, as well.

    Or, the radical alternative would be to not do that, and instead eliminate the public APIs of the individual effects entirely. Instead we define a full API for accessing effects via libopenshot, so calls are made in a structured way, and the same calls provide access to every currently-loaded effect. ...But that's a lot more effort and a lot more disruption.

  2. And, yeah, we'll probably have to figure out MacOS and Windows as well, to make this ready for prime-time. My understanding is, people still use them, for whatever reason.

Sorry for the brain-dump, I'll probably have more focused feedback once I've tried this out and gotten more of a hands-on feel for it. But thanks again for the contribution, this really seems like it'll be an awesome evolutionary next-step for libopenshot!

Copy link
Contributor

@ferdnyc ferdnyc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a quick drive-by preliminary review, actually. Incomplete, but it's a start...

.gitignore Outdated Show resolved Hide resolved
external-effects/super-blur/CMakeLists.txt Outdated Show resolved Hide resolved
external-effects/super-blur/CMakeLists.txt Outdated Show resolved Hide resolved
external-effects/super-blur/CMakeLists.txt Outdated Show resolved Hide resolved
tests/CMakeLists.txt Outdated Show resolved Hide resolved
external-effects/super-blur/SuperBlur.h Outdated Show resolved Hide resolved
external-effects/super-blur/SuperBlur.h Outdated Show resolved Hide resolved
include/EffectInfo.h Outdated Show resolved Hide resolved
src/EffectInfo.cpp Outdated Show resolved Hide resolved
@kirillmakhonin
Copy link
Author

@ferdnyc, I did some updates, could you please review them?
I did not add support for mac and windows right now, but, compilation and tests should not fail on these platforms.
Also I tried to add some versioning, but I'm sure that my approach is right, maybe OPENSHOT_VERSION_SO should be used.

ELSE()
# Link libraries to the new executable
target_link_libraries(openshot-test openshot ${UNITTEST++_LIBRARY})
ENDIF(NOT DISABLE_EXTERNAL_EFFECTS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple issues here:

  1. DynamicEffects_Tests.cpp will be in the test target sources list even when DISABLE_EXTERNAL_EFFECTS is set. That's okaaaay technically, because the whole file's wrapped in #ifdef TEST_PLUGIN, but if nothing else it's an extra source file being compiled for no reason.

  2. Bigger issue, if you want to test the dlopen() functionality of EffectInfo::LoadEffect, I suspect you can't link the executable with the library you're trying to dlopen() or it will invalidate the test.

    Because effect-superblur is added to the target_link_libraries() of openshot-test, it'll be dynamically linked with the executable:

    $ cd build
    $ make openshot-test
    $ ldd tests/openshot-test |grep -i effect
     libeffect-superblur.so => /home/ferd/rpmbuild/REPOS/libopenshot/build/external-effects/super-blur/libeffect-superblur.so (0x00007f41707c6000)

    The dynamic loader is sure to be smart enough that, when you call dlopen() on that file path, it's not going to load anything at all — it's just going to return a handle to the library that was already loaded at the start of program execution.

    To test dynamic-loading from the library file on disk, you don't want to link the executable to it. It should have no knowledge of that library prior to dlopen() being given the pathname to load it from.

I guess my preference would be something more like this:

Suggested change
ENDIF(NOT DISABLE_EXTERNAL_EFFECTS)
# Link libraries to the new executable
target_link_libraries(openshot-test openshot ${UNITTEST++_LIBRARY})
if (NOT DISABLE_EXTERNAL_EFFECTS)
target_sources(openshot-test PRIVATE DynamicEffects_Tests.cpp)
target_compile_definitions(openshot-test PRIVATE
TEST_PLUGIN="${PROJECT_BINARY_DIR}/external-effects/super-blur/libeffect-superblur.so"
TEST_PLUGIN_NAME="SuperBlur")
endif ()

That'll add in the source file and the definitions iff enabled, and leave the library unlinked so it'll have to be loaded from the path in the TEST_PLUGIN definition.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made compilation of file DynamicEffects_Tests.cpp optional, also I remove static linking with effect library.
@ferdnyc could you, please, have a look?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, you are OLD-school CMake, huh? 😆 Well, that's cool, whatever works — and this does, thanks. One day I'll drag this project kicking and screaming into the Modern CMake world, but that day is not today.

I'm actually a little surprised add_definitions() is still effective even when it comes after the creation of the executable target (as opposed to target_compile_definitions() operating on the target), but it appears that's the case.

I made compilation of file DynamicEffects_Tests.cpp optional,

And the add_dependency() is a good idea, I missed that.

also I remove static linking with effect library.

*dynamic linking. (A bit pedantic, I admit, but the distinction is critical. Static linking compiles the library right into the executable and creates no external dependencies. With a shared library the runtime dependency is still dynamic linking — as opposed to dynamic loading via dlopen(), which is what you're doing instead, and requires no prior linkage between the executable and the shared object file.)

Copy link
Contributor

@ferdnyc ferdnyc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping to finish this now, but have to cut it short and wanted to at least get you what I have already. There's a dangling allusion to "more on that later" that's unresolved, but I'll have to come back to that. (So, more on that even-later.)

Comment on lines +96 to +151
#if defined(__linux__)
EffectBase* EffectInfo::LoadEffect(std::string location){
pthread_mutex_lock(&m_mutex);

void * file = dlopen(location.c_str(), RTLD_NOW);
if (file == nullptr){
pthread_mutex_unlock(&m_mutex);
throw InvalidFile("Can not open file", location.c_str());
}
void * factory_ptr = dlsym(file, "factory");

if (factory_ptr == nullptr){
pthread_mutex_unlock(&m_mutex);
dlclose(file);
throw InvalidFile("Can not find requested plugin API in file", location.c_str());
}

EffectBase * (*factory)(uint16_t);
factory = (EffectBase* (*)(uint16_t)) factory_ptr;

EffectBase* instance = factory(OPENSHOT_PLUGIN_API_VERSION);

if (instance == nullptr){
pthread_mutex_unlock(&m_mutex);
dlclose(file);
throw InvalidFile("Plugin does not support current version of openshot", location.c_str());
}

m_loadedDynamicEffects.insert(make_pair(std::string(instance->info.name), factory));
m_loadedDynamicHandles.insert(m_loadedDynamicHandles.end(), file);

pthread_mutex_unlock(&m_mutex);
return instance;
}

void EffectInfo::UnloadDynamicEffects(){
pthread_mutex_lock(&m_mutex);

for (auto & handle : m_loadedDynamicHandles){
dlclose(handle);
}

m_loadedDynamicEffects.clear();
m_loadedDynamicHandles.clear();
pthread_mutex_unlock(&m_mutex);
}
#else
EffectBase* EffectInfo::LoadEffect(std::string location){
return NULL;
}

void EffectInfo::UnloadDynamicEffects(){

}
#endif

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be cleaner/easier to write as...

EffectBase* EffectInfo::LoadEffect(std::string location){
#if !defined(__linux__)
    return NULL;
#endif
    pthread_mutex_lock(&m_mutex);
    ...
}

void EffectInfo::UnloadDynamicEffects(){
#if defined(__linux__)
    pthread_mutex_lock(&m_mutex);

    for (auto & handle : m_loadedDynamicHandles){
        dlclose(handle);
    }

    m_loadedDynamicEffects.clear();
    m_loadedDynamicHandles.clear();
    pthread_mutex_unlock(&m_mutex);
#endif
}

Primary advantage: If the signature of EffectInfo::LoadEffect() or EffectInfo::UnloadDynamicEffects() changes, we don't have to worry about updating it in two places and making sure they're kept in sync.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Sorry, accidentally had an earlier version of this comment included in the review, along with the final one. I've deleted the redundant comment, but the email notification version may have been a bit confusing.)

@@ -31,6 +31,8 @@
#ifndef OPENSHOT_EFFECT_BASE_H
#define OPENSHOT_EFFECT_BASE_H

#define OPENSHOT_PLUGIN_API_VERSION 0x0001
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... as you mentioned in the main PR comments, I'd just use the SO version instead. (It's #defined as OPENSHOT_VERSION_SO in OpenShotVersion.h.)

The main reason being, we really don't have an effects API currently (and that's on us). The abstractions for effects use aren't really great. Callers generally need to have knowledge of the API for the specific effects they use, and to an extent that will also include the dynamic ones. (The JSON interfaces do somewhat alleviate that, but more on that later.)

As a result, the plugin compatibility will most likely be tied to the library API even if the plugin-loading API stays the same, and a plugin API versioning scheme just adds another numbering to keep track of.

But if this IS retained, it should be placed in include/OpenShotVersion.h.in instead of here. (Which is the input for a CMake-generated header file that you can include via #include "OpenShotVersion.h", to access any of the versioning macros.)

Comment on lines +34 to +35
#ifdef TEST_PLUGIN
#if defined(__linux__)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest combining these as

#if defined(__linux__) && defined(TEST_PLUGIN)

But since in theory we might have plugin-loading implementations for other OSes, leaving them separate may be better. The only thing I'd request is commenting the #endifs as:

#endif // defined(__linux__)
#endif // TEST_PLUGIN

since they're way too far away to match up visually.

}

#endif
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: File doesn't end in a newline.

@ferdnyc
Copy link
Contributor

ferdnyc commented Oct 15, 2019

@kirillmakhonin

Surprisingly enough, it may be possible to use the same code even on Windows:

https://github.com/dlfcn-win32/dlfcn-win32

(dlopen() already works natively on MacOS, the only wrinkle is that shared libraries are compiled with the .dylib extension, instead of .so.)

@jonoomph
Copy link
Member

@kirillmakhonin This looks very promising! Thanks again for your help implementing it. I'm feeling like we should keep iterating on this idea (in this PR), and before we merge, I would suggest we get this working on all 3 OSes. If I need to install additional things on our Windows build server, please let me know. We can also move this into an official branch, so our build servers can play with it and create test builds.

Also, most effect metadata comes from openshot.EffectInfo.Json(), where it is returned as JSON for the OpenShot UI. I see no reason why this method couldn't include metadata from loaded effects as well as built-in effects.

Another thought... how would we handle translations for externally loaded effects. I do love the idea of dropping an effect library into a folder, and launching OpenShot, and when detected, those effect libraries would be loaded, translations loaded, icons loaded, etc... and would be completely seamless to the user. 👍

@ferdnyc
Copy link
Contributor

ferdnyc commented Oct 30, 2019

@kirillmakhonin This looks very promising! Thanks again for your help implementing it. I'm feeling like we should keep iterating on this idea (in this PR)

Agreed. It's really promising, so it’s worth taking the time to make it a first-class feature that works well for all platforms AND for all consumers. (@jeffski and/or @DylanC may have some thoughts on this, perhaps?)

Also, most effect metadata comes from openshot.EffectInfo.Json(), where it is returned as JSON for the OpenShot UI. I see no reason why this method couldn't include metadata from loaded effects as well as built-in effects.

*nod* And that's a help, but still feels incomplete to me. The JSON interface to properties works for OpenShot, but it's less convenient for direct libopenshot consumers.

One of the things I was going to suggest as far as this PR goes, for the unit testing, was to reimplement the Blur tests from Timeline_Tests.cpp in SuperBlur:

TEST(Timeline_Effect_Blur)
{
// Create a timeline
Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO);
stringstream path_top;
path_top << TEST_MEDIA_PATH << "interlaced.png";
Clip clip_top(path_top.str());
clip_top.Layer(2);
t.AddClip(&clip_top);
// Add some effects out of order
Keyframe horizontal_radius(5.0);
Keyframe vertical_radius(5.0);
Keyframe sigma(3.0);
Keyframe iterations(3.0);
Blur blur(horizontal_radius, vertical_radius, sigma, iterations);
blur.Id("B");
blur.Layer(2);
t.AddEffect(&blur);
// Open Timeline
t.Open();
// Get frame
std::shared_ptr<Frame> f = t.GetFrame(1);
// Close reader
t.Close();
}

If you try to actually do that, you immediately run into the snag that, because dynamic effects use the factory pattern to instantiate the effect objects, you lose access to the effect's custom constructors. This isn't how you'd create a SuperBlur object:

Keyframe horizontal_radius(5.0);
Keyframe vertical_radius(5.0);
Keyframe sigma(3.0);
Keyframe iterations(3.0);
Blur blur(horizontal_radius, vertical_radius, sigma, iterations);

Now, sure, you could use JSON to serialize and apply the appropriate properties, and that works fine for OpenShot, but for libopenshot consumers I think that probably would feel overly complicated and tedious.

What might be better is if we come up with a standard setter/getter interface for effect parameters, and we implement that interface in the EffectBase class. It'd be nice to provide an interface for C++ callers that doesn't hardcode the parameters themselves the way the custom constructors do. Then, all of the dynamic effects (or even all of the effects, since there's no reason the static effects can't benefit from this as well) can just implement those setters/getters for their properties, instead of being configured through constructors that require callers to have the specific effect's individual unique interface fully defined at compile time.

That'd also make the bindings for effects far less complex. To the point where it might actually be possible to completely drop the SWIG-generated interface for each effect in favor of EffectBase setters/getters being the supported interface for direct callers, even with the built-in static effects. (OpenShot already doesn't use the openshot.effect.{Foo,Bar,Baz} Python classes for anything; it works exclusively through the JSON interface.)

Another thought... how would we handle translations for externally loaded effects. I do love the idea of dropping an effect library into a folder, and launching OpenShot, and when detected, those effect libraries would be loaded, translations loaded, icons loaded, etc... and would be completely seamless to the user.

Mmm, I hadn't even thought about translations. ...How do we handle translations for built-in effects? We don't, in libopenshot. It's not translated at all. Because of the JSON interface, all parameters already have string names, and those names are used by OpenShot to assemble the translations.

OpenShot queries the list of effects in the Effects model using openshot.EffectInfo.Json(), then applies translations while it's populating the model. To get the strings translated in the first place, src/language/generate_translations.py actually does essentially the same thing, loading each effect's properties and metadata and adding the strings to the translation master:

# Append on properties from libopenshot
objects = [openshot.Clip(), openshot.Bars(), openshot.Blur(), openshot.Brightness(),
           openshot.ChromaKey(), openshot.ColorShift(), openshot.Crop(), openshot.Deinterlace(), openshot.Hue(), openshot.Mask(),
           openshot.Negate(), openshot.Pixelate(), openshot.Saturation(), openshot.Shift(), openshot.Wave()]

# Loop through each libopenshot object
for object in objects:
    props = json.loads(object.PropertiesJSON(1))

    # Loop through props
    for key in props.keys():
        object = props[key]
        if "name" in object.keys():
            effects_text[object["name"]] = "libopenshot (Clip Properties)"
        if "choices" in object.keys():
            for choice in object["choices"]:
                effects_text[choice["name"]] = "libopenshot (Clip Properties)"

# Append Effect Meta Data
e = openshot.EffectInfo()
props = json.loads(e.Json())

# Loop through props
for effect in props:
    if "name" in effect:
        effects_text[effect["name"]] = "libopenshot (Effect Metadata)"
    if "description" in effect:
        effects_text[effect["description"]] = "libopenshot (Effect Metadata)"

In one sense you could say that this means nothing's changed, so there's no need to worry about it... in libopenshot. But in another sense, it really complicates things. Translations being managed completely external to libopenshot means that this CAN'T be solved entirely from within libopenshot. There's no code that can be added to the dynamic translations and exposed through the effects interface that can solve this, the way things are designed today. OpenShot has to have prior knowledge of every effect loaded.

Which does mean that, if we were to have "built-in" dynamic effects (ones bundled with libopenshot, that OpenShot therefore does have prior knowledge of), translations should still work the same as always. This PR already updates src/EffectInfo.cpp so that openshot.EffectInfo.JSON() will include the dynamic effects in its listing. As long as they're loaded when generate_translations.py makes its query, the strings will be included in the translation master.

For "third-party" effects, things become significantly trickier. I have no doubt that Qt's QTranslator system is modular enough to handle this situation, but we have to figure out the best way to make use of it.

If we're lucky, most of the names of parameters and things should be the same across all effects, so we may be able to reuse the strings already defined. To facilitate that, we may want to define a common translation context for all of the property names, possibly associated with either EffectBase or EffectInfo. (Since we're using gettext rather than Qt's lupdate, as the tool that collects and manages translation data, we'll have to figure out how — or if — that would work.)

Even still there are still at least some text strings — the name of the effect itself, its description, etc. — that won't fall into that shared-reuse category. Those would need to be translated individually for each effect. (Users do tend to be somewhat more accepting of strings like that being un-translated, though, if it comes down to it. As long as the actual user interface can be understood.)

There's a possibility we could just look for a .qm file (Qt compiled translation dictionary) alongside each effect plugin, and load that file into the translation system if it's present, without really concerning ourselves with exactly how the contents are created in the first place.

Or, we could have each plugin incorporate its translation data as a binary blob, and use the QTranslator::load(const uchar *data, int len, const QString &directory) signature to load that data. That makes things significantly more complicated from the POV of effect plugin creation, but has the advantage that everything remains self-contained in a single shared library file. (The effect's icon could even be handled the same way.)

Or... something else? ¯\_(ツ)_/¯

@ferdnyc
Copy link
Contributor

ferdnyc commented Oct 30, 2019

(I resolved the merge conflicts in tests/CMakeLists.txt, BTW. That's what that commit was earlier.)

@ferdnyc
Copy link
Contributor

ferdnyc commented Oct 30, 2019

If we're lucky, most of the names of parameters and things should be the same across all effects, so we may be able to reuse the strings already defined. To facilitate that, we may want to define a common translation context for all of the property names, possibly associated with either EffectBase or EffectInfo. (Since we're using gettext rather than Qt's lupdate, as the tool that collects and manages translation data, we'll have to figure out how — or if — that would work.)

Actually, scratch that. Because of the way they're collected now, all properties already have a common translation context. From src/language/OpenShot/OpenShot.pot:

#: /home/jonathan/apps/openshot-qt-git/src/windows/views/timeline_webview.py:2408
#: libopenshot (Clip Properties)
msgid "Brightness"
msgstr ""

#: /home/jonathan/apps/openshot-qt-git/src/windows/views/timeline_webview.py:2410
#: libopenshot (Clip Properties)
msgid "Contrast"
msgstr ""

So, dynamically-loaded effects would automatically be handled using that same context, and all of the translated strings from existing properties would be available for use with the dynamic effects' properties as well.

@ferdnyc
Copy link
Contributor

ferdnyc commented Oct 30, 2019

Actually, an even better example is:

#: libopenshot (Effect Metadata)
msgid "Brightness & Contrast"
msgstr ""

#: libopenshot (Clip Properties)
msgid "Sigma"
msgstr ""

#: libopenshot (Clip Properties)
msgid "Duration"
msgstr ""

#: libopenshot (Effect Metadata)
msgid "Color Saturation"
msgstr ""

No source-code contexts at all. (But it seems like strings will be reused for other contexts even if they are defined with a source context. For instance, the "Brightness" in my previous comment is the only instance of that string in the .pot file, even though at least some effects' metadata will also use the string "Brightness".)

@ferdnyc
Copy link
Contributor

ferdnyc commented Oct 30, 2019

Note to self, regarding translations generally: https://pypi.org/project/distutils_ui/

(Helpers that can be used in a distutils-built and -installed Python project to manage translations as .ts files created by PyQt5's pylupdate, instead of using gettext.)

@jonoomph
Copy link
Member

jonoomph commented Nov 1, 2019

@ferdnyc I love the idea of bundling a .qm file alongside an effect library. Although, it would also be great if that could be included in the library (not sure how though), like a resource file compiled into the library... hmmm.

Like you said, the shared context of OpenShot would still be used to translate strings (if the words have already been translated), but maybe an optional *.qm file would be loaded (if found), and that could be used to fill in the remaining translation gaps in an external effect.

If we really implement this correctly, this could end up being a huge deal for OpenShot... and other open-source projects which use libopenshot. The ability to quickly and easily create an effect seems sooo important. We could also make an Effects Download website, which could incorporate our build servers (for cross-platform building), and simplify the installation... although this is an entirely out-of-scope idea right now, and we should probably not get sidetracked thinking about that. 😜

@jonoomph jonoomph changed the title DynamicEffects: Add code for Linux with tests WIP: DynamicEffects: Add code for Linux with tests Nov 17, 2019
@musteresel
Copy link
Contributor

I've seen this uses dlopen(); regarding portability there's libltdl which is basically a portable dlopen library. Might be overkill, though. And I'm not sure about whether the licence would be an issue..

@PhysSong
Copy link
Contributor

Since libopenshot already uses Qt for image processing, it's also possible to use QLibrary and QMutex which come from Qt.

@github-actions
Copy link

Merge conflicts have been detected on this PR, please resolve.

@github-actions github-actions bot added the conflicts A PR with unresolved merge conflicts label Jan 28, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
conflicts A PR with unresolved merge conflicts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants