engine: add init/project/timeline families to the C ABI facade
- oakengine_init/shutdown with HEADLESS/RENDER flags: headless boots Config, NodeFactory, ColorManager, task/conform/proxy/frame/disk managers and the serializer (plus an offscreen QGuiApplication that Qt requires for QAction); RENDER adds RenderManager. Idempotent and upgradable, no UI anywhere - oakengine_project_* (17): create/load/save, modified state, name, footage enumeration with online check, undo/redo, sequence access - oakengine_sequence_* (13): name, length (seconds and rational), frame rate, per-type track counts, playhead (timestamp and seconds), work area, markers; sequences are borrowed handles owned by their project - pure-C oakengine_init_test covers init idempotency, save/load round-trip through a real fixture project, footage online checks, timeline parameters, and NULL/bounds safety - no GL required
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# 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/>.
|
||||
|
||||
# C ABI facade of liboakengine (mirrors the pattern documented in
|
||||
# render/ipc/CMakeLists.txt):
|
||||
# - include/oakengine/*.h public C API
|
||||
# - src/capi/*.cpp C ABI implementations
|
||||
# The init/project/timeline families wrap the engine core, the project
|
||||
# serializer and the sequence/viewer timeline objects.
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
include/oakengine/init.h
|
||||
include/oakengine/project.h
|
||||
include/oakengine/timeline.h
|
||||
src/capi/init.cpp
|
||||
src/capi/project.cpp
|
||||
src/capi/timeline.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,163 @@
|
||||
/***
|
||||
|
||||
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/init.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QGuiApplication>
|
||||
|
||||
#include "codec/conformmanager.h"
|
||||
#include "codec/proxymanager.h"
|
||||
#include "config/config.h"
|
||||
#include "coreengine.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/factory.h"
|
||||
#include "node/project/serializer/serializer.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/framemanager.h"
|
||||
#include "render/rendermanager.h"
|
||||
#include "task/taskmanager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Currently initialized OAKENGINE_INIT_* bits.
|
||||
int g_flags = 0;
|
||||
|
||||
// Qt requires exactly one application object for the process and cannot
|
||||
// safely destroy and re-create one, so when the library has to create it the
|
||||
// object (and its argv storage) is leaked intentionally.
|
||||
//
|
||||
// This is a QGuiApplication, not a plain QCoreApplication: EngineCore's
|
||||
// UndoStack member creates QActions in its constructor, and Qt6 QActions
|
||||
// dereference QGuiApplication private state (they crash without one). A
|
||||
// QGuiApplication is still a QCoreApplication, and with the offscreen QPA
|
||||
// plugin (defaulted below, overridable by the caller) no display
|
||||
// connection, window or other UI is ever created.
|
||||
void ensure_qcoreapplication()
|
||||
{
|
||||
if (QCoreApplication::instance()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Same default as the gtest harness (tests/gtest/main.cpp).
|
||||
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);
|
||||
|
||||
// Same identity as the editor (app/main.cpp) so Config and friends land
|
||||
// in the same locations.
|
||||
QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org"));
|
||||
QCoreApplication::setApplicationName(QStringLiteral("Oak Video Editor"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
int oakengine_init(int flags)
|
||||
{
|
||||
if (flags == 0 || (flags & ~(OAKENGINE_INIT_HEADLESS | OAKENGINE_INIT_RENDER)) != 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
if ((flags & OAKENGINE_INIT_HEADLESS) != 0 &&
|
||||
(g_flags & OAKENGINE_INIT_HEADLESS) == 0) {
|
||||
ensure_qcoreapplication();
|
||||
|
||||
// EngineCore shell: provides EngineCore::instance() and the global
|
||||
// undo stack. Never deleted -- EngineCore does not reset instance_ in
|
||||
// its destructor, so deleting would leave a dangling singleton. This
|
||||
// mirrors the render worker's headless bootstrap.
|
||||
if (!olive::EngineCore::instance()) {
|
||||
new olive::EngineCore(olive::EngineCore::CoreParams());
|
||||
}
|
||||
|
||||
olive::Config::load();
|
||||
olive::NodeFactory::initialize();
|
||||
olive::ColorManager::set_up_default_config();
|
||||
olive::TaskManager::create_instance();
|
||||
olive::ConformManager::create_instance();
|
||||
olive::ProxyManager::create_instance();
|
||||
olive::FrameManager::create_instance();
|
||||
// Not in EngineCore::start(), but required headless: loading a project
|
||||
// touches PlaybackCache::load_state() which dereferences
|
||||
// DiskManager::instance() (the render worker creates it for the same
|
||||
// reason).
|
||||
olive::DiskManager::create_instance();
|
||||
olive::ProjectSerializer::initialize();
|
||||
|
||||
g_flags |= OAKENGINE_INIT_HEADLESS;
|
||||
}
|
||||
|
||||
if ((flags & OAKENGINE_INIT_RENDER) != 0 &&
|
||||
(g_flags & OAKENGINE_INIT_RENDER) == 0) {
|
||||
olive::RenderManager::create_instance();
|
||||
|
||||
g_flags |= OAKENGINE_INIT_RENDER;
|
||||
}
|
||||
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_shutdown(void)
|
||||
{
|
||||
// Reverse of oakengine_init(), mirroring EngineCore::stop(). The
|
||||
// QCoreApplication and the EngineCore shell intentionally survive (see
|
||||
// oakengine_init()).
|
||||
if ((g_flags & OAKENGINE_INIT_RENDER) != 0) {
|
||||
olive::RenderManager::destroy_instance();
|
||||
}
|
||||
|
||||
if ((g_flags & OAKENGINE_INIT_HEADLESS) != 0) {
|
||||
olive::Config::save();
|
||||
|
||||
olive::ProjectSerializer::destroy();
|
||||
|
||||
olive::DiskManager::destroy_instance();
|
||||
|
||||
olive::ConformManager::destroy_instance();
|
||||
|
||||
olive::ProxyManager::destroy_instance();
|
||||
|
||||
olive::FrameManager::destroy_instance();
|
||||
|
||||
olive::TaskManager::destroy_instance();
|
||||
|
||||
olive::NodeFactory::destroy();
|
||||
}
|
||||
|
||||
g_flags = 0;
|
||||
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_init_flags(void)
|
||||
{
|
||||
return g_flags;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,391 @@
|
||||
/***
|
||||
|
||||
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/project.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFileInfo>
|
||||
#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/serializer.h"
|
||||
#include "undo/undostack.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::Project *impl(OakEngineProject *h)
|
||||
{
|
||||
return reinterpret_cast<olive::Project *>(h);
|
||||
}
|
||||
|
||||
const olive::Project *impl(const OakEngineProject *h)
|
||||
{
|
||||
return reinterpret_cast<const 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());
|
||||
}
|
||||
|
||||
// The footage node at `index` in iteration order over the graph, or nullptr.
|
||||
olive::Footage *footage_at(const olive::Project *p, int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
int i = 0;
|
||||
for (olive::Node *n : p->nodes()) {
|
||||
if (olive::Footage *f = dynamic_cast<olive::Footage *>(n)) {
|
||||
if (i == index) {
|
||||
return f;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The sequence node at `index` in iteration order over the graph, or nullptr.
|
||||
olive::Sequence *sequence_at(const olive::Project *p, int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
int i = 0;
|
||||
for (olive::Node *n : p->nodes()) {
|
||||
if (olive::Sequence *s = dynamic_cast<olive::Sequence *>(n)) {
|
||||
if (i == index) {
|
||||
return s;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int node_count_of_type(const olive::Project *p, bool sequences)
|
||||
{
|
||||
int count = 0;
|
||||
for (olive::Node *n : p->nodes()) {
|
||||
const bool match = sequences ?
|
||||
(dynamic_cast<olive::Sequence *>(n) != nullptr) :
|
||||
(dynamic_cast<olive::Footage *>(n) != nullptr);
|
||||
if (match) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 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,
|
||||
const QString &details, const QString &filename)
|
||||
{
|
||||
switch (code) {
|
||||
case olive::ProjectSerializer::k_project_too_old:
|
||||
return QStringLiteral(
|
||||
"This project is from a version of Oak Video Editor that is no "
|
||||
"longer supported in this version.");
|
||||
case olive::ProjectSerializer::k_project_too_new:
|
||||
return QStringLiteral(
|
||||
"This project is from a newer version of Oak Video Editor and "
|
||||
"cannot be opened in this version.");
|
||||
case olive::ProjectSerializer::k_unknown_version:
|
||||
return QStringLiteral("Failed to determine project version.");
|
||||
case olive::ProjectSerializer::k_file_error:
|
||||
return QStringLiteral("Failed to read file \"%1\" for reading.")
|
||||
.arg(filename);
|
||||
case olive::ProjectSerializer::k_xml_error:
|
||||
return QStringLiteral(
|
||||
"Failed to read XML document. File may be corrupt. Error was: %1")
|
||||
.arg(details);
|
||||
case olive::ProjectSerializer::k_no_data:
|
||||
return QStringLiteral("Failed to find any data to parse.");
|
||||
case olive::ProjectSerializer::k_success:
|
||||
case olive::ProjectSerializer::k_overwrite_error:
|
||||
break;
|
||||
}
|
||||
return QStringLiteral("Unknown error.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
OakEngineProject *oakengine_project_create(void)
|
||||
{
|
||||
// Not initialized on purpose: the project serializers require a fresh
|
||||
// project (root folder unset), so oakengine_project_new() and
|
||||
// oakengine_project_load() perform the one-time content setup.
|
||||
return wrap(new olive::Project());
|
||||
}
|
||||
|
||||
void oakengine_project_free(OakEngineProject *self)
|
||||
{
|
||||
delete impl(self);
|
||||
}
|
||||
|
||||
int oakengine_project_new(OakEngineProject *self)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (impl(self)->root() != nullptr) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
impl(self)->initialize();
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->undo_stack()->clear();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_project_load(OakEngineProject *self, const char *path,
|
||||
char *err, int err_size)
|
||||
{
|
||||
if (!self || !path) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Project *project = impl(self);
|
||||
if (project->root() != nullptr) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
const QString filename = QString::fromUtf8(path);
|
||||
project->set_filename(filename);
|
||||
|
||||
olive::ProjectSerializer::Result result = olive::ProjectSerializer::load(
|
||||
project, filename, olive::ProjectSerializer::k_project);
|
||||
if (result != olive::ProjectSerializer::k_success) {
|
||||
// The project may be partially loaded; the handle should be freed
|
||||
// (loading again is rejected above because root is set by then).
|
||||
string_to_buf(load_error_string(result.code(), result.get_details(),
|
||||
filename),
|
||||
err, err_size);
|
||||
return OAKENGINE_E_FAILED;
|
||||
}
|
||||
|
||||
// Validate footage like the application does: resolve files that moved
|
||||
// together with the project. Without a relink handler (none exists at
|
||||
// this layer) the project is accepted as-is.
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->validate_footage_in_loaded_project(
|
||||
project, project->get_saved_url());
|
||||
olive::EngineCore::instance()->undo_stack()->clear();
|
||||
}
|
||||
|
||||
project->set_modified(false);
|
||||
if (err && err_size > 0) {
|
||||
err[0] = '\0';
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_project_save(OakEngineProject *self, const char *path)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Project *project = impl(self);
|
||||
|
||||
QString filename = path ? QString::fromUtf8(path) : project->filename();
|
||||
if (filename.isEmpty()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::ProjectSerializer::SaveData data(olive::ProjectSerializer::k_project,
|
||||
project, filename);
|
||||
const bool compress = !filename.endsWith(QStringLiteral(".ovexml"),
|
||||
Qt::CaseInsensitive);
|
||||
olive::ProjectSerializer::Result result =
|
||||
olive::ProjectSerializer::save(data, compress);
|
||||
|
||||
switch (result.code()) {
|
||||
case olive::ProjectSerializer::k_success:
|
||||
project->set_filename(filename);
|
||||
project->set_modified(false);
|
||||
return OAKENGINE_OK;
|
||||
case olive::ProjectSerializer::k_overwrite_error:
|
||||
// The file could not be replaced and the project was written to a
|
||||
// temporary name instead; the engine counts this as a success.
|
||||
project->set_filename(result.get_details());
|
||||
project->set_modified(false);
|
||||
return OAKENGINE_OK;
|
||||
default:
|
||||
return OAKENGINE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakengine_project_is_modified(const OakEngineProject *self)
|
||||
{
|
||||
return self && impl(self)->is_modified() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_project_set_modified(OakEngineProject *self, int modified)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
impl(self)->set_modified(modified != 0);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_project_name(const OakEngineProject *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(self)->name(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_project_filename(const OakEngineProject *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(self)->filename(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_project_footage_count(const OakEngineProject *self)
|
||||
{
|
||||
return self ? node_count_of_type(impl(self), false) : 0;
|
||||
}
|
||||
|
||||
int oakengine_project_footage_filename(const OakEngineProject *self, int index,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Footage *f = footage_at(impl(self), index);
|
||||
if (!f) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
return string_to_buf(f->filename(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_project_footage_is_online(const OakEngineProject *self,
|
||||
int index)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::Project *project = impl(self);
|
||||
olive::Footage *f = footage_at(project, index);
|
||||
if (!f) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
const QString filename = f->filename();
|
||||
if (QFileInfo::exists(filename)) {
|
||||
return 1;
|
||||
}
|
||||
// Footage that moved together with the project file: resolve relative
|
||||
// paths against the project's directory (same rule as
|
||||
// EngineCore::validate_footage_in_loaded_project()).
|
||||
if (QFileInfo(filename).isRelative() && !project->filename().isEmpty()) {
|
||||
const QString resolved =
|
||||
QFileInfo(project->filename()).dir().filePath(filename);
|
||||
if (QFileInfo::exists(resolved)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int oakengine_project_can_undo(const OakEngineProject *self)
|
||||
{
|
||||
if (!self || !olive::EngineCore::instance()) {
|
||||
return 0;
|
||||
}
|
||||
return olive::EngineCore::instance()->undo_stack()->can_undo() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_project_can_redo(const OakEngineProject *self)
|
||||
{
|
||||
if (!self || !olive::EngineCore::instance()) {
|
||||
return 0;
|
||||
}
|
||||
return olive::EngineCore::instance()->undo_stack()->can_redo() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_project_undo(OakEngineProject *self)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->undo_stack()->undo();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_project_redo(OakEngineProject *self)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->undo_stack()->redo();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_project_sequence_count(const OakEngineProject *self)
|
||||
{
|
||||
return self ? node_count_of_type(impl(self), true) : 0;
|
||||
}
|
||||
|
||||
OakEngineSequence *oakengine_project_sequence_at(const OakEngineProject *self,
|
||||
int index)
|
||||
{
|
||||
if (!self) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap_seq(sequence_at(impl(self), index));
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,326 @@
|
||||
/***
|
||||
|
||||
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/timeline.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "coreengine.h"
|
||||
#include "node/nodeundo.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/folder/folder.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "timeline/timelinemarker.h"
|
||||
#include "timeline/timelineworkarea.h"
|
||||
#include "undo/undocommand.h"
|
||||
#include "undo/undostack.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::Sequence *impl(OakEngineSequence *h)
|
||||
{
|
||||
return reinterpret_cast<olive::Sequence *>(h);
|
||||
}
|
||||
|
||||
const olive::Sequence *impl(const OakEngineSequence *h)
|
||||
{
|
||||
return reinterpret_cast<const olive::Sequence *>(h);
|
||||
}
|
||||
|
||||
OakEngineSequence *wrap(olive::Sequence *s)
|
||||
{
|
||||
return reinterpret_cast<OakEngineSequence *>(s);
|
||||
}
|
||||
|
||||
// ViewerOutput::get_playhead() is not a const method in the engine; the
|
||||
// facade keeps const-correct handles and casts locally.
|
||||
olive::Sequence *mutable_impl(const OakEngineSequence *h)
|
||||
{
|
||||
return const_cast<olive::Sequence *>(
|
||||
reinterpret_cast<const olive::Sequence *>(h));
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
// Copy a QString into a fixed-capacity C buffer, always NUL-terminating and
|
||||
// truncating what does not fit.
|
||||
void copy_to_buf(const QString &s, char *dst, size_t cap)
|
||||
{
|
||||
const QByteArray utf = s.toUtf8();
|
||||
const size_t n = qMin(size_t(utf.size()), cap - 1);
|
||||
memcpy(dst, utf.constData(), n);
|
||||
dst[n] = '\0';
|
||||
}
|
||||
|
||||
// The sequence's frame duration as a Rational timebase (frame rate flipped).
|
||||
// Returns false when the sequence has no valid frame rate (no video params).
|
||||
bool time_base_of(const olive::Sequence *s, olive::Rational *out)
|
||||
{
|
||||
const olive::Rational frame_rate = s->get_video_params().frame_rate();
|
||||
if (frame_rate.isNull() || frame_rate.isNaN()) {
|
||||
return false;
|
||||
}
|
||||
*out = frame_rate.flipped();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rational seconds -> timestamp in timebase units, like
|
||||
// Timecode::time_to_timestamp with k_round rounding.
|
||||
int64_t time_to_ts(const olive::Rational &time, const olive::Rational &tb)
|
||||
{
|
||||
return olive::core::Timecode::time_to_timestamp(
|
||||
time, tb, olive::core::Timecode::k_round);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
OakEngineSequence *oakengine_sequence_new(OakEngineProject *project,
|
||||
const char *name)
|
||||
{
|
||||
olive::Project *p = reinterpret_cast<olive::Project *>(project);
|
||||
if (!p || !p->root()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
olive::Sequence *sequence = new olive::Sequence();
|
||||
sequence->set_default_parameters();
|
||||
sequence->set_label(QString::fromUtf8(name ? name : ""));
|
||||
|
||||
// Same undoable creation as the application's "Create New Sequence"
|
||||
// action (app/core.cpp), minus opening a viewer. Without an EngineCore
|
||||
// (library not initialized) the command is executed non-undoably.
|
||||
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||
command->add_child(new olive::NodeAddCommand(p, sequence));
|
||||
command->add_child(new olive::FolderAddChild(p->root(), sequence));
|
||||
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->undo_stack()->push(
|
||||
command, QStringLiteral("Create Sequence"));
|
||||
} else {
|
||||
command->redo_now();
|
||||
delete command;
|
||||
}
|
||||
|
||||
return wrap(sequence);
|
||||
}
|
||||
|
||||
int oakengine_sequence_name(const OakEngineSequence *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(self)->get_label(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_sequence_get_length(const OakEngineSequence *self,
|
||||
double *seconds)
|
||||
{
|
||||
if (!self || !seconds) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
*seconds = impl(self)->get_length().to_double();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_get_length_rational(const OakEngineSequence *self,
|
||||
int *num, int *den)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::Rational length = impl(self)->get_length();
|
||||
if (num) {
|
||||
*num = length.numerator();
|
||||
}
|
||||
if (den) {
|
||||
*den = length.denominator();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_get_frame_rate(const OakEngineSequence *self, int *num,
|
||||
int *den)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::Rational frame_rate = impl(self)->get_video_params().frame_rate();
|
||||
if (frame_rate.isNull() || frame_rate.isNaN()) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
if (num) {
|
||||
*num = frame_rate.numerator();
|
||||
}
|
||||
if (den) {
|
||||
*den = frame_rate.denominator();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_track_count(const OakEngineSequence *self, int *video,
|
||||
int *audio, int *subtitle)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::Sequence *s = impl(self);
|
||||
if (video) {
|
||||
*video = s->track_list(olive::Track::k_video)->get_track_count();
|
||||
}
|
||||
if (audio) {
|
||||
*audio = s->track_list(olive::Track::k_audio)->get_track_count();
|
||||
}
|
||||
if (subtitle) {
|
||||
*subtitle = s->track_list(olive::Track::k_subtitle)->get_track_count();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_get_playhead(const OakEngineSequence *self,
|
||||
int64_t *timestamp)
|
||||
{
|
||||
if (!self || !timestamp) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Rational tb;
|
||||
if (!time_base_of(impl(self), &tb)) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
*timestamp = time_to_ts(mutable_impl(self)->get_playhead(), tb);
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_set_playhead(OakEngineSequence *self, int64_t timestamp)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Rational tb;
|
||||
if (!time_base_of(impl(self), &tb)) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
impl(self)->set_playhead(
|
||||
olive::core::Timecode::timestamp_to_time(timestamp, tb));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_get_playhead_seconds(const OakEngineSequence *self,
|
||||
double *seconds)
|
||||
{
|
||||
if (!self || !seconds) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
*seconds = mutable_impl(self)->get_playhead().to_double();
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_workarea_is_enabled(const OakEngineSequence *self)
|
||||
{
|
||||
return self && impl(self)->get_work_area()->enabled() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_sequence_get_workarea(const OakEngineSequence *self, int64_t *in,
|
||||
int64_t *out)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Rational tb;
|
||||
if (!time_base_of(impl(self), &tb)) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
const olive::TimelineWorkArea *workarea = impl(self)->get_work_area();
|
||||
if (in) {
|
||||
*in = time_to_ts(workarea->in(), tb);
|
||||
}
|
||||
if (out) {
|
||||
*out = time_to_ts(workarea->out(), tb);
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_set_workarea(OakEngineSequence *self, int enabled,
|
||||
int64_t in, int64_t out)
|
||||
{
|
||||
if (!self) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::Rational tb;
|
||||
if (!time_base_of(impl(self), &tb)) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
olive::TimelineWorkArea *workarea = impl(self)->get_work_area();
|
||||
workarea->set_enabled(enabled != 0);
|
||||
workarea->set_range(
|
||||
olive::TimeRange(olive::core::Timecode::timestamp_to_time(in, tb),
|
||||
olive::core::Timecode::timestamp_to_time(out, tb)));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_sequence_marker_count(const OakEngineSequence *self)
|
||||
{
|
||||
if (!self) {
|
||||
return 0;
|
||||
}
|
||||
return int(impl(self)->get_markers()->size());
|
||||
}
|
||||
|
||||
int oakengine_sequence_marker_at(const OakEngineSequence *self, int index,
|
||||
int64_t *time, char *name, int name_size)
|
||||
{
|
||||
if (!self || index < 0) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::TimelineMarkerList *markers = impl(self)->get_markers();
|
||||
if (size_t(index) >= markers->size()) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
const olive::TimelineMarker *marker = *(markers->cbegin() + index);
|
||||
if (time) {
|
||||
olive::Rational tb;
|
||||
if (!time_base_of(impl(self), &tb)) {
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
*time = time_to_ts(marker->time().in(), tb);
|
||||
}
|
||||
if (name && name_size > 0) {
|
||||
copy_to_buf(marker->name(), name, size_t(name_size));
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
Reference in New Issue
Block a user