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:
@@ -44,6 +44,7 @@ add_subdirectory(timeline)
|
|||||||
add_subdirectory(tool)
|
add_subdirectory(tool)
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
add_subdirectory(undo)
|
add_subdirectory(undo)
|
||||||
|
add_subdirectory(src/capi)
|
||||||
|
|
||||||
add_library(oakengine SHARED
|
add_library(oakengine SHARED
|
||||||
${OLIVE_SOURCES}
|
${OLIVE_SOURCES}
|
||||||
@@ -225,4 +226,11 @@ if (BUILD_TESTS)
|
|||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
make_oakengine_test(oakengine_ipc_test)
|
make_oakengine_test(oakengine_ipc_test)
|
||||||
|
|
||||||
|
make_oakengine_test(oakengine_init_test)
|
||||||
|
# Resolves the real test assets (tests/demo.mp4, the footage fixture
|
||||||
|
# project) relative to the repository root, like tests/gtest does.
|
||||||
|
target_compile_definitions(oakengine_init_test PRIVATE
|
||||||
|
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||||
|
)
|
||||||
endif ()
|
endif ()
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
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_INIT_H
|
||||||
|
#define OAKENGINE_INIT_H
|
||||||
|
|
||||||
|
#include "export.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file init.h
|
||||||
|
* @brief C ABI for engine process initialization and shutdown
|
||||||
|
*
|
||||||
|
* oakengine_init() brings up the UI-independent engine services a consumer of
|
||||||
|
* liboakengine needs before touching projects or timelines. It never creates
|
||||||
|
* any UI: no QApplication, no main window, no display connection. When no
|
||||||
|
* application object exists yet, an offscreen QGuiApplication is created (Qt
|
||||||
|
* requires exactly one application object; it is leaked intentionally because
|
||||||
|
* Qt cannot safely re-create one). A QGuiApplication -- not a plain
|
||||||
|
* QCoreApplication -- is required because EngineCore's undo stack creates
|
||||||
|
* QActions in its constructor, and Qt6 QActions need QGuiApplication state;
|
||||||
|
* with the offscreen QPA plugin (the default, overridable through
|
||||||
|
* QT_QPA_PLATFORM) nothing graphical ever happens.
|
||||||
|
*
|
||||||
|
* The flag mask selects the service set:
|
||||||
|
*
|
||||||
|
* - OAKENGINE_INIT_HEADLESS: Config, NodeFactory, ColorManager, TaskManager,
|
||||||
|
* ConformManager, ProxyManager, FrameManager, DiskManager,
|
||||||
|
* ProjectSerializer and a process-wide EngineCore shell (holds the global
|
||||||
|
* undo stack and the EngineCore::instance() pointer engine code
|
||||||
|
* dereferences). DiskManager is not part of EngineCore::start() but is
|
||||||
|
* required because loading a project touches PlaybackCache state which
|
||||||
|
* dereferences DiskManager::instance(). Everything here runs headless;
|
||||||
|
* no GL is required.
|
||||||
|
*
|
||||||
|
* - OAKENGINE_INIT_RENDER: additionally creates the RenderManager. Only
|
||||||
|
* with this bit may a consumer end up needing a GL context (the actual
|
||||||
|
* render backends are dynamic engine plugins loaded on demand).
|
||||||
|
*
|
||||||
|
* Initialization is modelled on the render worker's headless bootstrap
|
||||||
|
* (worker/workermain.cpp) rather than EngineCore::start(), because start()
|
||||||
|
* unconditionally creates the RenderManager, starts the autorecovery timer
|
||||||
|
* and reads the recent-projects list -- application behavior that does not
|
||||||
|
* belong behind a library boundary.
|
||||||
|
*
|
||||||
|
* Conventions:
|
||||||
|
* - Return codes: 0 (OAKENGINE_OK) on success, a negative OAKENGINE_E_*
|
||||||
|
* error code on failure.
|
||||||
|
* - oakengine_init() is idempotent: calling it again is a no-op for flag
|
||||||
|
* bits already initialized and only brings up the missing bits (e.g.
|
||||||
|
* upgrading HEADLESS to HEADLESS|RENDER).
|
||||||
|
* - oakengine_shutdown() pairs with oakengine_init() and tears down the
|
||||||
|
* initialized services in reverse order. The QCoreApplication and the
|
||||||
|
* EngineCore shell are kept alive (see above), so oakengine_init() may be
|
||||||
|
* called again afterwards.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Status and error codes shared by the init/project/timeline families.
|
||||||
|
*/
|
||||||
|
#define OAKENGINE_OK 0 /**< Success. */
|
||||||
|
#define OAKENGINE_E_INVALID (-1) /**< NULL handle or invalid argument. */
|
||||||
|
#define OAKENGINE_E_STATE (-2) /**< Call not valid in the current state. */
|
||||||
|
#define OAKENGINE_E_FAILED (-3) /**< The engine reported a failure. */
|
||||||
|
#define OAKENGINE_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */
|
||||||
|
|
||||||
|
/** @brief Base headless engine services (no GL required). */
|
||||||
|
#define OAKENGINE_INIT_HEADLESS 0x01
|
||||||
|
/** @brief Render services on top of HEADLESS (may require GL). */
|
||||||
|
#define OAKENGINE_INIT_RENDER 0x02
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the engine services selected by `flags`.
|
||||||
|
*
|
||||||
|
* `flags` is a bitmask of OAKENGINE_INIT_HEADLESS and/or
|
||||||
|
* OAKENGINE_INIT_RENDER; 0 is invalid. Repeated calls are idempotent and may
|
||||||
|
* add the RENDER bit to a running HEADLESS instance.
|
||||||
|
*
|
||||||
|
* @return OAKENGINE_OK on success, OAKENGINE_E_INVALID for an empty mask.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_init(int flags);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tear down the services brought up by oakengine_init().
|
||||||
|
*
|
||||||
|
* Safe to call when not initialized (a no-op then). The QCoreApplication and
|
||||||
|
* the EngineCore shell survive shutdown intentionally.
|
||||||
|
*
|
||||||
|
* @return OAKENGINE_OK.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_shutdown(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Current initialization state as a flag bitmask (0 = not initialized).
|
||||||
|
*
|
||||||
|
* Only services that are actually up are reported, e.g. after upgrading a
|
||||||
|
* HEADLESS instance with the RENDER bit the result includes both bits.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_init_flags(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* OAKENGINE_INIT_H */
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
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_PROJECT_H
|
||||||
|
#define OAKENGINE_PROJECT_H
|
||||||
|
|
||||||
|
#include "export.h"
|
||||||
|
#include "init.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file project.h
|
||||||
|
* @brief C ABI for Oak project files (.ove)
|
||||||
|
*
|
||||||
|
* An OakEngineProject wraps the engine's olive::Project node graph. Projects
|
||||||
|
* are created empty with oakengine_project_create() and must then be given
|
||||||
|
* content exactly once: either oakengine_project_new() for a blank project or
|
||||||
|
* oakengine_project_load() to read a project file (the engine's project
|
||||||
|
* serializers require a fresh, uninitialized project, so loading into a
|
||||||
|
* project that already has content is rejected with OAKENGINE_E_STATE).
|
||||||
|
*
|
||||||
|
* Sequences are owned by their project through Qt's QObject parent chain;
|
||||||
|
* OakEngineSequence handles are borrowed views whose lifetime follows the
|
||||||
|
* project. See timeline.h for the sequence function family.
|
||||||
|
*
|
||||||
|
* Conventions (matching oakengine/ipc.h):
|
||||||
|
* - Booleans are int (1/0).
|
||||||
|
* - Return codes: 0 (OAKENGINE_OK) on success, negative OAKENGINE_E_* on
|
||||||
|
* failure.
|
||||||
|
* - String output uses the buf/size convention: the return value is the
|
||||||
|
* number of characters that would have been written excluding the NUL,
|
||||||
|
* so buf == NULL or a short buffer queries the required size. The output
|
||||||
|
* is NUL-terminated whenever buf_size > 0. A negative return value is an
|
||||||
|
* OAKENGINE_E_* error code.
|
||||||
|
* - NULL handles are accepted everywhere and yield a no-op / zero result /
|
||||||
|
* OAKENGINE_E_INVALID.
|
||||||
|
*
|
||||||
|
* Undo/redo note: the engine's undo stack is a process-wide singleton held by
|
||||||
|
* EngineCore (EngineCore::undo_stack()), not a per-project object. The
|
||||||
|
* project_undo/project_redo family therefore operates on that global stack
|
||||||
|
* and is not scoped to `self`; oakengine_project_new() and
|
||||||
|
* oakengine_project_load() clear it, matching the application's
|
||||||
|
* new/open-project behavior.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Opaque project handle. Owned by the caller; release with
|
||||||
|
* oakengine_project_free().
|
||||||
|
*/
|
||||||
|
typedef struct OakEngineProject OakEngineProject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Opaque sequence (timeline) handle.
|
||||||
|
*
|
||||||
|
* Typedef'd here so project.h and timeline.h can be included in any order.
|
||||||
|
* Handles are borrowed from their owning OakEngineProject (QObject parent
|
||||||
|
* chain) and must NOT be freed; they become invalid when the project is
|
||||||
|
* freed. The sequence function family lives in oakengine/timeline.h.
|
||||||
|
*/
|
||||||
|
typedef struct OakEngineSequence OakEngineSequence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Allocate an empty project shell. Owned by the caller.
|
||||||
|
*
|
||||||
|
* The project has no content until exactly one of oakengine_project_new() or
|
||||||
|
* oakengine_project_load() succeeds on it.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API OakEngineProject *oakengine_project_create(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Destroy a project and everything it owns (including sequences).
|
||||||
|
* NULL-safe.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API void oakengine_project_free(OakEngineProject *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize `self` as a new, blank project (untitled, not modified).
|
||||||
|
*
|
||||||
|
* Clears the global undo stack. Returns OAKENGINE_E_STATE if the project
|
||||||
|
* already has content (new/load may only be applied once).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_project_new(OakEngineProject *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load project content from a .ove file.
|
||||||
|
*
|
||||||
|
* On success the project's filename is set to `path` and the modified flag is
|
||||||
|
* cleared; the global undo stack is cleared. Footage is validated like the
|
||||||
|
* engine does (EngineCore::validate_footage_in_loaded_project): files that
|
||||||
|
* moved together with the project are resolved relatively; missing footage is
|
||||||
|
* tolerated (no relink UI exists at this layer, so the project is accepted
|
||||||
|
* as-is). On failure a human-readable reason is written to `err` using the
|
||||||
|
* buf/size convention (err may be NULL) and a negative code is returned.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_project_load(OakEngineProject *self,
|
||||||
|
const char *path, char *err,
|
||||||
|
int err_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Save the project to `path`.
|
||||||
|
*
|
||||||
|
* `path` == NULL saves under the project's current filename
|
||||||
|
* (Project::filename()); saving an untitled project without a path fails with
|
||||||
|
* OAKENGINE_E_INVALID. Files are gzip-compressed unless the target name ends
|
||||||
|
* in ".ovexml" (mirrors the application behavior). On success the project's
|
||||||
|
* filename is set to the target and the modified flag is cleared
|
||||||
|
* (EngineCore::on_project_saved() semantics, minus the UI recent-list).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_project_save(OakEngineProject *self,
|
||||||
|
const char *path);
|
||||||
|
|
||||||
|
OAKENGINE_API int oakengine_project_is_modified(const OakEngineProject *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the project modified flag (Project::set_modified).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_project_set_modified(OakEngineProject *self,
|
||||||
|
int modified);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Project display name (Project::name(): the filename's base name, or
|
||||||
|
* "(untitled)"). Uses the buf/size convention.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_project_name(const OakEngineProject *self,
|
||||||
|
char *buf, int buf_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Full path the project was loaded from / saved to, or "" if untitled
|
||||||
|
* (Project::filename()). Uses the buf/size convention.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_project_filename(const OakEngineProject *self,
|
||||||
|
char *buf, int buf_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Number of footage items in the project.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_project_footage_count(const OakEngineProject *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stored filename of the footage item at `index` (Footage::filename(),
|
||||||
|
* as recorded in the project file; may be relative). Uses the buf/size
|
||||||
|
* convention; returns OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_project_footage_filename(
|
||||||
|
const OakEngineProject *self, int index, char *buf, int buf_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 1 if the footage file at `index` exists on disk, 0 if not.
|
||||||
|
*
|
||||||
|
* Mirrors the engine's validation rule: the stored path is checked as-is
|
||||||
|
* first, then resolved relative to the project file's directory (footage
|
||||||
|
* that moved together with the project stays online). Returns
|
||||||
|
* OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_project_footage_is_online(const OakEngineProject *self, int index);
|
||||||
|
|
||||||
|
/* ---- Undo (global singleton stack, see the file comment above) ---------- */
|
||||||
|
|
||||||
|
OAKENGINE_API int oakengine_project_can_undo(const OakEngineProject *self);
|
||||||
|
OAKENGINE_API int oakengine_project_can_redo(const OakEngineProject *self);
|
||||||
|
OAKENGINE_API int oakengine_project_undo(OakEngineProject *self);
|
||||||
|
OAKENGINE_API int oakengine_project_redo(OakEngineProject *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Number of sequences (timelines) in the project.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_project_sequence_count(const OakEngineProject *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Borrowed handle of the sequence at `index`, or NULL when out of
|
||||||
|
* range. Do NOT free; the sequence is owned by the project.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API OakEngineSequence *
|
||||||
|
oakengine_project_sequence_at(const OakEngineProject *self, int index);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* OAKENGINE_PROJECT_H */
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
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_TIMELINE_H
|
||||||
|
#define OAKENGINE_TIMELINE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "export.h"
|
||||||
|
#include "init.h"
|
||||||
|
#include "project.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file timeline.h
|
||||||
|
* @brief C ABI for sequences (Oak timelines)
|
||||||
|
*
|
||||||
|
* An OakEngineSequence wraps the engine's olive::Sequence node
|
||||||
|
* (engine/node/project/sequence/sequence.h, a ViewerOutput). This family is
|
||||||
|
* intentionally read-mostly this round: playhead and workarea are simple
|
||||||
|
* writes, everything else is inspection. Clip/track editing is a later
|
||||||
|
* milestone.
|
||||||
|
*
|
||||||
|
* Handles are borrowed from their owning OakEngineProject (Qt QObject parent
|
||||||
|
* chain; sequences are added to the project graph which becomes their
|
||||||
|
* parent). There is deliberately no oakengine_sequence_free(): a sequence
|
||||||
|
* handle becomes invalid when its project is freed. Sequences created with
|
||||||
|
* oakengine_sequence_new() are additionally undoable -- undoing the creation
|
||||||
|
* removes the sequence from the project (but keeps it alive under the undo
|
||||||
|
* command), redoing brings it back.
|
||||||
|
*
|
||||||
|
* Time is exposed in the two representations used across the engine
|
||||||
|
* (olive::core::Timecode terminology, core/util/timecodefunctions.h):
|
||||||
|
*
|
||||||
|
* - seconds: a plain double (`*_seconds` accessors), or a rational seconds
|
||||||
|
* value as a numerator/denominator int pair (`*_rational`);
|
||||||
|
*
|
||||||
|
* - timestamp: an int64 count of timebase units, where the timebase is the
|
||||||
|
* sequence's frame duration (the frame rate flipped, e.g. 1001/30000 for
|
||||||
|
* a 30000/1001 sequence) -- i.e. a frame number. This matches how the
|
||||||
|
* engine converts between Rational times and frame timestamps
|
||||||
|
* (Timecode::time_to_timestamp / timestamp_to_time).
|
||||||
|
*
|
||||||
|
* Conventions match oakengine/project.h: booleans are int, 0
|
||||||
|
* (OAKENGINE_OK)/negative OAKENGINE_E_* return codes, buf/size string output,
|
||||||
|
* NULL handles yield no-ops or OAKENGINE_E_INVALID.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create a new sequence named `name` in `project` and return its
|
||||||
|
* borrowed handle (NULL on failure; `project` NULL -> NULL).
|
||||||
|
*
|
||||||
|
* The sequence gets the application's default parameters
|
||||||
|
* (ViewerOutput::set_default_parameters(): width/height/pixel aspect/
|
||||||
|
* interlacing/audio layout from Config, frame rate from the
|
||||||
|
* DefaultSequenceFrameRate config entry, 30000/1001 by default) and starts
|
||||||
|
* with zero tracks. The creation is pushed onto the global undo stack like
|
||||||
|
* the application's "Create New Sequence" action (minus opening a viewer).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API OakEngineSequence *
|
||||||
|
oakengine_sequence_new(OakEngineProject *project, const char *name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sequence name (Node::get_label()). Uses the buf/size convention.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_name(const OakEngineSequence *self,
|
||||||
|
char *buf, int buf_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Length of the sequence content in seconds
|
||||||
|
* (ViewerOutput::get_length()). 0 for an empty sequence.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_get_length(const OakEngineSequence *self,
|
||||||
|
double *seconds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Length of the sequence content as rational seconds
|
||||||
|
* (ViewerOutput::get_length().numerator()/denominator()).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_sequence_get_length_rational(const OakEngineSequence *self, int *num,
|
||||||
|
int *den);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sequence frame rate as a num/den pair, e.g. 30000/1001
|
||||||
|
* (ViewerOutput::get_video_params().frame_rate()).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_sequence_get_frame_rate(const OakEngineSequence *self, int *num,
|
||||||
|
int *den);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Number of tracks per track type (Sequence::track_list(type)->
|
||||||
|
* get_track_count()). Any of `video`/`audio`/`subtitle` may be NULL.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_track_count(const OakEngineSequence *self,
|
||||||
|
int *video, int *audio,
|
||||||
|
int *subtitle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Playhead position as a timestamp in timebase units (frame number;
|
||||||
|
* ViewerOutput::get_playhead() rescaled to the frame-rate timebase).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_sequence_get_playhead(const OakEngineSequence *self,
|
||||||
|
int64_t *timestamp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Move the playhead to `timestamp` (frame number;
|
||||||
|
* ViewerOutput::set_playhead()).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_set_playhead(OakEngineSequence *self,
|
||||||
|
int64_t timestamp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Playhead position in seconds.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_sequence_get_playhead_seconds(const OakEngineSequence *self,
|
||||||
|
double *seconds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 1 if the workarea (in/out range) is enabled
|
||||||
|
* (TimelineWorkArea::enabled()).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_sequence_workarea_is_enabled(const OakEngineSequence *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Workarea in/out points as timestamps in timebase units
|
||||||
|
* (TimelineWorkArea::in()/out()). Either pointer may be NULL.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_sequence_get_workarea(const OakEngineSequence *self, int64_t *in,
|
||||||
|
int64_t *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the workarea: enable flag plus in/out timestamps in timebase
|
||||||
|
* units (TimelineWorkArea::set_enabled()/set_range()).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_set_workarea(OakEngineSequence *self,
|
||||||
|
int enabled, int64_t in,
|
||||||
|
int64_t out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Number of timeline markers (TimelineMarkerList::size()).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int
|
||||||
|
oakengine_sequence_marker_count(const OakEngineSequence *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Marker at `index`: `time` receives its in-point as a timestamp in
|
||||||
|
* timebase units (may be NULL), `name` its label using the buf/size
|
||||||
|
* truncation convention (may be NULL to only fetch the time). Returns
|
||||||
|
* OAKENGINE_OK on success, OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_marker_at(const OakEngineSequence *self,
|
||||||
|
int index, int64_t *time,
|
||||||
|
char *name, int name_size);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* OAKENGINE_TIMELINE_H */
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
***/
|
||||||
|
|
||||||
|
// Pure C ABI test for the liboakengine init/project/timeline facade.
|
||||||
|
// Exercises engine init/shutdown, project create/new/save/load round-trips,
|
||||||
|
// the global undo stack, sequence inspection (length, frame rate, tracks,
|
||||||
|
// playhead, workarea, markers) and a fixture project with real footage
|
||||||
|
// (tests/demo.mp4). No Qt, no GL: oakengine_init() creates the
|
||||||
|
// QCoreApplication itself when needed.
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <direct.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "oakengine/init.h"
|
||||||
|
#include "oakengine/project.h"
|
||||||
|
#include "oakengine/timeline.h"
|
||||||
|
|
||||||
|
#ifndef OAK_TEST_SOURCE_DIR
|
||||||
|
#define OAK_TEST_SOURCE_DIR "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static char g_tmpdir[4096];
|
||||||
|
|
||||||
|
static void make_tmpdir(void)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
char base[MAX_PATH];
|
||||||
|
const DWORD len = GetTempPathA(MAX_PATH, base);
|
||||||
|
assert(len > 0 && len < MAX_PATH);
|
||||||
|
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_init_test_%lu", base,
|
||||||
|
(unsigned long)GetCurrentProcessId());
|
||||||
|
assert(_mkdir(g_tmpdir) == 0);
|
||||||
|
#else
|
||||||
|
strcpy(g_tmpdir, "/tmp/oakengine_init_test_XXXXXX");
|
||||||
|
assert(mkdtemp(g_tmpdir) != NULL);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void make_path(char *dst, size_t cap, const char *filename)
|
||||||
|
{
|
||||||
|
const int n = snprintf(dst, cap, "%s/%s", g_tmpdir, filename);
|
||||||
|
assert(n > 0 && (size_t)n < cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int file_exists(const char *path)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_init(void)
|
||||||
|
{
|
||||||
|
assert(oakengine_init(0) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_init(4) == OAKENGINE_E_INVALID); // unknown bit
|
||||||
|
|
||||||
|
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_init_flags() == OAKENGINE_INIT_HEADLESS);
|
||||||
|
|
||||||
|
// Repeated init is idempotent.
|
||||||
|
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_init_flags() == OAKENGINE_INIT_HEADLESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_project_basics(void)
|
||||||
|
{
|
||||||
|
OakEngineProject *p = oakengine_project_create();
|
||||||
|
assert(p != NULL);
|
||||||
|
|
||||||
|
// Fresh shell: untitled, unmodified, empty.
|
||||||
|
assert(oakengine_project_is_modified(p) == 0);
|
||||||
|
assert(oakengine_project_footage_count(p) == 0);
|
||||||
|
assert(oakengine_project_sequence_count(p) == 0);
|
||||||
|
assert(oakengine_project_can_undo(p) == 0);
|
||||||
|
assert(oakengine_project_can_redo(p) == 0);
|
||||||
|
|
||||||
|
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||||
|
// Content setup may only happen once.
|
||||||
|
assert(oakengine_project_new(p) == OAKENGINE_E_STATE);
|
||||||
|
|
||||||
|
// An untitled project reports the engine's "(untitled)" name and an
|
||||||
|
// empty filename.
|
||||||
|
char name[64];
|
||||||
|
const int name_len = oakengine_project_name(p, NULL, 0);
|
||||||
|
assert(name_len > 0);
|
||||||
|
assert(oakengine_project_name(p, name, sizeof(name)) == name_len);
|
||||||
|
assert((int)strlen(name) == name_len);
|
||||||
|
assert(strcmp(name, "(untitled)") == 0);
|
||||||
|
// buf/size truncation: reports the full size, writes what fits.
|
||||||
|
char tiny[4];
|
||||||
|
assert(oakengine_project_name(p, tiny, sizeof(tiny)) == name_len);
|
||||||
|
assert(strlen(tiny) == sizeof(tiny) - 1);
|
||||||
|
|
||||||
|
char filename[16];
|
||||||
|
assert(oakengine_project_filename(p, filename, sizeof(filename)) == 0);
|
||||||
|
assert(filename[0] == '\0');
|
||||||
|
|
||||||
|
// Modified flag round-trips.
|
||||||
|
assert(oakengine_project_set_modified(p, 1) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_project_is_modified(p) == 1);
|
||||||
|
assert(oakengine_project_set_modified(p, 0) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_project_is_modified(p) == 0);
|
||||||
|
|
||||||
|
// Saving an untitled project without a path fails.
|
||||||
|
assert(oakengine_project_save(p, NULL) == OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
oakengine_project_free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_sequence_and_save_load(void)
|
||||||
|
{
|
||||||
|
char path[4096];
|
||||||
|
make_path(path, sizeof(path), "roundtrip.ove");
|
||||||
|
|
||||||
|
OakEngineProject *p = oakengine_project_create();
|
||||||
|
assert(p != NULL);
|
||||||
|
assert(oakengine_project_new(p) == OAKENGINE_OK);
|
||||||
|
|
||||||
|
// Create a sequence through the facade; it is owned by the project.
|
||||||
|
OakEngineSequence *seq = oakengine_sequence_new(p, "Main");
|
||||||
|
assert(seq != NULL);
|
||||||
|
assert(oakengine_project_sequence_count(p) == 1);
|
||||||
|
assert(oakengine_project_sequence_at(p, 0) == seq);
|
||||||
|
assert(oakengine_project_sequence_at(p, 1) == NULL);
|
||||||
|
assert(oakengine_project_sequence_at(p, -1) == NULL);
|
||||||
|
|
||||||
|
// The creation went onto the global undo stack and marked the project.
|
||||||
|
assert(oakengine_project_can_undo(p) == 1);
|
||||||
|
assert(oakengine_project_is_modified(p) == 1);
|
||||||
|
|
||||||
|
// Sequence name round-trips.
|
||||||
|
char name[64];
|
||||||
|
const int name_len = oakengine_sequence_name(seq, NULL, 0);
|
||||||
|
assert(name_len == 4);
|
||||||
|
assert(oakengine_sequence_name(seq, name, sizeof(name)) == 4);
|
||||||
|
assert(strcmp(name, "Main") == 0);
|
||||||
|
|
||||||
|
// Empty sequence: zero length.
|
||||||
|
double seconds = -1.0;
|
||||||
|
assert(oakengine_sequence_get_length(seq, &seconds) == OAKENGINE_OK);
|
||||||
|
assert(seconds == 0.0);
|
||||||
|
int num = -1, den = -1;
|
||||||
|
assert(oakengine_sequence_get_length_rational(seq, &num, &den) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(num == 0);
|
||||||
|
|
||||||
|
// Default frame rate from Config's DefaultSequenceFrameRate entry
|
||||||
|
// (time base 1001/30000 -> frame rate 30000/1001).
|
||||||
|
num = den = -1;
|
||||||
|
assert(oakengine_sequence_get_frame_rate(seq, &num, &den) == OAKENGINE_OK);
|
||||||
|
assert(num == 30000 && den == 1001);
|
||||||
|
|
||||||
|
// Fresh sequences have no tracks.
|
||||||
|
int video = -1, audio = -1, subtitle = -1;
|
||||||
|
assert(oakengine_sequence_track_count(seq, &video, &audio, &subtitle) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(video == 0 && audio == 0 && subtitle == 0);
|
||||||
|
|
||||||
|
// Playhead starts at zero and accepts frame timestamps.
|
||||||
|
int64_t ts = -1;
|
||||||
|
assert(oakengine_sequence_get_playhead(seq, &ts) == OAKENGINE_OK);
|
||||||
|
assert(ts == 0);
|
||||||
|
assert(oakengine_sequence_set_playhead(seq, 30) == OAKENGINE_OK);
|
||||||
|
ts = -1;
|
||||||
|
assert(oakengine_sequence_get_playhead(seq, &ts) == OAKENGINE_OK);
|
||||||
|
assert(ts == 30);
|
||||||
|
// 30 frames at 1001/30000 per frame = 1.001 seconds.
|
||||||
|
seconds = -1.0;
|
||||||
|
assert(oakengine_sequence_get_playhead_seconds(seq, &seconds) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(fabs(seconds - 1.001) < 1e-9);
|
||||||
|
|
||||||
|
// Workarea: disabled by default, then set + read back.
|
||||||
|
assert(oakengine_sequence_workarea_is_enabled(seq) == 0);
|
||||||
|
assert(oakengine_sequence_set_workarea(seq, 1, 10, 40) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_workarea_is_enabled(seq) == 1);
|
||||||
|
int64_t in = -1, out = -1;
|
||||||
|
assert(oakengine_sequence_get_workarea(seq, &in, &out) == OAKENGINE_OK);
|
||||||
|
assert(in == 10 && out == 40);
|
||||||
|
|
||||||
|
// No markers on a fresh sequence.
|
||||||
|
assert(oakengine_sequence_marker_count(seq) == 0);
|
||||||
|
assert(oakengine_sequence_marker_at(seq, 0, NULL, NULL, 0) ==
|
||||||
|
OAKENGINE_E_NOT_FOUND);
|
||||||
|
|
||||||
|
// Save; the modified flag clears and the filename is adopted.
|
||||||
|
assert(oakengine_project_save(p, path) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_project_is_modified(p) == 0);
|
||||||
|
assert(file_exists(path));
|
||||||
|
char filename[4096];
|
||||||
|
assert(oakengine_project_filename(p, filename, sizeof(filename)) ==
|
||||||
|
(int)strlen(path));
|
||||||
|
assert(strcmp(filename, path) == 0);
|
||||||
|
|
||||||
|
// Undo removes the sequence, redo brings the same object back.
|
||||||
|
assert(oakengine_project_undo(p) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_project_sequence_count(p) == 0);
|
||||||
|
assert(oakengine_project_can_redo(p) == 1);
|
||||||
|
assert(oakengine_project_redo(p) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_project_sequence_count(p) == 1);
|
||||||
|
assert(oakengine_project_sequence_at(p, 0) == seq);
|
||||||
|
oakengine_project_free(p);
|
||||||
|
|
||||||
|
// Load the saved file into a fresh project.
|
||||||
|
OakEngineProject *q = oakengine_project_create();
|
||||||
|
assert(q != NULL);
|
||||||
|
char err[512];
|
||||||
|
assert(oakengine_project_load(q, path, err, sizeof(err)) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_project_is_modified(q) == 0);
|
||||||
|
assert(oakengine_project_can_undo(q) == 0); // load clears the undo stack
|
||||||
|
assert(oakengine_project_can_redo(q) == 0);
|
||||||
|
|
||||||
|
// Project name derives from the file's base name.
|
||||||
|
const int qname_len = oakengine_project_name(q, NULL, 0);
|
||||||
|
assert(qname_len == (int)strlen("roundtrip"));
|
||||||
|
assert(oakengine_project_name(q, name, sizeof(name)) == qname_len);
|
||||||
|
assert(strcmp(name, "roundtrip") == 0);
|
||||||
|
assert(oakengine_project_filename(q, filename, sizeof(filename)) ==
|
||||||
|
(int)strlen(path));
|
||||||
|
assert(strcmp(filename, path) == 0);
|
||||||
|
|
||||||
|
// The sequence survived the round trip, workarea included (the workarea
|
||||||
|
// is serialized by ViewerOutput::save_custom()).
|
||||||
|
assert(oakengine_project_sequence_count(q) == 1);
|
||||||
|
OakEngineSequence *loaded = oakengine_project_sequence_at(q, 0);
|
||||||
|
assert(loaded != NULL);
|
||||||
|
assert(oakengine_sequence_name(loaded, name, sizeof(name)) == 4);
|
||||||
|
assert(strcmp(name, "Main") == 0);
|
||||||
|
num = den = -1;
|
||||||
|
assert(oakengine_sequence_get_frame_rate(loaded, &num, &den) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(num == 30000 && den == 1001);
|
||||||
|
assert(oakengine_sequence_track_count(loaded, &video, &audio,
|
||||||
|
&subtitle) == OAKENGINE_OK);
|
||||||
|
assert(video == 0 && audio == 0 && subtitle == 0);
|
||||||
|
assert(oakengine_sequence_workarea_is_enabled(loaded) == 1);
|
||||||
|
in = out = -1;
|
||||||
|
assert(oakengine_sequence_get_workarea(loaded, &in, &out) == OAKENGINE_OK);
|
||||||
|
assert(in == 10 && out == 40);
|
||||||
|
assert(oakengine_project_footage_count(q) == 0);
|
||||||
|
|
||||||
|
// Saving with a NULL path reuses the project's current filename.
|
||||||
|
assert(oakengine_project_save(q, NULL) == OAKENGINE_OK);
|
||||||
|
assert(file_exists(path));
|
||||||
|
oakengine_project_free(q);
|
||||||
|
|
||||||
|
// Loading a missing file fails and reports a human-readable reason.
|
||||||
|
OakEngineProject *r = oakengine_project_create();
|
||||||
|
assert(r != NULL);
|
||||||
|
char missing[4096];
|
||||||
|
make_path(missing, sizeof(missing), "does-not-exist.ove");
|
||||||
|
assert(oakengine_project_load(r, missing, err, sizeof(err)) ==
|
||||||
|
OAKENGINE_E_FAILED);
|
||||||
|
assert(strlen(err) > 0);
|
||||||
|
// Loading into a project that already has content is rejected.
|
||||||
|
assert(oakengine_project_new(r) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_project_load(r, missing, err, sizeof(err)) ==
|
||||||
|
OAKENGINE_E_STATE);
|
||||||
|
oakengine_project_free(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tests/project_with_footage.ove is a small uncompressed project holding one
|
||||||
|
// footage item whose stored filename is the relative path "demo.mp4" (the
|
||||||
|
// real media file sits next to the fixture in tests/).
|
||||||
|
static void test_footage_fixture(void)
|
||||||
|
{
|
||||||
|
char fixture[4096];
|
||||||
|
const int n = snprintf(fixture, sizeof(fixture),
|
||||||
|
"%s/tests/project_with_footage.ove",
|
||||||
|
OAK_TEST_SOURCE_DIR);
|
||||||
|
assert(n > 0 && (size_t)n < sizeof(fixture));
|
||||||
|
assert(file_exists(fixture));
|
||||||
|
|
||||||
|
OakEngineProject *p = oakengine_project_create();
|
||||||
|
assert(p != NULL);
|
||||||
|
char err[512];
|
||||||
|
assert(oakengine_project_load(p, fixture, err, sizeof(err)) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
|
||||||
|
assert(oakengine_project_footage_count(p) == 1);
|
||||||
|
char filename[256];
|
||||||
|
assert(oakengine_project_footage_filename(p, 0, filename,
|
||||||
|
sizeof(filename)) > 0);
|
||||||
|
assert(strcmp(filename, "demo.mp4") == 0);
|
||||||
|
// The stored relative path resolves against the project file's
|
||||||
|
// directory (tests/), where demo.mp4 exists.
|
||||||
|
assert(oakengine_project_footage_is_online(p, 0) == 1);
|
||||||
|
// Out-of-range footage indexes report OAKENGINE_E_NOT_FOUND.
|
||||||
|
assert(oakengine_project_footage_filename(p, 1, filename,
|
||||||
|
sizeof(filename)) ==
|
||||||
|
OAKENGINE_E_NOT_FOUND);
|
||||||
|
assert(oakengine_project_footage_is_online(p, -1) ==
|
||||||
|
OAKENGINE_E_NOT_FOUND);
|
||||||
|
|
||||||
|
assert(oakengine_project_sequence_count(p) == 1);
|
||||||
|
OakEngineSequence *seq = oakengine_project_sequence_at(p, 0);
|
||||||
|
assert(seq != NULL);
|
||||||
|
char name[64];
|
||||||
|
assert(oakengine_sequence_name(seq, name, sizeof(name)) > 0);
|
||||||
|
assert(strcmp(name, "Fixture Sequence") == 0);
|
||||||
|
|
||||||
|
oakengine_project_free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_null_safety(void)
|
||||||
|
{
|
||||||
|
oakengine_project_free(NULL);
|
||||||
|
assert(oakengine_project_new(NULL) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_load(NULL, "/tmp/x.ove", NULL, 0) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_save(NULL, "/tmp/x.ove") == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_is_modified(NULL) == 0);
|
||||||
|
assert(oakengine_project_set_modified(NULL, 1) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_name(NULL, NULL, 0) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_filename(NULL, NULL, 0) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_footage_count(NULL) == 0);
|
||||||
|
assert(oakengine_project_footage_filename(NULL, 0, NULL, 0) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_footage_is_online(NULL, 0) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_can_undo(NULL) == 0);
|
||||||
|
assert(oakengine_project_can_redo(NULL) == 0);
|
||||||
|
assert(oakengine_project_undo(NULL) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_redo(NULL) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_sequence_count(NULL) == 0);
|
||||||
|
assert(oakengine_project_sequence_at(NULL, 0) == NULL);
|
||||||
|
|
||||||
|
assert(oakengine_sequence_new(NULL, "x") == NULL);
|
||||||
|
assert(oakengine_sequence_name(NULL, NULL, 0) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_get_length(NULL, NULL) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_get_length_rational(NULL, NULL, NULL) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_get_frame_rate(NULL, NULL, NULL) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_track_count(NULL, NULL, NULL, NULL) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_get_playhead(NULL, NULL) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_set_playhead(NULL, 0) == OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_get_playhead_seconds(NULL, NULL) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_workarea_is_enabled(NULL) == 0);
|
||||||
|
assert(oakengine_sequence_get_workarea(NULL, NULL, NULL) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_set_workarea(NULL, 0, 0, 0) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_marker_count(NULL) == 0);
|
||||||
|
assert(oakengine_sequence_marker_at(NULL, 0, NULL, NULL, 0) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_shutdown_pairing(void)
|
||||||
|
{
|
||||||
|
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||||
|
assert(oakengine_init_flags() == 0);
|
||||||
|
|
||||||
|
// Shutdown is idempotent.
|
||||||
|
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||||
|
|
||||||
|
// init may run again after a paired shutdown.
|
||||||
|
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_init_flags() == OAKENGINE_INIT_HEADLESS);
|
||||||
|
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||||
|
assert(oakengine_init_flags() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
make_tmpdir();
|
||||||
|
|
||||||
|
// Sandbox the config/cache/data locations so the engine never reads or
|
||||||
|
// writes the real user configuration. The engine's config path comes
|
||||||
|
// from QStandardPaths::AppDataLocation
|
||||||
|
// (FileFunctions::get_configuration_location()), which follows
|
||||||
|
// XDG_DATA_HOME on Linux; the other two are sandboxed for good measure.
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
assert(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0);
|
||||||
|
assert(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0);
|
||||||
|
assert(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
test_init();
|
||||||
|
test_project_basics();
|
||||||
|
test_sequence_and_save_load();
|
||||||
|
test_footage_fixture();
|
||||||
|
test_null_safety();
|
||||||
|
test_shutdown_pairing();
|
||||||
|
|
||||||
|
printf("oakengine_init_test: all assertions passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<olive version="230220" url="/home/mikesolar/Projects/oak/tests/project_with_footage.ove">
|
||||||
|
<project>
|
||||||
|
<project version="1">
|
||||||
|
<uuid>{b9147285-72ca-4b6b-b980-ce1dfbec408f}</uuid>
|
||||||
|
<nodes>
|
||||||
|
<node version="1" id="org.olivevideoeditor.Olive.folder" ptr="94695150158256">
|
||||||
|
<label>Root</label>
|
||||||
|
<input id="enabled_in">
|
||||||
|
<primary>
|
||||||
|
<keyframing>0</keyframing>
|
||||||
|
<standard>
|
||||||
|
<track>true</track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="child_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
<subelements count="2">
|
||||||
|
<element>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</element>
|
||||||
|
</subelements>
|
||||||
|
</input>
|
||||||
|
<connections>
|
||||||
|
<connection input="child_in" element="0">
|
||||||
|
<output>94695150298656</output>
|
||||||
|
</connection>
|
||||||
|
<connection input="child_in" element="1">
|
||||||
|
<output>94695150363552</output>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
<caches>
|
||||||
|
<audio>{725969c5-6732-4d64-9fc0-0cbf24c45dff}</audio>
|
||||||
|
<video>{819cf072-671a-413b-a5c2-ec8c254ceec6}</video>
|
||||||
|
<thumb>{3f699391-032f-4b27-9ab9-297faa2781ba}</thumb>
|
||||||
|
<waveform>{a11aa028-d950-460f-a7a6-d71df445ce68}</waveform>
|
||||||
|
</caches>
|
||||||
|
<custom/>
|
||||||
|
</node>
|
||||||
|
<node version="1" id="org.olivevideoeditor.Olive.footage" ptr="94695150298656">
|
||||||
|
<label>demo.mp4</label>
|
||||||
|
<input id="file_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track>demo.mp4</track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="enabled_in">
|
||||||
|
<primary>
|
||||||
|
<keyframing>0</keyframing>
|
||||||
|
<standard>
|
||||||
|
<track>true</track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="video_param_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
<depth>0</depth>
|
||||||
|
<timebase>0/1</timebase>
|
||||||
|
<format>-1</format>
|
||||||
|
<channelcount>0</channelcount>
|
||||||
|
<pixelaspectratio>1/1</pixelaspectratio>
|
||||||
|
<interlacing>0</interlacing>
|
||||||
|
<divider>1</divider>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<streamindex>0</streamindex>
|
||||||
|
<videotype>0</videotype>
|
||||||
|
<framerate>0/1</framerate>
|
||||||
|
<starttime>0</starttime>
|
||||||
|
<duration>0</duration>
|
||||||
|
<premultipliedalpha>0</premultipliedalpha>
|
||||||
|
<colorspace></colorspace>
|
||||||
|
<colorrange>0</colorrange>
|
||||||
|
<colorprimaries>0</colorprimaries>
|
||||||
|
<colortransfer>0</colortransfer>
|
||||||
|
</track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="audio_param_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track>
|
||||||
|
<samplerate>0</samplerate>
|
||||||
|
<channellayout>0</channellayout>
|
||||||
|
<format></format>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<streamindex>0</streamindex>
|
||||||
|
<duration>0</duration>
|
||||||
|
<timebase>0/1</timebase>
|
||||||
|
</track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="subtitle_param_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<caches>
|
||||||
|
<audio>{03070a9c-e9bd-4a6e-8532-7d502d8fea5e}</audio>
|
||||||
|
<video>{680e8c34-0d38-40c3-87b9-810db7df6ff0}</video>
|
||||||
|
<thumb>{d7346615-f439-4910-9c0b-c9b94df27883}</thumb>
|
||||||
|
<waveform>{7d154ff3-1794-451c-9495-528c5d5c4576}</waveform>
|
||||||
|
</caches>
|
||||||
|
<custom>
|
||||||
|
<timestamp>0</timestamp>
|
||||||
|
<viewer>
|
||||||
|
<workarea version="1">
|
||||||
|
<enabled>0</enabled>
|
||||||
|
<in>0/1</in>
|
||||||
|
<out>0/1</out>
|
||||||
|
</workarea>
|
||||||
|
<markers/>
|
||||||
|
</viewer>
|
||||||
|
</custom>
|
||||||
|
</node>
|
||||||
|
<node version="1" id="org.olivevideoeditor.Olive.sequence" ptr="94695150363552">
|
||||||
|
<label>Fixture Sequence</label>
|
||||||
|
<input id="enabled_in">
|
||||||
|
<primary>
|
||||||
|
<keyframing>0</keyframing>
|
||||||
|
<standard>
|
||||||
|
<track>true</track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="video_param_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
<depth>0</depth>
|
||||||
|
<timebase>0/1</timebase>
|
||||||
|
<format>-1</format>
|
||||||
|
<channelcount>0</channelcount>
|
||||||
|
<pixelaspectratio>1/1</pixelaspectratio>
|
||||||
|
<interlacing>0</interlacing>
|
||||||
|
<divider>1</divider>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<streamindex>0</streamindex>
|
||||||
|
<videotype>0</videotype>
|
||||||
|
<framerate>0/1</framerate>
|
||||||
|
<starttime>0</starttime>
|
||||||
|
<duration>0</duration>
|
||||||
|
<premultipliedalpha>0</premultipliedalpha>
|
||||||
|
<colorspace></colorspace>
|
||||||
|
<colorrange>0</colorrange>
|
||||||
|
<colorprimaries>0</colorprimaries>
|
||||||
|
<colortransfer>0</colortransfer>
|
||||||
|
</track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
<subelements count="1">
|
||||||
|
<element>
|
||||||
|
<standard>
|
||||||
|
<track>
|
||||||
|
<width>1920</width>
|
||||||
|
<height>1080</height>
|
||||||
|
<depth>1</depth>
|
||||||
|
<timebase>1/10</timebase>
|
||||||
|
<format>3</format>
|
||||||
|
<channelcount>4</channelcount>
|
||||||
|
<pixelaspectratio>1/1</pixelaspectratio>
|
||||||
|
<interlacing>0</interlacing>
|
||||||
|
<divider>1</divider>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<streamindex>0</streamindex>
|
||||||
|
<videotype>0</videotype>
|
||||||
|
<framerate>10/1</framerate>
|
||||||
|
<starttime>0</starttime>
|
||||||
|
<duration>0</duration>
|
||||||
|
<premultipliedalpha>0</premultipliedalpha>
|
||||||
|
<colorspace></colorspace>
|
||||||
|
<colorrange>0</colorrange>
|
||||||
|
<colorprimaries>0</colorprimaries>
|
||||||
|
<colortransfer>0</colortransfer>
|
||||||
|
</track>
|
||||||
|
</standard>
|
||||||
|
</element>
|
||||||
|
</subelements>
|
||||||
|
</input>
|
||||||
|
<input id="audio_param_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track>
|
||||||
|
<samplerate>0</samplerate>
|
||||||
|
<channellayout>0</channellayout>
|
||||||
|
<format></format>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<streamindex>0</streamindex>
|
||||||
|
<duration>0</duration>
|
||||||
|
<timebase>0/1</timebase>
|
||||||
|
</track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
<subelements count="1">
|
||||||
|
<element>
|
||||||
|
<standard>
|
||||||
|
<track>
|
||||||
|
<samplerate>48000</samplerate>
|
||||||
|
<channellayout>3</channellayout>
|
||||||
|
<format>f32p</format>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<streamindex>0</streamindex>
|
||||||
|
<duration>0</duration>
|
||||||
|
<timebase>1/48000</timebase>
|
||||||
|
</track>
|
||||||
|
</standard>
|
||||||
|
</element>
|
||||||
|
</subelements>
|
||||||
|
</input>
|
||||||
|
<input id="subtitle_param_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="tex_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="samples_in">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="track_in_0">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="track_in_1">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<input id="track_in_2">
|
||||||
|
<primary>
|
||||||
|
<standard>
|
||||||
|
<track></track>
|
||||||
|
</standard>
|
||||||
|
</primary>
|
||||||
|
</input>
|
||||||
|
<caches>
|
||||||
|
<audio>{c312cebb-c206-4be3-8685-44b1cefd2000}</audio>
|
||||||
|
<video>{6f929c83-c178-43de-8427-7f2aea486c96}</video>
|
||||||
|
<thumb>{edd6c747-f4a8-4ecd-869c-e2d522ea648f}</thumb>
|
||||||
|
<waveform>{654584d4-b0c7-40d7-9c12-95971f981e69}</waveform>
|
||||||
|
</caches>
|
||||||
|
<custom>
|
||||||
|
<workarea version="1">
|
||||||
|
<enabled>0</enabled>
|
||||||
|
<in>0/1</in>
|
||||||
|
<out>0/1</out>
|
||||||
|
</workarea>
|
||||||
|
<markers/>
|
||||||
|
</custom>
|
||||||
|
</node>
|
||||||
|
</nodes>
|
||||||
|
<settings>
|
||||||
|
<colorreferencespace>scene_linear</colorreferencespace>
|
||||||
|
<defaultinputcolorspace>Rec.709 OETF</defaultinputcolorspace>
|
||||||
|
<root>94695150158256</root>
|
||||||
|
</settings>
|
||||||
|
</project>
|
||||||
|
<layout version="1">
|
||||||
|
<folders/>
|
||||||
|
<timeline/>
|
||||||
|
<viewers/>
|
||||||
|
<data/>
|
||||||
|
<state></state>
|
||||||
|
</layout>
|
||||||
|
</project>
|
||||||
|
</olive>
|
||||||
Reference in New Issue
Block a user