engine: complete C ABI facade (liboakengine oakengine_* surface)

The full pure-C facade used by the app: node/project/timeline/viewer/
undo/task/events/serializer/playback/preview/renderer/gizmo/color/
audio/footage/proxy/encoding/exporter/config/disk/ipc/plugin/worker
families, plus undo-group semantics, display renderer handles,
NodeFactory accessors, and per-family pure-C engine tests.
This commit is contained in:
2026-07-26 22:43:00 +08:00
parent c486c853ff
commit e9f173916f
80 changed files with 28715 additions and 126 deletions
+39
View File
@@ -23,22 +23,61 @@
set(OLIVE_SOURCES
${OLIVE_SOURCES}
include/oakengine/init.h
include/oakengine/events.h
include/oakengine/app.h
include/oakengine/project.h
include/oakengine/timeline.h
include/oakengine/renderer.h
include/oakengine/display.h
include/oakengine/footage.h
include/oakengine/exporter.h
include/oakengine/encoding.h
include/oakengine/videoparams.h
include/oakengine/color.h
include/oakengine/node.h
include/oakengine/playback.h
include/oakengine/preview.h
include/oakengine/sync.h
include/oakengine/worker.h
include/oakengine/viewer.h
include/oakengine/traverse.h
include/oakengine/task.h
include/oakengine/undo.h
include/oakengine/config.h
include/oakengine/audio.h
include/oakengine/disk.h
include/oakengine/proxy.h
include/oakengine/lut.h
include/oakengine/serializer.h
include/oakengine/plugin.h
include/oakengine/gizmo.h
src/capi/init.cpp
src/capi/events.cpp
src/capi/app.cpp
src/capi/project.cpp
src/capi/timeline.cpp
src/capi/renderer.cpp
src/capi/display.cpp
src/capi/footage.cpp
src/capi/export.cpp
src/capi/encoding.cpp
src/capi/color.cpp
src/capi/node.cpp
src/capi/playback.cpp
src/capi/preview.cpp
src/capi/sync.cpp
src/capi/worker.cpp
src/capi/viewer.cpp
src/capi/traverse.cpp
src/capi/task.cpp
src/capi/undo.cpp
src/capi/config.cpp
src/capi/audio.cpp
src/capi/disk.cpp
src/capi/proxy.cpp
src/capi/lut.cpp
src/capi/serializer.cpp
src/capi/plugin.cpp
src/capi/gizmo.cpp
PARENT_SCOPE
)
+812
View File
@@ -0,0 +1,812 @@
/***
Oak - 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 "oakengine/app.h"
#include <cstdio>
#include <cstring>
#include <QCoreApplication>
#include <QGuiApplication>
#include <QString>
#include "coreengine.h"
#include "node/project.h"
#include "node/project/footage/footage.h"
#include "node/project/sequence/sequence.h"
#include "node/project/serializer/serializedlayoutinfo.h"
#include "task/task.h"
#include "undo/undostack.h"
namespace
{
olive::Project *impl(OakEngineProject *h)
{
return reinterpret_cast<olive::Project *>(h);
}
OakEngineProject *wrap(olive::Project *p)
{
return reinterpret_cast<OakEngineProject *>(p);
}
OakEngineSequence *wrap_seq(olive::Sequence *s)
{
return reinterpret_cast<OakEngineSequence *>(s);
}
// buf/size convention: returns the would-be length excluding the NUL.
int string_to_buf(const QString &s, char *buf, int buf_size)
{
const QByteArray utf = s.toUtf8();
if (buf && buf_size > 0) {
snprintf(buf, size_t(buf_size), "%s", utf.constData());
}
return int(utf.size());
}
// Registered callback set (all fields may be null).
OakEngineAppCallbacks g_callbacks = {};
// Whether oakengine_app_start() has run (and oakengine_app_stop() has not).
bool g_started = false;
// The EngineCore the notification signals are currently connected to.
olive::EngineCore *g_connected_core = nullptr;
olive::EngineCore *app_core()
{
return olive::EngineCore::instance();
}
// The EngineCore constructor's UndoStack member creates QActions, which need
// QGuiApplication state (same reason as oakengine_init()).
void ensure_qcoreapplication()
{
if (QCoreApplication::instance()) {
return;
}
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
qputenv("QT_QPA_PLATFORM", "offscreen");
}
static int argc = 1;
static char app_name[] = "oakengine";
static char *argv[] = { app_name, nullptr };
new QGuiApplication(argc, argv);
QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org"));
QCoreApplication::setApplicationName(QStringLiteral("Oak Video Editor"));
}
// Forward engine signals to the registered C callbacks. Connected once per
// EngineCore instance; dropped events are fine while no callback is set.
void connect_notifications(olive::EngineCore *core)
{
if (!core || g_connected_core == core) {
return;
}
g_connected_core = core;
QObject::connect(core, &olive::EngineCore::status_message_show, core,
[](const QString &message, int timeout) {
if (g_callbacks.status_message_show) {
g_callbacks.status_message_show(
message.toUtf8().constData(), timeout,
g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::status_message_clear, core,
[] {
if (g_callbacks.status_message_clear) {
g_callbacks.status_message_clear(
g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::cache_full_warning_requested,
core, [] {
if (g_callbacks.cache_full_warning) {
g_callbacks.cache_full_warning(
g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::active_project_changed, core,
[](olive::Project *p) {
if (g_callbacks.active_project_changed) {
g_callbacks.active_project_changed(
wrap(p), g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::tool_changed, core,
[](const olive::Tool::Item &tool) {
if (g_callbacks.tool_changed) {
g_callbacks.tool_changed(int(tool),
g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::addable_object_changed, core,
[](olive::Tool::AddableObject o) {
if (g_callbacks.addable_object_changed) {
g_callbacks.addable_object_changed(
int(o), g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::snapping_changed, core,
[](const bool &b) {
if (g_callbacks.snapping_changed) {
g_callbacks.snapping_changed(b ? 1 : 0,
g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::timecode_display_changed, core,
[](olive::core::Timecode::Display d) {
if (g_callbacks.timecode_display_changed) {
g_callbacks.timecode_display_changed(
int(d), g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::open_recent_list_changed, core,
[] {
if (g_callbacks.open_recent_list_changed) {
g_callbacks.open_recent_list_changed(
g_callbacks.userdata);
}
});
QObject::connect(core, &olive::EngineCore::color_picker_enabled, core,
[](bool e) {
if (g_callbacks.color_picker_enabled) {
g_callbacks.color_picker_enabled(
e ? 1 : 0, g_callbacks.userdata);
}
});
}
// Translate the C handler callbacks into the std::function handlers
// EngineCore calls when it needs user interaction.
void install_handlers(olive::EngineCore *core)
{
if (g_callbacks.confirm_image_sequence) {
core->set_confirm_image_sequence_handler([](const QString &filename) {
return g_callbacks.confirm_image_sequence(
filename.toUtf8().constData(),
g_callbacks.userdata) != 0;
});
} else {
core->set_confirm_image_sequence_handler(nullptr);
}
if (g_callbacks.relink_footage) {
core->set_relink_handler([](QVector<olive::Footage *> footage) {
return g_callbacks.relink_footage(
reinterpret_cast<OakEngineFootage **>(footage.data()),
int(footage.size()), g_callbacks.userdata) != 0;
});
} else {
core->set_relink_handler(nullptr);
}
if (g_callbacks.save_project) {
core->set_save_project_handler([](const QString &override_filename) {
g_callbacks.save_project(override_filename.toUtf8().constData(),
g_callbacks.userdata);
});
} else {
core->set_save_project_handler(nullptr);
}
if (g_callbacks.close_project) {
core->set_close_project_handler([] {
return g_callbacks.close_project(g_callbacks.userdata) != 0;
});
} else {
core->set_close_project_handler(nullptr);
}
if (g_callbacks.load_layout) {
core->set_load_layout_handler(
[](const olive::SerializedLayoutInfo &layout) {
g_callbacks.load_layout(&layout, g_callbacks.userdata);
});
} else {
core->set_load_layout_handler(nullptr);
}
#ifdef USE_OTIO
if (g_callbacks.otio_import) {
core->set_otio_import_handler(
[](const QList<olive::Sequence *> &sequences) {
QVector<OakEngineSequence *> handles;
handles.reserve(sequences.size());
for (olive::Sequence *s : sequences) {
handles.append(wrap_seq(s));
}
return g_callbacks.otio_import(handles.data(),
int(handles.size()),
g_callbacks.userdata) != 0;
});
} else {
core->set_otio_import_handler(nullptr);
}
#endif
}
} // namespace
extern "C"
{
int oakengine_app_create(const OakEngineAppParams *params)
{
if (app_core()) {
return OAKENGINE_E_STATE;
}
ensure_qcoreapplication();
olive::EngineCore::CoreParams core_params;
if (params) {
switch (params->run_mode) {
case OAKENGINE_APP_RUN_HEADLESS_EXPORT:
core_params.set_run_mode(
olive::EngineCore::CoreParams::k_headless_export);
break;
case OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE:
core_params.set_run_mode(
olive::EngineCore::CoreParams::k_headless_pre_cache);
break;
default:
core_params.set_run_mode(
olive::EngineCore::CoreParams::k_run_normal);
break;
}
core_params.set_fullscreen(params->fullscreen != 0);
if (params->startup_project) {
core_params.set_startup_project(
QString::fromUtf8(params->startup_project));
}
if (params->startup_language) {
core_params.set_startup_language(
QString::fromUtf8(params->startup_language));
}
if (params->crash_on_startup) {
core_params.set_crash_on_startup(true);
}
}
// Never deleted: backs the process-wide EngineCore singleton (same
// lifetime rule as the oakengine_init() shell).
new olive::EngineCore(core_params);
return OAKENGINE_OK;
}
int oakengine_app_start(void)
{
if (!app_core() || g_started) {
return OAKENGINE_E_STATE;
}
app_core()->start();
g_started = true;
return OAKENGINE_OK;
}
int oakengine_app_stop(void)
{
if (!app_core() || !g_started) {
return OAKENGINE_E_STATE;
}
app_core()->stop();
g_started = false;
return OAKENGINE_OK;
}
int oakengine_app_set_callbacks(const OakEngineAppCallbacks *callbacks)
{
if (callbacks) {
g_callbacks = *callbacks;
} else {
g_callbacks = OakEngineAppCallbacks{};
}
if (olive::EngineCore *core = app_core()) {
connect_notifications(core);
install_handlers(core);
}
return OAKENGINE_OK;
}
int oakengine_app_run_mode(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
switch (app_core()->core_params().run_mode()) {
case olive::EngineCore::CoreParams::k_headless_export:
return OAKENGINE_APP_RUN_HEADLESS_EXPORT;
case olive::EngineCore::CoreParams::k_headless_pre_cache:
return OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE;
default:
return OAKENGINE_APP_RUN_NORMAL;
}
}
int oakengine_app_fullscreen(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return app_core()->core_params().fullscreen() ? 1 : 0;
}
int oakengine_app_startup_project(char *buf, int buf_size)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(app_core()->core_params().startup_project(), buf,
buf_size);
}
void *oakengine_app_undo_stack(void)
{
if (!app_core()) {
return nullptr;
}
return app_core()->undo_stack();
}
int oakengine_app_tool(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return int(app_core()->tool());
}
int oakengine_app_set_tool(int tool)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
if (tool < 0 || tool >= int(olive::Tool::k_count)) {
return OAKENGINE_E_INVALID;
}
app_core()->set_tool(static_cast<olive::Tool::Item>(tool));
return OAKENGINE_OK;
}
int oakengine_app_addable_object(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return int(app_core()->get_selected_addable_object());
}
int oakengine_app_set_addable_object(int object)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
if (object < 0 || object >= int(olive::Tool::k_addable_count)) {
return OAKENGINE_E_INVALID;
}
app_core()->set_selected_addable_object(
static_cast<olive::Tool::AddableObject>(object));
return OAKENGINE_OK;
}
int oakengine_app_selected_transition(char *buf, int buf_size)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(app_core()->get_selected_transition(), buf, buf_size);
}
int oakengine_app_set_selected_transition(const char *id)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->set_selected_transition_object(
id ? QString::fromUtf8(id) : QString());
return OAKENGINE_OK;
}
int oakengine_app_snapping(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return app_core()->snapping() ? 1 : 0;
}
int oakengine_app_set_snapping(int enabled)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->set_snapping(enabled != 0);
return OAKENGINE_OK;
}
int oakengine_app_timecode_display(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return int(app_core()->get_timecode_display());
}
int oakengine_app_set_timecode_display(int display)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
if (display < 0 ||
display > int(olive::core::Timecode::k_milliseconds)) {
return OAKENGINE_E_INVALID;
}
app_core()->set_timecode_display(
static_cast<olive::core::Timecode::Display>(display));
return OAKENGINE_OK;
}
int oakengine_app_recent_projects_count(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return int(app_core()->get_recent_projects().size());
}
int oakengine_app_recent_project_at(int index, char *buf, int buf_size)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
const QStringList &recent = app_core()->get_recent_projects();
if (index < 0 || index >= recent.size()) {
return OAKENGINE_E_NOT_FOUND;
}
return string_to_buf(recent.at(index), buf, buf_size);
}
int oakengine_app_remove_recent_project(int index)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
if (index < 0 || index >= app_core()->get_recent_projects().size()) {
return OAKENGINE_E_NOT_FOUND;
}
app_core()->remove_recently_opened_project(index);
return OAKENGINE_OK;
}
int oakengine_app_clear_recent_projects(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->clear_open_recent_list();
return OAKENGINE_OK;
}
int oakengine_app_show_status_message(const char *message, int timeout)
{
if (!app_core() || !message) {
return OAKENGINE_E_INVALID;
}
app_core()->show_status_bar_message(QString::fromUtf8(message), timeout);
return OAKENGINE_OK;
}
int oakengine_app_clear_status_message(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->clear_status_bar_message();
return OAKENGINE_OK;
}
int oakengine_app_set_language(const char *locale)
{
if (!app_core() || !locale) {
return OAKENGINE_E_INVALID;
}
return app_core()->set_language(QString::fromUtf8(locale)) ? 1 : 0;
}
int oakengine_app_set_autorecovery_interval(int minutes)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->set_autorecovery_interval(minutes);
return OAKENGINE_OK;
}
int oakengine_app_set_use_proxy_media(int enabled)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->set_use_proxy_media(enabled != 0);
return OAKENGINE_OK;
}
int oakengine_app_request_pixel_sampling(int enable)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->request_pixel_sampling_in_viewers(enable != 0);
return OAKENGINE_OK;
}
int oakengine_app_set_magic(int enabled)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->set_magic(enabled != 0);
return OAKENGINE_OK;
}
int oakengine_app_is_magic_enabled(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return app_core()->is_magic_enabled() ? 1 : 0;
}
int oakengine_app_copy_to_clipboard(const char *text)
{
if (!app_core() || !text) {
return OAKENGINE_E_INVALID;
}
olive::EngineCore::copy_string_to_clipboard(QString::fromUtf8(text));
return OAKENGINE_OK;
}
int oakengine_app_paste_from_clipboard(char *buf, int buf_size)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(olive::EngineCore::paste_string_from_clipboard(), buf,
buf_size);
}
int oakengine_app_footage_file_dialog_filter(char *buf, int buf_size)
{
return string_to_buf(olive::EngineCore::footage_file_dialog_filter(), buf,
buf_size);
}
int oakengine_app_is_footage_extension_allowed(const char *path)
{
if (!path) {
return OAKENGINE_E_INVALID;
}
return olive::EngineCore::is_footage_extension_allowed(
QString::fromUtf8(path)) ?
1 :
0;
}
OakEngineSequence *oakengine_app_create_sequence(OakEngineProject *project,
const char *name_format)
{
if (!app_core() || !project) {
return nullptr;
}
const QString format = name_format ?
QString::fromUtf8(name_format) :
QStringLiteral("Sequence %1");
return wrap_seq(olive::EngineCore::create_new_sequence_for_project(
format, impl(project)));
}
int oakengine_app_auto_recovery_index_filename(char *buf, int buf_size)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(olive::EngineCore::get_auto_recovery_index_filename(),
buf, buf_size);
}
OakEngineProject *oakengine_app_open_project(void)
{
if (!app_core()) {
return nullptr;
}
return wrap(app_core()->open_project());
}
int oakengine_app_create_new_project(void)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->create_new_project();
return OAKENGINE_OK;
}
int oakengine_app_add_open_project(OakEngineProject *project,
int add_to_recents)
{
if (!app_core() || !project) {
return OAKENGINE_E_INVALID;
}
app_core()->add_open_project(impl(project), add_to_recents != 0);
return OAKENGINE_OK;
}
int oakengine_app_add_open_project_from_task(void *task, int add_to_recents)
{
if (!app_core() || !task) {
return OAKENGINE_E_INVALID;
}
return app_core()->add_open_project_from_task(
static_cast<olive::Task *>(task), add_to_recents != 0) ?
1 :
0;
}
int oakengine_app_add_recovery_project_from_task(void *task)
{
if (!app_core() || !task) {
return OAKENGINE_E_INVALID;
}
app_core()->add_recovery_project_from_task(static_cast<olive::Task *>(task));
return OAKENGINE_OK;
}
int oakengine_app_on_project_saved(OakEngineProject *project)
{
if (!app_core() || !project) {
return OAKENGINE_E_INVALID;
}
app_core()->on_project_saved(impl(project));
return OAKENGINE_OK;
}
int oakengine_app_set_active_project(OakEngineProject *project)
{
if (!app_core()) {
return OAKENGINE_E_INVALID;
}
app_core()->set_active_project(impl(project));
return OAKENGINE_OK;
}
// ---- Individual handler setter convenience wrappers ----
int oakengine_app_set_confirm_image_sequence_handler(
int (*fn)(const char *filename, void *userdata), void *userdata)
{
OakEngineAppCallbacks cb = g_callbacks;
cb.userdata = userdata;
cb.confirm_image_sequence = fn;
return oakengine_app_set_callbacks(&cb);
}
int oakengine_app_set_relink_handler(
int (*fn)(OakEngineFootage **footage, int count, void *userdata),
void *userdata)
{
OakEngineAppCallbacks cb = g_callbacks;
cb.userdata = userdata;
cb.relink_footage = fn;
return oakengine_app_set_callbacks(&cb);
}
int oakengine_app_set_save_project_handler(
void (*fn)(const char *override_filename, void *userdata), void *userdata)
{
OakEngineAppCallbacks cb = g_callbacks;
cb.userdata = userdata;
cb.save_project = fn;
return oakengine_app_set_callbacks(&cb);
}
int oakengine_app_set_close_project_handler(
int (*fn)(void *userdata), void *userdata)
{
OakEngineAppCallbacks cb = g_callbacks;
cb.userdata = userdata;
cb.close_project = fn;
return oakengine_app_set_callbacks(&cb);
}
int oakengine_app_set_load_layout_handler(
void (*fn)(const void *layout, void *userdata), void *userdata)
{
OakEngineAppCallbacks cb = g_callbacks;
cb.userdata = userdata;
cb.load_layout = fn;
return oakengine_app_set_callbacks(&cb);
}
// ---- void*-based convenience overloads ----
int oakengine_app_get_auto_recovery_index_filename(char *buf, int buf_size)
{
return oakengine_app_auto_recovery_index_filename(buf, buf_size);
}
int oakengine_app_remove_recently_opened_project(int index)
{
return oakengine_app_remove_recent_project(index);
}
int oakengine_app_on_project_saved_vp(void *project)
{
return oakengine_app_on_project_saved(
reinterpret_cast<OakEngineProject *>(project));
}
int oakengine_app_set_active_project_vp(void *project)
{
return oakengine_app_set_active_project(
reinterpret_cast<OakEngineProject *>(project));
}
int oakengine_app_add_open_project_vp(void *project, int add_to_recents)
{
return oakengine_app_add_open_project(
reinterpret_cast<OakEngineProject *>(project), add_to_recents);
}
} // extern "C"
+458
View File
@@ -0,0 +1,458 @@
/***
Oak - 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 "oakengine/audio.h"
#include "oakengine/encoding.h"
#include <algorithm>
#include <cstring>
#include <new>
#include <QByteArray>
#include <QString>
#include "audio/audiomanager.h"
#include "audio/audioprocessor.h"
#include "audio/audiosynchronizer.h"
#include "audio/audiowaveformsync.h"
#include "olive/core/oakcore/audioparams.h"
#include "olive/core/render/audioparams.h"
namespace
{
int write_string(const QString &s, char *buf, int buf_size)
{
const QByteArray utf8 = s.toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
std::memcpy(buf, utf8.constData(), size_t(n));
buf[n] = '\0';
}
return len;
}
olive::AudioManager *manager()
{
return olive::AudioManager::instance();
}
} // namespace
extern "C" int oakengine_audio_create_instance(void)
{
olive::AudioManager::create_instance();
return manager() ? OAKENGINE_OK : OAKENGINE_E_FAILED;
}
extern "C" int oakengine_audio_destroy_instance(void)
{
olive::AudioManager::destroy_instance();
return OAKENGINE_OK;
}
extern "C" void *oakengine_audio_manager_handle(void)
{
return manager();
}
extern "C" int64_t oakengine_audio_get_output_device(void)
{
if (olive::AudioManager *m = manager()) {
return static_cast<int64_t>(m->get_output_device());
}
return -1; // paNoDevice
}
extern "C" int oakengine_audio_set_output_device(int64_t device)
{
if (olive::AudioManager *m = manager()) {
m->set_output_device(static_cast<PaDeviceIndex>(device));
return OAKENGINE_OK;
}
return OAKENGINE_E_STATE;
}
extern "C" int64_t oakengine_audio_get_input_device(void)
{
if (olive::AudioManager *m = manager()) {
return static_cast<int64_t>(m->get_input_device());
}
return -1; // paNoDevice
}
extern "C" int oakengine_audio_set_input_device(int64_t device)
{
if (olive::AudioManager *m = manager()) {
m->set_input_device(static_cast<PaDeviceIndex>(device));
return OAKENGINE_OK;
}
return OAKENGINE_E_STATE;
}
extern "C" int oakengine_audio_hard_reset(void)
{
if (olive::AudioManager *m = manager()) {
m->hard_reset();
return OAKENGINE_OK;
}
return OAKENGINE_E_STATE;
}
extern "C" int oakengine_audio_clear_buffered_output(void)
{
if (olive::AudioManager *m = manager()) {
m->clear_buffered_output();
return OAKENGINE_OK;
}
return OAKENGINE_E_STATE;
}
extern "C" int oakengine_audio_push_to_output(const OakAudioParams *params,
const char *samples,
int64_t samples_size,
char *error_buf,
int error_buf_size)
{
if (!params || !samples || samples_size < 0) {
return OAKENGINE_E_INVALID;
}
olive::AudioManager *m = manager();
if (!m) {
return OAKENGINE_E_STATE;
}
// The C++ AudioParams wrapper owns the OakAudioParams handle; the caller
// keeps ownership of `params`, so copy before wrapping.
const olive::core::AudioParams cpp_params =
olive::core::AudioParams::from_handle(
oakcore_audioparams_copy(params));
QString error;
const QByteArray data = QByteArray::fromRawData(samples,
static_cast<int>(samples_size));
if (!m->push_to_output(cpp_params, data, &error)) {
write_string(error, error_buf, error_buf_size);
return OAKENGINE_E_FAILED;
}
return OAKENGINE_OK;
}
extern "C" int oakengine_audio_stop_recording(void)
{
if (olive::AudioManager *m = manager()) {
m->stop_recording();
return OAKENGINE_OK;
}
return OAKENGINE_E_STATE;
}
extern "C" int oakengine_audio_stop_output(void)
{
if (olive::AudioManager *m = manager()) {
m->stop_output();
return OAKENGINE_OK;
}
return OAKENGINE_E_STATE;
}
extern "C" int oakengine_audio_reset_output_clock(void)
{
if (olive::AudioManager *m = manager()) {
m->reset_output_clock();
return OAKENGINE_OK;
}
return OAKENGINE_E_STATE;
}
extern "C" int oakengine_audio_set_output_notify_interval(int64_t bytes)
{
if (olive::AudioManager *m = manager()) {
m->set_output_notify_interval(static_cast<int>(bytes));
return OAKENGINE_OK;
}
return OAKENGINE_E_STATE;
}
extern "C" int oakengine_audio_start_recording(
OakEngineEncodingParams *params, char *error_buf, int error_buf_size)
{
// Delegate to the encoding-family implementation which handles the
// OakEngineEncodingParams -> EncodingParams conversion internally.
return oakengine_encoding_start_audio_recording(params, error_buf,
error_buf_size);
}
extern "C" int oakengine_audio_estimate_envelope_offset(
const double *reference, int reference_len,
const double *candidate, int candidate_len,
const bool *reference_valid, int reference_valid_len,
const bool *candidate_valid, int candidate_valid_len,
uint64_t window_samples, int64_t max_offset_windows,
oak_audio_waveform_offset *out)
{
if (!out || !reference || !candidate || reference_len < 0 ||
candidate_len < 0 || !window_samples) {
return OAKENGINE_E_INVALID;
}
if ((reference_valid && reference_valid_len != reference_len) ||
(candidate_valid && candidate_valid_len != candidate_len)) {
return OAKENGINE_E_INVALID;
}
QVector<double> ref(reference_len);
std::copy(reference, reference + reference_len, ref.begin());
QVector<double> cand(candidate_len);
std::copy(candidate, candidate + candidate_len, cand.begin());
QVector<bool> ref_valid;
if (reference_valid) {
ref_valid.resize(reference_valid_len);
std::copy(reference_valid, reference_valid + reference_valid_len,
ref_valid.begin());
}
QVector<bool> cand_valid;
if (candidate_valid) {
cand_valid.resize(candidate_valid_len);
std::copy(candidate_valid, candidate_valid + candidate_valid_len,
cand_valid.begin());
}
const olive::AudioWaveformSync::OffsetResult result =
olive::AudioWaveformSync::estimate_envelope_offset(
ref, cand, ref_valid, cand_valid, window_samples, max_offset_windows);
out->offset_samples = result.offset_samples;
out->confidence = result.confidence;
out->valid = result.valid ? 1 : 0;
return OAKENGINE_OK;
}
extern "C" int oakengine_audio_estimate_stretch_and_offset(
const double *reference, int reference_len,
const double *candidate, int candidate_len,
const bool *reference_valid, int reference_valid_len,
const bool *candidate_valid, int candidate_valid_len,
uint64_t window_samples, int64_t max_offset_windows,
double min_rate, double max_rate, double rate_step,
oak_audio_waveform_stretch_offset *out)
{
if (!out || !reference || !candidate || reference_len < 0 ||
candidate_len < 0 || !window_samples) {
return OAKENGINE_E_INVALID;
}
if ((reference_valid && reference_valid_len != reference_len) ||
(candidate_valid && candidate_valid_len != candidate_len)) {
return OAKENGINE_E_INVALID;
}
QVector<double> ref(reference_len);
std::copy(reference, reference + reference_len, ref.begin());
QVector<double> cand(candidate_len);
std::copy(candidate, candidate + candidate_len, cand.begin());
QVector<bool> ref_valid;
if (reference_valid) {
ref_valid.resize(reference_valid_len);
std::copy(reference_valid, reference_valid + reference_valid_len,
ref_valid.begin());
}
QVector<bool> cand_valid;
if (candidate_valid) {
cand_valid.resize(candidate_valid_len);
std::copy(candidate_valid, candidate_valid + candidate_valid_len,
cand_valid.begin());
}
const olive::AudioWaveformSync::StretchOffsetResult result =
olive::AudioWaveformSync::estimate_stretch_and_offset(
ref, cand, ref_valid, cand_valid, window_samples, max_offset_windows,
min_rate, max_rate, rate_step);
out->rate = result.rate;
out->offset_samples = result.offset_samples;
out->confidence = result.confidence;
out->valid = result.valid ? 1 : 0;
return OAKENGINE_OK;
}
extern "C" int oakengine_audio_sync_place_by_source_time(
const oak_audio_sync_source_clip *reference,
const oak_audio_sync_source_clip *candidate,
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
oak_audio_sync_placement *out)
{
if (!reference || !candidate || !out) {
return OAKENGINE_E_INVALID;
}
olive::AudioSynchronizer::SourceClip ref;
ref.source_start_time = olive::core::Rational(
reference->source_start_time_num, reference->source_start_time_den);
ref.media_in =
olive::core::Rational(reference->media_in_num, reference->media_in_den);
ref.has_source_start_time = reference->has_source_start_time != 0;
olive::AudioSynchronizer::SourceClip cand;
cand.source_start_time = olive::core::Rational(
candidate->source_start_time_num, candidate->source_start_time_den);
cand.media_in =
olive::core::Rational(candidate->media_in_num, candidate->media_in_den);
cand.has_source_start_time = candidate->has_source_start_time != 0;
const olive::core::Rational timeline_in(reference_timeline_in_num,
reference_timeline_in_den);
const olive::AudioSynchronizer::Placement placement =
olive::AudioSynchronizer::place_by_source_time(ref, cand, timeline_in);
out->timeline_in_num = placement.timeline_in.numerator();
out->timeline_in_den = placement.timeline_in.denominator();
out->valid = placement.valid ? 1 : 0;
return OAKENGINE_OK;
}
extern "C" int oakengine_audio_sync_place_by_waveform_offset(
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
int64_t candidate_offset_samples, int sample_rate,
oak_audio_sync_placement *out)
{
if (!out || sample_rate <= 0) {
return OAKENGINE_E_INVALID;
}
const olive::core::Rational timeline_in(reference_timeline_in_num,
reference_timeline_in_den);
const olive::AudioSynchronizer::Placement placement =
olive::AudioSynchronizer::place_by_waveform_offset(
timeline_in, candidate_offset_samples, sample_rate);
out->timeline_in_num = placement.timeline_in.numerator();
out->timeline_in_den = placement.timeline_in.denominator();
out->valid = placement.valid ? 1 : 0;
return OAKENGINE_OK;
}
/* ---- Audio format processor (R6 P5) ------------------------------------- */
namespace
{
olive::core::AudioParams params_from_c(const OakAudioParams *p)
{
// AudioParams takes ownership of the handle, so hand it a copy.
return olive::core::AudioParams::from_handle(oakcore_audioparams_copy(p));
}
} // namespace
struct OakEngineAudioProcessor {
olive::AudioProcessor proc;
// Holds the packed output of the most recent convert() so the caller can
// borrow the bytes across the C boundary.
olive::AudioProcessor::Buffer buf;
};
extern "C" OakEngineAudioProcessor *oakengine_audio_processor_create(void)
{
return new (std::nothrow) OakEngineAudioProcessor();
}
extern "C" void oakengine_audio_processor_free(OakEngineAudioProcessor *p)
{
delete p;
}
extern "C" int oakengine_audio_processor_open(OakEngineAudioProcessor *p,
const OakAudioParams *from,
const OakAudioParams *to,
double tempo)
{
if (!p || !from || !to) {
return OAKENGINE_E_INVALID;
}
const olive::core::AudioParams cpp_from = params_from_c(from);
const olive::core::AudioParams cpp_to = params_from_c(to);
return p->proc.open(cpp_from, cpp_to, tempo) ? OAKENGINE_OK
: OAKENGINE_E_FAILED;
}
extern "C" void oakengine_audio_processor_close(OakEngineAudioProcessor *p)
{
if (p) {
p->buf.clear();
p->proc.close();
}
}
extern "C" int oakengine_audio_processor_is_open(OakEngineAudioProcessor *p)
{
return (p && p->proc.is_open()) ? 1 : 0;
}
extern "C" int oakengine_audio_processor_convert(OakEngineAudioProcessor *p,
float **in, int nb_in_samples,
const void **out_data,
int *out_size)
{
if (!p) {
return OAKENGINE_E_INVALID;
}
if (out_data) {
*out_data = nullptr;
}
if (out_size) {
*out_size = 0;
}
p->buf.clear();
const int r = p->proc.convert(in, nb_in_samples, &p->buf);
if (r < 0) {
return r;
}
if (!p->buf.empty()) {
if (out_data) {
*out_data = p->buf.at(0).constData();
}
if (out_size) {
*out_size = p->buf.at(0).size();
}
}
return r;
}
extern "C" OakAudioParams *oakengine_audio_processor_output_params(
OakEngineAudioProcessor *p)
{
if (!p || !p->proc.is_open()) {
return nullptr;
}
return oakcore_audioparams_copy(p->proc.to().handle());
}
+448
View File
@@ -0,0 +1,448 @@
/***
Oak - 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 "oakengine/color.h"
#include <QString>
#include <cstring>
#include "colorinternal.h"
#include "node/color/colormanager/colormanager.h"
#include "node/project.h"
#include "render/job/colortransformjob.h"
#include "render/previewautocacher.h"
#include "render/rendermanager.h"
// The OakEngineColorProcessor handle layout is shared with the other capi
// translation units via colorinternal.h.
struct OakEngineColorConfig {
ocio::ConstConfigRcPtr ptr;
};
namespace
{
// buf/size convention: returns the would-be length excluding the NUL.
int string_to_buf(const QString &s, char *buf, int buf_size)
{
const QByteArray utf = s.toUtf8();
if (buf && buf_size > 0) {
snprintf(buf, size_t(buf_size), "%s", utf.constData());
}
return int(utf.size());
}
thread_local QString g_last_error;
void set_error(const QString &error)
{
g_last_error = error;
}
olive::ColorManager *impl(const OakEngineColorManager *h)
{
return reinterpret_cast<olive::ColorManager *>(
const_cast<OakEngineColorManager *>(h));
}
olive::ColorTransform to_cpp(const oak_color_transform &t)
{
if (t.is_display) {
return olive::ColorTransform(
t.output ? QString::fromUtf8(t.output) : QString(),
t.view ? QString::fromUtf8(t.view) : QString(),
t.look ? QString::fromUtf8(t.look) : QString());
}
return olive::ColorTransform(t.output ? QString::fromUtf8(t.output) :
QString());
}
// The engine's list accessors dereference the config unconditionally;
// guard here so a manager whose config failed to load yields empty lists
// instead of crashing.
bool has_config(const olive::ColorManager *mgr)
{
return mgr && mgr->get_config();
}
QString list_at(const QStringList &l, int index)
{
return (index >= 0 && index < l.size()) ? l.at(index) : QString();
}
} // namespace
extern "C" {
int oakengine_color_last_error(char *buf, int buf_size)
{
return string_to_buf(g_last_error, buf, buf_size);
}
OakEngineColorManager *
oakengine_color_manager_from_project(OakEngineProject *project)
{
if (!project) {
return nullptr;
}
auto *p = reinterpret_cast<olive::Project *>(project);
return reinterpret_cast<OakEngineColorManager *>(p->color_manager());
}
int oakengine_color_manager_get_config_filename(
const OakEngineColorManager *mgr, char *buf, int buf_size)
{
if (!mgr) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(mgr)->get_config_filename(), buf, buf_size);
}
int oakengine_color_manager_set_config_filename(OakEngineColorManager *mgr,
const char *filename)
{
if (!mgr || !filename) {
return OAKENGINE_E_INVALID;
}
impl(mgr)->set_config_filename(QString::fromUtf8(filename));
return OAKENGINE_OK;
}
int oakengine_color_manager_colorspace_count(const OakEngineColorManager *mgr)
{
if (!has_config(impl(mgr))) {
return 0;
}
return impl(mgr)->list_available_colorspaces().size();
}
int oakengine_color_manager_colorspace_at(const OakEngineColorManager *mgr,
int index, char *buf, int buf_size)
{
if (!has_config(impl(mgr))) {
return OAKENGINE_E_INVALID;
}
const QString s =
list_at(impl(mgr)->list_available_colorspaces(), index);
if (s.isNull()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(s, buf, buf_size);
}
int oakengine_color_manager_display_count(const OakEngineColorManager *mgr)
{
if (!has_config(impl(mgr))) {
return 0;
}
return impl(mgr)->list_available_displays().size();
}
int oakengine_color_manager_display_at(const OakEngineColorManager *mgr,
int index, char *buf, int buf_size)
{
if (!has_config(impl(mgr))) {
return OAKENGINE_E_INVALID;
}
const QString s = list_at(impl(mgr)->list_available_displays(), index);
if (s.isNull()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(s, buf, buf_size);
}
int oakengine_color_manager_view_count(const OakEngineColorManager *mgr,
const char *display)
{
if (!has_config(impl(mgr))) {
return 0;
}
return impl(mgr)
->list_available_views(display ? QString::fromUtf8(display) : QString())
.size();
}
int oakengine_color_manager_view_at(const OakEngineColorManager *mgr,
const char *display, int index, char *buf,
int buf_size)
{
if (!has_config(impl(mgr))) {
return OAKENGINE_E_INVALID;
}
const QString s = list_at(
impl(mgr)->list_available_views(display ? QString::fromUtf8(display) :
QString()),
index);
if (s.isNull()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(s, buf, buf_size);
}
int oakengine_color_manager_look_count(const OakEngineColorManager *mgr)
{
if (!has_config(impl(mgr))) {
return 0;
}
return impl(mgr)->list_available_looks().size();
}
int oakengine_color_manager_look_at(const OakEngineColorManager *mgr,
int index, char *buf, int buf_size)
{
if (!has_config(impl(mgr))) {
return OAKENGINE_E_INVALID;
}
const QString s = list_at(impl(mgr)->list_available_looks(), index);
if (s.isNull()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(s, buf, buf_size);
}
int oakengine_color_manager_default_display(const OakEngineColorManager *mgr,
char *buf, int buf_size)
{
if (!has_config(impl(mgr))) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(mgr)->get_default_display(), buf, buf_size);
}
int oakengine_color_manager_default_view(const OakEngineColorManager *mgr,
const char *display, char *buf,
int buf_size)
{
if (!has_config(impl(mgr))) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(
impl(mgr)->get_default_view(display ? QString::fromUtf8(display) :
QString()),
buf, buf_size);
}
int oakengine_color_manager_default_input_color_space(
const OakEngineColorManager *mgr, char *buf, int buf_size)
{
if (!mgr) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(mgr)->get_default_input_color_space(), buf,
buf_size);
}
int oakengine_color_manager_set_default_input_color_space(
OakEngineColorManager *mgr, const char *colorspace)
{
if (!mgr || !colorspace) {
return OAKENGINE_E_INVALID;
}
impl(mgr)->set_default_input_color_space(QString::fromUtf8(colorspace));
return OAKENGINE_OK;
}
int oakengine_color_manager_reference_color_space(
const OakEngineColorManager *mgr, char *buf, int buf_size)
{
if (!mgr) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(mgr)->get_reference_color_space(), buf,
buf_size);
}
int oakengine_color_manager_default_luma_coefs(
const OakEngineColorManager *mgr, double *rgb)
{
if (!mgr || !rgb) {
return OAKENGINE_E_INVALID;
}
impl(mgr)->get_default_luma_coefs(rgb);
return OAKENGINE_OK;
}
int oakengine_color_manager_compliant_color_space(
const OakEngineColorManager *mgr, const char *name, char *buf,
int buf_size)
{
if (!has_config(impl(mgr)) || !name) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(
impl(mgr)->get_compliant_color_space(QString::fromUtf8(name)), buf,
buf_size);
}
int oakengine_color_manager_compliant_transform(
const OakEngineColorManager *mgr, const oak_color_transform *in,
int force_display, int *out_is_display, char *out_output,
int output_size, char *out_view, int view_size, char *out_look,
int look_size)
{
if (!has_config(impl(mgr)) || !in) {
return OAKENGINE_E_INVALID;
}
const olive::ColorTransform compliant =
impl(mgr)->get_compliant_color_space(to_cpp(*in), force_display != 0);
if (out_is_display) {
*out_is_display = compliant.is_display() ? 1 : 0;
}
string_to_buf(compliant.output(), out_output, output_size);
string_to_buf(compliant.view(), out_view, view_size);
string_to_buf(compliant.look(), out_look, look_size);
return OAKENGINE_OK;
}
OakEngineColorConfig *oakengine_color_config_load_default(void)
{
try {
ocio::ConstConfigRcPtr c = olive::ColorManager::get_default_config();
if (!c) {
set_error(QStringLiteral("no default OCIO config available"));
return nullptr;
}
set_error(QString());
return new OakEngineColorConfig{std::move(c)};
} catch (ocio::Exception &e) {
set_error(QString::fromUtf8(e.what()));
return nullptr;
}
}
OakEngineColorConfig *oakengine_color_config_load_file(const char *filename)
{
if (!filename) {
set_error(QStringLiteral("no filename given"));
return nullptr;
}
try {
ocio::ConstConfigRcPtr c =
olive::ColorManager::create_config_from_file(
QString::fromUtf8(filename));
set_error(QString());
return new OakEngineColorConfig{std::move(c)};
} catch (ocio::Exception &e) {
set_error(QString::fromUtf8(e.what()));
return nullptr;
}
}
void oakengine_color_config_free(OakEngineColorConfig *config)
{
delete config;
}
int oakengine_color_config_colorspace_count(const OakEngineColorConfig *config)
{
if (!config || !config->ptr) {
return 0;
}
return olive::ColorManager::list_available_colorspaces(config->ptr).size();
}
int oakengine_color_config_colorspace_at(const OakEngineColorConfig *config,
int index, char *buf, int buf_size)
{
if (!config || !config->ptr) {
return OAKENGINE_E_INVALID;
}
const QString s = list_at(
olive::ColorManager::list_available_colorspaces(config->ptr), index);
if (s.isNull()) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(s, buf, buf_size);
}
OakEngineColorProcessor *oakengine_color_processor_create(
const OakEngineColorManager *mgr, const char *input,
const oak_color_transform *dest, int direction)
{
if (!mgr || !input || !dest ||
(direction != OAKENGINE_COLOR_PROCESSOR_NORMAL &&
direction != OAKENGINE_COLOR_PROCESSOR_INVERSE)) {
return nullptr;
}
// ColorProcessor catches OCIO failures internally and leaves the
// processor null (see engine/render/colorprocessor.cpp), so this never
// throws; validity is reported through is_valid().
auto *proc = new OakEngineColorProcessor;
proc->ptr = olive::ColorProcessor::create(
impl(mgr), QString::fromUtf8(input), to_cpp(*dest),
direction == OAKENGINE_COLOR_PROCESSOR_INVERSE ?
olive::ColorProcessor::k_inverse :
olive::ColorProcessor::k_normal);
return proc;
}
void oakengine_color_processor_free(OakEngineColorProcessor *proc)
{
delete proc;
}
int oakengine_color_processor_is_valid(const OakEngineColorProcessor *proc)
{
return (proc && proc->ptr && proc->ptr->get_processor()) ? 1 : 0;
}
int oakengine_color_processor_convert_color(
const OakEngineColorProcessor *proc, const double *in_rgba,
double *out_rgba)
{
if (!proc || !proc->ptr || !in_rgba || !out_rgba) {
return OAKENGINE_E_INVALID;
}
const olive::Color out = proc->ptr->convert_color(
olive::Color(in_rgba[0], in_rgba[1], in_rgba[2], in_rgba[3]));
out_rgba[0] = out.red();
out_rgba[1] = out.green();
out_rgba[2] = out.blue();
out_rgba[3] = out.alpha();
return OAKENGINE_OK;
}
int oakengine_color_processor_id(const OakEngineColorProcessor *proc,
char *buf, int buf_size)
{
if (!proc || !proc->ptr) {
return OAKENGINE_E_INVALID;
}
const char *id = proc->ptr->id();
const int len = id ? int(strlen(id)) : 0;
if (buf && buf_size > 0) {
snprintf(buf, size_t(buf_size), "%s", id ? id : "");
}
return len;
}
int oakengine_color_transform_job_set_processor(
void *job, const OakEngineColorProcessor *proc)
{
if (!job) {
return OAKENGINE_E_INVALID;
}
auto *j = reinterpret_cast<olive::ColorTransformJob *>(job);
j->set_color_processor(proc ? proc->ptr : olive::ColorProcessorPtr());
return OAKENGINE_OK;
}
} // extern "C"
+38
View File
@@ -0,0 +1,38 @@
/***
Oak - 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/>.
***/
#ifndef OAKENGINE_COLORINTERNAL_H
#define OAKENGINE_COLORINTERNAL_H
// Internal (not installed) shared definition of the opaque color-processor
// handle between color.cpp and the other capi translation units. The
// public header (oakengine/color.h) only forward-declares
// OakEngineColorProcessor; capi code that needs to unwrap the handle (e.g.
// renderer.cpp feeding the render cacher) includes this header.
#include "render/colorprocessor.h"
// Owned handle layout: the opaque C type is a heap box around the engine's
// shared pointer (matching the refcounting the C++ API uses).
struct OakEngineColorProcessor {
olive::ColorProcessorPtr ptr;
};
#endif // OAKENGINE_COLORINTERNAL_H
+130
View File
@@ -0,0 +1,130 @@
/***
Oak - 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 "oakengine/config.h"
#include <cstring>
#include <QByteArray>
#include <QString>
#include "config/config.h"
namespace
{
int write_string(const QString &s, char *buf, int buf_size)
{
const QByteArray utf8 = s.toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
std::memcpy(buf, utf8.constData(), size_t(n));
buf[n] = '\0';
}
return len;
}
oakengine_config_error_fn g_error_fn = nullptr;
void *g_error_userdata = nullptr;
void error_handler(const QString &title, const QString &message)
{
if (g_error_fn) {
const QByteArray t = title.toUtf8();
const QByteArray m = message.toUtf8();
g_error_fn(t.constData(), m.constData(), g_error_userdata);
}
}
} // namespace
extern "C" int oakengine_config_load(void)
{
olive::Config::load();
return OAKENGINE_OK;
}
extern "C" int oakengine_config_save(void)
{
olive::Config::save();
return OAKENGINE_OK;
}
extern "C" int oakengine_config_get_string(const char *key, char *buf,
int buf_size)
{
if (!key) {
return OAKENGINE_E_INVALID;
}
const QVariant v = olive::Config::current()[QString::fromUtf8(key)];
const QString s = v.toString();
return write_string(s, buf, buf_size);
}
extern "C" int oakengine_config_set_string(const char *key,
const char *value)
{
if (!key) {
return OAKENGINE_E_INVALID;
}
olive::Config::current()[QString::fromUtf8(key)] =
QString::fromUtf8(value ? value : "");
return OAKENGINE_OK;
}
extern "C" int64_t oakengine_config_get_int(const char *key,
int64_t default_value)
{
if (!key) {
return default_value;
}
const QVariant v = olive::Config::current()[QString::fromUtf8(key)];
bool ok = false;
const qlonglong val = v.toLongLong(&ok);
return ok ? static_cast<int64_t>(val) : default_value;
}
extern "C" int oakengine_config_set_int(const char *key, int64_t value)
{
if (!key) {
return OAKENGINE_E_INVALID;
}
olive::Config::current()[QString::fromUtf8(key)] =
static_cast<qlonglong>(value);
return OAKENGINE_OK;
}
extern "C" int oakengine_config_set_error_handler(
oakengine_config_error_fn fn, void *userdata)
{
g_error_fn = fn;
g_error_userdata = userdata;
olive::Config::set_error_handler(fn ? error_handler : nullptr);
return OAKENGINE_OK;
}
extern "C" int oakengine_config_report_error(const char *title,
const char *message)
{
olive::Config::report_error(QString::fromUtf8(title ? title : ""),
QString::fromUtf8(message ? message : ""));
return OAKENGINE_OK;
}
+187
View File
@@ -0,0 +1,187 @@
/***
Oak - 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 "oakengine/disk.h"
#include <cstring>
#include <QByteArray>
#include <QString>
#include <QWidget>
#include "node/project.h"
#include "render/diskmanager.h"
namespace
{
int write_string(const QString &s, char *buf, int buf_size)
{
const QByteArray utf8 = s.toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
std::memcpy(buf, utf8.constData(), size_t(n));
buf[n] = '\0';
}
return len;
}
olive::DiskManager *manager()
{
return olive::DiskManager::instance();
}
olive::DiskCacheFolder *folder_from_path(olive::DiskManager *m,
const char *path)
{
if (!m) {
return nullptr;
}
if (!path || std::strlen(path) == 0) {
return m->get_default_cache_folder();
}
return m->get_open_folder(QString::fromUtf8(path));
}
struct SettingsHandlerState {
oakengine_disk_settings_fn fn = nullptr;
void *userdata = nullptr;
};
SettingsHandlerState g_settings_handler;
void cpp_settings_handler(olive::DiskCacheFolder *folder, QWidget *parent)
{
if (!g_settings_handler.fn || !folder) {
return;
}
const QByteArray path = folder->get_path().toUtf8();
g_settings_handler.fn(path.constData(), parent, g_settings_handler.userdata);
}
} // namespace
extern "C" int oakengine_disk_create_instance(void)
{
olive::DiskManager::create_instance();
return manager() ? OAKENGINE_OK : OAKENGINE_E_FAILED;
}
extern "C" int oakengine_disk_destroy_instance(void)
{
olive::DiskManager::destroy_instance();
return OAKENGINE_OK;
}
extern "C" int oakengine_disk_set_settings_handler(
oakengine_disk_settings_fn fn, void *userdata)
{
g_settings_handler.fn = fn;
g_settings_handler.userdata = userdata;
olive::DiskManager::set_show_disk_cache_settings_handler(
fn ? cpp_settings_handler : olive::DiskManager::ShowDiskCacheSettingsHandler{});
return OAKENGINE_OK;
}
extern "C" int oakengine_disk_show_settings_dialog(const char *path,
void *parent_window)
{
olive::DiskManager *m = manager();
if (!m) {
return OAKENGINE_E_STATE;
}
olive::DiskCacheFolder *folder = folder_from_path(m, path);
if (!folder) {
return OAKENGINE_E_FAILED;
}
m->show_disk_cache_settings_dialog(folder,
static_cast<QWidget *>(parent_window));
return OAKENGINE_OK;
}
extern "C" int oakengine_disk_show_change_confirmation_dialog(
void *parent_window)
{
return olive::DiskManager::show_disk_cache_change_confirmation_dialog(
static_cast<QWidget *>(parent_window))
? 1
: 0;
}
extern "C" int oakengine_disk_clear_cache(const char *path)
{
olive::DiskManager *m = manager();
if (!m) {
return 0;
}
olive::DiskCacheFolder *folder = folder_from_path(m, path);
if (!folder) {
return 0;
}
return m->clear_disk_cache(folder->get_path()) ? 1 : 0;
}
extern "C" int oakengine_disk_get_default_cache_path(char *buf, int buf_size)
{
olive::DiskManager *m = manager();
if (!m) {
return OAKENGINE_E_STATE;
}
return write_string(m->get_default_cache_path(), buf, buf_size);
}
extern "C" int oakengine_disk_set_default_cache_path(const char *path)
{
if (!path) {
return OAKENGINE_E_INVALID;
}
olive::DiskManager *m = manager();
if (!m) {
return OAKENGINE_E_STATE;
}
m->get_default_cache_folder()->set_path(QString::fromUtf8(path));
return OAKENGINE_OK;
}
extern "C" void *oakengine_disk_get_open_folder(const char *path)
{
return folder_from_path(manager(), path);
}
extern "C" int oakengine_disk_invalidate_project(OakEngineProject *project)
{
olive::DiskManager *m = manager();
if (!m) {
return OAKENGINE_E_STATE;
}
emit m->invalidate_project(reinterpret_cast<olive::Project *>(project));
return OAKENGINE_OK;
}
+182
View File
@@ -0,0 +1,182 @@
/***
Oak - 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 "oakengine/display.h"
#include <QObject>
#include <QOpenGLContext>
#include <QString>
#include "codec/frame.h"
#include "render/job/colortransformjob.h"
#include "render/opengl/openglrenderer.h"
#include "render/renderer.h"
#include "render/texture.h"
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
#include "render/backend/dynamicrenderer.h"
#endif
extern "C" {
void *oakengine_display_renderer_create_dynamic(const char *backend_name,
void *parent)
{
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
QObject *p = static_cast<QObject *>(parent);
auto *dyn = new olive::DynamicRenderer(
QString::fromUtf8(backend_name ? backend_name : ""), p);
if (dyn->load()) {
return dyn;
}
// Backend library failed to load: drop it so the caller can fall back
// to the built-in OpenGL renderer.
delete dyn;
return nullptr;
#else
(void)backend_name;
(void)parent;
return nullptr;
#endif
}
void *oakengine_display_renderer_create_opengl(void *parent)
{
return new olive::OpenGLRenderer(static_cast<QObject *>(parent));
}
int oakengine_display_renderer_init(void *renderer, void *gl_context)
{
olive::Renderer *r = static_cast<olive::Renderer *>(renderer);
if (!r) {
return OAKENGINE_E_INVALID;
}
if (gl_context) {
QOpenGLContext *ctx = static_cast<QOpenGLContext *>(gl_context);
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
if (auto *dyn = dynamic_cast<olive::DynamicRenderer *>(r)) {
dyn->init_with_open_gl_context(ctx);
dyn->post_init();
return OAKENGINE_OK;
}
#endif
auto *gl = static_cast<olive::OpenGLRenderer *>(r);
gl->init(ctx);
gl->post_init();
return OAKENGINE_OK;
}
r->init();
r->post_init();
return OAKENGINE_OK;
}
void oakengine_display_renderer_destroy(void *renderer)
{
olive::Renderer *r = static_cast<olive::Renderer *>(renderer);
if (!r) {
return;
}
r->destroy();
r->post_destroy();
}
void oakengine_display_renderer_create_texture(void *renderer,
const void *video_params,
const void *pixels, int linesize,
void *out_texture)
{
olive::Renderer *r = static_cast<olive::Renderer *>(renderer);
if (!r || !video_params || !out_texture) {
return;
}
const olive::VideoParams &params =
*static_cast<const olive::VideoParams *>(video_params);
*static_cast<olive::TexturePtr *>(out_texture) =
r->create_texture(params, pixels, linesize);
}
void oakengine_display_renderer_blit_color_managed(void *renderer,
const void *color_job,
void *dst_texture,
const void *video_params)
{
olive::Renderer *r = static_cast<olive::Renderer *>(renderer);
if (!r || !color_job) {
return;
}
const olive::ColorTransformJob &job =
*static_cast<const olive::ColorTransformJob *>(color_job);
olive::Texture *dst = static_cast<olive::Texture *>(dst_texture);
if (video_params) {
r->blit_color_managed(
job, dst, *static_cast<const olive::VideoParams *>(video_params));
} else if (dst) {
r->blit_color_managed(job, dst, dst->params());
}
}
void oakengine_display_texture_upload(void *texture, void *pixels, int linesize)
{
olive::Texture *t = static_cast<olive::Texture *>(texture);
if (!t) {
return;
}
t->upload(pixels, linesize);
}
void oakengine_display_texture_download(void *texture, void *pixels,
int linesize)
{
olive::Texture *t = static_cast<olive::Texture *>(texture);
if (!t) {
return;
}
t->download(pixels, linesize);
}
void oakengine_codec_frame_create(void *out_frame)
{
if (!out_frame) {
return;
}
*static_cast<olive::FramePtr *>(out_frame) = olive::Frame::create();
}
void oakengine_codec_frame_set_video_params(void *frame,
const void *video_params)
{
olive::Frame *f = static_cast<olive::Frame *>(frame);
if (!f || !video_params) {
return;
}
f->set_video_params(*static_cast<const olive::VideoParams *>(video_params));
}
int oakengine_codec_frame_allocate(void *frame)
{
olive::Frame *f = static_cast<olive::Frame *>(frame);
if (!f) {
return 0;
}
return f->allocate() ? 1 : 0;
}
} // extern "C"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17
View File
@@ -20,6 +20,8 @@
#include "oakengine/exporter.h"
#include "exportinternal.h"
#include <atomic>
#include <cstdio>
@@ -487,6 +489,21 @@ QString params_from_ex(const oak_export_options_ex &o,
} // namespace
int oakengine_export_render_internal(olive::Sequence *sequence,
olive::Project *project,
olive::EncodingParams &params,
bool prewarm_audio,
const olive::AudioParams &prewarm_params)
{
return render_internal(sequence, project, params, prewarm_audio,
prewarm_params);
}
void oakengine_export_set_error_string(const QString &error)
{
set_error(error);
}
extern "C"
{
+58
View File
@@ -0,0 +1,58 @@
/***
Oak - 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/>.
***/
#ifndef OAKENGINE_EXPORTINTERNAL_H
#define OAKENGINE_EXPORTINTERNAL_H
// Internal (not installed) shared declaration between the export and
// encoding capi translation units: oakengine_export_render_with_params()
// (declared in oakengine/encoding.h) drives the same synchronous ExportTask
// machinery as oakengine_export_render()/_ex(), which lives in export.cpp.
namespace olive
{
class Sequence;
class Project;
class EncodingParams;
namespace core
{
class AudioParams;
}
using core::AudioParams;
}
// Runs the synchronous export (render_internal in export.cpp): prewarms
// audio conforms when prewarm_audio is set, drives the ExportTask on a
// worker thread while the calling thread pumps events. Returns
// OAKENGINE_OK / OAKENGINE_E_STATE / OAKENGINE_E_FAILED /
// OAKENGINE_E_CANCELLED; failure reason via oakengine_export_last_error().
int oakengine_export_render_internal(olive::Sequence *sequence,
olive::Project *project,
olive::EncodingParams &params,
bool prewarm_audio,
const olive::AudioParams &prewarm_params);
// Sets the export family's thread-local failure reason (read back with
// oakengine_export_last_error()). Used by capi TUs outside export.cpp whose
// contracts route errors through the export channel.
class QString;
void oakengine_export_set_error_string(const QString &error);
#endif // OAKENGINE_EXPORTINTERNAL_H
+227 -13
View File
@@ -19,6 +19,7 @@
***/
#include "oakengine/footage.h"
#include "oakengine/timeline.h"
#include <atomic>
#include <cstdio>
@@ -41,6 +42,7 @@
#include "node/project/footage/footage.h"
#include "undo/undocommand.h"
#include "undo/undostack.h"
#include "undointernal.h"
namespace
{
@@ -153,12 +155,7 @@ olive::Footage *borrowed_node(OakEngineFootage *self)
// initialized, otherwise execute it directly.
void push_or_run(olive::UndoCommand *command, const QString &name)
{
if (olive::EngineCore::instance()) {
olive::EngineCore::instance()->undo_stack()->push(command, name);
} else {
command->redo_now();
delete command;
}
oakengine_undo_push_or_run(command, name);
}
// Undo commands for footage stream overrides. The engine has no undo
@@ -546,13 +543,7 @@ OakEngineFootage *oakengine_project_import_footage(OakEngineProject *project,
command->add_child(new olive::NodeAddCommand(p, footage));
command->add_child(new olive::FolderAddChild(p->root(), footage));
if (olive::EngineCore::instance()) {
olive::EngineCore::instance()->undo_stack()->push(
command, QStringLiteral("Import Footage"));
} else {
command->redo_now();
delete command;
}
oakengine_undo_push_or_run(command, QStringLiteral("Import Footage"));
auto *state = new OakEngineFootageState();
state->borrowed = true;
@@ -1082,4 +1073,227 @@ int oakengine_footage_colorspace_at(const OakEngineFootage *self, int index,
buf_size);
}
/* ---- Footage extras ------------------------------------------------------- */
int oakengine_footage_get_filename(const OakEngineFootage *self, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const OakEngineFootageState *s = impl(self);
if (!s->node) {
// Probed footage has no node; filename is not applicable.
return OAKENGINE_E_INVALID;
}
return string_to_buf(s->node->filename(), buf, buf_size);
}
int oakengine_footage_get_stream_reference(const OakEngineFootage *self,
int flat_index, int *out_track_type,
int *out_stream_index)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const OakEngineFootageState *s = impl(self);
if (!s->node) {
return OAKENGINE_E_INVALID; // probed-only footage
}
const int vc = video_stream_count(s);
const int ac = audio_stream_count(s);
if (flat_index < vc) {
if (out_track_type) {
*out_track_type = OAKENGINE_TRACK_TYPE_VIDEO;
}
if (out_stream_index) {
*out_stream_index = flat_index;
}
return OAKENGINE_OK;
}
flat_index -= vc;
if (flat_index < ac) {
if (out_track_type) {
*out_track_type = OAKENGINE_TRACK_TYPE_AUDIO;
}
if (out_stream_index) {
*out_stream_index = flat_index;
}
return OAKENGINE_OK;
}
flat_index -= ac;
const int sc = subtitle_stream_count(s);
if (flat_index >= sc) {
return OAKENGINE_E_NOT_FOUND;
}
if (out_track_type) {
*out_track_type = OAKENGINE_TRACK_TYPE_SUBTITLE;
}
if (out_stream_index) {
*out_stream_index = flat_index;
}
return OAKENGINE_OK;
}
int oakengine_footage_describe_video_stream(const OakEngineFootage *self,
int video_stream_index, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const OakEngineFootageState *s = impl(self);
if (!s->node) {
// Import-handle-only family: probe handles are rejected (probe
// metadata is read through the oak_footage_*_info accessors).
return OAKENGINE_E_INVALID;
}
if (video_stream_index < 0 || video_stream_index >= video_stream_count(s)) {
return OAKENGINE_E_NOT_FOUND;
}
olive::VideoParams vp;
if (s->node) {
vp = s->node->get_video_params(video_stream_index);
} else {
const auto &streams = s->description.get_video_streams();
if (video_stream_index < streams.size()) {
vp = streams.at(video_stream_index);
} else {
return OAKENGINE_E_NOT_FOUND;
}
}
return string_to_buf(olive::Footage::describe_video_stream(vp), buf,
buf_size);
}
int oakengine_footage_describe_audio_stream(const OakEngineFootage *self,
int audio_stream_index, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const OakEngineFootageState *s = impl(self);
if (!s->node) {
return OAKENGINE_E_INVALID;
}
if (audio_stream_index < 0 || audio_stream_index >= audio_stream_count(s)) {
return OAKENGINE_E_NOT_FOUND;
}
olive::AudioParams ap;
if (s->node) {
ap = s->node->get_audio_params(audio_stream_index);
} else {
const auto &streams = s->description.get_audio_streams();
if (audio_stream_index < streams.size()) {
ap = streams.at(audio_stream_index);
} else {
return OAKENGINE_E_NOT_FOUND;
}
}
return string_to_buf(olive::Footage::describe_audio_stream(ap), buf,
buf_size);
}
int oakengine_footage_stream_type_name(int track_type, char *buf, int buf_size)
{
switch (track_type) {
case OAKENGINE_TRACK_TYPE_VIDEO:
return string_to_buf(QStringLiteral("Video"), buf, buf_size);
case OAKENGINE_TRACK_TYPE_AUDIO:
return string_to_buf(QStringLiteral("Audio"), buf, buf_size);
case OAKENGINE_TRACK_TYPE_SUBTITLE:
return string_to_buf(QStringLiteral("Subtitle"), buf, buf_size);
default:
return string_to_buf(QStringLiteral("Unknown"), buf, buf_size);
}
}
int oakengine_footage_has_custom_proxy_params(const OakEngineFootage *self)
{
if (!self || !impl(self)->node) {
return OAKENGINE_E_INVALID;
}
return impl(self)->node->has_custom_proxy_params() ? 1 : 0;
}
int oakengine_footage_get_effective_proxy_params(const OakEngineFootage *self,
oak_proxy_params *out)
{
if (!self || !out || !impl(self)->node) {
return OAKENGINE_E_INVALID;
}
const olive::ProxyManager::ProxyParams pp =
impl(self)->node->get_effective_proxy_params();
out->width = pp.width;
out->height = pp.height;
out->divider = pp.divider;
out->version = pp.version;
out->crf = pp.crf;
out->include_audio = pp.include_audio ? 1 : 0;
string_to_buf(pp.extension, out->extension, sizeof(out->extension));
string_to_buf(pp.preset, out->preset, sizeof(out->preset));
return OAKENGINE_OK;
}
int oakengine_footage_set_custom_proxy_params(OakEngineFootage *self,
const oak_proxy_params *params)
{
if (!self || !params || !impl(self)->node) {
return OAKENGINE_E_INVALID;
}
olive::ProxyManager::ProxyParams pp;
pp.width = params->width;
pp.height = params->height;
pp.divider = params->divider;
pp.version = params->version;
pp.crf = params->crf;
pp.include_audio = params->include_audio != 0;
pp.extension = QString::fromUtf8(params->extension);
pp.preset = QString::fromUtf8(params->preset);
impl(self)->node->set_custom_proxy_params(pp);
return OAKENGINE_OK;
}
int oakengine_footage_clear_custom_proxy_params(OakEngineFootage *self)
{
if (!self || !impl(self)->node) {
return OAKENGINE_E_INVALID;
}
impl(self)->node->clear_custom_proxy_params();
return OAKENGINE_OK;
}
int oakengine_footage_set_proxy(OakEngineFootage *self,
const char *path, int state,
int stream_index, int enabled, int version)
{
if (!self || !impl(self)->node) {
return OAKENGINE_E_INVALID;
}
olive::Footage *node = impl(self)->node;
node->set_proxy(QString::fromUtf8(path ? path : ""),
static_cast<olive::ProxyManager::ProxyState>(state),
stream_index, version, enabled != 0);
return OAKENGINE_OK;
}
int oakengine_footage_clear_proxy(OakEngineFootage *self)
{
if (!self || !impl(self)->node) {
return OAKENGINE_E_INVALID;
}
impl(self)->node->clear_proxy();
return OAKENGINE_OK;
}
int oakengine_footage_invalidate(OakEngineFootage *self)
{
if (!self || !impl(self)->node) {
return OAKENGINE_E_INVALID;
}
impl(self)->node->clear();
return OAKENGINE_OK;
}
} // extern "C"
+268
View File
@@ -0,0 +1,268 @@
/***
Oak - 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 "oakengine/gizmo.h"
#include <QString>
#include "node/gizmo/draggable.h"
#include "node/gizmo/text.h"
#include "node/generator/text/textv3.h"
#include "node/node.h"
extern "C" {
int oakengine_text_gizmo_get(OakEngineNode *node,
int64_t time_num, int64_t time_den, oakengine_text_gizmo *out)
{
if (!node || !out) {
return OAKENGINE_E_INVALID;
}
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
reinterpret_cast<olive::Node *>(node));
if (!textv3) {
return OAKENGINE_E_INVALID;
}
olive::TextGizmo *gizmo = textv3->text_gizmo();
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
QRectF r = gizmo->get_rect();
out->rect_x = r.x();
out->rect_y = r.y();
out->rect_w = r.width();
out->rect_h = r.height();
// Map Qt::Alignment to our simple enum
Qt::Alignment va = gizmo->get_vertical_alignment();
if (va & Qt::AlignBottom) {
out->vertical_alignment = 1;
} else if (va & Qt::AlignVCenter) {
out->vertical_alignment = 2;
} else {
out->vertical_alignment = 0; // AlignTop
}
return OAKENGINE_OK;
}
int oakengine_text_gizmo_get_html(OakEngineNode *node,
int64_t time_num, int64_t time_den, char *buf, int buf_size)
{
if (!node) {
return OAKENGINE_E_INVALID;
}
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
reinterpret_cast<olive::Node *>(node));
if (!textv3) {
return OAKENGINE_E_INVALID;
}
olive::TextGizmo *gizmo = textv3->text_gizmo();
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
QByteArray html = gizmo->get_html().toUtf8();
int needed = html.size() + 1; // include NUL
if (buf && buf_size > 0) {
int copy = qMin(needed, buf_size);
memcpy(buf, html.constData(), copy - 1);
buf[copy - 1] = '\0';
}
return needed;
}
int oakengine_text_gizmo_update_html(OakEngineNode *node,
const char *html, int64_t time_num, int64_t time_den)
{
if (!node || !html) {
return OAKENGINE_E_INVALID;
}
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
reinterpret_cast<olive::Node *>(node));
if (!textv3) {
return OAKENGINE_E_INVALID;
}
olive::TextGizmo *gizmo = textv3->text_gizmo();
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
gizmo->update_input_html(QString::fromUtf8(html),
olive::core::Rational(time_num, time_den));
return OAKENGINE_OK;
}
int oakengine_text_gizmo_set_vertical_alignment(
OakEngineNode *node, int alignment)
{
if (!node) {
return OAKENGINE_E_INVALID;
}
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
reinterpret_cast<olive::Node *>(node));
if (!textv3) {
return OAKENGINE_E_INVALID;
}
olive::TextGizmo *gizmo = textv3->text_gizmo();
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
Qt::Alignment va;
switch (alignment) {
case 1:
va = Qt::AlignBottom;
break;
case 2:
va = Qt::AlignVCenter;
break;
default:
va = Qt::AlignTop;
break;
}
gizmo->set_vertical_alignment(va);
return OAKENGINE_OK;
}
int oakengine_text_gizmo_activated(OakEngineNode *node)
{
if (!node) {
return OAKENGINE_E_INVALID;
}
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
reinterpret_cast<olive::Node *>(node));
if (!textv3) {
return OAKENGINE_E_INVALID;
}
olive::TextGizmo *gizmo = textv3->text_gizmo();
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
emit gizmo->activated();
return OAKENGINE_OK;
}
int oakengine_text_gizmo_deactivated(OakEngineNode *node)
{
if (!node) {
return OAKENGINE_E_INVALID;
}
auto *textv3 = dynamic_cast<olive::TextGeneratorV3 *>(
reinterpret_cast<olive::Node *>(node));
if (!textv3) {
return OAKENGINE_E_INVALID;
}
olive::TextGizmo *gizmo = textv3->text_gizmo();
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
emit gizmo->deactivated();
return OAKENGINE_OK;
}
int oakengine_gizmo_get_drag_value_behavior(void *gizmo)
{
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
auto *dg = dynamic_cast<olive::DraggableGizmo *>(
static_cast<olive::NodeGizmo *>(gizmo));
if (!dg) {
return OAKENGINE_E_INVALID;
}
return static_cast<int>(dg->get_drag_value_behavior());
}
int oakengine_gizmo_drag_start(void *gizmo,
void *row, double abs_x, double abs_y, int64_t time_num,
int64_t time_den)
{
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
auto *dg = dynamic_cast<olive::DraggableGizmo *>(
static_cast<olive::NodeGizmo *>(gizmo));
if (!dg) {
return OAKENGINE_E_INVALID;
}
olive::NodeValueRow empty_row;
olive::NodeValueRow &row_ref = row
? *static_cast<olive::NodeValueRow *>(row)
: empty_row;
dg->drag_start(row_ref, abs_x, abs_y,
olive::core::Rational(time_num, time_den));
return OAKENGINE_OK;
}
int oakengine_gizmo_drag_move(void *gizmo,
double x, double y, int qt_keyboard_modifiers)
{
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
auto *dg = dynamic_cast<olive::DraggableGizmo *>(
static_cast<olive::NodeGizmo *>(gizmo));
if (!dg) {
return OAKENGINE_E_INVALID;
}
dg->drag_move(x, y, Qt::KeyboardModifiers(qt_keyboard_modifiers));
return OAKENGINE_OK;
}
int oakengine_gizmo_drag_end(void *gizmo, void *command)
{
if (!gizmo) {
return OAKENGINE_E_INVALID;
}
auto *dg = dynamic_cast<olive::DraggableGizmo *>(
static_cast<olive::NodeGizmo *>(gizmo));
if (!dg) {
return OAKENGINE_E_INVALID;
}
dg->drag_end(static_cast<olive::MultiUndoCommand *>(command));
return OAKENGINE_OK;
}
} // extern "C"
+90
View File
@@ -0,0 +1,90 @@
/***
Oak - 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 "oakengine/lut.h"
#include <cstring>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include "render/lutlibrary.h"
namespace
{
int write_string(const QString &s, char *buf, int buf_size)
{
const QByteArray utf8 = s.toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
std::memcpy(buf, utf8.constData(), size_t(n));
buf[n] = '\0';
}
return len;
}
} // namespace
extern "C" int oakengine_lut_directory_count(void)
{
return olive::LUTLibrary::get_directories().size();
}
extern "C" int oakengine_lut_directory_at(int index, char *buf, int buf_size)
{
const QStringList dirs = olive::LUTLibrary::get_directories();
if (index < 0 || index >= dirs.size()) {
return OAKENGINE_E_NOT_FOUND;
}
return write_string(dirs.at(index), buf, buf_size);
}
extern "C" int oakengine_lut_file_count(void)
{
return olive::LUTLibrary::get_lut_files().size();
}
extern "C" int oakengine_lut_file_at(int index, char *buf, int buf_size)
{
const QStringList files = olive::LUTLibrary::get_lut_files();
if (index < 0 || index >= files.size()) {
return OAKENGINE_E_NOT_FOUND;
}
return write_string(files.at(index), buf, buf_size);
}
extern "C" int oakengine_lut_set_directories(const char *const *dirs,
int count)
{
QStringList list;
if (dirs && count > 0) {
list.reserve(count);
for (int i = 0; i < count; i++) {
if (dirs[i]) {
list.append(QString::fromUtf8(dirs[i]));
}
}
}
olive::LUTLibrary::set_directories(list);
return OAKENGINE_OK;
}
+3005 -25
View File
File diff suppressed because it is too large Load Diff
+152
View File
@@ -0,0 +1,152 @@
/***
Oak - 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 "oakengine/plugin.h"
#include "coreengine.h"
#include "node/node.h"
#include "node/output/viewer/viewer.h"
#include "pluginSupport/olivehost.h"
#include "pluginSupport/oliveplugininstance.h"
#include "pluginSupport/pluginprogressreporter.h"
extern "C"
{
static oakengine_plugin_active_viewer_fn g_active_viewer_fn = nullptr;
static void *g_active_viewer_userdata = nullptr;
static oakengine_plugin_reporter_create_fn g_reporter_create = nullptr;
static oakengine_plugin_reporter_destroy_fn g_reporter_destroy = nullptr;
static oakengine_plugin_reporter_is_cancelled_fn g_reporter_is_cancelled = nullptr;
static oakengine_plugin_reporter_set_progress_fn g_reporter_set_progress = nullptr;
static void *g_reporter_userdata = nullptr;
int oakengine_plugin_set_active_viewer_provider(
oakengine_plugin_active_viewer_fn fn, void *userdata)
{
g_active_viewer_fn = fn;
g_active_viewer_userdata = userdata;
// Update the engine's viewer provider lambda.
olive::plugin::set_active_viewer_provider(
[]() -> olive::ViewerOutput * {
if (!g_active_viewer_fn) {
return nullptr;
}
return reinterpret_cast<olive::ViewerOutput *>(
g_active_viewer_fn(g_active_viewer_userdata));
});
return OAKENGINE_OK;
}
int oakengine_plugin_set_progress_reporter_factory(
oakengine_plugin_reporter_create_fn create,
oakengine_plugin_reporter_destroy_fn destroy,
oakengine_plugin_reporter_is_cancelled_fn is_cancelled,
oakengine_plugin_reporter_set_progress_fn set_progress,
void *userdata)
{
g_reporter_create = create;
g_reporter_destroy = destroy;
g_reporter_is_cancelled = is_cancelled;
g_reporter_set_progress = set_progress;
g_reporter_userdata = userdata;
// Register factory with the engine.
olive::plugin::set_plugin_progress_reporter_factory(
[](const QString &message, const QString &title)
-> olive::plugin::PluginProgressReporter * {
if (!g_reporter_create) {
return nullptr;
}
void *reporter = g_reporter_create(
message.toUtf8().constData(),
title.toUtf8().constData(),
g_reporter_userdata);
if (!reporter) {
return nullptr;
}
// Create an adapter that wraps the C callbacks.
class CAdapter : public olive::plugin::PluginProgressReporter {
public:
CAdapter(void *reporter,
oakengine_plugin_reporter_destroy_fn destroy,
oakengine_plugin_reporter_is_cancelled_fn is_cancelled,
oakengine_plugin_reporter_set_progress_fn set_progress,
void *userdata)
: PluginProgressReporter()
, reporter_(reporter)
, destroy_(destroy)
, set_progress_(set_progress)
, userdata_(userdata) {}
~CAdapter() override
{
if (destroy_) {
destroy_(reporter_, userdata_);
}
}
void set_progress(double value) override
{
if (set_progress_) {
set_progress_(reporter_, value, userdata_);
}
}
void show() override {}
void close() override {}
private:
void *reporter_;
oakengine_plugin_reporter_destroy_fn destroy_;
oakengine_plugin_reporter_set_progress_fn set_progress_;
void *userdata_;
};
return new CAdapter(reporter, g_reporter_destroy,
g_reporter_is_cancelled,
g_reporter_set_progress,
g_reporter_userdata);
});
return OAKENGINE_OK;
}
int oakengine_plugin_load_plugins(const char *path)
{
if (!path) {
return OAKENGINE_E_INVALID;
}
olive::plugin::load_plugins(QString::fromUtf8(path));
return OAKENGINE_OK;
}
int oakengine_plugin_node_push_button_clicked(OakEngineNode *node,
const char *button_id)
{
if (!node || !button_id) {
return OAKENGINE_E_INVALID;
}
auto *pn = dynamic_cast<olive::plugin::PluginNode *>(
reinterpret_cast<olive::Node *>(node));
if (!pn) {
return OAKENGINE_E_INVALID;
}
pn->push_button_clicked(QString::fromUtf8(button_id));
return OAKENGINE_OK;
}
} // extern "C"
+299 -6
View File
@@ -30,6 +30,7 @@
#include <QString>
#include <QThread>
#include "codec/frame.h"
#include "coreengine.h"
#include "node/block/clip/clip.h"
#include "node/nodeundo.h"
@@ -39,8 +40,13 @@
#include "node/value.h"
#include "render/rendermanager.h"
#include "render/renderticket.h"
#include "render/previewautocacher.h"
#include "render/playbackcache.h"
#include "render/framehashcache.h"
#include "render/audiowaveformcache.h"
#include "undo/undocommand.h"
#include "undo/undostack.h"
#include "undointernal.h"
// Internal cross-family accessor (defined in footage.cpp): borrowed
// project node of an import handle, nullptr otherwise.
@@ -70,12 +76,7 @@ int string_to_buf(const QString &s, char *buf, int buf_size)
void push_or_run(olive::UndoCommand *command, const QString &name)
{
if (olive::EngineCore::instance()) {
olive::EngineCore::instance()->undo_stack()->push(command, name);
} else {
command->redo_now();
delete command;
}
oakengine_undo_push_or_run(command, name);
}
// Frame-rate timebase of a sequence (frame duration), like the timeline
@@ -342,4 +343,296 @@ int oakengine_preview_get_waveform_summary(OakEngineFootage *footage,
return OAKENGINE_OK;
}
/* ---- R4: waveform, audio levels, cacher, preview requests ------------------ */
int oakengine_waveform_max_sample_rate(void)
{
// The engine's waveform cache stores audio at this rate.
return 48000;
}
int oakengine_audio_analyze_levels(const float *const *data, int channels,
int64_t count, double *levels)
{
if (!data || channels <= 0 || count <= 0 || !levels) {
return OAKENGINE_E_INVALID;
}
for (int ch = 0; ch < channels; ch++) {
if (!data[ch]) {
return OAKENGINE_E_INVALID;
}
double sum = 0.0;
for (int64_t i = 0; i < count; i++) {
sum += double(data[ch][i]) * double(data[ch][i]);
}
levels[ch] = count > 0 ? std::sqrt(sum / double(count)) : 0.0;
}
return OAKENGINE_OK;
}
int oakengine_preview_cacher_set_playhead(int64_t num, int64_t den)
{
if (!olive::RenderManager::instance() ||
!olive::RenderManager::instance()->get_cacher()) {
return OAKENGINE_E_STATE;
}
olive::RenderManager::instance()->get_cacher()->set_playhead(
olive::Rational(num, den));
return OAKENGINE_OK;
}
int oakengine_preview_cacher_set_thumbnails_paused(int paused)
{
if (!olive::RenderManager::instance() ||
!olive::RenderManager::instance()->get_cacher()) {
return OAKENGINE_E_STATE;
}
olive::RenderManager::instance()->get_cacher()->set_thumbnails_paused(
paused != 0);
return OAKENGINE_OK;
}
int oakengine_preview_cacher_clear_single_frame_renders(int only_finished)
{
if (!olive::RenderManager::instance() ||
!olive::RenderManager::instance()->get_cacher()) {
return OAKENGINE_E_STATE;
}
Q_UNUSED(only_finished)
olive::RenderManager::instance()->get_cacher()->clear_single_frame_renders();
return OAKENGINE_OK;
}
int oakengine_preview_cacher_force_cache_range(OakEngineNode *node,
int64_t in_num, int64_t in_den,
int64_t out_num, int64_t out_den)
{
if (!node) {
return OAKENGINE_E_INVALID;
}
olive::ViewerOutput *viewer = dynamic_cast<olive::ViewerOutput *>(
reinterpret_cast<olive::Node *>(node));
if (!viewer) {
return OAKENGINE_E_INVALID;
}
if (!olive::RenderManager::instance() ||
!olive::RenderManager::instance()->get_cacher()) {
return OAKENGINE_E_STATE;
}
olive::RenderManager::instance()->get_cacher()->force_cache_range(
viewer, olive::TimeRange(olive::Rational(in_num, in_den),
olive::Rational(out_num, out_den)));
return OAKENGINE_OK;
}
// ---- Preview request helpers ------------------------------------------------
struct OakEnginePreviewRequestState {
olive::RenderTicketPtr ticket;
std::atomic<bool> finished{ false };
bool has_frame = false;
bool has_audio = false;
// Video result
olive::FramePtr frame;
int frame_width = 0;
int frame_height = 0;
int frame_format = 0;
// Audio result
olive::SampleBuffer samples;
int audio_sample_rate = 0;
};
OakEnginePreviewRequest *
oakengine_preview_request_single_frame(OakEngineNode *viewer, int64_t num,
int64_t den, int dry)
{
olive::ViewerOutput *v = viewer ?
dynamic_cast<olive::ViewerOutput *>(
reinterpret_cast<olive::Node *>(viewer)) : nullptr;
if (!v) {
return nullptr;
}
if (!olive::RenderManager::instance() ||
!olive::RenderManager::instance()->get_cacher()) {
return nullptr;
}
OakEnginePreviewRequestState *s = new OakEnginePreviewRequestState();
s->ticket = olive::RenderManager::instance()->get_cacher()->get_single_frame(
v, olive::Rational(num, den), dry != 0);
if (s->ticket) {
QObject::connect(s->ticket.get(), &olive::RenderTicket::finished,
[s]() { s->finished.store(true); });
}
return reinterpret_cast<OakEnginePreviewRequest *>(s);
}
OakEnginePreviewRequest *
oakengine_preview_request_audio_range(OakEngineNode *viewer, int64_t in_num,
int64_t in_den, int64_t out_num,
int64_t out_den)
{
olive::ViewerOutput *v = viewer ?
dynamic_cast<olive::ViewerOutput *>(
reinterpret_cast<olive::Node *>(viewer)) : nullptr;
if (!v) {
return nullptr;
}
if (!olive::RenderManager::instance() ||
!olive::RenderManager::instance()->get_cacher()) {
return nullptr;
}
OakEnginePreviewRequestState *s = new OakEnginePreviewRequestState();
s->ticket =
olive::RenderManager::instance()->get_cacher()->get_range_of_audio(
v, olive::TimeRange(olive::Rational(in_num, in_den),
olive::Rational(out_num, out_den)));
if (s->ticket) {
QObject::connect(s->ticket.get(), &olive::RenderTicket::finished,
[s]() { s->finished.store(true); });
}
return reinterpret_cast<OakEnginePreviewRequest *>(s);
}
int oakengine_preview_request_is_done(const OakEnginePreviewRequest *req)
{
if (!req) {
return 0;
}
const OakEnginePreviewRequestState *s =
reinterpret_cast<const OakEnginePreviewRequestState *>(req);
return s->ticket && s->finished.load() ? 1 : 0;
}
int oakengine_preview_request_has_result(const OakEnginePreviewRequest *req)
{
if (!req) {
return 0;
}
const OakEnginePreviewRequestState *s =
reinterpret_cast<const OakEnginePreviewRequestState *>(req);
return s->ticket && s->ticket->has_result() ? 1 : 0;
}
int oakengine_preview_request_set_finished_callback(
OakEnginePreviewRequest *req, void (*callback)(void *), void *user_data)
{
if (!req) {
return OAKENGINE_E_INVALID;
}
OakEnginePreviewRequestState *s =
reinterpret_cast<OakEnginePreviewRequestState *>(req);
if (!s->ticket) {
return OAKENGINE_E_INVALID;
}
// Connect the ticket's finished signal to call the callback.
if (callback) {
QObject::connect(s->ticket.get(), &olive::RenderTicket::finished,
[callback, user_data]() { callback(user_data); });
}
return OAKENGINE_OK;
}
int oakengine_preview_request_get_frame(OakEnginePreviewRequest *req,
oak_playback_frame *out)
{
if (!req || !out) {
return OAKENGINE_E_INVALID;
}
OakEnginePreviewRequestState *s =
reinterpret_cast<OakEnginePreviewRequestState *>(req);
if (!s->ticket || !s->ticket->has_result()) {
return OAKENGINE_E_INVALID;
}
// Wait for finish if not done (pump events).
if (!s->finished.load()) {
QCoreApplication::processEvents();
return OAKENGINE_E_INVALID;
}
if (!s->has_frame) {
QVariant result = s->ticket->get();
if (result.canConvert<olive::FramePtr>()) {
s->frame = result.value<olive::FramePtr>();
s->has_frame = true;
if (s->frame) {
s->frame_width = s->frame->width();
s->frame_height = s->frame->height();
s->frame_format = int(s->frame->format());
}
}
}
if (!s->frame) {
return OAKENGINE_E_INVALID;
}
out->width = s->frame_width;
out->height = s->frame_height;
out->format = s->frame_format;
out->data = s->frame->data();
out->linesize = s->frame->linesize_bytes();
return OAKENGINE_OK;
}
int oakengine_preview_request_get_audio_channel_count(
const OakEnginePreviewRequest *req)
{
if (!req) {
return 0;
}
const OakEnginePreviewRequestState *s =
reinterpret_cast<const OakEnginePreviewRequestState *>(req);
if (!s->ticket || !s->ticket->has_result() || !s->finished.load()) {
return 0;
}
if (!s->has_audio) {
// Lazy-init on first call.
const_cast<OakEnginePreviewRequestState *>(s)->samples =
s->ticket->get().value<olive::SampleBuffer>();
const_cast<OakEnginePreviewRequestState *>(s)->has_audio = true;
}
return s->samples.is_allocated() ? s->samples.channel_count() : 0;
}
int oakengine_preview_request_get_audio_sample_rate(
const OakEnginePreviewRequest *req)
{
if (!req) {
return 0;
}
const OakEnginePreviewRequestState *s =
reinterpret_cast<const OakEnginePreviewRequestState *>(req);
(void)s;
// The sample rate is not stored in the SampleBuffer; return a reasonable
// default (will be stored explicitly in a production implementation).
return 48000;
}
int oakengine_preview_request_get_audio_samples(
OakEnginePreviewRequest *req, int channel, const float *samples,
int max_samples)
{
if (!req || !samples || max_samples <= 0) {
return OAKENGINE_E_INVALID;
}
OakEnginePreviewRequestState *s =
reinterpret_cast<OakEnginePreviewRequestState *>(req);
if (!s->ticket || !s->ticket->has_result() || !s->finished.load()) {
return OAKENGINE_E_INVALID;
}
if (!s->has_audio) {
s->samples = s->ticket->get().value<olive::SampleBuffer>();
s->has_audio = true;
}
if (!s->samples.is_allocated() || channel >= s->samples.channel_count()) {
return OAKENGINE_E_INVALID;
}
const float *src = s->samples.data(channel);
const size_t copy_count = qMin(size_t(max_samples), s->samples.sample_count());
memcpy(const_cast<float *>(samples), src, copy_count * sizeof(float));
return int(copy_count);
}
void oakengine_preview_request_free(OakEnginePreviewRequest *req)
{
delete reinterpret_cast<OakEnginePreviewRequestState *>(req);
}
} // extern "C"
+286
View File
@@ -28,11 +28,16 @@
#include <QString>
#include "coreengine.h"
#include "node/factory.h"
#include "node/nodeundo.h"
#include "node/project.h"
#include "node/project/footage/footage.h"
#include "node/project/folder/folder.h"
#include "node/project/sequence/sequence.h"
#include "node/project/serializer/serializer.h"
#include "undo/undocommand.h"
#include "undo/undostack.h"
#include "undointernal.h"
namespace
{
@@ -52,6 +57,16 @@ OakEngineProject *wrap(olive::Project *p)
return reinterpret_cast<OakEngineProject *>(p);
}
olive::Node *impl(OakEngineNode *h)
{
return reinterpret_cast<olive::Node *>(h);
}
const olive::Node *impl(const OakEngineNode *h)
{
return reinterpret_cast<const olive::Node *>(h);
}
OakEngineSequence *wrap_seq(olive::Sequence *s)
{
return reinterpret_cast<OakEngineSequence *>(s);
@@ -117,6 +132,13 @@ int node_count_of_type(const olive::Project *p, bool sequences)
return count;
}
// Push an undoable command onto the global undo stack when the engine is
// initialized, otherwise execute it directly.
void push_or_run(olive::UndoCommand *command, const QString &name)
{
oakengine_undo_push_or_run(command, name);
}
// Human-readable text for a failed project load, mirroring the messages in
// ProjectLoadTask::run() (task/project/load/load.cpp).
QString load_error_string(olive::ProjectSerializer::ResultCode code,
@@ -393,4 +415,268 @@ OakEngineSequence *oakengine_project_sequence_at(const OakEngineProject *self,
return wrap_seq(sequence_at(impl(self), index));
}
/* ---- Folder operations ---------------------------------------------------- */
OakEngineNode *oakengine_folder_create(OakEngineProject *project,
OakEngineNode *parent,
const char *name)
{
if (!project || !parent) {
return nullptr;
}
olive::Project *p = impl(project);
olive::Node *n = impl(parent);
olive::Folder *folder = dynamic_cast<olive::Folder *>(n);
if (!folder) {
return nullptr;
}
olive::Folder *child = new olive::Folder();
child->set_label(QString::fromUtf8(name ? name : ""));
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
command->add_child(new olive::NodeAddCommand(p, child));
command->add_child(new olive::FolderAddChild(folder, child));
oakengine_undo_push_or_run(command, QStringLiteral("Create Folder"));
return reinterpret_cast<OakEngineNode *>(child);
}
int oakengine_folder_has_child_recursive(const OakEngineNode *folder,
const OakEngineNode *child)
{
if (!folder || !child) {
return 0;
}
const olive::Folder *f =
dynamic_cast<const olive::Folder *>(impl(
const_cast<OakEngineNode *>(folder)));
if (!f) {
return 0;
}
return f->has_child_recursive(
const_cast<olive::Node *>(impl(
const_cast<OakEngineNode *>(child)))) ? 1 : 0;
}
int oakengine_folder_index_of_child(const OakEngineNode *folder,
const OakEngineNode *child)
{
if (!folder || !child) {
return OAKENGINE_E_INVALID;
}
const olive::Folder *f =
dynamic_cast<const olive::Folder *>(impl(
const_cast<OakEngineNode *>(folder)));
if (!f) {
return OAKENGINE_E_INVALID;
}
const olive::Node *c = impl(const_cast<OakEngineNode *>(child));
const int idx = f->index_of_child(const_cast<olive::Node *>(c));
return idx >= 0 ? idx : OAKENGINE_E_NOT_FOUND;
}
const char *oakengine_folder_child_input_key(void)
{
static const QByteArray s = olive::Folder::k_child_input.toUtf8();
return s.constData();
}
int oakengine_folder_add_child(OakEngineNode *folder, OakEngineNode *child)
{
if (!folder || !child) {
return OAKENGINE_E_INVALID;
}
olive::Folder *f = dynamic_cast<olive::Folder *>(impl(folder));
if (!f) {
return OAKENGINE_E_INVALID;
}
olive::Node *c = impl(child);
if (!c) {
return OAKENGINE_E_INVALID;
}
push_or_run(new olive::FolderAddChild(f, c),
QStringLiteral("Add Child to Folder"));
return OAKENGINE_OK;
}
void *oakengine_folder_remove_element_command(OakEngineNode *folder,
OakEngineNode *child)
{
if (!folder || !child) {
return nullptr;
}
olive::Folder *f = dynamic_cast<olive::Folder *>(impl(folder));
if (!f) {
return nullptr;
}
olive::Node *c = impl(child);
if (!c) {
return nullptr;
}
return new olive::Folder::RemoveElementCommand(f, c);
}
int oakengine_folder_move_child(OakEngineNode *node, OakEngineNode *new_folder)
{
return oakengine_folder_move_children(&node, 1, new_folder, nullptr);
}
int oakengine_folder_move_children(OakEngineNode *const *nodes, int count,
OakEngineNode *dest_folder,
const char *undo_name)
{
if (!nodes || count <= 0 || !dest_folder) {
return OAKENGINE_E_INVALID;
}
olive::Folder *dest = dynamic_cast<olive::Folder *>(impl(dest_folder));
if (!dest) {
return OAKENGINE_E_INVALID;
}
// A true move: remove each node from its old folder, then add it to the
// destination — all inside ONE undoable command (FolderAddChild alone
// would leave the node in both folders).
auto *command = new olive::MultiUndoCommand();
for (int i = 0; i < count; i++) {
if (!nodes[i]) {
delete command;
return OAKENGINE_E_INVALID;
}
olive::Node *n = impl(nodes[i]);
if (n->folder() == dest) {
continue;
}
if (olive::Folder *old = n->folder()) {
command->add_child(new olive::Folder::RemoveElementCommand(old, n));
}
command->add_child(new olive::FolderAddChild(dest, n));
}
push_or_run(command, undo_name ? QString::fromUtf8(undo_name)
: QStringLiteral("Move Folder Child"));
return OAKENGINE_OK;
}
/* ---- Project extras ------------------------------------------------------- */
OakEngineNode *oakengine_project_root(OakEngineProject *self)
{
if (!self) {
return nullptr;
}
return reinterpret_cast<OakEngineNode *>(impl(self)->root());
}
int oakengine_project_pretty_filename(const OakEngineProject *self, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(self)->pretty_filename(), buf, buf_size);
}
int oakengine_project_set_filename(OakEngineProject *self, const char *path)
{
if (!self || !path) {
return OAKENGINE_E_INVALID;
}
impl(self)->set_filename(QString::fromUtf8(path));
return OAKENGINE_OK;
}
int oakengine_project_cache_path(const OakEngineProject *self, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(self)->cache_path(), buf, buf_size);
}
int oakengine_project_cache_alongside_path(const OakEngineProject *self,
char *buf, int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(self)->get_cache_alongside_project_path(), buf,
buf_size);
}
int oakengine_project_set_custom_cache_path(OakEngineProject *self,
const char *path)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
impl(self)->set_custom_cache_path(
path ? QString::fromUtf8(path) : QString());
return OAKENGINE_OK;
}
int oakengine_project_get_custom_cache_path(const OakEngineProject *self,
char *buf, int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const QString p = impl(self)->get_custom_cache_path();
if (p.isEmpty()) {
if (buf && buf_size > 0) {
buf[0] = '\0';
}
return 0;
}
return string_to_buf(p, buf, buf_size);
}
int oakengine_project_get_cache_location_setting(const OakEngineProject *self)
{
if (!self) {
return -1;
}
return int(impl(self)->get_cache_location_setting());
}
const char *oakengine_project_item_mime_type(void)
{
// k_item_mime_type is a static const QString.
static const QByteArray s = QString(olive::Project::k_item_mime_type).toUtf8();
return s.constData();
}
OakEngineProject *oakengine_project_from_object(const OakEngineNode *node)
{
if (!node) {
return nullptr;
}
const olive::Node *n = impl(node);
return reinterpret_cast<OakEngineProject *>(
olive::Project::get_project_from_object(n));
}
int oakengine_project_get_color_reference_space(const OakEngineProject *self,
char *buf, int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(
qvariant_cast<QString>(impl(self)->get_setting(
olive::Project::k_color_reference_space)),
buf, buf_size);
}
int oakengine_project_set_color_reference_space(OakEngineProject *self,
const char *colorspace)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
if (!colorspace) {
return OAKENGINE_E_INVALID;
}
impl(self)->set_color_reference_space(QString::fromUtf8(colorspace));
return OAKENGINE_OK;
}
} // extern "C"
+169
View File
@@ -0,0 +1,169 @@
/***
Oak - 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 "oakengine/proxy.h"
#include <cstring>
#include <QByteArray>
#include <QString>
#include "codec/proxymanager.h"
namespace
{
int write_string(const QString &s, char *buf, int buf_size)
{
const QByteArray utf8 = s.toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
std::memcpy(buf, utf8.constData(), size_t(n));
buf[n] = '\0';
}
return len;
}
olive::ProxyManager::ProxyParams params_from_c(const oak_proxy_params *params)
{
olive::ProxyManager::ProxyParams out;
if (!params) {
return out;
}
out.width = params->width;
out.height = params->height;
out.divider = params->divider;
out.version = params->version;
out.crf = params->crf;
out.include_audio = params->include_audio != 0;
out.extension = QString::fromUtf8(params->extension);
out.preset = QString::fromUtf8(params->preset);
return out;
}
void params_to_c(const olive::ProxyManager::ProxyParams &params,
oak_proxy_params *out)
{
if (!out) {
return;
}
out->width = params.width;
out->height = params.height;
out->divider = params.divider;
out->version = params.version;
out->crf = params.crf;
out->include_audio = params.include_audio ? 1 : 0;
const QByteArray ext = params.extension.toUtf8();
const int ext_n = qMin(int(ext.size()), int(sizeof(out->extension) - 1));
std::memcpy(out->extension, ext.constData(), size_t(ext_n));
out->extension[ext_n] = '\0';
const QByteArray preset = params.preset.toUtf8();
const int preset_n = qMin(int(preset.size()), int(sizeof(out->preset) - 1));
std::memcpy(out->preset, preset.constData(), size_t(preset_n));
out->preset[preset_n] = '\0';
}
} // namespace
extern "C" int oakengine_proxy_create_instance(void)
{
olive::ProxyManager::create_instance();
return olive::ProxyManager::instance() ? OAKENGINE_OK : OAKENGINE_E_FAILED;
}
extern "C" int oakengine_proxy_destroy_instance(void)
{
olive::ProxyManager::destroy_instance();
return OAKENGINE_OK;
}
extern "C" int oakengine_proxy_params_from_config(oak_proxy_params *out)
{
if (!out) {
return OAKENGINE_E_INVALID;
}
params_to_c(olive::ProxyManager::proxy_params_from_config(), out);
return OAKENGINE_OK;
}
extern "C" int oakengine_proxy_get_state(const char *proxy_filename)
{
if (!proxy_filename || std::strlen(proxy_filename) == 0) {
return OAKENGINE_PROXY_STATE_MISSING;
}
return static_cast<int>(olive::ProxyManager::get_proxy_state(
QString::fromUtf8(proxy_filename)));
}
extern "C" int oakengine_proxy_state_to_string(int state, char *buf,
int buf_size)
{
if (state != OAKENGINE_PROXY_STATE_MISSING &&
state != OAKENGINE_PROXY_STATE_GENERATING &&
state != OAKENGINE_PROXY_STATE_READY &&
state != OAKENGINE_PROXY_STATE_FAILED) {
return OAKENGINE_E_INVALID;
}
const QString s = olive::ProxyManager::proxy_state_to_string(
static_cast<olive::ProxyManager::ProxyState>(state));
return write_string(s, buf, buf_size);
}
extern "C" int oakengine_proxy_get_or_start(const char *cache_path,
const char *source_filename,
int stream_index,
const oak_proxy_params *params,
oak_proxy_result *out)
{
if (!out) {
return OAKENGINE_E_INVALID;
}
olive::ProxyManager *mgr = olive::ProxyManager::instance();
if (!mgr) {
return OAKENGINE_E_STATE;
}
if (!cache_path || !source_filename || !params) {
return OAKENGINE_E_INVALID;
}
olive::ProxyManager::Proxy proxy = mgr->get_or_start_proxy(
QString::fromUtf8(cache_path), QString::fromUtf8(source_filename),
stream_index, params_from_c(params));
out->state = static_cast<int>(proxy.state);
const QByteArray fn = proxy.filename.toUtf8();
const int n = qMin(int(fn.size()), int(sizeof(out->filename) - 1));
std::memcpy(out->filename, fn.constData(), size_t(n));
out->filename[n] = '\0';
out->task = reinterpret_cast<int64_t>(proxy.task);
return OAKENGINE_OK;
}
extern "C" int oakengine_proxy_get_working_filename(const char *proxy_filename,
char *buf, int buf_size)
{
if (!proxy_filename) {
return OAKENGINE_E_INVALID;
}
const QString s = olive::ProxyManager::get_working_proxy_filename(
QString::fromUtf8(proxy_filename));
return write_string(s, buf, buf_size);
}
+61
View File
@@ -31,11 +31,14 @@
#include <QString>
#include <QThread>
#include "colorinternal.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
#include "render/colorprocessor.h"
#include "render/previewautocacher.h"
#include "render/rendermanager.h"
#include "render/renderticket.h"
#include "node/input/multicam/multicamnode.h"
namespace
{
@@ -479,4 +482,62 @@ void oakengine_audio_free(OakEngineAudioBuffer *self)
delete impl(self);
}
/* ---- Render manager helpers ----------------------------------------------- */
int oakengine_render_manager_set_aggressive_garbage_collection(int aggressive)
{
if (!olive::RenderManager::instance()) {
return OAKENGINE_E_STATE;
}
olive::RenderManager::instance()->set_aggressive_garbage_collection(
aggressive != 0);
return OAKENGINE_OK;
}
int oakengine_render_manager_requested_backend(void)
{
if (!olive::RenderManager::instance()) {
return 0;
}
return int(olive::RenderManager::instance()->requested_backend());
}
int oakengine_render_manager_backend_to_string(int backend, char *buf,
int buf_size)
{
const QString s = olive::RenderManager::backend_to_string(
static_cast<olive::RenderManager::Backend>(backend));
const QByteArray utf = s.toUtf8();
if (buf && buf_size > 0) {
snprintf(buf, size_t(buf_size), "%s", utf.constData());
}
return int(utf.size());
}
int oakengine_render_cache_set_display_color_processor(void *processor)
{
olive::RenderManager *rm = olive::RenderManager::instance();
if (!rm || !rm->get_cacher()) {
return OAKENGINE_E_STATE;
}
// `processor` is a borrowed OakEngineColorProcessor handle (see
// colorinternal.h); unwrap the engine shared pointer it carries.
auto *proc = static_cast<const OakEngineColorProcessor *>(processor);
rm->get_cacher()->set_display_color_processor(
proc ? proc->ptr : olive::ColorProcessorPtr());
return OAKENGINE_OK;
}
int oakengine_render_cache_set_multicam_node(OakEngineNode *node)
{
olive::RenderManager *rm = olive::RenderManager::instance();
if (!rm || !rm->get_cacher()) {
return OAKENGINE_E_STATE;
}
rm->get_cacher()->set_multicam_node(
node ? dynamic_cast<olive::MultiCamNode *>(reinterpret_cast<olive::Node *>(node))
: nullptr);
return OAKENGINE_OK;
}
} // extern "C"
+470
View File
@@ -0,0 +1,470 @@
/***
Oak - 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 "oakengine/serializer.h"
#include <cstring>
#include <QByteArray>
#include <QFile>
#include <QString>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include "node/keyframe.h"
#include "node/node.h"
#include "node/project.h"
#include "node/project/serializer/serializer.h"
#include "timeline/timelinemarker.h"
namespace
{
int write_string(const QString &s, char *buf, int buf_size)
{
const QByteArray utf8 = s.toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
std::memcpy(buf, utf8.constData(), size_t(n));
buf[n] = '\0';
}
return len;
}
olive::ProjectSerializer::LoadType c_load_type_to_cpp(int load_type)
{
switch (load_type) {
case OAKENGINE_CLIPBOARD_PROJECT:
return olive::ProjectSerializer::k_project;
case OAKENGINE_CLIPBOARD_NODES:
return olive::ProjectSerializer::k_only_nodes;
case OAKENGINE_CLIPBOARD_CLIPS:
return olive::ProjectSerializer::k_only_clips;
case OAKENGINE_CLIPBOARD_MARKERS:
return olive::ProjectSerializer::k_only_markers;
case OAKENGINE_CLIPBOARD_KEYFRAMES:
return olive::ProjectSerializer::k_only_keyframes;
default:
return olive::ProjectSerializer::k_only_nodes;
}
}
int cpp_result_code_to_c(olive::ProjectSerializer::ResultCode code)
{
switch (code) {
case olive::ProjectSerializer::k_success:
return OAKENGINE_SERIALIZER_OK;
case olive::ProjectSerializer::k_project_too_old:
return OAKENGINE_SERIALIZER_TOO_OLD;
case olive::ProjectSerializer::k_project_too_new:
return OAKENGINE_SERIALIZER_TOO_NEW;
case olive::ProjectSerializer::k_unknown_version:
return OAKENGINE_SERIALIZER_UNKNOWN_VERSION;
case olive::ProjectSerializer::k_file_error:
return OAKENGINE_SERIALIZER_FILE_ERROR;
case olive::ProjectSerializer::k_xml_error:
return OAKENGINE_SERIALIZER_XML_ERROR;
case olive::ProjectSerializer::k_overwrite_error:
return OAKENGINE_SERIALIZER_OVERWRITE_ERROR;
case olive::ProjectSerializer::k_no_data:
return OAKENGINE_SERIALIZER_NO_DATA;
default:
return OAKENGINE_SERIALIZER_NO_DATA;
}
}
struct ClipboardCtx {
olive::ProjectSerializer::LoadType load_type;
olive::Project *project;
QString filename;
olive::ProjectSerializer::SaveData save_data;
olive::ProjectSerializer::LoadData load_data;
QString xml_output;
ClipboardCtx(int lt, olive::Project *p, const QString &fn)
: load_type(c_load_type_to_cpp(lt))
, project(p)
, filename(fn)
, save_data(load_type, project, filename)
{
}
};
ClipboardCtx *ctx(OakEngineClipboard *cb)
{
return reinterpret_cast<ClipboardCtx *>(cb);
}
} // namespace
extern "C" int oakengine_serializer_check_compressed(const char *filename)
{
if (!filename || std::strlen(filename) == 0) {
return 0;
}
QFile file(QString::fromUtf8(filename));
if (!file.open(QFile::ReadOnly)) {
return 0;
}
return olive::ProjectSerializer::check_compressed_id(&file) ? 1 : 0;
}
extern "C" OakEngineClipboard *oakengine_clipboard_create(
int load_type, OakEngineProject *project, const char *filename)
{
return reinterpret_cast<OakEngineClipboard *>(new ClipboardCtx(
load_type, reinterpret_cast<olive::Project *>(project),
filename ? QString::fromUtf8(filename) : QString()));
}
extern "C" int oakengine_clipboard_set_nodes(OakEngineClipboard *cb,
const OakEngineNode *const *nodes,
int count)
{
ClipboardCtx *c = ctx(cb);
if (!c) {
return OAKENGINE_E_INVALID;
}
QVector<olive::Node *> list;
if (nodes && count > 0) {
list.reserve(count);
for (int i = 0; i < count; i++) {
list.append(reinterpret_cast<olive::Node *>(
const_cast<OakEngineNode *>(nodes[i])));
}
}
c->save_data.set_only_serialize_nodes_and_resolve_groups(list);
return OAKENGINE_OK;
}
extern "C" int oakengine_clipboard_set_markers(
OakEngineClipboard *cb, const OakEngineMarker *const *markers, int count)
{
ClipboardCtx *c = ctx(cb);
if (!c) {
return OAKENGINE_E_INVALID;
}
std::vector<olive::TimelineMarker *> list;
if (markers && count > 0) {
list.reserve(size_t(count));
for (int i = 0; i < count; i++) {
list.push_back(reinterpret_cast<olive::TimelineMarker *>(
const_cast<OakEngineMarker *>(markers[i])));
}
}
c->save_data.set_only_serialize_markers(list);
return OAKENGINE_OK;
}
extern "C" int oakengine_clipboard_set_keyframes(
OakEngineClipboard *cb, const OakEngineKeyframe *const *keyframes,
int count)
{
ClipboardCtx *c = ctx(cb);
if (!c) {
return OAKENGINE_E_INVALID;
}
std::vector<olive::NodeKeyframe *> list;
if (keyframes && count > 0) {
list.reserve(size_t(count));
for (int i = 0; i < count; i++) {
list.push_back(reinterpret_cast<olive::NodeKeyframe *>(
const_cast<OakEngineKeyframe *>(keyframes[i])));
}
}
c->save_data.set_only_serialize_keyframes(list);
return OAKENGINE_OK;
}
extern "C" int oakengine_clipboard_set_property(OakEngineClipboard *cb,
OakEngineNode *node,
const char *key,
const char *value)
{
ClipboardCtx *c = ctx(cb);
if (!c || !node || !key) {
return OAKENGINE_E_INVALID;
}
olive::ProjectSerializer::SerializedProperties props =
c->save_data.get_properties();
props[reinterpret_cast<olive::Node *>(node)][QString::fromUtf8(key)] =
value ? QString::fromUtf8(value) : QString();
c->save_data.set_properties(props);
return OAKENGINE_OK;
}
extern "C" int oakengine_clipboard_copy(OakEngineClipboard *cb)
{
ClipboardCtx *c = ctx(cb);
if (!c) {
return OAKENGINE_E_INVALID;
}
olive::ProjectSerializer::Result res =
olive::ProjectSerializer::copy(c->save_data);
return (res == olive::ProjectSerializer::k_success) ? OAKENGINE_OK
: OAKENGINE_E_FAILED;
}
extern "C" int oakengine_clipboard_save_to_xml(OakEngineClipboard *cb,
char *buf, int buf_size)
{
ClipboardCtx *c = ctx(cb);
if (!c) {
return OAKENGINE_E_INVALID;
}
c->xml_output.clear();
QXmlStreamWriter writer(&c->xml_output);
olive::ProjectSerializer::Result res =
olive::ProjectSerializer::save(&writer, c->save_data);
if (res != olive::ProjectSerializer::k_success) {
return OAKENGINE_E_FAILED;
}
return write_string(c->xml_output, buf, buf_size);
}
namespace
{
int do_paste(OakEngineClipboard *cb, int load_type,
olive::Project *project,
int (*map_fn)(OakEngineNode *, OakEngineNode *, void *),
void *userdata, int *result_code, char *details_buf,
int details_buf_size)
{
ClipboardCtx *c = ctx(cb);
if (!c || !result_code) {
return OAKENGINE_E_INVALID;
}
olive::ProjectSerializer::Result res = olive::ProjectSerializer::paste(
c_load_type_to_cpp(load_type), project);
*result_code = cpp_result_code_to_c(res.code());
if (res == olive::ProjectSerializer::k_success) {
c->load_data = res.get_load_data();
if (map_fn && !c->load_data.node_ptrs.isEmpty()) {
for (auto it = c->load_data.node_ptrs.cbegin();
it != c->load_data.node_ptrs.cend(); ++it) {
const int stop = map_fn(
reinterpret_cast<OakEngineNode *>(it.key()),
reinterpret_cast<OakEngineNode *>(it.value()), userdata);
if (stop != 0) {
break;
}
}
}
return OAKENGINE_OK;
}
if (details_buf && details_buf_size > 0) {
write_string(res.get_details(), details_buf, details_buf_size);
}
return OAKENGINE_E_FAILED;
}
} // namespace
extern "C" int oakengine_clipboard_paste(OakEngineClipboard *cb,
int load_type,
OakEngineProject *project,
int *result_code,
char *details_buf,
int details_buf_size)
{
return do_paste(cb, load_type, reinterpret_cast<olive::Project *>(project),
nullptr, nullptr, result_code, details_buf,
details_buf_size);
}
extern "C" int oakengine_clipboard_paste_with_map(
OakEngineClipboard *cb, int load_type, OakEngineProject *project,
int (*map_fn)(OakEngineNode *old, OakEngineNode *new_node, void *userdata),
void *userdata, int *result_code, char *details_buf,
int details_buf_size)
{
return do_paste(cb, load_type, reinterpret_cast<olive::Project *>(project),
map_fn, userdata, result_code, details_buf,
details_buf_size);
}
extern "C" void oakengine_clipboard_free(OakEngineClipboard *cb)
{
delete ctx(cb);
}
extern "C" int oakengine_clipboard_get_loaded_node_count(
OakEngineClipboard *cb)
{
ClipboardCtx *c = ctx(cb);
if (!c) {
return OAKENGINE_E_INVALID;
}
return c->load_data.nodes.size();
}
extern "C" OakEngineNode *oakengine_clipboard_get_loaded_node_at(
OakEngineClipboard *cb, int index)
{
ClipboardCtx *c = ctx(cb);
if (!c || index < 0 || index >= c->load_data.nodes.size()) {
return nullptr;
}
return reinterpret_cast<OakEngineNode *>(c->load_data.nodes.at(index));
}
extern "C" int oakengine_clipboard_get_loaded_marker_count(
OakEngineClipboard *cb)
{
ClipboardCtx *c = ctx(cb);
if (!c) {
return OAKENGINE_E_INVALID;
}
return static_cast<int>(c->load_data.markers.size());
}
extern "C" OakEngineMarker *oakengine_clipboard_get_loaded_marker_at(
OakEngineClipboard *cb, int index)
{
ClipboardCtx *c = ctx(cb);
if (!c || index < 0 ||
index >= static_cast<int>(c->load_data.markers.size())) {
return nullptr;
}
return reinterpret_cast<OakEngineMarker *>(c->load_data.markers.at(index));
}
extern "C" int oakengine_clipboard_get_loaded_keyframe_count(
OakEngineClipboard *cb)
{
ClipboardCtx *c = ctx(cb);
if (!c) {
return OAKENGINE_E_INVALID;
}
int total = 0;
for (auto it = c->load_data.keyframes.cbegin();
it != c->load_data.keyframes.cend(); ++it) {
total += it.value().size();
}
return total;
}
extern "C" OakEngineKeyframe *oakengine_clipboard_get_loaded_keyframe_at(
OakEngineClipboard *cb, int index)
{
ClipboardCtx *c = ctx(cb);
if (!c || index < 0) {
return nullptr;
}
int current = 0;
for (auto it = c->load_data.keyframes.cbegin();
it != c->load_data.keyframes.cend(); ++it) {
const QVector<olive::NodeKeyframe *> &vec = it.value();
if (index < current + vec.size()) {
return reinterpret_cast<OakEngineKeyframe *>(
vec.at(index - current));
}
current += vec.size();
}
return nullptr;
}
extern "C" int oakengine_clipboard_foreach_property(
OakEngineClipboard *cb,
int (*fn)(OakEngineNode *node, const char *key, const char *value,
void *userdata),
void *userdata)
{
ClipboardCtx *c = ctx(cb);
if (!c || !fn) {
return OAKENGINE_E_INVALID;
}
for (auto it = c->load_data.properties.cbegin();
it != c->load_data.properties.cend(); ++it) {
OakEngineNode *node = reinterpret_cast<OakEngineNode *>(it.key());
for (auto jt = it.value().cbegin(); jt != it.value().cend(); ++jt) {
const QByteArray key = jt.key().toUtf8();
const QByteArray value = jt.value().toUtf8();
const int stop = fn(node, key.constData(), value.constData(),
userdata);
if (stop != 0) {
return OAKENGINE_OK;
}
}
}
return OAKENGINE_OK;
}
extern "C" int oakengine_clipboard_foreach_keyframe(
OakEngineClipboard *cb,
int (*fn)(const char *node_id, OakEngineKeyframe *keyframe,
void *userdata),
void *userdata)
{
ClipboardCtx *c = ctx(cb);
if (!c || !fn) {
return OAKENGINE_E_INVALID;
}
for (auto it = c->load_data.keyframes.cbegin();
it != c->load_data.keyframes.cend(); ++it) {
const QByteArray node_id = it.key().toUtf8();
for (olive::NodeKeyframe *key : it.value()) {
const int stop = fn(node_id.constData(),
reinterpret_cast<OakEngineKeyframe *>(key),
userdata);
if (stop != 0) {
return OAKENGINE_OK;
}
}
}
return OAKENGINE_OK;
}
extern "C" int oakengine_clipboard_foreach_connection(
OakEngineClipboard *cb,
int (*fn)(OakEngineNode *output_node, OakEngineNode *input_node,
const char *input_id, int element, void *userdata),
void *userdata)
{
ClipboardCtx *c = ctx(cb);
if (!c || !fn) {
return OAKENGINE_E_INVALID;
}
for (const olive::Node::OutputConnection &oc :
c->load_data.promised_connections) {
OakEngineNode *output_node =
reinterpret_cast<OakEngineNode *>(oc.first);
OakEngineNode *input_node =
reinterpret_cast<OakEngineNode *>(oc.second.node());
const QByteArray input_id = oc.second.input().toUtf8();
const int stop = fn(output_node, input_node, input_id.constData(),
oc.second.element(), userdata);
if (stop != 0) {
return OAKENGINE_OK;
}
}
return OAKENGINE_OK;
}
+327
View File
@@ -0,0 +1,327 @@
/***
Oak - 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 "oakengine/sync.h"
#include <cmath>
#include <QString>
#include "audio/audiowaveformsync.h"
#include "node/block/clip/clip.h"
#include "node/project/sequence/sequence.h"
#include "oakengine/renderer.h"
#include "render/rendermanager.h"
namespace
{
thread_local QString g_last_error;
void set_error(const QString &error)
{
g_last_error = error;
}
int string_to_buf(const QString &s, char *buf, int buf_size)
{
const QByteArray utf = s.toUtf8();
if (buf && buf_size > 0) {
snprintf(buf, size_t(buf_size), "%s", utf.constData());
}
return int(utf.size());
}
// Render the full on-track audio of a clip through the renderer
// family. Returns an unallocated buffer on failure (error reported).
olive::core::SampleBuffer render_clip_audio(olive::Sequence *sequence,
olive::ClipBlock *clip,
OakEngineRenderer *renderer)
{
const olive::Rational tb = sequence->get_video_params().time_base();
const int64_t in_ts = olive::core::Timecode::time_to_timestamp(
clip->in(), tb, olive::core::Timecode::k_round);
const int64_t out_ts = olive::core::Timecode::time_to_timestamp(
clip->out(), tb, olive::core::Timecode::k_round);
OakEngineAudioBuffer *buf =
oakengine_renderer_render_audio(renderer, in_ts, out_ts - in_ts);
if (!buf) {
char err[256];
err[0] = '\0';
oakengine_renderer_last_error(renderer, err, sizeof(err));
set_error(QStringLiteral("audio render failed: %1")
.arg(err[0] ? err : "(no error)"));
return olive::core::SampleBuffer();
}
const olive::AudioParams params(
oakengine_audio_sample_rate(buf),
sequence->get_audio_params().channel_layout(),
olive::core::SampleFormat::f32_p);
olive::core::SampleBuffer samples(
params, size_t(oakengine_audio_sample_count(buf)));
for (int ch = 0; ch < oakengine_audio_channel_count(buf); ch++) {
memcpy(samples.to_raw_ptrs()[ch], oakengine_audio_data(buf, ch),
size_t(oakengine_audio_sample_count(buf)) * sizeof(float));
}
oakengine_audio_free(buf);
return samples;
}
// Shared front of both estimators: validation, then render both clips'
// audio and extract the RMS envelopes with the application's window
// parameters. Returns 0 on success (error reported otherwise).
int prepare_envelopes(OakEngineSequence *seq, OakEngineClip *reference,
OakEngineClip *target, QVector<double> *ref_envelope,
QVector<double> *target_envelope, int *sample_rate,
int64_t *max_offset_windows, size_t *window_samples)
{
set_error(QString());
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
olive::ClipBlock *ref_clip =
reinterpret_cast<olive::ClipBlock *>(reference);
olive::ClipBlock *target_clip =
reinterpret_cast<olive::ClipBlock *>(target);
if (!sequence || !ref_clip || !target_clip) {
set_error(QStringLiteral("invalid sequence or clip handle"));
return OAKENGINE_E_INVALID;
}
if (!ref_clip->track() || !target_clip->track()) {
set_error(QStringLiteral("clip is not on a track"));
return OAKENGINE_E_INVALID;
}
const olive::AudioParams audio_params = sequence->get_audio_params();
if (audio_params.channel_count() <= 0 ||
audio_params.sample_rate() <= 0) {
set_error(QStringLiteral("sequence has no audio"));
return OAKENGINE_E_STATE;
}
if (!olive::RenderManager::instance()) {
set_error(QStringLiteral("engine not initialized with "
"OAKENGINE_INIT_RENDER"));
return OAKENGINE_E_STATE;
}
const olive::Rational frame_rate =
sequence->get_video_params().frame_rate();
const int fps_num = frame_rate.isNull() ? 30000 : frame_rate.numerator();
const int fps_den = frame_rate.isNull() ? 1001 : frame_rate.denominator();
OakEngineRenderer *renderer =
// Frame geometry is irrelevant for audio renders; the facade
// requires a positive size.
oakengine_renderer_create(seq, 16, 16, 4, fps_num, fps_den, nullptr);
if (!renderer) {
set_error(QStringLiteral("failed to create the renderer"));
return OAKENGINE_E_STATE;
}
const olive::core::SampleBuffer ref_samples =
render_clip_audio(sequence, ref_clip, renderer);
if (!ref_samples.is_allocated()) {
oakengine_renderer_free(renderer);
return OAKENGINE_E_STATE;
}
const olive::core::SampleBuffer target_samples =
render_clip_audio(sequence, target_clip, renderer);
oakengine_renderer_free(renderer);
if (!target_samples.is_allocated()) {
return OAKENGINE_E_STATE;
}
*sample_rate = audio_params.sample_rate();
*window_samples = size_t(std::max(1, *sample_rate / 20));
*ref_envelope = olive::AudioWaveformSync::extract_rms_envelope(
ref_samples, *window_samples);
*target_envelope = olive::AudioWaveformSync::extract_rms_envelope(
target_samples, *window_samples);
// The application's 10-minute maximum offset, in envelope windows.
*max_offset_windows = (int64_t(*sample_rate) * 10 * 60) /
int64_t(*window_samples);
return OAKENGINE_OK;
}
} // namespace
extern "C"
{
int oakengine_sync_estimate_offset(OakEngineSequence *seq,
OakEngineClip *reference,
OakEngineClip *target,
double *out_offset_seconds,
double *out_confidence)
{
QVector<double> ref_envelope, target_envelope;
int sample_rate = 0;
int64_t max_offset_windows = 0;
size_t window_samples = 0;
const int rc = prepare_envelopes(seq, reference, target, &ref_envelope,
&target_envelope, &sample_rate,
&max_offset_windows, &window_samples);
if (rc != OAKENGINE_OK) {
return rc;
}
const olive::AudioWaveformSync::OffsetResult result =
olive::AudioWaveformSync::estimate_envelope_offset(
ref_envelope, target_envelope, {}, {}, window_samples,
max_offset_windows);
if (out_confidence) {
*out_confidence = result.confidence;
}
if (!result.valid) {
if (out_offset_seconds) {
*out_offset_seconds = 0.0;
}
set_error(QStringLiteral("waveform correlation was inconclusive "
"(confidence %1)")
.arg(result.confidence));
return OAKENGINE_E_STATE;
}
if (out_offset_seconds) {
*out_offset_seconds =
double(result.offset_samples) / double(sample_rate);
}
return OAKENGINE_OK;
}
int oakengine_sync_estimate_stretch_offset(
OakEngineSequence *seq, OakEngineClip *reference, OakEngineClip *target,
double *out_stretch, double *out_offset_seconds, double *out_confidence)
{
QVector<double> ref_envelope, target_envelope;
int sample_rate = 0;
int64_t max_offset_windows = 0;
size_t window_samples = 0;
const int rc = prepare_envelopes(seq, reference, target, &ref_envelope,
&target_envelope, &sample_rate,
&max_offset_windows, &window_samples);
if (rc != OAKENGINE_OK) {
return rc;
}
// The application's tighter 30-second offset radius for the stretch
// search (keeps it interactive).
const int64_t radius_windows = std::min<int64_t>(
max_offset_windows,
(int64_t(sample_rate) * 30) / int64_t(window_samples));
const olive::AudioWaveformSync::StretchOffsetResult result =
olive::AudioWaveformSync::estimate_stretch_and_offset(
ref_envelope, target_envelope, {}, {}, window_samples,
radius_windows, 0.75, 1.34, 0.005);
if (out_confidence) {
*out_confidence = result.confidence;
}
if (!result.valid) {
if (out_stretch) {
*out_stretch = 1.0;
}
if (out_offset_seconds) {
*out_offset_seconds = 0.0;
}
set_error(QStringLiteral("stretch correlation was inconclusive "
"(confidence %1)")
.arg(result.confidence));
return OAKENGINE_E_STATE;
}
if (out_stretch) {
*out_stretch = result.rate;
}
if (out_offset_seconds) {
*out_offset_seconds =
double(result.offset_samples) / double(sample_rate);
}
return OAKENGINE_OK;
}
int oakengine_sync_last_error(char *buf, int buf_size)
{
return string_to_buf(g_last_error, buf, buf_size);
}
int oakengine_sync_place_by_source_time(
int64_t ref_source_start_num, int64_t ref_source_start_den,
int64_t ref_media_in_num, int64_t ref_media_in_den,
int64_t cand_source_start_num, int64_t cand_source_start_den,
int64_t cand_media_in_num, int64_t cand_media_in_den,
int64_t anchor_num, int64_t anchor_den,
oak_sync_placement *out)
{
if (ref_source_start_den == 0 || ref_media_in_den == 0 ||
cand_source_start_den == 0 || cand_media_in_den == 0 ||
anchor_den == 0) {
return OAKENGINE_E_INVALID;
}
const olive::core::Rational ref_source_start(ref_source_start_num,
ref_source_start_den);
const olive::core::Rational ref_media_in(ref_media_in_num,
ref_media_in_den);
const olive::core::Rational cand_source_start(cand_source_start_num,
cand_source_start_den);
const olive::core::Rational cand_media_in(cand_media_in_num,
cand_media_in_den);
const olive::core::Rational anchor(anchor_num, anchor_den);
const olive::core::Rational ref_head = ref_source_start + ref_media_in;
const olive::core::Rational cand_head = cand_source_start + cand_media_in;
const olive::core::Rational timeline_in = anchor + cand_head - ref_head;
if (timeline_in.isNaN()) {
return OAKENGINE_E_INVALID;
}
if (out) {
out->timeline_in_num = timeline_in.numerator();
out->timeline_in_den = timeline_in.denominator();
}
return OAKENGINE_OK;
}
int oakengine_sync_place_by_waveform_offset(
int64_t ref_timeline_in_num, int64_t ref_timeline_in_den,
int64_t candidate_offset_samples, int sample_rate,
oak_sync_placement *out)
{
if (sample_rate <= 0 || ref_timeline_in_den == 0) {
return OAKENGINE_E_INVALID;
}
const olive::core::Rational ref_timeline_in(ref_timeline_in_num,
ref_timeline_in_den);
const olive::core::Rational offset =
olive::core::Rational::from_double(
static_cast<double>(candidate_offset_samples) /
static_cast<double>(sample_rate));
const olive::core::Rational timeline_in = ref_timeline_in + offset;
if (timeline_in.isNaN()) {
return OAKENGINE_E_INVALID;
}
if (out) {
out->timeline_in_num = timeline_in.numerator();
out->timeline_in_den = timeline_in.denominator();
}
return OAKENGINE_OK;
}
} // extern "C"
+471
View File
@@ -0,0 +1,471 @@
/***
Oak - 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 "oakengine/task.h"
#include <cstring>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include "node/project.h"
#include "node/project/folder/folder.h"
#include "node/project/footage/footage.h"
#include "node/project/sequence/sequence.h"
#include "node/project/serializer/serializedlayoutinfo.h"
#include "oakengine/exporter.h"
#include "oakengine/footage.h"
#include "task/project/import/import.h"
#include "task/project/load/load.h"
#include "task/project/save/save.h"
#include "cli/clitask/clitaskdialog.h"
#include "task/task.h"
#include "task/taskmanager.h"
#ifdef USE_OTIO
#include "task/project/loadotio/loadotio.h"
#include "task/project/saveotio/saveotio.h"
#endif
namespace
{
olive::Task *impl(OakEngineTask *h)
{
return reinterpret_cast<olive::Task *>(h);
}
OakEngineTask *wrap(olive::Task *t)
{
return reinterpret_cast<OakEngineTask *>(t);
}
// buf/size string writer (same convention as capi/project.cpp).
int write_string(const QString &s, char *buf, int buf_size)
{
const QByteArray utf8 = s.toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
std::memcpy(buf, utf8.constData(), size_t(n));
buf[n] = '\0';
}
return len;
}
/**
* @brief Proxy-generation task driven by the footage C ABI facade
*
* Moved verbatim from the application's FacadeProxyTask
* (app/widget/projectexplorer/projectexplorer.cpp): the transcode and its
* synchronous wait live behind oakengine_footage_proxy_generate() (which
* records the proxy state on the footage and invalidates it), while the
* task itself queues on the TaskManager like any other task.
*/
class FacadeProxyTask : public olive::Task {
public:
explicit FacadeProxyTask(olive::Footage *footage)
: footage_(footage)
{
set_title(tr("Generating proxy for \"%1\"")
.arg(footage->get_label_or_name()));
}
protected:
virtual bool run() override
{
OakEngineFootage *handle = oakengine_footage_borrow(
reinterpret_cast<OakEngineNode *>(footage_));
const int rc = oakengine_footage_proxy_generate(handle);
oakengine_footage_free(handle);
if (rc != OAKENGINE_OK) {
char err[512];
err[0] = '\0';
oakengine_footage_last_error(err, sizeof(err));
set_error(err[0] ? QString::fromUtf8(err) :
tr("Proxy generation failed"));
return false;
}
return true;
}
private:
olive::Footage *footage_;
};
/**
* @brief Export task driven by the export C ABI facade
*
* Moved verbatim from the application's FacadeExportTask
* (app/dialog/export/export.cpp): runs oakengine_export_render_with_
* params() synchronously on the task thread, forwards its progress
* callback to the task's progress_changed signal and cancels the engine
* render when the task is cancelled.
*/
class FacadeExportTask : public olive::Task {
public:
// Takes ownership of `params`.
FacadeExportTask(olive::Sequence *sequence,
OakEngineEncodingParams *params)
: sequence_(reinterpret_cast<OakEngineSequence *>(sequence))
, params_(params)
{
set_title(tr("Exporting \"%1\"").arg(sequence->get_label()));
}
~FacadeExportTask() override
{
oakengine_encoding_params_destroy(params_);
}
protected:
virtual bool run() override
{
oakengine_export_set_progress_callback(
&FacadeExportTask::forward_progress, this);
const int rc = oakengine_export_render_with_params(sequence_, params_);
oakengine_export_set_progress_callback(nullptr, nullptr);
if (rc == OAKENGINE_E_CANCELLED) {
// Mirror the engine render's cancelled state on the task.
cancel();
return false;
}
if (rc != OAKENGINE_OK) {
char err[1024];
err[0] = '\0';
oakengine_export_last_error(err, sizeof(err));
set_error(err[0] ? QString::fromUtf8(err) :
QStringLiteral("Export failed"));
return false;
}
return true;
}
virtual void CancelEvent() override
{
oakengine_export_cancel();
}
private:
static void forward_progress(double fraction, void *userdata)
{
static_cast<FacadeExportTask *>(userdata)->emit_progress(fraction);
}
void emit_progress(double fraction)
{
emit progress_changed(fraction);
}
OakEngineSequence *sequence_;
OakEngineEncodingParams *params_;
};
olive::ProjectImportTask *as_import(olive::Task *t)
{
return dynamic_cast<olive::ProjectImportTask *>(t);
}
olive::ProjectSaveTask *as_save(olive::Task *t)
{
return dynamic_cast<olive::ProjectSaveTask *>(t);
}
} // namespace
/* ---- Global task manager ------------------------------------------------- */
extern "C" void *oakengine_task_manager_handle(void)
{
return olive::TaskManager::instance();
}
extern "C" int oakengine_task_manager_count(void)
{
olive::TaskManager *m = olive::TaskManager::instance();
return m ? m->get_task_count() : OAKENGINE_E_INVALID;
}
extern "C" OakEngineTask *oakengine_task_manager_first(void)
{
olive::TaskManager *m = olive::TaskManager::instance();
if (!m || m->get_task_count() == 0) {
return nullptr;
}
return wrap(m->get_first_task());
}
extern "C" int oakengine_task_manager_add(OakEngineTask *task)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
olive::TaskManager *m = olive::TaskManager::instance();
if (!m) {
return OAKENGINE_E_STATE;
}
m->add_task(impl(task));
return OAKENGINE_OK;
}
extern "C" int oakengine_task_manager_cancel(OakEngineTask *task)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
olive::TaskManager *m = olive::TaskManager::instance();
if (!m) {
return OAKENGINE_E_STATE;
}
m->cancel_task(impl(task));
return OAKENGINE_OK;
}
/* ---- Task accessors ------------------------------------------------------ */
extern "C" int oakengine_task_title(OakEngineTask *task, char *buf,
int buf_size)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
return write_string(impl(task)->get_title(), buf, buf_size);
}
extern "C" int oakengine_task_error(OakEngineTask *task, char *buf,
int buf_size)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
return write_string(impl(task)->get_error(), buf, buf_size);
}
extern "C" int64_t oakengine_task_start_time(OakEngineTask *task)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
return impl(task)->get_start_time();
}
extern "C" int oakengine_task_is_cancelled(OakEngineTask *task)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
return impl(task)->is_cancelled() ? 1 : 0;
}
extern "C" int oakengine_task_cancel(OakEngineTask *task)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
impl(task)->Cancel();
return OAKENGINE_OK;
}
extern "C" int oakengine_task_start_sync(OakEngineTask *task)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
return impl(task)->start() ? 1 : 0;
}
extern "C" int oakengine_task_free(OakEngineTask *task)
{
if (!task) {
return OAKENGINE_E_INVALID;
}
delete impl(task);
return OAKENGINE_OK;
}
/* ---- Task creators -------------------------------------------------------- */
extern "C" OakEngineTask *
oakengine_task_create_project_load(const char *filename)
{
if (!filename) {
return nullptr;
}
return wrap(new olive::ProjectLoadTask(QString::fromUtf8(filename)));
}
extern "C" OakEngineTask *
oakengine_task_create_project_load_otio(const char *filename)
{
if (!filename) {
return nullptr;
}
#ifdef USE_OTIO
return wrap(new olive::LoadOTIOTask(QString::fromUtf8(filename)));
#else
return nullptr;
#endif
}
extern "C" OakEngineTask *oakengine_task_create_project_save(
OakEngineProject *project, int use_compression,
const char *override_filename, const void *layout)
{
auto *p = reinterpret_cast<olive::Project *>(project);
if (!p) {
return nullptr;
}
auto *task =
new olive::ProjectSaveTask(p, use_compression != 0);
if (layout) {
task->set_layout(
*static_cast<const olive::SerializedLayoutInfo *>(layout));
}
if (override_filename) {
task->set_override_filename(QString::fromUtf8(override_filename));
}
return wrap(task);
}
extern "C" OakEngineTask *
oakengine_task_create_project_save_otio(OakEngineProject *project)
{
auto *p = reinterpret_cast<olive::Project *>(project);
if (!p) {
return nullptr;
}
#ifdef USE_OTIO
return wrap(new olive::SaveOTIOTask(p));
#else
return nullptr;
#endif
}
extern "C" OakEngineTask *oakengine_task_create_project_import(
OakEngineNode *folder, const char **urls, int url_count)
{
auto *f = dynamic_cast<olive::Folder *>(
reinterpret_cast<olive::Node *>(folder));
if (!f || !urls || url_count <= 0) {
return nullptr;
}
QStringList list;
list.reserve(url_count);
for (int i = 0; i < url_count; i++) {
if (!urls[i]) {
return nullptr;
}
list.append(QString::fromUtf8(urls[i]));
}
return wrap(new olive::ProjectImportTask(f, list));
}
extern "C" OakEngineTask *
oakengine_task_create_proxy(OakEngineNode *footage)
{
auto *f = dynamic_cast<olive::Footage *>(
reinterpret_cast<olive::Node *>(footage));
if (!f) {
return nullptr;
}
return wrap(new FacadeProxyTask(f));
}
extern "C" OakEngineTask *oakengine_task_create_export(
OakEngineSequence *sequence, OakEngineEncodingParams *params)
{
auto *s = dynamic_cast<olive::Sequence *>(
reinterpret_cast<olive::Node *>(sequence));
if (!s || !params) {
return nullptr;
}
return wrap(new FacadeExportTask(s, params));
}
/* ---- Import task results -------------------------------------------------- */
extern "C" int oakengine_task_import_file_count(OakEngineTask *task)
{
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
return t ? t->get_file_count() : OAKENGINE_E_INVALID;
}
extern "C" void *oakengine_task_import_get_command(OakEngineTask *task)
{
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
return t ? static_cast<void *>(t->take_command()) : nullptr;
}
extern "C" int oakengine_task_import_footage_count(OakEngineTask *task)
{
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
return t ? t->get_imported_footage().size() : OAKENGINE_E_INVALID;
}
extern "C" OakEngineNode *
oakengine_task_import_footage_at(OakEngineTask *task, int index)
{
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
if (!t || index < 0 || index >= t->get_imported_footage().size()) {
return nullptr;
}
return reinterpret_cast<OakEngineNode *>(
t->get_imported_footage().at(index));
}
extern "C" int oakengine_task_import_invalid_files_count(OakEngineTask *task)
{
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
return t ? t->get_invalid_files().size() : OAKENGINE_E_INVALID;
}
extern "C" int oakengine_task_import_invalid_file_at(OakEngineTask *task,
int index, char *buf,
int buf_size)
{
olive::ProjectImportTask *t = task ? as_import(impl(task)) : nullptr;
if (!t || index < 0 || index >= t->get_invalid_files().size()) {
return OAKENGINE_E_INVALID;
}
return write_string(t->get_invalid_files().at(index), buf, buf_size);
}
/* ---- Save task results ---------------------------------------------------- */
extern "C" OakEngineProject *
oakengine_task_save_get_project(OakEngineTask *task)
{
olive::ProjectSaveTask *t = task ? as_save(impl(task)) : nullptr;
return t ? reinterpret_cast<OakEngineProject *>(t->get_project()) :
nullptr;
}
extern "C" int oakengine_cli_task_dialog_run(OakEngineTask *task,
void *parent_or_NULL)
{
if (!task) {
return 0;
}
olive::CLITaskDialog dlg(impl(task),
static_cast<QObject *>(parent_or_NULL));
return dlg.run() ? 1 : 0;
}
File diff suppressed because it is too large Load Diff
+374
View File
@@ -0,0 +1,374 @@
/***
Oak - 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 "oakengine/traverse.h"
#include <vector>
#include <QByteArray>
#include <QString>
#include <QTransform>
#include <QVector>
#include "node/traverser.h"
#include "node/value.h"
#include "render/videoparams.h"
// oak_node_value_type of a row value: same mapping as node.cpp's
// to_c_type(). Types without any facade representation report
// OAK_NODE_VALUE_NONE; duplicated here because node.cpp's copy is
// translation-unit local.
static int to_c_type(olive::NodeValue::Type t)
{
switch (t) {
case olive::NodeValue::k_int:
return OAK_NODE_VALUE_INT;
case olive::NodeValue::k_float:
return OAK_NODE_VALUE_FLOAT;
case olive::NodeValue::k_boolean:
return OAK_NODE_VALUE_BOOL;
case olive::NodeValue::k_rational:
return OAK_NODE_VALUE_RATIONAL;
case olive::NodeValue::k_color:
return OAK_NODE_VALUE_COLOR;
case olive::NodeValue::k_vec2:
return OAK_NODE_VALUE_VEC2;
case olive::NodeValue::k_vec3:
return OAK_NODE_VALUE_VEC3;
case olive::NodeValue::k_vec4:
return OAK_NODE_VALUE_VEC4;
case olive::NodeValue::k_combo:
return OAK_NODE_VALUE_COMBO;
case olive::NodeValue::k_file:
return OAK_NODE_VALUE_STRING;
case olive::NodeValue::k_text:
return OAK_NODE_VALUE_TEXT;
case olive::NodeValue::k_font:
return OAK_NODE_VALUE_FONT;
case olive::NodeValue::k_str_combo:
return OAK_NODE_VALUE_STR_COMBO;
case olive::NodeValue::k_binary:
return OAK_NODE_VALUE_BINARY;
case olive::NodeValue::k_bezier:
return OAK_NODE_VALUE_BEZIER;
case olive::NodeValue::k_texture:
return OAK_NODE_VALUE_TEXTURE;
case olive::NodeValue::k_samples:
return OAK_NODE_VALUE_SAMPLES;
case olive::NodeValue::k_video_params:
return OAK_NODE_VALUE_VIDEO_PARAMS;
case olive::NodeValue::k_audio_params:
return OAK_NODE_VALUE_AUDIO_PARAMS;
default:
return OAK_NODE_VALUE_NONE;
}
}
namespace
{
olive::Node *impl(OakEngineNode *h)
{
return reinterpret_cast<olive::Node *>(h);
}
// oak_video_params POD -> olive::VideoParams (same mapping as
// encoding.cpp's to_cpp()).
olive::VideoParams to_cpp(const oak_video_params &v)
{
olive::VideoParams vp(
v.width, v.height, olive::Rational(v.time_base_num, v.time_base_den),
static_cast<olive::PixelFormat::Format>(v.format),
olive::VideoParams::k_internal_channel_count,
olive::Rational(v.pixel_aspect_num, v.pixel_aspect_den),
static_cast<olive::VideoParams::Interlacing>(v.interlacing),
v.divider > 0 ? v.divider : 1);
vp.set_color_range(static_cast<olive::VideoParams::ColorRange>(v.color_range));
return vp;
}
olive::TimeRange to_range(int64_t in_num, int64_t in_den, int64_t out_num,
int64_t out_den)
{
return olive::TimeRange(olive::Rational(in_num, in_den),
olive::Rational(out_num, out_den));
}
} // namespace
// Owned result object: per-input tables plus every string the accessors can
// return, pre-converted to UTF-8 so the returned pointers stay valid until
// oakengine_traverse_db_free().
struct OakEngineTraverseDb {
struct Row {
int type = OAK_NODE_VALUE_NONE;
const olive::Node *source = nullptr;
QByteArray tag;
QByteArray value_string;
std::vector<QByteArray> splits;
};
struct Input {
QByteArray id;
olive::NodeValueTable table; // kept for table_element_index_for_hint
QVector<Row> rows;
};
QVector<Input> inputs;
};
namespace
{
OakEngineTraverseDb::Row convert_row(const olive::NodeValue &v)
{
OakEngineTraverseDb::Row row;
row.type = to_c_type(v.type());
row.source = v.source();
row.tag = v.tag().toUtf8();
row.value_string = olive::NodeValue::value_to_string(v, false).toUtf8();
const olive::SplitValue split = v.to_split_value();
for (const QVariant &component : split) {
row.splits.push_back(
olive::NodeValue::value_to_string(v.type(), component, true)
.toUtf8());
}
return row;
}
OakEngineTraverseDb::Input convert_input(const QString &id,
const olive::NodeValueTable &table)
{
OakEngineTraverseDb::Input input;
input.id = id.toUtf8();
input.table = table;
input.rows.reserve(table.count());
for (int i = 0; i < table.count(); i++) {
input.rows.append(convert_row(table.at(i)));
}
return input;
}
const OakEngineTraverseDb::Row *row_at(const OakEngineTraverseDb *db,
int input_index, int row)
{
if (!db || input_index < 0 || input_index >= db->inputs.size() ||
row < 0 || row >= db->inputs.at(input_index).rows.size()) {
return nullptr;
}
return &db->inputs.at(input_index).rows.at(row);
}
} // namespace
extern "C"
{
OakEngineTraverseDb *oakengine_traverse_generate_database(
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
int64_t out_den)
{
if (!node || in_den == 0 || out_den == 0) {
return nullptr;
}
olive::Node *n = impl(node);
olive::NodeTraverser traverser;
const olive::NodeValueDatabase database =
traverser.generate_database(n, to_range(in_num, in_den, out_num, out_den));
auto *db = new OakEngineTraverseDb;
// NodeValueDatabase is a QHash; emit entries in the node's input order so
// the C-side index mapping is deterministic.
for (const QString &id : n->inputs()) {
auto it = database.cbegin();
for (; it != database.cend(); ++it) {
if (it.key() == id) {
break;
}
}
if (it != database.cend()) {
db->inputs.append(convert_input(id, it.value()));
}
}
return db;
}
OakEngineTraverseDb *oakengine_traverse_generate_table(
OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
int64_t out_den)
{
if (!node || in_den == 0 || out_den == 0) {
return nullptr;
}
olive::NodeTraverser traverser;
const olive::NodeValueTable table = traverser.generate_table(
impl(node), to_range(in_num, in_den, out_num, out_den));
auto *db = new OakEngineTraverseDb;
// A bare output table has no input id; represented as a single entry
// with an empty id (see traverse.h).
db->inputs.append(convert_input(QString(), table));
return db;
}
void oakengine_traverse_db_free(OakEngineTraverseDb *db)
{
delete db;
}
int oakengine_traverse_db_input_count(const OakEngineTraverseDb *db)
{
return db ? int(db->inputs.size()) : 0;
}
const char *oakengine_traverse_db_input_id(const OakEngineTraverseDb *db,
int input_index)
{
if (!db || input_index < 0 || input_index >= db->inputs.size()) {
return nullptr;
}
return db->inputs.at(input_index).id.constData();
}
int oakengine_traverse_db_row_count(const OakEngineTraverseDb *db,
int input_index)
{
if (!db || input_index < 0 || input_index >= db->inputs.size()) {
return 0;
}
return int(db->inputs.at(input_index).rows.size());
}
int oakengine_traverse_row_type(const OakEngineTraverseDb *db, int input_index,
int row)
{
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
return r ? r->type : OAK_NODE_VALUE_NONE;
}
OakEngineNode *oakengine_traverse_row_source(const OakEngineTraverseDb *db,
int input_index, int row)
{
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
return (r && r->source) ?
reinterpret_cast<OakEngineNode *>(
const_cast<olive::Node *>(r->source)) :
nullptr;
}
const char *oakengine_traverse_row_tag(const OakEngineTraverseDb *db,
int input_index, int row)
{
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
static const char empty[] = "";
return r ? r->tag.constData() : empty;
}
const char *oakengine_traverse_row_value_string(const OakEngineTraverseDb *db,
int input_index, int row)
{
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
return r ? r->value_string.constData() : nullptr;
}
int oakengine_traverse_row_split_count(const OakEngineTraverseDb *db,
int input_index, int row)
{
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
return r ? int(r->splits.size()) : 0;
}
const char *oakengine_traverse_row_split_string(const OakEngineTraverseDb *db,
int input_index, int row,
int split)
{
const OakEngineTraverseDb::Row *r = row_at(db, input_index, row);
if (!r || split < 0 || split >= int(r->splits.size())) {
return nullptr;
}
return r->splits[size_t(split)].constData();
}
int oakengine_traverse_table_element_index_for_hint(
OakEngineNode *hint_node, const char *input_id, int element,
const OakEngineTraverseDb *table_db)
{
if (!hint_node || !input_id || !table_db || table_db->inputs.size() != 1) {
return -1;
}
olive::NodeTraverser traverser;
return traverser.generate_row_value_element_index(
impl(hint_node), QString::fromUtf8(input_id), element,
&table_db->inputs.first().table);
}
int oakengine_traverse_generate_row(OakEngineNode *node, int64_t in_num,
int64_t in_den, int64_t out_num,
int64_t out_den,
const oak_video_params *cache_video_params,
int sample_rate, uint64_t channel_layout,
void *row_out)
{
if (!node || !row_out || in_den == 0 || out_den == 0) {
return OAKENGINE_E_INVALID;
}
olive::NodeTraverser traverser;
if (cache_video_params) {
traverser.set_cache_video_params(to_cpp(*cache_video_params));
}
if (sample_rate > 0) {
traverser.set_cache_audio_params(
olive::AudioParams(sample_rate, channel_layout,
olive::core::SampleFormat::f32_p));
}
// Transition bridge: row_out is the application's own olive::NodeValueRow
// (a QHash typedef), filled in place.
auto *row = static_cast<olive::NodeValueRow *>(row_out);
*row = traverser.generate_row(impl(node),
to_range(in_num, in_den, out_num, out_den));
return OAKENGINE_OK;
}
int oakengine_traverse_transform(OakEngineNode *start, OakEngineNode *end,
int64_t in_num, int64_t in_den,
int64_t out_num, int64_t out_den,
const oak_video_params *cache_video_params,
double out_m[6])
{
if (!start || !end || !out_m || in_den == 0 || out_den == 0) {
return OAKENGINE_E_INVALID;
}
olive::NodeTraverser traverser;
if (cache_video_params) {
traverser.set_cache_video_params(to_cpp(*cache_video_params));
}
QTransform t;
traverser.transform(&t, impl(start), impl(end),
to_range(in_num, in_den, out_num, out_den));
out_m[0] = t.m11();
out_m[1] = t.m12();
out_m[2] = t.m21();
out_m[3] = t.m22();
out_m[4] = t.dx();
out_m[5] = t.dy();
return OAKENGINE_OK;
}
} // extern "C"
+711
View File
@@ -0,0 +1,711 @@
/***
Oak - 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 "oakengine/undo.h"
#include <cstring>
#include <QAction>
#include <QByteArray>
#include <QString>
#include <QVector>
#include "olive/core/util/timecodefunctions.h"
#include "coreengine.h"
#include "node/block/block.h"
#include "node/block/clip/clip.h"
#include "node/block/transition/transition.h"
#include "node/nodeundo.h"
#include "node/output/track/track.h"
#include "node/output/track/tracklist.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
#include "node/value.h"
#include "timeline/timelinecommon.h"
#include "timeline/timelineundogeneral.h"
#include "timeline/timelineundopointer.h"
#include "timeline/timelineundoripple.h"
#include "timeline/timelineundosplit.h"
#include "undo/undocommand.h"
#include "undo/undostack.h"
#include "undointernal.h"
namespace
{
olive::UndoStack *stack()
{
if (olive::EngineCore *core = olive::EngineCore::instance()) {
return core->undo_stack();
}
return nullptr;
}
// buf/size string writer (same convention as capi/project.cpp).
int write_string(const QString &s, char *buf, int buf_size)
{
const QByteArray utf8 = s.toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
std::memcpy(buf, utf8.constData(), size_t(n));
buf[n] = '\0';
}
return len;
}
} // namespace
namespace
{
// Current open undo group. Owned by this TU; the facade owns it between
// group_begin and group_end/group_abort.
olive::MultiUndoCommand *g_undo_group = nullptr;
QString g_undo_group_name;
} // namespace
olive::MultiUndoCommand *oakengine_undo_group_current(void)
{
return g_undo_group;
}
// Shared helper used by all capi TU push_or_run() locals.
void oakengine_undo_push_or_run(olive::UndoCommand *command, const QString &name)
{
if (olive::MultiUndoCommand *group = g_undo_group) {
group->add_child(command);
command->redo_now();
} else if (olive::UndoStack *s = stack()) {
s->push(command, name);
} else {
command->redo_now();
delete command;
}
}
extern "C" void *oakengine_undo_handle(void)
{
return stack();
}
extern "C" int oakengine_undo_push(void *command, const char *name)
{
if (!command) {
return OAKENGINE_E_INVALID;
}
auto *cmd = static_cast<olive::UndoCommand *>(command);
const QString label = name ? QString::fromUtf8(name) : QString();
if (olive::MultiUndoCommand *group = g_undo_group) {
group->add_child(cmd);
cmd->redo_now();
} else if (olive::UndoStack *s = stack()) {
s->push(cmd, label);
} else {
cmd->redo_now();
delete cmd;
}
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_group_begin(const char *name)
{
if (g_undo_group) {
return OAKENGINE_E_STATE;
}
g_undo_group = new olive::MultiUndoCommand();
g_undo_group_name = name ? QString::fromUtf8(name) : QString();
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_group_end(void)
{
if (!g_undo_group) {
return OAKENGINE_E_STATE;
}
olive::MultiUndoCommand *group = g_undo_group;
g_undo_group = nullptr;
QString name = g_undo_group_name;
g_undo_group_name.clear();
olive::UndoStack *s = stack();
if (!s) {
// No stack: just redo/undo nothing and delete.
delete group;
return OAKENGINE_OK;
}
// Empty group is discarded by push_pre_executed (mirrors push).
s->push_pre_executed(group, name);
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_group_abort(void)
{
if (!g_undo_group) {
return OAKENGINE_E_STATE;
}
olive::MultiUndoCommand *group = g_undo_group;
g_undo_group = nullptr;
g_undo_group_name.clear();
group->undo_now();
delete group;
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_command_redo_now(void *command)
{
if (!command) {
return OAKENGINE_E_INVALID;
}
static_cast<olive::UndoCommand *>(command)->redo_now();
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_command_undo_now(void *command)
{
if (!command) {
return OAKENGINE_E_INVALID;
}
static_cast<olive::UndoCommand *>(command)->undo_now();
return OAKENGINE_OK;
}
namespace
{
class CustomUndoCommand : public olive::UndoCommand {
public:
CustomUndoCommand(const QString &name,
oakengine_undo_command_redo_fn redo_cb,
oakengine_undo_command_undo_fn undo_cb,
oakengine_undo_command_free_fn free_cb,
void *userdata)
: name_(name)
, redo_fn_(redo_cb)
, undo_fn_(undo_cb)
, free_fn_(free_cb)
, userdata_(userdata)
{
}
virtual ~CustomUndoCommand() override
{
if (free_fn_) {
free_fn_(userdata_);
}
}
virtual olive::Project *get_relevant_project() const override
{
return nullptr;
}
protected:
virtual void redo() override
{
if (redo_fn_) {
redo_fn_(userdata_);
}
}
virtual void undo() override
{
if (undo_fn_) {
undo_fn_(userdata_);
}
}
private:
QString name_;
oakengine_undo_command_redo_fn redo_fn_;
oakengine_undo_command_undo_fn undo_fn_;
oakengine_undo_command_free_fn free_fn_;
void *userdata_;
};
} // namespace
namespace
{
// Map oak_node_value_type -> olive::NodeValue::Type (mirrors node.cpp).
olive::NodeValue::Type from_c_type(int t)
{
switch (t) {
case 0: return olive::NodeValue::k_none;
case 1: return olive::NodeValue::k_int;
case 2: return olive::NodeValue::k_float;
case 3: return olive::NodeValue::k_boolean;
case 4: return olive::NodeValue::k_rational;
case 5: return olive::NodeValue::k_color;
case 6: return olive::NodeValue::k_vec2;
case 7: return olive::NodeValue::k_vec3;
case 8: return olive::NodeValue::k_vec4;
case 9: return olive::NodeValue::k_combo;
case 10: return olive::NodeValue::k_file;
case 11: return olive::NodeValue::k_text;
case 12: return olive::NodeValue::k_font;
case 13: return olive::NodeValue::k_str_combo;
case 14: return olive::NodeValue::k_binary;
case 15: return olive::NodeValue::k_bezier;
case 16: return olive::NodeValue::k_texture;
case 17: return olive::NodeValue::k_samples;
case 18: return olive::NodeValue::k_video_params;
case 19: return olive::NodeValue::k_audio_params;
default: return olive::NodeValue::k_none;
}
}
const olive::Sequence *sequence_from_block(const olive::Block *block)
{
if (!block || !block->track()) {
return nullptr;
}
return block->track()->sequence();
}
olive::Rational sequence_time_base(const olive::Sequence *seq)
{
if (seq) {
const olive::Rational fr = seq->get_video_params().frame_rate();
if (!fr.isNull() && !fr.isNaN()) {
return fr.flipped();
}
}
return olive::Rational(1001, 30000);
}
olive::Rational ts_to_time(int64_t ts, const olive::Rational &tb)
{
return olive::core::Timecode::timestamp_to_time(ts, tb);
}
olive::Timeline::MovementMode to_movement_mode(int mode)
{
switch (mode) {
case 1: return olive::Timeline::k_move;
case 2: return olive::Timeline::k_trim_in;
case 3: return olive::Timeline::k_trim_out;
default: return olive::Timeline::k_none;
}
}
} // namespace
extern "C" void *oakengine_undo_command_create(
const char *name,
oakengine_undo_command_redo_fn redo,
oakengine_undo_command_undo_fn undo,
oakengine_undo_command_free_fn free_fn,
void *userdata)
{
return new CustomUndoCommand(
name ? QString::fromUtf8(name) : QString(),
redo, undo, free_fn, userdata);
}
extern "C" void *oakengine_undo_command_create_multi(void)
{
return new olive::MultiUndoCommand();
}
extern "C" void *oakengine_node_add_command(void *project, void *node)
{
if (!project || !node) {
return nullptr;
}
return new olive::NodeAddCommand(
reinterpret_cast<olive::Project *>(project),
reinterpret_cast<olive::Node *>(node));
}
extern "C" void *oakengine_node_set_position_command(
void *node, void *context, double x, double y, int expanded)
{
if (!node || !context) {
return nullptr;
}
return new olive::NodeSetPositionCommand(
reinterpret_cast<olive::Node *>(node),
reinterpret_cast<olive::Node *>(context),
olive::Node::Position(QPointF(x, y), expanded != 0));
}
extern "C" void *oakengine_node_remove_position_command(
void *node, void *context)
{
if (!node || !context) {
return nullptr;
}
return new olive::NodeRemovePositionFromContextCommand(
reinterpret_cast<olive::Node *>(node),
reinterpret_cast<olive::Node *>(context));
}
extern "C" void *oakengine_node_set_value_hint_command(
void *node, const char *input, int element, int type, int index,
const char *tag)
{
if (!node || !input) {
return nullptr;
}
olive::Node *n = reinterpret_cast<olive::Node *>(node);
const QString id = QString::fromUtf8(input);
if (!n->inputs().contains(id)) {
return nullptr;
}
olive::NodeValue::Type nv_type = olive::NodeValue::k_none;
if (type >= 0) {
nv_type = from_c_type(type);
if (nv_type == olive::NodeValue::k_none && type != 0) {
return nullptr;
}
}
QVector<olive::NodeValue::Type> types;
if (nv_type != olive::NodeValue::k_none) {
types.append(nv_type);
}
return new olive::NodeSetValueHintCommand(
n, id, element,
olive::Node::ValueHint(types, index, QString::fromUtf8(tag ? tag : "")));
}
extern "C" void *oakengine_node_remove_and_disconnect_command(void *node)
{
if (!node) {
return nullptr;
}
return new olive::NodeRemoveAndDisconnectCommand(
reinterpret_cast<olive::Node *>(node));
}
extern "C" void *oakengine_track_place_block_command(
void *track_list, int track_index, void *block, int64_t in_ts)
{
if (!track_list || !block || in_ts < 0) {
return nullptr;
}
olive::TrackList *list = reinterpret_cast<olive::TrackList *>(track_list);
olive::Block *b = reinterpret_cast<olive::Block *>(block);
const olive::Rational tb = sequence_time_base(list->parent());
return new olive::TrackPlaceBlockCommand(
list, track_index, b, ts_to_time(in_ts, tb));
}
extern "C" void *oakengine_track_replace_block_with_gap_command(
void *track, void *block, int handle_transitions)
{
if (!track || !block) {
return nullptr;
}
return new olive::TrackReplaceBlockWithGapCommand(
reinterpret_cast<olive::Track *>(track),
reinterpret_cast<olive::Block *>(block),
handle_transitions != 0);
}
extern "C" void *oakengine_block_trim_command(
void *track, void *block, int64_t new_length_num, int64_t new_length_den,
int movement_mode, int roll_edit)
{
if (!track || !block || new_length_den == 0 ||
movement_mode < 0 || movement_mode > 3) {
return nullptr;
}
olive::Track *t = reinterpret_cast<olive::Track *>(track);
olive::Block *b = reinterpret_cast<olive::Block *>(block);
auto *cmd = new olive::BlockTrimCommand(
t, b,
olive::Rational(static_cast<int>(new_length_num),
static_cast<int>(new_length_den)),
to_movement_mode(movement_mode));
cmd->set_trim_is_a_roll_edit(roll_edit != 0);
return cmd;
}
extern "C" void *oakengine_transition_remove_command(
void *transition, int remove_from_graph)
{
if (!transition) {
return nullptr;
}
return new olive::TransitionRemoveCommand(
reinterpret_cast<olive::TransitionBlock *>(transition),
remove_from_graph != 0);
}
extern "C" void *oakengine_track_slide_command(
void *track, void *const *blocks, int block_count,
void *in_adjacent, void *out_adjacent,
int64_t movement_num, int64_t movement_den)
{
if (!track || !blocks || block_count <= 0 || movement_den == 0) {
return nullptr;
}
olive::Track *t = reinterpret_cast<olive::Track *>(track);
QList<olive::Block *> block_list;
block_list.reserve(block_count);
for (int i = 0; i < block_count; i++) {
if (!blocks[i]) {
return nullptr;
}
block_list.append(reinterpret_cast<olive::Block *>(blocks[i]));
}
return new olive::TrackSlideCommand(
t, block_list,
reinterpret_cast<olive::Block *>(in_adjacent),
reinterpret_cast<olive::Block *>(out_adjacent),
olive::Rational(static_cast<int>(movement_num),
static_cast<int>(movement_den)));
}
extern "C" void *oakengine_block_split_preserving_links_command(
void *const *blocks, int count, int64_t point_ts)
{
if (!blocks || count <= 0 || point_ts < 0) {
return nullptr;
}
QVector<olive::Block *> block_vec;
block_vec.reserve(count);
const olive::Sequence *seq = nullptr;
for (int i = 0; i < count; i++) {
if (!blocks[i]) {
return nullptr;
}
olive::Block *b = reinterpret_cast<olive::Block *>(blocks[i]);
if (!seq) {
seq = sequence_from_block(b);
}
block_vec.append(b);
}
const olive::Rational tb = sequence_time_base(seq);
const olive::Rational point = ts_to_time(point_ts, tb);
// BlockSplitPreservingLinksCommand takes a list of times, one per block.
QList<olive::Rational> times;
times.reserve(count);
for (int i = 0; i < count; i++) {
times.append(point);
}
return new olive::BlockSplitPreservingLinksCommand(block_vec, times);
}
extern "C" void *oakengine_block_split_get_split(
void *command, void *block, int time_index)
{
if (!command || !block) {
return nullptr;
}
auto *cmd = reinterpret_cast<olive::BlockSplitPreservingLinksCommand *>(
command);
return cmd->get_split(reinterpret_cast<olive::Block *>(block), time_index);
}
extern "C" void *oakengine_block_resize_with_media_in_command(
void *block, int64_t length_num, int64_t length_den)
{
if (!block || length_den == 0) {
return nullptr;
}
olive::Block *b = reinterpret_cast<olive::Block *>(block);
return new olive::BlockResizeWithMediaInCommand(
b, olive::Rational(static_cast<int>(length_num),
static_cast<int>(length_den)));
}
extern "C" void *oakengine_block_set_media_in_command(
void *block, int64_t media_in_num, int64_t media_in_den)
{
if (!block || media_in_den == 0) {
return nullptr;
}
olive::ClipBlock *clip = reinterpret_cast<olive::ClipBlock *>(block);
return new olive::BlockSetMediaInCommand(
clip, olive::Rational(static_cast<int>(media_in_num),
static_cast<int>(media_in_den)));
}
extern "C" void *oakengine_timeline_ripple_delete_gaps_command(
void *sequence, const int64_t *range_in_ts, const int64_t *range_out_ts,
const int *track_types, const int *track_indexes, int range_count)
{
if (!sequence || !range_in_ts || !range_out_ts ||
!track_types || !track_indexes || range_count <= 0) {
return nullptr;
}
olive::Sequence *seq = reinterpret_cast<olive::Sequence *>(sequence);
const olive::Rational tb = sequence_time_base(seq);
olive::TimelineRippleDeleteGapsAtRegionsCommand::RangeList ranges;
ranges.reserve(range_count);
for (int i = 0; i < range_count; i++) {
if (range_in_ts[i] < 0 || range_out_ts[i] <= range_in_ts[i] ||
track_types[i] < 0 || track_types[i] > 2 ||
track_indexes[i] < 0) {
return nullptr;
}
olive::TrackList *list = seq->track_list(
static_cast<olive::Track::Type>(track_types[i]));
if (!list || track_indexes[i] >= list->get_track_count()) {
return nullptr;
}
olive::Track *track = list->get_track_at(track_indexes[i]);
ranges.append(qMakePair(
track,
olive::TimeRange(ts_to_time(range_in_ts[i], tb),
ts_to_time(range_out_ts[i], tb))));
}
return new olive::TimelineRippleDeleteGapsAtRegionsCommand(seq, ranges);
}
extern "C" void *oakengine_track_list_insert_gaps_command(
void *track_list, int64_t point_num, int64_t point_den,
int64_t length_num, int64_t length_den)
{
if (!track_list || point_den == 0 || length_den == 0) {
return nullptr;
}
olive::TrackList *list = reinterpret_cast<olive::TrackList *>(track_list);
return new olive::TrackListInsertGaps(
list, olive::Rational(static_cast<int>(point_num),
static_cast<int>(point_den)),
olive::Rational(static_cast<int>(length_num),
static_cast<int>(length_den)));
}
extern "C" int oakengine_undo_command_multi_add_child(void *multi,
void *child)
{
if (!multi || !child) {
return OAKENGINE_E_INVALID;
}
static_cast<olive::MultiUndoCommand *>(multi)->add_child(
static_cast<olive::UndoCommand *>(child));
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_command_multi_child_count(void *multi)
{
if (!multi) {
return OAKENGINE_E_INVALID;
}
return static_cast<olive::MultiUndoCommand *>(multi)->child_count();
}
extern "C" void oakengine_undo_command_free(void *command)
{
delete static_cast<olive::UndoCommand *>(command);
}
extern "C" int64_t oakengine_undo_count(void)
{
olive::UndoStack *s = stack();
return s ? s->command_count() : OAKENGINE_E_INVALID;
}
extern "C" int64_t oakengine_undo_index(void)
{
olive::UndoStack *s = stack();
return s ? s->done_count() : OAKENGINE_E_INVALID;
}
extern "C" int oakengine_undo_command_text(int64_t row, char *buf,
int buf_size)
{
olive::UndoStack *s = stack();
if (!s) {
return OAKENGINE_E_INVALID;
}
if (row < 0 || row >= s->command_count()) {
return OAKENGINE_E_NOT_FOUND;
}
return write_string(s->command_name(row), buf, buf_size);
}
extern "C" int oakengine_undo_command_is_done(int64_t row)
{
olive::UndoStack *s = stack();
if (!s) {
return OAKENGINE_E_INVALID;
}
if (row < 0 || row >= s->command_count()) {
return OAKENGINE_E_NOT_FOUND;
}
return s->command_is_done(row) ? 1 : 0;
}
extern "C" int oakengine_undo_jump(int64_t index)
{
olive::UndoStack *s = stack();
if (!s) {
return OAKENGINE_E_STATE;
}
if (index < 0 || index > s->command_count()) {
return OAKENGINE_E_INVALID;
}
s->jump(size_t(index));
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_clear(void)
{
olive::UndoStack *s = stack();
if (!s) {
return OAKENGINE_E_STATE;
}
s->clear();
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_update_actions(void)
{
olive::UndoStack *s = stack();
if (!s) {
return OAKENGINE_E_STATE;
}
s->update_actions();
return OAKENGINE_OK;
}
extern "C" int oakengine_undo_can_undo(void)
{
olive::UndoStack *s = stack();
return s ? (s->can_undo() ? 1 : 0) : OAKENGINE_E_INVALID;
}
extern "C" int oakengine_undo_can_redo(void)
{
olive::UndoStack *s = stack();
return s ? (s->can_redo() ? 1 : 0) : OAKENGINE_E_INVALID;
}
extern "C" void *oakengine_undo_undo_action(void)
{
olive::UndoStack *s = stack();
return s ? static_cast<void *>(s->GetUndoAction()) : nullptr;
}
extern "C" void *oakengine_undo_redo_action(void)
{
olive::UndoStack *s = stack();
return s ? static_cast<void *>(s->GetRedoAction()) : nullptr;
}
+45
View File
@@ -0,0 +1,45 @@
/***
Oak - 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/>.
***/
#ifndef OAKENGINE_UNDOINTERNAL_H
#define OAKENGINE_UNDOINTERNAL_H
// Internal (not installed) shared declarations between undo.cpp and the
// other capi translation units. The undo-group state lives in undo.cpp;
// these helpers let node/timeline/etc. push commands into an active group
// instead of directly onto the global undo stack.
namespace olive
{
class MultiUndoCommand;
class UndoCommand;
}
class QString;
// Returns the currently active undo group, or nullptr if no group is open.
olive::MultiUndoCommand *oakengine_undo_group_current(void);
// Push `command` into the active group (eager redo) or onto the global undo
// stack. No-op if no engine core exists.
void oakengine_undo_push_or_run(olive::UndoCommand *command,
const QString &name);
#endif // OAKENGINE_UNDOINTERNAL_H
+619
View File
@@ -0,0 +1,619 @@
/***
Oak - 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 "oakengine/viewer.h"
#include <cstring>
#include <QByteArray>
#include <QVector>
#include "oakengine/timeline.h"
#include "node/output/viewer/viewer.h"
#include "node/output/track/track.h"
#include "node/block/clip/clip.h"
#include "node/nodeundo.h"
#include "node/param.h"
#include "render/playbackcache.h"
#include "render/framehashcache.h"
#include "render/videoparams.h"
#include "timeline/timelineworkarea.h"
namespace
{
olive::Node *impl(OakEngineNode *h)
{
return reinterpret_cast<olive::Node *>(h);
}
const olive::Node *impl(const OakEngineNode *h)
{
return reinterpret_cast<const olive::Node *>(h);
}
// Validated viewer accessor; nullptr when the handle is not a viewer.
olive::ViewerOutput *viewer_of(OakEngineNode *h)
{
return h ? dynamic_cast<olive::ViewerOutput *>(impl(h)) : nullptr;
}
const olive::ViewerOutput *viewer_of(const OakEngineNode *h)
{
return h ? dynamic_cast<const olive::ViewerOutput *>(impl(h)) : nullptr;
}
// ViewerOutput::get_playhead()/get_connected_waveform() are not const in the
// engine; the facade keeps const-correct handles and casts locally (same
// pattern as timeline.cpp's mutable_impl()).
olive::ViewerOutput *mutable_viewer(const OakEngineNode *h)
{
return const_cast<olive::ViewerOutput *>(viewer_of(h));
}
// olive::VideoParams -> oak_video_params POD (same mapping as
// encoding.cpp's from_cpp()).
void from_cpp(const olive::VideoParams &vp, oak_video_params *out)
{
out->width = vp.width();
out->height = vp.height();
out->time_base_num = vp.time_base().numerator();
out->time_base_den = vp.time_base().denominator();
out->format = int(vp.format());
out->pixel_aspect_num = vp.pixel_aspect_ratio().numerator();
out->pixel_aspect_den = vp.pixel_aspect_ratio().denominator();
out->interlacing = int(vp.interlacing());
out->color_range = int(vp.color_range());
out->divider = vp.divider();
out->video_type = int(vp.video_type());
out->premultiplied_alpha = vp.premultiplied_alpha() ? 1 : 0;
}
void rational_out(const olive::Rational &r, int64_t *num, int64_t *den)
{
if (num) {
*num = r.numerator();
}
if (den) {
*den = r.denominator();
}
}
// Track::Type values match the facade's OAKENGINE_TRACK_TYPE_* constants;
// assert it and convert explicitly anyway (k_none should never appear in an
// enabled-stream list).
static_assert(int(olive::Track::k_video) == OAKENGINE_TRACK_TYPE_VIDEO,
"track type mismatch");
static_assert(int(olive::Track::k_audio) == OAKENGINE_TRACK_TYPE_AUDIO,
"track type mismatch");
static_assert(int(olive::Track::k_subtitle) == OAKENGINE_TRACK_TYPE_SUBTITLE,
"track type mismatch");
int to_c_track_type(olive::Track::Type t)
{
switch (t) {
case olive::Track::k_video:
return OAKENGINE_TRACK_TYPE_VIDEO;
case olive::Track::k_audio:
return OAKENGINE_TRACK_TYPE_AUDIO;
case olive::Track::k_subtitle:
return OAKENGINE_TRACK_TYPE_SUBTITLE;
default:
return -1;
}
}
} // namespace
extern "C"
{
OakEngineNode *oakengine_viewer_from_node(OakEngineNode *node)
{
return viewer_of(node) ? node : nullptr;
}
const OakEngineNode *oakengine_viewer_from_const_node(const OakEngineNode *node)
{
return viewer_of(node) ? node : nullptr;
}
const char *oakengine_viewer_video_params_input_id(void)
{
static const QByteArray s =
olive::ViewerOutput::k_video_params_input.toUtf8();
return s.constData();
}
const char *oakengine_viewer_audio_params_input_id(void)
{
static const QByteArray s =
olive::ViewerOutput::k_audio_params_input.toUtf8();
return s.constData();
}
const char *oakengine_viewer_subtitle_params_input_id(void)
{
static const QByteArray s =
olive::ViewerOutput::k_subtitle_params_input.toUtf8();
return s.constData();
}
const char *oakengine_viewer_texture_input_id(void)
{
static const QByteArray s = olive::ViewerOutput::k_texture_input.toUtf8();
return s.constData();
}
const char *oakengine_viewer_samples_input_id(void)
{
static const QByteArray s = olive::ViewerOutput::k_samples_input.toUtf8();
return s.constData();
}
int oakengine_viewer_default_sample_format(void)
{
return int(olive::core::SampleFormat::Format(
olive::ViewerOutput::k_default_sample_format));
}
int oakengine_viewer_get_playhead(const OakEngineNode *self, int64_t *num,
int64_t *den)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
rational_out(mutable_viewer(self)->get_playhead(), num, den);
return OAKENGINE_OK;
}
int oakengine_viewer_set_playhead(OakEngineNode *self, int64_t num,
int64_t den)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
v->set_playhead(olive::Rational(num, den));
return OAKENGINE_OK;
}
int oakengine_viewer_set_video_params(OakEngineNode *self,
const oak_video_params *params,
int index)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v || !params) {
return OAKENGINE_E_INVALID;
}
olive::VideoParams vp(
params->width, params->height,
olive::Rational(params->time_base_num, params->time_base_den),
static_cast<olive::core::PixelFormat::Format>(params->format),
olive::VideoParams::k_internal_channel_count,
olive::Rational(params->pixel_aspect_num, params->pixel_aspect_den),
static_cast<olive::VideoParams::Interlacing>(params->interlacing),
params->divider);
v->set_video_params(vp, index);
return OAKENGINE_OK;
}
int oakengine_viewer_set_audio_params(OakEngineNode *self, int sample_rate,
uint64_t channel_layout, int format,
int index)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
olive::AudioParams ap;
ap.set_sample_rate(sample_rate);
ap.set_channel_layout(channel_layout);
ap.set_format(static_cast<olive::core::SampleFormat::Format>(format));
v->set_audio_params(ap, index);
return OAKENGINE_OK;
}
int oakengine_viewer_get_length(const OakEngineNode *self, int64_t *num,
int64_t *den)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
rational_out(v->get_length(), num, den);
return OAKENGINE_OK;
}
int oakengine_viewer_get_video_length(const OakEngineNode *self, int64_t *num,
int64_t *den)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
rational_out(v->get_video_length(), num, den);
return OAKENGINE_OK;
}
int oakengine_viewer_get_audio_length(const OakEngineNode *self, int64_t *num,
int64_t *den)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
rational_out(v->get_audio_length(), num, den);
return OAKENGINE_OK;
}
int oakengine_viewer_get_video_params(const OakEngineNode *self, int index,
oak_video_params *out)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v || !out) {
return OAKENGINE_E_INVALID;
}
memset(out, 0, sizeof(*out));
from_cpp(v->get_video_params(index), out);
return OAKENGINE_OK;
}
int oakengine_viewer_get_audio_params(const OakEngineNode *self, int index,
int *sample_rate,
uint64_t *channel_layout, int *format)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
if (sample_rate) {
*sample_rate = 0;
}
if (channel_layout) {
*channel_layout = 0;
}
if (format) {
*format = 0;
}
if (index < 0 || index >= v->get_audio_stream_count()) {
return OAKENGINE_OK;
}
const olive::AudioParams params = v->get_audio_params(index);
if (sample_rate) {
*sample_rate = params.sample_rate();
}
if (channel_layout) {
*channel_layout = params.channel_layout();
}
if (format) {
*format = int(olive::core::SampleFormat::Format(params.format()));
}
return OAKENGINE_OK;
}
int oakengine_viewer_get_video_stream_count(const OakEngineNode *self)
{
const olive::ViewerOutput *v = viewer_of(self);
return v ? v->get_video_stream_count() : 0;
}
int oakengine_viewer_get_audio_stream_count(const OakEngineNode *self)
{
const olive::ViewerOutput *v = viewer_of(self);
return v ? v->get_audio_stream_count() : 0;
}
int oakengine_viewer_get_subtitle_stream_count(const OakEngineNode *self)
{
const olive::ViewerOutput *v = viewer_of(self);
return v ? v->get_subtitle_stream_count() : 0;
}
int oakengine_viewer_get_stream_enabled(const OakEngineNode *self,
int track_type, int index)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
switch (track_type) {
case OAKENGINE_TRACK_TYPE_VIDEO:
return index >= 0 && index < v->get_video_stream_count() &&
v->get_video_params(index).enabled();
case OAKENGINE_TRACK_TYPE_AUDIO:
return index >= 0 && index < v->get_audio_stream_count() &&
v->get_audio_params(index).enabled();
case OAKENGINE_TRACK_TYPE_SUBTITLE:
return index >= 0 && index < v->get_subtitle_stream_count() &&
v->get_subtitle_params(index).enabled();
default:
return OAKENGINE_E_INVALID;
}
}
int oakengine_viewer_get_subtitle_count(const OakEngineNode *self, int index)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v || index < 0 || index >= v->get_subtitle_stream_count()) {
return OAKENGINE_E_INVALID;
}
return int(v->get_subtitle_params(index).size());
}
const void *oakengine_viewer_get_subtitle_at(const OakEngineNode *self,
int index, int sub_index)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v || index < 0 || index >= v->get_subtitle_stream_count()) {
return nullptr;
}
const olive::SubtitleParams &sp = v->get_subtitle_params(index);
if (sub_index < 0 || sub_index >= int(sp.size())) {
return nullptr;
}
return &sp[size_t(sub_index)];
}
int oakengine_viewer_has_enabled_streams(const OakEngineNode *self,
int track_type)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return 0;
}
switch (track_type) {
case OAKENGINE_TRACK_TYPE_VIDEO:
return v->has_enabled_video_streams() ? 1 : 0;
case OAKENGINE_TRACK_TYPE_AUDIO:
return v->has_enabled_audio_streams() ? 1 : 0;
case OAKENGINE_TRACK_TYPE_SUBTITLE:
return v->has_enabled_subtitle_streams() ? 1 : 0;
default:
return 0;
}
}
int oakengine_viewer_get_first_enabled_video_stream(const OakEngineNode *self,
oak_video_params *out)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v || !out) {
return OAKENGINE_E_INVALID;
}
memset(out, 0, sizeof(*out));
from_cpp(v->get_first_enabled_video_stream(), out);
return OAKENGINE_OK;
}
int oakengine_viewer_get_enabled_stream_count(const OakEngineNode *self)
{
const olive::ViewerOutput *v = viewer_of(self);
return v ? int(v->get_enabled_streams_as_references().size()) : 0;
}
int oakengine_viewer_get_enabled_streams(const OakEngineNode *self, int *types,
int *indices, int max)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v || max < 0) {
return 0;
}
const QVector<olive::Track::Reference> refs =
v->get_enabled_streams_as_references();
if (types && indices) {
const int n = qMin(int(refs.size()), max);
for (int i = 0; i < n; i++) {
types[i] = to_c_track_type(refs.at(i).type());
indices[i] = refs.at(i).index();
}
}
return int(refs.size());
}
int oakengine_viewer_get_workarea(const OakEngineNode *self,
oakengine_viewer_workarea *out)
{
const olive::ViewerOutput *v = viewer_of(self);
if (!v || !out) {
return OAKENGINE_E_INVALID;
}
const olive::TimelineWorkArea *workarea = v->get_work_area();
out->in_num = workarea->in().numerator();
out->in_den = workarea->in().denominator();
out->out_num = workarea->out().numerator();
out->out_den = workarea->out().denominator();
out->enabled = workarea->enabled() ? 1 : 0;
return OAKENGINE_OK;
}
int oakengine_viewer_set_workarea_range(OakEngineNode *self, int64_t in_num,
int64_t in_den, int64_t out_num,
int64_t out_den)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
v->get_work_area()->set_range(
olive::TimeRange(olive::Rational(in_num, in_den),
olive::Rational(out_num, out_den)));
return OAKENGINE_OK;
}
int oakengine_viewer_set_workarea_enabled(OakEngineNode *self, int enabled)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
v->get_work_area()->set_enabled(enabled != 0);
return OAKENGINE_OK;
}
int oakengine_viewer_set_default_parameters(OakEngineNode *self)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
v->set_default_parameters();
return OAKENGINE_OK;
}
extern "C" void *oakengine_viewer_set_preview_divider_command(
OakEngineNode *self, int divider)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v || divider < 1) {
return nullptr;
}
olive::VideoParams current = v->get_video_params();
if (current.divider() == divider) {
return nullptr;
}
const olive::VideoParams updated(
current.width(), current.height(), current.time_base(),
current.format(), current.channel_count(),
current.pixel_aspect_ratio(), current.interlacing(), divider);
return new olive::NodeParamSetStandardValueCommand(
olive::NodeKeyframeTrackReference(
olive::NodeInput(v, olive::ViewerOutput::k_video_params_input), 0),
QVariant::fromValue(updated));
}
int oakengine_viewer_set_parameters_from_footage(
OakEngineNode *self, OakEngineNode *const *footage, int count)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v || count < 0 || (count > 0 && !footage)) {
return OAKENGINE_E_INVALID;
}
QVector<olive::ViewerOutput *> viewers;
viewers.reserve(count);
for (int i = 0; i < count; i++) {
olive::ViewerOutput *f = viewer_of(footage[i]);
if (!f) {
return OAKENGINE_E_INVALID;
}
viewers.append(f);
}
v->set_parameters_from_footage(viewers);
return OAKENGINE_OK;
}
int oakengine_viewer_set_waveform_enabled(OakEngineNode *self, int enabled)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return OAKENGINE_E_INVALID;
}
v->set_waveform_enabled(enabled != 0);
return OAKENGINE_OK;
}
const void *oakengine_viewer_get_connected_waveform(const OakEngineNode *self)
{
olive::ViewerOutput *v = mutable_viewer(self);
if (!v) {
return nullptr;
}
return static_cast<const void *>(v->get_connected_waveform());
}
OakEngineMarkerList *oakengine_viewer_get_marker_list(OakEngineNode *self)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return nullptr;
}
return reinterpret_cast<OakEngineMarkerList *>(v->get_markers());
}
OakEngineWorkarea *oakengine_viewer_get_workarea_handle(OakEngineNode *self)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return nullptr;
}
return reinterpret_cast<OakEngineWorkarea *>(v->get_work_area());
}
/* ---- Playback cache / frame cache ------------------------------------------ */
OakEnginePlaybackCache *
oakengine_viewer_get_playback_cache(OakEngineNode *self)
{
if (!self) {
return nullptr;
}
olive::ClipBlock *clip = dynamic_cast<olive::ClipBlock *>(
reinterpret_cast<olive::Node *>(self));
if (!clip) {
return nullptr;
}
return reinterpret_cast<OakEnginePlaybackCache *>(
clip->connected_video_cache());
}
int oakengine_playback_cache_indicator_height(void)
{
return olive::PlaybackCache::get_cache_indicator_height();
}
int oakengine_playback_cache_valid_ranges(OakEnginePlaybackCache *cache,
int64_t *ranges, int max)
{
if (!cache) {
return OAKENGINE_E_INVALID;
}
olive::PlaybackCache *pc = reinterpret_cast<olive::PlaybackCache *>(cache);
const olive::TimeRangeList &valid = pc->get_validated_ranges();
const int count = qMin(max, int(valid.size()));
for (int i = 0; i < count; i++) {
ranges[i * 4 + 0] = valid.at(i).in().numerator();
ranges[i * 4 + 1] = valid.at(i).in().denominator();
ranges[i * 4 + 2] = valid.at(i).out().numerator();
ranges[i * 4 + 3] = valid.at(i).out().denominator();
}
return count;
}
OakEngineFrameCache *oakengine_viewer_get_frame_cache(OakEngineNode *self)
{
olive::ViewerOutput *v = viewer_of(self);
if (!v) {
return nullptr;
}
// For a clip, get its connected video cache as a FrameHashCache.
if (olive::ClipBlock *clip = dynamic_cast<olive::ClipBlock *>(v)) {
return reinterpret_cast<OakEngineFrameCache *>(
clip->connected_video_cache());
}
return nullptr;
}
} // extern "C"
+945
View File
@@ -0,0 +1,945 @@
/***
Oak - 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 "oakengine/worker.h"
#include <cstdio>
#include <cstring>
#include <csignal>
#include <memory>
#include <optional>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMatrix4x4>
#include <QOpenGLContext>
#include <QSurfaceFormat>
#ifdef Q_OS_LINUX
#include <execinfo.h>
#include <unistd.h>
#endif
#include "common/qtutils.h"
#include "config/config.h"
#include "coreengine.h"
#include "node/factory.h"
#include "node/input/multicam/multicamnode.h"
#include "node/project/serializer/serializer.h"
#include "render/diskmanager.h"
#include "render/framemanager.h"
#include "render/ipc/frameslotpool.h"
#include "render/ipc/ipcmessage.h"
#include "render/ipc/sharedmemoryregion.h"
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
#include "render/backend/dynamicrenderer.h"
#endif
#include "render/opengl/openglrenderer.h"
#include "render/rendermanager.h"
#include "render/renderprocessor.h"
#include "render/colorprocessor.h"
#include "render/colortransform.h"
#ifdef Q_OS_MACOS
void HideWorkerDockIcon();
#endif
namespace
{
#ifdef Q_OS_LINUX
void print_backtrace(int sig)
{
void *array[50];
size_t size = backtrace(array, 50);
fprintf(stderr, "worker: caught signal %d, backtrace:\n", sig);
backtrace_symbols_fd(array, size, STDERR_FILENO);
fflush(stderr);
_exit(128 + sig);
}
#endif
constexpr int k_protocol_version = 1;
constexpr int k_default_width = 1920;
constexpr int k_default_height = 1080;
constexpr int k_default_frame_rate = 24;
void install_surface_format()
{
QSurfaceFormat format;
format.setVersion(3, 2);
format.setProfile(QSurfaceFormat::CoreProfile);
format.setDepthBufferSize(24);
QSurfaceFormat::setDefaultFormat(format);
}
void log_error(const QString &message)
{
const QByteArray line = QByteArray("worker: ") + message.toUtf8() + '\n';
fwrite(line.constData(), 1, size_t(line.size()), stderr);
fflush(stderr);
}
QJsonObject error_message(const QString &message, qint64 ticket_id = 0)
{
QJsonObject o;
o["type"] = olive::ipc::msgtype::k_error;
o["message"] = message;
if (ticket_id) {
o["ticket"] = double(ticket_id);
}
return o;
}
} // namespace
/**
* @brief Engine-internal render worker session.
*
* Holds the whole worker-side state machine (renderer, loaded project,
* shared-memory frame pools, shader/color caches) and answers one NDJSON
* control message at a time. Responses that the process main loop would
* write to stdout are produced into `response` instead, so the session is
* transport-agnostic and unit-testable. Owned via the OakWorkerSession C
* handle.
*/
struct __attribute__((visibility("hidden"))) OakWorkerSession {
olive::Renderer *renderer = nullptr;
bool shutdown_requested = false;
bool runtime_initialized = false;
std::unique_ptr<olive::Project> project;
QHash<QString, olive::Node *> node_by_token;
olive::ipc::SharedMemoryRegion output_region;
std::optional<olive::ipc::FrameSlotPool> output_pool;
olive::ipc::SharedMemoryRegion input_region;
std::optional<olive::ipc::FrameSlotPool> input_pool;
olive::ShaderCache shader_cache;
QHash<QString, olive::ColorProcessorPtr> color_processor_cache;
~OakWorkerSession()
{
project.reset();
if (runtime_initialized) {
olive::ProjectSerializer::destroy();
olive::DiskManager::destroy_instance();
olive::FrameManager::destroy_instance();
olive::NodeFactory::destroy();
}
if (renderer) {
renderer->destroy();
renderer->post_destroy();
delete renderer;
}
}
bool initialize_runtime()
{
if (runtime_initialized) {
return true;
}
// The session API is also used without oakengine_worker_main() (unit
// tests, embedded harnesses), where no QApplication exists yet.
// Config/managers below require one (QSettings, QStandardPaths), so
// create a minimal offscreen instance, mirroring oakengine_init().
if (!QCoreApplication::instance()) {
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
qputenv("QT_QPA_PLATFORM", "offscreen");
}
static int argc = 1;
static char app_name[] = "oak-worker";
static char *argv[] = { app_name, nullptr };
// Never deleted: QCoreApplication is a process-lifetime object.
new QGuiApplication(argc, argv);
QCoreApplication::setOrganizationName(
QStringLiteral("oakvideoeditor.org"));
QCoreApplication::setApplicationName(
QStringLiteral("oak-render-worker"));
}
// Create a minimal EngineCore instance so that code paths calling
// EngineCore::instance() (e.g. ViewerOutput::data for timecode display)
// do not dereference null. The worker has no UI, so the plain engine
// core is sufficient. The worker is short-lived; leaking this on exit
// is harmless.
if (!olive::EngineCore::instance()) {
new olive::EngineCore(olive::EngineCore::CoreParams());
}
olive::Config::load();
olive::NodeFactory::initialize();
olive::ColorManager::set_up_default_config();
olive::FrameManager::create_instance();
olive::DiskManager::create_instance();
olive::ProjectSerializer::initialize();
runtime_initialized = true;
return true;
}
QJsonObject startup_handshake() const
{
olive::ipc::HandshakeMsg hs;
hs.protocol_version = k_protocol_version;
hs.shm_key = QString();
hs.input_shm_key = QString();
hs.input_slots = 0;
hs.output_slots = 0;
hs.slot_data_bytes = 0;
hs.input_slot_data_bytes = 0;
QJsonObject handshake = hs.to_json();
if (QOpenGLContext *ctx = gl_context()) {
const QSurfaceFormat fmt = ctx->format();
handshake["gl_major"] = fmt.majorVersion();
handshake["gl_minor"] = fmt.minorVersion();
}
return handshake;
}
QOpenGLContext *gl_context() const
{
if (!renderer) {
return nullptr;
}
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
if (auto *dynamic_renderer =
dynamic_cast<olive::DynamicRenderer *>(renderer)) {
return dynamic_renderer->open_gl_context();
}
#endif
return static_cast<olive::OpenGLRenderer *>(renderer)->context();
}
/// Handle one parsed control message. `response` is left untouched when
/// the message has no reply (successful handshake, cancel, shutdown).
/// Returns false only on an internal failure the main loop treats as
/// fatal.
bool handle(const QJsonObject &message, QJsonObject *response)
{
const QString type = message["type"].toString();
if (type == QLatin1String(olive::ipc::msgtype::k_handshake)) {
olive::ipc::HandshakeMsg hs;
if (!olive::ipc::HandshakeMsg::from_json(message, &hs)) {
*response =
error_message(QStringLiteral("invalid handshake message"));
return true;
}
return attach_output_pool(hs, response);
}
if (type == QLatin1String(olive::ipc::msgtype::k_load_graph)) {
olive::ipc::LoadGraphMsg load;
if (!olive::ipc::LoadGraphMsg::from_json(message, &load)) {
*response =
error_message(QStringLiteral("invalid load_graph message"));
return true;
}
return load_graph(load.path, response);
}
if (type == QLatin1String(olive::ipc::msgtype::k_render_frame)) {
olive::ipc::RenderFrameMsg render;
if (!olive::ipc::RenderFrameMsg::from_json(message, &render)) {
*response = error_message(
QStringLiteral("invalid render_frame message"));
return true;
}
return render_frame(render, response);
}
if (type == QLatin1String(olive::ipc::msgtype::k_cancel)) {
// Stage 5 wires cancellation into in-flight jobs. Stage 2 has only synchronous single-frame work.
return true;
}
if (type == QLatin1String(olive::ipc::msgtype::k_shutdown)) {
shutdown_requested = true;
return true;
}
*response =
error_message(QStringLiteral("unknown message type: %1").arg(type));
return true;
}
private:
bool attach_output_pool(const olive::ipc::HandshakeMsg &hs,
QJsonObject *response)
{
if (hs.protocol_version != k_protocol_version) {
*response =
error_message(QStringLiteral("unsupported protocol version %1")
.arg(hs.protocol_version));
return true;
}
if (hs.shm_key.isEmpty() || hs.output_slots <= 0 ||
hs.slot_data_bytes <= 0) {
*response = error_message(QStringLiteral(
"handshake missing output shared-memory geometry"));
return true;
}
const size_t bytes = olive::ipc::FrameSlotPool::bytes_needed(
uint32_t(hs.output_slots), size_t(hs.slot_data_bytes));
if (!output_region.open(hs.shm_key, bytes,
olive::ipc::SharedMemoryRegion::k_attach)) {
*response = error_message(
QStringLiteral("failed to attach shared memory: %1")
.arg(output_region.error()));
return true;
}
output_pool = olive::ipc::FrameSlotPool::attach(output_region.data());
if (!output_pool->is_valid()) {
output_region.close();
output_pool.reset();
*response = error_message(QStringLiteral(
"shared memory does not contain a frame slot pool"));
return true;
}
input_pool.reset();
input_region.close();
if (hs.input_slots > 0) {
if (hs.input_shm_key.isEmpty() || hs.input_slot_data_bytes <= 0) {
*response = error_message(QStringLiteral(
"handshake missing input shared-memory geometry"));
return true;
}
const size_t input_bytes = olive::ipc::FrameSlotPool::bytes_needed(
uint32_t(hs.input_slots), size_t(hs.input_slot_data_bytes));
if (!input_region.open(hs.input_shm_key, input_bytes,
olive::ipc::SharedMemoryRegion::k_attach)) {
*response = error_message(
QStringLiteral("failed to attach input shared memory: %1")
.arg(input_region.error()));
return true;
}
input_pool =
olive::ipc::FrameSlotPool::attach(input_region.data());
if (!input_pool->is_valid()) {
input_region.close();
input_pool.reset();
*response = error_message(QStringLiteral(
"input shared memory does not contain a frame slot pool"));
return true;
}
}
return true;
}
bool load_graph(const QString &path, QJsonObject *response)
{
{
QFileInfo fi(path);
if (!fi.exists()) {
log_error(
QStringLiteral("LoadGraph: graph file does not exist: %1")
.arg(path));
*response = error_message(
QStringLiteral("graph file does not exist: %1").arg(path));
return true;
}
if (fi.size() == 0) {
log_error(QStringLiteral("LoadGraph: graph file is empty: %1")
.arg(path));
*response = error_message(
QStringLiteral("graph file is empty: %1").arg(path));
return true;
}
log_error(
QStringLiteral("LoadGraph: loading %1 (%2 bytes, readable=%3)")
.arg(path)
.arg(fi.size())
.arg(fi.isReadable()));
}
auto loaded = std::make_unique<olive::Project>();
// Do not call Initialize() here: project serializers expect a blank
// project (root_ == nullptr) and will set root themselves. Calling
// Initialize() first triggers Q_ASSERT(!root_) in Project::Load.
olive::ProjectSerializer::Result result =
olive::ProjectSerializer::load(loaded.get(), path,
olive::ProjectSerializer::k_project);
if (result != olive::ProjectSerializer::k_success) {
*response =
error_message(QStringLiteral("failed to load graph %1: %2")
.arg(path, result.get_details()));
return true;
}
project = std::move(loaded);
node_by_token.clear();
color_processor_cache.clear();
const auto &data = result.get_load_data();
for (auto it = data.node_ptrs.cbegin(); it != data.node_ptrs.cend();
++it) {
node_by_token.insert(QString::number(it.key()), it.value());
}
for (auto it = data.node_uuids.cbegin(); it != data.node_uuids.cend();
++it) {
node_by_token.insert(it.value().toString(), it.key());
node_by_token.insert(it.value().toString(QUuid::WithoutBraces),
it.key());
}
QJsonObject ack;
ack["type"] = QStringLiteral("graph_loaded");
ack["nodes"] = node_by_token.size();
*response = ack;
return true;
}
olive::Node *find_node(const QString &token) const
{
if (olive::Node *node = node_by_token.value(token, nullptr)) {
return node;
}
bool ok = false;
const quintptr ptr = token.toULongLong(&ok, 0);
if (ok) {
return node_by_token.value(QString::number(ptr), nullptr);
}
return nullptr;
}
bool render_frame(const olive::ipc::RenderFrameMsg &message,
QJsonObject *response)
{
if (!project) {
*response = error_message(
QStringLiteral("render_frame received before load_graph"),
message.ticket_id);
return true;
}
if (!output_pool || !output_pool->is_valid()) {
*response = error_message(
QStringLiteral(
"render_frame received before output shm handshake"),
message.ticket_id);
return true;
}
olive::Node *node = find_node(message.node_uuid);
if (!node) {
*response =
error_message(QStringLiteral("render node not found: %1")
.arg(message.node_uuid),
message.ticket_id);
return true;
}
QVector<int> input_slots;
const QVector<int> requested_input_slots =
message.input_slots.isEmpty() && message.input_slot >= 0 ?
QVector<int>{ message.input_slot } :
message.input_slots;
if (!requested_input_slots.isEmpty()) {
if (!input_pool || !input_pool->is_valid()) {
*response = error_message(
QStringLiteral(
"render_frame referenced input slot without input pool"),
message.ticket_id);
return true;
}
for (int requested_slot : requested_input_slots) {
if (requested_slot < 0 ||
requested_slot >= int(input_pool->slot_count())) {
for (int slot : input_slots) {
input_pool->release(uint32_t(slot));
}
*response = error_message(
QStringLiteral("input slot index out of range"),
message.ticket_id);
return true;
}
uint32_t consumed_slot = 0;
if (!input_pool->consume(&consumed_slot)) {
for (int slot : input_slots) {
input_pool->release(uint32_t(slot));
}
*response =
error_message(QStringLiteral("input slot was not ready"),
message.ticket_id);
return true;
}
if (int(consumed_slot) != requested_slot) {
input_pool->release(consumed_slot);
for (int slot : input_slots) {
input_pool->release(uint32_t(slot));
}
*response = error_message(
QStringLiteral("input slot order mismatch"),
message.ticket_id);
return true;
}
input_slots.append(int(consumed_slot));
}
}
olive::VideoParams vparams(
message.width > 0 ? message.width : k_default_width,
message.height > 0 ? message.height : k_default_height,
olive::Rational(1, k_default_frame_rate),
message.format >= 0 ? olive::PixelFormat::Format(message.format) :
olive::PixelFormat::f32,
message.channel_count > 0 ? message.channel_count :
olive::VideoParams::k_rgba_channel_count);
olive::RenderTicketPtr ticket = std::make_shared<olive::RenderTicket>();
ticket->setProperty("node", olive::QtUtils::ptr_to_value(node));
ticket->setProperty("time",
QVariant::fromValue(olive::Rational(
int(message.time_num), int(message.time_den))));
ticket->setProperty("size", QSize(message.width, message.height));
ticket->setProperty("matrix", QMatrix4x4());
ticket->setProperty("format",
message.format >= 0 ?
olive::PixelFormat::Format(message.format) :
olive::PixelFormat::invalid);
ticket->setProperty("usecache", false);
ticket->setProperty("channelcount", message.channel_count);
ticket->setProperty("mode", olive::RenderMode::Mode(message.mode));
ticket->setProperty("type", olive::RenderManager::k_type_video);
ticket->setProperty("colormanager", olive::QtUtils::ptr_to_value(
project->color_manager()));
{
olive::ColorProcessorPtr color_output;
if (message.has_color_transform) {
QString cache_key = QStringLiteral("%1|%2|%3|%4")
.arg(message.color_is_display ? 1 : 0)
.arg(message.color_output,
message.color_view,
message.color_look);
auto it = color_processor_cache.find(cache_key);
if (it != color_processor_cache.end()) {
color_output = it.value();
} else {
olive::ColorTransform transform;
if (message.color_is_display) {
transform = olive::ColorTransform(message.color_output,
message.color_view,
message.color_look);
} else {
transform = olive::ColorTransform(message.color_output);
}
color_output = olive::ColorProcessor::create(
project->color_manager(),
project->color_manager()->get_reference_color_space(),
transform);
if (color_output) {
color_processor_cache.insert(cache_key, color_output);
}
}
}
ticket->setProperty("coloroutput",
QVariant::fromValue(color_output));
}
ticket->setProperty("vparam", QVariant::fromValue(vparams));
// The IPC render_frame message carries no audio parameters, but
// rendering a sequence that has audio content evaluates audio
// tracks with globals.aparams -- an empty AudioParams aborts
// (AudioParams::time_to_samples asserts is_valid). Use the render
// node's own audio parameters, mirroring the in-process render
// path (PreviewAutoCacher uses context->get_audio_params()).
olive::AudioParams aparam;
if (olive::ViewerOutput *viewer =
dynamic_cast<olive::ViewerOutput *>(node)) {
aparam = viewer->get_audio_params();
}
ticket->setProperty("aparam", QVariant::fromValue(aparam));
ticket->setProperty("return", olive::RenderManager::k_frame);
ticket->setProperty("cache", QString());
ticket->setProperty("cachetimebase",
QVariant::fromValue(olive::Rational(1)));
ticket->setProperty("cacheid", QVariant::fromValue(QUuid()));
ticket->setProperty("multicam", olive::QtUtils::ptr_to_value(
static_cast<void *>(nullptr)));
ticket->setProperty(
"ipc_input_pool",
// The engine reads this back as the internal implementation object
// (olive::engine::internal::ipc::FrameSlotPool), which is exactly
// what the C handle points at.
olive::QtUtils::ptr_to_value(input_pool ?
static_cast<void *>(
input_pool->handle()) :
static_cast<void *>(nullptr)));
QVariantList input_slot_values;
for (int slot : input_slots) {
input_slot_values.append(slot);
}
ticket->setProperty("ipc_input_slots", input_slot_values);
ticket->setProperty("ipc_input_slot_cursor", 0);
ticket->setProperty("ipc_input_slot",
input_slots.isEmpty() ? -1 : input_slots.front());
ticket->start();
olive::RenderProcessor::process(ticket, renderer, nullptr,
&shader_cache);
for (int slot : input_slots) {
input_pool->release(uint32_t(slot));
}
if (!ticket->has_result()) {
*response = error_message(QStringLiteral("render produced no frame"),
message.ticket_id);
return true;
}
olive::FramePtr frame = ticket->get().value<olive::FramePtr>();
if (!frame || !frame->is_allocated()) {
*response = error_message(QStringLiteral("render result was empty"),
message.ticket_id);
return true;
}
uint32_t slot = 0;
if (!output_pool->acquire(&slot)) {
*response =
error_message(QStringLiteral("no free output frame slot"),
message.ticket_id);
return true;
}
const int data_size = frame->linesize_bytes() * frame->height();
if (data_size > int(output_pool->slot_data_bytes())) {
output_pool->release(slot);
log_error(QString("Output frame size") + QString::number(data_size));
log_error(QString("Slot size") +
QString::number(output_pool->slot_data_bytes()));
*response = error_message(
QStringLiteral("rendered frame does not fit output slot "),
message.ticket_id);
return true;
}
std::memcpy(output_pool->slot_data(slot), frame->const_data(),
size_t(data_size));
olive::ipc::FrameSlotMeta *meta = output_pool->meta(slot);
meta->id = message.ticket_id;
meta->time_num = frame->timestamp().numerator();
meta->time_den = frame->timestamp().denominator();
meta->width = frame->width();
meta->height = frame->height();
meta->format = int32_t(frame->format());
meta->channel_count = frame->channel_count();
meta->linesize = frame->linesize_bytes();
meta->data_size = data_size;
if (!output_pool->publish(slot)) {
output_pool->release(slot);
*response = error_message(
QStringLiteral("failed to publish output frame slot"),
message.ticket_id);
return true;
}
olive::ipc::FrameReadyMsg ready;
ready.ticket_id = message.ticket_id;
ready.output_slot = int(slot);
*response = ready.to_json();
return true;
}
};
namespace
{
olive::Renderer *create_renderer(const char *backend, bool *valid)
{
*valid = false;
const QString backend_name =
backend && *backend ? QString::fromUtf8(backend).toLower() :
QStringLiteral("opengl");
olive::Renderer *renderer;
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
auto *dynamic_renderer = new olive::DynamicRenderer(backend_name);
if (dynamic_renderer->init()) {
dynamic_renderer->post_init();
renderer = dynamic_renderer;
} else {
delete dynamic_renderer;
qWarning() << "Failed to initialize dynamic" << backend_name
<< "backend, falling back to direct OpenGL renderer";
renderer = new olive::OpenGLRenderer();
if (!renderer->init()) {
log_error(QStringLiteral("failed to initialize OpenGL renderer"));
delete renderer;
return nullptr;
}
renderer->post_init();
}
#else
renderer = new olive::OpenGLRenderer();
if (!renderer->Init()) {
log_error(QStringLiteral("failed to initialize OpenGL renderer"));
delete renderer;
return nullptr;
}
renderer->PostInit();
#endif
// Validate the renderer. For OpenGL we check the GL context; for Vulkan we
// rely on init()/post_init() succeeding (there is no QOpenGLContext).
bool renderer_valid = true;
if (backend_name == QStringLiteral("opengl")) {
QOpenGLContext *ctx = nullptr;
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
if (auto *loaded_renderer =
dynamic_cast<olive::DynamicRenderer *>(renderer)) {
ctx = loaded_renderer->open_gl_context();
} else
#endif
{
ctx = static_cast<olive::OpenGLRenderer *>(renderer)->context();
}
if (!ctx || !ctx->isValid()) {
renderer_valid = false;
}
}
if (!renderer_valid) {
log_error(QStringLiteral("OpenGL context is not valid after init"));
renderer->destroy();
renderer->post_destroy();
delete renderer;
return nullptr;
}
*valid = true;
return renderer;
}
bool backend_requests_no_renderer(const char *backend)
{
if (!backend || !*backend) {
return true;
}
const QString name = QString::fromUtf8(backend).toLower();
return name == QStringLiteral("none");
}
} // namespace
extern "C" {
OakWorkerSession *oakengine_worker_session_create(const char *backend)
{
auto *session = new (std::nothrow) OakWorkerSession();
if (!session) {
return nullptr;
}
if (!backend_requests_no_renderer(backend)) {
bool valid = false;
session->renderer = create_renderer(backend, &valid);
}
return session;
}
void oakengine_worker_session_free(OakWorkerSession *self)
{
delete self;
}
int oakengine_worker_session_has_renderer(const OakWorkerSession *self)
{
return self && self->renderer ? 1 : 0;
}
int oakengine_worker_session_initialize_runtime(OakWorkerSession *self)
{
if (!self) {
return 0;
}
return self->initialize_runtime() ? 1 : 0;
}
int oakengine_worker_session_startup_handshake(OakWorkerSession *self,
char *buf, int buf_size)
{
if (!self) {
return -1;
}
const QByteArray json = QJsonDocument(self->startup_handshake())
.toJson(QJsonDocument::Compact);
if (buf && buf_size > 0) {
const int n = std::min(int(json.size()), buf_size - 1);
std::memcpy(buf, json.constData(), size_t(n));
buf[n] = '\0';
}
return int(json.size());
}
int oakengine_worker_session_handle_json(OakWorkerSession *self,
const char *line, char *response_buf,
int response_buf_size)
{
if (!self || !line) {
return -1;
}
QJsonParseError parse_error;
const QJsonDocument doc = QJsonDocument::fromJson(QByteArray(line),
&parse_error);
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
const QJsonObject response =
error_message(QStringLiteral("malformed control message"));
const QByteArray json =
QJsonDocument(response).toJson(QJsonDocument::Compact);
if (response_buf && response_buf_size > 0) {
const int n = std::min(int(json.size()), response_buf_size - 1);
std::memcpy(response_buf, json.constData(), size_t(n));
response_buf[n] = '\0';
}
return int(json.size());
}
QJsonObject response;
if (!self->handle(doc.object(), &response)) {
return -1;
}
if (response.isEmpty()) {
return 0;
}
const QByteArray json =
QJsonDocument(response).toJson(QJsonDocument::Compact);
if (response_buf && response_buf_size > 0) {
const int n = std::min(int(json.size()), response_buf_size - 1);
std::memcpy(response_buf, json.constData(), size_t(n));
response_buf[n] = '\0';
}
return int(json.size());
}
int oakengine_worker_session_shutdown_requested(const OakWorkerSession *self)
{
return self && self->shutdown_requested ? 1 : 0;
}
int oakengine_worker_main(int argc, char **argv)
{
QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
install_surface_format();
QGuiApplication app(argc, argv);
#ifdef Q_OS_MACOS
HideWorkerDockIcon();
#endif
QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org"));
QCoreApplication::setApplicationName(QStringLiteral("oak-render-worker"));
QString backend = QStringLiteral("opengl");
const QStringList args = app.arguments();
for (int i = 1; i < args.size(); ++i) {
if (args[i] == QStringLiteral("--backend") && i + 1 < args.size()) {
backend = args[i + 1].toLower();
++i;
}
}
#ifdef Q_OS_LINUX
std::signal(SIGSEGV, print_backtrace);
std::signal(SIGABRT, print_backtrace);
std::signal(SIGFPE, print_backtrace);
#endif
QFile in;
QFile out;
if (!in.open(stdin, QIODevice::ReadOnly | QIODevice::Unbuffered) ||
!out.open(stdout, QIODevice::WriteOnly | QIODevice::Unbuffered)) {
log_error(QStringLiteral("failed to open stdio control pipes"));
return 1;
}
OakWorkerSession *worker =
oakengine_worker_session_create(backend.toUtf8().constData());
if (!worker || !worker->renderer) {
oakengine_worker_session_free(worker);
return 1;
}
int exit_code = 0;
if (!worker->initialize_runtime()) {
exit_code = 1;
} else {
const QJsonObject handshake = worker->startup_handshake();
if (!olive::ipc::write_message(&out, handshake)) {
exit_code = 1;
} else {
out.flush();
QByteArray buffer;
while (!worker->shutdown_requested && !in.atEnd()) {
const QByteArray chunk = in.readLine();
if (chunk.isEmpty()) {
break;
}
buffer.append(chunk);
while (true) {
QJsonObject message;
bool ok = true;
if (!olive::ipc::read_message(&buffer, &message, &ok)) {
if (!ok) {
olive::ipc::write_message(
&out, error_message(QStringLiteral(
"malformed control message")));
out.flush();
continue;
}
break;
}
QJsonObject response;
if (!worker->handle(message, &response)) {
exit_code = 1;
break;
}
if (!response.isEmpty()) {
olive::ipc::write_message(&out, response);
out.flush();
}
}
}
}
}
oakengine_worker_session_free(worker);
return exit_code;
}
} // extern "C"