refactor(plugin): de-Qt oakplugin and wrap it in a pure C ABI

- de-Qt src/plugin/src (olivehost/oliveplugininstance/oliveclip/
  paraminstance/image/pluginprogressreporter); QMessageBox/
  QApplication replaced by facade callbacks
- new C ABI in include/plugin/{error,host,instance}.h with
  refcounted OakPluginInstance value handle
- paraminstance bridges via oaknode C ABI (OakNodeNode value handle,
  oaknode_node_identity registry); undo via oakundo C ABI
- oliveclip textures are OakRenderTexture value handles; oakrender
  gains texture/copier/ticket C API additions
- oaknode gains get_input_at_time/set_input_at_time_undoable/
  node_identity/sequence_from_node/sequence_set_default_parameters/
  find_input_footage
- move avframeptr.h to src/common/src (shared by codec and render)
- node transition pluginSupport stubs bridge the real oakplugin
  headers; all oaknode-building standalone trees add src/plugin and
  link oakplugin into their test binaries
- plugin tests self-sufficient (ipc shim, OfxHost force_load,
  PRE_TEST discovery, OCIO env); timeline standalone gains
  render/c_api
- restore ffmpegdecoder.cpp in src/codec/src/ffmpeg/CMakeLists.txt
  (dropped in 3d004c081, caused jump-to-0 in decoder/encoder tests)

