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:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ¶ms =
|
||||
*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
@@ -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 ¶ms,
|
||||
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"
|
||||
{
|
||||
|
||||
|
||||
@@ -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 ¶ms,
|
||||
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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ¶ms,
|
||||
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);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
+1130
-28
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user