All six standalone trees green: codec 22, audio 40, task 110,
render 49, timeline 121, plugin 100.
This commit is contained in:
2026-08-07 22:18:36 +08:00
parent 0462842f8c
commit fd2111d560
54 changed files with 6014 additions and 324 deletions
+6
View File
@@ -0,0 +1,6 @@
add_subdirectory(src)
add_subdirectory(c_api)
if(BUILD_TESTS)
add_subdirectory(tests)
endif()
+4
View File
@@ -0,0 +1,4 @@
target_sources(oakplugin PRIVATE
host.cpp
instance.cpp
)
+155
View File
@@ -0,0 +1,155 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
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 <http://www.gnu.org/licenses/>.
***/
#include "plugin/host.h"
#include <cstring>
#include <string>
#include <vector>
#include "../src/olivehost.h"
namespace
{
olive::plugin::HostMessageHandler forwarder_fn;
oakplugin_message_fn user_fn;
void *user_userdata;
int copy_string(const std::string &value, char *buf, int buf_size)
{
int needed = int(value.size()) + 1;
if (buf && buf_size >= needed) {
memcpy(buf, value.c_str(), needed);
}
return needed;
}
std::vector<std::string> plugin_ids()
{
std::vector<std::string> ids;
OFX::Host::PluginCache *cache =
OFX::Host::PluginCache::getPluginCache();
if (!cache) {
return ids;
}
for (const auto &plugin : cache->getPlugins()) {
if (plugin) {
ids.push_back(plugin->getIdentifier());
}
}
return ids;
}
} // namespace
int oakplugin_host_init(void)
{
try {
olive::plugin::load_plugins();
return OAKPLUGIN_OK;
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
void oakplugin_host_shutdown(void)
{
// The OFX plugin cache is process-global; nothing to tear down here
// (the Current slots release their references on their own).
}
int oakplugin_host_scan(const char *const *bundle_dirs, int dir_count)
{
if (!bundle_dirs || dir_count < 0) {
return OAKPLUGIN_E_INVALID;
}
try {
for (int i = 0; i < dir_count; i++) {
if (bundle_dirs[i]) {
olive::plugin::load_plugins(bundle_dirs[i]);
}
}
return OAKPLUGIN_OK;
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
int oakplugin_host_plugin_count(void)
{
try {
return int(plugin_ids().size());
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
int oakplugin_host_plugin_id_at(int index, char *buf, int buf_size)
{
try {
std::vector<std::string> ids = plugin_ids();
if (index < 0 || index >= int(ids.size())) {
return OAKPLUGIN_E_NOT_FOUND;
}
return copy_string(ids[size_t(index)], buf, buf_size);
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
int oakplugin_host_plugin_label(const char *plugin_id, char *buf,
int buf_size)
{
if (!plugin_id) {
return OAKPLUGIN_E_INVALID;
}
try {
for (const std::string &id : plugin_ids()) {
if (id == plugin_id) {
return copy_string(id, buf, buf_size);
}
}
return OAKPLUGIN_E_NOT_FOUND;
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
void oakplugin_host_set_message_handler(oakplugin_message_fn fn,
void *userdata)
{
user_fn = fn;
user_userdata = userdata;
if (!fn) {
olive::plugin::set_host_message_handler(nullptr);
return;
}
olive::plugin::set_host_message_handler(
[](const char *type, const std::string &message) -> OfxStatus {
int answer = user_fn(type, message.c_str(), user_userdata);
return answer == OAKPLUGIN_MESSAGE_ANSWER_YES ? kOfxStatReplyYes
: kOfxStatReplyNo;
});
}
+399
View File
@@ -0,0 +1,399 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
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 <http://www.gnu.org/licenses/>.
***/
#include "plugin/instance.h"
#include <atomic>
#include <new>
#include <ofxhImageEffectAPI.h>
#include <ofxhPluginCache.h>
#include "../src/oliveclip.h"
#include "../src/oliveplugininstance.h"
namespace
{
struct InstanceBox {
olive::plugin::OlivePluginInstance *instance;
std::atomic<uint32_t> refs;
oakplugin_progress_fn progress_fn;
void *progress_userdata;
InstanceBox(olive::plugin::OlivePluginInstance *i)
: instance(i)
, refs(1)
, progress_fn(nullptr)
, progress_userdata(nullptr)
{
}
};
std::atomic<int> g_alive{ 0 };
void box_addref(void *ctx)
{
if (ctx) {
static_cast<InstanceBox *>(ctx)->refs.fetch_add(1);
}
}
void box_release(void *ctx)
{
if (!ctx) {
return;
}
InstanceBox *box = static_cast<InstanceBox *>(ctx);
if (box->refs.fetch_sub(1) == 1) {
delete box->instance;
delete box;
g_alive--;
}
}
OakPluginInstance make_handle(olive::plugin::OlivePluginInstance *instance)
{
OakPluginInstance handle = {};
if (!instance) {
return handle;
}
InstanceBox *box = new (std::nothrow) InstanceBox(instance);
if (!box) {
delete instance;
return handle;
}
handle.ctx = box;
handle.addref = box_addref;
handle.release = box_release;
handle.abi_version = OAKPLUGIN_ABI_VERSION;
g_alive++;
return handle;
}
InstanceBox *impl(OakPluginInstance h)
{
if (!h.ctx) {
return nullptr;
}
return static_cast<InstanceBox *>(h.ctx);
}
} // namespace
OakPluginInstance oakplugin_instance_create(const char *plugin_id)
{
if (!plugin_id) {
return OakPluginInstance{};
}
try {
OFX::Host::PluginCache *cache =
OFX::Host::PluginCache::getPluginCache();
if (!cache) {
return OakPluginInstance{};
}
for (const auto &plugin : cache->getPlugins()) {
if (plugin && plugin->getIdentifier() == plugin_id) {
auto *effect_plugin =
dynamic_cast<OFX::Host::ImageEffect::ImageEffectPlugin *>(
plugin);
if (!effect_plugin) {
return OakPluginInstance{};
}
auto *instance = effect_plugin->createInstance(
kOfxImageEffectContextFilter, nullptr);
auto *olive_instance =
dynamic_cast<olive::plugin::OlivePluginInstance *>(
instance);
if (!olive_instance) {
delete instance;
return OakPluginInstance{};
}
return make_handle(olive_instance);
}
}
return OakPluginInstance{};
} catch (...) {
return OakPluginInstance{};
}
}
void oakplugin_instance_free(OakPluginInstance *instance)
{
if (!instance || !instance->ctx) {
return;
}
instance->release(instance->ctx);
instance->ctx = NULL;
}
int oakplugin_instance_set_param(OakPluginInstance instance,
const char *param_id,
const oaknode_value *value)
{
InstanceBox *box = impl(instance);
if (!box || !param_id || !value) {
return OAKPLUGIN_E_INVALID;
}
try {
const std::map<std::string, OFX::Host::Param::Instance *> &params_map =
box->instance->getParams();
auto it = params_map.find(param_id);
if (it == params_map.end() || !it->second) {
return OAKPLUGIN_E_NOT_FOUND;
}
OFX::Host::Param::Instance *param = it->second;
switch (value->type) {
case OAKNODE_VALUE_INT:
case OAKNODE_VALUE_COMBO:
return static_cast<OFX::Host::Param::IntegerInstance *>(param)
->set(int(value->num)) == kOfxStatOK
? OAKPLUGIN_OK
: OAKPLUGIN_E_FAILED;
case OAKNODE_VALUE_BOOL:
return static_cast<OFX::Host::Param::BooleanInstance *>(param)
->set(value->num != 0) == kOfxStatOK
? OAKPLUGIN_OK
: OAKPLUGIN_E_FAILED;
case OAKNODE_VALUE_FLOAT:
return static_cast<OFX::Host::Param::DoubleInstance *>(param)
->set(value->f[0]) == kOfxStatOK
? OAKPLUGIN_OK
: OAKPLUGIN_E_FAILED;
case OAKNODE_VALUE_VEC2:
return static_cast<OFX::Host::Param::Double2DInstance *>(param)
->set(value->f[0], value->f[1]) == kOfxStatOK
? OAKPLUGIN_OK
: OAKPLUGIN_E_FAILED;
case OAKNODE_VALUE_VEC3:
return static_cast<OFX::Host::Param::Double3DInstance *>(param)
->set(value->f[0], value->f[1],
value->f[2]) == kOfxStatOK
? OAKPLUGIN_OK
: OAKPLUGIN_E_FAILED;
case OAKNODE_VALUE_COLOR:
case OAKNODE_VALUE_VEC4:
return static_cast<OFX::Host::Param::RGBAInstance *>(param)
->set(value->f[0], value->f[1], value->f[2],
value->f[3]) == kOfxStatOK
? OAKPLUGIN_OK
: OAKPLUGIN_E_FAILED;
default:
return OAKPLUGIN_E_INVALID;
}
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
int oakplugin_instance_get_param(OakPluginInstance instance,
const char *param_id, oaknode_value *out)
{
InstanceBox *box = impl(instance);
if (!box || !param_id || !out) {
return OAKPLUGIN_E_INVALID;
}
try {
const std::map<std::string, OFX::Host::Param::Instance *> &params_map =
box->instance->getParams();
auto it = params_map.find(param_id);
if (it == params_map.end() || !it->second) {
return OAKPLUGIN_E_NOT_FOUND;
}
OFX::Host::Param::Instance *param = it->second;
out->type = OAKNODE_VALUE_NONE;
const std::string &type = param->getType();
if (type == kOfxParamTypeInteger || type == kOfxParamTypeChoice) {
int v = 0;
static_cast<OFX::Host::Param::IntegerInstance *>(param)->get(v);
out->type = OAKNODE_VALUE_INT;
out->num = v;
} else if (type == kOfxParamTypeBoolean) {
bool v = false;
static_cast<OFX::Host::Param::BooleanInstance *>(param)->get(v);
out->type = OAKNODE_VALUE_BOOL;
out->num = v ? 1 : 0;
} else if (type == kOfxParamTypeDouble) {
double v = 0;
static_cast<OFX::Host::Param::DoubleInstance *>(param)->get(v);
out->type = OAKNODE_VALUE_FLOAT;
out->f[0] = v;
} else if (type == kOfxParamTypeDouble2D) {
double x = 0, y = 0;
static_cast<OFX::Host::Param::Double2DInstance *>(param)->get(x,
y);
out->type = OAKNODE_VALUE_VEC2;
out->f[0] = x;
out->f[1] = y;
} else if (type == kOfxParamTypeDouble3D) {
double x = 0, y = 0, z = 0;
static_cast<OFX::Host::Param::Double3DInstance *>(param)->get(x,
y,
z);
out->type = OAKNODE_VALUE_VEC3;
out->f[0] = x;
out->f[1] = y;
out->f[2] = z;
} else if (type == kOfxParamTypeRGBA) {
double r = 0, g = 0, b = 0, a = 0;
static_cast<OFX::Host::Param::RGBAInstance *>(param)->get(r, g,
b, a);
out->type = OAKNODE_VALUE_COLOR;
out->f[0] = r;
out->f[1] = g;
out->f[2] = b;
out->f[3] = a;
} else {
return OAKPLUGIN_E_INVALID;
}
return OAKPLUGIN_OK;
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
int oakplugin_instance_set_param_string(OakPluginInstance instance,
const char *param_id,
const char *value)
{
InstanceBox *box = impl(instance);
if (!box || !param_id) {
return OAKPLUGIN_E_INVALID;
}
try {
const std::map<std::string, OFX::Host::Param::Instance *> &params_map =
box->instance->getParams();
auto it = params_map.find(param_id);
if (it == params_map.end() || !it->second) {
return OAKPLUGIN_E_NOT_FOUND;
}
OFX::Host::Param::Instance *param = it->second;
return static_cast<OFX::Host::Param::StringInstance *>(param)
->set(value ? value : "") == kOfxStatOK
? OAKPLUGIN_OK
: OAKPLUGIN_E_FAILED;
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
int oakplugin_instance_get_param_string(OakPluginInstance instance,
const char *param_id, char *buf,
int buf_size)
{
InstanceBox *box = impl(instance);
if (!box || !param_id) {
return OAKPLUGIN_E_INVALID;
}
try {
const std::map<std::string, OFX::Host::Param::Instance *> &params_map =
box->instance->getParams();
auto it = params_map.find(param_id);
if (it == params_map.end() || !it->second) {
return OAKPLUGIN_E_NOT_FOUND;
}
OFX::Host::Param::Instance *param = it->second;
std::string value;
static_cast<OFX::Host::Param::StringInstance *>(param)->get(value);
int needed = int(value.size()) + 1;
if (buf && buf_size >= needed) {
memcpy(buf, value.c_str(), needed);
}
return needed;
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
int oakplugin_instance_render(OakPluginInstance instance,
OakRenderTexture dst, OakRenderTexture src,
double time_seconds)
{
InstanceBox *box = impl(instance);
if (!box) {
return OAKPLUGIN_E_INVALID;
}
try {
const char *clip_names[] = { "Output", "Source" };
for (const char *clip_name : clip_names) {
auto *clip =
dynamic_cast<olive::plugin::OliveClipInstance *>(
box->instance->getClip(clip_name));
if (!clip) {
continue;
}
if (clip->isOutput()) {
clip->setOutputTexture(dst, time_seconds);
} else if (src.ctx) {
clip->setInputTexture(src, time_seconds, false);
}
}
OfxRectI roi = { 0, 0, 0, 0 };
OfxPointD scale = { 1.0, 1.0 };
OfxStatus status = box->instance->renderAction(
time_seconds, kOfxImageFieldNone, roi, scale, false, false,
false);
return status == kOfxStatOK ? OAKPLUGIN_OK : OAKPLUGIN_E_FAILED;
} catch (...) {
return OAKPLUGIN_E_FAILED;
}
}
int oakplugin_instance_set_progress_cb(OakPluginInstance instance,
oakplugin_progress_fn fn,
void *userdata)
{
InstanceBox *box = impl(instance);
if (!box) {
return OAKPLUGIN_E_INVALID;
}
box->progress_fn = fn;
box->progress_userdata = userdata;
return OAKPLUGIN_OK;
}
int oakplugin_instance_cancel(OakPluginInstance instance)
{
InstanceBox *box = impl(instance);
if (!box) {
return OAKPLUGIN_E_INVALID;
}
if (box->progress_fn) {
box->progress_fn(1.0, box->progress_userdata);
}
return OAKPLUGIN_OK;
}
int oakplugin_debug_alive_count(void)
{
return g_alive.load();
}
+51
View File
@@ -0,0 +1,51 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
# Modifications Copyright (C) 2025 mikesolar
#
# 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 <http://www.gnu.org/licenses/>.
file(GLOB OAKPLUGIN_SOURCES CONFIGURE_DEPENDS *.cpp)
add_library(oakplugin SHARED ${OAKPLUGIN_SOURCES})
target_include_directories(oakplugin PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${OAK_REPO_ROOT}/include
${OAK_REPO_ROOT}/src/common/src
${OAK_REPO_ROOT}/src/undo/src
${OAK_REPO_ROOT}/core/include
${OAK_REPO_ROOT}/third_party/openfx/include
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
${OAK_REPO_ROOT}/ffmpeg_bridge/include
)
find_library(OAKPLUGIN_OFX_HOST_ARCHIVE NAMES OfxHost
PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport
NO_DEFAULT_PATH)
if(NOT OAKPLUGIN_OFX_HOST_ARCHIVE)
message(FATAL_ERROR
"libOfxHost.a not found; run the full-tree build once or set "
"OAKPLUGIN_OFX_HOST_ARCHIVE")
endif()
target_link_options(oakplugin PUBLIC
"-Wl,-force_load,${OAKPLUGIN_OFX_HOST_ARCHIVE}")
target_link_libraries(oakplugin PUBLIC
oaknode
oakrender
oakcommon
oakundo
olivecore
ffmpeg_bridge
)
+235
View File
@@ -0,0 +1,235 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2026 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "image.h"
#include "ofxImageEffect.h"
#include <algorithm>
namespace olive
{
namespace plugin
{
static const char *pixel_depth_to_ofx(core::PixelFormat format)
{
switch (format) {
case core::PixelFormat::u8:
return kOfxBitDepthByte;
case core::PixelFormat::u16:
return kOfxBitDepthShort;
case core::PixelFormat::f16:
return kOfxBitDepthHalf;
default:
break;
case core::PixelFormat::f32:
return kOfxBitDepthFloat;
case core::PixelFormat::u10:
case core::PixelFormat::invalid:
case core::PixelFormat::count:
break;
}
return kOfxBitDepthNone;
}
static const char *components_to_ofx(int channel_count)
{
switch (channel_count) {
case 1:
return kOfxImageComponentAlpha;
case 3:
return kOfxImageComponentRGB;
case 4:
return kOfxImageComponentRGBA;
default:
break;
}
return kOfxImageComponentNone;
}
Image::Image(OFX::Host::ImageEffect::ClipInstance &clip_instance)
: OFX::Host::ImageEffect::Image(clip_instance)
, width_(0)
, height_(0)
, format_(core::PixelFormat::invalid)
, premultiplied_alpha_(false)
, channel_count_(0)
, row_bytes_(0)
, bounds_{ 0, 0, 0, 0 }
, rod_{ 0, 0, 0, 0 }
{
}
Image::Image(OFX::Host::ImageEffect::ClipInstance &clip_instance,
const VideoParams &params, const OfxRectI &bounds,
const OfxRectI &rod, bool clear)
: OFX::Host::ImageEffect::Image(clip_instance)
, width_(0)
, height_(0)
, format_(core::PixelFormat::invalid)
, premultiplied_alpha_(false)
, channel_count_(0)
, row_bytes_(0)
, bounds_{ 0, 0, 0, 0 }
, rod_{ 0, 0, 0, 0 }
{
allocate_from_params(params, bounds, rod, clear);
}
Image::~Image()
{
}
void Image::allocate_from_params(const VideoParams &params,
const OfxRectI &bounds, const OfxRectI &rod,
bool clear)
{
allocate(bounds.x2 - bounds.x1, bounds.y2 - bounds.y1, params.format(),
params.channel_count(), params.premultiplied_alpha(), bounds, rod,
clear);
}
void Image::ensure_allocated_from_params(const VideoParams &params,
const OfxRectI &bounds,
const OfxRectI &rod, bool clear)
{
bool same = (width_ == bounds.x2 - bounds.x1) &&
(height_ == bounds.y2 - bounds.y1) &&
(format_ == params.format()) &&
(channel_count_ == params.channel_count()) &&
(premultiplied_alpha_ == params.premultiplied_alpha()) &&
(bounds_.x1 == bounds.x1) && (bounds_.y1 == bounds.y1) &&
(bounds_.x2 == bounds.x2) && (bounds_.y2 == bounds.y2) &&
(rod_.x1 == rod.x1) && (rod_.y1 == rod.y1) &&
(rod_.x2 == rod.x2) && (rod_.y2 == rod.y2);
if (!same) {
allocate_from_params(params, bounds, rod, clear);
} else if (clear && !image_.empty()) {
std::fill(image_.begin(), image_.end(), 0);
}
}
void Image::allocate(int width, int height, core::PixelFormat format,
int channel_count, bool premultiplied_alpha,
const OfxRectI &bounds, const OfxRectI &rod, bool clear)
{
width_ = width;
height_ = height;
format_ = format;
channel_count_ = channel_count;
premultiplied_alpha_ = premultiplied_alpha;
bounds_ = bounds;
rod_ = rod;
int bytes_per_component = format_.byte_count();
row_bytes_ = width_ * channel_count_ * bytes_per_component;
int buffer_size = row_bytes_ * height_;
if (buffer_size < 0) {
buffer_size = 0;
}
image_.resize(static_cast<size_t>(buffer_size));
if (clear && !image_.empty()) {
std::fill(image_.begin(), image_.end(), 0);
}
setPointerProperty(kOfxImagePropData, image_.data());
setIntProperty(kOfxImagePropRowBytes, row_bytes_);
setIntProperty(kOfxImagePropBounds, bounds.x1, 0);
setIntProperty(kOfxImagePropBounds, bounds.y1, 1);
setIntProperty(kOfxImagePropBounds, bounds.x2, 2);
setIntProperty(kOfxImagePropBounds, bounds.y2, 3);
setIntProperty(kOfxImagePropRegionOfDefinition, rod.x1, 0);
setIntProperty(kOfxImagePropRegionOfDefinition, rod.y1, 1);
setIntProperty(kOfxImagePropRegionOfDefinition, rod.x2, 2);
setIntProperty(kOfxImagePropRegionOfDefinition, rod.y2, 3);
setStringProperty(kOfxImageEffectPropComponents,
components_to_ofx(channel_count_));
setStringProperty(kOfxImageEffectPropPixelDepth, pixel_depth_to_ofx(format_));
setStringProperty(kOfxImageEffectPropPreMultiplication,
premultiplied_alpha_ ? kOfxImagePreMultiplied :
kOfxImageUnPreMultiplied);
}
core::PixelFormat Image::pixel_format()
{
if (format_ != core::PixelFormat::invalid) {
return format_;
}
std::string type = getStringProperty(kOfxImageEffectPropPixelDepth);
if (type == kOfxBitDepthByte) {
format_ = core::PixelFormat::u8;
} else if (type == kOfxBitDepthShort) {
format_ = core::PixelFormat::u16;
} else if (type == kOfxBitDepthHalf) {
format_ = core::PixelFormat::f16;
} else if (type == kOfxBitDepthFloat) {
format_ = core::PixelFormat::f32;
} else {
format_ = core::PixelFormat::invalid;
}
return format_;
}
bool Image::premultiplied_alpha()
{
std::string premultiplied =
getStringProperty(kOfxImageEffectPropPreMultiplication);
premultiplied_alpha_ = (premultiplied == kOfxImagePreMultiplied);
return premultiplied_alpha_;
}
int Image::width()
{
int bounds[4] = { 0 };
getIntPropertyN(kOfxImagePropBounds, bounds, 4);
width_ = bounds[2] - bounds[0];
return width_;
}
int Image::height()
{
int bounds[4] = { 0 };
getIntPropertyN(kOfxImagePropBounds, bounds, 4);
height_ = bounds[3] - bounds[1];
return height_;
}
int Image::channel_count()
{
std::string type = getStringProperty(kOfxImageEffectPropComponents);
if (type == kOfxImageComponentAlpha) {
channel_count_ = 1;
} else if (type == kOfxImageComponentRGBA) {
channel_count_ = 4;
} else if (type == kOfxImageComponentRGB) {
channel_count_ = 3;
} else {
channel_count_ = 0;
}
return channel_count_;
}
} // namespace plugin
} // namespace olive
+81
View File
@@ -0,0 +1,81 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2026 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef OAK_OLIVE_EDITOR_PLUGIN_IMAGE_H
#define OAK_OLIVE_EDITOR_PLUGIN_IMAGE_H
#include "ofxCore.h"
#include "ofxImageEffect.h"
#include "ofxhClip.h"
#include "olive/core/render/pixelformat.h"
#include "loopmode.h"
#include "videoparams.h"
#include <cstdint>
#include <string>
#include <vector>
namespace olive
{
namespace plugin
{
class Image : public OFX::Host::ImageEffect::Image {
public:
Image(OFX::Host::ImageEffect::ClipInstance &clip_instance);
Image(OFX::Host::ImageEffect::ClipInstance &clip_instance,
const VideoParams &params, const OfxRectI &bounds,
const OfxRectI &rod, bool clear = true);
~Image();
uint8_t *data()
{
return (uint8_t *)getPointerProperty(kOfxImagePropData);
}
int width();
int height();
core::PixelFormat pixel_format();
bool premultiplied_alpha();
int channel_count();
void allocate_from_params(const VideoParams &params, const OfxRectI &bounds,
const OfxRectI &rod, bool clear = true);
void ensure_allocated_from_params(const VideoParams &params,
const OfxRectI &bounds, const OfxRectI &rod,
bool clear = false);
void allocate(int width, int height, core::PixelFormat format,
int channel_count, bool premultiplied_alpha,
const OfxRectI &bounds, const OfxRectI &rod,
bool clear = true);
int row_bytes() const
{
return row_bytes_;
}
protected:
std::vector<uint8_t> image_;
int width_;
int height_;
core::PixelFormat format_;
bool premultiplied_alpha_;
int channel_count_;
int row_bytes_;
OfxRectI bounds_;
OfxRectI rod_;
};
}
}
#endif //OAK_OLIVE_EDITOR_PLUGIN_IMAGE_H
File diff suppressed because it is too large Load Diff
+124
View File
@@ -0,0 +1,124 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef OAK_OLIVECLIP_H
#define OAK_OLIVECLIP_H
#include <map>
#include <memory>
#include "ofxCore.h"
#include "ofxhClip.h"
#include "common/videoparams.h"
#include "image.h"
#include "render/renderer.h"
namespace olive
{
namespace plugin
{
class OliveClipInstance : public OFX::Host::ImageEffect::ClipInstance {
public:
OliveClipInstance(OFX::Host::ImageEffect::Instance *effect_instance,
OFX::Host::ImageEffect::ClipDescriptor &desc,
OakVideoParams params)
: ClipInstance(effect_instance, desc)
, params_(params)
, name_(desc.getName())
{
if (params_.ctx) {
params_.addref(params_.ctx);
}
default_region_of_definition_ = { 0, 0, 0, 0 };
}
~OliveClipInstance() override
{
if (params_.ctx) {
oakcommon_videoparams_free(&params_);
}
for (auto &pair : images_) {
delete pair.second;
}
for (auto &pair : input_textures_) {
oakrender_display_texture_free(&pair.second);
}
for (auto &pair : output_textures_) {
oakrender_display_texture_free(&pair.second);
}
}
OFX::Host::ImageEffect::Image *getOutputImage(OfxTime time);
const std::string &getUnmappedBitDepth() const override;
const std::string &getUnmappedComponents() const override;
const std::string &getPremult() const override;
double getAspectRatio() const override;
double getFrameRate() const override;
void getFrameRange(double &start_frame, double &end_frame) const override;
const std::string &getFieldOrder() const override;
bool getConnected() const override;
double getUnmappedFrameRate() const override;
void getUnmappedFrameRange(double &start_frame,
double &end_frame) const override;
bool getContinuousSamples() const override;
OFX::Host::ImageEffect::Image *
getImage(OfxTime time, const OfxRectD *optional_bounds) override;
OfxRectD getRegionOfDefinition(OfxTime time) const override;
void setRegionOfDefinition(OfxRectD region_of_definition, OfxTime time);
void setDefaultRegionOfDefinition(OfxRectD region_of_definition);
void setParams(const olive::VideoParams &params);
#ifdef OFX_SUPPORTS_OPENGLRENDER
OFX::Host::ImageEffect::Texture *
loadTexture(OfxTime time, const char *format,
const OfxRectD *optional_bounds) override;
#endif
void setInputTexture(OakRenderTexture texture, OfxTime time,
bool readback_cpu = true);
void setOutputTexture(OakRenderTexture texture, OfxTime time);
// Get the plugin-preferred VideoParams based on base class _pixelDepth/_components
OakVideoParams getPluginPreferredParams() const;
// Prune old entries from the images_ cache to prevent unbounded growth.
// Output clip images are not pruned (they are typically single-frame).
void prune_images_cache();
static constexpr int k_max_input_image_cache = 8;
private:
OakVideoParams params_;
std::map<OfxTime, OfxRectD> region_of_definitions_;
OfxRectD default_region_of_definition_;
std::string name_;
std::map<OfxTime, Image *> images_;
std::map<OfxTime, OakRenderTexture> input_textures_;
std::map<OfxTime, OakRenderTexture> output_textures_;
};
}
}
#endif //OAK_OLIVECLIP_H
+321
View File
@@ -0,0 +1,321 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "olivehost.h"
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <filesystem>
#include <memory>
#include <ofxhBinary.h>
#include <ofxhPluginCache.h>
#include <ofxMessage.h>
#include "common/current.h"
#include "oliveplugininstance.h"
using namespace OFX::Host;
using namespace olive::plugin;
namespace olive
{
namespace plugin
{
class PluginNode;
}
}
#ifndef OAK_APP_VERSION
#define OAK_APP_VERSION "0.0.0"
#endif
namespace
{
olive::plugin::HostMessageHandler message_handler_;
void add_plugin_path(OFX::Host::PluginCache *cache, const std::string &path,
bool recurse = true)
{
if (!cache || path.empty()) {
return;
}
std::error_code ec;
if (!std::filesystem::exists(path, ec)) {
return;
}
cache->addFileToPath(
std::filesystem::weakly_canonical(path, ec).string(), recurse);
}
void add_plugin_paths_from_env(OFX::Host::PluginCache *cache,
const char *env_var)
{
const char *raw = std::getenv(env_var);
if (!raw || !*raw) {
return;
}
const char separator =
#if defined(_WIN32)
';';
#else
':';
#endif
std::string remaining(raw);
size_t pos;
while ((pos = remaining.find(separator)) != std::string::npos) {
std::string path = remaining.substr(0, pos);
remaining.erase(0, pos + 1);
if (!path.empty()) {
add_plugin_path(cache, path);
}
}
if (!remaining.empty()) {
add_plugin_path(cache, remaining);
}
}
std::string format_message(const char *format, va_list args)
{
char buffer[1024];
buffer[0] = '\0';
vsnprintf(buffer, sizeof(buffer), format, args);
return buffer;
}
} // namespace
void olive::plugin::set_host_message_handler(HostMessageHandler handler)
{
message_handler_ = std::move(handler);
}
olive::plugin::HostMessageHandler olive::plugin::get_host_message_handler()
{
return message_handler_;
}
void olive::plugin::load_plugins(const std::string &path)
{
OakCurrent current = oakcommon_current_instance();
void *host_ptr = nullptr;
void *cache_ptr = nullptr;
oakcommon_current_get_plugin_host(current, &host_ptr);
oakcommon_current_get_plugin_cache(current, &cache_ptr);
std::shared_ptr<OliveHost> host;
std::shared_ptr<ImageEffect::PluginCache> image_effect_plugin_cache;
if (host_ptr) {
host = *static_cast<std::shared_ptr<OliveHost> *>(host_ptr);
}
if (cache_ptr) {
image_effect_plugin_cache =
*static_cast<std::shared_ptr<ImageEffect::PluginCache> *>(
cache_ptr);
}
if (!host || !image_effect_plugin_cache) {
host = std::make_shared<OliveHost>();
image_effect_plugin_cache =
std::make_shared<ImageEffect::PluginCache>(*host);
// The Current slots hold owning shared_ptr copies; the destroy
// callbacks free those copies when the slots are replaced or the
// current object dies.
auto *host_slot = new std::shared_ptr<OliveHost>(host);
oakcommon_current_set_plugin_host(
current, host_slot,
[](void *p) { delete static_cast<std::shared_ptr<OliveHost> *>(p); });
auto *cache_slot =
new std::shared_ptr<ImageEffect::PluginCache>(
image_effect_plugin_cache);
oakcommon_current_set_plugin_cache(
current, cache_slot,
[](void *p) {
delete static_cast<std::shared_ptr<ImageEffect::PluginCache> *>(
p);
});
image_effect_plugin_cache->registerInCache(
*OFX::Host::PluginCache::getPluginCache());
}
oakcommon_current_free(&current);
OFX::Host::PluginCache *cache = OFX::Host::PluginCache::getPluginCache();
cache->setPluginHostPath("Olive");
const std::string home_path = std::getenv("HOME") ? std::getenv("HOME") : "";
if (!home_path.empty()) {
add_plugin_path(cache, home_path + "/.OFX/Plugins");
add_plugin_path(cache, home_path + "/.local/share/OFX/Plugins");
add_plugin_path(cache,
home_path + "/.local/share/olive/ofx/Plugins");
}
// Application-relative plugin paths are resolved by the facade; scan
// the default locations relative to the working directory here.
add_plugin_path(cache, "../OFX/Plugins");
add_plugin_path(cache, "../share/olive/ofx/Plugins");
add_plugin_path(cache, "../lib/olive/ofx/Plugins");
add_plugin_paths_from_env(cache, "OLIVE_OFX_PLUGIN_PATH");
add_plugin_paths_from_env(cache, "OLIVE_PLUGIN_PATH");
if (!path.empty()) {
add_plugin_path(cache, path, true);
}
cache->scanPluginFiles();
}
OliveHost::OliveHost()
{
// Identify the host to plugins; HostSupport seeds these with "UNKNOWN".
_properties.setStringProperty(kOfxPropName, "Oak Video Editor");
_properties.setStringProperty(kOfxPropLabel, "Oak Video Editor");
_properties.setStringProperty(kOfxPropVersionLabel, OAK_APP_VERSION);
// Numeric version for plugins that query kOfxPropVersion directly.
int version_parts[3] = { 0, 0, 0 };
sscanf(OAK_APP_VERSION, "%d.%d.%d", &version_parts[0], &version_parts[1],
&version_parts[2]);
_properties.setIntProperty(kOfxPropVersion, version_parts[0], 0);
_properties.setIntProperty(kOfxPropVersion, version_parts[1], 1);
_properties.setIntProperty(kOfxPropVersion, version_parts[2], 2);
}
OliveHost::~OliveHost()
{
}
void OliveHost::destroy_instance(OFX::Host::ImageEffect::Instance *instance)
{
if (!instance) {
return;
}
for (auto it = instances_.begin(); it != instances_.end(); ++it) {
if (it->get() == instance) {
instances_.erase(it);
break;
}
}
}
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
OliveHost::makeDescriptor(ImageEffect::ImageEffectPlugin *plugin)
{
std::shared_ptr<OFX::Host::ImageEffect::Descriptor> desc =
std::make_shared<ImageEffect::Descriptor>(plugin);
descriptors_.push_back(std::shared_ptr<ImageEffect::Descriptor>(desc));
return desc;
}
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
OliveHost::makeDescriptor(const ImageEffect::Descriptor &root_context,
ImageEffect::ImageEffectPlugin *plugin)
{
std::shared_ptr<OFX::Host::ImageEffect::Descriptor> desc =
std::make_shared<ImageEffect::Descriptor>(root_context, plugin);
descriptors_.push_back(std::shared_ptr<ImageEffect::Descriptor>(desc));
return desc;
}
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
OliveHost::makeDescriptor(const std::string &bundle_path,
ImageEffect::ImageEffectPlugin *plugin)
{
std::shared_ptr<OFX::Host::ImageEffect::Descriptor> desc =
std::make_shared<ImageEffect::Descriptor>(bundle_path, plugin);
descriptors_.push_back(std::shared_ptr<ImageEffect::Descriptor>(desc));
return desc;
}
ImageEffect::Instance *
OliveHost::newInstance(void *client_data, ImageEffect::ImageEffectPlugin *plugin,
ImageEffect::Descriptor &desc,
const std::string &context)
{
auto *instance = new OlivePluginInstance(plugin, desc, context, true);
if (client_data) {
instance->set_node_handle(
*static_cast<OakNodeNode *>(client_data));
}
instances_.push_back(std::shared_ptr<OlivePluginInstance>(instance));
return instance;
}
OfxStatus OliveHost::vmessage(const char *type, const char *id,
const char *format, va_list args)
{
if (!type || !format) {
return kOfxStatFailed;
}
std::string message = format_message(format, args);
if (message_handler_) {
return message_handler_(type, message);
}
// Headless default: log to stderr, questions get "no".
fprintf(stderr, "OFX message: %s %s\n", type, message.c_str());
if (strcmp(type, kOfxMessageQuestion) == 0) {
return kOfxStatReplyNo;
}
return kOfxStatOK;
}
OfxStatus OliveHost::setPersistentMessage(const char *type, const char *id,
const char *format, va_list args)
{
if (!type || !format) {
return kOfxStatFailed;
}
std::string message = format_message(format, args);
if (strcmp(type, kOfxMessageError) == 0) {
persistent_messages_.push_back({ HostMessageType::error, message });
} else if (strcmp(type, kOfxMessageWarning) == 0) {
persistent_messages_.push_back({ HostMessageType::warning, message });
} else if (strcmp(type, kOfxMessageMessage) == 0) {
persistent_messages_.push_back({ HostMessageType::message, message });
} else {
return kOfxStatFailed;
}
if (message_handler_) {
return message_handler_(type, message);
}
fprintf(stderr, "OFX %s: %s\n", type, message.c_str());
return kOfxStatOK;
}
OfxStatus OliveHost::clearPersistentMessage()
{
persistent_messages_.clear();
return kOfxStatOK;
}
+132
View File
@@ -0,0 +1,132 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_OLIVE_HOST_H
#define OAK_OLIVE_HOST_H
#include <cstdint>
#include <list>
#include <memory>
#include <string>
#include <vector>
#include "ofxCore.h"
#include "ofxhHost.h"
#include "ofxhImageEffect.h"
#include "ofxhImageEffectAPI.h"
namespace olive
{
namespace plugin
{
enum class HostMessageType { error, warning, message };
struct HostPersistentMessage {
HostMessageType type;
std::string message;
};
/**
* @brief UI message handler for OFX host messages
*
* Registered by the facade. Return kOfxStatReplyYes/No for questions,
* kOfxStatOK otherwise. Without a handler, messages are logged to
* stderr and questions get kOfxStatReplyNo (headless default).
*/
using HostMessageHandler =
std::function<OfxStatus(const char *type, const std::string &message)>;
void set_host_message_handler(HostMessageHandler handler);
/** @brief Currently registered handler (may be empty). */
HostMessageHandler get_host_message_handler();
void load_plugins(const std::string &path = std::string());
class OliveHost : public OFX::Host::ImageEffect::Host {
public:
OliveHost();
~OliveHost() override;
void destroy_instance(OFX::Host::ImageEffect::Instance *instance);
bool pluginSupported(OFX::Host::ImageEffect::ImageEffectPlugin *plugin,
std::string &reason) const override
{
if (!plugin) {
reason = "null plugin";
return false;
}
if (plugin->getContexts().empty()) {
reason = "no supported contexts (describe failed)";
return false;
}
return true;
};
OFX::Host::ImageEffect::Instance *
newInstance(void *client_data,
OFX::Host::ImageEffect::ImageEffectPlugin *plugin,
OFX::Host::ImageEffect::Descriptor &desc,
const std::string &context) override;
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
makeDescriptor(OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override;
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
makeDescriptor(const OFX::Host::ImageEffect::Descriptor &root_context,
OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override;
std::shared_ptr<OFX::Host::ImageEffect::Descriptor>
makeDescriptor(const std::string &bundle_path,
OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override;
/// vmessage
OfxStatus vmessage(const char *type, const char *id,
const char *format, va_list args) override;
/// vmessage
OfxStatus setPersistentMessage(const char *type, const char *id,
const char *format, va_list args) override;
/// vmessage
OfxStatus clearPersistentMessage() override;
#ifdef OFX_SUPPORTS_OPENGLRENDER
/// @see OfxImageEffectOpenGLRenderSuiteV1.flushResources()
virtual OfxStatus flushOpenGLResources() const override
{
return kOfxStatFailed;
};
#endif
int persistent_message_count() const
{
return int(persistent_messages_.size());
}
const std::vector<HostPersistentMessage> &persistent_messages() const
{
return persistent_messages_;
}
private:
std::vector<std::shared_ptr<OFX::Host::ImageEffect::Descriptor>>
descriptors_;
std::vector<std::shared_ptr<OFX::Host::ImageEffect::Instance>> instances_;
std::vector<HostPersistentMessage> persistent_messages_;
};
}
}
#endif
+608
View File
@@ -0,0 +1,608 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "oliveplugininstance.h"
#include <cstdio>
#include <cstring>
#include "ofxGPURender.h"
#include "node/sequence.h"
#include "olivehost.h"
#include "ofxMessage.h"
#include "oliveclip.h"
#include "paraminstance.h"
namespace olive
{
namespace plugin
{
namespace
{
const std::string k_image_field_none_str(kOfxImageFieldNone);
const std::string k_image_field_upper_str(kOfxImageFieldUpper);
const std::string k_image_field_lower_str(kOfxImageFieldLower);
std::string format_ofx_message(const char *format, va_list args)
{
char buffer[1024];
va_list args_copy;
va_copy(args_copy, args);
const int needed = vsnprintf(buffer, sizeof(buffer), format, args_copy);
va_end(args_copy);
if (needed < 0) {
return std::string();
}
if (needed < static_cast<int>(sizeof(buffer))) {
return std::string(buffer);
}
std::vector<char> dynamic_buffer(size_t(needed) + 1);
const int written =
vsnprintf(dynamic_buffer.data(), dynamic_buffer.size(), format, args);
if (written < 0) {
return std::string();
}
return std::string(dynamic_buffer.data());
}
const std::string &field_order_for_params(OakVideoParams params)
{
int interlacing = 0;
if (params.ctx) {
oakcommon_videoparams_get_interlacing(params, &interlacing);
}
switch (interlacing) {
case OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST:
return k_image_field_upper_str;
case OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST:
return k_image_field_lower_str;
default:
return k_image_field_none_str;
}
}
std::thread::id main_thread_id_;
bool main_thread_id_set_ = false;
ActiveViewerProvider active_viewer_provider_;
UndoSubmitFn undo_submit_fn_;
} // namespace
bool is_gui_thread()
{
if (!main_thread_id_set_) {
main_thread_id_ = std::this_thread::get_id();
main_thread_id_set_ = true;
}
return std::this_thread::get_id() == main_thread_id_;
}
void set_main_thread_id(std::thread::id id)
{
main_thread_id_ = id;
main_thread_id_set_ = true;
}
void set_active_viewer_provider(ActiveViewerProvider provider)
{
active_viewer_provider_ = std::move(provider);
}
void set_undo_submit_callback(UndoSubmitFn fn)
{
undo_submit_fn_ = std::move(fn);
}
const std::string &OlivePluginInstance::getDefaultOutputFielding() const
{
return field_order_for_params(params_);
}
void register_node_instance(uintptr_t identity,
OlivePluginInstance *instance);
void OlivePluginInstance::set_node_handle(OakNodeNode node)
{
if (node_.ctx) {
register_node_instance(oaknode_node_identity(node_), nullptr);
}
node_ = node;
if (node_.ctx) {
register_node_instance(oaknode_node_identity(node_), this);
}
for (const auto &entry : getParams()) {
if (!entry.second) {
continue;
}
if (auto *bound = dynamic_cast<NodeBoundParam *>(entry.second)) {
bound->set_node(node_);
}
}
}
OfxStatus OlivePluginInstance::vmessage(const char *type, const char *id,
const char *format, va_list args)
{
const std::string message = format_ofx_message(format, args);
if (message.empty()) {
return kOfxStatFailed;
}
// UI messages route through the host's registered handler
HostMessageHandler handler = get_host_message_handler();
if (handler) {
return handler(type, message);
}
fprintf(stderr, "OFX message: %s %s\n", type, message.c_str());
if (strcmp(type, kOfxMessageQuestion) == 0) {
return kOfxStatReplyNo;
}
return kOfxStatOK;
}
OfxStatus OlivePluginInstance::setPersistentMessage(const char *type,
const char *id,
const char *format,
va_list args)
{
const std::string message = format_ofx_message(format, args);
if (message.empty()) {
return kOfxStatFailed;
}
ErrorType error_type;
// If this is an error message
if (strncmp(type, kOfxMessageError, strlen(kOfxMessageError)) == 0) {
error_type = ErrorType::error;
}
// A warning
else if (strncmp(type, kOfxMessageWarning, strlen(kOfxMessageWarning)) ==
0) {
error_type = ErrorType::warning;
}
// A simple information
else if (strncmp(type, kOfxMessageMessage, strlen(kOfxMessageMessage)) ==
0) {
error_type = ErrorType::message;
} else {
return kOfxStatFailed;
}
persistent_errors_.push_back({ error_type, message });
HostMessageHandler handler = get_host_message_handler();
if (handler) {
return handler(type, message);
}
fprintf(stderr, "OFX %s: %s\n", type, message.c_str());
return kOfxStatOK;
}
OfxStatus OlivePluginInstance::clearPersistentMessage()
{
persistent_errors_.clear();
return kOfxStatOK;
}
void OlivePluginInstance::getProjectSize(double &x_size, double &y_size) const
{
double par = 1.0;
int par_num = 0, par_den = 1;
int width = 0, height = 0;
if (params_.ctx) {
oakcommon_videoparams_get_pixel_aspect_ratio(params_, &par_num,
&par_den);
oakcommon_videoparams_get_width(params_, &width);
oakcommon_videoparams_get_height(params_, &height);
}
if (par_den != 0) {
par = double(par_num) / par_den;
}
x_size = width * par;
y_size = height;
}
void OlivePluginInstance::getProjectOffset(double &x_offset,
double &y_offset) const
{
double par = 1.0;
int par_num = 0, par_den = 1;
float x = 0, y = 0;
if (params_.ctx) {
oakcommon_videoparams_get_pixel_aspect_ratio(params_, &par_num,
&par_den);
oakcommon_videoparams_get_x(params_, &x);
oakcommon_videoparams_get_y(params_, &y);
}
if (par_den != 0) {
par = double(par_num) / par_den;
}
x_offset = x * par;
y_offset = y;
}
void OlivePluginInstance::getProjectExtent(double &x_size, double &y_size) const
{
getProjectSize(x_size, y_size);
}
double OlivePluginInstance::getProjectPixelAspectRatio() const
{
int par_num = 0, par_den = 1;
if (params_.ctx) {
oakcommon_videoparams_get_pixel_aspect_ratio(params_, &par_num,
&par_den);
}
if (par_den == 0) {
return 1.0;
}
double par = double(par_num) / par_den;
if (par == 0.0) {
return 1.0; // default PAR when not explicitly set
}
return par;
}
double OlivePluginInstance::getFrameRate() const
{
int num = 0, den = 1;
if (params_.ctx) {
oakcommon_videoparams_get_frame_rate(params_, &num, &den);
}
return den ? double(num) / den : 0.0;
}
double OlivePluginInstance::getEffectDuration() const
{
// Return a default duration value
return 100.0;
}
double OlivePluginInstance::getFrameRecursive() const
{
// Return current frame (this would typically be set by the host during rendering)
return 0.0;
}
void OlivePluginInstance::getRenderScaleRecursive(double &x, double &y) const
{
// Return default render scale (1.0, 1.0)
x = 1.0;
y = 1.0;
}
OFX::Host::Param::Instance *
OlivePluginInstance::newParam(const std::string &name,
OFX::Host::Param::Descriptor &desc)
{
const std::string &type = desc.getType();
if (type == kOfxParamTypeInteger) {
return new IntegerInstance(node_, desc, this);
} else if (type == kOfxParamTypeDouble) {
return new DoubleInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeBoolean) {
return new BooleanInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeChoice) {
return new ChoiceInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeString) {
return new StringInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeRGBA) {
return new RGBAInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeRGB) {
return new RGBInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeDouble2D) {
return new Double2DInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeInteger2D) {
return new Integer2DInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeDouble3D) {
return new Double3DInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeInteger3D) {
return new Integer3DInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeCustom || type == kOfxParamTypeBytes) {
return new CustomInstance(node_, name, desc, this);
} else if (type == kOfxParamTypeGroup) {
return new GroupInstance(desc, this);
} else if (type == kOfxParamTypePage) {
return new PageInstance(desc, this);
} else if (type == kOfxParamTypePushButton) {
return new PushbuttonInstance(node_, name, desc, this);
}
return nullptr; // 未实现的类型
}
OfxStatus OlivePluginInstance::editBegin(const std::string &name)
{
edit_depth_++;
if (edit_depth_ == 1) {
edit_command_ = {};
edit_label_.clear();
edit_first_label_.clear();
edit_param_count_ = 0;
if (!name.empty()) {
edit_first_label_ = "Change " + name;
}
}
return kOfxStatOK;
}
OfxStatus OlivePluginInstance::editEnd()
{
if (edit_depth_ > 0) {
edit_depth_--;
}
if (edit_depth_ == 0 && edit_command_.ctx) {
std::string label = edit_label_;
if (label.empty()) {
if (edit_param_count_ <= 1 && !edit_first_label_.empty()) {
label = edit_first_label_;
} else if (edit_param_count_ > 1 && !edit_first_label_.empty()) {
label = edit_first_label_ + " (+" +
std::to_string(edit_param_count_ - 1) + ")";
} else {
label = "Edit Parameters";
}
}
if (undo_submit_fn_) {
undo_submit_fn_(edit_command_, label);
} else {
oakundo_command_redo_now(edit_command_);
oakundo_command_free(&edit_command_);
}
edit_command_ = {};
edit_label_.clear();
edit_first_label_.clear();
edit_param_count_ = 0;
}
return kOfxStatOK;
}
void OlivePluginInstance::submit_undo_command(OakUndoCommand command,
const std::string &label)
{
if (!command.ctx) {
return;
}
if (edit_depth_ > 0) {
if (!edit_command_.ctx) {
edit_command_ = oakundo_command_init_multi();
}
edit_param_count_++;
if (!label.empty() && edit_first_label_.empty()) {
edit_first_label_ = label;
}
oakundo_command_redo_now(command);
oakundo_command_multi_add_child(edit_command_, command);
return;
}
if (undo_submit_fn_ && is_gui_thread()) {
undo_submit_fn_(command, label);
return;
}
oakundo_command_redo_now(command);
oakundo_command_free(&command);
}
void OlivePluginInstance::progressStart(const std::string &message,
const std::string &messageid)
{
(void)messageid;
progress_cancelled_ = false;
progress_active_ = true;
if (progress_reporter_) {
progress_reporter_->close();
progress_reporter_.reset();
}
std::string dialog_message =
message.empty() ? "Processing..." : message;
progress_reporter_.reset(
create_plugin_progress_reporter(dialog_message, "OpenFX"));
progress_reporter_->set_cancel_callback(
[this](void *) { progress_cancelled_ = true; }, nullptr);
progress_reporter_->show();
}
void OlivePluginInstance::progressEnd()
{
progress_active_ = false;
progress_cancelled_ = false;
if (progress_reporter_) {
progress_reporter_->close();
progress_reporter_.reset();
}
}
bool OlivePluginInstance::progressUpdate(double t)
{
if (!progress_active_) {
return true;
}
if (progress_reporter_) {
double clamped = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t);
progress_reporter_->set_progress(clamped);
}
return !progress_cancelled_;
}
#ifdef OFX_SUPPORTS_OPENGLRENDER
OfxStatus OlivePluginInstance::contextAttachedAction()
{
if (!open_gl_enabled_) {
return kOfxStatReplyDefault;
}
return kOfxStatOK;
}
OfxStatus OlivePluginInstance::contextDetachedAction()
{
if (!open_gl_enabled_) {
return kOfxStatReplyDefault;
}
return kOfxStatOK;
}
#endif
double OlivePluginInstance::timeLineGetTime()
{
if (active_viewer_provider_) {
OakNodeNode viewer = active_viewer_provider_();
if (viewer.ctx) {
// Playhead as seconds
int num = 0, den = 1;
if (oaknode_sequence_get_playhead(
oaknode_sequence_from_node(viewer),
&num, &den) == OAKNODE_OK && den != 0) {
return double(num) / den;
}
}
}
return 0.0;
}
void OlivePluginInstance::timeLineGotoTime(double t)
{
if (active_viewer_provider_) {
OakNodeNode viewer = active_viewer_provider_();
if (viewer.ctx) {
olive::core::Rational r = olive::core::Rational::from_double(t);
oaknode_sequence_set_playhead(
oaknode_sequence_from_node(viewer), r.numerator(),
r.denominator());
}
}
}
void OlivePluginInstance::timeLineGetBounds(double &t1, double &t2)
{
if (active_viewer_provider_) {
OakNodeNode viewer = active_viewer_provider_();
if (viewer.ctx) {
int len_num = 0, len_den = 1;
if (oaknode_sequence_get_length(
oaknode_sequence_from_node(viewer), &len_num,
&len_den) == OAKNODE_OK && len_den != 0) {
t1 = 0.0;
t2 = double(len_num) / len_den;
return;
}
}
}
t1 = 0.0;
t2 = 0.0;
}
void OlivePluginInstance::setCustomInArgs(const std::string &action,
OFX::Host::Property::Set &in_args)
{
if (action == kOfxImageEffectActionRender ||
action == kOfxImageEffectActionBeginSequenceRender ||
action == kOfxImageEffectActionEndSequenceRender) {
in_args.setIntProperty(kOfxImageEffectPropOpenGLEnabled,
open_gl_enabled_ ? 1 : 0);
}
}
OFX::Host::ImageEffect::ClipInstance *OlivePluginInstance::newClipInstance(
OFX::Host::ImageEffect::Instance *plugin,
OFX::Host::ImageEffect::ClipDescriptor *descriptor, int index)
{
// Create a new clip instance
OliveClipInstance *clip_instance =
new OliveClipInstance(plugin, *descriptor, params_);
// Initialize base class clip properties from VideoParams so that
// setupClipPreferencesArgs and plugin constructors (which may fetch
// clips and query their properties before getClipPreferences is called)
// have valid defaults instead of kOfxImageComponentNone / kOfxBitDepthNone.
std::string depth = kOfxBitDepthFloat; // host default
std::string comp = kOfxImageComponentRGBA; // host default
int format = -1;
int channels = 0;
if (params_.ctx) {
oakcommon_videoparams_get_format(params_, &format);
oakcommon_videoparams_get_channel_count(params_, &channels);
}
switch (format) {
case OAKCOMMON_PIXEL_FORMAT_U8:
depth = kOfxBitDepthByte;
break;
case OAKCOMMON_PIXEL_FORMAT_U16:
depth = kOfxBitDepthShort;
break;
case OAKCOMMON_PIXEL_FORMAT_F16:
depth = kOfxBitDepthHalf;
break;
case OAKCOMMON_PIXEL_FORMAT_F32:
depth = kOfxBitDepthFloat;
break;
default:
break; // keep F32 default
}
switch (channels) {
case 1:
comp = kOfxImageComponentAlpha;
break;
case 3:
comp = kOfxImageComponentRGB;
break;
case 4:
comp = kOfxImageComponentRGBA;
break;
default:
break; // keep RGBA default
}
clip_instance->setPixelDepth(depth);
clip_instance->setComponents(comp);
return clip_instance;
}
OlivePluginInstance::~OlivePluginInstance()
{
if (node_.ctx) {
register_node_instance(oaknode_node_identity(node_), nullptr);
}
_created = false;
if (params_.ctx) {
oakcommon_videoparams_free(&params_);
}
}
}
}
+253
View File
@@ -0,0 +1,253 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_OLIVE_INSTANCE_H
#define OAK_OLIVE_INSTANCE_H
#include <functional>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "ofxCore.h"
#include "ofxImageEffect.h"
#include "ofxhImageEffect.h"
#include "common/videoparams.h"
#include "node/node.h"
#include "pluginprogressreporter.h"
#include "undo/undocommand.h"
namespace olive
{
namespace plugin
{
/**
* @brief Whether the calling thread is the registered main (GUI) thread
*
* The facade registers the main thread at startup with
* set_main_thread_id(); before registration the first caller's thread
* is treated as main (headless default).
*/
bool is_gui_thread();
void set_main_thread_id(std::thread::id id);
class PluginProgressReporter;
enum class ErrorType { error, warning, message };
struct PersistentErrors {
ErrorType type;
std::string message;
};
/**
* @brief Provider returning the currently active viewer as an oaknode
* handle
*
* Registered by the facade. Without a provider, the OFX timeline suite
* falls back to its safe defaults (current time 0, empty bounds,
* seeking does nothing).
*/
using ActiveViewerProvider = std::function<OakNodeNode()>;
void set_active_viewer_provider(ActiveViewerProvider provider);
/**
* @brief Undo submission callback (facade pushes onto its undo stack)
*
* The command handle is consumed by the callback (the facade stack
* takes a reference; see oakundo push semantics). Without a callback,
* commands are redone immediately and released.
*/
using UndoSubmitFn = std::function<void(OakUndoCommand command,
const std::string &label)>;
void set_undo_submit_callback(UndoSubmitFn fn);
class OlivePluginInstance : public OFX::Host::ImageEffect::Instance {
public:
OlivePluginInstance(OFX::Host::ImageEffect::ImageEffectPlugin *plugin,
OFX::Host::ImageEffect::Descriptor &desc,
const std::string &context, bool interactive)
: OFX::Host::ImageEffect::Instance(plugin, desc, context, interactive)
{
}
OlivePluginInstance(OlivePluginInstance &instance)
: Instance(instance._plugin, *instance._descriptor, instance._context,
instance._interactive)
{
// Do NOT shallow-copy _clips: Instance::~Instance() deletes them,
// which would cause a double-free. Clips are re-created in populate().
_created = instance._created;
_clipPrefsDirty = instance._clipPrefsDirty;
_continuousSamples = instance._continuousSamples;
_frameVarying = instance._frameVarying;
_outputPreMultiplication = instance._outputPreMultiplication;
_outputFielding = instance._outputFielding;
_outputFrameRate = instance._outputFrameRate;
}
explicit OlivePluginInstance(Instance &instance)
: Instance(instance) {};
~OlivePluginInstance() override;
const std::string &getDefaultOutputFielding() const override;
void setVideoParam(OakVideoParams params)
{
if (params_.ctx) {
oakcommon_videoparams_free(&params_);
}
params_ = params;
if (params_.ctx) {
params_.addref(params_.ctx);
}
}
void set_node_handle(OakNodeNode node);
OakNodeNode node_handle() const
{
return node_;
}
void setOpenGLEnabled(bool enabled)
{
open_gl_enabled_ = enabled;
}
bool isCreated() const
{
return _created;
}
OFX::Host::ImageEffect::ClipInstance *
newClipInstance(OFX::Host::ImageEffect::Instance *plugin,
OFX::Host::ImageEffect::ClipDescriptor *descriptor,
int index) override;
OfxStatus vmessage(const char *type, const char *id, const char *format,
va_list args) override;
OfxStatus setPersistentMessage(const char *type, const char *id,
const char *format, va_list args) override;
OfxStatus clearPersistentMessage() override;
int persistent_message_count() const
{
return int(persistent_errors_.size());
}
const std::vector<PersistentErrors> &persistent_messages() const
{
return persistent_errors_;
}
void getProjectSize(double &x_size, double &y_size) const override;
void getProjectOffset(double &x_offset, double &y_offset) const override;
void getProjectExtent(double &x_size, double &y_size) const override;
// The pixel aspect ratio of the current project
double getProjectPixelAspectRatio() const override;
// The duration of the effect
// This contains the duration of the plug-in effect, in frames.
double getEffectDuration() const override;
// For an instance, this is the frame rate of the project the effect is in.
double getFrameRate() const override;
/// This is called whenever a param is changed by the plugin so that
/// the recursive instanceChangedAction will be fed the correct frame
double getFrameRecursive() const override;
/// This is called whenever a param is changed by the plugin so that
/// the recursive instanceChangedAction will be fed the correct
/// renderScale
void getRenderScaleRecursive(double &x, double &y) const override;
////////////////////////////////////////////////////////////////////////////////
// overridden for Param::SetInstance
/// make a parameter instance
OFX::Host::Param::Instance *
newParam(const std::string &name,
OFX::Host::Param::Descriptor &descriptor) override;
void submit_undo_command(OakUndoCommand command, const std::string &label);
/// Triggered when the plug-in calls OfxParameterSuiteV1::paramEditBegin
virtual OfxStatus editBegin(const std::string &name) override;
/// Triggered when the plug-in calls OfxParameterSuiteV1::paramEditEnd
virtual OfxStatus editEnd() override;
////////////////////////////////////////////////////////////////////////////////
// overridden for Progress::ProgressI
/// Start doing progress.
virtual void progressStart(const std::string &message,
const std::string &messageid) override;
/// finish yer progress
virtual void progressEnd() override;
/// set the progress to some level of completion, returns
/// false if you should abandon processing, true to continue
virtual bool progressUpdate(double t) override;
#ifdef OFX_SUPPORTS_OPENGLRENDER
virtual OfxStatus contextAttachedAction() override;
virtual OfxStatus contextDetachedAction() override;
#endif
////////////////////////////////////////////////////////////////////////////////
// overridden for TimeLine::TimeLineI
/// get the current time on the timeline. This is not necessarily the same
/// time as being passed to an action (eg render)
double timeLineGetTime() override;
/// set the timeline to a specific time
void timeLineGotoTime(double t) override;
/// get the first and last times available on the effect's timeline
void timeLineGetBounds(double &t1, double &t2) override;
void setCustomInArgs(const std::string &action,
OFX::Host::Property::Set &in_args) override;
private:
std::vector<PersistentErrors> persistent_errors_;
OakVideoParams params_ = {};
OakNodeNode node_ = {};
int edit_depth_ = 0;
OakUndoCommand edit_command_ = {};
std::string edit_label_;
std::string edit_first_label_;
int edit_param_count_ = 0;
std::unique_ptr<PluginProgressReporter> progress_reporter_;
bool progress_cancelled_ = false;
bool progress_active_ = false;
bool open_gl_enabled_ = false;
public:
std::mutex &mutex()
{
return mutex_;
}
private:
std::mutex mutex_;
};
}
}
#endif
+75
View File
@@ -0,0 +1,75 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "paraminstance.h"
#include <map>
#include "oliveplugininstance.h"
namespace olive
{
namespace plugin
{
namespace
{
/**
* @brief Node-identity → plugin instance registry, maintained by
* OlivePluginInstance::set_node_handle()
*/
std::map<uintptr_t, OlivePluginInstance *> &instance_registry()
{
static std::map<uintptr_t, OlivePluginInstance *> registry;
return registry;
}
} // namespace
void register_node_instance(uintptr_t identity, OlivePluginInstance *instance)
{
if (instance) {
instance_registry()[identity] = instance;
} else {
instance_registry().erase(identity);
}
}
void submit_undo_command(OakNodeNode node, OakUndoCommand command,
const std::string &label)
{
if (!command.ctx) {
return;
}
if (node.ctx) {
auto it = instance_registry().find(oaknode_node_identity(node));
if (it != instance_registry().end() && it->second) {
it->second->submit_undo_command(command, label);
return;
}
}
// No instance bound: run immediately and release
oakundo_command_redo_now(command);
oakundo_command_free(&command);
}
}
}
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "pluginprogressreporter.h"
namespace olive
{
namespace plugin
{
namespace
{
/**
* @brief No-op reporter used when no UI factory is registered
*
* Never reports cancellation, so processing always continues.
*/
class NullPluginProgressReporter : public PluginProgressReporter {
public:
void set_progress(double value) override
{
(void)value;
}
void show() override
{
}
void close() override
{
}
};
PluginProgressReporterFactory reporter_factory_;
}
void set_plugin_progress_reporter_factory(
PluginProgressReporterFactory factory)
{
reporter_factory_ = std::move(factory);
}
PluginProgressReporter *
create_plugin_progress_reporter(const std::string &message,
const std::string &title)
{
if (reporter_factory_) {
return reporter_factory_(message, title);
}
return new NullPluginProgressReporter();
}
}
}
+113
View File
@@ -0,0 +1,113 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef OAK_PLUGIN_PROGRESS_REPORTER_H
#define OAK_PLUGIN_PROGRESS_REPORTER_H
#include <functional>
#include <string>
namespace olive
{
namespace plugin
{
/**
* @brief UI-independent interface for reporting plugin progress
*
* The engine cannot show UI itself, so OFX progress reporting goes through
* this interface. The UI layer registers a factory (see
* set_plugin_progress_reporter_factory()) that creates a reporter wrapping a
* ProgressDialog; without a factory, a no-op reporter is used instead.
*
* Cancellation is delivered through a C-style callback.
*/
class PluginProgressReporter {
public:
PluginProgressReporter() = default;
virtual ~PluginProgressReporter() = default;
virtual void set_progress(double value) = 0;
virtual void show() = 0;
virtual void close() = 0;
/**
* @brief Register a callback to be invoked when the user cancels.
*
* Only one callback is supported; subsequent calls replace the
* previous registration. Pass nullptr to clear.
*/
void set_cancel_callback(std::function<void(void *)> cb, void *userdata)
{
cancel_callback_ = std::move(cb);
cancel_callback_userdata_ = userdata;
}
/**
* @brief Mark this reporter as cancelled and notify the registered
* callback.
*/
void set_cancelled()
{
cancelled_ = true;
if (cancel_callback_) {
cancel_callback_(cancel_callback_userdata_);
}
}
bool cancelled() const
{
return cancelled_;
}
private:
std::function<void(void *)> cancel_callback_;
void *cancel_callback_userdata_ = nullptr;
bool cancelled_ = false;
};
/**
* @brief Factory creating a PluginProgressReporter for a progress session
*
* Registered by the UI layer at startup. The caller takes ownership of the
* returned reporter.
*/
using PluginProgressReporterFactory =
std::function<PluginProgressReporter *(const std::string &message,
const std::string &title)>;
void set_plugin_progress_reporter_factory(
PluginProgressReporterFactory factory);
/**
* @brief Create a progress reporter through the registered factory
*
* Without a factory, returns a no-op reporter so engine code can run
* headless. The caller takes ownership of the returned reporter.
*/
PluginProgressReporter *
create_plugin_progress_reporter(const std::string &message,
const std::string &title);
}
}
#endif // OAK_PLUGIN_PROGRESS_REPORTER_H
+168
View File
@@ -0,0 +1,168 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
# Modifications Copyright (C) 2025 mikesolar
#
# 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 <http://www.gnu.org/licenses/>.
cmake_minimum_required(VERSION 3.16)
project(oakplugin-standalone CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# oakplugin manipulates node graphs through the oaknode C ABI; the
# not-yet-split engine modules' symbols are allowed to dangle, resolved
# by transitional stubs in src/{node,render}/transition.
set(OAK_REPO_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake")
set(BUILD_TESTS ON CACHE BOOL "" FORCE)
set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# The /opt/otio OpenTimelineIO dylibs use @loader_path install names that
# break when linked from a build tree; oakplugin does not need OTIO.
set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON)
find_package(EXPAT REQUIRED)
find_package(OpenColorIO CONFIG REQUIRED)
find_package(OpenImageIO CONFIG REQUIRED)
set(OCIO_LIBRARIES OpenColorIO::OpenColorIO)
set(OCIO_INCLUDE_DIRS ${OpenColorIO_INCLUDE_DIRS})
set(OIIO_LIBRARIES OpenImageIO::OpenImageIO)
set(OIIO_INCLUDE_DIRS ${OpenImageIO_INCLUDE_DIRS})
add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core)
target_include_directories(olivecore PUBLIC
${OAK_REPO_ROOT}/third_party/openfx/include
)
add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge)
set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo)
add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common)
add_subdirectory(${OAK_REPO_ROOT}/src/render/src ${CMAKE_BINARY_DIR}/render)
add_subdirectory(${OAK_REPO_ROOT}/src/render/c_api ${CMAKE_BINARY_DIR}/render_c_api)
set(BUILD_TESTS ON)
enable_testing()
set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/timeline ${CMAKE_BINARY_DIR}/timeline)
add_subdirectory(${OAK_REPO_ROOT}/src/codec ${CMAKE_BINARY_DIR}/codec)
set(BUILD_TESTS ON)
add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node)
add_subdirectory(${OAK_REPO_ROOT}/src/plugin ${CMAKE_BINARY_DIR}/task)
target_include_directories(oakcodec PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
${OAK_REPO_ROOT}/src/render/src
${OAK_REPO_ROOT}/src/node/src
)
foreach(t oakcodec oaktimeline)
if(TARGET ${t})
target_link_options(${t} PRIVATE
"-undefined" "dynamic_lookup"
)
endif()
endforeach()
# oaknode needs its transition stubs and the oakrender real headers
# (src/node/transition/render/* bridge there); src/render/transition
# must precede src/node/transition.
target_include_directories(oaknode BEFORE PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
${OAK_REPO_ROOT}/src/render/src
)
target_include_directories(oaknode PUBLIC
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
/opt/homebrew/include
/opt/homebrew/include/Imath
)
target_link_options(oaknode PRIVATE
"-undefined" "dynamic_lookup"
)
target_include_directories(oakrender BEFORE PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
)
target_include_directories(oakrender PUBLIC
${OAK_REPO_ROOT}/engine/include
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
/opt/homebrew/include
/opt/homebrew/include/Imath
/opt/homebrew/include/OpenEXR
)
foreach(t oakrender oakgl oakgl2 oakvulkan)
if(TARGET ${t})
target_link_options(${t} PRIVATE
"-undefined" "dynamic_lookup"
)
endif()
endforeach()
target_include_directories(oakplugin PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
${OAK_REPO_ROOT}/src/render/src
${OAK_REPO_ROOT}/src/node/src
)
target_link_options(oakplugin PRIVATE
"-undefined" "dynamic_lookup"
)
target_link_libraries(oakrender PRIVATE
oaknode
oakcommon
oakundo
olivecore
ffmpeg_bridge
${OCIO_LIBRARIES}
${OIIO_LIBRARIES}
"-framework OpenGL"
"-framework CoreVideo"
"-framework Metal"
"-framework QuartzCore"
)
target_link_libraries(oaknode-gtest PRIVATE oakrender oaktimeline oakcodec)
# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
# dynamic_lookup; the real implementation (engine/src/capi/ipc.cpp) is still
# Qt-based, so test binaries link an inert shim instead.
target_sources(oaknode-gtest PRIVATE
${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp
)
target_include_directories(oaknode-gtest PRIVATE
${OAK_REPO_ROOT}/engine/include
)
# OFX typeinfo is referenced via dynamic_lookup; force-load the host
# support archive so RTTI resolves (same as oaknode's test wiring).
# oakplugin-gtest wires itself up in src/plugin/tests/CMakeLists.txt.
find_library(OAKNODE_OFX_HOST_ARCHIVE NAMES OfxHost
PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport
NO_DEFAULT_PATH)
if(NOT OAKNODE_OFX_HOST_ARCHIVE)
message(FATAL_ERROR
"libOfxHost.a not found; run the full-tree build once or set "
"OAKNODE_OFX_HOST_ARCHIVE")
endif()
target_link_options(oaknode-gtest PRIVATE
"-Wl,-force_load,${OAKNODE_OFX_HOST_ARCHIVE}")
# TimelineAddTrackCommand, which lives in liboakplugin; the two
# libraries resolve each other at runtime (dynamic_lookup).
target_link_libraries(oaknode-gtest PRIVATE oaktimeline)
+65
View File
@@ -0,0 +1,65 @@
# Oak Video Editor - Non-Linear Video Editor
# Copyright (C) 2026 Oak Team
#
# 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 <http://www.gnu.org/licenses/>.
find_package(GTest REQUIRED)
include(GoogleTest)
# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone
# build (see src/plugin/standalone) sets OAK_REPO_ROOT explicitly.
if(NOT DEFINED OAK_REPO_ROOT)
set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR})
endif()
add_executable(oakplugin-gtest
plugin_test.cpp
)
target_link_libraries(oakplugin-gtest PRIVATE
oakplugin
oakrender
GTest::gtest
GTest::gtest_main
)
# liboakrender/liboaknode dangle OFX host symbols (-undefined
# dynamic_lookup); force-load the host support archive into the test
# process so dyld finds them in the flat namespace at startup. Mirrors
# src/render/tests/CMakeLists.txt.
if(NOT DEFINED OAKRENDER_OFX_HOST_ARCHIVE)
find_library(OAKRENDER_OFX_HOST_ARCHIVE NAMES OfxHost
PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport)
endif()
if(NOT OAKRENDER_OFX_HOST_ARCHIVE)
message(FATAL_ERROR
"libOfxHost.a not found; run the full-tree build once or set "
"OAKRENDER_OFX_HOST_ARCHIVE")
endif()
target_link_options(oakplugin-gtest PRIVATE
"-Wl,-force_load,${OAKRENDER_OFX_HOST_ARCHIVE}")
# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
# dynamic_lookup; the test binary links the inert shim from
# src/node/standalone instead.
target_sources(oakplugin-gtest PRIVATE
${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp)
target_include_directories(oakplugin-gtest PRIVATE
${OAK_REPO_ROOT}/engine/include
)
gtest_discover_tests(oakplugin-gtest
DISCOVERY_MODE PRE_TEST
PROPERTIES ENVIRONMENT
"OCIO=${OAK_REPO_ROOT}/engine/render/ocioconf/config.ocio")
+106
View File
@@ -0,0 +1,106 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
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 <http://www.gnu.org/licenses/>.
***/
#include <cstring>
#include <gtest/gtest.h>
#include "plugin/host.h"
#include "plugin/instance.h"
TEST(OakPluginHost, InitAndEnumerate)
{
EXPECT_EQ(oakplugin_host_init(), OAKPLUGIN_OK);
int count = oakplugin_host_plugin_count();
EXPECT_GE(count, 0);
char buf[256];
EXPECT_EQ(oakplugin_host_plugin_id_at(-1, buf, sizeof(buf)),
OAKPLUGIN_E_NOT_FOUND);
EXPECT_EQ(oakplugin_host_plugin_id_at(count + 100, buf, sizeof(buf)),
OAKPLUGIN_E_NOT_FOUND);
EXPECT_EQ(oakplugin_host_plugin_label(nullptr, buf, sizeof(buf)),
OAKPLUGIN_E_INVALID);
const char *dirs[] = { "/nonexistent/ofx/path" };
EXPECT_EQ(oakplugin_host_scan(dirs, 1), OAKPLUGIN_OK);
EXPECT_EQ(oakplugin_host_scan(nullptr, 1), OAKPLUGIN_E_INVALID);
oakplugin_host_shutdown();
}
TEST(OakPluginHost, MessageHandler)
{
static int calls = 0;
calls = 0;
oakplugin_host_set_message_handler(
[](const char *type, const char *message, void *userdata) -> int {
int *counter = static_cast<int *>(userdata);
(*counter)++;
EXPECT_STREQ(type, "Message");
EXPECT_STRNE(message, "");
return OAKPLUGIN_MESSAGE_ANSWER_NO;
},
&calls);
// No plugin loaded here; just verify registration does not crash and
// the default path works after clearing
oakplugin_host_set_message_handler(nullptr, nullptr);
SUCCEED();
}
TEST(OakPluginInstance, CreateUnknownIdFails)
{
EXPECT_EQ(oakplugin_host_init(), OAKPLUGIN_OK);
OakPluginInstance instance =
oakplugin_instance_create("com.example.nonexistent.plugin");
EXPECT_EQ(instance.ctx, nullptr);
oakplugin_instance_free(&instance); // no-op on empty
EXPECT_EQ(oakplugin_debug_alive_count(), 0);
}
TEST(OakPluginInstance, InvalidArgs)
{
OakPluginInstance empty = {};
oakplugin_instance_free(nullptr); // no-op
oakplugin_instance_free(&empty); // no-op
oaknode_value value = {};
EXPECT_EQ(oakplugin_instance_set_param(empty, "p", &value),
OAKPLUGIN_E_INVALID);
EXPECT_EQ(oakplugin_instance_get_param(empty, "p", &value),
OAKPLUGIN_E_INVALID);
EXPECT_EQ(oakplugin_instance_set_param_string(empty, "p", "x"),
OAKPLUGIN_E_INVALID);
EXPECT_EQ(oakplugin_instance_get_param_string(empty, "p", nullptr, 0),
OAKPLUGIN_E_INVALID);
EXPECT_EQ(oakplugin_instance_render(empty, OakRenderTexture{},
OakRenderTexture{}, 0.0),
OAKPLUGIN_E_INVALID);
EXPECT_EQ(oakplugin_instance_set_progress_cb(empty, nullptr, nullptr),
OAKPLUGIN_E_INVALID);
EXPECT_EQ(oakplugin_instance_cancel(empty), OAKPLUGIN_E_INVALID);
}